Optics¶
base
¶
DeepLensOptics
¶
DeepLensOptics(model_path: str | Path, sensor_resolution: tuple[int, int] | None = None, focus_distance_m: float | None = None, device: str | device | None = None, dtype: dtype = float32, psf_ks: int = 65)
Bases: OpticsModel
Physical optics backed by a DeepLens geometric lens (GeoLens).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_path
|
str | Path
|
DeepLens lens file ( |
required |
sensor_resolution
|
tuple[int, int] | None
|
|
None
|
focus_distance_m
|
float | None
|
Object focus distance in meters ( |
None
|
device
|
str | device | None
|
Torch device. Defaults to CUDA when available. |
None
|
dtype
|
dtype
|
Torch dtype for the lens. |
float32
|
psf_ks
|
int
|
PSF kernel size in pixels. DeepLens centers even-size PSFs between
pixels (on-axis centroid at |
65
|
focal_length_mm
property
¶
Paraxial effective focal length computed by DeepLens (not a lens-file header).
intrinsics
¶
intrinsics() -> CameraIntrinsics
Paraxial pinhole intrinsics at the sensor resolution (no distortion).
set_focus
¶
Refocus the lens to an object distance in meters (inf = infinity).
generate_psf
¶
generate_psf(field_positions: Tensor | Sequence[Sequence[float]], depths: Tensor | Sequence[float], wavelengths: Sequence[float] | None = None, ks: int | None = None, spp: int | None = None) -> Tensor
Compute PSFs on a field × depth × wavelength grid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
field_positions
|
Tensor | Sequence[Sequence[float]]
|
|
required |
depths
|
Tensor | Sequence[float]
|
|
required |
wavelengths
|
Sequence[float] | None
|
Wavelengths in micrometers. Defaults to the lens RGB wavelengths, giving an RGB PSF stack. |
None
|
ks
|
int | None
|
Kernel size in pixels (defaults to |
None
|
spp
|
int | None
|
Rays per point source (DeepLens default when |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
|
sample_depths
¶
Depth layers in meters, far -> near, uniform in disparity.
When the focus distance is known (focus_distance_m) and lies inside
[near_m, far_m], it is an explicit layer, with the remaining layers
split in proportion to the disparity range on each side. This avoids
interpolating two defocused PSFs where the image should be sharpest.
DeepLens' own focal-plane estimate (calc_focal_plane) is not used:
it is Monte Carlo and unstable for lenses focused near infinity.
bank_key
¶
bank_key(grid: tuple[int, int], near_m: float, far_m: float, num_depths: int, ks: int | None = None, spp: int | None = None, seed: int = 0) -> dict[str, Any]
Content-based cache key for build_psf_bank (see twinrobo.optics.cache).
lens_rays_key
¶
Content-based identity of trace_lens_rays output (lens, focus, sensor, sampling).
trace_lens_rays
¶
trace_lens_rays(rays_per_pixel: int = 8, seed: int = 0, overfill: float = 1.15, chunk_rays: int = 4000000, progress=None, candidates: int = 4)
Trace every sensor pixel through the lens into object space (see LensRays).
Each pixel gets rays_per_pixel rays per RGB wavelength: a jittered
point inside the pixel aimed at a stratified point of the disk on the exit
pupil plane through which that pixel's light passes (_ray_aim; the
pupil enlarged by overfill bounds it), so the stop blocks few of them.
Rays are traced backward (sensor -> object) with DeepLens' own surface tracer.
candidates: each pixel traces candidates x rays_per_pixel rays and
keeps rays_per_pixel that pass the lens, in random order. Strongly
vignetted pixels then still get their full ray count (less noise); their
brightness comes from the separately traced relative illumination.
build_psf_bank
¶
build_psf_bank(grid: tuple[int, int] = (17, 11), near_m: float = 0.1, far_m: float = inf, num_depths: int = 16, ks: int | None = None, spp: int | None = None, seed: int = 0) -> PSFBank
Precompute the runtime PSFBank on a (Gw, Gh) field grid x depth layers.
DeepLens samples rays at random; seed fixes them, so a bank is the same every
time it is built on a device (the caller's random state is left untouched).
render_reference
¶
render_reference(rgb: Tensor, depth: Tensor, psf_grid: tuple[int, int] = (17, 11), near_m: float = 0.3, far_m: float = DEFAULT_FAR_M, num_layers: int = 16, depths_m: Tensor | Sequence[float] | None = None) -> Tensor
High-fidelity offline reference render, built from DeepLens primitives.
The full DeepLens PSF map is computed at every depth layer
(psf_map_rgb), then DeepLens' block-wise, depth-interpolated PSF-map
convolution is applied (conv_psf_map_depth_interp, disparity
interpolation). It has no occlusion handling and uses one PSF per grid
block. PSFs are recomputed on every call, so it is far too slow for
per-frame simulation. Use it for validation and reference data only. The runtime renderer is
twinrobo.optics.dense_depth.DenseDepthRenderer.
Invalid depth (non-finite or <= 0, e.g. simulator background) is
replaced by far_m. Depth outside the layer range is clamped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rgb
|
Tensor
|
|
required |
depth
|
Tensor
|
|
required |
psf_grid
|
tuple[int, int]
|
|
(17, 11)
|
near_m
|
float
|
Nearest depth layer (m), via |
0.3
|
far_m
|
float
|
Farthest depth layer (m). |
DEFAULT_FAR_M
|
num_layers
|
int
|
Number of depth layers. |
16
|
Returns:
| Type | Description |
|---|---|
Tensor
|
|
render
¶
OpticsModel.render: the slow offline reference renderer.
For runtime use, build a PSFBank with build_psf_bank and render with
twinrobo.optics.dense_depth.DenseDepthRenderer.
PSFBank
dataclass
¶
PSFBank(psfs: Tensor, depths_m: Tensor, sensor_resolution: tuple[int, int], wavelengths_um: list[float] | None = None, metadata: dict[str, Any] = dict())
PSFCache
¶
get_or_build
¶
get_or_build(key: dict[str, Any], build: Callable[[], PSFBank], device=None, source: str | Path | None = None) -> PSFBank
Load or build the bank for key; source (a preset file) is recorded as a user.
release
¶
Drop source from every bank's users; delete banks nobody uses any more.
Returns the deleted .pt files. Legacy banks (no used_by) are kept.
DenseDepthRenderer
¶
DenseDepthRenderer(bank: PSFBank, eps: float = 0.0001, padding: str = 'reflect')
Bases: OpticsModel
Occlusion-aware, spatially varying, depth-dependent PSF rendering on the GPU.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bank
|
PSFBank
|
PSF bank. Its |
required |
eps
|
float
|
Normalization threshold. Where the blurred cumulative alpha is
below |
0.0001
|
padding
|
str
|
|
'reflect'
|
render_differentiable
¶
render without no_grad: gradients flow to rgb (and to bank.psfs
through the blurred layers). The cached unit response H_k(1) is detached, so
rebuild the renderer if the bank PSFs change.
distortion
¶
Parametric lens distortion (OpenCV / Brown-Conrady) and its rendering warp.
Geometry and blur are modelled separately. Blur comes from the PSF bank (PSFs recentered on the chief ray, so it carries no geometric shift); geometry comes from the spec's intrinsics plus this distortion model, which is what a calibration (e.g. a vendor factory calibration file) measures.
Rendering: the simulator renders a wider pinhole source image with the same
focal length at the center; DistortionWarp resamples it onto the distorted
sensor (bilinear for color, nearest for depth, so no false mid-depths appear at
edges). The source covers the undistorted field of every sensor pixel.
Coordinates follow OpenCV: normalized x = (u - cx) / fx, y = (v - cy) / fy
with pixel (0, 0) at the center of the top-left pixel and +y down.
RadialDistortion
dataclass
¶
RadialDistortion(k1: float = 0.0, k2: float = 0.0, p1: float = 0.0, p2: float = 0.0, k3: float = 0.0)
OpenCV distortion (k1, k2, p1, p2, k3): normalized undistorted -> distorted.
from_dict
classmethod
¶
from_dict(d: dict[str, Any] | None) -> RadialDistortion | None
From calibration.distortion; None unless model is parametric.
undistort
¶
Distorted -> undistorted by fixed-point iteration (OpenCV's scheme).
DistortionWarp
¶
DistortionWarp(intrinsics: CameraIntrinsics, distortion: RadialDistortion, margin_px: int = 2)
Resample a wide pinhole source render onto a distorted sensor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
intrinsics
|
CameraIntrinsics
|
The camera's (sensor) intrinsics. |
required |
distortion
|
RadialDistortion
|
Its distortion model. |
required |
margin_px
|
int
|
Extra source pixels around the needed field (bilinear support). |
2
|
pinhole
¶
The undistorted pinhole view with the sensor's intrinsics (for comparison).
rectify
¶
Undistort a sensor image onto the pinhole with the sensor's intrinsics.
This is what vendor SDKs deliver as "rectified": the lens blur stays, the geometry becomes pinhole. Pixels whose ray falls outside the sensor (corners, for barrel distortion) repeat the border.
field_of_view
¶
field_of_view(intr: CameraIntrinsics, dist: RadialDistortion | None) -> dict[str, float]
Real horizontal / vertical / diagonal FoV in degrees (through the principal point).
lensrays
¶
Object-space rays of a real lens: sensor pixel -> lens -> scene (simulator independent).
LensRays holds, for every sensor pixel, n rays per wavelength that leave the
sensor, pass through the lens, and enter the scene. They are traced once by an
optics backend (DeepLensOptics.trace_lens_rays) and then used by the
ray-based renderers (twinrobo.plugins.mujoco.lensrender). They depend only on the
lens, focus and sensor, never on the scene, so they are cached.
Conventions. The camera frame is MuJoCo's and Isaac's (USD): x right, y up, and
the camera looks along -z. A ray is o + s * (tx, ty, -1):
origin(ox, oy)in meters on the camera'sz = 0plane (the lens front vertex plane; for the bundled lenses the stop is there).tan(tx, ty): direction slopes. They are stored as a residual on top of the pixel's paraxial pinhole direction (intrinsics), which keeps fp16 exact to well below a pixel.weight: 0 for rays the lens blocks, elsecos^4of the sensor-side angle. A pixel's color is the weight-normalized mean of its rays' radiance timesillumination: the pixel's relative illumination (vignetting andcos^4falloff, 1 on axis), traced densely on a coarse grid. Splitting brightness from blur keeps a few rays per pixel from turning vignetting into noise.
Pixel (u, v) (row 0 = top) is the image a camera outputs, i.e. upright; the
optical inversion on the sensor is handled when tracing.
LensRays
dataclass
¶
LensRays(intrinsics: CameraIntrinsics, wavelengths_um: list[float], origin: Tensor, tan_res: Tensor, weight: Tensor, chief_tan: Tensor, illumination: Tensor, pupil_radius_m: float, metadata: dict[str, Any] = dict())
pinhole_tan
¶
Paraxial pinhole slopes of the pixel centers, [H, W, 2].
rays
¶
(origin [h, W, n, 2], tan [h, W, n, 2], weight [h, W, n]) as fp32 for rows.
max_tan
¶
Largest |tx|, |ty| of any unblocked ray (the field a source view must cover).
with_geometry
¶
with_geometry(intrinsics: CameraIntrinsics, distortion=None) -> LensRays
These rays with a calibrated camera's geometry and the traced lens' blur.
Every ray of a pixel is turned by the same small angle, so that the pixel's
chief ray points where the calibration (intrinsics and OpenCV
distortion, a RadialDistortion or None) says it does. The spread of
the rays around it (blur, lateral color) and the vignetting stay the lens'
own. For a surrogate lens this keeps the catalog's geometry instead of the
surrogate design's distortion.
rectify_grid
¶
grid_sample grid [1, H, W, 2] undistorting a lens image to the paraxial pinhole.
For each rectified pixel (paraxial direction t*), finds the sensor
position whose chief ray has direction t* by Newton iteration on the
chief-ray map. This is what a stereo SDK's rectification does, computed from
the traced lens instead of a fitted distortion model.
concentric_disk
¶
Shirley-Chiu map of the unit square to the unit disk (area preserving, low distortion).
stratified_samples
¶
count x n jittered stratified samples of the unit square (n split into a grid).
lensrender
¶
Lens-ray rendering: sensor -> real lens -> scene, for any simulator.
"raycast" traces every sensor pixel's rays through the lens (LensRays, per RGB
wavelength) and intersects each with the scene's triangles on the GPU (NVIDIA Warp
BVH) for its exact hit point. The hit's color is the simulator's own shading (no
PBR): the entrance pupil is split into views cells, the simulator rasterizes one
wide pinhole view (RGB + depth) from each cell's center, and a hit takes its color
from the nearest view in which it is visible (a depth test). This gives exact per-ray
visibility with the simulator's shading, which is view-dependent only through
specular highlights.
A simulator plugs in through a lens scene (LensRayRenderer.render_scene):
render_views(views)->(rgb [k, 3, h, w] linear, depth [k, 1, h, w] m): the views across the pupil, pinholes ofviews.fovy_degatviews.width x views.height, from the camera shifted by eachviews.centersoffset (m, camera frame);camera_pose()->(R [3, 3], p [3]): camera axes as columns (x right, y up, looking along -z) and position, world frame, meters;mesh(device)-> aTriangleMeshof the scene at the current state.
Adapters: twinrobo.plugins.mujoco.lensrender (MuJoCo) and twinrobo.plugins.isaac.lens
(Isaac Sim).
Rays depend on lens, focus and sensor only, so they are traced once (lens_rays)
and cached on the GPU.
PupilViews
¶
PupilViews(rays: LensRays, k: int, margin: float = 0.06, oversample: float = 1.0)
k rasterized views (linear RGB + z-depth) from the pupil cell centers.
set_views
¶
Install rendered views: rgb [k, 3, h, w] linear, depth [k, 1, h, w] m.
depth_at
¶
Nearest-tap z-depth of per-ray views k along tan.
shade
¶
Color of per-ray views k [M] along tan, using only taps at depth zref.
A lookup beside a silhouette would otherwise blend the occluder into the
surface behind it (and rasterizers antialias color, so silhouette pixels are
themselves blends). Preference: matching depth off the silhouette ("clean"),
then matching depth on it, then the nearest tap. Returns (color, clean,
seen): whether a clean tap was used, and whether any tap matches zref.
color_at
¶
Plain bilinear color of per-ray views k along tan (no depth test).
TriangleMesh
¶
Scene triangles as one GPU BVH (NVIDIA Warp), posed per frame and refit.
local [V, 3] vertices in their owners' frames, faces [F, 3], owner [V]:
the index of each vertex's rigid frame. update poses all of them.
update
¶
Pose every owner: world = R[owner] @ local + t[owner].
R [P, 3, 3] rotations (with scale, if any) and t [P, 3] translations.
cast
¶
Hit distance along unit dirs (1e30 for a miss), [M].
cast_faces
¶
Hit distance along unit dirs (1e30 for a miss) and the face hit (-1).
face_owner
¶
The owner (rigid frame) of each face [M] (faces of one owner share it).
face_normal
¶
World-frame unit normals of faces [M] at the last update.
LensRayRenderer
¶
LensRayRenderer(rays: LensRays, method: str = 'raycast', views: int = 7, shading: str = 'corrected', chunk: int = 4000000, oversample: float = 1.0)
Render a camera through a real lens by ray casting ("raycast").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rays
|
LensRays
|
traced |
required |
method
|
str
|
|
'raycast'
|
views
|
int
|
number of pupil cells / rasterized shading views. |
7
|
oversample
|
float
|
resolution of the views relative to the sensor. Above 1 resolves detail thinner than a sensor pixel (the views are the shading source). |
1.0
|
shading
|
str
|
|
'corrected'
|
chunk
|
int
|
rays per GPU batch. |
4000000
|
render_scene
¶
(rgb [1, 3, H, W] linear, depth [1, 1, H, W] m) of a lens scene's current state.
lens_rays
¶
lens_rays(reference, rays_per_pixel: int = 8, status=None, intrinsics=None, distortion=None) -> LensRays
Traced rays of a DeepLensOptics (cached; the first trace takes seconds).
With intrinsics (and optionally distortion), the rays take that
calibrated geometry and keep the lens' blur (LensRays.with_geometry); see
CameraTwin.lens_geometry. Without, the traced lens' own distortion is kept.
pupil_centers
¶
pupil_centers(rays: LensRays, k: int, iters: int = 12) -> Tensor
k cell centers [k, 2] (m) on the entrance pupil: k-means of the ray origins.
check_method
¶
Raise a clear error for an unknown or removed rendering method.
lens_frame
¶
lens_frame(twin, renderer: LensRayRenderer, rgb: Tensor, depth: Tensor, ideal: Tensor, rectify: bool, timestamp=None, metadata: dict | None = None, force_depth: bool = False)
A CameraFrame from a lens render: sensor and ISP, then optional rectification.
rgb/depth come from LensRayRenderer.render_scene; ideal is the paraxial
pinhole view of the same sensor (renderer.rays.intrinsics), linear.
rectify undistorts with the lens' own chief rays, as stereo SDKs deliver.
force_depth: expose depth even if the camera outputs none (see CameraTwin.process).