223 lines
9.4 KiB
Markdown
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.
|