release: bump version to 0.50.0
This commit is contained in:
118
motion_agent/recognizer.py
Normal file
118
motion_agent/recognizer.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""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,
|
||||
)
|
||||
Reference in New Issue
Block a user