Source code for robotblockset.cameras.opencv_videocapture

"""OpenCV VideoCapture camera backend.

This module defines an OpenCV ``VideoCapture`` camera wrapper implementing the RGB camera interface.
It provides a lightweight backend for webcams and video files, with configurable resolution and frame rate,
and conversion utilities for consistent image formats used across robotblockset. The module is primarily
intended for development, testing, and interface validation without specialized camera hardware.

Key functionalities include:
- OpenCV ``VideoCapture`` integration through the ``RGBCamera`` interface.
- Support for webcam devices and video-file input streams.
- Configurable capture resolution and target frame-rate settings.
- RGB image retrieval in float and uint8 formats via shared converters.
- Optional intrinsics matrix support for calibration-aware workflows.
- Context-managed resource handling for clean camera release.

Copyright (c) 2026 Jozef Stefan Institute

Authors: Leon Zlajpah.
"""

from __future__ import annotations

import math
import os
from typing import Any, Optional, Tuple

import cv2
from robotblockset.cameras.interfaces import RGBCamera
from robotblockset.cameras.image_converter import ImageConverter
from robotblockset.rbs_typing import CameraIntrinsicsMatrixType, CameraResolutionType, NumpyFloatImageType, NumpyIntImageType


[docs] class OpenCVVideoCapture(RGBCamera): """Wrapper around OpenCV's VideoCapture so we can test the camera interface without external cameras.""" # Some standard resolutions that are likely to be supported by webcams. # 16:9 RESOLUTION_1080 = (1920, 1080) RESOLUTION_720 = (1280, 720) # 4:3 RESOLUTION_768 = (1024, 768) RESOLUTION_480 = (640, 480) def __init__( self, video_capture_args: Tuple[Any, ...] = (0,), intrinsics_matrix: Optional[CameraIntrinsicsMatrixType] = None, resolution: CameraResolutionType = RESOLUTION_480, fps: int = 30, ) -> None: if not video_capture_args: raise ValueError("video_capture_args must contain a camera index or stream source") source = video_capture_args[0] self.video_capture = cv2.VideoCapture(*video_capture_args) try: if not self.video_capture.isOpened(): # Preserve a specific error for local file paths while allowing # URL streams, image sequences, and backend-specific pipelines. if isinstance(source, str) and self._looks_like_local_file(source) and not os.path.isfile(source): raise FileNotFoundError(f"Could not find video file {source}") raise RuntimeError(f"Cannot open video capture source {source}") self._is_local_file = isinstance(source, str) and os.path.isfile(source) # Note that the following will not forcibly set the resolution. If the user's webcam # does not support the desired resolution, OpenCV will silently select a close match. self.video_capture.set(cv2.CAP_PROP_FRAME_WIDTH, resolution[0]) self.video_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, resolution[1]) self.video_capture.set(cv2.CAP_PROP_FPS, fps) self._intrinsics_matrix = intrinsics_matrix self._fps = float(self.video_capture.get(cv2.CAP_PROP_FPS)) self._resolution = ( math.floor(self.video_capture.get(cv2.CAP_PROP_FRAME_WIDTH)), math.floor(self.video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT)), ) except Exception: self.video_capture.release() raise @staticmethod def _looks_like_local_file(source: str) -> bool: """Return whether a string source appears to identify a local file.""" if "://" in source or "!" in source: return False directory, filename = os.path.split(source) return bool(directory or os.path.splitext(filename)[1]) @property def fps(self) -> float: """The frames per second of the camera.""" return self._fps @property def resolution(self) -> CameraResolutionType: return self._resolution def __enter__(self) -> RGBCamera: return self def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: self.video_capture.release()
[docs] def intrinsics_matrix(self) -> CameraIntrinsicsMatrixType: """Obtain the intrinsics matrix of the camera. Raises ------ RuntimeError: You must explicitly pass an intrinsics object to the constructor. Returns ------- CameraIntrinsicsMatrixType: The intrinsics matrix. """ if self._intrinsics_matrix is None: raise RuntimeError("OpenCVVideoCapture does not have a preset intrinsics matrix. Pass it to the constructor if you know it.") return self._intrinsics_matrix
def _grab_images(self) -> None: ret, image = self.video_capture.read() if not ret or image is None or image.size == 0: if self._is_local_file: raise EOFError("Cannot receive frame: reached the end of the video file") raise RuntimeError("Cannot receive frame from camera or stream") self._frame = image def _retrieve_rgb_image(self) -> NumpyFloatImageType: return ImageConverter.from_opencv_format(self._frame).image_in_numpy_format def _retrieve_rgb_image_as_int(self) -> NumpyIntImageType: return ImageConverter.from_opencv_format(self._frame).image_in_numpy_int_format
if __name__ == "__main__": import robotblockset.cameras.manual_test_hw as test import numpy as np camera = OpenCVVideoCapture(video_capture_args=(2,), intrinsics_matrix=np.eye(3)) # Perform tests test.manual_test_camera(camera) test.manual_test_rgb_camera(camera) test.profile_rgb_throughput(camera) # Live viewer cv2.namedWindow("OpenCV Webcam RGB", cv2.WINDOW_NORMAL) while True: color_image = camera.get_rgb_image_as_int() color_image = ImageConverter.from_numpy_int_format(color_image).image_in_opencv_format cv2.imshow("OpenCV Webcam RGB", color_image) key = cv2.waitKey(1) if key == ord("q"): break