source/rig/AlignPointCloud.cpp (331 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 "source/rig/AlignPointCloud.h"
#include <boost/algorithm/string/split.hpp>
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <folly/FileUtil.h>
#include <folly/Format.h>
#include <folly/dynamic.h>
#include <folly/json.h>
#include "source/calibration/Calibration.h"
#include "source/calibration/FeatureDetector.h"
#include "source/calibration/FeatureMatcher.h"
#include "source/calibration/MatchCorners.h"
#include "source/conversion/PointCloudUtil.h"
#include "source/util/ImageUtil.h"
#include "source/util/SystemUtil.h"
using namespace fb360_dep;
using namespace fb360_dep::calibration;
using namespace fb360_dep::point_cloud_util;
using Image = cv::Mat_<uint8_t>;
const std::string kUsageMessage = R"(
- Aligns point cloud to camera rig. The transformation includes translation, rotation and scaling.
- Example:
./AlignPointCloud \
--color=/path/to/background/color \
--point_cloud=/path/to/lidar/points.pts \
--rig_in=/path/to/rigs/rig.json \
--rig_out=/path/to/rigs/rig_aligned.json
)";
DEFINE_string(cameras, "", "subset of cameras to use for aligment (comma-separated)");
DEFINE_string(debug_dir, "", "path to debug output");
DEFINE_double(lidar_match_score, 0.85, "minimum score for an accepted lidar match");
DEFINE_bool(lock_rotation, false, "don't rotate the rig");
DEFINE_bool(lock_scale, false, "don't scale the rig");
DEFINE_bool(lock_translation, false, "don't translate the rig");
DEFINE_double(outlier_factor, 5, "reject if error is factor * median");
DEFINE_string(point_cloud, "", "path to the point cloud file (required)");
using FeatureList = std::vector<Match3D>;
void saveDisparityImages(
const Camera::Rig& rig,
const std::vector<PointCloudProjection>& projectedPointClouds,
const filesystem::path& outputDir) {
filesystem::create_directories(outputDir);
for (ssize_t i = 0; i < ssize(rig); ++i) {
const std::string imageFilename = folly::sformat("{}/{}.tif", outputDir.string(), rig[i].id);
cv_util::imwriteExceptionOnFail(imageFilename, projectedPointClouds[i].disparityImage);
}
}
void saveDebugImages(
const Camera::Rig& rig,
const std::vector<PointCloudProjection>& projectedPointClouds,
const filesystem::path& outputDir) {
filesystem::create_directories(outputDir);
for (ssize_t i = 0; i < ssize(rig); ++i) {
const std::string imageFilename = folly::sformat("{}/{}.tif", outputDir.string(), rig[i].id);
cv_util::imwriteExceptionOnFail(imageFilename, projectedPointClouds[i].image);
}
}
FeatureList createFeatureList(
const std::vector<Keypoint>& imageCorners,
const std::vector<Keypoint>& lidarCorners,
const Overlap& overlap,
const std::string& camId,
const cv::Mat_<cv::Point3f>& coordinateImage) {
FeatureList features;
for (const Match& match : overlap.matches) {
if (match.score >= FLAGS_lidar_match_score) {
Match3D match3D;
match3D.coords = imageCorners[match.corners[0]].coords;
match3D.lidarCoords = lidarCorners[match.corners[1]].coords;
const cv::Point3f point = coordinateImage(match3D.lidarCoords.y(), match3D.lidarCoords.x());
match3D.point.x() = point.x;
match3D.point.y() = point.y;
match3D.point.z() = point.z;
match3D.score = match.score;
// Only save matches that coorrespond to non-empty points
if (point.x != 0 || point.y != 0 || point.z != 0) {
features.emplace_back(match3D);
}
}
}
return features;
}
void saveLidarMatches(
const Camera::Rig& rig,
std::vector<FeatureList> allFeatures,
const filesystem::path& outputDir) {
filesystem::create_directories(outputDir);
for (ssize_t i = 0; i < ssize(rig); ++i) {
folly::dynamic allMatches = folly::dynamic::array;
for (const Match3D& feature : allFeatures[i]) {
folly::dynamic matchData = folly::dynamic::object;
folly::dynamic featureData =
folly::dynamic::object("x", feature.coords.x())("y", feature.coords.y());
matchData["coords"] = featureData;
folly::dynamic lidarData =
folly::dynamic::object("x", feature.lidarCoords.x())("y", feature.lidarCoords.y());
matchData["lidar_coords"] = lidarData;
folly::dynamic pointData = folly::dynamic::object("x", feature.point.x())(
"y", feature.point.y())("z", feature.point.z());
matchData["point"] = pointData;
matchData["score"] = feature.score;
allMatches.push_back(matchData);
}
std::string filename = folly::sformat("{}/{}.json", outputDir.string(), rig[i].id);
LOG(INFO) << folly::sformat("Saving matches to file: {}", filename);
CHECK(folly::writeFile(folly::toPrettyJson(allMatches), filename.c_str()));
}
}
void renderReprojections(
const Camera::Rig& rig,
std::vector<FeatureList> allFeatures,
const filesystem::path& outputDir) {
filesystem::create_directories(outputDir);
const std::vector<cv::Mat_<cv::Vec3w>> images =
image_util::loadImages<cv::Vec3w>(FLAGS_color, rig, FLAGS_frame, FLAGS_threads);
const cv::Scalar green(0, 1, 0);
const cv::Scalar red(0, 0, 1);
for (int i = 0; i < int(rig.size()); ++i) {
for (const Match3D& feature : allFeatures[i]) {
// draw red line from image feature to reprojected world point
// then continue in green in the same direction but scale x as far
Camera::Vector2 proj = rig[i].pixel(feature.point);
cv::line(
images[i],
cv::Point2f(proj.x(), proj.y()),
cv::Point2f(feature.coords.x(), feature.coords.y()),
green,
2);
}
std::string errorsFile = folly::sformat("{}/{}.png", outputDir.string(), rig[i].id);
cv_util::imwriteExceptionOnFail(errorsFile, images[i]);
}
}
std::vector<FeatureList> generateFeatures(const Camera::Rig& rig, const PointCloud& pointCloud) {
LOG(INFO) << "Loading images";
const std::vector<Image> images = loadChannels(rig);
const std::vector<PointCloudProjection>& projectedPointClouds =
generateProjectedImages(pointCloud, rig);
if (FLAGS_debug_dir != "") {
const filesystem::path initialProjectionDir =
filesystem::path(FLAGS_debug_dir) / "initial_projections";
saveDebugImages(rig, projectedPointClouds, initialProjectionDir);
const filesystem::path initialDisparityDir =
filesystem::path(FLAGS_debug_dir) / "initial_disparities";
saveDisparityImages(rig, projectedPointClouds, initialDisparityDir);
}
// For every camera
std::vector<FeatureList> allFeatures;
for (ssize_t i = 0; i < ssize(rig); ++i) {
bool useNearest = false; // enable bilinear interpolation on the camera image
std::vector<Keypoint> imageCorners = findCorners(rig[i], images[i], useNearest);
Camera lidarCamera = rig[i];
const Image& lidarImage = extractSingleChannelImage(projectedPointClouds[i].image);
lidarCamera.id = folly::sformat("{}_lidar", rig[i].id);
useNearest = true; // don't interpolate the lidar projection
std::vector<Keypoint> lidarCorners = findCorners(lidarCamera, lidarImage, useNearest);
Overlap overlap =
findMatches(images[i], imageCorners, rig[i], lidarImage, lidarCorners, lidarCamera);
LOG(INFO) << folly::sformat("Found {} matches", overlap.matches.size());
FeatureList camFeatures = createFeatureList(
imageCorners, lidarCorners, overlap, rig[i].id, projectedPointClouds[i].coordinateImage);
allFeatures.emplace_back(camFeatures);
}
return allFeatures;
}
void solve(ceres::Problem& problem) {
ceres::Solver::Options options;
options.use_inner_iterations = true;
options.max_num_iterations = 500;
options.minimizer_progress_to_stdout = false;
ceres::Solver::Summary summary;
Solve(options, &problem, &summary);
LOG(INFO) << summary.BriefReport();
}
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];
}
void logMedianErrors(const Camera::Rig& rig, const std::vector<FeatureList>& allFeatures) {
std::vector<std::vector<Camera::Real>> errors;
for (int i = 0; i < int(rig.size()); ++i) {
std::vector<Camera::Real> cameraErrors;
for (auto& feature : allFeatures[i]) {
Camera::Real residual = (rig[i].pixel(feature.point) - feature.coords).norm();
cameraErrors.push_back(residual);
}
errors.push_back(cameraErrors);
}
// compute median for each camera
std::vector<Camera::Real> medians(ssize(errors));
for (ssize_t i = 0; i < ssize(errors); ++i) {
medians[i] = calcPercentile(errors[i]);
LOG(INFO) << folly::sformat(
"{} median: {} 25%: {} 90%: {} 95%: {}",
rig[i].id,
calcPercentile(errors[i]),
calcPercentile(errors[i], 0.25),
calcPercentile(errors[i], 0.90),
calcPercentile(errors[i], 0.95));
}
}
std::vector<FeatureList> removeOutliers(
const Camera::Rig& rig,
const std::vector<FeatureList>& allFeatures) {
std::vector<FeatureList> inlyingFeatures;
for (ssize_t i = 0; i < ssize(rig); ++i) {
FeatureList cameraFeatures;
std::vector<Camera::Real> cameraErrors;
for (const Match3D& feature : allFeatures[i]) {
const Camera::Real residual = (rig[i].pixel(feature.point) - feature.coords).norm();
cameraErrors.push_back(residual);
}
const double median = calcPercentile(cameraErrors);
LOG(INFO) << folly::sformat("Median {} {}", rig[i].id, median);
for (ssize_t j = 0; j < ssize(cameraErrors); ++j) {
if (cameraErrors[j] < FLAGS_outlier_factor * median) {
cameraFeatures.push_back(allFeatures[i][j]);
}
}
LOG(INFO) << folly::sformat(
"{} median unfiltered: {} outlier threshold: {} unfiltered match count: {} accepted matches count: {}",
rig[i].id,
median,
FLAGS_outlier_factor * median,
cameraErrors.size(),
cameraFeatures.size());
inlyingFeatures.push_back(cameraFeatures);
}
return inlyingFeatures;
}
Camera::Rig alignPointCloud(
const Camera::Rig& rig,
const std::string& includeCamList,
const std::vector<FeatureList>& allFeatures,
bool lockRotation = false,
bool lockTranslation = false,
bool lockScale = false) {
ceres::Problem problem;
Camera::Vector3 rotation(0, 0, 0);
Camera::Vector3 translation(0, 0, 0);
Eigen::UniformScaling<double> scale(1);
const std::vector<FeatureList>& inlyingFeatures = removeOutliers(rig, allFeatures);
logMedianErrors(rig, inlyingFeatures);
std::vector<std::string> includeCams;
boost::split(includeCams, includeCamList, [](char c) { return c == ','; });
int alignmentCameras = 0;
for (int i = 0; i < int(rig.size()); ++i) {
if (!includeCamList.empty() &&
std::find(includeCams.begin(), includeCams.end(), rig[i].id) == includeCams.end()) {
LOG(INFO) << folly::sformat("Excluding camera {} from calibration ", rig[i].id);
continue;
}
alignmentCameras++;
for (const Match3D& feature : inlyingFeatures[i]) {
PointCloudFunctor::addResidual(problem, rotation, translation, scale, rig[i], feature);
}
}
if (alignmentCameras == 1) {
LOG(INFO) << "Single camera aligment detected. Locking rig scale to 1.";
lockScale = true;
}
problem.SetParameterLowerBound(&scale.factor(), 0, 0.25);
problem.SetParameterLowerBound(rotation.data(), 0, -M_PI);
problem.SetParameterLowerBound(rotation.data(), 1, -M_PI);
problem.SetParameterLowerBound(rotation.data(), 2, -M_PI / 2);
problem.SetParameterUpperBound(rotation.data(), 0, M_PI);
problem.SetParameterUpperBound(rotation.data(), 1, M_PI);
problem.SetParameterUpperBound(rotation.data(), 2, M_PI / 2);
if (lockRotation) {
problem.SetParameterBlockConstant(rotation.data());
}
if (lockTranslation) {
problem.SetParameterBlockConstant(translation.data());
}
if (lockScale) {
problem.SetParameterBlockConstant(&scale.factor());
}
solve(problem);
LOG(INFO) << folly::sformat(
"New rotation values: {} {} {}", rotation[0], rotation[1], rotation[2]);
LOG(INFO) << folly::sformat(
"New translation values: {} {} {}", translation[0], translation[1], translation[2]);
LOG(INFO) << folly::sformat("New scale: {}", scale.factor());
const Camera::Rig transformedRig = transformRig(rig, rotation, translation, scale);
logMedianErrors(transformedRig, inlyingFeatures);
return transformedRig;
}
int main(int argc, char* argv[]) {
system_util::initDep(argc, argv, kUsageMessage);
CHECK_NE(FLAGS_rig_in, "");
CHECK_NE(FLAGS_color, "");
CHECK_NE(FLAGS_point_cloud, "");
CHECK_NE(FLAGS_rig_out, "");
// Read in the rig and reference rig
LOG(INFO) << "Loading the cameras";
const Camera::Rig rig =
image_util::filterDestinations(Camera::loadRig(FLAGS_rig_in), FLAGS_cameras);
const int validFrame = image_util::getSingleFrame(FLAGS_color, rig, FLAGS_frame);
FLAGS_frame = image_util::intToStringZeroPad(validFrame);
LOG(INFO) << "Loading point cloud";
const PointCloud& pointCloud = extractPoints(FLAGS_point_cloud, FLAGS_threads);
std::vector<FeatureList> allFeatures = generateFeatures(rig, pointCloud);
if (FLAGS_debug_dir != "") {
const filesystem::path matchesDir = filesystem::path(FLAGS_debug_dir) / "matches";
saveLidarMatches(rig, allFeatures, matchesDir);
const filesystem::path initialReprojectionDir =
filesystem::path(FLAGS_debug_dir) / "initial_reprojections";
renderReprojections(rig, allFeatures, initialReprojectionDir);
}
const Camera::Rig& transformedRig = alignPointCloud(
rig,
FLAGS_cameras,
allFeatures,
FLAGS_lock_rotation,
FLAGS_lock_translation,
FLAGS_lock_scale);
if (FLAGS_debug_dir != "") {
const filesystem::path finalReprojectionDir =
filesystem::path(FLAGS_debug_dir) / "final_reprojections";
renderReprojections(transformedRig, allFeatures, finalReprojectionDir);
const filesystem::path finalProjectionDir =
filesystem::path(FLAGS_debug_dir) / "final_projections";
const std::vector<PointCloudProjection>& projectedPointClouds =
generateProjectedImages(pointCloud, transformedRig);
saveDebugImages(transformedRig, projectedPointClouds, finalProjectionDir);
const filesystem::path finalDisparityDir =
filesystem::path(FLAGS_debug_dir) / "final_disparities";
saveDisparityImages(rig, projectedPointClouds, finalDisparityDir);
}
Camera::saveRig(FLAGS_rig_out, transformedRig);
return 0;
}