Visual SLAM ↔ Python: a clean bridge via subprocess
How to integrate an open-source C++ Visual SLAM engine into a FastAPI back-end without exotic bindings, then align the result on a floor plan with two GPS constraints. Lessons from an indoor mapping back-office.
From a stream of pixels to a navigable map
The project: turn 360° videos into navigable indoor maps. The pipeline's centrepiece is Visual SLAM — Simultaneous Localization And Mapping — which jointly recovers the camera's 3D trajectory and a 3D point cloud of the environment.
We picked an open-source C++ SLAM engine, built on OpenCV, g2o and Eigen. Our back-end is Python (FastAPI). Question: how do we make them talk cleanly?
Subprocess, not bindings
Three technical options on the table:
pybind11 bindings
Tempting on paper. But the SLAM engine exposes a stateful API (YAML config, observers, callbacks). Maintaining bindings that cover all of it duplicates their public surface in C++, with a heavy maintenance tax every upstream release.
Separate network service (gRPC)
Clean, scalable. But oversized: we run one SLAM job at a time per site, on demand. Adding a service to package, deploy and monitor for that is pure complexity tax.
Subprocess + neutral serialisation
We run the SLAM binary as a subprocess from FastAPI, hand it the video and the YAML config, and read back a map.msg (msgpack) on the Python side. Simple, observable, isolated from the web runtime.
We took the third one. SLAM binary crashes? The subprocess dies, we capture the return code, log it, surface a clean error. No risk of taking FastAPI down with it.
From video to map.msg
- 1
The 360° video is uploaded to Cloud Storage via signed URL — no backend proxy, the client uploads directly.
- 2
On demand, FastAPI extracts key frames (OpenCV) at a configurable cadence. Not all at once: a streamed pipeline keeps disk IO bounded.
- 3
FastAPI invokes the SLAM engine as a subprocess with the config (camera intrinsics, scale factor, FBoW vocabulary) and the frames folder.
- 4
The binary writes a
map.msgfile containing keyframes (poses + descriptors) and landmarks (3D + observation counts). - 5
On the Python side, we parse
map.msgwithmsgpack, hydrate SQLAlchemy models, and store everything in PostgreSQL.
Three properties of the subprocess
On the FastAPI side, the pattern is short: spawn the SLAM binary with asyncio.create_subprocess_exec, await the returncode, parse the resulting map.msg with the msgpack library. No binding, no FFI, no intermediate server — just a Unix process and an output file. What we get:
- Runtime isolation: a SLAM job crash never takes the API down.
- Clean cancellation: killing the subprocess cancels the job without leaving corrupted state on the Python side.
- Observability: stdout/stderr are captured and logged, so debugging is just reading logs like any other Unix process.
Aligning the trajectory on a plan
The engine gives us the trajectory in an arbitrary SLAM frame (origin at the first key frame, relative metric scale). To make it useful in the back-office, we have to align it on the floor plan in WGS84 coordinates (lon, lat).
The operator clicks a few key frames and says "this point in SLAM = this point on the plan". With enough constraints, we compute the rigid transform that aligns everything.
1 constraint
Translation only: we offset the SLAM frame so the point matches. Orientation and scale stay as SLAM produced them.
≥2 constraints
Sim(2) via the Umeyama algorithm: we jointly solve for 2D rotation, translation and scale that minimise the squared error over the correspondences. Closed-form, robust, deterministic.
Auto-fit
With only one constraint but the plan bounds defined, we can auto-scale the trajectory to fill the plan — quick sanity check.
What we keep from it
- To integrate a C++ library with a stateful API, subprocesses are underrated. They draw a clean boundary: a crash there is not a crash here.
- msgpack is an excellent IPC format: binary, typed, multi-language, without pickle's attack surface.
- For browser-side 3D perf (thousands of frustums), we used plain Three.js instead of react-three-fiber — the per-frame React cost is non-trivial at that scale.
- Sim(2) Umeyama georeferencing fits in 30 lines of NumPy. Don't be intimidated by the SLAM literature: most of the useful bricks have closed-form solutions.
- Always test with a 'broken' video (missing frame, exotic codec, zero size). The subprocess must fail cleanly, not crash the worker.