Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Guidelines for modifications:
* Krishna Lakhi
* Lin He
* Lionel Gulich
* Lior Ben Horin
* Lorenz Wellhausen
* Lotus Li
* Louis Le Lay
Expand Down
15 changes: 15 additions & 0 deletions docs/source/api/lab/isaaclab.sim.spawners.rst
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ Sensors

PinholeCameraCfg
FisheyeCameraCfg
OpenCvDistortionCfg
OpenCvPinholeDistortionCfg
OpenCvFisheyeDistortionCfg

.. autofunction:: spawn_camera

Expand All @@ -222,6 +225,18 @@ Sensors
:members:
:exclude-members: __init__, func

.. autoclass:: OpenCvDistortionCfg
:members:
:exclude-members: __init__, func

.. autoclass:: OpenCvPinholeDistortionCfg
:members:
:exclude-members: __init__, func

.. autoclass:: OpenCvFisheyeDistortionCfg
:members:
:exclude-members: __init__, func

From Files
----------

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Added
^^^^^

* Added :class:`~isaaclab.sim.spawners.sensors.OpenCvPinholeDistortionCfg` and
:class:`~isaaclab.sim.spawners.sensors.OpenCvFisheyeDistortionCfg` and a ``distortion`` field on
:class:`~isaaclab.sim.spawners.sensors.PinholeCameraCfg`, letting a camera carry an OpenCV
``fx/fy/cx/cy`` + distortion-coefficient calibration. The model is authored on the camera prim as
the ``omni:lensdistortion:*`` USD API, which the RTX/OVRTX renderer honors natively.

Changed
^^^^^^^

* Changed :meth:`~isaaclab.sensors.camera.Camera._update_intrinsic_matrices` to read the authored
``fx/fy/cx/cy`` when an OpenCV lens-distortion model is present, so
:attr:`~isaaclab.sensors.camera.Camera.data` intrinsics reflect a real calibration (non-square
pixels or an off-center principal point) instead of assuming ``fx == fy`` and a centered principal
point.
47 changes: 37 additions & 10 deletions source/isaaclab/isaaclab/sensors/camera/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ def __init__(self, cfg: CameraCfg):

