import numpy as np import json import cv2 import threading from dataclasses import dataclass, field, asdict from typing import List, Optional, Tuple, Dict from pygrabber.dshow_graph import FilterGraph class DShowMJPGCapture: """ cv2.VideoCapture-like wrapper (read/release/isOpened) that selects an exact DirectShow media type (e.g. MJPG at a given resolution) before capturing. This exists because cv2.VideoCapture(..., cv2.CAP_DSHOW).set(CAP_PROP_FOURCC, ...) is unreliable on some webcams: OpenCV opens its own separate DirectShow filter graph, which renegotiates its own format independently of anything set through pygrabber beforehand. Doing format selection AND frame grabbing in the same graph (via pygrabber's sample grabber) avoids that. """ def __init__(self, index: int, width: int, height: int, fourcc_substr: str = "MJPG", timeout: float = 2.0): self.index = index self.width = width self.height = height self.selected_format = None self._opened = False self._latest_frame = None self._frame_event = threading.Event() self._timeout = timeout self._graph = FilterGraph() self._graph.add_video_input_device(index) device = self._graph.get_input_device() formats = device.get_formats() match = next( (f for f in formats if f["width"] == width and f["height"] == height and fourcc_substr.upper() in f["media_type_str"].upper()), None, ) if match is None: raise RuntimeError( f"No {fourcc_substr} format at {width}x{height} available on camera {index}. " f"Available formats: {formats}" ) device.set_format(match["index"]) self.selected_format = match self._graph.add_sample_grabber(self._on_frame) self._graph.add_null_render() self._graph.prepare_preview_graph() self._graph.run() self._opened = True def _on_frame(self, frame): self._latest_frame = frame self._frame_event.set() def isOpened(self) -> bool: return self._opened def read(self): if not self._opened: return False, None self._frame_event.clear() self._graph.grab_frame() if not self._frame_event.wait(self._timeout): return False, None return True, self._latest_frame def release(self): if self._opened: self._graph.stop() self._graph.remove_filters() self._opened = False # No-ops for compatibility with code paths that still call .set()/.get() # on the capture object (real config happens via device.set_format above). def set(self, prop_id, value): return False def get(self, prop_id): return 0.0 # ----- Helper Functions ----- def compute_distance(p1, p2): return np.hypot(p2[0] - p1[0], p2[1] - p1[1]) # 4) compute straightness per finger def finger_straight(i_tip, i_pip, i_mcp): d1 = np.hypot(*(np.array(i_tip)-np.array(i_pip))) d2 = np.hypot(*(np.array(i_pip)-np.array(i_mcp))) return float(np.clip(d1/(d2+1e-3), 0, 1)) def default_profile(): return [] @dataclass class CameraObject(): capture: Optional[cv2.VideoCapture] index: int pxHeight: int = 1536 pxWidth: int = 2048 # Calibration camera_matrix: Optional[np.ndarray] = None # 3x3 distortion_coefficients: Optional[np.ndarray] = None # 1x5 / 1x8 rotation_matrix_world_to_camera: Optional[np.ndarray] = None # 3x3 (world->cam) translation_vector_world_to_camera: Optional[np.ndarray] = None # 3x1 (world->cam) camera_projection_matrix: Optional[np.ndarray] = None def load_video_capture(self): #self.capture = cv2.VideoCapture(self.index, cv2.CAP_MSMF) self.capture = cv2.VideoCapture(self.index, cv2.CAP_ANY) #self.capture = cv2.VideoCapture(self.index, cv2.CAP_DSHOW) self.capture.set(cv2.CAP_PROP_FRAME_WIDTH, 2048.0) self.capture.set(cv2.CAP_PROP_FRAME_HEIGHT, 1536.0) @dataclass class Hand2D(): camera_id: str # e.g. "overhead_left" handedness: str # "Left" / "Right" score: float # detection/tracking confidence landmarks_px: np.ndarray # shape (21,2) in pixels @dataclass class CharucoDetection: camera_id: int # Output of Charuco detection charuco_corners: np.ndarray # (N, 1, 2) float32 charuco_ids: np.ndarray # (N, 1) int32 gray_frame: Optional[np.ndarray] = None @dataclass class CalibrationSample: detections: dict[int, CharucoDetection] # camera_id → detection