Skip to content

fix(rerun): stabilize Go2 3D detection previews - #3571

Open
HaroldGin931 wants to merge 2 commits into
dimensionalOS:mainfrom
HaroldGin931:fix/rerun-variable-size-detection-images
Open

fix(rerun): stabilize Go2 3D detection previews#3571
HaroldGin931 wants to merge 2 commits into
dimensionalOS:mainfrom
HaroldGin931:fix/rerun-variable-size-detection-images

Conversation

@HaroldGin931

Copy link
Copy Markdown

Contribution path

  • Small, safe change that does not need a tracking issue

Problem

unitree-go2-detection publishes cropped detection images to Rerun. Because crop dimensions vary between frames, sending them as JPEG rr.EncodedImage values under the same entity path makes Rerun treat them as a continuous video stream whose encoding properties change over time.

In our reproduction, the replay can continue running if these dynamic crop entities are not inspected in the Rerun Viewer. However, selecting one of the affected image entities and displaying it in the Viewer panel triggers the rendering path and can cause the Rerun Viewer process to panic.

The issue can be reproduced by running:

uv run dimos --replay --replay-db go2_short run unitree-go2-detection

And click following area,

Rerun Viewer screenshot:

image

Key Rerun log:

[2026-08-20T16:50:45Z WARN  re_viewer_context::cache::video_stream_cache]
Detected change of video encoding properties (like size, bit depth, compression etc.) over time. This is not supported and may cause playback issues.

[2026-08-20T16:50:50Z ERROR re_viewer_context::cache::video_stream_cache]
The video stream codec details of "/world/detected_image_0" changed
(from VideoEncodingDetails {
    codec_string: "image/jpeg",
    coded_dimensions: [704, 627],
    bit_depth: Some(8),
    chroma_subsampling: None,
    stsd: None
} to VideoEncodingDetails {
    codec_string: "image/jpeg",
    coded_dimensions: [190, 265],
    bit_depth: Some(8),
    chroma_subsampling: None,
    stsd: None
}). This is not supported.

[2026-08-20T16:50:50Z ERROR re_viewer_context::cache::video_stream_cache]
The video stream codec details of "/world/detected_image_1" changed
(from VideoEncodingDetails {
    codec_string: "image/jpeg",
    coded_dimensions: [196, 405],
    bit_depth: Some(8),
    chroma_subsampling: None,
    stsd: None
} to VideoEncodingDetails {
    codec_string: "image/jpeg",
    coded_dimensions: [86, 213],
    bit_depth: Some(8),
    chroma_subsampling: None,
    stsd: None
}). This is not supported.

thread 'main' panicked at
'range end index 73696 out of range for slice of length 73272'

re_renderer/src/resource_managers/image_data_to_texture.rs:383

There is also a separate slot-alignment problem in the existing Detection3DModule visualization:

  1. The 2D preview slots are selected directly from the first three 2D detections.
  2. Some detections can be discarded during 3D projection because no valid point cloud is produced.
  3. The remaining successful 3D detections are compacted before being published.
  4. As a result, detected_image_N and detected_pointcloud_N can refer to different detections.

This is a slot-compaction mismatch rather than a temporal synchronization problem.

A runnable reproduction is available on the following branch:

https://github.com/HaroldGin931/dimos/tree/chore/repro-go2-2d-3d-crop-mismatch

Run it with:

uv run dimos --replay --replay-db go2_short run unitree-go2-detection

The reproduction compares hashes from the original 2D detection crops with the crops associated with the compacted 3D results. It pauses Rerun on the first mismatch and prints output similar to:

Detection crop mismatch; pausing Rerun
  2D display:
    slot: 0
    name: bottle
    hash: 701b21f6700c...
  3D display:
    slot: 0
    name: bottle
    hash: 5e892938c53b...
  Source:
    3D display slot 0 image comes from 2D detection slot 1

In this example, 2D display slot 0 and 3D display slot 0 have different image hashes. The 3D result shown in slot 0 originated from 2D detection slot 1 because an earlier detection was discarded during 3D projection.

After Rerun pauses, the corresponding 2D and 3D slot entries can be inspected in the Viewer. Their displayed crops are visibly different, demonstrating that the original visualization incorrectly treated the two independently ranked slots as a matching image and point-cloud pair.

This PR only fixes the Go2 visualization path and does not change the underlying 2D-to-3D filtering behavior. We do not currently have access to a G1 or other affected robots for validation, so this PR makes the smallest change that can be tested end-to-end on Go2. Other Blueprints publishing variable-sized detected_image_* streams may require a separately tested follow-up.

Solution

  • Publish a crop from each successful 3D detection alongside its corresponding point cloud.

  • Add detected_3d_image_0/1/2 outputs so the image and point cloud originate from the same Detection3DPC.

  • Letterbox each 3D crop to a fixed 320×320 preview while preserving its aspect ratio.

  • Map each crop and point-cloud pair to the same Rerun entity, for example:

    world/detections/3d/slot_0
    

    This allows selecting one slot in Rerun and inspecting both its image and point cloud.

  • Disable the inherited raw 2D crop previews in the Go2 detection Blueprint because those slots are ranked before unsuccessful 3D projections are removed.

  • Reuse the existing Go2 visualization bundle and Rerun layout rather than adding a second vis_module().

  • Keep the global Image.to_rerun() behavior unchanged so fixed-resolution camera streams continue using rr.EncodedImage.

How to Test

uv run dimos --replay --replay-db go2_short run unitree-go2-detection

Verify that:

  • The replay enters normal operation.
  • Selecting a 3D detection slot shows its crop and corresponding point cloud.
  • Detection previews remain fixed at 320×320.
  • Rerun no longer reports changing video encoding properties for the 3D crop entities.
  • The Rerun Viewer does not panic when selecting detection entries.
  • The original camera stream, Pinhole visualization, and 3D projection remain available.

AI assistance

OpenAI Codex with GPT-5 assisted with codebase analysis, debugging, implementation, validation, and drafting this PR description. The changes were manually reviewed and the replay behavior was manually verified.

Checklist

  • I have read and approved the CLA.

@github-actions github-actions Bot added the first-time-contributor PR opened by an author who had not previously committed to this repository label Aug 20, 2026
@greptile-apps

greptile-apps Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This change adds fixed-size letterboxed previews for successful 3D detections and routes detection images and point clouds through shared Go2 visualization slots.

The viewer-disabled configuration is not preserved: the detection blueprint directly creates a Rerun bridge even though the viewer="none" configuration omits that bridge. As a result, deployments that disable visualization can still start Rerun publishing.

The reported shared-slot mapping issue was dropped after an executed check exercised /detector3d/3d/slot_0/image, /detector3d/3d/slot_0/pointcloud, and the corresponding slot-2 paths. Each path has four components and resolves to the same world/detections/3d/slot_N entity, contradicting the reported failure.

Confidence Score: 4/5

Not safe to merge as-is because a deployment configured with no viewer can still start the Rerun bridge.

A focused executable source probe confirmed the viewer-none baseline, the unconditional bridge declaration, and the coordinator startup path. The shared-slot topic behavior was also directly exercised and behaved correctly.

Files Needing Attention: dimos/robot/unitree/go2/blueprints/smart/unitree_go2_detection.py needs its Rerun bridge construction to respect the selected viewer backend.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex authored a viewer-none blueprint introspection script and generated logs to observe inherited and detection Go2 blueprint behavior with viewer none.
  • T-Rex validated the executable check by running the detector3d topic-mapping script and reviewing the output log, confirming exit code 0 and exact source usage.
  • T-Rex recorded a posted P1 finding proof and the accompanying review reference.
  • T-Rex documented the transition from inherited capture to detection capture, including the start of all modules and the blocked full import due to missing pydantic_settings, with a final dependency-free probe succeeding against the exact files.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 Detection blueprint re-enables Rerun bridge when viewer is none

    • Bug
      • vis_module's viewer="none" branch omits RerunBridgeModule and creates only WebsocketVisModule at dimos/visualization/vis_module.py:71-72. However, dimos/robot/unitree/go2/blueprints/smart/unitree_go2_detection.py:54-64 unconditionally supplies RerunBridgeModule.blueprint(**detection_rerun_config) at line 59. The coordinator subsequently deploys and starts active modules at dimos/core/coordination/module_coordinator.py:365-370.
    • Cause
      • The detection blueprint directly adds a bridge independently of the viewer-selected visualization bundle, so it bypasses the inherited viewer="none" omission.
    • Fix
      • Construct the detection-specific Rerun configuration through the viewer-aware visualization factory, or conditionally add the replacement bridge only when the resolved viewer backend is rerun.

    T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "Merge branch 'main' into fix/rerun-varia..." | Re-trigger Greptile

unitree_go2,
# Replaces the RerunBridgeModule already present in unitree_go2 while
# leaving its single vis_module bundle and websocket modules intact.
RerunBridgeModule.blueprint(**detection_rerun_config),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Viewer-none configuration still starts Rerun

The base Go2 visualization configuration omits RerunBridgeModule when the viewer backend is none, but this blueprint unconditionally adds a replacement bridge here. Since active blueprint modules are started by the coordinator, a deployment that explicitly disables the viewer still starts Rerun publishing. Build the detection-specific bridge through the viewer-aware visualization configuration, or add it only when the resolved backend is Rerun.

Artifacts

Authored viewer-none blueprint introspection script

  • Dependency-free AST probe used to inspect the exact viewer-none branch, detection bridge declaration, and coordinator startup path; it provides the executable comparison method.

Inherited Go2 blueprint behavior with viewer none

  • Executed inherited-case capture shows the viewer-none branch includes WebsocketVisModule and zero inherited Rerun bridges, establishing the baseline.

Detection Go2 blueprint behavior with viewer none

  • Executed detection-case capture shows the direct bridge at line 59 and the coordinator startup call, confirming viewer-none does not suppress this bridge.

View artifacts

T-Rex Ran code and verified through T-Rex

@leshy leshy Aug 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems legit and it bugs me a bit, our impl of how we add viewers is a bit messy but can you sort something out? other then that tested on my end ready to merge

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core issue: vis_module() in unitree_go2_basic.py is viewer-aware (skips RerunBridgeModule for "none"), but the detection blueprint calls RerunBridgeModule.blueprint() unconditionally, bypassing that guard.

The clean fix is to mirror the same guard from vis_module():

Suggested change
RerunBridgeModule.blueprint(**detection_rerun_config),
detection_rerun_config = {
**rerun_config,
"topic_to_entity": _detection_topic_to_entity,
}
from dimos.core.global_config import global_config
def _detection_vis_modules() -> list:
"""Return bridge module only when the configured viewer is Rerun."""
if global_config.viewer == "rerun":
return [RerunBridgeModule.blueprint(**detection_rerun_config)]
return []
unitree_go2_detection = (
autoconnect(
unitree_go2,
# Replaces the RerunBridgeModule already present in unitree_go2 while
# leaving its single vis_module bundle and websocket modules intact.
# Guarded so deployments with viewer=none skip this entirely.
*_detection_vis_modules(),

That said, the messiness you're pointing at is real: vis_module() encapsulates the backend switch cleanly, but detection_rerun_config needs topic_to_entity which vis_module() doesn't expose as a first-class parameter. The better longer-term fix is to thread topic_to_entity (and other bridge-specific overrides) through vis_module() so that all blueprints go through the same guarded factory rather than calling RerunBridgeModule.blueprint() directly. That would be a small addition to vis_module's rerun_config dict — RerunBridgeModule already accepts it — but it's a broader change across all robot blueprints.

For now the guard above unblocks the merge. Happy to draft the vis_module() refactor as a follow-up if useful.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is simple — swap the direct RerunBridgeModule.blueprint() call for vis_module(), matching the pattern in unitree_go2_basic.py:

Import changes:

Suggested change
RerunBridgeModule.blueprint(**detection_rerun_config),
+from dimos.core.global_config import global_config
from dimos.robot.unitree.go2.blueprints.basic.unitree_go2_basic import rerun_config
from dimos.robot.unitree.go2.blueprints.smart.unitree_go2 import unitree_go2
from dimos.robot.unitree.go2.connection import GO2Connection
-from dimos.visualization.rerun.bridge import RerunBridgeModule
+from dimos.visualization.vis_module import vis_module

Call site (replacing the RerunBridgeModule.blueprint(...) line):

Suggested change
RerunBridgeModule.blueprint(**detection_rerun_config),
+ # Replaces the vis_module bundle already present in unitree_go2 while
+ # leaving its websocket module intact.
+ vis_module(
+ viewer_backend=global_config.viewer,
+ rerun_config=detection_rerun_config,
+ ),

vis_module() already has the match viewer_backend dispatch — "rerun" adds the bridge, "none" skips it entirely. No other changes needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

first-time-contributor PR opened by an author who had not previously committed to this repository

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants