Add RecGen for object shape completion - #28
Conversation
Behind perception.recgen.enabled in tiptop.yml, replaces the convex-hull mesh built in segment_pointcloud_by_masks with a RecGen reconstruction: post per-object mask + camera-frame RGB-D + intrinsics to the RecGen server, transform the returned mesh cam->world, and pass it through the existing cuRobo conversion. Grasp linking continues to use the masked depth point cloud so behaviour is unchanged. target_faces controls server-side decimation; recommended off in production since RecGen adds ~10-20s per object vs convex hulls. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "close" gripper action's label parser used \w+ which excludes apostrophes, so labels like "Pick(Rubik's_cube, grasp1, q1)" captured only "Rubik" and crashed lookup with KeyError. Capture up to the next comma instead, which is the actual end of the first argument. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds an optional RecGen-based per-object mesh reconstruction path to replace the existing convex-hull mesh generation in segment_pointcloud_by_masks, gated by perception.recgen.enabled in tiptop.yml. It introduces a new async RecGen client + shape-completion wrapper, integrates it into run_perception/scene processing, and includes a small regex fix in the run-visualization script for object labels containing apostrophes.
Changes:
- Add
tiptop.perception.recgen(HTTP msgpack client + health check) andtiptop.perception.shape_completion(per-mask RecGen reconstruction + masked depth PCD creation). - Update
tiptop_run.pyto optionally call RecGen during scene geometry processing and include RecGen in server health checks when enabled. - Fix label parsing in
viz_tiptop_run.pyto handle object names with apostrophes.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
tiptop/tiptop_run.py |
Integrates RecGen shape completion behind config and updates server health checks + perception pipeline call sites. |
tiptop/scripts/viz_tiptop_run.py |
Adjusts regex for parsing grasped object names from action labels. |
tiptop/perception/shape_completion.py |
New RecGen-based per-object reconstruction + masked depth point cloud generation. |
tiptop/perception/recgen.py |
New async RecGen HTTP client (msgpack payloads) + health check helper. |
tiptop/config/tiptop.yml |
Adds perception.recgen config block (url/enabled/target_faces). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Skip object on empty masked depth instead of raising. Avoids aborting the whole pipeline when a single object's mask has no above-table data — matches the convex-hull path's behaviour. - Drop mask erosion from the RecGen path. Erosion can fully erase thin objects, and RecGen handles edge noise on its own. - Fix the pre-existing process_scene_geometry docstring: rgb_map is in [0, 1] (predict_depth_and_grasps normalises it). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Removing erosion outright regressed scene 5 — RecGen reconstructs from the noisier full mask, mesh sizes grow (green_block 56k -> 159k faces), and the place pose for Place(yellow_block, ..., bowl) now starts in collision. Mirror segment_pointcloud_by_masks instead: erode, then fall back to the un-eroded mask if fewer than 10 valid points remain (the thin-objects case Copilot flagged). Integ suite back to 4/5 passing; scene 2 still fails for the known recgen-side can-shape artifact called out in the PR description. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
williamshen-nz
left a comment
There was a problem hiding this comment.
Added comments
- process_scene_geometry back to sync; RecGen call moved up to run_perception so the existing asyncio.to_thread wrap stays at the call site (Copilot's event-loop blocking concern). - Helper _filter_pcds_above_z applies the table-relative z filter + outlier removal once after RecGen; shape_completion no longer needs max_z. - Guard outlier removal: skip objects with < 10 above-table points. - Per-object RecGen timing log moved from generate_shape_async into shape_completion so it carries the object label. - recgen.py: document the response payload keys/shapes; drop unnecessary np.ascontiguousarray calls (msgpack-numpy handles non-contig fine). - shape_completion.py: trim module docstring; drop the fix-history narration. - viz_tiptop_run.py: inline comment explaining the [^,]+ regex captures labels with apostrophes like "Rubik's_cube". - Pre-commit reformatting across all four files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Rename `checks` to `health_checks` in check_server_health for clarity. - Add a comment at the RecGen dispatch site in run_perception explaining why the await happens here rather than inside process_scene_geometry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related bugs Copilot flagged: - reconstruct_objects_with_recgen could call the (~10–20s) RecGen server for degenerate masks/depth (fewer than 10 valid points even after the eroded -> un-eroded fallback). Skip the object instead, matching segment_pointcloud_by_masks. - _filter_pcds_above_z can drop a label when nothing survives the table-relative z filter, but the caller wasn't filtering recgen_meshes to match — the downstream loop would KeyError on object_pcds[label]. Drop the mesh entry too, and assert recgen_pcds is paired with recgen_meshes when set. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Drop unused cfg local in process_scene_geometry; restore inline tiptop_cfg().perception.mask_erosion_pixels matching the surrounding style. - Trim the recgen dispatch-site comment in run_perception from 4 lines to 2. - Remove stray double-backticks from the recgen_pcds docstring. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
remove_statistical_outlier(nb_neighbors=10, ...) needs more than 10 points to be well-defined (kNN with k=10 needs strictly more than 10 neighbors to choose from). Skip the object with <= 10 points above the table rather than risk an Open3D error on degenerate inputs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
williamshen-nz
left a comment
There was a problem hiding this comment.
Getting closer and closer!
- tiptop.yml: link to the RecGen server repo above the recgen url. - recgen.py: restructure generate_shape_async docstring to Google style with Args/Returns, move the target_faces note above the Args block. - recgen.py: drop the defensive (mask > 0).astype(np.uint8) — the type hint already promises bool and msgpack-numpy / the recgen server both handle bool fine. - tiptop_run.py: reword the recgen_meshes/recgen_pcds docstring to not overclaim that the meshes are unchanged RecGen output (we transform to world frame in shape_completion). - tiptop_run.py: trim the dispatch-site comment to one line; drop the rationale. - tiptop_run.py: inline the _filter_pcds_above_z helper — it was only used in the RecGen branch, so extracting it created an inconsistency with the convex-hull path that also does its own filtering. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
I think recgen doesn't like top down viewpoints. Maybe try front facing more on the robot |
If every object is skipped (too few points above the table), object_pcds is empty and the KDTree construction crashes with a cryptic np.vstack([]) error. Raise a clear error instead. Covers both the RecGen and convex-hull paths since the guard sits after object_pcds is finalized. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The only caller never passed object_pcds, so it was always None and the code always fell back to the computed point clouds. Drop the parameter and the dead fallback branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The gripper "close" action's label parser used \w+, which excludes apostrophes, so a label like "Pick(Rubik's_cube, grasp1, q1)" captured only "Rubik" and crashed the object lookup with KeyError. Capture up to the next comma instead, which is the actual end of the first argument. Split out from #28.
# Conflicts: # tiptop/recording.py # tiptop/scripts/viz_tiptop_run.py
williamshen-nz
left a comment
There was a problem hiding this comment.
Self-review after the main sync. Overall this is in good shape to land as an experimental, off-by-default feature.
The thing that mattered most — the default convex-hull path is unchanged. The only code shared between the two paths is the extracted masked_object_points, and I checked it line-for-line against the old inlined logic in segment_pointcloud_by_masks: same erosion, same < 10 valid-point threshold, same un-eroded fallback, same return. The recgen_meshes is None branch calls segment_pointcloud_by_masks exactly as before, and removing the never-used object_pcds param is safe. The new if not object_pcds: raise guard fires on both paths but only replaces a would-be cryptic np.vstack([]) crash with a clear error — strict improvement. So no regression risk to the production path.
A few things I verified while reviewing, for the record: depth handed to RecGen (depth_results["depth_map"]) is the same meters-scale map depth_to_xyz consumes, so units are consistent; the asyncio.run(_check()) health check is only reached from the synchronous main(), so there's no nested-event-loop hazard; and nothing else in the tree uses msgpack, so the global patch is currently isolated.
The inline notes below are all non-blocking engineering discussion, not correctness blockers. One more minor consistency nit not worth an inline: reconstruct_objects_with_recgen normalizes masks with a guarded masks[:, 0], while the RecGen branch in process_scene_geometry uses an unguarded masks.squeeze(1) — harmless since both assume (n, 1, h, w), but worth unifying on one idiom eventually.
Replace the module-level msgpack_numpy.patch() with explicit default=encode / object_hook=decode on RecGen's packb/unpackb. patch() globally swaps msgpack's default codecs for the whole process and ran at import even with recgen disabled, so it also governed bamboo's robot-control messages, which use stock msgpack and convert numpy to lists themselves. A stray numpy value in a bamboo command that stock msgpack would reject loudly could otherwise be silently encoded as a numpy ext-type the C++ control node can't parse. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The explicit encode/decode hooks at the call sites are self-evident; the module-level note on why msgpack_numpy.patch() is avoided was iteration rationale that belongs in the PR description. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1k visibly under-tessellates reconstructed meshes in Rerun. The face count only affects visualization and the static world mesh: cuTAMP samples movable-object collision spheres via area-weighted surface sampling (sample_greedy_surface_spheres, 50 spheres @ 5mm), which is insensitive to tessellation. The bump is effectively free. Per-object response grows 0.02 -> 0.20 MB against a 7.4 MB per-object request (rgb+depth+mask), so it is ~3% of transfer. RecGen wall time is unchanged: 5-object runs average 20.1s at 1k and 19.7s at 10k, with fully overlapping ranges. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RecGen deliberately receives the un-eroded mask while the point cloud path erodes; the asymmetry reads as a bug without a note, so record it at the source. Also log the previously-silent skip when a label has no RecGen mesh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Self-review pass before merge. Three corrections to the record above, since the thread currently describes behaviour the branch no longer has. Mask erosion — the earlier note is superseded. My comment on Worth noting the old failure signal is now masked: the The pre-POST skip from Leaving it out. The object is still logged when dropped ( Health check scope. The description's "fails fast before the slow cuRobo warmup" holds for the live path only. Also in this pass
Verified unchanged for the convex-hull path: the Known follow-ups, none blocking: RecGen re-uploads identical |
…rning The 600s per-request timeout was 30-60x the ~10-20s per object RecGen actually takes, and a large outlier against the other perception clients (FoundationStereo 10s, M2T2 30s, SAM2 30s). At ten minutes a wedged server reads as a hang; 180s still leaves generous headroom while failing while someone is watching. The experimental warning now reports object count and concurrency, the two facts that predict the wait, so the delay is expected rather than surprising. It deliberately quotes no per-object latency: that depends on the server's GPUs and what else is in flight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds an Unreleased section covering the six PRs merged since v0.2.0 plus the RecGen work in #28, so the 0.3.0 notes are not reconstructed from git log later. Documents the two experimental features, which had no coverage at all, on a new Experimental Features page: place-next-to and RecGen shape completion. Both are off by default and share the same opt-in framing, and RecGen's setup, config, and caveats belong together rather than split across three pages. Server setup links out to the recgen repo instead of duplicating it. Cross-linked from the convex-hull section of Limitations, which describes the limitation RecGen mitigates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds CHANGELOG.md covering everything since v0.2.0 as 0.3.0, and a new docs/experimental.md for the two experimental features (place-next-to and RecGen), neither of which had any docs. Cross-linked from Limitations, Getting Started, and Evaluation. The RecGen runtimes on that page are measured across the 26 recorded runs rather than the "~10-20s per object" estimate from #28: requests fan out across GPUs, so total time tracks the slowest object until they start queueing. Also moves pick-only instructions from the "bad" to the "good" list in Getting Started, since #27 made them work.
Summary
Replaces the convex-hull mesh built per object in
segment_pointcloud_by_maskswith a RecGen reconstruction, behindperception.recgen.enabledintiptop.yml(off by default). Per object: POST the camera-frame RGB-D + mask + intrinsics to the RecGen server, transform the returned camera-frame mesh into world frame viaworld_from_cam, and feed the result through the existing cuRobo conversion. Grasp linking continues to use the masked depth point cloud — behaviour is unchanged for that path.perception.recgen.target_facesenables server-side quadric edge-collapse decimation (usesfast-simplification, lives in recgen/scripts/server.py) to keep wire size down — raw RecGen meshes are ~0.5–1.5M faces / ~30 MB each; decimated to ~1000 faces / ~30 KB. The recgen server change is already onmainthere.Per-object requests fan out concurrently (bounded by
perception.recgen.concurrency, ~= the gateway's GPU count) rather than serially. RecGen is awaited up-front inrun_perception;process_scene_geometrystays synchronous and runs in a worker thread so the event loop isn't blocked, deriving each object's point cloud from its mask. Objects whose masked depth has too few valid points are skipped, and the mesh/pcd/grasp dicts are kept in sync.RecGen is recommended off in production: it adds ~10–20s per object vs convex hulls.
Also includes a perception-server health check (FoundationStereo, M2T2, and RecGen when enabled) before the slow cuRobo IK/MotionGen warmup, so a down server fails fast.
Test plan
pixi run test-integration— 5/5 passed on the latest full run (scene 2 is borderline; see known issues)pixi run tiptop-h5 --h5-path tests/assets/tiptop_scene1_obs.h5 --task-instruction "Put the Rubik's cube in the bowl."— perception + plan complete, recgen meshes look right in rerunpixi run tiptop-h5 --h5-path tests/assets/tiptop_scene5_obs.h5 --task-instruction "Put 3 blocks in the bowl."— 4 movables + bowl reconstructed, 25-step plan foundpixi run viz-tiptop-run --save-dir <run>— perception + plan viz both render correctlyenabled: false) — full integ suite passes, confirming no regression to the existing pathKnown issues / follow-ups
Put the can in the mug.) is borderline. RecGen sometimes reconstructs the (partially occluded) can over-extended in X — observed9.4 × 4.7 × 7.6 cmvs the expected ~6.5 × 6.5 × 12 — which makes it wider than the 8 cm mug and fails cuTAMP'sStablePlacementconstraint (0/256 satisfying). It passed on the latest run, but the reconstruction quality on this occluded object is variable. Recgen-side quality issue, not an integration bug; the convex-hull path is stable here because it's dominated by observed depth.green_blockin scene 5: two near-identical overlapping cuboid meshes). Quadric edge collapse can't merge geometrically distinct surfaces, so decimation can't bring those below ~50k faces. Workarounds (largest-component filtering / vertex welding) not implemented here — flagged for recgen-side investigation.target_faces=10000is the default (raised from 1000 in 9d42739). 1000 visibly under-tessellates meshes in Rerun. The bump is effectively free: face count only affects visualisation and the static world mesh, since cuTAMP samples movable-object collision spheres via area-weighted surface sampling (sample_greedy_surface_spheres, 50 spheres @ 5mm), which is insensitive to tessellation. Per-object response grows 0.02 → 0.20 MB against a 7.4 MB per-object request, and RecGen wall time is unchanged (5-object runs: 20.1s avg at 1k, 19.7s at 10k).2026-07-31 — synced with
main. Mergedmainin (no rebase/force-push, history preserved). The two incidental fixes this PR originally bundled have since landed separately and were dropped from the branch during the merge (they now matchmain):The branch now reduces to the RecGen shape-completion feature plus the pre-warmup server health check.
enabled: falseis already the default, so no pre-merge config flip is needed.