Source code for isaaclab.sim.spawners.sensors.sensors

# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

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

if TYPE_CHECKING:
    from . import sensors_cfg

# import logger
logger = logging.getLogger(__name__)

CUSTOM_PINHOLE_CAMERA_ATTRIBUTES = {
    "projection_type": ("cameraProjectionType", Sdf.ValueTypeNames.Token),
}
"""Custom attributes for pinhole camera model.

The dictionary maps the attribute name in the configuration to the attribute name in the USD prim.
"""


CUSTOM_FISHEYE_CAMERA_ATTRIBUTES = {
    "projection_type": ("cameraProjectionType", Sdf.ValueTypeNames.Token),
    "fisheye_nominal_width": ("fthetaWidth", Sdf.ValueTypeNames.Float),
    "fisheye_nominal_height": ("fthetaHeight", Sdf.ValueTypeNames.Float),
    "fisheye_optical_centre_x": ("fthetaCx", Sdf.ValueTypeNames.Float),
    "fisheye_optical_centre_y": ("fthetaCy", Sdf.ValueTypeNames.Float),
    "fisheye_max_fov": ("fthetaMaxFov", Sdf.ValueTypeNames.Float),
    "fisheye_polynomial_a": ("fthetaPolyA", Sdf.ValueTypeNames.Float),
    "fisheye_polynomial_b": ("fthetaPolyB", Sdf.ValueTypeNames.Float),
    "fisheye_polynomial_c": ("fthetaPolyC", Sdf.ValueTypeNames.Float),
    "fisheye_polynomial_d": ("fthetaPolyD", Sdf.ValueTypeNames.Float),
    "fisheye_polynomial_e": ("fthetaPolyE", Sdf.ValueTypeNames.Float),
    "fisheye_polynomial_f": ("fthetaPolyF", Sdf.ValueTypeNames.Float),
}
"""Custom attributes for fisheye camera model.

The dictionary maps the attribute name in the configuration to the attribute name in the USD prim.
"""


# 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)


[docs] @clone def spawn_camera( prim_path: str, cfg: sensors_cfg.PinholeCameraCfg | sensors_cfg.FisheyeCameraCfg, translation: tuple[float, float, float] | None = None, orientation: tuple[float, float, float, float] | None = None, **kwargs, ) -> Usd.Prim: """Create a USD camera prim with given projection type. The function creates various attributes on the camera prim that specify the camera's properties. These are later used by ``omni.replicator.core`` to render the scene with the given camera. .. note:: This function is decorated with :func:`clone` that resolves prim path into list of paths if the input prim path is a regex pattern. This is done to support spawning multiple assets from a single and cloning the USD prim at the given path expression. Args: prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, then the asset is spawned at all the matching prim paths. cfg: The configuration instance. translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case this is set to the origin. orientation: The orientation in (x, y, z, w) to apply to the prim w.r.t. its parent prim. Defaults to None, in which case this is set to identity. **kwargs: Additional keyword arguments, like ``clone_in_fabric``. Returns: The created prim. Raises: ValueError: If a prim already exists at the given path. """ # obtain stage handle stage = get_current_stage() # spawn camera if it doesn't exist. if not stage.GetPrimAtPath(prim_path).IsValid(): create_prim(prim_path, "Camera", translation=translation, orientation=orientation, stage=stage) else: raise ValueError(f"A prim already exists at path: '{prim_path}'.") # lock camera from viewport (this disables viewport movement for camera) if cfg.lock_camera: change_prim_property( prop_path=f"{prim_path}.omni:kit:cameraLock", value=True, stage=stage, type_to_create_if_not_exist=Sdf.ValueTypeNames.Bool, ) # decide the custom attributes to add if cfg.projection_type == "pinhole": attribute_types = CUSTOM_PINHOLE_CAMERA_ATTRIBUTES else: attribute_types = CUSTOM_FISHEYE_CAMERA_ATTRIBUTES # TODO: Adjust to handle aperture offsets once supported by omniverse # Internal ticket from rendering team: OM-42611 if cfg.horizontal_aperture_offset > 1e-4 or cfg.vertical_aperture_offset > 1e-4: logger.warning("Camera aperture offsets are not supported by Omniverse. These parameters will be ignored.") # custom attributes in the config that are not USD Camera parameters non_usd_cfg_param_names = [ "func", "copy_from_source", "lock_camera", "visible", "semantic_tags", "from_intrinsic_matrix", "spawn_path", "distortion", ] # get camera prim prim = stage.GetPrimAtPath(prim_path) # create attributes for the fisheye camera model # note: for pinhole those are already part of the USD camera prim for attr_name, attr_type in attribute_types.values(): # check if attribute does not exist if prim.GetAttribute(attr_name).Get() is None: # create attribute based on type prim.CreateAttribute(attr_name, attr_type) # set attribute values for param_name, param_value in cfg.__dict__.items(): # check if value is valid if param_value is None or param_name in non_usd_cfg_param_names: continue # obtain prim property name if param_name in attribute_types: # check custom attributes prim_prop_name = attribute_types[param_name][0] else: # convert attribute name in prim to cfg name 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 cfg.distortion is not None: _author_opencv_distortion(prim, cfg.distortion) # return the prim return prim
@clone def spawn_sensor_frame( prim_path: str, cfg: sensors_cfg.SensorFrameCfg, translation: tuple[float, float, float] | None = None, orientation: tuple[float, float, float, float] | None = None, **kwargs, ) -> Usd.Prim: """Create a plain USD Xform prim as a sensor attachment frame. .. note:: This function is decorated with :func:`clone` that resolves prim path into list of paths if the input prim path is a regex pattern. Args: prim_path: The prim path or pattern to spawn the asset at. cfg: The configuration instance. translation: Local translation (x, y, z) [m] w.r.t. the parent prim. Defaults to None (origin). orientation: Local orientation as quaternion (x, y, z, w) w.r.t. the parent prim. Defaults to None (identity). **kwargs: Additional keyword arguments, like ``clone_in_fabric``. Returns: The created USD prim. Raises: ValueError: If a prim already exists at the given path. """ stage = get_current_stage() if not stage.GetPrimAtPath(prim_path).IsValid(): prim = create_prim( prim_path, "Xform", translation=translation, orientation=orientation, stage=stage, ) else: raise ValueError(f"A prim already exists at path: '{prim_path}'.") return prim