camenduru's picture
ceres-solver and colmap
7b7496d
// Copyright (c) 2022, ETH Zurich and UNC Chapel Hill.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
// its contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)
#include "exe/model.h"
#include "base/gps.h"
#include "base/pose.h"
#include "base/similarity_transform.h"
#include "estimators/coordinate_frame.h"
#include "util/misc.h"
#include "util/option_manager.h"
#include "util/threading.h"
namespace colmap {
namespace {
std::vector<std::pair<Eigen::Vector3d, Eigen::Vector3d>>
ComputeEqualPartsBounds(const Reconstruction& reconstruction,
const Eigen::Vector3i& split) {
std::vector<std::pair<Eigen::Vector3d, Eigen::Vector3d>> bounds;
const auto bbox = reconstruction.ComputeBoundingBox();
const Eigen::Vector3d extent = bbox.second - bbox.first;
const Eigen::Vector3d offset(extent(0) / split(0), extent(1) / split(1),
extent(2) / split(2));
for (int k = 0; k < split(2); ++k) {
for (int j = 0; j < split(1); ++j) {
for (int i = 0; i < split(0); ++i) {
Eigen::Vector3d min_bound(bbox.first(0) + i * offset(0),
bbox.first(1) + j * offset(1),
bbox.first(2) + k * offset(2));
bounds.emplace_back(min_bound, min_bound + offset);
}
}
}
return bounds;
}
Eigen::Vector3d TransformLatLonAltToModelCoords(
const SimilarityTransform3& tform, double lat, double lon, double alt) {
// Since this is intended for use in ENU aligned models we want to define the
// altitude along the ENU frame z axis and not the Earth's radius. Thus, we
// set the altitude to 0 when converting from LLA to ECEF and then we use the
// altitude at the end, after scaling, to set it as the z coordinate in the
// ENU frame.
Eigen::Vector3d xyz = GPSTransform(GPSTransform::WGS84)
.EllToXYZ({Eigen::Vector3d(lat, lon, 0.0)})[0];
tform.TransformPoint(&xyz);
xyz(2) = tform.Scale() * alt;
return xyz;
}
void WriteBoundingBox(const std::string& reconstruction_path,
const std::pair<Eigen::Vector3d, Eigen::Vector3d>& bounds,
const std::string& suffix = "") {
const Eigen::Vector3d extent = bounds.second - bounds.first;
// write axis-aligned bounding box
{
const std::string path =
JoinPaths(reconstruction_path, "bbox_aligned" + suffix + ".txt");
std::ofstream file(path, std::ios::trunc);
CHECK(file.is_open()) << path;
// Ensure that we don't loose any precision by storing in text.
file.precision(17);
file << bounds.first.transpose() << std::endl;
file << bounds.second.transpose() << std::endl;
}
// write oriented bounding box
{
const std::string path =
JoinPaths(reconstruction_path, "bbox_oriented" + suffix + ".txt");
std::ofstream file(path, std::ios::trunc);
CHECK(file.is_open()) << path;
// Ensure that we don't loose any precision by storing in text.
file.precision(17);
const Eigen::Vector3d center = (bounds.first + bounds.second) * 0.5;
file << center.transpose() << std::endl << std::endl;
file << "1 0 0\n0 1 0\n0 0 1" << std::endl << std::endl;
file << extent.transpose() << std::endl;
}
}
std::vector<Eigen::Vector3d> ConvertCameraLocations(
const bool ref_is_gps, const std::string& alignment_type,
const std::vector<Eigen::Vector3d>& ref_locations) {
if (ref_is_gps) {
const GPSTransform gps_transform(GPSTransform::WGS84);
if (alignment_type != "enu") {
std::cout << "\nConverting Alignment Coordinates from GPS (lat/lon/alt) "
"to ECEF.\n";
return gps_transform.EllToXYZ(ref_locations);
} else {
std::cout << "\nConverting Alignment Coordinates from GPS (lat/lon/alt) "
"to ENU.\n";
return gps_transform.EllToENU(ref_locations, ref_locations[0](0),
ref_locations[0](1));
}
} else {
std::cout << "\nCartesian Alignment Coordinates extracted (MUST NOT BE "
"GPS coords!).\n";
return ref_locations;
}
}
void ReadFileCameraLocations(const std::string& ref_images_path,
const bool ref_is_gps,
const std::string& alignment_type,
std::vector<std::string>* ref_image_names,
std::vector<Eigen::Vector3d>* ref_locations) {
for (const auto& line : ReadTextFileLines(ref_images_path)) {
std::stringstream line_parser(line);
std::string image_name;
Eigen::Vector3d camera_position;
line_parser >> image_name >> camera_position[0] >> camera_position[1] >>
camera_position[2];
ref_image_names->push_back(image_name);
ref_locations->push_back(camera_position);
}
*ref_locations =
ConvertCameraLocations(ref_is_gps, alignment_type, *ref_locations);
}
void ReadDatabaseCameraLocations(const std::string& database_path,
const bool ref_is_gps,
const std::string& alignment_type,
std::vector<std::string>* ref_image_names,
std::vector<Eigen::Vector3d>* ref_locations) {
Database database(database_path);
for (const auto& image : database.ReadAllImages()) {
if (image.HasTvecPrior()) {
ref_image_names->push_back(image.Name());
ref_locations->push_back(image.TvecPrior());
}
}
*ref_locations =
ConvertCameraLocations(ref_is_gps, alignment_type, *ref_locations);
}
void WriteComparisonErrorsCSV(const std::string& path,
const std::vector<double>& rotation_errors,
const std::vector<double>& translation_errors,
const std::vector<double>& proj_center_errors) {
CHECK_EQ(rotation_errors.size(), translation_errors.size());
CHECK_EQ(rotation_errors.size(), proj_center_errors.size());
std::ofstream file(path, std::ios::trunc);
CHECK(file.is_open()) << path;
file.precision(17);
file << "# Model comparison pose errors: one entry per common image"
<< std::endl;
file << "# <rotation error (deg)>, <translation error>, <proj center error>"
<< std::endl;
for (size_t i = 0; i < rotation_errors.size(); ++i) {
file << rotation_errors[i] << ", " << translation_errors[i] << ", "
<< proj_center_errors[i] << std::endl;
}
}
void PrintErrorStats(std::ostream& out, std::vector<double>& vals) {
const size_t len = vals.size();
if (len == 0) {
out << "Cannot extract error statistics from empty input" << std::endl;
return;
}
std::sort(vals.begin(), vals.end());
out << "Min: " << vals.front() << std::endl;
out << "Max: " << vals.back() << std::endl;
out << "Mean: " << Mean(vals) << std::endl;
out << "Median: " << Median(vals) << std::endl;
out << "P90: " << vals[size_t(0.9 * len)] << std::endl;
out << "P99: " << vals[size_t(0.99 * len)] << std::endl;
}
void PrintComparisonSummary(std::ostream& out,
std::vector<double>& rotation_errors,
std::vector<double>& translation_errors,
std::vector<double>& proj_center_errors) {
out << "# Image pose error summary" << std::endl;
out << std::endl << "Rotation angular errors (degrees)" << std::endl;
PrintErrorStats(out, rotation_errors);
out << std::endl << "Translation distance errors" << std::endl;
PrintErrorStats(out, translation_errors);
out << std::endl << "Projection center distance errors" << std::endl;
PrintErrorStats(out, proj_center_errors);
}
} // namespace
// Align given reconstruction with user provided cameras positions
// (can be used for geo-registration for instance).
// The cameras positions to be used for aligning the reconstruction
// model must be provided either by a txt file (with each line being: img_name x
// y z) or through a colmap database file containing a prior position for the
// registered images.
//
// Required Options:
// - input_path: path to initial reconstruction model
// - output_path: path to store the aligned reconstruction model
//
// Additional Options:
// - database_path: path to database file with prior positions for
// reconstruction images
// - ref_images_path: path to txt file with prior positions for reconstruction
// images (WARNING: provide only one of the above)
// - ref_is_gps: if true the prior positions are converted from GPS
// (lat/lon/alt) to ECEF or ENU
// - merge_image_and_ref_origins: if true the reconstuction will be shifted so
// that the first prior position is used for its camera position
// - transform_path: path to store the Sim3 transformation used for the
// alignment
// - alignment_type:
// > plane: align with reconstruction principal plane
// > ecef: align with ecef coords. (requires gps coords. or user provided
// ecef coords.)
// > enu: align with enu coords. (requires gps coords. or user provided enu
// coords.)
// > enu-plane: align to ecef and then to enu plane (requires gps
// coords. or user provided ecef coords.)
// > enu-plane-unscaled: same as above but do not apply the computed
// scale when aligning the reconstruction
// > custom: align to provided coords.
// - min_common_images: minimum number of images with prior positions to perform
// the estimate an alignment
// - estimate_scale: if true apply the computed scale when aligning the
// reconstruction
// - robust_alignment: if true use a ransac-based estimation for robust
// alignment
// - robust_alignment_max_error: ransac error to use if robust alignment is
// enabled
int RunModelAligner(int argc, char** argv) {
std::string input_path;
std::string output_path;
std::string database_path;
std::string ref_images_path;
bool ref_is_gps = true;
bool merge_origins = false;
std::string transform_path;
std::string alignment_type = "custom";
int min_common_images = 3;
bool robust_alignment = true;
bool estimate_scale = true;
RANSACOptions ransac_options;
OptionManager options;
options.AddRequiredOption("input_path", &input_path);
options.AddRequiredOption("output_path", &output_path);
options.AddDefaultOption("database_path", &database_path);
options.AddDefaultOption("ref_images_path", &ref_images_path);
options.AddDefaultOption("ref_is_gps", &ref_is_gps);
options.AddDefaultOption("merge_image_and_ref_origins", &merge_origins);
options.AddDefaultOption("transform_path", &transform_path);
options.AddDefaultOption(
"alignment_type", &alignment_type,
"{plane, ecef, enu, enu-plane, enu-plane-unscaled, custom}");
options.AddDefaultOption("min_common_images", &min_common_images);
options.AddDefaultOption("estimate_scale", &estimate_scale);
options.AddDefaultOption("robust_alignment", &robust_alignment);
options.AddDefaultOption("robust_alignment_max_error",
&ransac_options.max_error);
options.Parse(argc, argv);
StringToLower(&alignment_type);
const std::unordered_set<std::string> alignment_options{
"plane", "ecef", "enu", "enu-plane", "enu-plane-unscaled", "custom"};
if (alignment_options.count(alignment_type) == 0) {
std::cerr << "ERROR: Invalid `alignment_type` - supported values are "
"{'plane', 'ecef', 'enu', 'enu-plane', 'enu-plane-unscaled', "
"'custom'}"
<< std::endl;
return EXIT_FAILURE;
}
if (robust_alignment && ransac_options.max_error <= 0) {
std::cout << "ERROR: You must provide a maximum alignment error > 0"
<< std::endl;
return EXIT_FAILURE;
}
if (alignment_type != "plane" && database_path.empty() &&
ref_images_path.empty()) {
std::cerr << "ERROR: Location alignment requires either database or "
"location file path."
<< std::endl;
return EXIT_FAILURE;
}
std::vector<std::string> ref_image_names;
std::vector<Eigen::Vector3d> ref_locations;
if (!ref_images_path.empty() && database_path.empty()) {
ReadFileCameraLocations(ref_images_path, ref_is_gps, alignment_type,
&ref_image_names, &ref_locations);
} else if (!database_path.empty() && ref_images_path.empty()) {
ReadDatabaseCameraLocations(database_path, ref_is_gps, alignment_type,
&ref_image_names, &ref_locations);
} else if (alignment_type != "plane") {
std::cerr << "ERROR: Use location file or database, not both" << std::endl;
return EXIT_FAILURE;
}
if (alignment_type != "plane" &&
static_cast<int>(ref_locations.size()) < min_common_images) {
std::cout << "ERROR: Cannot align with insufficient reference locations."
<< std::endl;
return EXIT_FAILURE;
}
Reconstruction reconstruction;
reconstruction.Read(input_path);
SimilarityTransform3 tform;
bool alignment_success = true;
if (alignment_type == "plane") {
PrintHeading2("Aligning reconstruction to principal plane");
AlignToPrincipalPlane(&reconstruction, &tform);
} else {
PrintHeading2("Aligning reconstruction to " + alignment_type);
std::cout << StringPrintf(" => Using %d reference images",
ref_image_names.size())
<< std::endl;
if (estimate_scale) {
if (robust_alignment) {
alignment_success = reconstruction.AlignRobust(
ref_image_names, ref_locations, min_common_images, ransac_options,
&tform);
} else {
alignment_success = reconstruction.Align(ref_image_names, ref_locations,
min_common_images, &tform);
}
} else {
if (robust_alignment) {
alignment_success = reconstruction.AlignRobust<false>(
ref_image_names, ref_locations, min_common_images, ransac_options,
&tform);
} else {
alignment_success = reconstruction.Align<false>(
ref_image_names, ref_locations, min_common_images, &tform);
}
}
std::vector<double> errors;
errors.reserve(ref_image_names.size());
for (size_t i = 0; i < ref_image_names.size(); ++i) {
const Image* image = reconstruction.FindImageWithName(ref_image_names[i]);
if (image != nullptr) {
errors.push_back((image->ProjectionCenter() - ref_locations[i]).norm());
}
}
std::cout << StringPrintf("=> Alignment error: %f (mean), %f (median)",
Mean(errors), Median(errors))
<< std::endl;
if (alignment_success && StringStartsWith(alignment_type, "enu-plane")) {
PrintHeading2("Aligning ECEF aligned reconstruction to ENU plane");
AlignToENUPlane(&reconstruction, &tform,
alignment_type == "enu-plane-unscaled");
}
}
if (merge_origins) {
for (size_t i = 0; i < ref_image_names.size(); i++) {
const Image* first_image =
reconstruction.FindImageWithName(ref_image_names[i]);
if (first_image != nullptr) {
const Eigen::Vector3d& first_img_position = ref_locations[i];
const Eigen::Vector3d trans_align =
first_img_position - first_image->ProjectionCenter();
const SimilarityTransform3 origin_align(
1.0, ComposeIdentityQuaternion(), trans_align);
std::cout << "\n Aligning Reconstruction's origin with Ref origin : "
<< first_img_position.transpose() << "\n";
reconstruction.Transform(origin_align);
// Update the Sim3 transformation in case it is stored next
tform = SimilarityTransform3(tform.Scale(), tform.Rotation(),
tform.Translation() + trans_align);
break;
}
}
}
if (alignment_success) {
std::cout << "=> Alignment succeeded" << std::endl;
reconstruction.Write(output_path);
if (!transform_path.empty()) {
tform.Write(transform_path);
}
return EXIT_SUCCESS;
} else {
std::cout << "=> Alignment failed" << std::endl;
return EXIT_FAILURE;
}
}
int RunModelAnalyzer(int argc, char** argv) {
std::string path;
OptionManager options;
options.AddRequiredOption("path", &path);
options.Parse(argc, argv);
Reconstruction reconstruction;
reconstruction.Read(path);
std::cout << StringPrintf("Cameras: %d", reconstruction.NumCameras())
<< std::endl;
std::cout << StringPrintf("Images: %d", reconstruction.NumImages())
<< std::endl;
std::cout << StringPrintf("Registered images: %d",
reconstruction.NumRegImages())
<< std::endl;
std::cout << StringPrintf("Points: %d", reconstruction.NumPoints3D())
<< std::endl;
std::cout << StringPrintf("Observations: %d",
reconstruction.ComputeNumObservations())
<< std::endl;
std::cout << StringPrintf("Mean track length: %f",
reconstruction.ComputeMeanTrackLength())
<< std::endl;
std::cout << StringPrintf("Mean observations per image: %f",
reconstruction.ComputeMeanObservationsPerRegImage())
<< std::endl;
std::cout << StringPrintf("Mean reprojection error: %fpx",
reconstruction.ComputeMeanReprojectionError())
<< std::endl;
return EXIT_SUCCESS;
}
int RunModelComparer(int argc, char** argv) {
std::string input_path1;
std::string input_path2;
std::string output_path;
double min_inlier_observations = 0.3;
double max_reproj_error = 8.0;
OptionManager options;
options.AddRequiredOption("input_path1", &input_path1);
options.AddRequiredOption("input_path2", &input_path2);
options.AddDefaultOption("output_path", &output_path);
options.AddDefaultOption("min_inlier_observations", &min_inlier_observations);
options.AddDefaultOption("max_reproj_error", &max_reproj_error);
options.Parse(argc, argv);
if (!output_path.empty() && !ExistsDir(output_path)) {
std::cerr << "ERROR: Provided output path is not a valid directory"
<< std::endl;
return EXIT_FAILURE;
}
Reconstruction reconstruction1;
reconstruction1.Read(input_path1);
PrintHeading1("Reconstruction 1");
std::cout << StringPrintf("Images: %d", reconstruction1.NumRegImages())
<< std::endl;
std::cout << StringPrintf("Points: %d", reconstruction1.NumPoints3D())
<< std::endl;
Reconstruction reconstruction2;
reconstruction2.Read(input_path2);
PrintHeading1("Reconstruction 2");
std::cout << StringPrintf("Images: %d", reconstruction2.NumRegImages())
<< std::endl;
std::cout << StringPrintf("Points: %d", reconstruction2.NumPoints3D())
<< std::endl;
PrintHeading1("Comparing reconstructed image poses");
const auto common_image_ids =
reconstruction1.FindCommonRegImageIds(reconstruction2);
std::cout << StringPrintf("Common images: %d", common_image_ids.size())
<< std::endl;
Eigen::Matrix3x4d alignment;
if (!ComputeAlignmentBetweenReconstructions(reconstruction2, reconstruction1,
min_inlier_observations,
max_reproj_error, &alignment)) {
std::cout << "=> Reconstruction alignment failed" << std::endl;
return EXIT_FAILURE;
}
const SimilarityTransform3 tform(alignment);
std::cout << "Computed alignment transform:" << std::endl
<< tform.Matrix() << std::endl;
const size_t num_images = common_image_ids.size();
std::vector<double> rotation_errors(num_images, 0.0);
std::vector<double> translation_errors(num_images, 0.0);
std::vector<double> proj_center_errors(num_images, 0.0);
for (size_t i = 0; i < num_images; ++i) {
const image_t image_id = common_image_ids[i];
const Image& image1 = reconstruction1.Image(image_id);
Image& image2 = reconstruction2.Image(image_id);
tform.TransformPose(&image2.Qvec(), &image2.Tvec());
const Eigen::Vector4d normalized_qvec1 = NormalizeQuaternion(image1.Qvec());
const Eigen::Quaterniond quat1(normalized_qvec1(0), normalized_qvec1(1),
normalized_qvec1(2), normalized_qvec1(3));
const Eigen::Vector4d normalized_qvec2 = NormalizeQuaternion(image2.Qvec());
const Eigen::Quaterniond quat2(normalized_qvec2(0), normalized_qvec2(1),
normalized_qvec2(2), normalized_qvec2(3));
rotation_errors[i] = RadToDeg(quat1.angularDistance(quat2));
translation_errors[i] = (image1.Tvec() - image2.Tvec()).norm();
proj_center_errors[i] =
(image1.ProjectionCenter() - image2.ProjectionCenter()).norm();
}
if (output_path.empty()) {
PrintComparisonSummary(std::cout, rotation_errors, translation_errors,
proj_center_errors);
} else {
const std::string errors_path = JoinPaths(output_path, "errors.csv");
WriteComparisonErrorsCSV(errors_path, rotation_errors, translation_errors,
proj_center_errors);
const std::string summary_path =
JoinPaths(output_path, "errors_summary.txt");
std::ofstream file(summary_path, std::ios::trunc);
CHECK(file.is_open()) << summary_path;
PrintComparisonSummary(file, rotation_errors, translation_errors,
proj_center_errors);
}
return EXIT_SUCCESS;
}
int RunModelConverter(int argc, char** argv) {
std::string input_path;
std::string output_path;
std::string output_type;
bool skip_distortion = false;
OptionManager options;
options.AddRequiredOption("input_path", &input_path);
options.AddRequiredOption("output_path", &output_path);
options.AddRequiredOption("output_type", &output_type,
"{BIN, TXT, NVM, Bundler, VRML, PLY, R3D, CAM}");
options.AddDefaultOption("skip_distortion", &skip_distortion);
options.Parse(argc, argv);
Reconstruction reconstruction;
reconstruction.Read(input_path);
StringToLower(&output_type);
if (output_type == "bin") {
reconstruction.WriteBinary(output_path);
} else if (output_type == "txt") {
reconstruction.WriteText(output_path);
} else if (output_type == "nvm") {
reconstruction.ExportNVM(output_path, skip_distortion);
} else if (output_type == "bundler") {
reconstruction.ExportBundler(output_path + ".bundle.out",
output_path + ".list.txt", skip_distortion);
} else if (output_type == "r3d") {
reconstruction.ExportRecon3D(output_path, skip_distortion);
} else if (output_type == "cam") {
reconstruction.ExportCam(output_path, skip_distortion);
} else if (output_type == "ply") {
reconstruction.ExportPLY(output_path);
} else if (output_type == "vrml") {
const auto base_path = output_path.substr(0, output_path.find_last_of("."));
reconstruction.ExportVRML(base_path + ".images.wrl",
base_path + ".points3D.wrl", 1,
Eigen::Vector3d(1, 0, 0));
} else {
std::cerr << "ERROR: Invalid `output_type`" << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
int RunModelCropper(int argc, char** argv) {
Timer timer;
timer.Start();
std::string input_path;
std::string output_path;
std::string boundary;
std::string gps_transform_path;
bool is_gps = false;
OptionManager options;
options.AddRequiredOption("input_path", &input_path);
options.AddRequiredOption("output_path", &output_path);
options.AddRequiredOption("boundary", &boundary);
options.AddDefaultOption("gps_transform_path", &gps_transform_path);
options.Parse(argc, argv);
if (!ExistsDir(input_path)) {
std::cerr << "ERROR: `input_path` is not a directory" << std::endl;
return EXIT_FAILURE;
}
if (!ExistsDir(output_path)) {
std::cerr << "ERROR: `output_path` is not a directory" << std::endl;
return EXIT_FAILURE;
}
std::vector<double> boundary_elements = CSVToVector<double>(boundary);
if (boundary_elements.size() != 2 && boundary_elements.size() != 6) {
std::cerr << "ERROR: Invalid `boundary` - supported values are "
"'x1,y1,z1,x2,y2,z2' or 'p1,p2'."
<< std::endl;
return EXIT_FAILURE;
}
Reconstruction reconstruction;
reconstruction.Read(input_path);
PrintHeading2("Calculating boundary coordinates");
std::pair<Eigen::Vector3d, Eigen::Vector3d> bounding_box;
if (boundary_elements.size() == 6) {
SimilarityTransform3 tform;
if (!gps_transform_path.empty()) {
PrintHeading2("Reading model to ECEF transform");
is_gps = true;
tform = SimilarityTransform3::FromFile(gps_transform_path).Inverse();
}
bounding_box.first =
is_gps ? TransformLatLonAltToModelCoords(tform, boundary_elements[0],
boundary_elements[1],
boundary_elements[2])
: Eigen::Vector3d(boundary_elements[0], boundary_elements[1],
boundary_elements[2]);
bounding_box.second =
is_gps ? TransformLatLonAltToModelCoords(tform, boundary_elements[3],
boundary_elements[4],
boundary_elements[5])
: Eigen::Vector3d(boundary_elements[3], boundary_elements[4],
boundary_elements[5]);
} else {
bounding_box = reconstruction.ComputeBoundingBox(boundary_elements[0],
boundary_elements[1]);
}
PrintHeading2("Cropping reconstruction");
reconstruction.Crop(bounding_box).Write(output_path);
WriteBoundingBox(output_path, bounding_box);
std::cout << "=> Cropping succeeded" << std::endl;
timer.PrintMinutes();
return EXIT_SUCCESS;
}
int RunModelMerger(int argc, char** argv) {
std::string input_path1;
std::string input_path2;
std::string output_path;
double max_reproj_error = 64.0;
OptionManager options;
options.AddRequiredOption("input_path1", &input_path1);
options.AddRequiredOption("input_path2", &input_path2);
options.AddRequiredOption("output_path", &output_path);
options.AddDefaultOption("max_reproj_error", &max_reproj_error);
options.Parse(argc, argv);
Reconstruction reconstruction1;
reconstruction1.Read(input_path1);
PrintHeading2("Reconstruction 1");
std::cout << StringPrintf("Images: %d", reconstruction1.NumRegImages())
<< std::endl;
std::cout << StringPrintf("Points: %d", reconstruction1.NumPoints3D())
<< std::endl;
Reconstruction reconstruction2;
reconstruction2.Read(input_path2);
PrintHeading2("Reconstruction 2");
std::cout << StringPrintf("Images: %d", reconstruction2.NumRegImages())
<< std::endl;
std::cout << StringPrintf("Points: %d", reconstruction2.NumPoints3D())
<< std::endl;
PrintHeading2("Merging reconstructions");
if (reconstruction1.Merge(reconstruction2, max_reproj_error)) {
std::cout << "=> Merge succeeded" << std::endl;
PrintHeading2("Merged reconstruction");
std::cout << StringPrintf("Images: %d", reconstruction1.NumRegImages())
<< std::endl;
std::cout << StringPrintf("Points: %d", reconstruction1.NumPoints3D())
<< std::endl;
} else {
std::cout << "=> Merge failed" << std::endl;
}
reconstruction1.Write(output_path);
return EXIT_SUCCESS;
}
int RunModelOrientationAligner(int argc, char** argv) {
std::string input_path;
std::string output_path;
std::string method = "MANHATTAN-WORLD";
ManhattanWorldFrameEstimationOptions frame_estimation_options;
OptionManager options;
options.AddImageOptions();
options.AddRequiredOption("input_path", &input_path);
options.AddRequiredOption("output_path", &output_path);
options.AddDefaultOption("method", &method,
"{MANHATTAN-WORLD, IMAGE-ORIENTATION}");
options.AddDefaultOption("max_image_size",
&frame_estimation_options.max_image_size);
options.Parse(argc, argv);
StringToLower(&method);
if (method != "manhattan-world" && method != "image-orientation") {
std::cout << "ERROR: Invalid `method` - supported values are "
"'MANHATTAN-WORLD' or 'IMAGE-ORIENTATION'."
<< std::endl;
return EXIT_FAILURE;
}
Reconstruction reconstruction;
reconstruction.Read(input_path);
PrintHeading1("Aligning Reconstruction");
Eigen::Matrix3d tform;
if (method == "manhattan-world") {
const Eigen::Matrix3d frame = EstimateManhattanWorldFrame(
frame_estimation_options, reconstruction, *options.image_path);
if (frame.col(0).lpNorm<1>() == 0) {
std::cout << "Only aligning vertical axis" << std::endl;
tform = RotationFromUnitVectors(frame.col(1), Eigen::Vector3d(0, 1, 0));
} else if (frame.col(1).lpNorm<1>() == 0) {
tform = RotationFromUnitVectors(frame.col(0), Eigen::Vector3d(1, 0, 0));
std::cout << "Only aligning horizontal axis" << std::endl;
} else {
tform = frame.transpose();
std::cout << "Aligning horizontal and vertical axes" << std::endl;
}
} else if (method == "image-orientation") {
const Eigen::Vector3d gravity_axis =
EstimateGravityVectorFromImageOrientation(reconstruction);
tform = RotationFromUnitVectors(gravity_axis, Eigen::Vector3d(0, 1, 0));
} else {
LOG(FATAL) << "Alignment method not supported";
}
std::cout << "Using the rotation matrix:" << std::endl;
std::cout << tform << std::endl;
reconstruction.Transform(SimilarityTransform3(
1, RotationMatrixToQuaternion(tform), Eigen::Vector3d(0, 0, 0)));
std::cout << "Writing aligned reconstruction..." << std::endl;
reconstruction.Write(output_path);
return EXIT_SUCCESS;
}
int RunModelSplitter(int argc, char** argv) {
Timer timer;
timer.Start();
std::string input_path;
std::string output_path;
std::string split_type;
std::string split_params;
std::string gps_transform_path;
int num_threads = -1;
int min_reg_images = 10;
int min_num_points = 100;
double overlap_ratio = 0.0;
double min_area_ratio = 0.0;
bool is_gps = false;
OptionManager options;
options.AddRequiredOption("input_path", &input_path);
options.AddRequiredOption("output_path", &output_path);
options.AddRequiredOption("split_type", &split_type,
"{tiles, extent, parts}");
options.AddRequiredOption("split_params", &split_params);
options.AddDefaultOption("gps_transform_path", &gps_transform_path);
options.AddDefaultOption("num_threads", &num_threads);
options.AddDefaultOption("min_reg_images", &min_reg_images);
options.AddDefaultOption("min_num_points", &min_num_points);
options.AddDefaultOption("overlap_ratio", &overlap_ratio);
options.AddDefaultOption("min_area_ratio", &min_area_ratio);
options.Parse(argc, argv);
if (!ExistsDir(input_path)) {
std::cerr << "ERROR: `input_path` is not a directory" << std::endl;
return EXIT_FAILURE;
}
if (!ExistsDir(output_path)) {
std::cerr << "ERROR: `output_path` is not a directory" << std::endl;
return EXIT_FAILURE;
}
if (overlap_ratio < 0) {
std::cout << "WARN: Invalid `overlap_ratio`; resetting to 0" << std::endl;
overlap_ratio = 0.0;
}
PrintHeading1("Splitting sparse model");
std::cout << StringPrintf(" => Using \"%s\" split type", split_type.c_str())
<< std::endl;
Reconstruction reconstruction;
reconstruction.Read(input_path);
SimilarityTransform3 tform;
if (!gps_transform_path.empty()) {
PrintHeading2("Reading model to ECEF transform");
is_gps = true;
tform = SimilarityTransform3::FromFile(gps_transform_path).Inverse();
}
const double scale = tform.Scale();
// Create the necessary number of reconstructions based on the split method
// and get the bounding boxes for each sub-reconstruction
PrintHeading2("Computing bound_coords");
std::vector<std::string> tile_keys;
std::vector<std::pair<Eigen::Vector3d, Eigen::Vector3d>> exact_bounds;
StringToLower(&split_type);
if (split_type == "tiles") {
std::ifstream file(split_params);
CHECK(file.is_open()) << split_params;
double x1, y1, z1, x2, y2, z2;
std::string tile_key;
std::vector<std::pair<Eigen::Vector3d, Eigen::Vector3d>> bounds;
tile_keys.clear();
file >> tile_key >> x1 >> y1 >> z1 >> x2 >> y2 >> z2;
while (!file.fail()) {
tile_keys.push_back(tile_key);
if (is_gps) {
exact_bounds.emplace_back(
TransformLatLonAltToModelCoords(tform, x1, y1, z1),
TransformLatLonAltToModelCoords(tform, x2, y2, z2));
} else {
exact_bounds.emplace_back(Eigen::Vector3d(x1, y1, z1),
Eigen::Vector3d(x2, y2, z2));
}
file >> tile_key >> x1 >> y1 >> z1 >> x2 >> y2 >> z2;
}
} else if (split_type == "extent") {
std::vector<double> parts = CSVToVector<double>(split_params);
Eigen::Vector3d extent(std::numeric_limits<double>::max(),
std::numeric_limits<double>::max(),
std::numeric_limits<double>::max());
for (size_t i = 0; i < parts.size(); ++i) {
extent(i) = parts[i] * scale;
}
const auto bbox = reconstruction.ComputeBoundingBox();
const Eigen::Vector3d full_extent = bbox.second - bbox.first;
const Eigen::Vector3i split(
static_cast<int>(full_extent(0) / extent(0)) + 1,
static_cast<int>(full_extent(1) / extent(1)) + 1,
static_cast<int>(full_extent(2) / extent(2)) + 1);
exact_bounds = ComputeEqualPartsBounds(reconstruction, split);
} else if (split_type == "parts") {
auto parts = CSVToVector<int>(split_params);
Eigen::Vector3i split(1, 1, 1);
for (size_t i = 0; i < parts.size(); ++i) {
split(i) = parts[i];
if (split(i) < 1) {
std::cerr << "ERROR: Cannot split in less than 1 parts for dim " << i
<< std::endl;
return EXIT_FAILURE;
}
}
exact_bounds = ComputeEqualPartsBounds(reconstruction, split);
} else {
std::cout << "ERROR: Invalid split type: " << split_type << std::endl;
return EXIT_FAILURE;
}
std::vector<std::pair<Eigen::Vector3d, Eigen::Vector3d>> bounds;
for (const auto& bbox : exact_bounds) {
const Eigen::Vector3d padding =
(overlap_ratio * (bbox.second - bbox.first));
bounds.emplace_back(bbox.first - padding, bbox.second + padding);
}
PrintHeading2("Applying split and writing reconstructions");
const size_t num_parts = bounds.size();
std::cout << StringPrintf(" => Splitting to %d parts", num_parts)
<< std::endl;
const bool use_tile_keys = split_type == "tiles";
auto SplitReconstruction = [&](const int idx) {
Reconstruction tile_recon = reconstruction.Crop(bounds[idx]);
// calculate area covered by model as proportion of box area
auto bbox_extent = bounds[idx].second - bounds[idx].first;
auto model_bbox = tile_recon.ComputeBoundingBox();
auto model_extent = model_bbox.second - model_bbox.first;
double area_ratio =
(model_extent(0) * model_extent(1)) / (bbox_extent(0) * bbox_extent(1));
int tile_num_points = tile_recon.NumPoints3D();
std::string name = use_tile_keys ? tile_keys[idx] : std::to_string(idx);
const bool include_tile =
area_ratio >= min_area_ratio && //
tile_num_points >= min_num_points && //
tile_recon.NumRegImages() >= static_cast<size_t>(min_reg_images);
if (include_tile) {
std::cout << StringPrintf(
"Writing reconstruction %s with %d images, %d points, "
"and %.2f%% area coverage",
name.c_str(), tile_recon.NumRegImages(), tile_num_points,
100.0 * area_ratio)
<< std::endl;
const std::string reconstruction_path = JoinPaths(output_path, name);
CreateDirIfNotExists(reconstruction_path);
tile_recon.Write(reconstruction_path);
WriteBoundingBox(reconstruction_path, bounds[idx]);
WriteBoundingBox(reconstruction_path, exact_bounds[idx], "_exact");
} else {
std::cout << StringPrintf(
"Skipping reconstruction %s with %d images, %d points, "
"and %.2f%% area coverage",
name.c_str(), tile_recon.NumRegImages(), tile_num_points,
100.0 * area_ratio)
<< std::endl;
}
};
ThreadPool thread_pool(GetEffectiveNumThreads(num_threads));
for (size_t idx = 0; idx < num_parts; ++idx) {
thread_pool.AddTask(SplitReconstruction, idx);
}
thread_pool.Wait();
timer.PrintMinutes();
return EXIT_SUCCESS;
}
int RunModelTransformer(int argc, char** argv) {
std::string input_path;
std::string output_path;
std::string transform_path;
bool is_inverse = false;
OptionManager options;
options.AddRequiredOption("input_path", &input_path);
options.AddRequiredOption("output_path", &output_path);
options.AddRequiredOption("transform_path", &transform_path);
options.AddDefaultOption("is_inverse", &is_inverse);
options.Parse(argc, argv);
std::cout << "Reading points input: " << input_path << std::endl;
Reconstruction recon;
bool is_dense = false;
if (HasFileExtension(input_path, ".ply")) {
is_dense = true;
recon.ImportPLY(input_path);
} else if (ExistsDir(input_path)) {
recon.Read(input_path);
} else {
std::cerr << "Invalid model input; not a PLY file or sparse reconstruction "
"directory."
<< std::endl;
return EXIT_FAILURE;
}
std::cout << "Reading transform input: " << transform_path << std::endl;
SimilarityTransform3 tform = SimilarityTransform3::FromFile(transform_path);
if (is_inverse) {
tform = tform.Inverse();
}
std::cout << "Applying transform to recon with " << recon.NumPoints3D()
<< " points" << std::endl;
recon.Transform(tform);
std::cout << "Writing output: " << output_path << std::endl;
if (is_dense) {
recon.ExportPLY(output_path);
} else {
recon.Write(output_path);
}
return EXIT_SUCCESS;
}
} // namespace colmap