File size: 2,752 Bytes
9f85448 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | from __future__ import annotations
import sys
import threading
import time
import cv2
import numpy as np
class CameraStream:
"""Threaded camera frame grabber."""
def __init__(self, index: int = 0, width: int = 1280, height: int = 720,
fps: int = 60) -> None:
backend = cv2.CAP_AVFOUNDATION if sys.platform == "darwin" else cv2.CAP_ANY
cap = cv2.VideoCapture(index, backend)
if not cap.isOpened():
cap = cv2.VideoCapture(index)
if not cap.isOpened():
raise RuntimeError(
f"Could not open camera {index}. "
"On macOS grant camera access in System Settings, Privacy and "
"Security, Camera, then restart."
)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
cap.set(cv2.CAP_PROP_FPS, fps)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
self.cap = cap
ok, frame = cap.read()
if not ok or frame is None:
cap.release()
raise RuntimeError("Camera opened but returns no frames.")
self._frame: np.ndarray = frame
self._seq = 0
self._lock = threading.Lock()
self._stop = threading.Event()
self.failed = False
self._thread = threading.Thread(target=self._loop, daemon=True)
self._thread.start()
@property
def shape(self) -> tuple[int, int]:
"""Frame height and width."""
return self._frame.shape[0], self._frame.shape[1]
@property
def source_fps(self) -> float:
"""Camera reported FPS."""
v = self.cap.get(cv2.CAP_PROP_FPS)
return float(v) if v and v > 0 else 0.0
def _loop(self) -> None:
"""Background capture loop."""
while not self._stop.is_set():
ok, frame = self.cap.read()
if not ok or frame is None:
self.failed = True
break
with self._lock:
self._frame = frame
self._seq += 1
def read(self) -> tuple[np.ndarray, int]:
"""Latest frame and sequence."""
with self._lock:
return self._frame, self._seq
def wait_next(self, last_seq: int, timeout: float = 1.0) -> tuple[np.ndarray, int]:
"""Wait for fresh frame."""
deadline = time.perf_counter() + timeout
while True:
frame, seq = self.read()
if seq != last_seq or self.failed or time.perf_counter() > deadline:
return frame, seq
time.sleep(0.001)
def release(self) -> None:
"""Stop thread and release."""
self._stop.set()
self._thread.join(timeout=1.0)
self.cap.release()
|