Skip to main content

TwinRobo

Physically grounded digital twins of real cameras for robot learning in simulation.

CI License: AGPL-3.0 Commercial license Camera catalog: Apache-2.0 Python 3.12 Status: alpha Docs Demo

Existing simulators render through a perfect pinhole camera, while in the real world robots see through real cameras: lenses with their own field of view, defocus blur, distortion, aberration and vignetting. Policies trained on pinhole images meet a different image at deployment, causing a significant domain gap between simulation and reality. TwinRobo closes that gap: it renders scenes through the actual optics of the camera you will deploy, as a drop-in for the simulators robot learning already uses.

One environment, any real camera. TwinRobo is an open environment for aligning perception with off-the-shelf cameras: pick a camera from the catalog, and every simulator, rendering method and dataset replay in TwinRobo sees through it. The catalog grows with the community: add a camera and it works everywhere at once; camera makers can become partners and ship verified models of their products.

An Isaac Sim tabletop: the simulator's pinhole (left) and the Stereolabs ZED X 2.2 mm, every pixel ray-traced through its lens (right)

At a glance

MuJoCo (incl. LIBERO, RoboCasa) NVIDIA Isaac Sim 6.x
Adapter twinrobo.plugins.mujoco.MujocoCameraTwin twinrobo.plugins.isaac.IsaacCameraTwin
psf: pinhole + lens blur and distortion ✅ ✅
raycast: lens rays cast into the scene ✅ ✅
RGB on the GPU, ground-truth depth on request ✅ ✅
Cameras on robot links, stereo modules ✅ any USD camera prim
Setup pip install Docker recipe included

Cameras: 12 in the catalog, including Intel RealSense D435 / D455, Stereolabs ZED X, ZED X Mini and ZED 2i, Luxonis OAK-D, Logitech C920 and the Raspberry Pi Camera Module 3, as single cameras or stereo pairs with rectified output. Every camera works in every simulator and rendering method. Add yours or calibrate one.

Features

  • Real lens optics. Lenses are traced with DeepLens: depth- and field-dependent blur, aberration and vignetting from a lens prescription instead of a hand-tuned filter, and distortion from the camera's calibration or the lens' own.
  • Two rendering methods, per camera. A fast 2.5D PSF renderer, and a GPU ray cast (NVIDIA Warp) that traces every pixel's rays through the lens into the scene, exact even for defocused foreground objects. More
  • Drop-in for robot simulators. MuJoCo, LIBERO (robosuite 1.4), RoboCasa kitchens (robosuite 1.5) and NVIDIA Isaac Sim (any USD stage, RTX rendering). CameraTwinLiberoEnv swaps a camera's observations in place, so policies and eval scripts run unchanged; recorded demos replay exactly, so any camera can be placed in an existing episode. More
  • Camera catalog and stereo. The cameras robots use (RealSense, ZED, OAK-D, webcams, Raspberry Pi), stereo modules with rectified output and adjustable baselines, and cameras mountable on any robot link. Adding a camera is a YAML file and a lens file. More
  • Calibrate a real unit. python -m twinrobo.calibration turns ChArUco, slanted-edge and flat-field captures into a measured entry: intrinsics, distortion, focus, vignetting and stereo baseline. More
  • Validated in both simulators. The PSF renderer matches DeepLens' reference renderer at about 46 dB PSNR. With the lens-ray methods, points land within a pixel of where the lens' chief rays point (within 0.1 px in Isaac). On a defocused foreground object, ray cast reproduces the exact coverage of its partially covered edge to within ~0.03 (Isaac) and ~0.05 (MuJoCo), where pinhole + PSF errs by 0.13–0.4.

Installation

TwinRobo requires Python 3.12 (inherited from DeepLens) and a CUDA GPU for practical speeds.

The current release, v0.1.0 (changes), from PyPI:

conda create -n twinrobo python=3.12 -y && conda activate twinrobo
pip install "twinrobo-core[mujoco]"     # TwinRobo, DeepLens, MuJoCo and NVIDIA Warp

