📷 Camera & Vision — Getting RGB-D Images¶
Beyond reading joint angles and body poses, you can also obtain images from the simulation — like giving the robot eyes.
OrcaGym receives real-time rendered camera frames from OrcaStudio via WebSocket.
Quick Start: Getting Your First Image¶
"""
first_camera.py — Get the first image from a simulation camera
"""
import time
import numpy as np
from orca_gym.sensor.rgbd_camera import CameraWrapper
# 1. Create camera wrapper
# name: camera name (arbitrary)
# port: WebSocket port (configured in OrcaStudio)
camera = CameraWrapper(name="front_camera", port=8765)
# 2. Start camera stream (a background thread auto-receives and decodes frames)
camera.start()
print("Camera started, waiting for first frame...")
# 3. Wait for the first frame to arrive
while not camera.is_first_frame_received():
time.sleep(0.1)
print(f"✅ First frame received!")
# 4. Get the image
image = camera.image # NumPy array, shape (H, W, 3), format BGR
print(f" Resolution: {image.shape[1]}x{image.shape[0]}")
print(f" Data type: {image.dtype}")
print(f" Pixel range: [{image.min()}, {image.max()}]")
print(f" Frame index: {camera.image_index}")
# 5. Save the image (with OpenCV)
import cv2
cv2.imwrite("first_frame.png", image)
print("✅ Image saved to first_frame.png")
# 6. Stop the camera
camera.stop()
Integrating the Camera into an Environment Class¶
Make the camera part of the environment, capturing an image at each step:
"""
vision_env.py — Environment with camera observations
"""
import time
import numpy as np
from gymnasium import spaces
from orca_gym.environment.euler.orca_gym_euler_env import OrcaGymEulerEnv
from orca_gym.sensor.rgbd_camera import CameraWrapper
class VisionEnv(OrcaGymEulerEnv):
"""Environment that includes camera images in observations."""
def __init__(self, frame_skip, orcagym_addr, agent_names, time_step,
camera_port: int = 8765, **kwargs):
super().__init__(
frame_skip=frame_skip,
orcagym_addr=orcagym_addr,
agent_names=agent_names,
time_step=time_step,
**kwargs,
)
# -- Set up camera --
self._camera = CameraWrapper(name="agent_view", port=camera_port)
self._camera.start()
# Wait for first frame
print("Waiting for camera to be ready...")
while not self._camera.is_first_frame_received():
time.sleep(0.1)
print(f"✅ Camera ready: {self._camera.image.shape}")
# Action space
self.action_space = spaces.Box(
low=-1.0, high=1.0, shape=(self.model.nu,), dtype=np.float32
)
obs_sample = self._get_obs()
self.observation_space = spaces.Dict({
key: spaces.Box(low=-np.inf, high=np.inf, shape=v.shape, dtype=v.dtype)
if isinstance(v, np.ndarray) and v.dtype != np.uint8
else spaces.Box(low=0, high=255, shape=v.shape, dtype=np.uint8)
for key, v in obs_sample.items()
})
def _get_obs(self):
"""
Observation = proprioception (joint angles) + vision (camera image)
Proprioception: the state the robot "feels" about itself
Vision: what the camera sees
"""
return {
# Proprioception
"joint_pos": self.data.qpos.copy().astype(np.float32),
"joint_vel": self.data.qvel.copy().astype(np.float32),
# Vision
"image": self._camera.image.copy(), # (H, W, 3) uint8
}
def step(self, action):
action = np.asarray(action, dtype=np.float32).reshape(self.model.nu)
self.do_simulation(action, self.frame_skip)
obs = self._get_obs()
reward = 0.0
terminated = False
truncated = False
return obs, reward, terminated, truncated, {}
def reset_model(self):
self.set_joint_qpos(self.init_qpos)
self.set_joint_qvel(self.init_qvel)
self.mj_forward()
self._sync_view()
return self._get_obs(), {}
def close(self):
self._camera.stop()
super().close()
Displaying the Camera Feed¶
Live display with Matplotlib:
import matplotlib.pyplot as plt
def show_camera_live(camera: CameraWrapper, duration: float = 10.0):
"""
Live display of camera feed (for duration seconds).
Note: This is only a simple display example.
Real RL training does not require frame-by-frame display — just use the image array directly.
"""
plt.ion() # interactive mode
fig, ax = plt.subplots()
img_display = ax.imshow(np.zeros((480, 640, 3), dtype=np.uint8))
ax.set_title("Camera Feed")
ax.axis('off')
start = time.time()
while time.time() - start < duration:
frame = camera.image.copy()
# OpenCV BGR -> Matplotlib RGB
frame_rgb = frame[..., ::-1]
img_display.set_data(frame_rgb)
fig.canvas.flush_events()
plt.pause(0.03) # ~30 FPS
plt.ioff()
plt.close()
Multi-Camera Setup¶
Need multiple viewpoints? Create multiple CameraWrapper instances:
def setup_multi_camera():
"""Start multiple cameras simultaneously."""
cameras = {
"front": CameraWrapper("front", port=8765),
"side": CameraWrapper("side", port=8766),
"top": CameraWrapper("top", port=8767),
}
# Start all
for cam in cameras.values():
cam.start()
# Wait for all cameras to be ready
for name, cam in cameras.items():
while not cam.is_first_frame_received():
time.sleep(0.1)
print(f"✅ {name}: {cam.image.shape}")
# Synchronized retrieval of all views
def get_all_views():
return {name: cam.image.copy() for name, cam in cameras.items()}
return cameras, get_all_views
# Usage
cameras, get_views = setup_multi_camera()
views = get_views()
print(f"Available views: {list(views.keys())}")
Camera Parameter Configuration¶
Each camera's parameters can be configured in OrcaStudio:
| Parameter | Description | Typical Values |
|---|---|---|
| Resolution | Image width x height | 640x480, 1280x720 |
| Frame rate | Frames per second | 15, 30, 60 |
| RGB | Whether to output color image | True |
| Depth | Whether to output depth image | True (when needed) |
Configure on the Python side via CameraSensorInfo:
from orca_gym.scene.orca_gym_scene import CameraSensorInfo
# Configure sensor parameters for a specific camera
camera_config = CameraSensorInfo(
capture_rgb=True, # output RGB image
capture_depth=True, # output depth map
save_mp4_file=False, # do not save to file
use_dds=False, # do not use DDS compression
)
scene.set_camera_sensor_info("camera_actor_name", camera_config)
Performance Tips¶
- Do not
imshowin the main loop — image display is very CPU-intensive. During training, just process the array directly. - Downscale images — if full resolution is not needed, resize in
_get_obs(). - Lower the frame rate — 30 FPS is usually sufficient; higher frame rates waste bandwidth.
- Asynchronous rendering — the camera decodes frames in a background thread without blocking the main simulation thread.
def _get_obs(self):
image = self._camera.image.copy()
# Downscale to 128x128 to reduce computation
small_image = cv2.resize(image, (128, 128))
return {..., "image": small_image}
Next Step¶
Now you can both "feel" (read state) and "see" (camera). Next, learn how to precisely control the robot: 🎮 Simple Controller.