feat: add OpenCV camera source wrapper

This commit is contained in:
pyr0ball 2026-07-13 10:41:10 -07:00
parent d5f5b47cef
commit ab7664cd51
2 changed files with 85 additions and 0 deletions

34
bookmark/camera.py Normal file
View file

@ -0,0 +1,34 @@
from typing import Protocol
import cv2
import numpy as np
class FrameSource(Protocol):
def read_frame(self) -> np.ndarray: ...
class CameraSource:
"""Thin wrapper around cv2.VideoCapture for a single physical camera."""
def __init__(self, device_index: int = 0) -> None:
self.device_index = device_index
self._capture: cv2.VideoCapture | None = None
def open(self) -> None:
self._capture = cv2.VideoCapture(self.device_index)
if not self._capture.isOpened():
raise RuntimeError(f"Could not open camera at index {self.device_index}")
def read_frame(self) -> np.ndarray:
if self._capture is None:
raise RuntimeError("Camera not opened - call open() first")
ok, frame = self._capture.read()
if not ok:
raise RuntimeError("Failed to read frame from camera")
return frame
def close(self) -> None:
if self._capture is not None:
self._capture.release()
self._capture = None

51
tests/test_camera.py Normal file
View file

@ -0,0 +1,51 @@
from unittest.mock import MagicMock, patch
import numpy as np
import pytest
from bookmark.camera import CameraSource
def test_open_raises_when_capture_not_opened():
with patch("bookmark.camera.cv2.VideoCapture") as mock_video_capture:
mock_instance = MagicMock()
mock_instance.isOpened.return_value = False
mock_video_capture.return_value = mock_instance
source = CameraSource(device_index=0)
with pytest.raises(RuntimeError, match="Could not open camera"):
source.open()
def test_read_frame_returns_frame_from_capture():
with patch("bookmark.camera.cv2.VideoCapture") as mock_video_capture:
mock_instance = MagicMock()
mock_instance.isOpened.return_value = True
fake_frame = np.zeros((10, 10, 3), dtype=np.uint8)
mock_instance.read.return_value = (True, fake_frame)
mock_video_capture.return_value = mock_instance
source = CameraSource(device_index=0)
source.open()
frame = source.read_frame()
assert frame.shape == (10, 10, 3)
def test_read_frame_raises_when_capture_fails():
with patch("bookmark.camera.cv2.VideoCapture") as mock_video_capture:
mock_instance = MagicMock()
mock_instance.isOpened.return_value = True
mock_instance.read.return_value = (False, None)
mock_video_capture.return_value = mock_instance
source = CameraSource(device_index=0)
source.open()
with pytest.raises(RuntimeError, match="Failed to read frame"):
source.read_frame()
def test_read_frame_raises_if_not_opened():
source = CameraSource(device_index=0)
with pytest.raises(RuntimeError, match="not opened"):
source.read_frame()