Core¶
CameraTwin
¶
CameraTwin(spec: CameraSpec | None = None, optics: OpticsModel | None = None, sensor: SensorModel | None = None, isp: ISPModel | None = None, intrinsics: CameraIntrinsics | None = None, distortion: RadialDistortion | None = None, reference: OpticsModel | None = None)
A deployed camera configuration: optics, sensor and ISP.
outputs_depth
property
¶
Whether the camera's depth output is simulated (outputs.depth).
True without a spec (nothing says otherwise).
render_intrinsics
property
¶
render_intrinsics: CameraIntrinsics | None
The pinhole a simulator should render for this twin.
Without distortion these are the camera intrinsics. With distortion it is a
wider, centered pinhole with the same central focal length, which process
warps onto the distorted sensor.
from_spec
classmethod
¶
from_spec(path: str | Path | CameraSpec, device: str | device | None = None, cache: PSFCache | None = None, build_psf: bool = True) -> CameraTwin
Load a spec; builds and caches its PSF bank on first use (slow), then reuses it.
build_psf=False skips the PSF bank (identity optics), for twins rendered
only with the lens-ray methods (twinrobo.plugins.mujoco.lensrender).
from_catalog
classmethod
¶
from_catalog(camera_id: str, registry: CatalogRegistry | None = None, device: str | device | None = None, cache: PSFCache | None = None, build_psf: bool = True) -> CameraTwin
A catalog camera (vendor/model/...). build_psf=False skips the PSF bank,
which only the psf rendering method needs (the lens-ray methods trace the lens).
sensor_color
¶
The light a sensor of this camera records: luminance for mono sensors.
A mono sensor has one intensity channel; it is returned in all three
([B, 3, H, W], equal channels) so frames keep the RGB convention. Color
sensors are unchanged.
lens_geometry
¶
Keyword arguments of lens_rays giving the lens-ray methods this camera's geometry.
The spec's intrinsics and distortion (lens.geometry: calibration, the
default), so every rendering method has the catalog's field of view and
distortion. Empty, keeping the traced lens' own geometry, for
lens.geometry: lens, for calibration.distortion.model: physical and
when the spec gives no intrinsics.
process
¶
process(rgb: Tensor, depth: Tensor, timestamp: float | None = None, exposure: float = 1.0, metadata: dict[str, Any] | None = None, rectify: bool = False, force_depth: bool = False) -> CameraFrame
Turn an ideal simulator RGB-D observation into a CameraTwin frame.
With distortion, an observation rendered at render_intrinsics is warped onto
the sensor first; frame.rgb_ideal is then the undistorted pinhole view with
the camera's intrinsics and frame.depth the depth seen through the lens.
Observations of any other size skip the warp (blur only).
rectify: undistort the output (as vendor SDKs do by default): frame.rgb
and frame.depth are then pinhole images with the camera's intrinsics, the
lens blur kept. No effect without distortion.
force_depth: expose frame.depth even if the camera outputs none
(outputs_depth false): the simulator's ground truth, which the real camera
would not give. Without it, reading such a frame's depth raises
DepthUnavailableError. depth is still required: the lens blur uses it.
CameraSpec
dataclass
¶
CameraSpec(schema_version: str, id: str, manufacturer: str | None, product: str | None, sensor: SensorSpec, lens: LensSpec, calibration: dict[str, Any] = dict(), sensor_model: dict[str, Any] = dict(), isp: dict[str, Any] = dict(), outputs: OutputsSpec = OutputsSpec(), validation: ValidationSpec = ValidationSpec(), source_path: Path | None = None)
from_yaml
classmethod
¶
from_yaml(path: str | Path, **overrides: Any) -> CameraSpec
Load a spec file; keyword overrides go to apply_overrides (width=, ...).
apply_overrides
¶
apply_overrides(data: dict[str, Any], *, width: int | None = None, height: int | None = None, focus_distance_m: float | None = None, near_m: float | None = None, far_m: float | None = None, depth_samples: int | None = None, field_samples: tuple[int, int] | None = None) -> dict[str, Any]
A copy of raw spec data with common changes applied: the same camera, re-configured.
width/height: the same sensor read out at another resolution. Explicit intrinsics and the pixel pitch scale with it (pixel-edge convention), so the field of view is unchanged.focus_distance_m: refocus the lens.near_m,far_m,depth_samples,field_samples(w, h): the PSF bank's depth range and sampling (calibration.psf).
CatalogRegistry
¶
CatalogRegistry(roots: list[str | Path] | None = None, use_env: bool = True, use_builtin: bool = True)
frame
¶
CameraFrame and TwinRobo tensor conventions.
Tensor conventions (the single source of truth for all TwinRobo modules):
rgb:torch.Tensor [B, 3, H, W], float, linear radiance-like values nominally in[0, 1](not sRGB-encoded). Channel order R, G, B.depth:torch.Tensor [B, 1, H, W], float, metric depth in meters, positive, measured along the optical axis (z-depth, not ray distance). Values that are non-finite (inf/nan, e.g. simulator background) or<= 0are invalid; consumers must mask or clamp them explicitly. Only cameras whose depth output is simulated (outputs.depthin their spec) expose it; none of the catalog does yet (see ROADMAP.md). Readingframe.depthof any other camera raisesDepthUnavailableError, unless the frame was made withforce_depth=True: the simulator's ground truth (labels, evaluation), not a depth sensor's reading.- Image coordinates: row 0 is the top of the image, column 0 is the left.
- Device: tensors stay on the producing device (normally CUDA). TwinRobo never moves per-frame data to the CPU unless the caller explicitly asks for it.
Backend-specific units (e.g. DeepLens uses millimeters and negative object
z) are converted only inside the corresponding adapter
(twinrobo.optics.deeplens).
CameraFrame
dataclass
¶
CameraFrame(rgb: Tensor, rgb_ideal: Tensor | None = None, rgb_optical: Tensor | None = None, raw: Tensor | None = None, depth: Tensor | None = None, timestamp: float | None = None, metadata: dict[str, Any] | None = None, has_depth: bool = True)
One simulated camera observation.
depth is readable only when has_depth is true: the camera outputs depth, or
the frame was made with force_depth=True. Otherwise reading it raises
DepthUnavailableError (a real camera of that kind gives no depth).
to
¶
to(device: str | device) -> CameraFrame
Return a copy with all tensors moved to device (explicit transfer only).
check_rgb
¶
Raise ValueError if rgb does not follow the [B, 3, H, W] convention.
check_depth
¶
Raise ValueError if depth does not follow the [B, 1, H, W] convention.
valid_depth_mask
¶
Boolean mask of valid depth pixels (finite and strictly positive).
CameraIntrinsics
dataclass
¶
Pinhole intrinsics in pixels (pixel-index coordinates: pixel (0, 0) center is 0).
For a CameraTwin these are the paraxial intrinsics of the lens at the spec resolution. They are the right pinhole for the simulator camera while distortion is not rendered.
stereo
¶
Stereo camera modules: two eyes in one housing, placed as one unit.
A module file (<catalog root>/<id>/module.yaml) names the per-eye CameraSpecs
and the right eye's pose in the left eye's camera frame. The module's pose is
the LEFT eye's (Stereolabs convention), so placing a module means placing its
left eye; the right eye follows rigidly (on a robot link it follows the link).
Simulator side, a module is expanded into two ordinary cameras: eye_mounts
turns a module mount into left/right CameraMounts, which render like any
other custom camera. Rectified output (what vendor SDKs deliver) is handled per
eye by CameraTwin.process(rectify=True); with identical eye intrinsics and a
pure translation the rectified pair is row-aligned.
StereoModule
dataclass
¶
StereoModule(id: str, left: Eye, right: Eye, manufacturer: str | None = None, product: str | None = None, housing_mm: tuple[float, float, float] | None = None, output: str = 'rectified', source_path: Path | None = None)
with_baseline
¶
with_baseline(baseline_m: float) -> StereoModule
The same module with another baseline: the right eye moved along the same direction from the left eye (the housing widened or narrowed with it).
Robots differ in how far apart they mount a stereo pair; this lets one catalog module stand for any of them.
Eye
dataclass
¶
Eye(spec: Path | None, translation_m: tuple[float, float, float] = (0.0, 0.0, 0.0), rotation_rpy_deg: tuple[float, float, float] = (0.0, 0.0, 0.0))
ModuleRegistry
¶
Stereo modules in the catalog roots (same roots as CatalogRegistry).
eye_poses
¶
eye_poses(module: StereoModule, pos, R_left: ndarray) -> dict[str, tuple[ndarray, ndarray]]
{eye: (position, camera axes)} in the parent frame, from the left eye's pose.
Camera axes follow MuJoCo (columns x right, y up, z backwards).
exceptions
¶
TwinRobo exception hierarchy.
CameraTwinError
¶
Bases: Exception
Base class for all TwinRobo errors.
SpecError
¶
Bases: CameraTwinError
A CameraSpec is malformed or uses an unsupported schema version.
CatalogError
¶
Bases: CameraTwinError
A camera ID cannot be resolved in the catalog.
OpticsBackendError
¶
Bases: CameraTwinError
An optics backend (e.g. DeepLens) is unavailable or failed.
DepthUnavailableError
¶
Bases: CameraTwinError
A frame's depth was read; its camera has no simulated depth output (outputs.depth).
SimulatorError
¶
Bases: CameraTwinError
A simulator adapter (e.g. Isaac Sim) is unavailable or failed.