SB
articles
Article · Vision 2026-05 · 12 min de lecture

Visual SLAM ↔ Python : un pont propre via sous-processus

J'ai intégré un moteur Visual SLAM C++ open-source dans un back-end FastAPI, sans bindings exotiques, pour le back-office de cartographie indoor qu'on a construit à Okeenea. La partie la plus tordue : aligner le résultat sur un plan d'étage avec seulement deux contraintes GPS.

Contexte

Du flot de pixels à un plan navigable

On m'a demandé de transformer des vidéos 360° en plans indoor navigables. Sur le papier ça tient en une phrase, en pratique la pièce qui fait tout le travail c'est le Visual SLAM (Simultaneous Localization And Mapping), qui reconstruit en parallèle la trajectoire 3D de la caméra et un nuage de points de l'environnement.

Le moteur qu'on a retenu est un moteur SLAM open-source en C++, construit sur OpenCV, g2o et Eigen. Notre back-end tourne en Python (FastAPI). Le vrai sujet de cet article, c'est comment on a fait dialoguer les deux sans que ça devienne un chantier en soi.

Le choix

Le sous-processus plutôt que le binding

Trois options techniques sur la table :

Bindings pybind11

Tentant sur le papier. Mais le moteur SLAM expose une API stateful complexe (config YAML, observers, callbacks). Maintenir un binding qui couvre tout ça revient à dupliquer leur surface publique en C++ avec un coût de maintenance énorme à chaque release upstream.

Service réseau séparé (gRPC)

Propre, scalable. Mais surdimensionné : on a un seul job SLAM à la fois par site, déclenché à la demande. Ajouter un service à packager, déployer et monitorer pour ça serait du complexity tax.

Sous-processus + format de sérialisation neutre

On exécute le binaire SLAM en sous-processus depuis FastAPI, on lui passe la vidéo et la config YAML, on récupère un map.msg (msgpack) qu'on parse côté Python. Simple et observable, isolé du runtime web.

On a pris la troisième. Le binaire SLAM crashe ? Le sous-processus meurt, on capture le code de retour, on logue, on remonte une erreur propre. Aucun risque que ça emporte FastAPI avec lui.

Pipeline

De la vidéo au map.msg

  1. 1

    La vidéo 360° est uploadée vers Cloud Storage via signed URL. Pas de proxy backend : le client uploade directement.

  2. 2

    À la demande, FastAPI extrait les frames clés (OpenCV) à une cadence configurable. Pas tout d'un coup : pipeline streamé pour limiter l'IO disque.

  3. 3

    FastAPI invoque le moteur SLAM en sous-processus avec la config (camera intrinsics, scale factor, vocabulary FBoW) et le dossier de frames.

  4. 4

    Le binaire écrit un fichier map.msg contenant keyframes (poses + descripteurs) et map points (3D + observations).

  5. 5

    Côté Python on parse le map.msg via la lib msgpack, on materialise des modèles SQLAlchemy, et on stocke en PostgreSQL.

Le bridge

Trois propriétés du sous-processus

Côté FastAPI, le pattern est court : on lance le binaire SLAM via asyncio.create_subprocess_exec, on attend le returncode, on parse le map.msg produit avec la lib msgpack. Pas de binding, pas de FFI, pas de serveur intermédiaire. Juste un process Unix et un fichier de sortie. Ce qu'on gagne :

  • Isolation runtime : le crash d'un job SLAM ne tue jamais l'API.
  • Cancellation propre : tuer le sous-processus revient à l'annuler, sans state corrupted côté Python.
  • Observable : stdout/stderr sont capturés et loggés, on debug en lisant les logs comme n'importe quel process Unix.
Géoréférencement

Aligner la trajectoire sur un plan

Le moteur nous donne la trajectoire dans un repère SLAM arbitraire (origine à la première frame, échelle métrique relative). Pour que ce soit utile dans le back-office, il faut l'aligner sur le plan d'étage en coordonnées WGS84 (lon, lat).

L'opérateur clique sur quelques keyframes et indique « ici dans le SLAM = ici sur le plan ». Avec assez de contraintes, on calcule la transformation rigide qui aligne tout.

1 contrainte

Translation seule : on déplace le repère SLAM pour que le point coïncide. L'orientation et l'échelle restent celles du SLAM.

≥2 contraintes

Sim(2) via l'algorithme d'Umeyama : on résout simultanément rotation 2D, translation et échelle qui minimisent l'erreur quadratique sur les correspondances. Closed-form et robuste, donc déterministe.

Auto-fit

Si on n'a qu'une contrainte mais que l'opérateur a posé les bornes du plan, on peut auto-scaler la trajectoire pour qu'elle remplisse le plan (un sanity check rapide).

Bilan

Ce qu’on retient

  • Pour intégrer une lib C++ avec une API stateful, les sous-processus sont sous-estimés. Ça trace une frontière nette entre son crash et le mien, et j'aurais dû y penser plus tôt au lieu de partir sur pybind11 par réflexe.
  • msgpack est un excellent format d'IPC : binaire, typé et multi-langage, sans la surface d'attaque de pickle.
  • Pour la perf 3D côté navigateur (milliers de frustums), on a utilisé Three.js directement plutôt que react-three-fiber : le coût React par frame est non négligeable à ces volumes.
  • Le géoréférencement Sim(2) Umeyama est en 30 lignes de NumPy. Ne pas se laisser intimider par la littérature SLAM : la plupart des briques utiles ont des solutions closed-form.
  • Toujours tester avec une vidéo « cassée » (frame manquante, codec exotique, taille zéro). Le sous-processus doit échouer proprement, pas planter le worker.
  • Ce qu'on n'a pas encore résolu : l'auto-fit d'échelle est un peu fragile dès que le bâtiment a une forme tordue, on le désactive à la main sur certains sites. Pas fière de cette rustine, mais elle tient pour l'instant.