Optics and rendering¶
TwinRobo turns what a simulator renders into what a real camera would record. This page covers how an image is formed: the PSF pipeline, lens distortion, and the two rendering methods a camera can use.
Conventions¶
Defined in twinrobo/frame.py, used across the package:
| Quantity | Shape | Units |
|---|---|---|
rgb |
[B, 3, H, W] float |
linear, nominally [0, 1] |
depth |
[B, 1, H, W] float |
meters, positive z-depth; inf/nan/<=0 = invalid |
Tensors stay on the GPU. DeepLens units (negative millimeters for object depth,
micrometers for wavelength) appear only inside twinrobo/optics/deeplens.py,
the single DeepLens boundary.
The PSF pipeline¶
offline (DeepLens, once per camera config, cached)
DeepLensOptics.build_psf_bank -> PSFBank [depth, field_y, field_x, rgb, ks, ks]
runtime (CameraTwin, GPU, no DeepLens calls)
DenseDepthRenderer.render(rgb, depth)
soft depth-layer assignment (disparity)
spatially varying scatter blur (bilinear PSF interpolation, FFT overlap-add)
occlusion-aware normalized layer compositing (near -> far)
from twinrobo import CameraTwin
# The first call builds the PSF bank with DeepLens (~20 s on a GPU) and caches it
# under $TWINROBO_CACHE/psf (default ~/.cache/twinrobo/psf); later calls load it.
camera = CameraTwin.from_catalog("stereolabs/zed-x/2.2mm")
frame = camera.process(rgb, depth) # in: linear RGB [B,3,H,W], metric depth [B,1,H,W], on GPU
frame.rgb_ideal, frame.rgb_optical, frame.rgb
To precompute a bank: python tools/generate_psf/generate_psf.py <spec.yaml>.
At 1920x1200 the renderer agrees with DeepLens' block PSF-map renderer at
about 46 dB PSNR. Its point responses match PSFs traced directly by DeepLens
close to the Monte Carlo noise floor
(validation). DeepLensOptics.render_reference
is the slow physical reference, used for validation only.
Lens distortion is modeled separately from blur. A spec with calibrated
intrinsics and calibration.distortion.model: opencv (k1 k2 p1 p2 k3) is
rendered from a wider pinhole source (camera.render_intrinsics, same central
focal length). DistortionWarp resamples it onto the distorted sensor
(bilinear color, nearest depth) before the PSF blur. frame.rgb_ideal is then
the undistorted pinhole view with the camera's intrinsics. With rectify=True
the output is undistorted back to that pinhole, keeping the lens blur, as
stereo SDKs deliver.
Rendering methods¶
Each TwinRobo camera picks how its image is formed, per MujocoCameraTwin.
| Method | How | Per frame, 1920×1200 (RTX 3090) | Gets right |
|---|---|---|---|
psf (default) |
pinhole render + depth- and field-dependent PSF blur (2.5D) | ~0.1–0.4 s | blur of what the pinhole sees |
raycast |
every pixel's rays traced through the real lens (DeepLens) and intersected with the scene's triangles on the GPU (NVIDIA Warp BVH); each hit shaded from a view rendered across the pupil that sees it | ~0.9 s | defocus across the aperture, chromatic aberration, vignetting, with exact per-ray visibility (see-through around defocused foreground) |
from twinrobo.plugins.mujoco import MujocoCameraTwin
cam = MujocoCameraTwin(
twin,
backend,
camera="agentview",
render="raycast",
rays_per_pixel=8,
pupil_views=7,
shading="corrected",
view_oversample=1,
)
frame = cam.get_frame() # frame.metadata has the render stats
- Lens rays (
twinrobo.optics.lensrays, traced byDeepLensOptics.trace_lens_rays): - sensor pixel -> exit pupil -> lens -> object-space rays, per RGB wavelength, in the MuJoCo/USD camera frame. They depend only on lens, focus and sensor, so they are traced once per config (~13 s, 0.6 GB for 1920×1200 at 8 rays/px) and cached.
- Brightness is a separate, densely traced relative-illumination map, so a few rays per pixel don't turn vignetting into noise. Strongly vignetted pixels are retraced with 4x candidates.
- Shading is the simulator's own (no PBR). The shading views
(
pupil_views) are MuJoCo rasterizations from points on the entrance pupil, and hits look up colors there with depth-aware filtering. - Lens shading:
corrected(default) is what a camera ISP outputs after lens-shading correction;rawkeeps the sensor's vignetting and cos^4 falloff. - Geometry (
lens.geometryin the spec): calibration(default): every pixel's rays are turned together so that its chief ray follows the spec's intrinsics and distortion (LensRays.with_geometry). The lens file then sets blur, chromatic aberration and vignetting only; both methods have the same geometry. This is right for the catalog, whose lens files are surrogates (a published design scaled to the camera's focal length), not the camera's own lens.lens: the traced lens' own distortion, for a real lens prescription. Specs without intrinsics or withcalibration.distortion.model: physicaluse it too.- Rectify: lens-ray images are undistorted with their chief-ray map
(
LensRays.rectify_grid): the calibration's geometry, or the lens' own. - Validation (
tests/plugins/mujoco/test_lensrender.py): - A marker lands where the lens' chief ray points (282.5 px; the paraxial pinhole says 287).
- A post 0.3 m in front of the focus plane: its partially covered, defocused edges match the exact answer computed from the traced rays to ~0.05 with ray cast (pinhole + PSF 0.2–0.4).
- Limits:
- Objects thinner than a sensor pixel: MuJoCo multisamples color, so their
silhouettes are blends in every view. Raise
view_oversamplefor them. - Height fields and SDF geoms are not ray cast.
- Scenes come from a simulator's lens scene (
twinrobo.optics.lensrender): MuJoCo (twinrobo.plugins.mujoco.lensrender) and Isaac Sim (twinrobo.plugins.isaac.lens). - Lens files matter: ray-based methods trace the lens file literally. The ZED X lens in the catalog is a blur surrogate (a scaled Canon RF16mm prescription); its image circle does not cover the full ZED X sensor, so its corners render dark.
Performance notes¶
At 1920x1200 with 16 layers and a 17x11 PSF grid on an RTX 3090, the PSF renderer takes about 470 ms when all 16 depth layers are occupied (a ground plane does that) and about 40 ms for a single plane. The cost scales with the number of occupied layers. Levers not yet explored: layer/tile sparsity, fewer blurred alpha channels, low-rank PSF bases.