You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

324 lines
12 KiB

import time
from typing import Any, Dict, Optional, Tuple
import cv2
import depthai as dai
import gymnasium as gym
import numpy as np
from decoupled_wbc.control.base.sensor import Sensor
from decoupled_wbc.control.sensor.sensor_server import (
CameraMountPosition,
ImageMessageSchema,
SensorServer,
)
class OAKConfig:
"""Configuration for the OAK camera."""
color_image_dim: Tuple[int, int] = (640, 480) # RGB camera resolution
monochrome_image_dim: Tuple[int, int] = (640, 480) # Monochrome camera resolution
fps: int = 30
enable_color: bool = True # Enable CAM_A (RGB)
enable_mono_cameras: bool = False # Enable CAM_B & CAM_C (Monochrome stereo pair)
mount_position: str = CameraMountPosition.EGO_VIEW.value
class OAKSensor(Sensor, SensorServer):
"""Sensor for the OAK camera family."""
def __init__(
self,
run_as_server: bool = False,
port: int = 5555,
config: OAKConfig = OAKConfig(),
device_id: Optional[str] = None,
mount_position: str = CameraMountPosition.EGO_VIEW.value,
):
"""Initialize the OAK camera."""
self.config = config
self.mount_position = mount_position
self._run_as_server = run_as_server
device_infos = dai.Device.getAllAvailableDevices()
assert len(device_infos) > 0, f"No OAK devices found for {mount_position}"
print(f"Device infos: {device_infos}")
if device_id is not None:
device_found = False
for device_info in device_infos:
if device_info.getDeviceId() == device_id:
self.device = dai.Device(device_info)
device_found = True
break
if not device_found:
raise ValueError(f"Device with ID {device_id} not found")
else:
self.device = dai.Device()
print(f"Connected to OAK device: {self.device.getDeviceName(), self.device.getDeviceId()}")
print(f"Device ID: {self.device.getDeviceId()}")
sockets: list[dai.CameraBoardSocket] = self.device.getConnectedCameras()
print(f"Available cameras: {[str(s) for s in sockets]}")
# Create pipeline (without context manager to persist across method calls)
self.pipeline = dai.Pipeline(self.device)
self.output_queues = {}
# Configure RGB camera (CAM_A)
if config.enable_color and dai.CameraBoardSocket.CAM_A in sockets:
self.cam_rgb = self.pipeline.create(dai.node.Camera)
cam_socket = dai.CameraBoardSocket.CAM_A
self.cam_rgb = self.cam_rgb.build(cam_socket)
# Create RGB output queue
self.output_queues["color"] = self.cam_rgb.requestOutput(
config.color_image_dim,
fps=config.fps,
).createOutputQueue()
print("Enabled CAM_A (RGB)")
# Configure Monochrome cameras (CAM_B & CAM_C)
if config.enable_mono_cameras:
if dai.CameraBoardSocket.CAM_B in sockets:
self.cam_mono_left = self.pipeline.create(dai.node.Camera)
cam_socket = dai.CameraBoardSocket.CAM_B
self.cam_mono_left = self.cam_mono_left.build(cam_socket)
# Create mono left output queue
self.output_queues["mono_left"] = self.cam_mono_left.requestOutput(
config.monochrome_image_dim,
fps=config.fps,
).createOutputQueue()
print("Enabled CAM_B (Monochrome Left)")
if dai.CameraBoardSocket.CAM_C in sockets:
self.cam_mono_right = self.pipeline.create(dai.node.Camera)
cam_socket = dai.CameraBoardSocket.CAM_C
self.cam_mono_right = self.cam_mono_right.build(cam_socket)
# Create mono right output queue
self.output_queues["mono_right"] = self.cam_mono_right.requestOutput(
config.monochrome_image_dim,
fps=config.fps,
).createOutputQueue()
print("Enabled CAM_C (Monochrome Right)")
assert len(self.output_queues) > 0, "No output queues enabled"
# auto exposure compensation, for CoRL demo
# cam_q_in = self.cam_rgb.inputControl.createInputQueue()
# ctrl = dai.CameraControl()
# ctrl.setAutoExposureEnable()
# ctrl.setAutoExposureCompensation(-2)
# cam_q_in.send(ctrl)
# Start pipeline on device
self.pipeline.start()
if run_as_server:
self.start_server(port)
def read(self) -> Optional[Dict[str, Any]]:
"""Read images from the camera."""
if not self.pipeline.isRunning():
print(f"[ERROR] OAK pipeline stopped for {self.mount_position}")
return None
# Check if device is still connected
if not self.device.isPipelineRunning():
print(f"[ERROR] OAK device disconnected for {self.mount_position}")
return None
timestamps = {}
images = {}
rgb_frame_time = None
# Get color frame if enabled
if "color" in self.output_queues:
try:
rgb_frame = self.output_queues["color"].get()
rgb_frame_time = rgb_frame.getTimestamp()
if rgb_frame is not None:
images[self.mount_position] = rgb_frame.getCvFrame()[..., ::-1] # BGR to RGB
timestamps[self.mount_position] = (
rgb_frame_time - dai.Clock.now()
).total_seconds() + time.time()
except Exception as e:
print(f"[ERROR] Failed to read color frame from {self.mount_position}: {e}")
return None
# Get mono frames if enabled
if "mono_left" in self.output_queues:
try:
mono_left_frame = self.output_queues["mono_left"].get()
mono_left_frame_time = mono_left_frame.getTimestamp()
if mono_left_frame is not None:
key = f"{self.mount_position}_left_mono"
images[key] = mono_left_frame.getCvFrame()
timestamps[key] = (
mono_left_frame_time - dai.Clock.now()
).total_seconds() + time.time()
except Exception as e:
print(f"[ERROR] Failed to read mono_left frame from {self.mount_position}: {e}")
return None
if "mono_right" in self.output_queues:
try:
mono_right_frame = self.output_queues["mono_right"].get()
mono_right_frame_time = mono_right_frame.getTimestamp()
if mono_right_frame is not None:
key = f"{self.mount_position}_right_mono"
images[key] = mono_right_frame.getCvFrame()
timestamps[key] = (
mono_right_frame_time - dai.Clock.now()
).total_seconds() + time.time()
except Exception as e:
print(f"[ERROR] Failed to read mono_right frame from {self.mount_position}: {e}")
return None
if (
rgb_frame_time is not None
and (rgb_frame_time - dai.Clock.now()).total_seconds() <= -0.2
):
print(
f"[{self.mount_position}] OAK latency too large: "
f"{(dai.Clock.now() - rgb_frame_time).total_seconds() * 1000}ms"
)
return {
"timestamps": timestamps,
"images": images,
}
def serialize(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Serialize data using ImageMessageSchema."""
serialized_msg = ImageMessageSchema(timestamps=data["timestamps"], images=data["images"])
return serialized_msg.serialize()
def observation_space(self) -> gym.Space:
spaces = {}
if self.config.enable_color:
spaces["color_image"] = gym.spaces.Box(
low=0,
high=255,
shape=(self.config.color_image_dim[1], self.config.color_image_dim[0], 3),
dtype=np.uint8,
)
if self.config.enable_mono_cameras:
spaces["mono_left_image"] = gym.spaces.Box(
low=0,
high=255,
shape=(self.config.monochrome_image_dim[1], self.config.monochrome_image_dim[0]),
dtype=np.uint8,
)
spaces["mono_right_image"] = gym.spaces.Box(
low=0,
high=255,
shape=(self.config.monochrome_image_dim[1], self.config.monochrome_image_dim[0]),
dtype=np.uint8,
)
return gym.spaces.Dict(spaces)
def close(self):
"""Close the camera connection."""
if self._run_as_server:
self.stop_server()
if hasattr(self, "pipeline") and self.pipeline.isRunning():
self.pipeline.stop()
self.device.close()
def run_server(self):
"""Run the server."""
if not self._run_as_server:
raise ValueError("This function is only available when run_as_server is True")
while True:
frame = self.read()
if frame is None:
continue
msg = self.serialize(frame)
self.send_message({self.mount_position: msg})
def __del__(self):
self.close()
if __name__ == "__main__":
"""Test function for OAK camera."""
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--server", action="store_true", help="Run as server")
parser.add_argument("--client", action="store_true", help="Run as client")
parser.add_argument("--host", type=str, default="localhost", help="Server IP address")
parser.add_argument("--port", type=int, default=5555, help="Port number")
parser.add_argument("--device-id", type=str, default=None, help="Specific device ID")
parser.add_argument(
"--enable-mono", action="store_true", help="Enable monochrome cameras (CAM_B & CAM_C)"
)
parser.add_argument("--mount-position", type=str, default="ego_view", help="Mount position")
parser.add_argument("--show-image", action="store_true", help="Display images")
args = parser.parse_args()
oak_config = OAKConfig()
if args.enable_mono:
oak_config.enable_mono_cameras = True
if args.server:
# Run as server
oak = OAKSensor(
run_as_server=True,
port=args.port,
config=oak_config,
device_id=args.device_id,
mount_position=args.mount_position,
)
print(f"Starting OAK server on port {args.port}")
oak.run_server()
else:
# Run standalone
oak = OAKSensor(run_as_server=False, config=oak_config, device_id=args.device_id)
print("Running OAK camera in standalone mode")
while True:
frame = oak.read()
if frame is None:
print("Waiting for frame...")
time.sleep(0.5)
continue
if "color_image" in frame:
print(f"Color image shape: {frame['color_image'].shape}")
if "mono_left_image" in frame:
print(f"Mono left image shape: {frame['mono_left_image'].shape}")
if "mono_right_image" in frame:
print(f"Mono right image shape: {frame['mono_right_image'].shape}")
if "depth_image" in frame:
print(f"Depth image shape: {frame['depth_image'].shape}")
if args.show_image:
if "color_image" in frame:
cv2.imshow("Color Image", frame["color_image"])
if "mono_left_image" in frame:
cv2.imshow("Mono Left", frame["mono_left_image"])
if "mono_right_image" in frame:
cv2.imshow("Mono Right", frame["mono_right_image"])
if "depth_image" in frame:
depth_colormap = cv2.applyColorMap(
cv2.convertScaleAbs(frame["depth_image"], alpha=0.03), cv2.COLORMAP_JET
)
cv2.imshow("Depth Image", depth_colormap)
if cv2.waitKey(1) == ord("q"):
break
time.sleep(0.01)
cv2.destroyAllWindows()
oak.close()