feat: add OpenCV camera source wrapper
This commit is contained in:
parent
d5f5b47cef
commit
ab7664cd51
2 changed files with 85 additions and 0 deletions
34
bookmark/camera.py
Normal file
34
bookmark/camera.py
Normal 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
51
tests/test_camera.py
Normal 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()
|
||||
Loading…
Reference in a new issue