Skip to content

Add OpenCV lens distortion rendering to Newton - #6851

Open
liorbenhorin wants to merge 2 commits into
isaac-sim:developfrom
liorbenhorin:liorbenhorin/opencv-lens-distortion-cameras-newton
Open

Add OpenCV lens distortion rendering to Newton#6851
liorbenhorin wants to merge 2 commits into
isaac-sim:developfrom
liorbenhorin:liorbenhorin/opencv-lens-distortion-cameras-newton

Conversation

@liorbenhorin

Copy link
Copy Markdown

Description

PR #6608 introduced renderer-agnostic OpenCV camera calibration configs and native RTX/OVRTX rendering. The Newton path was intentionally left as a documented no-op that emitted a warning and rendered using a centered, square-pixel pinhole projection.

This PR completes backend parity by making NewtonWarpRenderer consume the same OpenCV calibration and generate distorted per-pixel camera rays.

This PR adds native OpenCV lens-distortion rendering to Newton:

  • OpenCV pinhole: Adds a Warp kernel that inverts the OpenCV forward model per pixel using fixed-point iteration. It supports rational radial (k1..k6), tangential (p1, p2), and thin-prism (s1..s4) distortion - conritbuted by @AntoineRichard in Add native OpenCV lens-distortion rendering for the Newton renderer liorbenhorin/IsaacLab#1
  • OpenCV fisheye: Uses Newton’s native compute_camera_rays_fisheye_opencv helper rather than maintaining a duplicate Isaac Lab implementation.
  • Calibrated intrinsics: Both paths honor fx/fy/cx/cy, including non-square focal lengths and an off-center principal point.
  • Disabled distortion: apply_lens_distortion=False mutes the coefficients while retaining the calibrated intrinsics, matching RTX/OVRTX behavior.
  • Renderer behavior: Replaces the previous Newton warning/no-op path with distortion-aware ray generation.
  • Tests: Adds CPU unit tests for pinhole ray generation and CUDA integration tests for pinhole rendering, fisheye rendering, and intrinsic-matrix readback.

This is a stacked follow-up to #6608. It does not change the public camera configuration or USD authoring path introduced there; it only adds Newton renderer consumption of the existing model.

Images

image222

Checklist

  • I have read and understood the contribution guidelines
  • I have run the pre-commit checks with ./isaaclab.sh --format
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • I have added a changelog fragment under source/<pkg>/changelog.d/ for every touched package (do not edit CHANGELOG.rst or bump extension.toml — CI handles that)
  • I have added my name to the CONTRIBUTORS.md or my name already exists there

Use Newton's native fisheye ray generation and add full OpenCV pinhole distortion so calibrated cameras render consistently across backends.
@liorbenhorin
liorbenhorin requested a review from a team August 2, 2026 11:02
@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label Aug 2, 2026
@greptile-apps

greptile-apps Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds OpenCV pinhole and fisheye ray generation to the Newton renderer, including calibrated intrinsics and coefficient muting.

  • Adds a Warp kernel that inverts rational radial, tangential, and thin-prism pinhole distortion.
  • Routes OpenCV fisheye configurations through Newton's native fisheye helper.
  • Adds CPU ray tests and CUDA rendering/intrinsics integration tests.

Confidence Score: 4/5

The PR appears safe to merge, with a non-blocking gap in coverage for the newly advertised rational and thin-prism pinhole coefficients.

The renderer dispatch and tested projection paths are coherent, but regressions in k4-k6 and s1-s4 handling would not be detected because all added fixtures disable those terms.

Files Needing Attention: source/isaaclab_newton/test/renderers/test_opencv_distortion_rays.py

Important Files Changed

Filename Overview
source/isaaclab_newton/isaaclab_newton/renderers/newton_warp_renderer.py Dispatches OpenCV camera configurations to calibrated fisheye or pinhole ray generation and preserves coefficient-muting semantics.
source/isaaclab_newton/isaaclab_newton/renderers/opencv_distortion_rays.py Implements the intended OpenCV pinhole inversion and coordinate conversion, though numerical behavior for the full advertised coefficient set lacks test coverage.
source/isaaclab_newton/test/renderers/test_opencv_distortion_rays.py Validates normalization, pinhole inversion, calibrated intrinsics, and zero distortion, but does not exercise rational denominator or thin-prism coefficients.
source/isaaclab_newton/test/sensors/test_camera_opencv_distortion_newton.py Adds end-to-end CUDA tests for pinhole distortion, fisheye rendering, and intrinsic-matrix readback.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    C[Camera distortion config] --> M{OpenCV model}
    M -->|opencvFisheye| F[Newton fisheye ray helper]
    M -->|opencvPinhole| P[Warp fixed-point inversion kernel]
    F --> R[Per-pixel camera rays]
    P --> R
    R --> N[Newton tiled renderer]
    N --> O[Camera outputs]
