Skip to content

The viewer

sfmview is a web viewer for sfmkit runs: both reconstructions in 3D, the old photograph as each of them placed it, COLMAP's dense cloud, and — while reconstruct works — the model growing step by step, with a timeline to rewind it.

It is its own package, with its own dependencies and its own image, and it does not import sfmkit. The two share a set of files and a message format, and nothing else.

The five things the viewer is for, in order: the model and its cameras, COLMAP's dense cloud, a camera opened with its photograph faded over the points, both models in one frame and at one scale, and the old photograph placed against the finished model.

Its look is a dense tool panel in the manner of Blender and Rerun: neutral greys, square corners, hairlines, figures in monospace. One signal colour, international orange, marks what is selected, current or live; the data keep their own — amber for sfmkit, blue for COLMAP.

Running it

conda activate sfmview
sfmview --projects projects --data data      # http://127.0.0.1:8000
Option Default Environment
--projects projects sfmkit's projects: a run and its photographs are both inside one SFMVIEW_PROJECTS
--host 127.0.0.1 address to listen on; 0.0.0.0 in a container SFMVIEW_HOST
--port 8000 SFMVIEW_PORT
--broker — Redis URL for live progress SFMVIEW_BROKER

With Docker: Install with Docker.

What it shows

  • Runs, with their stages, their metrics, where each stage ran (CPU or GPU), and whether one is live right now.
  • Both models in one frame: sfmkit's and COLMAP's, aligned the way evaluate aligns them, so what is drawn is what is scored.
  • The old photograph, in its own colour, as sfmkit placed it and as COLMAP did — the disagreement between the two is visible rather than a number in a table.
  • The photographs themselves: hover a camera to see which one it is, click it to look through it, with its frustum drawn to the scene and its reach and transparency under your hand.
  • The dense cloud, when the run has one.

One run, at the speed it happened: verify counting RANSAC down the right-hand pane, then reconstruct putting a camera into the left-hand one at a time.

Live progress

Pointed at the same Redis that sfmkit publishes to, the viewer follows a run as it is built: each camera appears as it is registered, and the timeline can be dragged back through the steps. A run at work is marked live in the list; one whose heartbeat stops before its end — sfmkit killed, or the broker lost — is no longer shown as live.

The messages are documented in Live messages.

What the viewer reads

The contract with sfmkit, and the whole of it. A run is projects/<name>/runs/<config>/, one directory per stage; the viewer only reads, and only these:

File Stage What it takes
*/manifest.json every stage stage, timestamp, config (sfm.reference, localize.query), device where a stage records one; from evaluate's, the metrics
reconstruct/reconstruction.npz reconstruct K, image_names, rotations, translations, points, and colors if present
localize/query_pose.npz localize R, t, and K if present
colmap/{cameras,images,points3D}.txt colmap COLMAP's text model, image names without extension
evaluate/evaluation.json evaluate reference, scale_image
dense/fused.ply dense served as it is; the browser parses it

That table is not the contract itself: sfmcontracts/run.schema.json is, and both packages' tests check themselves against it — sfmkit that it writes those arrays with those shapes, the viewer that it is given them. A run missing some of these shows what it has. Outputs of localize and evaluate older than the reconstruction are ignored: they describe the one before.

Inside

Ports and adapters, checked by import-linter rather than merely described:

src/sfmview/
├── domain/      values (RunId, Camera, Model, Scene) and frames.py; no I/O
├── ports.py     RunStore and StepSource: the interfaces the API needs
├── adapters/    runs_fs.py, colmap_text.py, sfmkit_files.py,
│                redis_steps.py and memory_steps.py
├── api/         create_app(store, steps), the routes, the WebSocket, the schemas
├── web/         index.html, style.css, js/{main,api,live,ui,scene,geometry}.js
└── main.py      the composition root: settings, adapters, uvicorn

The API sees the ports and never an adapter, so its tests run against a store and a stream in memory; another source of runs, or another broker, is a new adapter and nothing else. Both StepSources pass one suite — Redis's when a server is given, as CI does.