119 lines
3.5 KiB
Python
119 lines
3.5 KiB
Python
"""Gesture recognizer interfaces.
|
|
|
|
The production recognizer is intentionally thin in this skeleton. It validates
|
|
that optional CV dependencies are available and keeps the protocol independent
|
|
from any specific model implementation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any, Protocol
|
|
|
|
from .cameras import MotionAgentDependencyError
|
|
from .events import GestureName, SkeletonEvent, SkeletonJoint, now_ms
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GestureObservation:
|
|
gesture: GestureName
|
|
confidence: float
|
|
intensity: float = 1.0
|
|
timestamp_ms: int | None = None
|
|
|
|
|
|
class GestureRecognizer(Protocol):
|
|
name: str
|
|
|
|
def recognize(self, frame: Any) -> GestureObservation | None:
|
|
"""Return a gesture observation for the current frame."""
|
|
|
|
def debug_skeleton(
|
|
self,
|
|
frame: Any,
|
|
*,
|
|
camera_id: str,
|
|
mode: str,
|
|
matched_gesture: GestureName | None = None,
|
|
confidence: float = 0.0,
|
|
) -> SkeletonEvent | None:
|
|
"""Return normalized skeleton debug data when available."""
|
|
|
|
|
|
class MediaPipeGestureRecognizer:
|
|
name = "mediapipe-opencv"
|
|
|
|
def __init__(self) -> None:
|
|
try:
|
|
import cv2 # noqa: F401
|
|
import mediapipe # noqa: F401
|
|
except ImportError as exc:
|
|
raise MotionAgentDependencyError(
|
|
"MediaPipe and OpenCV are required for live gesture recognition. "
|
|
"Add mediapipe and opencv-python with uv, or use --dry-run for protocol testing."
|
|
) from exc
|
|
|
|
def recognize(self, frame: Any) -> GestureObservation | None:
|
|
_ = frame
|
|
return None
|
|
|
|
def debug_skeleton(
|
|
self,
|
|
frame: Any,
|
|
*,
|
|
camera_id: str,
|
|
mode: str,
|
|
matched_gesture: GestureName | None = None,
|
|
confidence: float = 0.0,
|
|
) -> SkeletonEvent | None:
|
|
_ = frame, camera_id, mode, matched_gesture, confidence
|
|
return None
|
|
|
|
|
|
class NullGestureRecognizer:
|
|
name = "dry-run"
|
|
|
|
def recognize(self, frame: Any) -> GestureObservation | None:
|
|
_ = frame
|
|
return None
|
|
|
|
def debug_skeleton(
|
|
self,
|
|
frame: Any,
|
|
*,
|
|
camera_id: str,
|
|
mode: str,
|
|
matched_gesture: GestureName | None = None,
|
|
confidence: float = 0.0,
|
|
) -> SkeletonEvent | None:
|
|
_ = frame
|
|
now = now_ms()
|
|
sway = ((now // 250) % 6 - 2.5) * 0.015
|
|
joints = [
|
|
SkeletonJoint("head", 0.5, 0.18, 1.0),
|
|
SkeletonJoint("neck", 0.5, 0.3, 1.0),
|
|
SkeletonJoint("left_shoulder", 0.38, 0.34, 1.0),
|
|
SkeletonJoint("right_shoulder", 0.62, 0.34, 1.0),
|
|
SkeletonJoint("left_elbow", 0.31 + sway, 0.48, 0.95),
|
|
SkeletonJoint("right_elbow", 0.69 - sway, 0.48, 0.95),
|
|
SkeletonJoint("left_wrist", 0.24 + sway, 0.62, 0.9),
|
|
SkeletonJoint("right_wrist", 0.76 - sway, 0.62, 0.9),
|
|
]
|
|
bones = [
|
|
("head", "neck"),
|
|
("neck", "left_shoulder"),
|
|
("neck", "right_shoulder"),
|
|
("left_shoulder", "left_elbow"),
|
|
("left_elbow", "left_wrist"),
|
|
("right_shoulder", "right_elbow"),
|
|
("right_elbow", "right_wrist"),
|
|
]
|
|
return SkeletonEvent(
|
|
joints=joints,
|
|
bones=bones,
|
|
matched_gesture=matched_gesture,
|
|
confidence=confidence,
|
|
camera_id=camera_id,
|
|
mode=mode,
|
|
)
|