Live messages¶
While reconstruct works it publishes what it has, step by step, and the
viewer draws it as it arrives. Neither knows the other: sfmkit writes to a
Redis stream, the viewer reads from it, and the message format between them is
a schema both test against — sfmcontracts/step.schema.json.
A change on one side breaks a test on the other, not a run.
# sfmkit publishes when this names a Redis
SFMKIT_BROKER=redis://localhost:6379 sfmkit reconstruct --config projects/valencia/configs/cpu.yaml
# the viewer reads when this names the same one
sfmview --projects projects --broker redis://localhost:6379
With Docker both are set for you; see Install with Docker.
How it goes¶
sequenceDiagram
autonumber
participant K as sfmkit run
participant R as Redis
participant V as sfmview
participant B as the browser
B->>V: GET /api/runs
B->>V: WS /api/projects/valencia/runs/cpu/live
V->>R: XRANGE (history)
R-->>V: what the last run left, if anything
V-->>B: {"now": "<stream id>"}, then each entry
K->>R: DEL, then XADD stage match start
Note over K,R: the first message of a run empties the stream
R-->>V: stage match start
V-->>B: relayed unchanged
K->>R: SET alive, renewed every 5 s
K->>R: XADD stage match end (41.2 s)
K->>R: XADD start (K, images, reference)
loop one per camera registered
K->>R: XADD step (cameras and points as they stand)
R-->>V: step
V-->>B: relayed
end
K->>R: XADD end, EXPIRE in a week
K->>R: DEL alive
R-->>V: end
V-->>B: relayed
B->>V: GET .../scene (the files are complete now)
Nothing in that diagram is a request from one program to the other: sfmkit writes to Redis and the viewer reads from it, and neither would notice the other being replaced.
The keys¶
| Key | What it is |
|---|---|
sfmkit:steps:<project>/<config> |
the stream, one JSON message per entry, in the entry's data field |
sfmkit:alive:<project>/<config> |
the heartbeat: set while the run works, 15 s expiry, renewed every 5 s |
The stream is named after the run's directory, exactly as the viewer names
runs, so --out cannot write into another run's stream. It is trimmed to a
thousand entries — far more than any run's steps — and emptied by the first
message of a run, so a repeat replaces its predecessor rather than appending
to it. The first and not the start, because a whole run speaks about its
earlier stages before the reconstruction opens, and those would be wiped
mid-run.
A finished run's stream stays, which is what lets its timeline be rewound after
the fact, but not for ever: the end (or the failed) sets an expiry of a
week on it. A broker that saw many runs and never forgot one would only grow.
The heartbeat is the difference between a run at work and a run that died without a word: killed outright, sfmkit stops renewing the key and Redis lets it expire, so the viewer stops calling it live.
The messages¶
Five kinds, all carrying v (the version, 1), kind, and run
(<project>/<config>). Poses are world to camera, x_cam = R X + t, in
OpenCV's axes; point arrays are flat and finite-only; any number that is not
finite is sent as null where the schema allows it.
stage¶
A stage of a whole run beginning, finishing, or stopping it. Only
reconstruct has anything to draw; the rest are silent for minutes at a time,
and a page watching a run should not have to guess whether it is matching or
dead.
{"v": 1, "kind": "stage", "run": "valencia/cpu", "stage": "match",
"state": "end", "seconds": 41.2, "note": "92 pairs on cuda"}
| Field | |
|---|---|
stage |
calibrate, match, verify, reconstruct, localize, colmap, dense, evaluate, changes or figures |
state |
start, end or failed |
seconds |
what it took, on end or failed |
note |
a line of its own: why it stopped, or what it did |
Only sfmkit run sends these: a single stage invoked on its own is its own
whole run, and says nothing about the others.
start¶
The run opening: what it will try to register, and with what camera. The stream is emptied immediately before this is written.
{
"v": 1, "kind": "start", "run": "valencia/cpu",
"K": [[3029.0, 0.0, 2016.0], [0.0, 3029.0, 1134.0], [0.0, 0.0, 1.0]],
"images": ["Img01", "Img02", "Img03"],
"reference": "Img01"
}
reference is the camera the finished model will be anchored to. A watcher
needs it to draw the steps where the finished model will sit: until there is
one, it has no transform to take, and would otherwise place the model in the
seed pair's frame and have it jump when the run ends.
step¶
One per camera registered, and one for the final global refinement, whose
image is the string global refinement rather than a photograph's name. It
carries the whole model as it stood — the poses of every registered camera and
every triangulated point — so a viewer that arrives late can draw any step
without replaying the ones before it.
{
"v": 1, "kind": "step", "run": "valencia/cpu",
"step": 0, "image": "Img14",
"n_registered": 2, "n_points": 446,
"rmse_before": null, "rmse_after": 2.22,
"bundle_seconds": 0.4,
"cameras": [
{"name": "Img14", "R": [[1,0,0],[0,1,0],[0,0,1]], "t": [0,0,0]},
{"name": "Img04", "R": [["…"]], "t": [-1.0, 0.0, 0.0]}
],
"points": [0.1, 0.2, 5.0, -0.3, 0.1, 6.0]
}
| Field | |
|---|---|
step |
0 for the seed pair, then one per camera |
image |
the camera just added, or global refinement |
n_registered, n_points |
the model's size after this step |
rmse_before, rmse_after |
reprojection error in pixels, around this step's bundle adjustment; null where there is nothing to report |
bundle_seconds |
what that bundle adjustment took |
cameras |
every registered camera, name, R, t |
points |
flat x y z, the triangulated points only, at most 20 000 of them |
A step's points are capped. Carrying the whole model is what lets any step
be drawn without replaying the ones before it, but it costs steps times points
of the broker's memory, and a step is kept for as long as the stream is. Past
20 000 points sfmkit sends a stride through them instead — the whole model
thinned, not a corner of it — while n_points goes on reporting the true
count. Valencia, at 2 830 points, never reaches it; a few hundred cameras would.
end¶
The run finished, with what it ended up with. For the viewer this also means the files on disk are now complete, so it refetches them.
failed¶
The run stopped short, with the reason — an interruption included, so Ctrl-C leaves a stream that says what happened rather than one that simply stops.
The contract, as a file¶
The message format is sfmcontracts/step.schema.json, which both test
suites validate against — a change on one side breaks a test on the other. What
JSON Schema cannot say is who publishes what, where, and who listens; that is
sfmcontracts/asyncapi.yaml, an AsyncAPI 3 document that names the
two channels (the stream and the heartbeat), the WebSocket the viewer relays
them on, and points each message at the schema rather than repeating it.
It is the same idea as the OpenAPI that FastAPI serves for the HTTP side at
/docs: the interface written down in the form its tools understand,
so it can be read, diffed and generated from.
Reading them yourself¶
The messages are plain JSON on a plain Redis stream, so anything that speaks Redis can watch a run:
redis-cli XRANGE sfmkit:steps:valencia/cpu - + COUNT 1
redis-cli EXISTS sfmkit:alive:valencia/cpu # 1 while the run is at work
Or through the viewer, which needs no Redis client of your own:
WS /api/runs/{project}/{config}/live.
When the broker is not there¶
Publishing never stops a run. Without SFMKIT_BROKER sfmkit runs exactly as it
did before any of this existed; if the broker goes away mid-run it says so once
and carries on to the end. The viewer with no --broker shows finished runs and
reports live: null on /api/health.