Skip to content

feat: Bag recorder#17

Merged
MJohnson459 merged 3 commits into
mainfrom
bag-recorder
Jun 29, 2026
Merged

feat: Bag recorder#17
MJohnson459 merged 3 commits into
mainfrom
bag-recorder

Conversation

@MJohnson459

Copy link
Copy Markdown
Contributor

Introduces rolling bag captures. We record two levels of bags "min" and "perception" where "min" is lightweight topics like lidar and TF and "perception" includes camera data, etc. This allows us to clean up at different rates mixing a low resolution long history with a high resolution short term memory.

The bags are automatically cleaned up based on a threshold and a new bag_pruner.py node. When the limits are reached the oldest bag file is removed.

MJohnson459 and others added 3 commits June 29, 2026 11:29
`pixi run record` (record_launch.py) records the perception-relevant topics —
compressed image, camera_info, tf, and filtered scan — to a timestamped dir
under ~/.mote/bags, split into 10-minute segments (--max-bag-duration; override
with split:=). Adds the ros2bag CLI verb, the recorder, and the mcap storage
plugin to the robot env, which were absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the single rosbag recorder with two purpose-split streams written
under ~/.mote/bags: a light "min" stream (lidar + TF, 10-minute segments)
and a heavy "perception" stream (compressed camera + the same lidar/TF,
1-minute segments). Each stream gets its own rolling disk budget via a new
bag_pruner that deletes the oldest .mcap segments once the cap is exceeded,
so continuous recording never fills the disk. A mote-record systemd service
(enabled by install-systemd) records automatically on boot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BuMPbaua66V8R1KsRUUPYD
@MJohnson459 MJohnson459 merged commit 16b2fab into main Jun 29, 2026
2 checks passed
@MJohnson459 MJohnson459 deleted the bag-recorder branch June 29, 2026 12:27
MJohnson459 added a commit that referenced this pull request Jun 29, 2026
Reconcile the lock with the merged pixi.toml so it carries both the depth env
(rebased) and the bag-recorder deps (from main #17).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS
MJohnson459 added a commit that referenced this pull request Jul 4, 2026
* Add L1 camera-perception spike: geometry, classical seg, depth rescale

Monocular obstacle-detection spike under mote_perception (offline; no ROS nodes
or deps added yet):
- ground_projection.py: shared pixel<->floor geometry (camera->base via static TF)
- free_space.py: classical appearance floor segmentation (spike — fast but
  false-positive prone under variable lighting)
- depth_rescale.py: robust per-frame metric rescaling of learned mono-depth
  against the known floor plane (RANSAC affine-in-disparity) — the chosen L1
  direction; inlier fraction gates seed contamination
- tools/: offline bag harnesses (geometry overlay, classical/BEV/depth eval,
  segmentation video) for evaluating approaches against recorded bags

Depth + floor-rescale gives ~0.19 m median range vs lidar and is lighting-robust;
findings drove the decision to pursue learned depth off-board.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add off-board depth-obstacle node + inference server

The L1 obstacle pipeline as two processes so torch stays out of the ROS/robot env:
- tools/depth_server.py: keeps Depth Anything V2 (metric, indoor) resident and
  serves depth over a socket. Runs in a throwaway torch venv on the workstation.
- depth_obstacle_node (rclpy, no torch; runs anywhere): forwards each compressed
  frame to the server, metrically rescales the returned depth against the known
  floor plane (depth_rescale), back-projects, keeps points above z_obstacle
  (default 0.02 m), and publishes /camera_obstacles. The cloud is stamped at image
  capture time so Nav2 places it via tf at the moment it was seen — how the
  off-board (~0.6 s, inference-bound) latency is absorbed without inflation. Lidar
  stays the primary, low-latency obstacle/clearing source; this is a supplementary
  marker for the low/thin things the 2D scan misses.

z_obstacle=0.02 chosen from a floor-noise sweep across the bag: floor height noise
is ~1.5 cm p99, so <1.5 cm false-positives on the floor; 2 cm is clean and the
lidar already covers >=6 cm. depth_obstacles.py gains an obstacle-tint overlay.
Validated end-to-end against a recorded bag via ros2 bag play. README documents it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* PR feedback

* Remove old CV solution. Cleanup

* Fix numpy import in depth workstation script

The workstation depth task runs in the default/ROS env, so the nested `pixi run depth-server` inherited a PYTHONPATH pointing at the ROS Python 3.12 site-packages, and the depth env's Python 3.14 then loaded those incompatible numpy C-extensions. Drop PYTHONPATH for the server child only; the ROS node still needs it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Add depth-perception debug views and camera-pitch tool

Diagnostics for the L1 monocular-depth obstacle pipeline:
- depth_obstacle_node gains a publish_debug param (default true) that publishes
  the rescaled metric depth as a 32FC1 Image (/camera_depth) and the unfiltered,
  floor-inclusive cloud (/camera_cloud_full) for geometry checks.
- mote.rviz: Camera Obstacles + Camera Cloud (full) PointCloud2 displays
  (AxisColor by height) and a Depth image display.
- tools/measure_camera_pitch.py: lays the calibration checkerboard on the floor
  and solvePnPs its plane to read the camera's pitch/roll/height relative to the
  floor it sits on, folding in chassis tilt and local floor slope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Anchor depth scale to lidar instead of the floor plane

The floor-plane rescale fits a narrow near-floor band and extrapolates to far
walls, and depends on the camera->floor angle, which was measured to wander ~1.5
deg across rest positions (floor slope + how the robot rests) — so the obstacle
cloud over-ranged past the walls even stationary. Lidar gives metric range on the
walls themselves through the body-fixed lidar->camera transform, which is immune
to chassis/floor tilt.

LidarDepthRescaler matches each scan return to its camera pixel, samples the model
depth there, and fits the shared affine-in-disparity correction on those pairs.
The node buffers scans and matches the one nearest the image *capture* stamp
(absorbing the ~0.6 s off-board latency). rescale_source = auto|lidar|floor; auto
holds the last good lidar (a,b) when a scan can't constrain it rather than falling
back to the floor fit it replaces. Logs scale source, pair count, and scan dt.

Scale only; the cloud is still back-projected through the level-URDF transform, so
residual pitch can still skew floor-point z-classification — a follow-up plane-fit
on the lidar-scaled floor points will recover that.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Fix scan starvation; add depth-pipeline diagnostics

The single-threaded executor was monopolized by the ~0.5 s blocking inference in
_on_image, so _on_scan was starved, the scan buffer went stale, and no scan
landed within scan_max_dt of an image's capture stamp — the node fit lidar once
at startup then held that one (bad) (a,b) forever. Run on a MultiThreadedExecutor
with the scan subscription in its own callback group so scans keep buffering
during inference; snapshot the deque when matching (read on the image thread,
appended on the scan thread).

Diagnostics for chasing fit quality: log scan-buffer depth, matched dt, pair
count, and the fitted (a,b); publish the raw pre-rescale model depth
(/camera_depth_raw) next to the rescaled one to separate model noise from a
runaway rescale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Regenerate pixi.lock after rebase onto main

Reconcile the lock with the merged pixi.toml so it carries both the depth env
(rebased) and the bag-recorder deps (from main #17).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Constrain lidar depth fit to break RANSAC bistability

Offline replay of the recorded bags (depth server + the real pairing/fit) showed
the raw model depth is clean every frame, but the lidar affine fit was RANSAC-
bistable: the scan's returns include a large cluster at one range (a wall filling
the view) that supports a near-flat degenerate line (slope ~0, depth inverted)
with inlier support equal to the true line, so the unconstrained fit flipped
between them frame to frame -- ~40% of frames collapsed to inverted/exploded depth,
which is the noise. Good fits had a in [1.6, 3.1]; degenerate ones a in [-0.32, 0.10].

fit_affine_disparity takes optional a_min/disp_floor: when set, RANSAC only scores
physically valid models (slope >= a_min, and a*disp_floor + b > 0 so corrected
disparity can't blow up), and keeps the valid seed if the least-squares refit drifts
off it. Default stays unconstrained, so the floor path is untouched (verified: exact
recovery). LidarDepthRescaler passes a_min=0.5 and rejects a residual invalid fit so
the node holds last-good. On both bags this takes degenerate frames 19/40 and 15/40
-> 0/40 while leaving the good-frame inliers (69%) and median depth (0.64 m) exactly
as the baseline; a pure-scale (b=0) alternative also reached 0 but shifted the median
to 0.74 m, so the 2-DOF constraint won on accuracy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Add depth_bag_replay tool for offline pipeline analysis

Replays a recorded bag through the depth server + the real lidar pairing/fit and
reports per-frame raw depth, pair count/spread, fitted (a,b)/inliers, and rescaled
depth, saving colorized raw/corrected maps. A collapsed fit prints DEGENERATE. This
is the rig that localised the RANSAC bistability; keep it for analysing future
'depth goes to noise' bags without the robot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Fit lidar depth with Theil-Sen to stop frame-to-frame flicker

After the a<0 constraint, the cloud still flickered on a static scene: the fit was
bimodal between two physically valid lines (a~2.5 median depth 0.58 m, and a~1.1
median 0.95 m) with near-equal inlier support, and the count-RANSAC flipped between
them frame to frame. Offline replay showed the pairs actually follow one line
(near-constant local slope ~1.9); the bug is the inlier-*count* objective, which is
multimodal when the scatter is two-sided -- a steeper or shallower line catches the
same count.

Fit the lidar pairs with Theil-Sen (median of pairwise slopes, then median
intercept) -- a unique, deterministic central estimate. Across the three bags this
cuts the per-frame median-depth std from 0.18 m to 0.04-0.09 m and a-std from 0.66 to
0.14-0.22, with the mean unchanged, so it's stabilizing, not biasing. Theil-Sen is
naturally positive (no inverted line) with intercept ~0 (no blow-up), so the a_min
constraint added last commit is now redundant -- kept only as a defensive reject for a
pathological scan, and the in-RANSAC constraint is reverted. The floor seed keeps
count-RANSAC (its one-sided obstacle rejection is a different need; exact recovery
verified). No temporal smoothing -- Theil-Sen is stable per-frame with zero lag, which
EMA would trade away under motion.

Still scale-only and not yet validated on a moving/on-robot bag (occlusion-edge
parallax can exceed Theil-Sen's ~29% breakdown); the cloud is also still the full
image (the Phase-2 over-range/plane-fit gap).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Add depth_bag_eval: accuracy + speed + visual harness for a bag

Model-agnostic eval that talks to whatever depth server is on --port (V2 now, a V3
server later), so models are compared by pointing it at each. Per sampled frame it
measures, against the time-nearest lidar scan: held-out AbsRel/RMSE/delta1 after the
best affine alignment (the model's in-band shape/scale fidelity, comparable across
models) and server round-trip latency. Saves three views to inspect by eye -- the
depth map with lidar returns overprinted, a side elevation (range vs height) that
shows whether vertical edges lean into the distance, and a top-down BEV.

V2 baseline on bag 20260630_103318: ~470 ms/frame (CPU), AbsRel 0.231, delta1 57.5%,
and the side view shows the scene ramping up with range -- i.e. the slant is largely
in the depth itself, motivating the V2-vs-V3 comparison.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Let depth server pick the model; fix replay's stale fit call

depth_server takes --model and --disparity so the eval harness can compare models
(metric outputs depth; relative/SSI models output disparity -> invert to depth, the
pipeline refits scale either way). depth_bag_replay was still calling
fit_affine_disparity(a_min=...), removed when the lidar path moved to Theil-Sen --
point it at fit_affine_disparity_theilsen.

Finding (rescale-anyway pipeline, 3 bags): relative V2-Small beats V2-Metric-Indoor
on every bag -- delta1 90.6/74.3/76.9% vs 57.5/70.9/72.7%, lower AbsRel/RMSE, ~40 ms
faster, and a stable non-bimodal fit. The metric model's absolute scale is discarded
by our per-frame affine, so it buys nothing and is worse-conditioned. The slant
persists across both, so it is geometric (back-projection/pitch), not the model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Add DA3 (V3) server + raw/aligned accuracy in the eval rig

depth_server_da3.py serves Depth Anything 3 over the same socket protocol so the rig
compares it like any other model. DA3 needs Python<=3.13 (the pixi depth env is 3.14),
so it runs in its own uv venv (documented in the file); its export path pulls heavy
3D/video deps (open3d/moviepy/pycolmap) that single-image depth never uses, so those
are stubbed at import -- the actual install is a venv + CPU torch + a few small deps.
Takes --model and --intrinsics (metric variants use a canonical-focal transform).

depth_bag_eval now reports raw (no-rescale) AND affine-aligned accuracy, so 'does a
metric model need rescaling' is answerable directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Switch depth pipeline default to relative V2-Small

The node refits a full affine in disparity against lidar every frame, so a metric
model's absolute scale is discarded; measured over 3 bags, relative V2-Small beats
V2-Metric-Indoor on accuracy (aligned delta1 ~91 vs ~57% on one bag, better on all),
is faster, and gives a stable non-bimodal fit. Make it the default MODEL. Relative
models output disparity, so invert to depth by default; --metric passes a metric
model through unchanged. Bare `pixi run depth-server` (used by depth_workstation.sh)
now serves the relative model -- no node change needed.

Note: swapping the model id alone (without the inversion) yields a garbled depth map,
since the node expects depth and the relative model emits disparity -- the inversion
is the missing piece.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Colour camera clouds with true RGB; make depth node subscriber-aware

Pack each point's camera pixel into the camera_obstacles and
camera_cloud_full clouds as an RGB8 field and switch the RViz displays
to RGB8, so the depth cloud renders in real camera colour rather than a
Z-axis gradient.

Make depth_obstacle_node lazy: skip the off-board inference and all
downstream work unless an output is subscribed, gating each stage (raw
and corrected depth images, full cloud, obstacle cloud) on its own
subscription count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Phase 2: level the depth cloud by fitting the floor plane

Fit the floor plane (z = a*x + b*y + c, RANSAC over near-floor points)
each frame and rotate the whole cloud about the camera origin so the
floor is level. This removes the residual camera tilt the level URDF
transform misses (dynamic pitch / floor slope), which previously left a
range-dependent z-ramp: floor points drifted above the obstacle gate
(false positives) and verticals leaned into the distance.

It is a rotation, not a z-only shear: the lean is a rotation of the
cloud about the camera, so it sits in x as well as z; rotating the
normal onto +z straightens verticals and flattens the floor together.
Over-large or unconstrained fits are rejected and the last good rotation
held, so the cloud can't snap flat<->tilted frame to frame.

Add ground_projection.fit_ground_plane + level_rotation (shared, pure),
a plane_fit toggle, and a plane-levelled side view to depth_bag_eval for
before/after inspection. Validated on a synthetic known-tilt cloud
(exact recovery) and a recorded bag (1-3 deg recovered, floor flat after).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JV1CtqERcMH4YB1gQLNDGS

* Lower LidarDepthRescaler a_min for the relative depth model

The lidar affine slope a maps the model's disparity onto the true
(lidar) disparity, so its magnitude absorbs the model's arbitrary
disparity units. The pipeline default is now the relative (SSI) V2-Small
model, whose disparity scale puts a valid fit near 0.25-0.5 -- an order
of magnitude below the metric model a_min=0.5 was picked for. That
threshold rejected every valid relative-model fit, so the node never
obtained a lidar scale and silently fell back to the floor fit (the
over-ranging path lidar anchoring exists to replace).

Drop a_min to 0.05 so it guards only the sign/near-flat degeneracy
(a <= ~0) it was meant to, independent of the model's disparity scale.
Verified on recorded bags: valid fits land a=0.25-0.47 (corrected depth
AbsRel 0.13-0.33, delta1 79-92% vs lidar); with a_min=0.5 all were
rejected, with 0.05 all pass and the cloud is lidar-anchored again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLZtYP7Uf7uZd7XTh2XZnW

* Feed camera obstacles into a near-band Nav2 voxel layer

Wire the /camera_obstacles cloud into a dedicated VoxelLayer on the local
costmap, so the low/near things the single lidar plane misses (cables,
thresholds, chair/table legs, a clothes-horse cross-bar) actually stop
the robot. Until now the cloud was produced but nothing consumed it.

- Clamp the published cloud to the mount's validated near band by
  dropping the node range_max default 3.0 -> 1.2 m. Past ~1.2 m the
  monocular depth compresses into false positives; the near band is both
  where this layer is trusted and what the goal cares about.
- Add camera_layer (VoxelLayer) to the local costmap only, separate from
  the lidar obstacle_layer so the camera can never clear a lidar mark and
  a laggy source never touches the global plan. It marks and clears from
  its own dense observations. sensor_frame pins the clearing-ray origin to
  the camera height (cloud stays leveled base_footprint) so rays descend
  onto the floor instead of rising through the low-obstacle band.

Offline validation on the recorded low-obstacle bags: the near band
(<=1.2 m) detects the dumbbell, clothes-horse legs, lamp base and chair
legs while clean floor marks ~0 points. Live nav2/robot behaviour --
including standstill decay of any phantom over open floor -- is the
documented remaining gate (see README caveat).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLZtYP7Uf7uZd7XTh2XZnW

* Document the live bring-up gate for the camera obstacle layer

All validation so far is offline against recorded bags. Record the
workstation+Pi co-run, the ordered live checks (camera_layer actually
marks / off-board latency vs transform_tolerance / drive past a low
obstacle / motion-only FP from a stale held level rotation), and note the
bright-glare-floor FP result, so the first robot session doesn't
rediscover them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLZtYP7Uf7uZd7XTh2XZnW

* Add a go-under height gate so the robot passes under furniture it fits

The camera obstacle band had no meaningful upper bound (marked up to
0.8 m), so a chair seat or tabletop the robot fits beneath was marked as
an obstacle and the robot would refuse to path under it.

Cap the obstacle band at the robot's height plus a margin. Because the
camera layer is a 3D VoxelLayer, capping the marking height means the
legs of the furniture (which reach the floor) still mark while the seat
or top above the cap does not -- so the robot avoids the legs but drives
through the clear gap between them.

- Nav2 camera_layer max_obstacle_height 0.8 -> 0.18 m (the authoritative
  gate: current ~0.13 m chassis + margin), voxel grid retuned to a 0.20 m
  top (z_resolution 0.025, z_voxels 8). Documented how to reconfigure for
  the ~0.30 m arm build (raise gate + grid; the benefit shrinks there).
- Node: split the obstacle-cloud ceiling (new z_obstacle_max, default
  0.5 m) from the debug-cloud ceiling (z_ceiling stays 1.6 m), so it stops
  streaming points Nav2 discards while the full debug view is unaffected.

Verified offline (bag 145232 f1532): marks drop 26984 -> 5060 between an
0.8 m and 0.15 m cap, keeping the floor-reaching chair legs while the
seats/backs above the cap stop marking (go_under_height_gate.png).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLZtYP7Uf7uZd7XTh2XZnW

* Process only the latest camera frame in the depth obstacle node

Inference is slower than the camera frame rate, so a deeper image queue
only ever feeds stale frames. Depth 1 drops the backlog and keeps the
published obstacle cloud as fresh as the pipeline allows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLZtYP7Uf7uZd7XTh2XZnW

* Add live depth-obstacle output images for docs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLZtYP7Uf7uZd7XTh2XZnW

* Document live depth-obstacle output; fix stale pipeline description

The README's pipeline paragraph still described the old metric model +
floor-plane rescale; update it to the relative model with lidar-anchored
scale, and add live output images.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLZtYP7Uf7uZd7XTh2XZnW

* Fix depth-pipeline bugs found in PR review

Make every degenerate path fail safe and loud instead of publishing a
wrong or silently-empty obstacle cloud:

- rescale_source:=lidar no longer falls back to the floor fit when the
  lidar is unavailable; it skips the frame as the forced mode promises.
- An empty or degenerate floor seed returns inlier fraction 0 (frame
  rejected with a warning) instead of a maximally-confident garbage fit
  that published empty clouds forever.
- _ground_correct now subtracts the fitted floor offset c after
  leveling, so a depth-scale error can't shift the whole floor across
  the 2 cm obstacle threshold.
- Lidar returns outside the pinhole FOV are rejected before projection;
  the distortion polynomial could fold far-off-axis points back into
  image bounds and bias the Theil-Sen scale fit.
- The depth map is resized to the camera_info resolution before any
  consumer indexes it, and a corrupt color frame is skipped instead of
  crashing the callback.
- The server replies with an H=0,W=0 sentinel for a frame it cannot
  process and survives decode/inference/socket errors; previously one
  corrupt JPEG killed the process for the rest of the mission. The node
  validates msg.format (image_transport style, e.g. "rgb8; jpeg
  compressed bgr8") before sending and handles the sentinel without
  waiting out the socket timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Make the offline depth tools runnable from a clean checkout

depth_obstacles.py, bag_overlay.py, and bev_motion.py hardcoded output
paths under an ephemeral per-session scratch directory that no longer
exists, so the committed validation harness could not reproduce its
results. depth_obstacles.py gains argparse with ~/.mote defaults; the
other two derive their output directory from the bag path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remove the DA3 depth server

The pipeline committed to relative Depth Anything V2-Small (measured
both more accurate and faster); the DA3 server needed its own uv venv
on Python <= 3.13 plus import stubs for unused 3D/video deps, and
duplicated the whole serve loop. depth_bag_eval can still compare
models by pointing at any server that speaks the wire protocol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop dead state and an unused dependency

self.grid was stored but never read after building the undistorted
rays; pixels_to_ground NaN-ed invalid points twice; sensor_msgs_py was
declared in package.xml but never imported.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Consolidate the depth pipeline around shared modules

Review feedback: the code had grown as self-contained files with the
common pieces copy-pasted, the floor-plane scale anchor was still wired
in despite testing showing it unreliable (floor gradients and resting
pitch shift it), and the socket protocol existed only as inline code in
three places.

- New depth_wire.py: the wire protocol in one place — the spec, the
  framing helpers used by the server, a DepthClient (persistent
  connection, reconnect, sentinel handling) used by the node and the
  offline tools, and the rationale for a hand-rolled protocol over
  gRPC/ROS for this link. The node sheds all connection code.
- Floor-plane rescale removed (depth_rescale.py deleted): the node is
  lidar-anchored only — fit, else hold the last good correction, else
  skip the frame loudly. One scale path to debug. The Theil-Sen fit and
  the disparity-affine apply move into lidar_rescale.py; the
  rescale_source parameter goes away.
- GroundProjector gains cached pixel_rays() and back_project(), the one
  implementation of back-projection; the node, depth_bag_eval, and
  depth_obstacles all used private copies (depth_obstacles' copy had
  drifted: it ignored the distortion coefficients).
- tools/bag_utils.py: shared bag loading, base transforms, scan
  matching, colorize — previously four private copies of the rosbag2
  boilerplate.
- depth_obstacles.py rewritten server-based on the shared modules: same
  stages and gates as the live node, BEV with both point sets in
  base_footprint. bag_overlay.py's lidar overlay now transforms through
  /tf_static — the scan frame is yawed 90 deg from base, so plotting
  raw scan coordinates was wrong, not approximate.
- bev_motion.py deleted: the classical motion-parallax prototype the
  learned-depth approach replaced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Update perception docs; drop the misaligned BEV figure

The camera-vs-lidar BEV image was rendered by an ad-hoc script that was
never committed, and the lidar in it does not show a doorway that
should be visible — consistent with plotting raw scan coordinates
without the scan->base transform (the scan frame is yawed 90 deg from
base). Removed rather than defended; depth_obstacles.py is now the
committed, frame-correct generator for a replacement once regenerated
against a live bag.

The README pipeline section drops the floor-fallback description,
points at depth_wire.py for the protocol, and gains a tools inventory.
CLAUDE.md's mote_perception section now describes the L1 pipeline
instead of stopping at L0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant