Skip to content

Fm/tianji undistort - #854

Open
farbod-nv wants to merge 1 commit into
mainfrom
fm/tianji_undistort
Open

Fm/tianji undistort#854
farbod-nv wants to merge 1 commit into
mainfrom
fm/tianji_undistort

Conversation

@farbod-nv

@farbod-nv farbod-nv commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #(issue)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Testing

Checklist

  • I have read and understood the contribution guidelines
  • I have run the linter and formatter with SKIP=check-copyright-year pre-commit run --all-files
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix/feature works (or explained why not)
  • I have signed off all my commits (git commit -s) per the DCO

Summary by CodeRabbit

  • New Features

    • Added native OpenXR cylinder and equirectangular texture layers alongside quad layers.
    • Added Python APIs for creating, submitting, positioning, and configuring the new layer types.
    • Added configurable OpenXR or built-in compositing, alpha blending, curved display shapes, and gimbal placement.
    • Added lens calibration and GPU-based undistortion options for camera visualization.
    • Added runtime detection for supported OpenXR composition-layer shapes.
  • Documentation

    • Expanded layer, camera streaming, CloudXR, configuration, placement, calibration, and troubleshooting guidance.
    • Updated camera visualization examples and configurations with new shape and compositor options.
  • Bug Fixes

    • Improved stereo submission handling, placement behavior, runtime startup, cleanup, and camera frame-rate reporting.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 70cb6b23-91c3-44c8-83e1-c44d6fc94334

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds native OpenXR cylinder and equirectangular layers alongside generalized quad-layer handling, shared CUDA image mailboxes, backend shape validation, and Python bindings. The camera visualization example gains configurable surface shapes, CloudXR startup handling, gimbal placement, GPU lens undistortion, calibration-based geometry, and runtime statistics. Tests and documentation cover the new APIs, OpenXR constraints, placement behavior, remapping pipeline, and configuration options.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CameraSource
  participant UndistortSource
  participant VizRunner
  participant VizSession
  participant XrBackend
  participant OpenXrRuntime
  CameraSource->>UndistortSource: Produce calibrated frame
  UndistortSource->>VizRunner: Return remapped frame
  VizRunner->>VizSession: Submit layer image
  VizSession->>XrBackend: Record native layer
  XrBackend->>OpenXrRuntime: Submit shaped composition layer
Loading

Possibly related PRs

  • NVIDIA/IsaacTeleop#840: Covers the same Televiz documentation and native cylinder/equirectangular layer API expansion.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title hints at undistortion, but the added "Fm/tianji" prefix makes it too opaque to clearly describe the PR. Use a clearer title like "Add calibrated camera undistortion to camera_viz".
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fm/tianji_undistort

Comment @coderabbitai help to get the list of available commands.

"SourceSpec",
"UndistortSettings",
"UndistortSource",
"VizRunner",
Point a camera at its calibration JSON and the feed is undistorted on
the GPU before display. A per-eye sampling LUT is precomputed once from
the intrinsics + distortion (+ optional stereo rectification, folded in
so pairs come out row-aligned); each frame is then a single bilinear
remap kernel on the producer's CUDA stream. Kannala-Brandt fisheye and
Brown-Conrady models; the gr00t shw5g ChArUco JSON is accepted
verbatim; buffers materialize lazily on the frame's device (multi-GPU
hosts).

The remap targets the projection matching the display shape — quad →
rectilinear, cylinder → cylindrical, equirect → equirectangular — so
the shown image is angle-correct end to end. A calibrated camera
defaults to shape: cylinder (rectilinear stretches badly at wide FOV
and is refused beyond 160°; cylindrical keeps the full FOV at uniform
angular resolution — the gr00t WebXR client's in-shader undistort is
rectilinear-only). Output FOV is derived from the calibration (Newton
inversion of the fisheye polynomial at the image borders) and the
layer geometry follows automatically: cylinder arc + aspect = the
camera's true FOV, quads sized to the FOV at the configured distance —
1:1 visual angle with no manual tuning; undistort.hfov_deg/vfov_deg/
out_width/out_height override.

