source/calibration/GeometricCalibration.h (319 lines of code) (raw):
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <ceres/ceres.h>
#include "source/util/Camera.h"
namespace fb360_dep {
namespace calibration {
using ReprojectionErrorOutlier = std::pair<double, double>; // <original_error, weighted_error>
Camera makeCamera(
const Camera& camera,
const Camera::Vector3& position,
const Camera::Vector3& rotation,
const Camera::Vector2& principal,
const Camera::Real& focal,
const Camera::Distortion& distortion) {
Camera result = camera;
result.position = position;
result.setRotation(rotation);
result.principal = principal;
result.setScalarFocal(focal);
result.setDistortion(distortion);
return result;
}
void cartesianToSpherical(
Camera::Real& radius,
Camera::Real& theta,
Camera::Real& phi,
const Camera::Vector3& cartesianCoords) {
radius = cartesianCoords.norm();
theta = acos(cartesianCoords.z() / radius);
phi = atan(cartesianCoords.y() / cartesianCoords.x());
}
Camera::Vector3
sphericalToCartesian(const Camera::Real radius, const Camera::Real theta, const Camera::Real phi) {
Camera::Vector3 cartesianCoords;
cartesianCoords.x() = radius * sin(theta) * cos(phi);
cartesianCoords.y() = radius * sin(theta) * sin(phi);
cartesianCoords.z() = radius * cos(theta);
return cartesianCoords;
}
struct SphericalReprojectionFunctor {
static ceres::CostFunction* addResidual(
ceres::Problem& problem,
Camera::Real& theta,
Camera::Real& phi,
Camera::Vector3& rotation,
Camera::Vector2& principal,
Camera::Real& focal,
Camera::Distortion& distortion,
Camera::Vector3& world,
Camera::Real radius,
Camera::Vector3& referencePosition,
const Camera& camera,
const Camera::Vector2& pixel,
bool robust = false,
const int weight = 1) {
auto* cost = new CostFunction(
new SphericalReprojectionFunctor(camera, pixel, weight, radius, referencePosition));
auto* loss = robust ? new ceres::HuberLoss(1.0) : nullptr;
problem.AddResidualBlock(
cost,
loss,
&theta,
&phi,
rotation.data(),
principal.data(),
&focal,
distortion.data(),
world.data());
return cost;
}
bool operator()(
double const* const theta,
double const* const phi,
double const* const rotation,
double const* const principal,
double const* const focal,
double const* const distortion,
double const* const world,
double* residuals) const {
// create a camera using parameters
Camera::Vector3 position = sphericalToCartesian(radius, theta[0], phi[0]);
position += referencePosition;
Camera modified = makeCamera(
camera,
position,
Eigen::Map<const Camera::Vector3>(rotation),
Eigen::Map<const Camera::Vector2>(principal),
*focal,
Eigen::Map<const Camera::Distortion>(distortion));
// transform world with that camera and compare to pixel
Eigen::Map<const Camera::Vector3> w(world);
Eigen::Map<Camera::Vector2> r(residuals);
r = modified.pixel(w) - pixel;
r = r / sqrt(weight);
return true;
}
private:
using CostFunction = ceres::NumericDiffCostFunction<
SphericalReprojectionFunctor,
ceres::CENTRAL,
2, // residuals
1, // theta
1, // phi
3, // rotation
2, // principal
1, // focal
Camera::Distortion::SizeAtCompileTime, // distortion
3>; // world
SphericalReprojectionFunctor(
const Camera& camera,
const Camera::Vector2& pixel,
const int weight,
const double radius,
const Camera::Vector3& referencePosition)
: camera(camera),
pixel(pixel),
weight(weight),
radius(radius),
referencePosition(referencePosition) {}
const Camera& camera;
const Camera::Vector2 pixel;
const int weight;
const Camera::Real radius;
const Camera::Vector3& referencePosition;
};
struct ReprojectionFunctor {
static ceres::CostFunction* addResidual(
ceres::Problem& problem,
Camera::Vector3& position,
Camera::Vector3& rotation,
Camera::Vector2& principal,
Camera::Real& focal,
Camera::Distortion& distortion,
Camera::Vector3& world,
const Camera& camera,
const Camera::Vector2& pixel,
bool robust = false,
const int weight = 1) {
auto* cost = new CostFunction(new ReprojectionFunctor(camera, pixel, weight));
auto* loss = robust ? new ceres::HuberLoss(1.0) : nullptr;
problem.AddResidualBlock(
cost,
loss,
position.data(),
rotation.data(),
principal.data(),
&focal,
distortion.data(),
world.data());
return cost;
}
bool operator()(
double const* const position,
double const* const rotation,
double const* const principal,
double const* const focal,
double const* const distortion,
double const* const world,
double* residuals) const {
// create a camera using parameters
Camera modified = makeCamera(
camera,
Eigen::Map<const Camera::Vector3>(position),
Eigen::Map<const Camera::Vector3>(rotation),
Eigen::Map<const Camera::Vector2>(principal),
*focal,
Eigen::Map<const Camera::Distortion>(distortion));
// transform world with that camera and compare to pixel
Eigen::Map<const Camera::Vector3> w(world);
Eigen::Map<Camera::Vector2> r(residuals);
r = modified.pixel(w) - pixel;
r = r / sqrt(weight);
return true;
}
private:
using CostFunction = ceres::NumericDiffCostFunction<
ReprojectionFunctor,
ceres::CENTRAL,
2, // residuals
3, // position
3, // rotation
2, // principal
1, // focal
Camera::Distortion::SizeAtCompileTime, // distortion
3>; // world
ReprojectionFunctor(const Camera& camera, const Camera::Vector2& pixel, const int weight)
: camera(camera), pixel(pixel), weight(weight) {}
const Camera& camera;
const Camera::Vector2 pixel;
const int weight;
};
// The problem with using a world coordinate as the variable when triangulating is that the solver
// might overshoot and end up behind you. This happens a lot if the initial estimate is e.g. 1000 m
// away: The solver realizes it is way too far and follows the gradient back close to zero. Very
// often it will blow past zero and end up behind you
// The trick to fix this is to use inv = world / |world|^2 as the variable instead. Using
// disparity = 1 / |world|,
// it can be seen that
// inv = disparty * unit(world),
// so this accomplishes two things:
// - The variable is proportional to disparity. We care about pixels, not meters
// - To end up behind you, the solver needs to cross through infinity (hard) instead of zero (easy)
struct TriangulationFunctor {
static ceres::CostFunction* addResidual(
ceres::Problem& problem,
Camera::Vector3& inv,
const Camera& camera,
const Camera::Vector2& pixel,
const bool robust = false) {
auto* cost = new CostFunction(new TriangulationFunctor(camera, pixel));
auto* loss = robust ? new ceres::HuberLoss(1.0) : nullptr;
problem.AddResidualBlock(cost, loss, inv.data());
return cost;
}
bool operator()(double const* const inv, double* residuals) const {
Eigen::Map<const Camera::Vector3> i(inv);
Eigen::Map<Camera::Vector2> r(residuals);
Camera::Vector3 w = i / i.squaredNorm();
// transform world with camera and compare to pixel
r = camera.pixel(w) - pixel;
return true;
}
private:
using CostFunction = ceres::NumericDiffCostFunction<
TriangulationFunctor,
ceres::CENTRAL,
2, // residuals
3>; // inverse world
TriangulationFunctor(const Camera& camera, const Camera::Vector2& pixel)
: camera(camera), pixel(pixel) {}
const Camera& camera;
const Camera::Vector2 pixel;
};
using Observations = std::vector<std::pair<const Camera&, Camera::Vector2>>;
Camera::Vector3 averageAtDistance(const Observations& observations, const Camera::Real distance) {
Camera::Vector3 sum(0, 0, 0);
for (const auto& obs : observations) {
sum += obs.first.rig(obs.second, distance);
}
return sum / observations.size();
}
Camera::Vector3 triangulateNonlinear(const Observations& observations, const bool forceInFront) {
CHECK_GE(observations.size(), 2);
ceres::Solver::Options options;
options.max_num_iterations = 10;
// initial value is average of distant points
const Camera::Real kInitialDistance = 10; // 10 meters, not hugely important
Camera::Vector3 world = averageAtDistance(observations, kInitialDistance);
Camera::Vector3 inv = world / world.squaredNorm();
ceres::Problem problem;
for (const auto& obs : observations) {
TriangulationFunctor::addResidual(problem, inv, obs.first, obs.second);
}
ceres::Solver::Summary summary;
ceres::Solve(options, &problem, &summary);
world = inv / inv.squaredNorm();
if (forceInFront) {
for (const auto& obs : observations) {
if (obs.first.isBehind(world)) {
return averageAtDistance(observations, Camera::kNearInfinity);
}
}
}
return world;
}
double calcPercentile(std::vector<double> values, double percentile = 0.5) {
if (values.empty()) {
return NAN;
}
CHECK_LT(percentile, 1);
size_t index(percentile * values.size());
std::nth_element(values.begin(), values.begin() + index, values.end());
return values[index];
}
Camera::Vector2 reprojectionError(const ceres::Problem& problem, ceres::ResidualBlockId id) {
auto cost = problem.GetCostFunctionForResidualBlock(id);
std::vector<double*> parameterBlocks;
problem.GetParameterBlocksForResidualBlock(id, ¶meterBlocks);
Camera::Vector2 residual;
cost->Evaluate(parameterBlocks.data(), residual.data(), nullptr);
return residual;
}
bool reprojectionErrorOutlier(
const ceres::Problem& problem,
ceres::ResidualBlockId id,
double& originalError,
double& weightedError) {
auto loss = problem.GetLossFunctionForResidualBlock(id);
originalError = reprojectionError(problem, id).norm();
double lossOutput[3];
loss->Evaluate(originalError, lossOutput);
weightedError = lossOutput[0];
return lossOutput[1] < 1 || lossOutput[2] < 0;
}
std::vector<double> getReprojectionErrorNorms(
const ceres::Problem& problem,
const double* parameter = nullptr,
bool weighted = false) {
std::vector<double> result;
std::vector<ceres::ResidualBlockId> ids;
if (parameter) {
problem.GetResidualBlocksForParameterBlock(parameter, &ids);
} else {
problem.GetResidualBlocks(&ids);
}
for (auto& id : ids) {
double errorNorm = reprojectionError(problem, id).norm();
if (weighted) {
double weightedErrorNorm;
reprojectionErrorOutlier(problem, id, errorNorm, weightedErrorNorm);
result.push_back(weightedErrorNorm);
} else {
result.push_back(errorNorm);
}
}
return result;
}
std::vector<ReprojectionErrorOutlier> getReprojectionErrorOutliers(
const ceres::Problem& problem,
const double* parameter = nullptr) {
std::vector<ReprojectionErrorOutlier> result;
std::vector<ceres::ResidualBlockId> ids;
if (parameter) {
problem.GetResidualBlocksForParameterBlock(parameter, &ids);
} else {
problem.GetResidualBlocks(&ids);
}
for (auto& id : ids) {
double originalError;
double weightedError;
if (reprojectionErrorOutlier(problem, id, originalError, weightedError)) {
result.push_back(std::make_pair(originalError, weightedError));
}
}
return result;
}
} // namespace calibration
} // namespace fb360_dep