# UsdGeom Camera prim for the sensor
self._sensor_prims: list[UsdGeom.Camera] = list()
# Guard so the lens-distortion image-size mismatch warning is emitted at most once.
self._warned_distortion_image_size: bool = False
# Allocated in :meth:`_create_buffers` once the renderer's output contract is known.
self._data: CameraData | None = None
# Renderer and render data — assigned in _initialize_impl.
Expand Down Expand Up @@ -651,18 +653,43 @@ def _update_intrinsic_matrices(self, env_ids: Sequence[int] | wp.array | None =
for matrix_id, i in enumerate(env_ids_np):
# Get corresponding sensor prim
sensor_prim = self._sensor_prims[int(i)]
# get camera parameters
# currently rendering does not use aperture offsets or vertical aperture
focal_length = sensor_prim.GetFocalLengthAttr().Get()
horiz_aperture = sensor_prim.GetHorizontalApertureAttr().Get()

# get viewport parameters
height, width = self.image_shape
# extract intrinsic parameters
f_x = (width * focal_length) / horiz_aperture
f_y = f_x
c_x = width * 0.5
c_y = height * 0.5
# Prefer an authored OpenCV lens-distortion model when present: it carries the
# authoritative fx/fy/cx/cy that the RTX/OVRTX renderer projects through. These may be
# non-square (fx != fy) or off-center, unlike the focal-length/aperture readback which
# assumes square pixels and a centered principal point.
raw_prim = sensor_prim.GetPrim()
distortion_model = raw_prim.GetAttribute("omni:lensdistortion:model").Get()
if distortion_model:
prefix = f"omni:lensdistortion:{distortion_model}"
f_x = float(raw_prim.GetAttribute(f"{prefix}:fx").Get())
f_y = float(raw_prim.GetAttribute(f"{prefix}:fy").Get())
c_x = float(raw_prim.GetAttribute(f"{prefix}:cx").Get())
c_y = float(raw_prim.GetAttribute(f"{prefix}:cy").Get())

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 Unguarded None from missing distortion intrinsic attrs

GetAttribute(...).Get() returns None when the attribute is not authored on the prim; float(None) then raises TypeError on every subsequent update step. The imageSize read two lines below is already None-checked, but fx/fy/cx/cy are not. A USD scene that carries omni:lensdistortion:model without the matching intrinsic attributes (e.g., a hand-authored prim or a third-party export that only writes the schema token) will crash the camera sensor continuously from the first update call.

# the intrinsics are reported in the calibrated pixel space; warn once if the render
# resolution differs, since the reported matrix will not match the rendered pixels.
image_size = raw_prim.GetAttribute(f"{prefix}:imageSize").Get()
if image_size is not None and (int(image_size[0]), int(image_size[1])) != (width, height):
if not self._warned_distortion_image_size:
logger.warning(
"Camera lens-distortion 'imageSize' %s does not match the render resolution"
" (%d, %d). The reported intrinsic matrix is in the calibrated pixel space.",
(int(image_size[0]), int(image_size[1])),
width,
height,
)
self._warned_distortion_image_size = True

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 Single warning flag covers all N environment cameras

_warned_distortion_image_size is a per-Camera-instance flag, but one Camera object manages N sensor prims (one per environment). Once any single env's prim triggers the warning, the flag is set and all subsequent per-prim mismatches in the same instance — including those in different environments with different calibration sizes — are silently dropped. In a large multi-env setup a user would see one warning and have no indication that the mismatch also affects all other envs. Consider including the env index in the message, or emitting the warning per prim the first time it fires.

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!

else:
# get camera parameters
# currently rendering does not use aperture offsets or vertical aperture
focal_length = sensor_prim.GetFocalLengthAttr().Get()
horiz_aperture = sensor_prim.GetHorizontalApertureAttr().Get()
# extract intrinsic parameters
f_x = (width * focal_length) / horiz_aperture
f_y = f_x
c_x = width * 0.5
c_y = height * 0.5
# create intrinsic matrix for depth linear
intrinsic_matrices[matrix_id, 0, 0] = f_x
intrinsic_matrices[matrix_id, 0, 2] = c_x
Expand Down
6 changes: 6 additions & 0 deletions source/isaaclab/isaaclab/sim/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ __all__ = [
"spawn_camera",
"spawn_sensor_frame",
"FisheyeCameraCfg",
"OpenCvDistortionCfg",
"OpenCvFisheyeDistortionCfg",
"OpenCvPinholeDistortionCfg",
"PinholeCameraCfg",
"SensorFrameCfg",
"spawn_capsule",
Expand Down Expand Up @@ -332,6 +335,9 @@ from .spawners import (
MjcfFileCfg,
MultiAssetSpawnerCfg,
MultiUsdFileCfg,
OpenCvDistortionCfg,
OpenCvFisheyeDistortionCfg,
OpenCvPinholeDistortionCfg,
PhysicsMaterialCfg,
PinholeCameraCfg,
PreviewSurfaceCfg,
Expand Down
14 changes: 13 additions & 1 deletion source/isaaclab/isaaclab/sim/spawners/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ __all__ = [
"spawn_camera",
"spawn_sensor_frame",
"FisheyeCameraCfg",
"OpenCvDistortionCfg",
"OpenCvFisheyeDistortionCfg",
"OpenCvPinholeDistortionCfg",
"PinholeCameraCfg",
"SensorFrameCfg",
"spawn_capsule",
Expand Down Expand Up @@ -126,7 +129,16 @@ from .meshes import (
MeshRectangleCfg,
MeshSphereCfg,
)
from .sensors import spawn_camera, spawn_sensor_frame, FisheyeCameraCfg, PinholeCameraCfg, SensorFrameCfg
from .sensors import (
spawn_camera,
spawn_sensor_frame,
FisheyeCameraCfg,
OpenCvDistortionCfg,
OpenCvFisheyeDistortionCfg,
OpenCvPinholeDistortionCfg,
PinholeCameraCfg,
SensorFrameCfg,
)
from .shapes import (
spawn_capsule,
spawn_cone,
Expand Down
12 changes: 11 additions & 1 deletion source/isaaclab/isaaclab/sim/spawners/sensors/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,19 @@ __all__ = [
"spawn_camera",
"spawn_sensor_frame",
"FisheyeCameraCfg",
"OpenCvDistortionCfg",
"OpenCvFisheyeDistortionCfg",
"OpenCvPinholeDistortionCfg",
"PinholeCameraCfg",
"SensorFrameCfg",
]

from .sensors import spawn_camera, spawn_sensor_frame
from .sensors_cfg import FisheyeCameraCfg, PinholeCameraCfg, SensorFrameCfg
from .sensors_cfg import (
FisheyeCameraCfg,
OpenCvDistortionCfg,
OpenCvFisheyeDistortionCfg,
OpenCvPinholeDistortionCfg,
PinholeCameraCfg,
SensorFrameCfg,
)
65 changes: 64 additions & 1 deletion source/isaaclab/isaaclab/sim/spawners/sensors/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import logging
from typing import TYPE_CHECKING

from pxr import Sdf, Usd
from pxr import Gf, Sdf, Usd

from isaaclab.sim.utils import change_prim_property, clone, create_prim, get_current_stage
from isaaclab.utils import to_camel_case
Expand Down Expand Up @@ -48,6 +48,65 @@
"""


# OpenCV lens-distortion models authored as the ``omni:lensdistortion:*`` USD API. The RTX/OVRTX
# renderer honors these attributes natively; they are read back into ``camera.data.intrinsic_matrices``
# by :meth:`~isaaclab.sensors.camera.Camera._update_intrinsic_matrices`.
_OPENCV_DISTORTION_API_SCHEMAS = {
"opencvPinhole": "OmniLensDistortionOpenCvPinholeAPI",
"opencvFisheye": "OmniLensDistortionOpenCvFisheyeAPI",
}
"""Maps an OpenCV distortion model discriminator to its applied USD API schema name."""

_OPENCV_DISTORTION_COEFFS = {
"opencvPinhole": ("k1", "k2", "k3", "k4", "k5", "k6", "p1", "p2", "s1", "s2", "s3", "s4"),
"opencvFisheye": ("k1", "k2", "k3", "k4"),
}
"""Maps an OpenCV distortion model discriminator to its distortion-coefficient field names."""


def _author_opencv_distortion(prim: Usd.Prim, cfg: sensors_cfg.OpenCvDistortionCfg) -> None:
"""Author an OpenCV lens-distortion model on a camera prim as the ``omni:lensdistortion:*`` API.

The attributes are authored explicitly (not through the generic camelCase loop of
:func:`spawn_camera`) because their names are namespaced (e.g. ``omni:lensdistortion:opencvPinhole:k1``)
and cannot be produced by :func:`~isaaclab.utils.to_camel_case`. Applying the schema only edits prim
metadata, so it survives ``stage.ExportToString()`` and does not require the schema to be registered.

Args:
prim: The camera prim to author the distortion model on.
cfg: The OpenCV distortion configuration.

Raises:
ValueError: If the distortion ``model`` is not a supported OpenCV model.
"""
if cfg.model not in _OPENCV_DISTORTION_API_SCHEMAS:
raise ValueError(
f"Unsupported OpenCV distortion model: '{cfg.model}'. Supported models are:"
f" {list(_OPENCV_DISTORTION_API_SCHEMAS)}."
)
prefix = f"omni:lensdistortion:{cfg.model}"

# apply the schema and set the model discriminator token
prim.AddAppliedSchema(_OPENCV_DISTORTION_API_SCHEMAS[cfg.model])

def _set_attr(name: str, type_name: Sdf.ValueTypeName, value) -> None:
attr = prim.GetAttribute(name) or prim.CreateAttribute(name, type_name)
attr.Set(value)

_set_attr("omni:lensdistortion:model", Sdf.ValueTypeNames.Token, cfg.model)
_set_attr(
f"{prefix}:imageSize",
Sdf.ValueTypeNames.Int2,
Gf.Vec2i(int(cfg.image_size[0]), int(cfg.image_size[1])),
)
for name in ("fx", "fy", "cx", "cy"):
_set_attr(f"{prefix}:{name}", Sdf.ValueTypeNames.Float, float(getattr(cfg, name)))
# coefficients are muted (authored as zero) unless apply_lens_distortion is set
for name in _OPENCV_DISTORTION_COEFFS[cfg.model]:
value = float(getattr(cfg, name)) if cfg.apply_lens_distortion else 0.0
_set_attr(f"{prefix}:{name}", Sdf.ValueTypeNames.Float, value)


@clone
def spawn_camera(
prim_path: str,
Expand Down Expand Up @@ -119,6 +178,7 @@ def spawn_camera(
"semantic_tags",
"from_intrinsic_matrix",
"spawn_path",
"distortion",
]
# get camera prim
prim = stage.GetPrimAtPath(prim_path)
Expand All @@ -143,6 +203,9 @@ def spawn_camera(
prim_prop_name = to_camel_case(param_name, to="cC")
# get attribute from the class
prim.GetAttribute(prim_prop_name).Set(param_value)
# author the OpenCV lens-distortion model (renderer-agnostic; RTX/OVRTX honors it natively)
if getattr(cfg, "distortion", None) is not None:
_author_opencv_distortion(prim, cfg.distortion)
# return the prim
return prim

Expand Down
Loading
Loading