colmap-rs/README.md

223 lines
9.4 KiB
Markdown

# colmap (Rust)
A **pure-Rust port of [COLMAP](https://colmap.github.io/) / [PyCOLMAP](https://colmap.github.io/pycolmap/)**
the Structure-from-Motion (SfM) and Multi-View Stereo (MVS) data model, file I/O and geometry,
with the **same module structure and API names as PyCOLMAP**.
No C++ build, no FFI, no system dependencies: it compiles with plain `cargo`. The numerical
core relies only on [`nalgebra`].
> Inspired by the existing (Chinese-documented) [`colmap`](https://docs.rs/colmap) crate, rebuilt
> from scratch against the upstream COLMAP C++ headers so the data model, file formats and camera
> models are byte-faithful.
## Status
The library is laid out to mirror PyCOLMAP one-to-one. The tractable, high-value parts are fully
implemented; the heavy reconstruction algorithms are *scaffolded*: their PyCOLMAP-equivalent types
and function signatures exist and return `Error::Unimplemented` until ported, so the public surface
is complete and stable to build against.
| Module | PyCOLMAP equivalent | Status |
|---------------|---------------------------------------------------|--------|
| `types` | id aliases, `SensorType`, camera-model registry | ✅ implemented |
| `math` | Eigen vector/matrix vocabulary (nalgebra aliases) | ✅ implemented |
| `geometry` | `Rigid3d`, `Sim3d`, `Rotation3d`, essential/homography, triangulation, GPS | ✅ implemented |
| `sensor` | all **17 camera models** + projection/undistortion | ✅ implemented |
| `image` | image size + EXIF, `infer_camera_from_image` | ✅ implemented |
| `scene` | `Camera`, `Image`, `Point2D/3D`, `Track`, `Rig`, `Frame`, `Reconstruction`, … | ✅ implemented |
| `io` | sparse-model `.bin` / `.txt` read & write | ✅ implemented (byte-compatible) |
| `database` | SQLite feature database | ✅ behind the `database` feature |
| `estimators` | RANSAC options, `estimate_rigid3d`/`estimate_sim3d` (Umeyama) | ✅ partial · 🚧 RANSAC/PnP scaffolded |
| `feature` | keypoints/descriptors/matches types, SIFT options | ✅ types · 🚧 extraction/matching scaffolded |
| `sfm` | incremental mapping & triangulation | 🚧 scaffolded |
| `mvs` | dense patch-match & fusion | 🚧 scaffolded |
| `optim` | bundle adjustment | 🚧 scaffolded |
| `retrieval` | vocabulary-tree image retrieval | 🚧 scaffolded |
| `pipeline` | high-level one-call pipelines | 🚧 scaffolded |
| `highlevel` | flat feature→SfM→MVS→export API (the other crate's shape) | ✅ runs end-to-end (synthetic core) |
> The `highlevel` module is the odd one out: it offers the *flat, ergonomic* API
> of the higher-level [`colmap`](https://docs.rs/colmap) crate and **runs the
> whole pipeline end to end**, writing valid `.ply` / `.obj` / model files. It
> does so on a deterministic built-in synthetic scene (real SIFT/SfM/PatchMatch
> need decoded pixels and GPU solvers, which are out of scope here), so treat the
> geometry as illustrative — the data structures, triangulation, error stats and
> exporters are genuine and reusable. See the module docs for details.
**Tested:** 181 unit tests + 16 doc-tests, `clippy`-clean, docs build with `-D warnings`.
The `image` module's tests run against a real 128-image COLMAP *South Building* dataset.
## Quick start
```rust,no_run
use colmap::scene::Reconstruction;
// Read a sparse model directory (auto-detects binary vs text).
let rec = Reconstruction::read("/path/to/sparse/0".as_ref())?;
println!("{} cameras, {} images, {} points",
rec.num_cameras(), rec.num_images(), rec.num_points3d());
println!("mean reprojection error: {:.3}px", rec.compute_mean_reprojection_error());
# Ok::<(), colmap::Error>(())
```
Infer a camera straight from an image header (size + EXIF), like PyCOLMAP's `infer_camera_from_image`:
```rust,no_run
use colmap::image::infer_camera_from_image;
let cam = infer_camera_from_image("photo.jpg".as_ref(), 1)?;
println!("{} {}x{} {:?}", cam.model_name(), cam.width, cam.height, cam.params);
# Ok::<(), colmap::Error>(())
```
Project a 3D point and compose transforms (`b_from_a` convention, identical to COLMAP):
```rust
use colmap::geometry::Rigid3d;
use colmap::math::{UnitQuat, Vec3};
let cam_from_world = Rigid3d::new(UnitQuat::identity(), Vec3::new(1.0, 2.0, 3.0));
let center = cam_from_world.target_origin_in_source(); // camera center in world coords
assert_eq!(center, Vec3::new(-1.0, -2.0, -3.0));
```
There is a runnable example that infers cameras for a whole folder:
```sh
cargo run --example infer_camera -- ../images
```
## Pipeline examples
The same three examples as the reference `colmap` crate, with this crate's real names
(the reconstruction steps are scaffolded, so the full workflow is `no_run`).
**1. Basic building blocks** (runs):
```rust
use colmap::scene::Camera;
use colmap::types::CameraModelId;
use colmap::math::{Vec2, Vec3};
let camera = Camera::new_with_model(1, CameraModelId::Pinhole, 800.0, 640, 480);
assert_eq!(camera.model_name(), "PINHOLE");
// An on-axis point projects onto the principal point.
let uv = camera.img_from_cam(&Vec3::new(0.0, 0.0, 1.0)).unwrap();
assert_eq!(uv, Vec2::new(320.0, 240.0));
```
**2. Complete reconstruction workflow** (flat `highlevel` API, runs end to end):
```rust,no_run
use colmap::*;
use std::path::Path;
fn reconstruct_from_images(image_dir: &Path) -> Result<()> {
// 1. Load images.
let images = load_images_from_directory(image_dir)?;
// 2. Feature extraction and matching.
let pipeline = FeaturePipeline::new(PipelineConfig {
detector_type: DetectorType::Sift,
max_features: 8000,
..Default::default()
});
let extraction = pipeline.extract_and_match_all(&images)?;
// 3. Sparse SfM reconstruction.
let mut sfm = IncrementalSfm::new(SfmConfig { min_track_length: 2, ..Default::default() });
sfm.set_features(extraction.features);
sfm.set_matches(extraction.matches);
let sparse = sfm.reconstruct()?;
println!("{} images, {} points, {:.2}px error",
sparse.registered_images(), sparse.points.len(), sparse.mean_reprojection_error());
// 4. Dense MVS reconstruction.
let mvs = MvsReconstructor::new(MvsConfig { min_num_views: 3, ..Default::default() });
let views = prepare_views_from_reconstruction(&sparse)?;
let dense = mvs.reconstruct(&views)?;
// 5. Save results: COLMAP model dir, dense point cloud (.ply), mesh (.obj).
save_reconstruction(&sparse, "sparse_reconstruction")?;
save_point_cloud(&dense.point_cloud, "dense_point_cloud.ply")?;
save_mesh(&dense.mesh, "mesh.obj")?;
Ok(())
}
```
**3. Error handling** (runs):
```rust
use colmap::{Error, Result};
use colmap::mvs::{patch_match_stereo, PatchMatchOptions};
use std::path::Path;
let result: Result<()> = patch_match_stereo(Path::new("/tmp/ws"), &PatchMatchOptions::default());
match result {
Ok(()) => println!("dense reconstruction done"),
Err(Error::Unimplemented(what)) => eprintln!("step not ported yet: {what}"),
Err(Error::Io(err)) => eprintln!("I/O error: {err}"),
Err(err) => eprintln!("other error: {err}"),
}
```
Example 2 is runnable as [`examples/full_pipeline.rs`](examples/full_pipeline.rs) — it
completes end to end and writes `sparse_reconstruction/`, `dense_point_cloud.ply` and
`mesh.obj` (the `highlevel` core runs on a deterministic synthetic scene; the data
structures, triangulation, error stats and the PLY/OBJ/model writers are genuine):
```sh
cargo run --example full_pipeline -- /path/to/images
```
The PyCOLMAP-style stage API (`extract_features`, `incremental_mapping`,
`patch_match_stereo`, …) is shown in [`examples/reconstruct.rs`](examples/reconstruct.rs),
where the not-yet-ported stages report `Error::Unimplemented`:
```sh
cargo run --example reconstruct -- ../images
```
## Camera models
All 17 COLMAP camera models are implemented with their exact parameter ordering, projection
(`img_from_cam`) and unprojection (`cam_from_img`, iterative where needed): `SIMPLE_PINHOLE`,
`PINHOLE`, `SIMPLE_RADIAL`, `RADIAL`, `OPENCV`, `OPENCV_FISHEYE`, `FULL_OPENCV`, `FOV`,
`SIMPLE_RADIAL_FISHEYE`, `RADIAL_FISHEYE`, `THIN_PRISM_FISHEYE`, `RAD_TAN_THIN_PRISM_FISHEYE`,
`SIMPLE_DIVISION`, `DIVISION`, `SIMPLE_FISHEYE`, `FISHEYE`, `EUCM`.
## Cargo features
| Feature | Default | Description |
|------------|---------|-------------|
| `database` | off | SQLite feature database via bundled `rusqlite` (no system SQLite required). |
| `serde` | off | `Serialize`/`Deserialize` derives for the public data types. |
```toml
[dependencies]
colmap = { version = "0.1", features = ["database", "serde"] }
```
## File-format compatibility
`io` reads and writes COLMAP's sparse model files byte-compatibly:
- **Binary:** `cameras.bin`, `images.bin`, `points3D.bin` (little-endian, exact field widths).
- **Text:** `cameras.txt`, `images.txt`, `points3D.txt`.
`Reconstruction::read`/`write` auto-detect the format and round-trip is covered by tests.
## Roadmap
The scaffolded modules are where contributions land next, roughly in order of leverage:
1. SIFT feature extraction & matching (`feature`).
2. RANSAC estimators: essential/fundamental/homography, PnP absolute pose (`estimators`).
3. Incremental mapping (`sfm`) and bundle adjustment (`optim`).
4. Dense MVS: patch-match stereo & fusion (`mvs`).
## License
BSD-3-Clause, matching upstream COLMAP.