Download GR00T-WholeBodyControl/gear_sonic/scripts/run_camera_viewer.py from Timsty/groot_deployment: direct link, hf CLI and curl.
- Browser
- Download file 9.15 kB
-
https://huggingface.co/Timsty/groot_deployment/resolve/main/GR00T-WholeBodyControl/gear_sonic/scripts/run_camera_viewer.py
- Command line
-
hf download hf://Timsty/groot_deployment/GR00T-WholeBodyControl/gear_sonic/scripts/run_camera_viewer.py
-
curl -L -o run_camera_viewer.py https://huggingface.co/Timsty/groot_deployment/resolve/main/GR00T-WholeBodyControl/gear_sonic/scripts/run_camera_viewer.py
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 | |
| 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) | |