Timsty's picture
Add files using upload-large-folder tool
1f3a93e verified
Raw
History Blame Contribute Delete
9.15 kB
"""
ROS-free camera viewer with optional recording.
Connects to a ZMQ camera server (MuJoCo sim SensorServer or real robot camera)
and displays live camera feeds using OpenCV. Supports recording to MP4.
Virtual environment setup (run from repo root):
bash install_scripts/install_data_collection.sh
source .venv_data_collection/bin/activate
Usage:
python gear_sonic/scripts/run_camera_viewer.py --camera-host localhost --camera-port 5555
Headless recording:
python gear_sonic/scripts/run_camera_viewer.py --no-display --record --duration 10
Controls (OpenCV window must be focused):
R - Start/stop recording
Q - Quit
Output structure:
camera_recordings/
└── rec_20260403_143052/
├── ego_view.mp4
└── head_left_color_image.mp4
"""
from dataclasses import dataclass
from pathlib import Path
import time
from typing import Optional
import cv2
import numpy as np
import tyro
from gear_sonic.camera.composed_camera import ComposedCameraClientSensor
@dataclass
class CameraViewerConfig:
"""CLI config for the ROS-free camera viewer."""
camera_host: str = "localhost"
"""Camera server hostname."""
camera_port: int = 5555
"""Camera server port."""
fps: int = 30
"""Target display refresh rate (Hz)."""
output_path: Optional[str] = None
"""Output directory for recordings. Auto-creates 'camera_recordings/' if not set."""
codec: str = "mp4v"
"""Video codec for recording (e.g., 'mp4v', 'XVID')."""
max_display_width: int = 640
"""Max width per camera tile in the display window."""
display: bool = True
"""Show the OpenCV preview window. Disable this on headless servers."""
record: bool = False
"""Start recording immediately instead of waiting for the R key."""
duration: Optional[float] = None
"""Stop automatically after this many seconds of recording."""
def main(config: CameraViewerConfig):
if not config.display and not config.record:
raise ValueError("--no-display requires --record")
if config.duration is not None and not config.record:
raise ValueError("--duration requires --record")
if config.duration is not None and config.duration <= 0:
raise ValueError("--duration must be greater than zero")
client = ComposedCameraClientSensor(server_ip=config.camera_host, port=config.camera_port)
print("Waiting for first camera frame...")
sample = None
for _ in range(100):
sample = client.read(blocking=False)
if sample and sample.get("images"):
break
time.sleep(0.1)
if sample is None or not sample.get("images"):
print("ERROR: No camera frames received after 10s. Check the camera server.")
return
camera_names = sorted(sample["images"].keys())
print(f"Detected {len(camera_names)} camera stream(s): {camera_names}")
output_dir = Path(config.output_path) if config.output_path else Path("camera_recordings")
is_recording = False
video_writers: dict[str, cv2.VideoWriter] = {}
frame_count = 0
recording_start_time = 0.0
recording_dir = Path(".")
loop_period = 1.0 / config.fps
window_name = "SONIC Camera Viewer"
print(f"Target FPS: {config.fps}")
print(f"Recordings will be saved to: {output_dir}")
if config.display:
print("Controls: R = start/stop recording, Q = quit")
if config.record:
recording_dir = output_dir / f"rec_{time.strftime('%Y%m%d_%H%M%S')}"
recording_dir.mkdir(parents=True, exist_ok=True)
fourcc = cv2.VideoWriter_fourcc(*config.codec)
for name in camera_names:
img = sample["images"].get(name)
if img is not None:
h, w = img.shape[:2]
path = recording_dir / f"{name}.mp4"
writer = cv2.VideoWriter(str(path), fourcc, config.fps, (w, h))
if not writer.isOpened():
raise RuntimeError(f"Failed to open video writer: {path}")
video_writers[name] = writer
is_recording = True
recording_start_time = time.time()
print(f"Recording started: {recording_dir}")
try:
while True:
t_start = time.monotonic()
image_data = client.read(blocking=False)
if image_data is None or not image_data.get("images"):
elapsed = time.monotonic() - t_start
remaining = loop_period - elapsed
if remaining > 0:
time.sleep(remaining)
continue
tiles = []
for name in camera_names:
img = image_data["images"].get(name)
if img is None:
continue
if img.shape[2] == 3:
img_bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
else:
img_bgr = img
if is_recording and name in video_writers:
video_writers[name].write(img_bgr)
h, w = img_bgr.shape[:2]
if w > config.max_display_width:
scale = config.max_display_width / w
img_bgr = cv2.resize(
img_bgr, (config.max_display_width, int(h * scale))
)
label = f"{name}"
if is_recording:
label = f"[REC] {name}"
cv2.putText(
img_bgr, label, (10, 25),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2,
)
tiles.append(img_bgr)
if tiles:
max_h = max(t.shape[0] for t in tiles)
padded = []
for t in tiles:
if t.shape[0] < max_h:
pad = np.zeros(
(max_h - t.shape[0], t.shape[1], 3), dtype=np.uint8
)
t = np.vstack([t, pad])
padded.append(t)
canvas = np.hstack(padded)
if is_recording:
frame_count += 1
elapsed_rec = time.time() - recording_start_time
status = f"REC {frame_count}f / {elapsed_rec:.1f}s"
cv2.putText(
canvas, status, (canvas.shape[1] - 300, 25),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2,
)
if config.display:
cv2.imshow(window_name, canvas)
key = cv2.waitKey(1) & 0xFF if config.display else 0xFF
if key == ord("q"):
print("Quit requested.")
break
elif key == ord("r"):
if not is_recording:
recording_dir = output_dir / f"rec_{time.strftime('%Y%m%d_%H%M%S')}"
recording_dir.mkdir(parents=True, exist_ok=True)
fourcc = cv2.VideoWriter_fourcc(*config.codec)
video_writers = {}
for name in camera_names:
img = image_data["images"].get(name)
if img is not None:
h, w = img.shape[:2]
path = recording_dir / f"{name}.mp4"
video_writers[name] = cv2.VideoWriter(
str(path), fourcc, config.fps, (w, h)
)
is_recording = True
recording_start_time = time.time()
frame_count = 0
print(f"Recording started: {recording_dir}")
else:
is_recording = False
for writer in video_writers.values():
writer.release()
video_writers = {}
duration = time.time() - recording_start_time
print(
f"Recording stopped - {duration:.1f}s, {frame_count} frames "
f"-> {recording_dir}"
)
if (
is_recording
and config.duration is not None
and time.time() - recording_start_time >= config.duration
):
print(f"Recording duration reached: {config.duration:.1f}s")
break
elapsed = time.monotonic() - t_start
remaining = loop_period - elapsed
if remaining > 0:
time.sleep(remaining)
except KeyboardInterrupt:
print("\nExiting...")
finally:
if video_writers:
for writer in video_writers.values():
writer.release()
if is_recording:
duration = time.time() - recording_start_time
print(f"Final recording: {duration:.1f}s, {frame_count} frames")
client.close()
if config.display:
cv2.destroyAllWindows()
if __name__ == "__main__":
config = tyro.cli(CameraViewerConfig)
main(config)