or with Isaac Sim 6.1 (Ubuntu 22.04 or later; one package comes from NVIDIA's index; use its own environment, as Isaac Sim brings its own MuJoCo):

pip install "twinrobo-core[isaac]" --extra-index-url https://pypi.nvidia.com

From a checkout, for development or the LIBERO support:

git clone https://github.com/TwinRobo/TwinRobo-Core.git && cd TwinRobo-Core
conda create -n twinrobo python=3.12 -y && conda activate twinrobo
pip install -e ".[dev]"          # core + DeepLens
pip install -e ".[libero]"       # + MuJoCo, NVIDIA Warp and LIBERO support

LIBERO itself is used from a checkout (LIBERO_ROOT). RoboCasa needs robosuite 1.5 and gets its own environment; see simulators. Isaac Sim also runs in NVIDIA's container, with TwinRobo installed on Isaac's own Python and torch:

docker pull nvcr.io/nvidia/isaac-sim:6.1.0        # needs the NVIDIA Container Toolkit
docker/isaac/run.sh examples/01_isaac_camera.py   # builds the image on first use

Quick start: mount a real camera on a robot

Pick a camera from the catalog, mount it on a robot link, and render what it would record. This runs in robosuite's PickPlace scene (pip install -e ".[libero]" brings robosuite; no LIBERO checkout needed):

import robosuite

from twinrobo import CameraSpec, CameraTwin, CatalogRegistry
from twinrobo.plugins.mujoco import MujocoCameraTwin, RobosuiteRenderer, to_uint8
from twinrobo.plugins.mujoco.mounts import CameraMount, mounted

env = robosuite.make(
    "PickPlace",
    robots="Panda",
    has_renderer=False,
    has_offscreen_renderer=True,
    use_camera_obs=False,
)
env.reset()

# 1. Pick a real camera from the catalog (here read out at 960x600).
path = CatalogRegistry().resolve("stereolabs/zed-x/2.2mm")
twin = CameraTwin.from_spec(CameraSpec.from_yaml(path, width=960, height=600))

# 2. Mount it on a robot link: position (m) and yaw/pitch/roll (deg) in the link's frame.
wrist = CameraMount("wrist", body="robot0_right_hand", pos=(0.08, 0, 0), rpy_deg=(67, -48, 173))

# 3. Render what that camera would record.
with mounted(env.sim.model._model, env.sim.data._data, wrist) as host:
    frame = MujocoCameraTwin(twin, RobosuiteRenderer(env), camera=host).get_frame(force_depth=True)

image = to_uint8(frame.rgb)  # the real camera's image, uint8 [H, W, 3]
pinhole = to_uint8(frame.rgb_ideal)  # the simulator's ideal pinhole, same camera
depth = frame.depth[0, 0]  # ground-truth z-depth (m): a label, not the ZED X's own depth

The first call builds the lens' PSF bank with DeepLens (about 20 s on a GPU) and caches it; after that a frame takes a fraction of a second. The mount moves with the link, so it follows the robot as it acts or replays a demo.

examples/05_mount_camera_on_robot.py does this for three catalog cameras on the same wrist mount and draws what each one gives you:

MUJOCO_GL=egl python examples/05_mount_camera_on_robot.py   # writes outputs/05_mount_camera.jpg

Three catalog cameras on the same wrist mount: pinhole vs TwinRobo image, close-up, difference and depth

Read it top to bottom:

  • Simulator pinhole / TwinRobo camera: the field of view is each lens' own. The ZED X 2.2 mm's wide lens shows its barrel distortion, which a pinhole cannot.
  • Close-up: the same patch at native resolution; the real lens softens the lettering, as the camera would.
  • Difference ×4: where the optics change the image: distortion shifts edges for the 2.2 mm lens; blur changes edges and texture for the others.
  • Ground-truth depth: the simulator's metric z-depth, aligned pixel for pixel with the camera's image; a label for training, not an output of the camera (force_depth=True).

Next steps

  • Ray casting: pass render="raycast" to MujocoCameraTwin to trace real lens rays into the scene (see Rendering methods).

  • Isaac Sim: the same twin on any USD camera prim:

    from twinrobo.plugins.isaac import IsaacCameraTwin
    
    twin = CameraTwin.from_catalog("stereolabs/zed-x/2.2mm", build_psf=False)
    cam = IsaacCameraTwin(twin, "/World/Camera", render="raycast")
    frame = cam.get_frame()  # frame.rgb: the ZED X's view, on the GPU
    

    docker/isaac/run.sh examples/01_isaac_camera.py --render raycast renders the image at the top of this page. More

  • A LIBERO policy's camera: wrap an env so its observations come from the twin, and eval scripts run unchanged:

    from twinrobo.datasets.libero import CameraTwinLiberoEnv, make_env
    
    env, task, init_states = make_env("libero_spatial", 0, resolution=128)
    env = CameraTwinLiberoEnv(env, {"agentview": twin})
    obs = env.reset()  # obs["agentview_image"] now comes from the twin
    
  • An RGB-D frame from any source: twin.process(rgb, depth) takes linear RGB [B,3,H,W] and metric depth [B,1,H,W] on the GPU (depth sets each pixel's defocus) and returns the camera's image.

  • Your own camera: copy a catalog spec, or add one for everyone; see Cameras and CONTRIBUTING.

Rendering methods

Method How Per frame, 1920×1200 (RTX 3090) Best for
psf pinhole render + depth- and field-dependent PSF blur ~0.1–0.4 s fast training data
raycast lens rays intersected with the scene on the GPU ~0.9 s (MuJoCo), ~1.5 s (Isaac) defocus across the aperture, aberration, vignetting; exact defocus around occluders

Ray casting is simulator independent (twinrobo.optics.lensrender): a simulator supplies the camera pose, views rasterized from points across the lens pupil (for shading), and the scene's triangles.

Details and validation

Supported simulators

Simulator Scenes and data Rendering methods
MuJoCo any MJCF scene psf, raycast
LIBERO LIBERO tasks and demos; Panda, Sawyer, UR5e, iiwa, Jaco, Gen3, multi-camera variants psf, raycast
RoboCasa procedurally generated kitchens, LeRobot demo datasets, PandaOmron psf, raycast
NVIDIA Isaac Sim 6.x any USD stage, RTX rendering (Docker recipe included) psf, raycast

Camera catalog

Vendor Cameras Stereo modules (baseline)
Intel RealSense D435 / D435i color and IR imager, D455 color and IR imager D435 (50 mm), D455 (95 mm)
Stereolabs ZED X 2.2 mm and 4 mm eyes, ZED 2i 2.1 mm eye ZED X (120 mm), ZED X Mini (50 mm), ZED 2i (120 mm)
Luxonis OAK-D color and mono OAK-D (75 mm)
Logitech C920 HD Pro Webcam
Raspberry Pi Camera Module 3, Camera Module 3 Wide

All entries are estimated today: geometry fitted to the datasheet fields of view, blur from a surrogate lens of matching field, every number sourced in the entry's header. The catalog page lists each with its sensor, lens and field of view, generated from the catalog files.

The camera catalog is Apache-2.0. Everything in twinrobo/catalog/ (camera specs, stereo modules and lens files) is licensed under the Apache License 2.0, separately from the AGPL-3.0 code: use the camera data in any project, commercial or not, with or without TwinRobo.

Add your camera

The catalog is the heart of TwinRobo, and it is open. Every camera added becomes available to everyone, in every simulator, rendering method and dataset replay, with no simulator-specific work. If your robot uses a camera that is not here, it is the most valuable contribution you can make:

  1. Create twinrobo/catalog/<vendor>/<model>/<lens>/ with a camera.yaml (sensor, lens, focus, calibration; copy an existing entry) and a DeepLens lens.json. No prescription published? Build a surrogate of the same field of view and f-number with tools/catalog/make_surrogate_lens.py.
  2. Run pytest tests/test_catalog.py and render it once: CameraTwin.from_catalog("<vendor>/<model>/<lens>").
  3. Open a pull request, with where each number came from. Catalog contributions are published under Apache-2.0, like the rest of the catalog.

Calibrated a real unit? Upgrading an entry from estimated to measured is just as welcome. The full guide is in CONTRIBUTING; to ask for a camera instead, open a camera request.

For camera makers: become a partner

Put your camera in every robot-learning simulator. Robot teams can now choose and validate cameras in simulation before they buy hardware. The TwinRobo Camera Partner program lets camera and lens makers ship manufacturer_verified models of their products, and lets companies sponsor the open environment their customers use:

  • You contribute any of: optical data (a lens prescription or measured PSFs), factory calibration, sample units, or sponsorship of compute and maintenance. Published entries are Apache-2.0, free for your customers to use anywhere; data you cannot publish can stay private.
  • You get a verified catalog entry that works in MuJoCo, Isaac Sim and every rendering method, your company listed as a partner here and on the docs site, a showcase example with your camera, and early integration of new products.

Read about the program or open a partner inquiry.

Documentation

The documentation site, with the API reference, is at twinrobo.github.io; its source is in TwinRobo/twinrobo.github.io.

Optics and rendering PSF pipeline, distortion, the two rendering methods, validation
Cameras and stereo CameraSpec, catalog, stereo modules, spec overrides
Simulators MuJoCo, LIBERO, RoboCasa and Isaac Sim: setup, behavior, limits
API reference every public class and function, from the docstrings
Development layout, tests, status

Contributing

Beyond cameras, also wanted: new simulator adapters (a lens scene is three methods; see twinrobo.optics.lensrender), sensor noise and ISP models, and documentation. See CONTRIBUTING, and the ROADMAP for what is planned (simulated depth cameras first).

Roadmap

  • Simulated depth cameras: depth from the stereo modules, with a real sensor's errors
  • Faster ray casting in Isaac Sim (fewer, shared shading views)
  • Sensor noise and ISP models (interfaces are in place)
  • measured catalog entries from calibration captures of real units
  • More cameras and lenses in the catalog

Acknowledgements

TwinRobo builds on DeepLens for lens modeling, MuJoCo, NVIDIA Isaac Sim, robosuite, LIBERO, RoboCasa and NVIDIA Warp. Third-party files and their licenses are listed in NOTICE.

License

TwinRobo is dual-licensed: the GNU AGPL-3.0 (LICENSE, with an additional permission for NVIDIA Isaac Sim / Omniverse in LICENSE-EXCEPTION.md), free for research, education and open-source use, or a commercial license for proprietary products and hosted services (COMMERCIAL.md). The camera catalog (twinrobo/catalog/) is Apache-2.0, free to use anywhere. Third-party material is listed in NOTICE.

Metadata

Release files for twinrobo-core 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for twinrobo-core 0.1.0
File Size Uploaded Provenance Actions
twinrobo_core-0.1.0.tar.gz 163.0 kB With provenance Details for twinrobo_core-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for twinrobo-core 0.1.0
File Interpreter ABI Platform Actions
twinrobo_core-0.1.0-py3-none-any.whl Python 3 none any Details for twinrobo_core-0.1.0-py3-none-any.whl

Total release size: 362.0 kB

Release files / twinrobo_core-0.1.0.tar.gz

Download URL twinrobo_core-0.1.0.tar.gz
Size 163.0 kB
Tags Source
SHA-256 checksum
How to use checksums
20781cb3267217df9b8e2f78020427664d51eb9472a58ba12b1848315c004fe7
BLAKE2b-256 checksum
How to use checksums
879c93b39cb39c36a443d87bbcf4fadfa3187c9c47710807355ae649e2078929
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

Release files / twinrobo_core-0.1.0-py3-none-any.whl

Download URL twinrobo_core-0.1.0-py3-none-any.whl
Size 199.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6230c7b82afdcf90a515ef1a1bc45435a0e9091a9224dfdeeff701a3122d67a9
BLAKE2b-256 checksum
How to use checksums
b40ad8441ae8a5a2ab5d756f12b100c71c8d31e07bc2673d89cda1a82b1485c3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

Security for twinrobo-core 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for twinrobo_core-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page