//! Incremental Structure-from-Motion (SfM). //! //! This module mirrors the high-level incremental mapping pipeline of //! [PyCOLMAP](https://colmap.github.io/pycolmap/) (`pycolmap.incremental_mapping` //! and the underlying `IncrementalMapper` / `IncrementalPipeline` controllers). //! //! It exposes the **option structs** and **driver function signatures** that the //! C++ implementation uses, so that calling code can be written and type-checked //! against the stable public surface. The heavy numerical core — robust two-view //! initialization, absolute-pose registration, triangulation and bundle //! adjustment scheduling — is not ported in this pure-Rust crate yet and the //! driver functions therefore return [`crate::Error::Unimplemented`]. use std::path::Path; use crate::scene::Reconstruction; /// Options that control a single run of the incremental mapper. /// /// These mirror a subset of COLMAP's `IncrementalMapperOptions`. Only the most /// load-bearing knobs are surfaced here; the defaults match COLMAP's defaults. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct IncrementalMapperOptions { /// Minimum number of two-view inliers required to initialize the model /// from the seed image pair. pub init_min_num_inliers: usize, /// Minimum number of inliers required to accept an absolute-pose /// (image registration) estimate. pub abs_pose_min_num_inliers: usize, /// Minimum number of feature matches an image pair must have to be used /// at all during mapping. pub min_num_matches: usize, /// Whether to reconstruct multiple disjoint models from the remaining, /// not-yet-registered images. pub multiple_models: bool, /// Upper bound on the number of distinct models to reconstruct when /// [`multiple_models`](Self::multiple_models) is enabled. pub max_num_models: usize, } impl Default for IncrementalMapperOptions { fn default() -> Self { Self { init_min_num_inliers: 100, abs_pose_min_num_inliers: 30, min_num_matches: 15, multiple_models: true, max_num_models: 50, } } } /// Options that control the full incremental SfM pipeline. /// /// Mirrors a subset of COLMAP's `IncrementalPipelineOptions`, which wraps an /// [`IncrementalMapperOptions`] together with pipeline-level scheduling knobs /// such as how often global bundle adjustment is triggered. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct IncrementalPipelineOptions { /// Minimum number of feature matches an image pair must have to be used. pub min_num_matches: usize, /// The growth ratio of registered images that triggers a new round of /// global bundle adjustment (e.g. `1.1` => re-run when the model grows /// by 10%). pub ba_global_images_ratio: f64, /// Options forwarded to the underlying incremental mapper. pub mapper: IncrementalMapperOptions, } impl Default for IncrementalPipelineOptions { fn default() -> Self { Self { min_num_matches: 15, ba_global_images_ratio: 1.1, mapper: IncrementalMapperOptions::default(), } } } /// Run the full incremental Structure-from-Motion pipeline. /// /// Reads features and matches from the COLMAP feature database at /// `database_path`, registers images one by one, triangulates points and /// bundle-adjusts, writing the resulting sparse model(s) under `output_path`. /// Mirrors `pycolmap.incremental_mapping`. /// /// # Arguments /// /// * `database_path` — path to the SQLite feature database. /// * `image_path` — root directory containing the source images. /// * `output_path` — directory under which reconstructed model(s) are written. /// * `options` — pipeline configuration. /// /// # Errors /// /// Returns [`crate::Error::Unimplemented`]; the numerical SfM core is not ported /// in this pure-Rust crate. pub fn incremental_mapping( database_path: &Path, image_path: &Path, output_path: &Path, options: &IncrementalPipelineOptions, ) -> crate::Result> { let _ = (database_path, image_path, output_path, options); Err(crate::Error::Unimplemented("incremental SfM mapping")) } /// Triangulate additional 3D points into an existing reconstruction. /// /// Given an already-registered set of images (poses fixed), this completes and /// merges feature tracks read from the database into new and existing 3D points. /// Mirrors `pycolmap.triangulate_points`. /// /// # Errors /// /// Returns [`crate::Error::Unimplemented`]; triangulation is not ported in this /// pure-Rust crate. pub fn triangulate_points( rec: &mut Reconstruction, database_path: &Path, ) -> crate::Result<()> { let _ = (rec, database_path); Err(crate::Error::Unimplemented("point triangulation")) } #[cfg(test)] mod tests { use super::*; use std::path::Path; #[test] fn mapper_options_defaults() { let o = IncrementalMapperOptions::default(); assert_eq!(o.init_min_num_inliers, 100); assert_eq!(o.abs_pose_min_num_inliers, 30); assert_eq!(o.min_num_matches, 15); assert!(o.multiple_models); assert_eq!(o.max_num_models, 50); } #[test] fn pipeline_options_defaults() { let o = IncrementalPipelineOptions::default(); assert_eq!(o.min_num_matches, 15); assert!((o.ba_global_images_ratio - 1.1).abs() < 1e-12); assert_eq!(o.mapper, IncrementalMapperOptions::default()); } #[test] fn incremental_mapping_is_unimplemented() { let err = incremental_mapping( Path::new("db.db"), Path::new("images"), Path::new("out"), &IncrementalPipelineOptions::default(), ) .unwrap_err(); assert!(matches!(err, crate::Error::Unimplemented(_))); } #[test] fn triangulate_points_is_unimplemented() { let mut rec = Reconstruction { rigs: Default::default(), cameras: Default::default(), frames: Default::default(), images: Default::default(), points3d: Default::default(), }; let err = triangulate_points(&mut rec, Path::new("db.db")).unwrap_err(); assert!(matches!(err, crate::Error::Unimplemented(_))); } }