From 1ad79b1230d20d1c545155f1956ec2b7ea6febd0 Mon Sep 17 00:00:00 2001 From: Johnathon Selstad Date: Wed, 14 Jan 2026 11:39:24 -0800 Subject: [PATCH 1/2] Add EQUIRECTANGULAR camera model support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Configure FetchContent to use local COLMAP/PoseLib with equirectangular - Add EQUIRECTANGULAR handling in view_graph_calibration.cc - Support spherical camera intrinsics in camera.h - Enables global SfM pipeline for 360° panoramic images Co-Authored-By: Claude Opus 4.5 --- cmake/FindDependencies.cmake | 13 +++++---- glomap/estimators/view_graph_calibration.cc | 15 ++++++++++ glomap/scene/camera.h | 32 ++++++++++++++++++++- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/cmake/FindDependencies.cmake b/cmake/FindDependencies.cmake index c1d0110e..413fc354 100644 --- a/cmake/FindDependencies.cmake +++ b/cmake/FindDependencies.cmake @@ -25,13 +25,14 @@ if(TESTS_ENABLED) endif() include(FetchContent) + +# Use local PoseLib with EQUIRECTANGULAR support FetchContent_Declare(PoseLib - GIT_REPOSITORY https://github.com/PoseLib/PoseLib.git - GIT_TAG 7e9f5f53372e43f89655040d4dfc4a00e5ace11c # 2.0.5 + SOURCE_DIR /home/selstad/Desktop/colmap_workspace/PoseLib EXCLUDE_FROM_ALL SYSTEM ) -message(STATUS "Configuring PoseLib...") +message(STATUS "Configuring PoseLib (local with EQUIRECTANGULAR)...") if (FETCH_POSELIB) FetchContent_MakeAvailable(PoseLib) else() @@ -39,12 +40,12 @@ else() endif() message(STATUS "Configuring PoseLib... done") +# Use local COLMAP with EQUIRECTANGULAR support FetchContent_Declare(COLMAP - GIT_REPOSITORY https://github.com/colmap/colmap.git - GIT_TAG c5f9cefc87e5dd596b638e4cee0ff543c7d14755 # Oct 23 2025 + SOURCE_DIR /home/selstad/Desktop/colmap_workspace/colmap EXCLUDE_FROM_ALL ) -message(STATUS "Configuring COLMAP...") +message(STATUS "Configuring COLMAP (local with EQUIRECTANGULAR)...") set(UNINSTALL_ENABLED OFF CACHE INTERNAL "") set(GUI_ENABLED OFF CACHE INTERNAL "") if (FETCH_COLMAP) diff --git a/glomap/estimators/view_graph_calibration.cc b/glomap/estimators/view_graph_calibration.cc index a66b5a2c..680573a1 100644 --- a/glomap/estimators/view_graph_calibration.cc +++ b/glomap/estimators/view_graph_calibration.cc @@ -54,6 +54,10 @@ void ViewGraphCalibrator::Reset( focals_.clear(); focals_.reserve(cameras.size()); for (const auto& [camera_id, camera] : cameras) { + // Skip spherical cameras - they don't have a focal length to estimate + if (camera.IsSpherical()) { + continue; + } focals_[camera_id] = camera.Focal(); } @@ -85,6 +89,12 @@ void ViewGraphCalibrator::AddImagePair( const camera_t camera_id1 = images.at(image_pair.image_id1).camera_id; const camera_t camera_id2 = images.at(image_pair.image_id2).camera_id; + // Skip image pairs involving spherical cameras + if (cameras.at(camera_id1).IsSpherical() || + cameras.at(camera_id2).IsSpherical()) { + return; + } + if (camera_id1 == camera_id2) { problem_->AddResidualBlock( FetzerFocalLengthSameCameraCost::Create( @@ -123,6 +133,11 @@ void ViewGraphCalibrator::CopyBackResults( std::unordered_map& cameras) { size_t counter = 0; for (auto& [camera_id, camera] : cameras) { + // Skip spherical cameras - they were not optimized + if (camera.IsSpherical()) { + continue; + } + if (!problem_->HasParameterBlock(&(focals_[camera_id]))) continue; // if the estimated parameter is too crazy, reject it diff --git a/glomap/scene/camera.h b/glomap/scene/camera.h index eeaf883f..ccae4062 100644 --- a/glomap/scene/camera.h +++ b/glomap/scene/camera.h @@ -20,18 +20,48 @@ struct Camera : public colmap::Camera { bool has_refined_focal_length = false; + // Returns true if this is a spherical camera model (e.g., EQUIRECTANGULAR) + // Spherical cameras do not have meaningful focal length or principal point + inline bool IsSpherical() const; + inline double Focal() const; inline Eigen::Vector2d PrincipalPoint() const; inline Eigen::Matrix3d GetK() const; }; -double Camera::Focal() const { return (FocalLengthX() + FocalLengthY()) / 2.0; } +bool Camera::IsSpherical() const { + // Check by model name since model_id may not be defined for EQUIRECTANGULAR + // in older COLMAP versions + return ModelName() == "EQUIRECTANGULAR" || ModelName() == "SPHERICAL"; +} + +double Camera::Focal() const { + // Spherical cameras don't have a meaningful focal length + // Return a dummy value based on image dimensions for compatibility + if (IsSpherical()) { + return static_cast(std::max(width, height)) / M_PI; + } + return (FocalLengthX() + FocalLengthY()) / 2.0; +} Eigen::Vector2d Camera::PrincipalPoint() const { + // Spherical cameras don't have a principal point in the traditional sense + // Return the image center for compatibility + if (IsSpherical()) { + return Eigen::Vector2d(width / 2.0, height / 2.0); + } return Eigen::Vector2d(PrincipalPointX(), PrincipalPointY()); } Eigen::Matrix3d Camera::GetK() const { + // Spherical cameras don't have a 3x3 intrinsic matrix + // Return identity-like matrix for compatibility (should not be used) + if (IsSpherical()) { + Eigen::Matrix3d K = Eigen::Matrix3d::Identity(); + K(0, 2) = width / 2.0; + K(1, 2) = height / 2.0; + return K; + } Eigen::Matrix3d K; K << FocalLengthX(), 0, PrincipalPointX(), 0, FocalLengthY(), PrincipalPointY(), 0, 0, 1; From a68462fa10d9800b592013f3c278390bc1824db4 Mon Sep 17 00:00:00 2001 From: Johnathon Selstad Date: Thu, 15 Jan 2026 06:36:26 -0800 Subject: [PATCH 2/2] Address PR review comments and fix COLMAP API compatibility - Use GIT_REPOSITORY in FetchContent instead of hardcoded local paths - Fix M_PI usage with explicit constant value - Add LOG(WARNING) for GetK() on spherical cameras - Use model_id check with fallback to ModelName() for IsSpherical() - Update DatabaseCache::Create to use new Options struct API - Handle std::optional types for TwoViewGeometry fields (F, H, cam2_from_cam1) Co-Authored-By: Claude Opus 4.5 --- cmake/FindDependencies.cmake | 14 +++++++----- glomap/controllers/track_retriangulation.cc | 11 ++++----- glomap/io/colmap_converter.cc | 24 +++++++++++++------- glomap/processors/view_graph_manipulation.cc | 10 ++++---- glomap/scene/camera.h | 13 +++++++---- 5 files changed, 44 insertions(+), 28 deletions(-) diff --git a/cmake/FindDependencies.cmake b/cmake/FindDependencies.cmake index c6885f76..64525887 100644 --- a/cmake/FindDependencies.cmake +++ b/cmake/FindDependencies.cmake @@ -22,13 +22,14 @@ endif() include(FetchContent) -# Use local PoseLib with EQUIRECTANGULAR support +# PoseLib with EQUIRECTANGULAR support FetchContent_Declare(PoseLib - SOURCE_DIR /home/selstad/Desktop/colmap_workspace/PoseLib + GIT_REPOSITORY https://github.com/zalo/PoseLib.git + GIT_TAG feature/equirectangular EXCLUDE_FROM_ALL SYSTEM ) -message(STATUS "Configuring PoseLib (local with EQUIRECTANGULAR)...") +message(STATUS "Configuring PoseLib (with EQUIRECTANGULAR support)...") if (FETCH_POSELIB) FetchContent_MakeAvailable(PoseLib) else() @@ -36,12 +37,13 @@ else() endif() message(STATUS "Configuring PoseLib... done") -# Use local COLMAP with EQUIRECTANGULAR support +# COLMAP with EQUIRECTANGULAR support FetchContent_Declare(COLMAP - SOURCE_DIR /home/selstad/Desktop/colmap_workspace/colmap + GIT_REPOSITORY https://github.com/zalo/colmap.git + GIT_TAG feature/equirectangular EXCLUDE_FROM_ALL ) -message(STATUS "Configuring COLMAP (local with EQUIRECTANGULAR)...") +message(STATUS "Configuring COLMAP (with EQUIRECTANGULAR support)...") set(UNINSTALL_ENABLED OFF CACHE INTERNAL "") set(GUI_ENABLED OFF CACHE INTERNAL "") if (FETCH_COLMAP) diff --git a/glomap/controllers/track_retriangulation.cc b/glomap/controllers/track_retriangulation.cc index 0b133e0d..cd2fa22c 100644 --- a/glomap/controllers/track_retriangulation.cc +++ b/glomap/controllers/track_retriangulation.cc @@ -18,12 +18,11 @@ bool RetriangulateTracks(const TriangulatorOptions& options, std::unordered_map& images, std::unordered_map& tracks) { // Following code adapted from COLMAP - auto database_cache = - colmap::DatabaseCache::Create(database, - options.min_num_matches, - false, // ignore_watermarks - {} // reconstruct all possible images - ); + colmap::DatabaseCache::Options cache_options; + cache_options.min_num_matches = options.min_num_matches; + cache_options.ignore_watermarks = false; + // image_names defaults to empty set (reconstruct all possible images) + auto database_cache = colmap::DatabaseCache::Create(database, cache_options); // Check whether the image is in the database cache. If not, set the image // as not registered to avoid memory error. diff --git a/glomap/io/colmap_converter.cc b/glomap/io/colmap_converter.cc index 4c7fd030..1d6de86d 100644 --- a/glomap/io/colmap_converter.cc +++ b/glomap/io/colmap_converter.cc @@ -380,19 +380,27 @@ void ConvertDatabaseToGlomap(const colmap::Database& database, // Collect the fundemental matrices if (two_view.config == colmap::TwoViewGeometry::UNCALIBRATED) { - image_pair.F = two_view.F; + if (two_view.F.has_value()) { + image_pair.F = two_view.F.value(); + } } else if (two_view.config == colmap::TwoViewGeometry::CALIBRATED) { - FundamentalFromMotionAndCameras( - cameras.at(images.at(image_pair.image_id1).camera_id), - cameras.at(images.at(image_pair.image_id2).camera_id), - two_view.cam2_from_cam1, - &image_pair.F); + if (two_view.cam2_from_cam1.has_value()) { + FundamentalFromMotionAndCameras( + cameras.at(images.at(image_pair.image_id1).camera_id), + cameras.at(images.at(image_pair.image_id2).camera_id), + two_view.cam2_from_cam1.value(), + &image_pair.F); + } } else if (two_view.config == colmap::TwoViewGeometry::PLANAR || two_view.config == colmap::TwoViewGeometry::PANORAMIC || two_view.config == colmap::TwoViewGeometry::PLANAR_OR_PANORAMIC) { - image_pair.H = two_view.H; - image_pair.F = two_view.F; + if (two_view.H.has_value()) { + image_pair.H = two_view.H.value(); + } + if (two_view.F.has_value()) { + image_pair.F = two_view.F.value(); + } } image_pair.config = two_view.config; diff --git a/glomap/processors/view_graph_manipulation.cc b/glomap/processors/view_graph_manipulation.cc index 24c36726..474589ed 100644 --- a/glomap/processors/view_graph_manipulation.cc +++ b/glomap/processors/view_graph_manipulation.cc @@ -288,11 +288,13 @@ void ViewGraphManipulater::DecomposeRelPose( return; image_pair.config = two_view_geometry.config; - image_pair.cam2_from_cam1 = two_view_geometry.cam2_from_cam1; + if (two_view_geometry.cam2_from_cam1.has_value()) { + image_pair.cam2_from_cam1 = two_view_geometry.cam2_from_cam1.value(); - if (image_pair.cam2_from_cam1.translation.norm() > EPS) { - image_pair.cam2_from_cam1.translation = - image_pair.cam2_from_cam1.translation.normalized(); + if (image_pair.cam2_from_cam1.translation.norm() > EPS) { + image_pair.cam2_from_cam1.translation = + image_pair.cam2_from_cam1.translation.normalized(); + } } }); } diff --git a/glomap/scene/camera.h b/glomap/scene/camera.h index ccae4062..518ee562 100644 --- a/glomap/scene/camera.h +++ b/glomap/scene/camera.h @@ -4,6 +4,7 @@ #include #include +#include #include @@ -30,16 +31,18 @@ struct Camera : public colmap::Camera { }; bool Camera::IsSpherical() const { - // Check by model name since model_id may not be defined for EQUIRECTANGULAR - // in older COLMAP versions - return ModelName() == "EQUIRECTANGULAR" || ModelName() == "SPHERICAL"; + // Check model_id for EQUIRECTANGULAR (most robust) + // Also check model name as fallback for compatibility + return model_id == colmap::CameraModelId::kEquirectangular || + ModelName() == "EQUIRECTANGULAR" || ModelName() == "SPHERICAL"; } double Camera::Focal() const { // Spherical cameras don't have a meaningful focal length // Return a dummy value based on image dimensions for compatibility if (IsSpherical()) { - return static_cast(std::max(width, height)) / M_PI; + constexpr double kPi = 3.141592653589793238462643383279502884L; + return static_cast(std::max(width, height)) / kPi; } return (FocalLengthX() + FocalLengthY()) / 2.0; } @@ -57,6 +60,8 @@ Eigen::Matrix3d Camera::GetK() const { // Spherical cameras don't have a 3x3 intrinsic matrix // Return identity-like matrix for compatibility (should not be used) if (IsSpherical()) { + LOG(WARNING) << "GetK() called on spherical camera - intrinsic matrix is " + "not meaningful for equirectangular cameras"; Eigen::Matrix3d K = Eigen::Matrix3d::Identity(); K(0, 2) = width / 2.0; K(1, 2) = height / 2.0;