mctrl 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mctrl-0.1.0.dist-info/METADATA +692 -0
- mctrl-0.1.0.dist-info/RECORD +33 -0
- mctrl-0.1.0.dist-info/WHEEL +4 -0
- mctrl-0.1.0.dist-info/entry_points.txt +3 -0
- mindcontrol/__init__.py +10 -0
- mindcontrol/__main__.py +4 -0
- mindcontrol/app.py +432 -0
- mindcontrol/autotune.py +493 -0
- mindcontrol/calibrate.py +199 -0
- mindcontrol/capture.py +159 -0
- mindcontrol/config.py +243 -0
- mindcontrol/control/__init__.py +1 -0
- mindcontrol/control/bridge.py +360 -0
- mindcontrol/control/events.py +34 -0
- mindcontrol/control/keyboard.py +101 -0
- mindcontrol/control/modes.py +194 -0
- mindcontrol/control/mouse.py +256 -0
- mindcontrol/debug_view.py +188 -0
- mindcontrol/devices.py +222 -0
- mindcontrol/filters.py +95 -0
- mindcontrol/fusion.py +240 -0
- mindcontrol/geometry.py +200 -0
- mindcontrol/gestures/__init__.py +1 -0
- mindcontrol/gestures/engine.py +465 -0
- mindcontrol/logs.py +87 -0
- mindcontrol/models.py +59 -0
- mindcontrol/pipeline.py +376 -0
- mindcontrol/record.py +454 -0
- mindcontrol/replay.py +193 -0
- mindcontrol/session.py +328 -0
- mindcontrol/tracking/__init__.py +1 -0
- mindcontrol/tracking/gaze.py +272 -0
- mindcontrol/tracking/hands.py +102 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Hand landmark tracking.
|
|
2
|
+
|
|
3
|
+
One tracker instance per camera. MediaPipe's VIDEO running mode is used rather
|
|
4
|
+
than LIVE_STREAM because it is synchronous -- the pipeline stays a plain loop
|
|
5
|
+
instead of a callback maze -- while still carrying tracking state between frames,
|
|
6
|
+
which is what makes landmarks stable enough to point with.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import cv2
|
|
12
|
+
import mediapipe as mp
|
|
13
|
+
import numpy as np
|
|
14
|
+
from mediapipe.tasks import python as mp_tasks
|
|
15
|
+
from mediapipe.tasks.python import vision
|
|
16
|
+
|
|
17
|
+
from .. import geometry, models
|
|
18
|
+
from ..capture import Frame
|
|
19
|
+
from ..config import GestureConfig, TrackingConfig
|
|
20
|
+
from ..geometry import HandFeatures
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _to_array(landmarks: list) -> np.ndarray:
|
|
24
|
+
return np.array([[p.x, p.y, p.z] for p in landmarks], dtype=np.float32)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HandTracker:
|
|
28
|
+
"""Detects hands in a camera's frames and reports their measured features."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, cfg: TrackingConfig, gestures: GestureConfig, mirrored: bool) -> None:
|
|
31
|
+
self._gestures = gestures
|
|
32
|
+
self._mirrored = mirrored
|
|
33
|
+
self._last_timestamp = -1
|
|
34
|
+
options = vision.HandLandmarkerOptions(
|
|
35
|
+
base_options=mp_tasks.BaseOptions(
|
|
36
|
+
model_asset_path=str(models.ensure("hand_landmarker.task"))
|
|
37
|
+
),
|
|
38
|
+
running_mode=vision.RunningMode.VIDEO,
|
|
39
|
+
num_hands=cfg.max_hands,
|
|
40
|
+
min_hand_detection_confidence=cfg.hand_detection_confidence,
|
|
41
|
+
min_hand_presence_confidence=cfg.hand_presence_confidence,
|
|
42
|
+
min_tracking_confidence=cfg.hand_tracking_confidence,
|
|
43
|
+
)
|
|
44
|
+
self._landmarker = vision.HandLandmarker.create_from_options(options)
|
|
45
|
+
|
|
46
|
+
def process(self, frame: Frame) -> list[HandFeatures]:
|
|
47
|
+
image = mp.Image(
|
|
48
|
+
image_format=mp.ImageFormat.SRGB,
|
|
49
|
+
data=cv2.cvtColor(frame.image, cv2.COLOR_BGR2RGB),
|
|
50
|
+
)
|
|
51
|
+
# VIDEO mode rejects a timestamp that does not advance, which two frames
|
|
52
|
+
# landing in the same millisecond will do.
|
|
53
|
+
timestamp = max(frame.timestamp_ms, self._last_timestamp + 1)
|
|
54
|
+
self._last_timestamp = timestamp
|
|
55
|
+
result = self._landmarker.detect_for_video(image, timestamp)
|
|
56
|
+
|
|
57
|
+
hands: list[HandFeatures] = []
|
|
58
|
+
for index, image_landmarks in enumerate(result.hand_landmarks):
|
|
59
|
+
image_points = _to_array(image_landmarks)
|
|
60
|
+
world = (
|
|
61
|
+
_to_array(result.hand_world_landmarks[index])
|
|
62
|
+
if index < len(result.hand_world_landmarks)
|
|
63
|
+
else _aspect_corrected(image_points, frame.image.shape)
|
|
64
|
+
)
|
|
65
|
+
category = result.handedness[index][0]
|
|
66
|
+
seen = category.category_name
|
|
67
|
+
# A mirrored frame inverts what the model calls left and right, so the
|
|
68
|
+
# label is flipped back to the hand you are actually holding up.
|
|
69
|
+
physical = _flip(seen) if self._mirrored else seen
|
|
70
|
+
hands.append(
|
|
71
|
+
geometry.measure(
|
|
72
|
+
world=world,
|
|
73
|
+
image_points=image_points,
|
|
74
|
+
handedness=physical,
|
|
75
|
+
seen_handedness=seen,
|
|
76
|
+
score=float(category.score),
|
|
77
|
+
thresholds=self._gestures,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
return hands
|
|
81
|
+
|
|
82
|
+
def close(self) -> None:
|
|
83
|
+
self._landmarker.close()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _flip(label: str) -> str:
|
|
87
|
+
return {"Left": "Right", "Right": "Left"}.get(label, label)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _aspect_corrected(points: np.ndarray, shape: tuple[int, ...]) -> np.ndarray:
|
|
91
|
+
"""Make normalised image landmarks isotropic, for when world landmarks are absent.
|
|
92
|
+
|
|
93
|
+
Normalised x and y each span 0..1 over different pixel counts, so on a 16:9
|
|
94
|
+
frame a horizontal centimetre reads smaller than a vertical one. Scaling x
|
|
95
|
+
(and z, which shares x's scale) by the aspect ratio restores one common unit.
|
|
96
|
+
"""
|
|
97
|
+
height, width = shape[0], shape[1]
|
|
98
|
+
aspect = width / max(height, 1)
|
|
99
|
+
scaled = points.copy()
|
|
100
|
+
scaled[:, 0] *= aspect
|
|
101
|
+
scaled[:, 2] *= aspect
|
|
102
|
+
return scaled
|