From 524cc6281281312790e032c7b30892421e9299e1 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Sun, 26 Apr 2026 20:16:18 -0700 Subject: [PATCH] feat(input/gestures): add CameraCapture and public __init__ exports --- circuitforge_core/input/gestures/__init__.py | 15 ++++++ circuitforge_core/input/gestures/camera.py | 57 ++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 circuitforge_core/input/gestures/camera.py diff --git a/circuitforge_core/input/gestures/__init__.py b/circuitforge_core/input/gestures/__init__.py index e69de29..f6ed014 100644 --- a/circuitforge_core/input/gestures/__init__.py +++ b/circuitforge_core/input/gestures/__init__.py @@ -0,0 +1,15 @@ +""" +cf_input.gestures — camera capture, hand detection, landmark normalization. + +Public API: + CameraCapture — OpenCV frame source + HandsDetector — MediaPipe Hands wrapper + HandLandmarks — immutable detected hand dataclass + normalize_hand() — scale/translation-invariant feature vector +""" + +from circuitforge_core.input.gestures.camera import CameraCapture +from circuitforge_core.input.gestures.hands import HandLandmarks, HandsDetector +from circuitforge_core.input.gestures.normalizer import normalize_hand + +__all__ = ["CameraCapture", "HandLandmarks", "HandsDetector", "normalize_hand"] diff --git a/circuitforge_core/input/gestures/camera.py b/circuitforge_core/input/gestures/camera.py new file mode 100644 index 0000000..5f9a58f --- /dev/null +++ b/circuitforge_core/input/gestures/camera.py @@ -0,0 +1,57 @@ +""" +OpenCV camera capture — context manager wrapping VideoCapture. + +Yields BGR frames. Callers convert to RGB before passing to HandsDetector: + frame_rgb = frame_bgr[:, :, ::-1] +""" + +from __future__ import annotations + +from typing import Iterator + +import cv2 +import numpy as np + + +class CameraCapture: + """ + Thin wrapper around cv2.VideoCapture. + + Usage: + with CameraCapture(device_index=0) as cam: + for frame_bgr in cam.frames(): + process(frame_bgr) + """ + + def __init__( + self, + device_index: int = 0, + width: int = 640, + height: int = 480, + fps: int = 30, + ) -> None: + self._cap = cv2.VideoCapture(device_index) + self._cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) + self._cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) + self._cap.set(cv2.CAP_PROP_FPS, fps) + + @property + def is_open(self) -> bool: + return self._cap.isOpened() + + def frames(self) -> Iterator[np.ndarray]: + """Yield BGR uint8 frames until camera fails or caller breaks.""" + while self._cap.isOpened(): + ok, frame = self._cap.read() + if not ok: + break + yield frame + + def release(self) -> None: + self._cap.release() + + def __enter__(self) -> CameraCapture: + return self + + def __exit__(self, *_: object) -> None: + self.release()