"You spent four sections teaching me to find tracks, register cameras, triangulate points, and balance the whole thing with bundle adjustment. Adorable. There is a button for that. It is called
A Reconstruction Pipeline That Has Seen Every Tutorial Ever Writtencolmap automatic_reconstructor, and it has read your homework."
COLMAP packages everything the previous four sections built by hand, feature tracks, two-view seeding, incremental registration, triangulation, and bundle adjustment, into a tool you drive with a few commands. Understanding the pieces is exactly what lets you read its output, diagnose its failures, and upgrade it. This section runs the production pipeline: the COLMAP graphical and command-line workflows, the SQLite database and the binary model files it writes, the learned-feature swap with hloc that rescues the hard image pairs of Chapter 10, the global solver GLOMAP that trades the slow incremental loop of Section 14.2 for a single fast estimate, and the dense and neural reconstructions, multi-view stereo, NeRF, and Gaussian splatting, that consume the poses this chapter produces. By the end you can turn a folder of photographs into a posed, point-rich 3D model and know which knob to turn when it comes out wrong.
The last four sections were a from-scratch education: Section 14.1 chained matches into tracks, Section 14.2 grew a model camera by camera, Section 14.3 polished it with bundle adjustment, and Section 14.4 turned the same machinery into a real-time map. That education was not wasted effort to be replaced by a library call; it is the only thing that makes the library call diagnosable. When COLMAP returns a reconstruction that has split into two disconnected pieces, or registers eleven of your forty images and gives up, the fix is never in the documentation. It is in knowing that two-view seeding needs a wide baseline, that PnP registration needs enough existing points in view, and that a drifting model is a bundle-adjustment-and-track-length problem. This section is the bridge from the textbook pipeline to the one practitioners actually run, and it keeps one foot on each side of that bridge.
1. COLMAP: The Pipeline in Five Commands Beginner
COLMAP (Schönberger and Frahm, "Structure-from-Motion Revisited," CVPR 2016) is the default open-source structure-from-motion tool, and its design follows the chapter exactly. It runs in three logical stages: feature extraction (detect and describe keypoints in every image, the SIFT of Chapter 10), matching (find correspondences between image pairs and verify them with the two-view geometry of Chapter 13), and mapping (the incremental loop of Section 14.2 with the bundle adjustment of Section 14.3 folded in). Figure 14.5.1 lays the stages out alongside the section that taught each one.
The fastest path is the one-shot driver, which chooses sensible defaults and runs all three stages. Given a directory project/images/, the entire reconstruction is one command:
# One-shot reconstruction. COLMAP picks GPU SIFT, exhaustive matching for small
# sets, and writes everything under project/.
colmap automatic_reconstructor \
--workspace_path project \
--image_path project/images
# project/ now contains:
# database.db SQLite: keypoints, descriptors, matches, two-view geometries
# sparse/0/{cameras,images,points3D}.bin the reconstructed model(s)
# dense/ (if dense stereo ran) depth maps + fused point cloud
automatic_reconstructor wraps the same extract / match / map stages the chapter built by hand; the multiple sparse/N/ folders appear only when the image set splits into separate connected components, the productionized form of the disconnected-graph problem from Section 14.1.
For anything beyond a quick look you run the stages explicitly, because each exposes parameters that decide success. The matcher choice is the most consequential: exhaustive_matcher compares every pair (quadratic, fine for tens of images), sequential_matcher matches each frame to its temporal neighbors plus loop-closure candidates (ideal for video, echoing Section 14.4), and vocab_tree_matcher uses a visual vocabulary to propose likely pairs for large unordered collections, the "Building Rome in a Day" idea from Section 14.1.
# Explicit five-command pipeline, the form you actually tune.
colmap feature_extractor \
--database_path project/database.db \
--image_path project/images \
--SiftExtraction.estimate_affine_shape 1 # better repeatability under viewpoint change
colmap sequential_matcher \ # use exhaustive_ or vocab_tree_ for unordered sets
--database_path project/database.db \
--SequentialMatching.loop_detection 1 # enable loop closure for video
colmap mapper \
--database_path project/database.db \
--image_path project/images \
--output_path project/sparse \
--Mapper.ba_global_function_tolerance 1e-6 # bundle-adjustment convergence (Section 14.3)
colmap model_converter \ # export to a human-readable text model
--input_path project/sparse/0 \
--output_path project/sparse_text \
--output_type TXT
colmap subcommand is one of the chapter's stages; the flags that matter most are the matcher choice (exhaustive / sequential / vocab-tree, depending on whether the images are ordered) and the mapper's bundle-adjustment tolerances, which trade reconstruction time against final reprojection error.Every COLMAP failure maps onto a concept from this chapter. A reconstruction that registers a fraction of the images has a track-and-matching problem (Section 14.1): the unregistered images share too few verified correspondences with the model. A model that splits into two sparse/N/ folders has a disconnected view graph. A banana-shaped reconstruction of a straight corridor is drift, the bundle-adjustment-and-loop-closure issue of Sections 14.3 and 14.4. A reconstruction that collapses to a plane chose a degenerate seed pair. The tool is a fast, well-tuned implementation of the pipeline you now understand; debugging it is applied chapter content, not documentation archaeology.
2. Reading the Database and the Model Intermediate
COLMAP's two outputs are a SQLite database and a reconstruction model, and reading them directly turns the tool from a black box into an instrument. The database (database.db) holds everything from the front end: a cameras table of intrinsic models, an images table, a keypoints table (one blob of detected points per image), a descriptors table, a matches table (raw putative matches per pair), and a two_view_geometries table (the inlier matches that survived geometric verification, plus the fundamental, essential, or homography matrix). That last table is the chapter's correspondence-and-RANSAC pipeline frozen on disk.
The model is three binary files in sparse/0/: cameras.bin (intrinsics), images.bin (one entry per registered image: its quaternion-and-translation pose plus the 2D-to-3D observations), and points3D.bin (each triangulated point's XYZ, color, reprojection error, and the track of images that see it, the track from Section 14.1 made concrete). COLMAP ships pycolmap, a Python binding that reads both without you parsing the binary format. The following listing opens a finished model and computes the diagnostics every reconstruction should be checked against.
import pycolmap
import numpy as np
# Load a finished reconstruction (the sparse/0 folder COLMAP wrote).
rec = pycolmap.Reconstruction("project/sparse/0")
print(rec.summary()) # quick stats: #images registered, #points, mean track length
# How many images actually got registered, and which were dropped?
n_reg = rec.num_reg_images()
print(f"registered {n_reg} images, {len(rec.points3D)} points")
# Track length: the count of images observing each 3D point. Short tracks
# (seen by only 2 cameras) are poorly constrained; long tracks are gold.
track_lengths = np.array([p.track.length() for p in rec.points3D.values()])
print(f"mean track length {track_lengths.mean():.2f}, "
f"fraction length-2 {np.mean(track_lengths == 2):.2%}")
# Reprojection error: the bundle-adjustment residual (Section 14.3) per point.
errors = np.array([p.error for p in rec.points3D.values()])
print(f"mean reprojection error {errors.mean():.3f} px") # healthy models: well under 1 px
# Per-image camera centers, for plotting the trajectory or a top-down map.
centers = np.array([img.projection_center() for img in rec.images.values()])
print("scene extent (m, in the model's arbitrary scale):",
centers.max(0) - centers.min(0))
pycolmap. The four printed diagnostics, registration count, track-length distribution, mean reprojection error, and scene extent, are the health check for any reconstruction; a model with many length-2 tracks and a sub-pixel mean error is well constrained, while a high registration drop-out signals a matching problem upstream.
A typical healthy run on a 40-image object scan prints something like registered 40 images, 18204 points, a mean track length near 4, a length-2 fraction below 25 percent, and a mean reprojection error around 0.6 px. Those numbers are the quantitative version of "the bundle adjustment converged and the tracks are long." When the registration count is far below the image count, the cure is upstream, in matching, which is exactly where the next subsection improves the tool.
pycolmap exposes not just the readers above but the entire reconstruction pipeline as functions. The hand-built incremental mapper of Section 14.2 (several hundred lines of seeding, PnP registration, triangulation, and filtering) collapses to roughly five calls: pycolmap.extract_features(db, image_dir), pycolmap.match_exhaustive(db), then pycolmap.incremental_mapping(db, image_dir, output_dir), with optional pycolmap.triangulate_points(...) and pycolmap.bundle_adjustment(...). The library handles the database schema, GPU SIFT, the geometric-verification RANSAC, the next-best-view scheduling, robust triangulation, and the sparse Schur-complement bundle adjustment (Section 14.3) internally. You write five lines; COLMAP runs the thousands that took the field two decades to get right.
3. Upgrading the Front End: Learned Features with hloc Advanced
COLMAP's classical SIFT front end is excellent on well-textured, well-lit, densely-overlapping image sets and fragile everywhere else: low texture, repetitive structure, day-to-night or seasonal change, and especially wide baselines, the cases where the hand-crafted descriptors of Chapter 10 were always going to struggle. The modern fix is to keep COLMAP's mapper (the geometry is sound) and replace only the front end with learned detection and matching. The Hierarchical Localization toolbox, hloc, is the standard wrapper for doing exactly that. It detects keypoints with SuperPoint, matches them with SuperGlue or the lighter LightGlue, optionally proposes image pairs with a global retrieval network (NetVLAD), and feeds the verified matches into COLMAP's database for triangulation and mapping.
The architectural point is the same as Section 14.4's "geometry is solved, learning replaces perception": the bundle adjustment, triangulation, and pose estimation are unchanged classical machinery, while the perception layer (what is a keypoint, do these two patches match) becomes a network trained on the failures that broke SIFT. The result is dramatically higher registration rates on hard scenes for a few extra lines of orchestration.
from pathlib import Path
from hloc import extract_features, match_features, reconstruction, pairs_from_retrieval
images = Path("project/images")
outputs = Path("project/hloc")
# Pre-defined configs select the network and its weights.
retrieval_conf = extract_features.confs["netvlad"] # global descriptor for pair proposal
feature_conf = extract_features.confs["superpoint_aachen"] # learned local keypoints
matcher_conf = match_features.confs["superglue"] # learned matcher (or "lightglue")
# 1. Global descriptors, then propose the 20 most similar pairs per image
# (the learned analogue of vocab-tree matching from Section 14.1).
retrieval_path = extract_features.main(retrieval_conf, images, outputs)
sfm_pairs = outputs / "pairs-netvlad.txt"
pairs_from_retrieval.main(retrieval_path, sfm_pairs, num_matched=20)
# 2. Learned local features and matches.
feature_path = extract_features.main(feature_conf, images, outputs)
match_path = match_features.main(matcher_conf, sfm_pairs, feature_conf["output"], outputs)
# 3. Hand the verified matches to COLMAP's incremental mapper unchanged.
model = reconstruction.main(outputs / "sfm", images, sfm_pairs, feature_path, match_path)
print(model.summary()) # same pycolmap Reconstruction object as Section 2
hloc. Only the perception layer changed; the output is the identical pycolmap.Reconstruction from the previous subsection, so the same health checks apply. On wide-baseline or appearance-changing scenes this routinely lifts registration from a minority of images to nearly all of them.Who: The lead reconstruction engineer on a cultural-heritage digitization team at a museum-services nonprofit, working with a conservation archivist.
Situation: They were digitizing a Gothic cathedral interior from 1,200 crowd-sourced visitor photographs spanning a decade, shot on different cameras across different seasons, with harsh window backlight against deep-shadow vaulting.
Problem: Stock COLMAP with SIFT registered only 430 of the 1,200 images and produced three disconnected models: the nave, a chapel, and a confused blob of the rose window. The engineer's first instinct, throwing more photographs at the pipeline, only produced a fourth disconnected model.
Dilemma: Three paths competed. Recapturing the interior on a controlled photo shoot would guarantee overlap but cost a week of scaffolded access the cathedral would not grant again. Hand-seeding image pairs to stitch the fragments was free but would take days of tedious clicking with no guarantee of success. Swapping the matching front end promised a fix but meant trusting a learned matcher nobody on the team had run in production.
Decision: They first ran a diagnosis: reading the COLMAP database showed the unregistered images had plenty of detected keypoints but almost no verified matches, which pointed at the matcher, not at coverage. That made the front-end swap the obvious, lowest-cost fix.
How: They replaced SIFT with SuperPoint + SuperGlue through hloc, using NetVLAD to propose cross-appearance pairs, then handed the verified matches to the same COLMAP mapper unchanged on the same hardware. SIFT could not match a sunlit afternoon photo of a pillar to a flash-lit evening photo of the same pillar; the learned matcher could.
Result: Registration jumped from 430 to 1,150 of the 1,200 images, collapsing into a single connected model, with no new captures and roughly a day of integration work.
Lesson: When a reconstruction fragments, read the database first: missing images that lack correspondences need a front-end fix, while those that lack overlap need a capture fix, and only the verified-match counts tell you which problem you actually have.
4. Global SfM and the Neural Hand-off Advanced
Incremental SfM (Section 14.2) is accurate but slow: it solves a fresh bundle adjustment after every camera, so cost grows steeply with image count. Global SfM takes a different route. It first estimates all relative rotations and translations from the pairwise two-view geometries, then solves two global problems, a rotation averaging that makes every pairwise rotation mutually consistent and a translation averaging (and triangulation) that fixes the global geometry, finishing with a single bundle adjustment over everything at once. Historically global methods were faster but more fragile, especially in translation averaging. GLOMAP (Pan et al., ECCV 2024) closed that gap: it reuses COLMAP's database and front end, runs a robust global solver, and produces reconstructions of comparable accuracy to incremental COLMAP while being an order of magnitude or more faster on large scenes.
# GLOMAP consumes a COLMAP database (same feature_extractor + matcher as Section 1),
# then solves the whole reconstruction globally instead of camera-by-camera.
colmap feature_extractor --database_path project/database.db --image_path project/images
colmap exhaustive_matcher --database_path project/database.db
glomap mapper \
--database_path project/database.db \
--image_path project/images \
--output_path project/sparse_glomap # writes the same cameras/images/points3D format
pycolmap and the Section 2 diagnostics work unchanged; the difference is internal, a global rotation-and-translation solve replacing the incremental loop, which on large unordered collections is the difference between hours and minutes.
Whichever solver produces it, the sparse model is rarely the final deliverable. A sparse cloud of triangulated keypoints is enough to localize cameras but too thin to look like a scene. Two routes densify it. The classical route is multi-view stereo (MVS): COLMAP's patch_match_stereo estimates a depth map per image by the dense matching ideas of Chapter 13 generalized from two views to many, then stereo_fusion merges them into a dense point cloud, which meshing turns into a textured surface. The modern route hands the poses to a neural scene representation, and this is the chapter's most important forward link.
Neural Radiance Fields (NeRF) and 3D Gaussian Splatting do not estimate camera poses; they assume them. The standard input to both is a set of images plus the exact camera intrinsics and extrinsics, which in practice means a COLMAP reconstruction. Every "drop your photos here" NeRF or splatting demo you have used runs COLMAP first, silently, to recover the poses this chapter taught you to compute. The classical geometry of Part II is therefore not superseded by the neural era; it is the load-bearing first stage of it. When a NeRF comes out blurry, the cause is very often bad poses, which is a structure-from-motion problem you can now diagnose with pycolmap. The illustration below casts that hand-off as a transaction.
The hand-off is mechanical. NeRF (Mildenhall et al., 2020) trains a small network to map a 3D position and viewing direction to color and density, optimizing it so that volume-rendering the network along camera rays reproduces the input images; 3D Gaussian Splatting (3DGS) (Kerbl et al., SIGGRAPH 2023) replaces the network with millions of explicit oriented Gaussians that rasterize in real time, and crucially initializes them from the COLMAP sparse point cloud, so this chapter's points3D.bin is literally the splatting model's starting state. Both are the subject of Chapter 27; here it is enough to see that the COLMAP folder feeds them directly.
# The neural hand-off in its most common form: convert a COLMAP model into the
# transforms.json that NeRF / Gaussian-splatting trainers (e.g. nerfstudio) expect.
import pycolmap, numpy as np, json
rec = pycolmap.Reconstruction("project/sparse/0")
frames = []
for img in rec.images.values():
# COLMAP stores world-to-camera; renderers want camera-to-world (the inverse).
world_to_cam = img.cam_from_world.matrix() # 3x4 [R | t]
T = np.eye(4); T[:3, :4] = world_to_cam
cam_to_world = np.linalg.inv(T)
frames.append({"file_path": f"images/{img.name}",
"transform_matrix": cam_to_world.tolist()})
cam = next(iter(rec.cameras.values())) # shared intrinsics (Chapter 12)
meta = {"fl_x": cam.focal_length_x, "fl_y": cam.focal_length_y,
"cx": cam.principal_point_x, "cy": cam.principal_point_y,
"w": cam.width, "h": cam.height, "frames": frames}
json.dump(meta, open("project/transforms.json", "w"), indent=2)
print(f"wrote {len(frames)} posed frames for neural training")
The newest work attacks the entire pipeline of this chapter with a single feed-forward network, no incremental loop, often no calibration, sometimes no poses at all. DUSt3R (Wang et al., CVPR 2024) and its follow-up MASt3R regress aligned pointmaps directly from an uncalibrated image pair, recovering depth, intrinsics, and relative pose in one pass. VGGT (Wang et al., CVPR 2025, the 2025 best-paper) extends this to hundreds of views, predicting cameras, depth, and 3D points in a single transformer forward pass that takes seconds where COLMAP takes minutes. MASt3R-SfM and follow-ons feed these predictions into a lightweight global optimizer for COLMAP-quality models without SIFT. On the rendering side, GLOMAP-plus-3DGS and pose-free splatting methods are collapsing the "estimate poses then train a renderer" two-step into one. The throughline for a reader of this chapter: these systems do not abolish structure from motion, they amortize it; they learn, from millions of scenes, a fast approximation of the very geometry, correspondence, triangulation, pose, and bundle adjustment, that Sections 14.1 through 14.4 derived from first principles. Understanding the classical pipeline is what lets you read their failure modes and know when the trustworthy slow tool is still the right call.
The "transforms.json" pose file that nearly every NeRF and Gaussian-splatting codebase reads was popularized by NVIDIA's Instant-NGP, and its camera convention is subtly different from COLMAP's, OpenCV's, and Blender's. The resulting "my reconstruction is mirrored / upside down / inside out" bug is so common that converting between camera conventions is, by a wide margin, the most frequent question in NeRF community forums. Every one of those bugs is a sign-and-axis disagreement in exactly the extrinsic matrices this chapter taught you to build.
5. Choosing a Pipeline Intermediate
With several tools on the table, the practical question is which to reach for. The decision turns on three properties of your images: are they ordered (video or a planned capture) or an unordered pile; how many are there; and how hard is the appearance (texture, lighting, baseline). The table below summarizes the chapter's working advice.
| Situation | Reach for | Why |
|---|---|---|
| Tens of well-textured photos, good overlap | COLMAP, exhaustive matcher | SIFT is plenty; exhaustive matching is cheap at this scale and most thorough. |
| Ordered video / planned scan | COLMAP sequential matcher (or visual SLAM, Section 14.4) | Temporal order makes matching linear; loop detection closes drift. |
| Hundreds-plus unordered images | GLOMAP, or COLMAP vocab-tree | Global solve or vocab-tree pairing avoids quadratic matching and incremental slowdown. |
| Hard appearance: low texture, lighting / seasonal change, wide baselines | hloc (SuperPoint + SuperGlue / LightGlue) into COLMAP | Learned perception rescues the matches SIFT cannot make; geometry stays classical. |
| Need a photorealistic render, not just poses | COLMAP / GLOMAP poses, then 3DGS or NeRF (Chapter 27) | The sparse model and its point cloud initialize the neural renderer. |
| Seconds-scale preview, calibration unknown | VGGT / DUSt3R-family feed-forward models | One network pass; trade peak accuracy for speed and zero setup. |
Reading the table top to bottom traces the chapter's own arc: the classical tool first, then the upgrades that target a specific weakness (scale, perception, rendering, speed), each leaving the core geometry intact. That is the honest state of the field in 2026. The neural reconstructors are extraordinary and getting better monthly, but the structure-from-motion pipeline of this chapter remains the accuracy reference they are measured against and the silent first stage inside most of them.
The chapter's five verbs are easiest to feel when you drive them yourself. The Hands-On Lab at the end of this section runs all of them on a real image set with pycolmap, then reads back the same vital signs that Exercises 14.5.1 through 14.5.3 ask you to interpret on paper.
You run COLMAP on 60 photos and pycolmap reports: 38 images registered, mean track length 2.3, length-2 fraction 71 percent, mean reprojection error 1.9 px, and the model split into sparse/0/ (31 images) and sparse/1/ (7 images). Diagnose the reconstruction. Which two numbers are unhealthy and what does each indicate? Is the split more likely a matching failure or a coverage gap, and which single diagnostic from Section 2 would settle it? Propose the most likely single change (from Sections 1, 3, or 4) that would merge the model and lengthen the tracks.
Take a small image set with at least one hard pair (a strong viewpoint or lighting change). Reconstruct it twice: once with stock COLMAP (SIFT) and once with hloc (SuperPoint + SuperGlue) into COLMAP's mapper. For each, report registered-image count, mean track length, and mean reprojection error from pycolmap. Then identify, in the database's two_view_geometries table, the hard pair, and compare the number of verified inliers each front end produced on it. Write two sentences explaining the registration difference in terms of verified matches rather than detected keypoints.
You convert a COLMAP model to transforms.json using the Section 4 listing, train a Gaussian-splatting model, and the render comes out correctly shaped but mirrored left-to-right. The point cloud and reprojection errors from pycolmap are healthy. Where is the bug, in COLMAP, in your conversion, or in the renderer, and why do the healthy pycolmap diagnostics rule out two of the three? Name the specific operation in the conversion most likely responsible (hint: it is not the world-to-camera inversion, which is already in the listing) and explain why a mirrored-but-otherwise-correct result points squarely at a coordinate-convention disagreement rather than at the geometry.
Build one self-contained script, reconstruct.py, that takes a folder of photographs and runs the entire structure-from-motion pipeline of this chapter with pycolmap: extract and match features, run incremental mapping (seed, register, triangulate, bundle-adjust), then read back the reconstruction's vital signs and export camera poses plus a point cloud you can open in any 3D viewer. The output is a sparse 3D model of a real object you photographed yourself, the exact artifact a NeRF or Gaussian-splat trainer in Chapter 27 consumes as input.
What You'll Practice
- Driving the five-verb pipeline (track, seed, register, triangulate, adjust) from Python rather than the COLMAP GUI (Sections 14.1 through 14.3).
- Reading a reconstruction's vital signs: registered-image count, mean track length, length-2 fraction, and mean reprojection error (Sections 14.2 and 14.5).
- Inspecting and exporting camera intrinsics and extrinsics, the $K$ matrix and $[R \mid t]$ poses of Chapter 12.
- Diagnosing a multi-model split and capture-quality failures from numbers, not vibes.
Setup
pip install pycolmap numpy
You also need 20 to 40 overlapping photos of one rigid object or scene. Walk around a textured object (a building corner, a statue, a cluttered desk) taking a photo every 15 to 20 degrees with generous overlap, following the capture advice from Section 3 of this section. Put them all in a folder named images/. No GPU is required; pycolmap ships the full COLMAP engine as a Python wheel, so nothing else needs installing.
Put the section's reconstruction workflow into practice below. Work through the steps in order; each one prints a checkpoint so you can confirm progress before moving on. A complete reference solution is folded at the end.
Step 1: Set up the project and extract features
Create a COLMAP database and run the feature extractor over every image. This is the detect-and-describe front end of Chapter 10, the raw material every later verb consumes.
from pathlib import Path
import pycolmap
image_dir = Path("images")
work = Path("work"); work.mkdir(exist_ok=True)
db_path = work / "database.db"
# TODO: extract SIFT features from every image into db_path.
# Hint: pycolmap.extract_features(database_path=db_path, image_path=image_dir)
...
print("Features extracted into", db_path)
Hint
pycolmap.extract_features(database_path=db_path, image_path=image_dir) creates the database if it does not exist and fills the keypoints and descriptors tables. On a CPU-only machine it uses the SIFT implementation, no CUDA needed.
Step 2: Match features into a verified match graph
Turn per-image descriptors into geometrically verified pairwise matches: the edges of the match graph from Section 14.1. For a few dozen unordered photos, exhaustive matching is cheap and the most thorough choice.
# TODO: run exhaustive matching over the database you just built.
# Hint: pycolmap.match_exhaustive(database_path=db_path)
...
print("Matching done; the database now holds verified two-view geometries")
Hint
pycolmap.match_exhaustive(db_path) compares every image pair and runs RANSAC verification, populating the two_view_geometries table. If you captured an ordered video instead, pycolmap.match_sequential is the linear-cost alternative from the Section 5 table.
Step 3: Run incremental mapping
This single call performs the heart of the chapter: it seeds from one well-chosen pair, then loops registering each next camera with PnP, triangulating fresh points, and bundle-adjusting, exactly the recipe of Sections 14.2 and 14.3. It returns one reconstruction per connected component it could not merge.
sparse = work / "sparse"; sparse.mkdir(exist_ok=True)
# TODO: run incremental mapping and capture the returned dict of reconstructions.
# Hint: maps = pycolmap.incremental_mapping(db_path, image_dir, sparse)
maps = ...
print(f"Produced {len(maps)} reconstruction(s)")
Hint
pycolmap.incremental_mapping(database_path, image_path, output_path) returns a dict keyed by integer model id. A healthy single-component capture yields len(maps) == 1; more than one means the graph fragmented, the multi-model split that Exercise 14.5.1 asks you to diagnose.
Step 4: Read the reconstruction's vital signs
Pick the largest model and compute the four diagnostics this section trains you to read. Track length and reprojection error together tell you whether the geometry is trustworthy before you ever look at the point cloud.
rec = max(maps.values(), key=lambda r: r.num_reg_images())
track_lengths = [p.track.length() for p in rec.points3D.values()]
mean_track = sum(track_lengths) / len(track_lengths)
len2_frac = sum(t == 2 for t in track_lengths) / len(track_lengths)
# TODO: compute the mean reprojection error over all 3D points.
# Hint: each point exposes rec.points3D[id].error (in pixels)
mean_reproj = ...
print(f"Registered images : {rec.num_reg_images()}")
print(f"3D points : {rec.num_points3D()}")
print(f"Mean track length : {mean_track:.2f}")
print(f"Length-2 fraction : {len2_frac:.0%}")
print(f"Mean reproj error : {mean_reproj:.2f} px")
Hint
Average p.error across rec.points3D.values(). A healthy small reconstruction registers most of your images, shows a mean track length above 3, a length-2 fraction well under half, and a mean reprojection error under about 1 pixel.
Step 5: Inspect one camera's intrinsics and pose
Open one registered image and read the calibration the optimizer recovered: the focal length and principal point inside $K$, and the world-to-camera rotation and translation. These are the Chapter 12 quantities, now solved for from pixels alone.
img = next(iter(rec.images.values()))
cam = rec.cameras[img.camera_id]
print("Camera model :", cam.model.name)
print("Params (f, cx, cy, ...):", cam.params)
# TODO: print the 3x4 world-to-camera matrix for this image.
# Hint: img.cam_from_world.matrix() gives the [R | t] extrinsics
print("Pose [R | t]:\n", ...)
Hint
img.cam_from_world is a rigid transform; .matrix() returns the 3x4 array whose left block is $R$ and right column is $t$. This is the same world-to-camera convention Code Fragment 6 inverts when exporting to a neural renderer.
Step 6: Export poses and a point cloud you can open
Persist the model in COLMAP's binary format and write the sparse points to a PLY file any 3D viewer (MeshLab, CloudCompare, Blender) can open. This is the deliverable: a portable 3D reconstruction of your own scene.
model_out = work / "model"; model_out.mkdir(exist_ok=True)
rec.write(model_out) # cameras.bin, images.bin, points3D.bin
# TODO: export the sparse point cloud as a PLY file for a 3D viewer.
# Hint: rec.export_PLY(str(work / "points.ply"))
...
print("Wrote model to", model_out, "and points.ply")
Hint
rec.export_PLY(path) writes every triangulated point with its averaged color. Open points.ply and you should recognize the silhouette of the object you photographed, sparse but unmistakable.
Step 7: The Right Tool, the one-call automatic pipeline
You drove the verbs one at a time to see them; in production the same result is one function. Reproduce the whole script with COLMAP's bundled automatic reconstructor, mirroring this chapter's Right Tool principle.
# TODO: run the all-in-one automatic pipeline (extract, match, map) in one call.
# Hint: pycolmap.AutomaticReconstructor or the automatic_reconstruction helper
...
# Compare the registered-image count and mean reprojection error against your
# hand-driven run from Steps 1 through 4; they should match closely.
Hint
COLMAP exposes a one-shot automatic reconstruction that chains extraction, matching, and mapping with sensible defaults. The roughly thirty lines of Steps 1 through 6 collapse to a single call; the library picks the matcher, the seed pair, and the bundle-adjustment schedule for you. You wrote the long version so you can read its logs when the short version fails.
Expected Output
Steps 1 and 2 print progress as features are extracted and pairs verified. Step 3 reports the number of reconstructions, ideally one for a well-captured set. Step 4 prints a vital-signs block resembling: roughly 30 of 35 images registered, several thousand 3D points, mean track length around 4 to 6, length-2 fraction under 25 percent, and mean reprojection error near 0.6 to 0.9 px. Step 5 shows a sensible focal length in pixels and a 3x4 pose matrix. Step 6 writes points.ply; opening it reveals a sparse but recognizable 3D outline of your object. If instead you see many images unregistered, a track length near 2, or a multi-model split, you have reproduced the failure modes of Exercise 14.5.1, and the fix is almost always capture (more overlap, more texture, less motion blur), not code.
Stretch Goals
- Deliberately undercapture: shoot the same object with only 8 widely spaced photos, rerun, and watch the track length collapse and the model fragment. Then add intermediate views until it merges, building intuition for the overlap requirement of Section 14.1.
- Swap the SIFT front end for learned features by running the same images through hloc (SuperPoint + LightGlue) into COLMAP's mapper, and compare verified-inlier counts on your hardest pair, the head-to-head of Exercise 14.5.2.
- Convert the exported model to a
transforms.jsonwith the Code Fragment 6 listing and train a Gaussian-splatting model from Chapter 27 on your own reconstruction, closing the loop from pixels to a photorealistic render.
Complete Solution
from pathlib import Path
import pycolmap
image_dir = Path("images")
work = Path("work"); work.mkdir(exist_ok=True)
db_path = work / "database.db"
# Step 1: feature extraction (detect + describe, Chapter 10 front end)
pycolmap.extract_features(database_path=db_path, image_path=image_dir)
# Step 2: exhaustive matching with RANSAC verification (Section 14.1 match graph)
pycolmap.match_exhaustive(database_path=db_path)
# Step 3: incremental mapping = seed, register, triangulate, adjust (14.2 + 14.3)
sparse = work / "sparse"; sparse.mkdir(exist_ok=True)
maps = pycolmap.incremental_mapping(db_path, image_dir, sparse)
print(f"Produced {len(maps)} reconstruction(s)")
# Step 4: vital signs of the largest model
rec = max(maps.values(), key=lambda r: r.num_reg_images())
track_lengths = [p.track.length() for p in rec.points3D.values()]
mean_track = sum(track_lengths) / len(track_lengths)
len2_frac = sum(t == 2 for t in track_lengths) / len(track_lengths)
mean_reproj = sum(p.error for p in rec.points3D.values()) / rec.num_points3D()
print(f"Registered images : {rec.num_reg_images()}")
print(f"3D points : {rec.num_points3D()}")
print(f"Mean track length : {mean_track:.2f}")
print(f"Length-2 fraction : {len2_frac:.0%}")
print(f"Mean reproj error : {mean_reproj:.2f} px")
# Step 5: one camera's intrinsics and pose (Chapter 12 quantities, solved from pixels)
img = next(iter(rec.images.values()))
cam = rec.cameras[img.camera_id]
print("Camera model :", cam.model.name)
print("Params (f, cx, cy, ...):", cam.params)
print("Pose [R | t]:\n", img.cam_from_world.matrix())
# Step 6: export model and a PLY point cloud
model_out = work / "model"; model_out.mkdir(exist_ok=True)
rec.write(model_out)
rec.export_PLY(str(work / "points.ply"))
print("Wrote model to", model_out, "and points.ply")
# Step 7: the Right Tool, one-call automatic reconstruction
auto = work / "auto"; auto.mkdir(exist_ok=True)
pycolmap.AutomaticReconstructor(
pycolmap.AutomaticReconstructionOptions(
workspace_path=str(auto),
image_path=str(image_dir),
dense=False, # sparse model only; no GPU needed
)
).run()
print("Automatic pipeline finished; compare its sparse/ model to your hand-driven run")