Loading

Reviews (1): Last reviewed commit: "Add OpenCV lens distortion rendering to ..." | Re-trigger Greptile

Comment on lines +45 to +47
_PINHOLE_COEFFS = dict(
k1=0.1, k2=-0.05, k3=0.01, k4=0.0, k5=0.0, k6=0.0, p1=0.001, p2=-0.002, s1=0.0, s2=0.0, s3=0.0, s4=0.0
)

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.

P2 Exercise all distortion coefficients

The shared nontrivial fixture leaves k4k6 and s1s4 at zero, so the round-trip tests never exercise the newly advertised rational-denominator or thin-prism inversion paths. Add nonzero, valid values for these terms to catch coefficient-ordering, sign, and numerical regressions that otherwise produce incorrect edge-of-frame rays without failing this suite.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@isaaclab-review-bot isaaclab-review-bot 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.

Isaac Lab Review Bot

The Newton backend now consumes existing OpenCV camera calibration configs for pinhole and fisheye ray generation, but the pinhole render-grid mapping introduces a half-pixel calibration offset that should be corrected before merge.

  • Design and architecture: The implementation appropriately keeps distortion handling within the Newton renderer, delegates fisheye rays to Newton’s native helper, and uses a dedicated Warp kernel for the full OpenCV pinhole model. The cached ray-field design matches the renderer’s existing camera-ray layout.
  • API: No public camera configuration or renderer signatures are changed. Existing OpenCV calibration fields are consumed by Newton, including calibrated intrinsics and the existing apply_lens_distortion flag. The changelog fragment documents the resulting backend behavior change.
  • Implementation: The coefficient handling, OpenCV-to-OpenGL basis conversion, ray-buffer allocation, and fixed-point inversion are coherent. However, mapping each render pixel to (px + 0.5) times the calibration scale without subtracting 0.5 shifts rays relative to OpenCV’s integer-centered pixel coordinates, including at native calibration resolution. Use a center-preserving mapping for both axes so the effective principal point matches the authored cx and cy.

Minor fixes needed. Posted 1 actionable finding inline.

Automated review; human maintainers own approval decisions.


# Map the render pixel onto the calibrated image grid, then to distorted normalized coordinates.
# OpenCV image y points down.
u = ((wp.float32(px) + 0.5) / wp.float32(width)) * image_width

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.

🔵 Suggestion · Implementation — Half-pixel offset in calibrated pixel mapping

OpenCV intrinsics are expressed in a frame where integer pixel coordinates are pixel centers, but this maps render pixel px to (px + 0.5) * image_width / width, shifting every ray by half a calibrated pixel and biasing the effective principal point relative to the authored cx/cy. A center-preserving mapping such as (px + 0.5) * image_width / width - 0.5 (and likewise for v) keeps the rendered rays consistent with the calibration this feature exists to honor.

@kellyguo11
kellyguo11 requested a review from daniela-hase August 2, 2026 23:49
@kellyguo11 kellyguo11 moved this to Backlog in Isaac Lab Aug 3, 2026
@kellyguo11 kellyguo11 moved this from Backlog to In review in Isaac Lab Aug 3, 2026

@AntoineRichard AntoineRichard left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI-generated review (Codex).

Requesting changes for test validity, stale public documentation, and code hygiene. The renderer path is directionally sound, but the integration fixture is non-invertible over the tested frame, the new integration test emits deprecation warnings, and the public camera configuration still says Newton ignores this model. I also found avoidable documentation/test duplication and comments that narrate review history or unsupported rationale.

I did not repeat the two existing inline threads about full coefficient coverage and pixel-center mapping; both remain unresolved. Before changing the latter, reconcile it with Newton's native fisheye helper, which currently uses the same half-pixel mapping.

Verification at this SHA: ./isaaclab.sh -f passed in an isolated checkout; all 4 CPU ray tests passed; the 3 CUDA integration tests collected and skipped because CUDA is unavailable here. The PR's isaaclab_newton CI job passed; a separate isaaclab_tasks [3/3] job is failing.

