pySLAM NodeSet
The pySLAM NodeSet (ModelPySlamNodeSet, workspace/nodesets/model/model_pyslam/) wraps pySLAM (Luigi Freda, GPL-3.0) as a streaming visual-SLAM session. Where VGGT reconstructs geometry from N views in one feed-forward pass, pySLAM ingests an RGB-D stream one frame at a time and incrementally tracks the camera pose while accumulating a sparse 3-D map β classic feature SLAM (ORB features + DBoW3 loop closure, C++ core), a moving camera building a map as it goes. Feed it a policy's egocentric frames and it reconstructs the walked scene alongside the run.
The nodeset has eleven nodes in three groups: the streaming session above (five nodes β the four core verbs plus a dense volumetric map); three stateless perception nodes that use pySLAM's standalone classes as pure functions β feature extraction, feature matching, and evo trajectory evaluation (ATE / RPE); and three neural full-surface nodes β dense depth prediction, semantic segmentation, and feed-forward multi-view reconstruction (DUSt3R / MASt3R / VGGT). The last two groups share the same container but never touch the map. The aim is to expose pySLAM's whole capability surface β AgentCanvas supporting pySLAM-the-model, not a curated subset for one downstream graph.
env: container β agentcanvas/pyslam:cpu-fixed for the SLAM / feature / depth / semantic path, agentcanvas/pyslam:cuda (adds compiled multi-view backends) for reconstruct_multiview, rootless Docker, not a conda env Β· backend: GPL-3.0 pySLAM behind an HTTP bridge, never imported in the framework process Β· front-end: ORB2 features + DBOW3 loop closure, RGB-D by default Β· GPU via rootless CDI, on by default
pyslam_tum_slam graph streams TUM fr3/long_office_household frame-by-frame into a live SLAM session β no simulator, no policy, CPU-only: raw RGB + depth, the estimated camera path (blue) SE3-fitted onto ground truth (grey) top-down, and the sparse 3-D map densifying in real time; then an orbit of the finished map.| Primitive | Purpose |
|---|---|
model_pyslam__reset | (Re)start the session. The first call builds the Slam instance from the camera intrinsics + presets; later calls clear the map (reset_session) for a new episode, never rebuild. |
model_pyslam__track | Feed one RGB(-D) frame β pose (4Γ4 world-from-camera) + tracking_state + num_map_points. The map accumulates as a side effect. |
model_pyslam__get_trajectory | The accumulated camera trajectory β one 4Γ4 pose per successfully tracked frame, plus the total frame count. |
model_pyslam__get_map | Export the sparse map (3-D landmarks) to a host-side .npz handle β the path travels the wire, never the geometry. |
model_pyslam__get_dense_map | Export the dense volumetric map (fused voxel cloud / TSDF mesh) from pySLAM's volumetric integrator. Requires PYSLAM_VOLUMETRIC=1; empty handle otherwise. |
| Stateless perception β pure functions over pySLAM's standalone classes, no map state: | |
model_pyslam__extract_features | RGB β keypoints (NΓ6) + descriptors (NΓD). pySLAM's local-feature front-end as a standalone tool. |
model_pyslam__match_features | Two descriptor blocks β matched index pairs (Hamming / L2 auto from dtype). |
model_pyslam__eval_trajectory | Estimated vs ground-truth trajectory β ATE + RPE statistics (evo, Umeyama-aligned). |
| Neural full-surface β pySLAM's dense-perception bank on the GPU, stateless: | |
model_pyslam__predict_depth | RGB (+ right image) β dense metric depth map. Depth-Anything V2 / Depth Pro / MASt3R (mono) or SGBM / RAFT-Stereo / CREStereo (stereo). |
model_pyslam__segment_semantic | RGB β per-pixel semantic (+ instance) map. DeepLabV3 / SegFormer / CLIP / Detic / YOLO. |
model_pyslam__reconstruct_multiview | N overlapping RGB views β fused 3-D scene handle + per-view camera poses. DUSt3R / MASt3R / VGGT family (:cuda image). |
1. The container bridge
pySLAM is GPL-3.0 and carries a native C++ core (g2o, Pangolin, DBoW3) pinned to Python 3.11.9. Rather than vendor that source or build a sibling conda env, the nodeset keeps the whole dependency behind a container boundary β the same treatment habitat_sim gets. Three layers sit between a canvas node and the SLAM math; the framework side never runs import pyslam, and every per-frame call is one HTTP round-trip to a shim running inside the container.
rgb/depth into the pySLAM nodes, whose pose and map handle flow on typed wires into the viewer nodes, and the GraphExecutor fires each per step. Middle β firing a pySLAM node reaches the shared session through the _NODESET singleton, then the client, which never imports pySLAM and instead docker runs the container and talks HTTP. Bottom β the shim forwards to the session driving Slam. Results ride back up the wires into the viewers (right), so the container's output re-enters the graph.| Layer | File | Where | Role |
|---|---|---|---|
| Nodes + NodeSet | __init__.py | framework | Eleven canvas nodes (five session verbs + three stateless perception + three neural full-surface); the nodeset owns the container lifecycle and holds the client as its session. |
| Client | _client.py | framework | PySlamContainerClient β presents the exact surface of a PySlamSession but forwards every call over HTTP; docker run/rm; writes map handles host-side. |
| Shim | _server.py | container | A small FastAPI app β the only place import pyslam is legal. Holds one session; exposes /reset, /track, /get_map, β¦ |
| Session | _backend.py | container | PySlamSession β drives one pySLAM Slam pinned to a single worker thread (pySLAM's background threads have thread affinity). |
The nodeset directory is bind-mounted read-only into the container, so _server.py/_backend.py run inside without baking our code into the image β only fastapi/uvicorn and pySLAM live there. Container access is forced onto the user's rootless Docker daemon (DOCKER_HOST/XDG_RUNTIME_DIR set to the user socket), so the whole thing needs no sudo.
One call, end to end. The graph executor fires a freshly-constructed node each step, so a node cannot hold the session on self β it reaches the shared client through the process-level singleton _NODESET that initialize() set (Β§3). A single track then flows node β client β shim β session: the client serialises the frame and POSTs it over the loopback port; the shim hands it to the PySlamSession, which marshals the call onto its one worker thread and returns the pose as JSON. The single thread is not incidental β pySLAM's local-mapping / loop-closing / volumetric background threads are bound to the thread that constructed Slam, so every track must run on that same thread (the same single-thread-affinity discipline the GL/physics env nodesets use).
What rides the wire. Two payload classes cross the bridge differently. Light results (pose, tracking state, point counts) travel as JSON; heavy geometry β frames in, sparse / dense maps and multi-view clouds out β travels as raw .npz bytes over an octet-stream, never base64-in-JSON. And on the graph wire only a handle path travels: the client writes the returned .npz to a host-side file (framework-user-owned, sidestepping rootless-Docker uid remapping) and passes that path downstream, so a multi-thousand-point map never inflates the executor's in-memory state.
| Endpoint (POST unless noted) | Body β response |
|---|---|
GET /health | β {ok, built} β the readiness probe the client polls after docker run |
/configure_camera | JSON intrinsics β {ok} β pins the pinhole camera |
/start Β· /reset Β· /close | β {ok} β build Slam Β· clear the map Β· quit (join bg threads) |
/track | npz(rgb[, depth[, ts]]) β JSON pose Β· tracking_state Β· num_map_points |
/get_trajectory | β JSON poses Β· states Β· num_frames |
/get_map Β· /get_dense_map | β npz bytes (points, β¦) + X-Num-* headers; the client writes the host handle |
| Stateless perception β no session, npz in / npz out: | |
/extract_features Β· /match_features Β· /predict_depth Β· /segment_semantic Β· /reconstruct_multiview | npz(images / descriptors) β npz(results) + X-* count headers |
/eval_trajectory | JSON(poses_est, poses_gt) β JSON(ate, rpe) β pure math, no arrays |
The session's shape is fixed at launch, not per call. Sensor mode, feature / loop presets, volumetric integration, indoor/outdoor regime β all chosen when the container starts: initialize() passes them as -e environment variables on docker run, and the shim reads them when it lazily builds the session (_get_session()). That is why those knobs live as environment variables (Β§7) rather than call arguments β they define the container, while only per-frame data (images, depth) rides the HTTP body. The per-node Reset config fields override the env defaults right before the first configure_camera.
2. A streaming session, not a stateless pass
Every other foundation-model nodeset here is a stateless primitive (parallelism = "shared", one server across eval workers). pySLAM is the exception: its map is mutable per-episode state that accumulates across track calls, so it is a session. Consequences:
parallelism = "replicated"β each batch-eval worker needs its own map, hence its own container (one container per client).server_python = Noneβ the nodeset runs in local mode: it is not itself a server-mode subprocess (the container is). It reaches pySLAM over HTTP, not by importing it.- The
Slaminstance is built lazily on the firstreset(once camera intrinsics are known), then reused; per-episoderesetonly clears the map.
The three Tier-2 perception nodes (Β§5) are the counterexample within this nodeset: they are stateless pure functions that reuse the same container but never touch the Slam object, so β unlike track / get_map β they do not gate on the session being built.
3. Weaving a passive observer into the dataflow
A SLAM observer riding alongside a navigation policy is easy to wire wrong, because two graph-executor facts fight the naΓ―ve topology:
- Fresh instances. The executor fires freshly-constructed
node_cls()objects, not theget_tools(self)instances that carry a nodeset back-reference. So nodes reach the shared session through a process-level singleton thatinitialize()sets, read asself._nodeset or _NODESETβ safe because local mode is always single-process. - Demand-driven pruning. A node whose outputs feed nothing is pruned and never fires. A passive observer therefore must be woven into the live path:
track'snum_map_pointsis wired to aniterOutport, andreset'sepisode_okto aniterIninit port, so both actually run every step / once per episode.
The shipped example, vln/unverified/pyslam_slam_probe.json, is a pure-policy VLN-CE navigator (CMA policy, no LLM) with pySLAM observing: the policy drives, pySLAM reconstructs the walked scene, and the wiring above keeps the observer alive.
4. Camera intrinsics & depth scale
Two alignment details make an environment's RGB-D stream usable by pySLAM:
- Intrinsics. Wire the environment's
intrinsicsoutput (Habitat's observe port emits{fx, fy, cx, cy, width, height}) intoreset.intrinsicsfor an exact pinhole β no hard-coded HFOV. Absent that,cam_hfov+ width/height derivefxfrom the field of view. - Depth scale. RGB-D SLAM needs depth in metres. Habitat VLN-CE renders depth normalised to
[0,1]over a 10 m range, so setdepth_scale = 10.0on the reset node β the track node multiplies depth back to metric before feeding pySLAM. (pySLAM's ownDepthMapFactorapplies only to file-loaded depth, not an ndarray passed totrack(), so the scaling is node-side.) When RGB and depth grids differ (e.g. CMA's RGB 224 vs depth 256), depth is nearest-neighbour resampled onto the RGB grid first.
5. Canvas Nodes
model_pyslam__reset
| Field | Detail |
|---|---|
| Inputs | trigger, intrinsics (ANY β {fx,fy,cx,cy,width,height}), cam_width, cam_height, cam_hfov (all optional) |
| Outputs | episode_ok (BOOL), info (TEXT β session state JSON, or {"error": β¦}) |
| Config | sensor_type (rgbd / mono / stereo) Β· feature_preset (default ORB2) Β· loop_preset (default DBOW3, or off) Β· cam_hfov Β· depth_scale |
| Backend call | first fire: configure_camera β start (builds Slam); every fire: reset_session to clear the map |
model_pyslam__track
| Field | Detail |
|---|---|
| Inputs | rgb (IMAGE β HxWx3 uint8 or .npy path), depth (DEPTH β metres, RGB-D only, opt), timestamp (ANY, opt), gt_pose (ANY β env observe.pose, captured frame-aligned for eval_trajectory, opt) |
| Outputs | pose (POSE β 4Γ4 world-from-camera, or null if LOST), tracking_state (TEXT), num_map_points (ANY) |
| Backend call | depth resample β depth Γ depth_scale β Slam.track(rgb, None, depth, id, ts); pose from tracking.cur_R / cur_t |
{ "pose": [[1.0, 0.0, 0.0, 0.12], [0.0, 1.0, 0.0, 0.03], [0.0, 0.0, 1.0, 0.58], [0.0, 0.0, 0.0, 1.0]], "tracking_state": "OK", "num_map_points": 1862 }
tracking_state walks NOT_INITIALIZED β OK (β LOST if track drops).
model_pyslam__get_trajectory
| Field | Detail |
|---|---|
| Inputs | trigger (ANY, opt) |
| Outputs | poses (ANY β list of 4Γ4, estimated), num_frames (ANY β total frames fed to track), gt_poses (ANY β frame-aligned ground truth captured by track, for eval_trajectory) |
model_pyslam__get_map
| Field | Detail |
|---|---|
| Inputs | trigger (ANY, opt) |
| Outputs | map_handle (TEXT β path to a .npz), num_points (ANY), num_keyframes (ANY) |
| Backend call | shim ships points as .npz bytes; the client writes them host-side (framework-user-owned, sidestepping rootless-Docker uid remapping) |
{ "map_handle": "outputs/pyslam_maps/map_0001_001862.npz", "num_points": 1862, "num_keyframes": 12 }
The .npz holds a points array of shape (N, 3) β the sparse landmark cloud in world coordinates.
model_pyslam__get_dense_map
Export the dense map β not the sparse landmark cloud but pySLAM's own volumetric integrator output, a fused voxel point cloud or TSDF mesh built over the tracked keyframes. A session verb: it requires the session to be built with volumetric integration on (PYSLAM_VOLUMETRIC=1) and returns an empty handle otherwise.
| Field | Detail |
|---|---|
| Inputs | trigger (ANY, opt) |
| Outputs | dense_handle (TEXT β path to a .npz, or empty), num_points (ANY β fused voxel points), num_vertices (ANY β TSDF mesh vertices) |
| Config | via env: PYSLAM_VOLUMETRIC Β· PYSLAM_VOLUMETRIC_TYPE (VOXEL_GRID / TSDF / VOXEL_SEMANTIC_GRID) Β· PYSLAM_ENV (indoor / outdoor depth-truncation) |
| Backend call | flush the integrator's keyframe queue and wait for it to drain (the integrator is a separate process), then add_update_output_task + pop_output β point cloud / mesh arrays |
The drain-wait matters: the volumetric integrator consumes keyframes on its own process via multiprocessing queues, so popping an output before the queue drains returns a partial or empty map. The node blocks until the queue is empty, then extracts a complete volume (VOXEL_GRID is the verified default).
The first five nodes are the streaming session; the next three are stateless perception β they reuse the container but hold no map state, so they never gate on the session being built.
model_pyslam__extract_features
pySLAM's local-feature front-end as a standalone tool β detect keypoints and compute descriptors on one image.
| Field | Detail |
|---|---|
| Inputs | rgb (IMAGE β HxWx3 uint8 or .npy path) |
| Outputs | keypoints (ANY β NΓ6 [x, y, size, angle, response, octave]), descriptors (ANY β NΓD; uint8 for ORB, float for SIFT), num_keypoints (ANY) |
| Config | detector (default ORB β OpenCV, no weights; also SIFT / AKAZE / BRISK / SuperPoint / XFeat) Β· descriptor (blank = same as detector) Β· num_features |
| Backend call | feature_manager_factory(...).detectAndCompute |
ORB2 (the ORB-SLAM2 interface) needs a C++ binding the CPU image lacks β use plain ORB; learned detectors auto-download a checkpoint on first use.
model_pyslam__match_features
Match two descriptor blocks (from Extract Features) β matched index pairs. Classical matchers run on the arrays alone β no images, no weights.
| Field | Detail |
|---|---|
| Inputs | descriptors_a (ANY β NΓD), descriptors_b (ANY β MΓD) |
| Outputs | idxs_a (ANY), idxs_b (ANY β aligned matched indices), num_matches (ANY) |
| Config | matcher_type (BF / FLANN) Β· ratio_test (Lowe, default 0.7) |
| Backend call | feature_matcher_factory(...).match; distance auto from dtype (Hamming for binary ORB/AKAZE, L2 for SIFT) |
model_pyslam__eval_trajectory
Score an estimated trajectory against ground truth β ATE + RPE via evo. Pairs naturally with Get Trajectory. Pure math, no weights, no GPU.
| Field | Detail |
|---|---|
| Inputs | poses_est (ANY β list of 4Γ4), poses_gt (ANY β list of 4Γ4), is_monocular (ANY, opt β overrides config) |
| Outputs | ate_rmse (ANY β metres), rpe_rmse (ANY β metres, Ξ=1 frame), metrics (TEXT β full ATE + RPE stats JSON) |
| Config | is_monocular β off = metric SE3 align; on = Sim3 scale align |
| Backend call | utilities.evaluation.evaluate_evo (Umeyama-align β APE) + evo RPE |
A near-straight-line trajectory (a straight-corridor episode) makes Umeyama alignment rank-deficient, so ate_rmse can come back null (evo cannot align) while rpe_rmse still returns via a no-align fallback β an evo limitation, not a fault.
The last three nodes are neural full-surface β pySLAM ships a bank of learned dense-perception backends behind one factory each, and exposing pySLAM's whole surface means these get first-class nodes too. Like the stateless perception nodes they hold no map state, but they run neural nets on the GPU, so the first call loads (or downloads) a checkpoint. (The streaming session's own algorithm variety β feature detector, loop detector, sensor mode β is reached by config passthrough on the Reset node, not by new nodes.)
model_pyslam__predict_depth
Dense metric depth from a single image β pySLAM's depth-estimator bank as a standalone tool.
| Field | Detail |
|---|---|
| Inputs | rgb (IMAGE), image_right (IMAGE β stereo estimators only, opt) |
| Outputs | depth (DEPTH β HxW float32, metres), depth_range (ANY β [min, max]), estimator (TEXT) |
| Config | estimator (Depth-Anything V2 / Depth Pro / MASt3R mono Β· SGBM / RAFT-Stereo / CREStereo stereo) Β· environment (indoor / outdoor) Β· min_depth Β· max_depth |
| Backend call | depth_estimator_factory(...).infer(rgb, right) β depth map; device=None picks CUDA |
Mono estimators use rgb alone; stereo ones also consume image_right. SGBM is pure OpenCV/CPU; the learned estimators run on the GPU and auto-download a checkpoint on first use.
model_pyslam__segment_semantic
Per-pixel semantic segmentation β pySLAM's segmentation bank as a standalone tool.
| Field | Detail |
|---|---|
| Inputs | rgb (IMAGE) |
| Outputs | semantics (ANY β HxW int labels, or HxWxC / HxWxD volume), instances (ANY β HxW, or null), num_classes (ANY) |
| Config | model (DeepLabV3 / SegFormer / CLIP / Detic / YOLO / EOV-Seg) Β· feature_type (label / probability / feature vector) Β· dataset (Cityscapes / ADE20K / VOC / NYU40) |
| Backend call | semantic_segmentation_factory(...).infer(rgb) β SemanticSegmentationOutput; device=None picks CUDA |
model_pyslam__reconstruct_multiview
Feed-forward multi-view 3-D reconstruction β pySLAM's scene-from-views stack (the DUSt3R / MASt3R / VGGT family). Give a list of β₯2 overlapping views; get a fused global point cloud plus per-view camera poses in one pass. Needs the agentcanvas/pyslam:cuda image (the multi-view backends carry compiled CUDA kernels).
| Field | Detail |
|---|---|
| Inputs | images (ANY β a list of β₯2 overlapping RGB frames or paths) |
| Outputs | scene_handle (TEXT β path to a .npz: points, colours, mesh, intrinsics), camera_poses (ANY β per-view 4Γ4 cam-to-world), num_points (ANY), num_views (ANY) |
| Config | backend (MASt3R Β· DUSt3R Β· VGGT Β· VGGT-Robust Β· Fast3r Β· Depth-Anything V3) Β· as_pointcloud (point cloud, or mesh + cloud) |
| Backend call | scene_from_views_factory(...).reconstruct(images, as_pointcloud) β SceneFromViewsResult |
Weights are external, not baked into the image (Β§6): MASt3R / MV-DUSt3R load checkpoints from a mounted data/models/pyslam/ folder; the HuggingFace-runtime backends (VGGT / VGGT-Robust / Depth-Anything V3 / Fast3r / pure DUSt3R) download into a mounted cache on first use. MV-DUSt3R is a known gap β a version drift in pySLAM's bundled copy leaves it unusable; the other backends cover multi-view. MASt3R is the verified default (a 104k-point Chateau reconstruction runs end-to-end).
6. Deployment & lifecycle
Slam instance is built lazily on the first reset; track/get_map/reset repeat per episode (the loop clears the map but reuses the built session and container); shutdown quits Slam and removes the container.initialize() constructs the client and docker run -ds the image (nodeset dir bind-mounted at /opt/bridge, a free loopback port published), then polls /health until the shim answers β the blocking Docker call offloaded off the event loop. shutdown() posts /close (quits Slam, joins its background threads) then docker rm -fs the container. The image is built once via rootless Docker; pySLAM is cloned and compiled at build time, never entering the repo.
GPU. The container is launched with --device nvidia.com/gpu=all so the neural backends (learned depth, semantic segmentation, multi-view reconstruction) run on the GPU β the classic SLAM core stays on CPU. Rootless Docker reaches the GPU through CDI (the legacy --gpus cgroup path fails unprivileged). GPU is on by default and falls back to CPU when unavailable β set PYSLAM_GPU=0 to force CPU.
Two images, auto-selected. cpu-fixed already carries a CUDA torch ("cpu" only means the SLAM C++ core is CPU-built), so the depth and semantic backends run on the GPU there with no separate image. The multi-view backends are the exception: they carry compiled CUDA kernels (curope) that must be built with nvcc present, so reconstruct_multiview needs the heavier agentcanvas/pyslam:cuda image (built by docker/Dockerfile.cuda). When PYSLAM_IMAGE is unset the client picks the image by GPU: GPU present β :cuda (the whole surface works out of the box), no GPU β :cpu-fixed (the leaner image; multi-view needs a GPU anyway). A GPU start that fails on a host without CDI also degrades to the CPU image. Pin PYSLAM_IMAGE to override.
Weights are external. Neither image bakes in model weights β they carry only code and compiled kernels. The multi-view checkpoints live in a host data/models/pyslam/ folder and mount read-only into the container at runtime (PYSLAM_WEIGHTS_DIR overrides the location); the HuggingFace-runtime backends download into a mounted cache on first use. Fetch the explicit checkpoints with the tracked scripts under docker/weights/:
cd workspace/nodesets/model/model_pyslam/docker/weights bash download_mast3r.sh # MASt3R (~2.5GB) β the verified default bash download_all.sh # mast3r + mvdust3r explicit checkpoints
export DOCKER_HOST="unix:///run/user/$(id -u)/docker.sock" bash workspace/nodesets/model/model_pyslam/docker/build.sh agentcanvas/pyslam:cpu-fixed
Run the probe graph through the experiment wrapper. worker_count=1 is required β a higher count auto-routes the nodeset to server mode, which the container-bridge (local-mode) design does not support.
# One episode of the pure-policy probe graph, submitted to the slot backend. # worker_count=1 is REQUIRED: >1 auto-routes the nodeset to server mode, # which the container-bridge (local-mode) nodeset does not support. AGENTCANVAS_BACKEND_URL=http://127.0.0.1:8002 \ python .claude/commands/experiment/bin/submit.py \ architect-cma pyslam_slam_probe \ episode_count=1 worker_count=1 split=val_unseen step_budget=80
7. Environment Variables
| Variable | Default | Purpose |
|---|---|---|
PYSLAM_SENSOR | rgbd | Sensor mode β mono / stereo / rgbd |
PYSLAM_FEATURE | ORB2 | A feature_tracker_configs preset name |
PYSLAM_LOOP | DBOW3 | A loop_detector_configs preset name, or off |
PYSLAM_IMAGE | auto (GPU β :cuda, else :cpu-fixed) | Pin the container image; unset picks by GPU availability |
PYSLAM_ARTIFACT_DIR | outputs/pyslam_maps | Host dir where map / scene handles are written |
PYSLAM_VOLUMETRIC | 0 | Enable the dense volumetric integrator for get_dense_map |
PYSLAM_VOLUMETRIC_TYPE | VOXEL_GRID | Dense-map output β VOXEL_GRID / TSDF / VOXEL_SEMANTIC_GRID |
PYSLAM_ENV | INDOOR | Dense depth-truncation regime β INDOOR / OUTDOOR |
PYSLAM_WEIGHTS_DIR | data/models/pyslam | Host folder of external multi-view weights mounted into the container |
PYSLAM_GPU | 1 | GPU on/off; 0 forces CPU-only |
Per-node config fields on the Reset node (sensor type, presets, HFOV, depth scale) override these env defaults.
8. Operational notes
- Bootstrap latency. RGB-D SLAM needs a few frames of parallax to initialise. On a validated Habitat run the front-end spent ~16 discrete steps in
NOT_INITIALIZEDbefore locking on, then tracked steadily β reconstructing a metric room-scale cloud (~1.5β1.9k points, core extent β 5 Γ 3 Γ 7 m) matching the walked path. - Coverage is episode-dependent. Episodes dominated by in-place rotation, very short paths, or low-texture views may never accumulate enough parallax to initialise, and legitimately return zero map points β an inherent property of feature SLAM under discrete large-rotation motion, not a fault. The front-end honestly reports "not initialised" rather than emitting spurious geometry.
- Per-episode reset is clean. Across a multi-episode run each episode re-bootstraps from an empty map (
reset_session); maps never carry over, and a failed-to-initialise episode exports an empty cloud rather than the previous episode's points.