// Copyright (c), 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.

#include "colmap/exe/sfm.h"

#include "colmap/controllers/automatic_reconstruction.h"
#include "colmap/controllers/bundle_adjustment.h"
#include "colmap/controllers/global_pipeline.h"
#include "colmap/controllers/hierarchical_pipeline.h"
#include "colmap/controllers/option_manager.h"
#include "colmap/controllers/rotation_averaging.h"
#include "colmap/estimators/bundle_adjustment.h"
#include "colmap/estimators/solvers/similarity_transform.h"
#include "colmap/estimators/view_graph_calibration.h"
#include "colmap/exe/gui.h"
#include "colmap/scene/reconstruction.h"
#include "colmap/sfm/observation_manager.h"
#include "colmap/util/file.h"
#include "colmap/util/misc.h"
#include "colmap/util/opengl_utils.h"

#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>

namespace colmap {
namespace {

std::pair<std::vector<image_t>, std::vector<Eigen::Vector3d>>
ExtractExistingImages(const Reconstruction& reconstruction) {
  std::vector<image_t> fixed_image_ids = reconstruction.RegImageIds();
  std::vector<Eigen::Vector3d> orig_fixed_image_positions;
  orig_fixed_image_positions.reserve(fixed_image_ids.size());
  for (const image_t image_id : fixed_image_ids) {
    orig_fixed_image_positions.push_back(
        reconstruction.Image(image_id).ProjectionCenter());
  }
  return {std::move(fixed_image_ids), std::move(orig_fixed_image_positions)};
}

void UpdateDatabasePosePriorsCovariance(
    const std::filesystem::path& database_path,
    const Eigen::Matrix3d& covariance) {
  auto database = Database::Open(database_path);
  DatabaseTransaction database_transaction(database.get());

  LOG(INFO)
      << "Setting up database pose priors with the same covariance matrix: \n"
      << covariance << '\n';

  for (auto& pose_prior : database->ReadAllPosePriors()) {
    pose_prior.position_covariance = covariance;
    database->UpdatePosePrior(pose_prior);
  }
}

}  // namespace

int RunAutomaticReconstructor(int argc, char** argv) {
  AutomaticReconstructionController::Options reconstruction_options;
  std::filesystem::path image_list_path;
  std::string data_type = "individual";
  std::string quality = "high";
  std::string feature = "sift";
  std::string mapper = "incremental";
  std::string mesher = "poisson";
  std::string ba_backend = "ceres";

  OptionManager options;
  options.AddRequiredOption("workspace_path",
                            &reconstruction_options.workspace_path);
  options.AddRequiredOption("image_path", &reconstruction_options.image_path);
  options.AddDefaultOption("image_list_path", &image_list_path);
  options.AddDefaultOption("mask_path", &reconstruction_options.mask_path);
  options.AddDefaultOption("vocab_tree_path",
                           &reconstruction_options.vocab_tree_path);
  options.AddDefaultOption(
      "data_type", &data_type, "{individual, video, internet}");
  options.AddDefaultOption("quality", &quality, "{low, medium, high, extreme}");
  options.AddDefaultOption("camera_model",
                           &reconstruction_options.camera_model);
  options.AddDefaultOption("single_camera",
                           &reconstruction_options.single_camera);
  options.AddDefaultOption("single_camera_per_folder",
                           &reconstruction_options.single_camera_per_folder);
  options.AddDefaultOption("camera_params",
                           &reconstruction_options.camera_params);
  options.AddDefaultOption("extraction", &reconstruction_options.extraction);
  options.AddDefaultOption("matching", &reconstruction_options.matching);
  options.AddDefaultOption("sparse", &reconstruction_options.sparse);
  options.AddDefaultOption("dense", &reconstruction_options.dense);
  options.AddDefaultOption("feature", &feature, "{sift, aliked}");
  options.AddDefaultOption(
      "mapper", &mapper, "{incremental, hierarchical, global}");
  options.AddDefaultOption("mesher", &mesher, "{poisson, delaunay}");
  options.AddDefaultOption("num_threads", &reconstruction_options.num_threads);
  options.AddDefaultOption("random_seed", &reconstruction_options.random_seed);
  options.AddDefaultOption("use_gpu", &reconstruction_options.use_gpu);
  options.AddDefaultOption("gpu_index", &reconstruction_options.gpu_index);
  options.AddDefaultOption("Mapper.ba_backend", &ba_backend, "{ceres, caspar}");
  if (!options.Parse(argc, argv)) {
    return EXIT_FAILURE;
  }

  if (!image_list_path.empty()) {
    reconstruction_options.image_names = ReadTextFileLines(image_list_path);
  }

  StringToUpper(&data_type);
  reconstruction_options.data_type =
      AutomaticReconstructionController::DataTypeFromString(data_type);

  StringToUpper(&quality);
  reconstruction_options.quality =
      AutomaticReconstructionController::QualityFromString(quality);

  StringToUpper(&feature);
  reconstruction_options.feature =
      AutomaticReconstructionController::FeatureFromString(feature);

  StringToUpper(&mapper);
  reconstruction_options.mapper =
      AutomaticReconstructionController::MapperFromString(mapper);

  StringToUpper(&mesher);
  reconstruction_options.mesher =
      AutomaticReconstructionController::MesherFromString(mesher);

  StringToUpper(&ba_backend);
  reconstruction_options.ba_backend =
      BundleAdjustmentBackendFromString(ba_backend);

  auto reconstruction_manager = std::make_shared<ReconstructionManager>();

  AutomaticReconstructionController controller(reconstruction_options,
                                               reconstruction_manager);
  if (controller.RequiresOpenGL()) {
    QApplication app(argc, argv);
    controller.Setup();
    RunThreadWithOpenGLContext(&controller);
  } else {
    controller.Setup();
    controller.Start();
    controller.Wait();
  }

  return EXIT_SUCCESS;
}

int RunBundleAdjuster(int argc, char** argv) {
  std::filesystem::path input_path;
  std::filesystem::path output_path;

  OptionManager options;
  options.AddRequiredOption("input_path", &input_path);
  options.AddRequiredOption("output_path", &output_path);
  options.AddBundleAdjustmentOptions();
  if (!options.Parse(argc, argv)) {
    return EXIT_FAILURE;
  }

  if (!ExistsDir(input_path)) {
    LOG(ERROR) << "`input_path` is not a directory";
    return EXIT_FAILURE;
  }

  if (!ExistsDir(output_path)) {
    LOG(ERROR) << "`output_path` is not a directory";
    return EXIT_FAILURE;
  }

  auto reconstruction = std::make_shared<Reconstruction>();
  reconstruction->Read(input_path);

  BundleAdjustmentController ba_controller(options, reconstruction);
  ba_controller.Run();

  reconstruction->Write(output_path);

  return EXIT_SUCCESS;
}

int RunColorExtractor(int argc, char** argv) {
  std::filesystem::path input_path;
  std::filesystem::path output_path;
  int num_threads = -1;

  OptionManager options;
  options.AddImageOptions();
  options.AddDefaultOption("input_path", &input_path);
  options.AddRequiredOption("output_path", &output_path);
  options.AddDefaultOption("num_threads", &num_threads);
  if (!options.Parse(argc, argv)) {
    return EXIT_FAILURE;
  }

  Reconstruction reconstruction;
  reconstruction.Read(input_path);
  reconstruction.ExtractColorsForAllImages(*options.image_path, num_threads);
  reconstruction.Write(output_path);

  return EXIT_SUCCESS;
}

bool RunIncrementalMapperImpl(
    const std::filesystem::path& database_path,
    const std::filesystem::path& image_path,
    const std::filesystem::path& output_path,
    const std::shared_ptr<IncrementalPipelineOptions>& mapper_options,
    std::shared_ptr<ReconstructionManager>& reconstruction_manager,
    std::function<void()> initial_image_pair_callback,
    std::function<void()> next_image_callback) {
  // If fix_existing_frames is enabled, we store the initial positions of
  // existing images in order to transform them back to the original coordinate
  // frame, as the reconstruction is normalized multiple times for numerical
  // stability.
  std::vector<Eigen::Vector3d> orig_fixed_image_positions;
  std::vector<image_t> fixed_image_ids;
  const bool exists_input_reconstruction = reconstruction_manager->Size() > 0;
  if (mapper_options->fix_existing_frames && exists_input_reconstruction) {
    std::tie(fixed_image_ids, orig_fixed_image_positions) =
        ExtractExistingImages(*reconstruction_manager->Get(0));
  }

  mapper_options->image_path = image_path;

  auto database = Database::Open(database_path);

  IncrementalPipeline mapper(mapper_options, database, reconstruction_manager);

  // In case a new reconstruction is started, write results of individual sub-
  // models to as their reconstruction finishes instead of writing all results
  // after all reconstructions finished.
  size_t prev_num_reconstructions = 0;
  if (!exists_input_reconstruction) {
    mapper.AddCallback(IncrementalPipeline::LAST_IMAGE_REG_CALLBACK, [&]() {
      // If the number of reconstructions has not changed, the last model
      // was discarded for some reason.
      if (reconstruction_manager->Size() > prev_num_reconstructions) {
        const auto reconstruction_path =
            output_path / std::to_string(prev_num_reconstructions);
        CreateDirIfNotExists(reconstruction_path);
        reconstruction_manager->Get(prev_num_reconstructions)
            ->Write(reconstruction_path);
        prev_num_reconstructions = reconstruction_manager->Size();
      }
    });
  }

  if (initial_image_pair_callback) {
    mapper.AddCallback(IncrementalPipeline::INITIAL_IMAGE_PAIR_REG_CALLBACK,
                       std::move(initial_image_pair_callback));
  }

  if (next_image_callback) {
    mapper.AddCallback(IncrementalPipeline::NEXT_IMAGE_REG_CALLBACK,
                       std::move(next_image_callback));
  }

  mapper.Run();

  if (reconstruction_manager->Size() == 0) {
    LOG(ERROR) << "Failed to create any sparse model";
    return false;
  }

  // In case the reconstruction is continued from an existing reconstruction, do
  // not create sub-folders but directly write the results.
  if (exists_input_reconstruction) {
    const auto& reconstruction = reconstruction_manager->Get(0);

    // Transform the final reconstruction back to the original coordinate frame.
    if (mapper_options->fix_existing_frames) {
      if (fixed_image_ids.size() < 3) {
        LOG(WARNING) << "Too few images to transform the reconstruction.";
      } else {
        std::vector<Eigen::Vector3d> new_fixed_image_positions;
        new_fixed_image_positions.reserve(fixed_image_ids.size());
        for (const image_t image_id : fixed_image_ids) {
          new_fixed_image_positions.push_back(
              reconstruction->Image(image_id).ProjectionCenter());
        }
        Sim3d orig_from_new;
        if (EstimateSim3d(new_fixed_image_positions,
                          orig_fixed_image_positions,
                          orig_from_new)) {
          reconstruction->Transform(orig_from_new);
        } else {
          LOG(WARNING) << "Failed to transform the reconstruction back "
                          "to the input coordinate frame.";
        }
      }
    }

    reconstruction->Write(output_path);
  }

  return true;
}

int RunMapper(int argc, char** argv) {
  std::filesystem::path input_path;
  std::filesystem::path output_path;

  OptionManager options;
  options.AddDatabaseOptions();
  options.AddImageOptions();
  options.AddDefaultOption("input_path", &input_path);
  options.AddRequiredOption("output_path", &output_path);
  options.AddMapperOptions();
  if (!options.Parse(argc, argv)) {
    return EXIT_FAILURE;
  }

  if (!ExistsDir(output_path)) {
    LOG(ERROR) << "`output_path` is not a directory.";
    return EXIT_FAILURE;
  }

  auto reconstruction_manager = std::make_shared<ReconstructionManager>();
  if (!input_path.empty()) {
    if (!ExistsDir(input_path)) {
      LOG(ERROR) << "`input_path` is not a directory.";
      return EXIT_FAILURE;
    }
    reconstruction_manager->Read(input_path);
  }

  if (!RunIncrementalMapperImpl(*options.database_path,
                                *options.image_path,
                                output_path,
                                options.mapper,
                                reconstruction_manager)) {
    return EXIT_FAILURE;
  }

  if (input_path.empty()) {
    for (size_t i = 0; i < reconstruction_manager->Size(); ++i) {
      const auto reconstruction_path = output_path / std::to_string(i);
      options.Write(reconstruction_path / "project.ini");
    }
  }

  return EXIT_SUCCESS;
}

bool RunGlobalMapperImpl(
    const std::filesystem::path& database_path,
    const std::filesystem::path& image_path,
    const std::filesystem::path& output_path,
    const std::shared_ptr<GlobalPipelineOptions>& mapper_options,
    std::shared_ptr<ReconstructionManager>& reconstruction_manager) {
  GlobalPipelineOptions options = *mapper_options;
  options.image_path = image_path;

  GlobalPipeline global_mapper(std::move(options),
                               Database::Open(database_path),
                               reconstruction_manager);
  global_mapper.Run();

  if (reconstruction_manager->Size() == 0) {
    LOG(ERROR) << "Failed to create sparse model";
    return false;
  }

  reconstruction_manager->Write(output_path);
  return true;
}

int RunGlobalMapper(int argc, char** argv) {
  std::filesystem::path output_path;

  OptionManager options;
  options.AddDatabaseOptions();
  options.AddImageOptions();
  options.AddRequiredOption("output_path", &output_path);
  options.AddGlobalMapperOptions();
  if (!options.Parse(argc, argv)) {
    return EXIT_FAILURE;
  }

  if (!ExistsDir(output_path)) {
    LOG(ERROR) << "`output_path` is not a directory.";
    return EXIT_FAILURE;
  }

  auto reconstruction_manager = std::make_shared<ReconstructionManager>();
  if (!RunGlobalMapperImpl(*options.database_path,
                           *options.image_path,
                           output_path,
                           options.global_mapper,
                           reconstruction_manager)) {
    return EXIT_FAILURE;
  }

  options.Write(output_path / "project.ini");
  return EXIT_SUCCESS;
}

int RunHierarchicalMapper(int argc, char** argv) {
  HierarchicalPipeline::Options mapper_options;
  std::filesystem::path output_path;

  OptionManager options;
  options.AddDatabaseOptions();
  options.AddRequiredOption("image_path", &mapper_options.image_path);
  options.AddRequiredOption("output_path", &output_path);
  options.AddDefaultOption("num_threads", &mapper_options.num_threads);
  options.AddDefaultOption("num_workers", &mapper_options.num_workers);
  options.AddDefaultOption("image_overlap",
                           &mapper_options.clustering_options.image_overlap);
  options.AddDefaultOption(
      "leaf_max_num_images",
      &mapper_options.clustering_options.leaf_max_num_images);
  options.AddMapperOptions();
  if (!options.Parse(argc, argv)) {
    return EXIT_FAILURE;
  }

  if (!ExistsDir(output_path)) {
    LOG(ERROR) << "`output_path` is not a directory.";
    return EXIT_FAILURE;
  }

  mapper_options.incremental_options = *options.mapper;
  auto reconstruction_manager = std::make_shared<ReconstructionManager>();
  HierarchicalPipeline hierarchical_mapper(
      mapper_options,
      Database::Open(*options.database_path),
      reconstruction_manager);
  hierarchical_mapper.Run();

  if (reconstruction_manager->Size() == 0) {
    LOG(ERROR) << "failed to create sparse model";
    return EXIT_FAILURE;
  }

  reconstruction_manager->Write(output_path);
  options.Write(output_path / "project.ini");

  return EXIT_SUCCESS;
}

int RunPosePriorMapper(int argc, char** argv) {
  std::filesystem::path input_path;
  std::filesystem::path output_path;

  bool overwrite_priors_covariance = false;
  double prior_position_std_x = 1.;
  double prior_position_std_y = 1.;
  double prior_position_std_z = 1.;

  OptionManager options;
  options.AddDatabaseOptions();
  options.AddImageOptions();
  options.AddDefaultOption("input_path", &input_path);
  options.AddRequiredOption("output_path", &output_path);
  options.AddMapperOptions();

  options.mapper->use_prior_position = true;

  options.AddDefaultOption(
      "overwrite_priors_covariance",
      &overwrite_priors_covariance,
      "Priors covariance read from database. If true, overwrite the priors "
      "covariance using the following prior_position_std_... options");
  options.AddDefaultOption("prior_position_std_x", &prior_position_std_x);
  options.AddDefaultOption("prior_position_std_y", &prior_position_std_y);
  options.AddDefaultOption("prior_position_std_z", &prior_position_std_z);
  options.AddDefaultOption("use_robust_loss_on_prior_position",
                           &options.mapper->use_robust_loss_on_prior_position);
  options.AddDefaultOption("prior_position_loss_scale",
                           &options.mapper->prior_position_loss_scale);
  if (!options.Parse(argc, argv)) {
    return EXIT_FAILURE;
  }

  if (!ExistsDir(output_path)) {
    LOG(ERROR) << "`output_path` is not a directory.";
    return EXIT_FAILURE;
  }

  if (overwrite_priors_covariance) {
    const Eigen::Matrix3d covariance =
        Eigen::Vector3d(
            prior_position_std_x, prior_position_std_y, prior_position_std_z)
            .cwiseAbs2()
            .asDiagonal();
    UpdateDatabasePosePriorsCovariance(*options.database_path, covariance);
  }

  auto reconstruction_manager = std::make_shared<ReconstructionManager>();
  if (input_path != "") {
    if (!ExistsDir(input_path)) {
      LOG(ERROR) << "`input_path` is not a directory.";
      return EXIT_FAILURE;
    }
    reconstruction_manager->Read(input_path);
  }

  if (!RunIncrementalMapperImpl(*options.database_path,
                                *options.image_path,
                                output_path,
                                options.mapper,
                                reconstruction_manager)) {
    return EXIT_FAILURE;
  }

  if (input_path.empty()) {
    for (size_t i = 0; i < reconstruction_manager->Size(); ++i) {
      const auto reconstruction_path = output_path / std::to_string(i);
      options.Write(reconstruction_path / "project.ini");
    }
  }

  return EXIT_SUCCESS;
}

int RunPointFiltering(int argc, char** argv) {
  std::filesystem::path input_path;
  std::filesystem::path output_path;

  int min_track_len = 2;
  double max_reproj_error = 4.0;
  double min_tri_angle = 1.5;

  OptionManager options;
  options.AddRequiredOption("input_path", &input_path);
  options.AddRequiredOption("output_path", &output_path);
  options.AddDefaultOption("min_track_len", &min_track_len);
  options.AddDefaultOption("max_reproj_error", &max_reproj_error);
  options.AddDefaultOption("min_tri_angle", &min_tri_angle);
  if (!options.Parse(argc, argv)) {
    return EXIT_FAILURE;
  }

  Reconstruction reconstruction;
  reconstruction.Read(input_path);

  ObservationManager obs_manager(reconstruction);
  size_t num_filtered =
      obs_manager.FilterAllPoints3D(max_reproj_error, min_tri_angle);
  num_filtered += obs_manager.FilterPoints3DWithShortTracks(min_track_len);

  LOG(INFO) << "Filtered observations: " << num_filtered;

  reconstruction.Write(output_path);

  return EXIT_SUCCESS;
}

int RunPointTriangulator(int argc, char** argv) {
  std::filesystem::path input_path;
  std::filesystem::path output_path;
  bool clear_points = true;
  bool refine_intrinsics = false;

  OptionManager options;
  options.AddDatabaseOptions();
  options.AddImageOptions();
  options.AddRequiredOption("input_path", &input_path);
  options.AddRequiredOption("output_path", &output_path);
  options.AddDefaultOption(
      "clear_points",
      &clear_points,
      "Whether to clear all existing points and observations and recompute "
      "the image_ids based on matching filenames between the model and the "
      "database");
  options.AddDefaultOption("refine_intrinsics",
                           &refine_intrinsics,
                           "Whether to refine the intrinsics of the cameras "
                           "(fixing the principal point)");
  options.AddMapperOptions();
  if (!options.Parse(argc, argv)) {
    return EXIT_FAILURE;
  }

  if (!ExistsDir(input_path)) {
    LOG(ERROR) << "`input_path` is not a directory";
    return EXIT_FAILURE;
  }

  if (!ExistsDir(output_path)) {
    LOG(ERROR) << "`output_path` is not a directory";
    return EXIT_FAILURE;
  }

  LOG_HEADING1("Loading model");

  auto reconstruction = std::make_shared<Reconstruction>();
  reconstruction->Read(input_path);

  RunPointTriangulatorImpl(reconstruction,
                           *options.database_path,
                           *options.image_path,
                           output_path,
                           *options.mapper,
                           clear_points,
                           refine_intrinsics);
  return EXIT_SUCCESS;
}

void RunPointTriangulatorImpl(
    const std::shared_ptr<Reconstruction>& reconstruction,
    const std::filesystem::path& database_path,
    const std::filesystem::path& image_path,
    const std::filesystem::path& output_path,
    const IncrementalPipelineOptions& options,
    const bool clear_points,
    const bool refine_intrinsics) {
  THROW_CHECK_GE(reconstruction->NumRegImages(), 2)
      << "Need at least two images for triangulation";
  if (clear_points) {
    reconstruction->DeleteAllPoints2DAndPoints3D();
    reconstruction->TranscribeImageIdsToDatabase(
        *Database::Open(database_path));
  }

  auto custom_options = std::make_shared<IncrementalPipelineOptions>(options);
  custom_options->image_path = image_path;
  custom_options->load_all_images = true;
  custom_options->fix_existing_frames = true;
  custom_options->ba_refine_focal_length = refine_intrinsics;
  custom_options->ba_refine_principal_point = false;
  custom_options->ba_refine_extra_params = refine_intrinsics;

  auto reconstruction_manager = std::make_shared<ReconstructionManager>();
  IncrementalPipeline mapper(
      custom_options, Database::Open(database_path), reconstruction_manager);
  mapper.TriangulateReconstruction(reconstruction);
  reconstruction->Write(output_path);
}

int RunRotationAverager(int argc, char** argv) {
  std::filesystem::path output_path;
  std::filesystem::path image_list_path;

  RotationAveragingPipelineOptions controller_options;

  OptionManager options;
  options.AddDatabaseOptions();
  options.AddRequiredOption("output_path", &output_path);
  options.AddDefaultOption("image_list_path", &image_list_path);
  options.AddDefaultOption("min_num_matches",
                           &controller_options.min_num_matches);
  options.AddDefaultOption("ignore_watermarks",
                           &controller_options.ignore_watermarks);
  options.AddDefaultOption("num_threads", &controller_options.num_threads);
  options.AddDefaultOption("random_seed", &controller_options.random_seed);
  options.AddDefaultOption("use_gravity",
                           &controller_options.rotation_estimation.use_gravity);
  options.AddDefaultOption(
      "use_stratified", &controller_options.rotation_estimation.use_stratified);
  options.AddDefaultOption("refine_gravity",
                           &controller_options.refine_gravity);
  options.AddGravityRefinerOptions();
  if (!options.Parse(argc, argv)) {
    return EXIT_FAILURE;
  }

  controller_options.gravity_refiner = *options.gravity_refiner;

  if (!image_list_path.empty()) {
    controller_options.image_names = ReadTextFileLines(image_list_path);
  }

  if (!ExistsDir(output_path)) {
    LOG(ERROR) << "`output_path` is not a directory";
    return EXIT_FAILURE;
  }

  auto database = Database::Open(*options.database_path);
  auto reconstruction = std::make_shared<Reconstruction>();

  RotationAveragingPipeline controller(
      controller_options, std::move(database), reconstruction);
  controller.Run();

  if (reconstruction->NumRegFrames() == 0) {
    LOG(ERROR) << "No frames registered";
    return EXIT_FAILURE;
  }

  LOG(INFO) << "Writing reconstruction to " << output_path;
  reconstruction->Write(output_path);

  return EXIT_SUCCESS;
}

int RunViewGraphCalibrator(int argc, char** argv) {
  ViewGraphCalibrationOptions calibration_options;

  OptionManager options;
  options.AddDatabaseOptions();
  options.AddDefaultOption(
      "cross_validate_prior_focal_lengths",
      &calibration_options.cross_validate_prior_focal_lengths,
      "Cross-validate prior focal lengths");
  options.AddDefaultOption(
      "min_calibrated_pair_ratio",
      &calibration_options.min_calibrated_pair_ratio,
      "Minimum ratio of calibrated pairs for cross-validation");
  options.AddDefaultOption("reestimate_relative_pose",
                           &calibration_options.reestimate_relative_pose,
                           "Re-estimate relative poses after calibration");
  options.AddDefaultOption("min_focal_length_ratio",
                           &calibration_options.min_focal_length_ratio,
                           "Minimum ratio of estimated to prior focal length");
  options.AddDefaultOption("max_focal_length_ratio",
                           &calibration_options.max_focal_length_ratio,
                           "Maximum ratio of estimated to prior focal length");
  options.AddDefaultOption("max_calibration_error",
                           &calibration_options.max_calibration_error,
                           "Maximum calibration error for an image pair");
  options.AddDefaultOption("relpose_max_error",
                           &calibration_options.relpose_max_error,
                           "Maximum error for relative pose re-estimation");
  options.AddDefaultOption("relpose_min_num_inliers",
                           &calibration_options.relpose_min_num_inliers,
                           "Minimum inliers for relative pose re-estimation");
  options.AddDefaultOption(
      "relpose_min_inlier_ratio",
      &calibration_options.relpose_min_inlier_ratio,
      "Minimum inlier ratio for relative pose re-estimation");
  if (!options.Parse(argc, argv)) {
    return EXIT_FAILURE;
  }

  auto database = Database::Open(*options.database_path);

  if (!CalibrateViewGraph(calibration_options, database.get())) {
    LOG(ERROR) << "View graph calibration failed";
    return EXIT_FAILURE;
  }

  LOG(INFO) << "View graph calibration completed successfully";
  return EXIT_SUCCESS;
}

}  // namespace colmap