Comment on lines +4 to +11
* Added OpenCV lens-distortion support to the Newton renderer: a camera cfg carrying an OpenCV
pinhole (``k1..k6``, ``p1``, ``p2``, ``s1..s4``) or fisheye (``k1..k4``) distortion model on
``spawn.distortion`` is now rendered through the distortion instead of as a centered, square-pixel
pinhole. Fisheye cameras use Newton's native OpenCV fisheye ray helper, while pinhole cameras use
an Isaac Lab kernel supporting rational radial, tangential, and thin-prism distortion. Both paths
honor the calibrated ``fx/fy/cx/cy`` intrinsics (including non-square focal lengths and an
off-center principal point). With ``apply_lens_distortion=False`` the distortion coefficients are
muted while the intrinsics are still applied, matching the RTX/OVRTX behavior.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[AI-generated] Changelog — keep this user-facing

This repeats the implementation and PR description (kernel/helper choices and the coefficient inventory). The changelog guidelines explicitly exclude internal details. Condense it to the behavior, for example: Added OpenCV pinhole and fisheye lens-distortion rendering to the Newton backend, including calibrated intrinsics and coefficient muting through apply_lens_distortion.

Comment on lines +407 to +409
# Match the PR kernel's forward-facing camera hemisphere and avoid validating the
# OpenCV polynomial outside its physically meaningful calibration range.
max_fov=math.pi,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[AI-generated] Comment — make it functional

PR kernel is review-history language, and max_fov caps the accepted ray angle; it does not validate the polynomial. Replace both lines with the durable statement # Limit fisheye rays to the forward hemisphere.

Comment on lines +553 to +555
OpenCV lens distortion (``spawn.distortion``) needs no preparation here: it is consumed at
ray-generation time by :meth:`RenderData._build_distortion_rays`, which inverts the OpenCV
forward model per pixel to trace the distorted camera-space rays.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[AI-generated] Documentation — update the source of truth

This adds renderer-local implementation prose while the public docstrings for OpenCvDistortionCfg and PinholeCameraCfg.distortion in sensors_cfg.py still state that Newton does not apply the model. Remove this duplicated paragraph and update those public docstrings to describe Newton support; otherwise the documented API contradicts the implementation.

