feat(mapping): add a voxel clear mask so a sensor can erase its own ghost - #3712
Conversation
d713c5a to
3502047
Compare
Greptile SummaryThis change adds a point-cloud clear mask for removing mapped voxels and updates the mapper’s persistent and live-overlay data. Rust tests reproduced that clearing a healthy voxel leaves it eligible for point emission and leaves neighboring support counts inflated, so the clear operation does not fully remove the voxel’s map presence. Confidence Score: 4/5Not safe to merge until voxel clearing synchronizes the healthy-chunk index and neighboring support counters. Focused Rust execution exercised the healthy-voxel clearing path twice and consistently showed that a deleted voxel remains emitted and that a neighbor retains excess support. Files Needing Attention: dimos/mapping/ray_tracing/rust/src/voxel_ray_tracer.rs
What T-Rex did
|
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## main #3712 +/- ##
==========================================
- Coverage 78.01% 78.00% -0.01%
==========================================
Files 1301 1301
Lines 125328 125329 +1
Branches 10952 10952
==========================================
- Hits 97769 97765 -4
- Misses 24375 24381 +6
+ Partials 3184 3183 -1
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 3 files with indirect coverage changes 🚀 New features to boost your workflow:
|
…host Ray tracing only clears what a ray passes through. A sensor that occludes itself - a wrist camera staring past its own arm - can never clear the volume its arm hides: no ray reaches through the arm, so record_miss never fires there. The arm's own returns land in the map and stay, and over a work session the robot slowly walls itself in. A publisher that knows those points are free now says so on a voxel_clear_mask port. The mask is world-frame metric points, stamp-ordered and gated against world_frame, quantized through the same path returns take. VoxelMap::clear_voxels deletes by key, not with a bare remove: it also pulls the key out of the healthy-chunk index emit_points scans and decrements every existing 26-neighbour's support counter, exactly as record_miss does on its removal path. Skipping either leaves an index pointing at a voxel that is gone and support counts that never come back down. An unhealthy voxel was never in its neighbours' counts, so removing it leaves them alone. The fine-cell bitmask rides inside the removed voxel, so the fine layer needs no separate cleanup. Mapper::clear_metric drops this frame's live hits alongside the voxels, so a cleared voxel cannot come back out of the live overlay.
3502047 to
787ae2c
Compare
aclauer
left a comment
There was a problem hiding this comment.
Does setting the shadow_depth config not fix this? It defaults to 0.1
Manipulation becomes the first non-navigation consumer of the ray-tracing voxel map. A wrist camera builds an occupancy map of the workspace, and the planner treats those voxels as one octree obstacle. Three producers, all model-driven so they work for any URDF: - RobotTfPublisher resolves a complete TF tree from measured joint state. A manipulator's sensor pose comes from forward kinematics, not SLAM, and main had no generic equivalent - only a G1-specific publisher. An incomplete joint state publishes nothing rather than leaving links at a stale pose. - PointCloudSelfFilter cuts the robot out of its own camera's cloud, and emits the map cells the robot occupies as a clear mask. It emits the previous frame's cells too: a link that moved leaves a ghost behind it that ray tracing can never clear, because the link occluded that volume while it was there. The mask floors world coordinates by voxel_size exactly as the mapper does, or it would name cells the map does not hold. - GlobalMapObstacleBridge reconciles each complete map snapshot into the planning world under one stable obstacle ID, so a republished map replaces the obstacle instead of piling up new ones. ObstacleType.OCTREE carries occupied cell centers plus a resolution into roboplan's addOcTreeGeometry. Coal reads a cell as center, edge length, cost and occupancy threshold; every cell here is occupied, so cost is 1.0 against the 0.5 default. Verified against roboplan 0.5.1 and the pinned 0.6.1: adding cells flips a collision-free configuration to colliding and blocks a path through them, while a clear path stays clear. Obstacle.points is a tuple of tuples, not an ndarray. Obstacles are deepcopied into world snapshots, compared, and pickled across worker RPC, and an ndarray field breaks all three. The shape-string obstacle RPCs cannot carry a map, so ManipulationModule gains set_voxel_obstacle alongside them. Additive: no existing caller changes. An empty point list removes the obstacle, which is how a mapper says the space it owns is clear. Validation caps an octree at 200k points, since every point crosses worker RPC as a pickled tuple and an unbounded map would stall the pipe. Depends on the voxel_clear_mask port (#3712) for the mask to land anywhere, and on the TFLookup forward_tolerance keyword (#3711) for the self filter to wait on a wrist transform still in flight.
Manipulation becomes the first non-navigation consumer of the ray-tracing voxel map. A wrist camera builds an occupancy map of the workspace, and the planner treats those voxels as one octree obstacle. Three producers, all model-driven so they work for any URDF: - RobotTfPublisher resolves a complete TF tree from measured joint state. A manipulator's sensor pose comes from forward kinematics, not SLAM, and main had no generic equivalent - only a G1-specific publisher. An incomplete joint state publishes nothing rather than leaving links at a stale pose. - PointCloudSelfFilter cuts the robot out of its own camera's cloud, and emits the map cells the robot occupies as a clear mask. It emits the previous frame's cells too: a link that moved leaves a ghost behind it that ray tracing can never clear, because the link occluded that volume while it was there. The mask floors world coordinates by voxel_size exactly as the mapper does, or it would name cells the map does not hold. - GlobalMapObstacleBridge reconciles each complete map snapshot into the planning world under one stable obstacle ID, so a republished map replaces the obstacle instead of piling up new ones. ObstacleType.OCTREE carries occupied cell centers plus a resolution into roboplan's addOcTreeGeometry. Coal reads a cell as center, edge length, cost and occupancy threshold; every cell here is occupied, so cost is 1.0 against the 0.5 default. Verified against roboplan 0.5.1 and the pinned 0.6.1: adding cells flips a collision-free configuration to colliding and blocks a path through them, while a clear path stays clear. Obstacle.points is a tuple of tuples, not an ndarray. Obstacles are deepcopied into world snapshots, compared, and pickled across worker RPC, and an ndarray field breaks all three. The shape-string obstacle RPCs cannot carry a map, so ManipulationModule gains set_voxel_obstacle alongside them. Additive: no existing caller changes. An empty point list removes the obstacle, which is how a mapper says the space it owns is clear. Validation caps an octree at 200k points, since every point crosses worker RPC as a pickled tuple and an unbounded map would stall the pipe. Depends on the voxel_clear_mask port (#3712) for the mask to land anywhere, and on the TFLookup forward_tolerance keyword (#3711) for the self filter to wait on a wrist transform still in flight.
The physics
Ray tracing clears a voxel by passing a ray through it to a farther return. That works when the sensor sees past the thing it is mapping.
A wrist camera does not. Its own arm sits between the lens and the space behind it, so no ray ever reaches through the arm and
record_missnever fires in that volume. Meanwhile the arm's own returns land in the map as ordinary hits. The result: the arm deposits voxels of itself, and there is no mechanism that can ever erase them. Over a work session the robot slowly walls itself in with a ghost of where it has been, and a planner readingglobal_maprefuses to go there.Ray-trace clearing cannot fix this — the geometry forbids it. The information has to come from somewhere else: a publisher that knows, from URDF plus TF, that a given volume is the robot and therefore free.
What this adds
A
voxel_clear_mask: Input<PointCloud2>port. The cloud is world-frame metric points meaning "these are definitely free"; the node deletes the voxels covering them.world_frame. The points are metric positions, so the mask must already be in the world frameTests
cargo test --workspace --all-features --locked: 60 passed.cargo fmt --checkandcargo clippy -D warningsclean..Files: 6.