UndistortSource wraps any FrameSource (all camera types + RTP), so the
remap runs exactly once per new frame. 10 new tests (LUT math on CPU,
GPU remap round-trip); validated live against CloudXR with the real
shw5g stereo calibration (auto FOV 127.4x101.8 deg, zero pipeline loss
at 2560x1984 stereo).

Signed-off-by: Farbod Motlagh <fmotlagh@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/camera_viz/camera_viz.py`:
- Around line 255-264: Update the calibrated size calculation in the
resolve_settings flow so tan-based FOV sizing is used only for quad/rectilinear
shapes with valid rectilinear FOVs. For cylinder/equirect cameras or FOVs
outside the rectilinear range, retain the aspect-derived _DEFAULT_PLANE_WIDTH_M
fallback before assigning spec["size"].
- Around line 534-544: Update the pre-flight shape check around _shape_for to
pass the same calibration presence used by _build_local_entries and
_build_rtp_entries, specifically whether "calib" exists in each camera config.
Keep the existing XR-mode validation and quad exemption unchanged so calibrated
cameras with no explicit shape are correctly treated as cylinders.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ac2db08c-e838-4deb-8a01-15c925b968fc

📥 Commits

Reviewing files that changed from the base of the PR and between 745559b and b0f22d2.

📒 Files selected for processing (44)
  • docs/source/getting_started/televiz.rst
  • docs/source/references/camera_streaming.rst
  • examples/camera_viz/README.md
  • examples/camera_viz/camera_viz.py
  • examples/camera_viz/configs/oakd.yaml
  • examples/camera_viz/configs/realsense.yaml
  • examples/camera_viz/configs/replay.yaml
  • examples/camera_viz/configs/synthetic.yaml
  • examples/camera_viz/configs/synthetic_stereo.yaml
  • examples/camera_viz/configs/v4l2.yaml
  • examples/camera_viz/configs/zed.yaml
  • examples/camera_viz/pipeline/__init__.py
  • examples/camera_viz/pipeline/runner.py
  • examples/camera_viz/pipeline/undistort.py
  • examples/camera_viz/placements/lock_modes.py
  • examples/camera_viz/scripts/_install_deps.sh
  • examples/camera_viz/sources/zed.py
  • examples/camera_viz/tests/test_undistort.py
  • src/viz/layers/cpp/CMakeLists.txt
  • src/viz/layers/cpp/cylinder_layer.cpp
  • src/viz/layers/cpp/equirect_layer.cpp
  • src/viz/layers/cpp/image_layer_base.cpp
  • src/viz/layers/cpp/inc/viz/layers/cylinder_layer.hpp
  • src/viz/layers/cpp/inc/viz/layers/equirect_layer.hpp
  • src/viz/layers/cpp/inc/viz/layers/image_layer_base.hpp
  • src/viz/layers/cpp/inc/viz/layers/projection_layer.hpp
  • src/viz/layers/cpp/inc/viz/layers/quad_layer.hpp
  • src/viz/layers/cpp/quad_layer.cpp
  • src/viz/layers_tests/cpp/CMakeLists.txt
  • src/viz/layers_tests/cpp/test_quad_layer.cpp
  • src/viz/layers_tests/cpp/test_shaped_layers.cpp
  • src/viz/python/layers_bindings.cpp
  • src/viz/python/session_bindings.cpp
  • src/viz/python/viz_init.py
  • src/viz/session/cpp/inc/viz/session/display_backend.hpp
  • src/viz/session/cpp/inc/viz/session/layer_base.hpp
  • src/viz/session/cpp/inc/viz/session/viz_session.hpp
  • src/viz/session/cpp/inc/viz/session/xr_backend.hpp
  • src/viz/session/cpp/viz_compositor.cpp
  • src/viz/session/cpp/viz_session.cpp
  • src/viz/session/cpp/xr_backend.cpp
  • src/viz/session_tests/cpp/test_quad_milestone.cpp
  • src/viz/xr/cpp/inc/viz/xr/openxr_session.hpp
  • src/viz/xr/cpp/openxr_session.cpp

Comment on lines 255 to +264
if spec is not None and "size" not in spec:
spec = {
**spec,
"size": [_DEFAULT_PLANE_WIDTH_M, _DEFAULT_PLANE_WIDTH_M * height / width],
}
if undistort is not None:
d = float(spec.get("distance", PlacementConfig.distance))
size = [
2.0 * d * math.tan(undistort.hfov_rad / 2.0),
2.0 * d * math.tan(undistort.vfov_rad / 2.0),
]
else:
size = [_DEFAULT_PLANE_WIDTH_M, _DEFAULT_PLANE_WIDTH_M * height / width]
spec = {**spec, "size": size}

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Calibrated size uses tan(fov/2) regardless of shape — negative/degenerate size for wide-FOV cylinder feeds.

resolve_settings only rejects FOV ≥ 160° for rectilinear; a cylinder/equirect camera legitimately keeps hfov_rad > π (see test_rectilinear_rejects_degenerate_fov). 2 d tan(hfov/2) is then negative, and that value is fed into PlacementConfig.size_meters → placement validation blows up (or a garbage quad size is propagated). Gate the angular sizing to the quad shape, or fall back to the aspect-derived size when hfov/vfov exceed the rectilinear range.

🐛 Sketch
-        if undistort is not None:
+        if undistort is not None and undistort.projection == "rectilinear":
             d = float(spec.get("distance", PlacementConfig.distance))
             size = [
                 2.0 * d * math.tan(undistort.hfov_rad / 2.0),
                 2.0 * d * math.tan(undistort.vfov_rad / 2.0),
             ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if spec is not None and "size" not in spec:
spec = {
**spec,
"size": [_DEFAULT_PLANE_WIDTH_M, _DEFAULT_PLANE_WIDTH_M * height / width],
}
if undistort is not None:
d = float(spec.get("distance", PlacementConfig.distance))
size = [
2.0 * d * math.tan(undistort.hfov_rad / 2.0),
2.0 * d * math.tan(undistort.vfov_rad / 2.0),
]
else:
size = [_DEFAULT_PLANE_WIDTH_M, _DEFAULT_PLANE_WIDTH_M * height / width]
spec = {**spec, "size": size}
if spec is not None and "size" not in spec:
if undistort is not None and undistort.projection == "rectilinear":
d = float(spec.get("distance", PlacementConfig.distance))
size = [
2.0 * d * math.tan(undistort.hfov_rad / 2.0),
2.0 * d * math.tan(undistort.vfov_rad / 2.0),
]
else:
size = [_DEFAULT_PLANE_WIDTH_M, _DEFAULT_PLANE_WIDTH_M * height / width]
spec = {**spec, "size": size}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/camera_viz/camera_viz.py` around lines 255 - 264, Update the
calibrated size calculation in the resolve_settings flow so tan-based FOV sizing
is used only for quad/rectilinear shapes with valid rectilinear FOVs. For
cylinder/equirect cameras or FOVs outside the rectilinear range, retain the
aspect-derived _DEFAULT_PLANE_WIDTH_M fallback before assigning spec["size"].

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/camera_viz/camera_viz.py`:
- Around line 255-264: Update the calibrated size calculation in the
resolve_settings flow so tan-based FOV sizing is used only for quad/rectilinear
shapes with valid rectilinear FOVs. For cylinder/equirect cameras or FOVs
outside the rectilinear range, retain the aspect-derived _DEFAULT_PLANE_WIDTH_M
fallback before assigning spec["size"].
- Around line 534-544: Update the pre-flight shape check around _shape_for to
pass the same calibration presence used by _build_local_entries and
_build_rtp_entries, specifically whether "calib" exists in each camera config.
Keep the existing XR-mode validation and quad exemption unchanged so calibrated
cameras with no explicit shape are correctly treated as cylinders.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ac2db08c-e838-4deb-8a01-15c925b968fc

📥 Commits

Reviewing files that changed from the base of the PR and between 745559b and b0f22d2.

📒 Files selected for processing (44)
  • docs/source/getting_started/televiz.rst
  • docs/source/references/camera_streaming.rst
  • examples/camera_viz/README.md
  • examples/camera_viz/camera_viz.py
  • examples/camera_viz/configs/oakd.yaml
  • examples/camera_viz/configs/realsense.yaml
  • examples/camera_viz/configs/replay.yaml
  • examples/camera_viz/configs/synthetic.yaml
  • examples/camera_viz/configs/synthetic_stereo.yaml
  • examples/camera_viz/configs/v4l2.yaml
  • examples/camera_viz/configs/zed.yaml
  • examples/camera_viz/pipeline/__init__.py
  • examples/camera_viz/pipeline/runner.py
  • examples/camera_viz/pipeline/undistort.py
  • examples/camera_viz/placements/lock_modes.py
  • examples/camera_viz/scripts/_install_deps.sh
  • examples/camera_viz/sources/zed.py
  • examples/camera_viz/tests/test_undistort.py
  • src/viz/layers/cpp/CMakeLists.txt
  • src/viz/layers/cpp/cylinder_layer.cpp
  • src/viz/layers/cpp/equirect_layer.cpp
  • src/viz/layers/cpp/image_layer_base.cpp
  • src/viz/layers/cpp/inc/viz/layers/cylinder_layer.hpp
  • src/viz/layers/cpp/inc/viz/layers/equirect_layer.hpp
  • src/viz/layers/cpp/inc/viz/layers/image_layer_base.hpp
  • src/viz/layers/cpp/inc/viz/layers/projection_layer.hpp
  • src/viz/layers/cpp/inc/viz/layers/quad_layer.hpp
  • src/viz/layers/cpp/quad_layer.cpp
  • src/viz/layers_tests/cpp/CMakeLists.txt
  • src/viz/layers_tests/cpp/test_quad_layer.cpp
  • src/viz/layers_tests/cpp/test_shaped_layers.cpp
  • src/viz/python/layers_bindings.cpp
  • src/viz/python/session_bindings.cpp
  • src/viz/python/viz_init.py
  • src/viz/session/cpp/inc/viz/session/display_backend.hpp
  • src/viz/session/cpp/inc/viz/session/layer_base.hpp
  • src/viz/session/cpp/inc/viz/session/viz_session.hpp
  • src/viz/session/cpp/inc/viz/session/xr_backend.hpp
  • src/viz/session/cpp/viz_compositor.cpp
  • src/viz/session/cpp/viz_session.cpp
  • src/viz/session/cpp/xr_backend.cpp
  • src/viz/session_tests/cpp/test_quad_milestone.cpp
  • src/viz/xr/cpp/inc/viz/xr/openxr_session.hpp
  • src/viz/xr/cpp/openxr_session.cpp
🛑 Comments failed to post (1)
examples/camera_viz/camera_viz.py (1)

534-544: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Pre-flight shape check disagrees with the entry builders — non-quad layers slip into window mode.

_shape_for here is called without has_calib, so a camera with calib: and no explicit shape: resolves to "quad" and the XR-mode guard is skipped. _build_local_entries / _build_rtp_entries pass has_calib="calib" in cam and get "cylinder", so window mode then tries to create a runtime-only cylinder layer — exactly the failure this guard exists to prevent.

🐛 Proposed fix
-        shape, _, _, _ = _shape_for(cam["name"], placements_cfg)
+        shape, _, _, _ = _shape_for(
+            cam["name"], placements_cfg, has_calib="calib" in cam
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    placements_cfg = cfg.get("display", {}).get("placements", {})
    for cam in _enabled_cameras(cfg):
        shape, _, _, _ = _shape_for(
            cam["name"], placements_cfg, has_calib="calib" in cam
        )
        if shape == "quad":
            continue
        if effective_mode != "xr":
            raise SystemExit(
                f"camera_viz: placements.{cam['name']}.shape: {shape} is "
                "composited by the OpenXR runtime and requires XR mode; "
                "use --mode xr or shape: quad in window mode."
            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/camera_viz/camera_viz.py` around lines 534 - 544, Update the
pre-flight shape check around _shape_for to pass the same calibration presence
used by _build_local_entries and _build_rtp_entries, specifically whether
"calib" exists in each camera config. Keep the existing XR-mode validation and
quad exemption unchanged so calibrated cameras with no explicit shape are
correctly treated as cylinders.

height: 720
fps: 60
bus_type: usb # usb | gmsl
# calib: calib/zed_fisheye.json # per-eye K/D (+R_rect) calibration:

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.

Where is this file?

# out_width: 1920 # output resolution (default: source size)
# out_height: 1080

The calibration JSON carries ``model`` (``fisheye`` for Kannala-Brandt, ``brown``/``pinhole``

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.

Should give an example definition. The description here is vague.

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.

2 participants