Comment on lines +6 to +20
"""Warp ray generation for OpenCV pinhole lens distortion under the Newton renderer.

The Newton tiled camera renders by tracing an explicit per-pixel ray field of shape
``(camera_count, height, width, 2)`` (``wp.vec3f``): index ``0`` holds the ray origin in camera
space (always ``wp.vec3f(0.0)``) and index ``1`` the normalized ray direction in camera space.
Newton uses the OpenGL camera convention (``+X`` right, ``+Y`` up, looking down ``-Z``).

To honor an OpenCV ``fx/fy/cx/cy`` + distortion-coefficient calibration, for each output pixel the
kernel below inverts the OpenCV forward distortion model to recover the *undistorted* normalized
image coordinates ``(x_u, y_u)`` (OpenCV image ``y`` points down), then emit the camera-space ray
``normalize(vec3(x_u, -y_u, -1))`` -- the negation of ``y`` and ``z`` maps OpenCV camera space
(``+Z`` forward, ``+Y`` down) onto Newton's OpenGL camera space.

OpenCV fisheye ray generation is provided directly by Newton's
``SensorTiledCamera.utils.compute_camera_rays_fisheye_opencv`` helper.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[AI-generated] Documentation — remove duplication

Keep the module docstring to a one-sentence scope. Ray layout and coordinate conversion are repeated in the kernel documentation/body, while fisheye delegation is not implemented in this module. These copies add maintenance surface without a second contract.

Comment on lines +27 to +30
# Number of fixed-point iterations used to invert the distortion model. OpenCV's own
# ``undistortPoints`` defaults to a similar iteration budget; the inversion converges well within
# this for realistic calibrations.
_INVERSION_ITERATIONS = 20

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[AI-generated] Comment — correct the iteration claim

OpenCV documents the default undistortPoints path as 5 iterations, so 20 is not a similar budget, and this PR has no convergence test supporting “well within.” Keep the comment functional (what the constant controls), or add evidence for the selected bound and failure behavior.

Comment on lines +155 to +164
def test_pinhole_off_center_principal_point_is_honored():
"""The ray through the principal-point pixel looks straight ahead (down -Z)."""
rays = _launch_pinhole({k: 0.0 for k in _PINHOLE_COEFFS})
directions = rays[0, :, :, 1, :]
# pixel closest to the principal point on the render grid
px = int(round(_CALIB["cx"] / _IMAGE_W * WIDTH - 0.5))
py = int(round(_CALIB["cy"] / _IMAGE_H * HEIGHT - 0.5))
direction = directions[py, px]
assert direction[0] == pytest.approx(0.0, abs=2e-2)
assert direction[1] == pytest.approx(0.0, abs=2e-2)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[AI-generated] Test — remove redundant weak coverage

test_pinhole_zero_coeffs_matches_ideal_projection already checks sampled rays against the full off-center calibration. This test adds no independent oracle, and its 2e-2 tolerance cannot detect sub-pixel principal-point errors. Remove it, or use a calibration whose principal point lands exactly on a sampled pixel and assert the expected ray tightly.

Comment on lines +82 to +88
# Example real-world OpenCV pinhole calibration (fx != fy, off-center principal point).
_CALIB = dict(fx=339.26592887, fy=338.82010626, cx=323.55809091, cy=250.27360914)
_COEFFS = dict(k1=0.07702322, k2=-0.13605453, k3=0.05163219, p1=-0.00024938, p2=-0.00175006)
# scale the (mild) real coefficients so the barrel effect is unambiguous in the assertion
_K_SCALE = 15.0
# OpenCV fisheye (equidistant) coefficients; the base fisheye projection alone differs strongly from pinhole
_FISHEYE_COEFFS = dict(k1=0.1, k2=-0.05, k3=0.0, k4=0.0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[AI-generated] Important — use an invertible calibration fixture

Multiplying these radial coefficients by 15 makes the forward radial mapping non-monotonic inside this frame: its derivative first becomes negative near normalized radius 0.899, while the corners reach about 1.2. The inverse is therefore ambiguous, yet the test only requires a different image, so invalid rays can pass. Use a synthetic coefficient set that stays monotonic over the full frame and assert representative rays or render samples against an oracle.

Comment on lines +107 to +113
spawn=sim_utils.CuboidCfg(
size=(0.01, 0.01, 0.01),
rigid_props=sim_utils.RigidBodyPropertiesCfg(),
mass_props=sim_utils.MassPropertiesCfg(mass=0.001),
collision_props=sim_utils.CollisionPropertiesCfg(),
physics_material=sim_utils.RigidBodyMaterialCfg(),
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 0.0)),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[AI-generated] Test hygiene — do not add deprecated APIs

These RigidBodyPropertiesCfg, CollisionPropertiesCfg, and RigidBodyMaterialCfg constructors emit three deprecation warnings during collection. New tests should use the solver-common RigidBodyBaseCfg, CollisionBaseCfg, and RigidBodyMaterialBaseCfg equivalents (or omit properties the off-screen anchor does not need).

Comment on lines +198 to +204
assert distorted.shape == (HEIGHT, WIDTH, 1)
# both frames render geometry (the ground plane fills the frame)
assert np.isfinite(distorted).mean() > 0.9
assert np.isfinite(reference).mean() > 0.9
# the renderer applied the lens distortion: the distance maps warp well beyond render noise
mean_abs_diff = _mean_abs_distance_diff(distorted, reference)
assert mean_abs_diff > 0.05, f"distorted vs reference distance maps differ by only {mean_abs_diff:.4f} m"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[AI-generated] Comments — remove assertion narration

The two comments restate the assertions immediately below them. Remove them and let the test/helper names carry the contract; apply the same cleanup to the repeated comments in the fisheye test.

Comment on lines +210 to +220
def test_opencv_distortion_intrinsics_match_authored_newton(device):
"""The Newton camera reports intrinsics matching the authored, non-square, off-center calibration."""
_distance, k = _render_distance(_pinhole_distortion(True), device=device)

assert k[0, 0] == pytest.approx(_CALIB["fx"], abs=1e-2)
assert k[1, 1] == pytest.approx(_CALIB["fy"], abs=1e-2)
assert k[0, 2] == pytest.approx(_CALIB["cx"], abs=1e-2)
assert k[1, 2] == pytest.approx(_CALIB["cy"], abs=1e-2)
# not the stock fx == fy / centered-principal-point collapse
assert k[0, 0] != k[1, 1]
assert k[0, 2] != pytest.approx(WIDTH / 2)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[AI-generated] Test — remove unrelated duplicate integration coverage

The Newton renderer does not compute Camera.data.intrinsic_matrices; Camera._update_intrinsic_matrices does. source/isaaclab/test/sensors/test_opencv_distortion.py::test_readback_uses_authored_fx_fy_cx_cy already checks these same values without a CUDA scene. Remove this test and simplify _render_distance to return only data needed to verify the changed renderer path.

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

Labels

isaac-lab Related to Isaac Lab team

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

4 participants