SB
articles
Article · Vision 2026-05 · 12 min read

Visual SLAM ↔ Python: a clean bridge via subprocess

I integrated an open-source C++ Visual SLAM engine into a FastAPI back-end, no exotic bindings, for the indoor mapping back-office we built at Okeenea. The trickiest part: aligning the result on a floor plan with only two GPS constraints.

Context

From a stream of pixels to a navigable map

I was asked to turn 360° videos into navigable indoor maps. Sounds simple in one sentence, but the piece that does all the actual work is Visual SLAM (Simultaneous Localization And Mapping), which jointly recovers the camera's 3D trajectory and a 3D point cloud of the environment.

The engine we settled on is an open-source C++ SLAM engine, built on OpenCV, g2o and Eigen. Our back-end runs Python (FastAPI). The real subject of this article is how we got the two talking to each other without it turning into its own project.

The call

Subprocess over 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 and 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.

Pipeline

From video to map.msg

  1. 1

    The 360° video is uploaded to Cloud Storage via signed URL. No backend proxy: the client uploads directly.

  2. 2

    On demand, FastAPI extracts key frames (OpenCV) at a configurable cadence. Not all at once: a streamed pipeline keeps disk IO bounded.

  3. 3

    FastAPI invokes the SLAM engine as a subprocess with the config (camera intrinsics, scale factor, FBoW vocabulary) and the frames folder.

  4. 4

    The binary writes a map.msg file containing keyframes (poses + descriptors) and landmarks (3D + observation counts).

  5. 5

    On the Python side, we parse map.msg with msgpack, hydrate SQLAlchemy models, and store everything in PostgreSQL.

The bridge

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.
Georeferencing

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 and robust, so deterministic.

Auto-fit

With only one constraint but the plan bounds defined, we can auto-scale the trajectory to fill the plan (a quick sanity check).

Takeaways

What we keep from it

  • To integrate a C++ library with a stateful API, subprocesses are underrated. They draw a clean line between its crash and mine, and I should have reached for this before defaulting to pybind11.
  • msgpack is an excellent IPC format: binary, typed, and 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.
  • What's still unresolved: the auto-fit scaling gets flaky on oddly-shaped buildings, so we disable it by hand on a few sites. Not proud of that patch, but it holds for now.