-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgestures.py
More file actions
232 lines (200 loc) · 10 KB
/
gestures.py
File metadata and controls
232 lines (200 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
"""
Gesture detection and state management.
Simple, clean gesture recognition: activate, click, scroll.
"""
import time
import math
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Dict, Any, Tuple
import config
class ControlMode(Enum):
"""Application control mode."""
IDLE = "IDLE" # Mouse control disabled
ACTIVE = "ACTIVE" # Mouse control enabled
@dataclass
class GestureState:
"""Tracks the current state of gesture detection."""
mode: ControlMode = ControlMode.IDLE
pinch_active: bool = False
pinch_start_time: Optional[float] = None
last_click_time: float = 0.0
last_mode_toggle_time: float = 0.0 # Track last mode toggle to prevent accidental toggles
scroll_active: bool = False
scroll_amount: float = 0.0
previous_ring_y: Optional[float] = None
smoothed_scroll: float = 0.0 # Smoothed scroll amount for stability
# Advanced click detection (inspired by handTrack)
baseline_hand_size: Optional[float] = None # Baseline hand size when not pinching
smoothed_hand_size: float = 0.0 # Smoothed hand size for stability
hand_size_change_detected: bool = False # Whether hand size change indicates pinch
pinch_start_distance: float = 0.0 # Distance when pinch started (for validation)
def calculate_distance(point1: Tuple[float, float], point2: Tuple[float, float]) -> float:
"""Calculate Euclidean distance between two 2D points."""
dx = point1[0] - point2[0]
dy = point1[1] - point2[1]
return math.sqrt(dx * dx + dy * dy)
def calculate_hand_size(landmarks: list[Tuple[float, float]]) -> float:
"""Calculate hand size as distance from wrist to middle finger tip."""
if not landmarks or len(landmarks) <= config.MIDDLE_FINGER_TIP_ID:
return 0.1
wrist = landmarks[config.WRIST_ID]
middle_tip = landmarks[config.MIDDLE_FINGER_TIP_ID]
return calculate_distance(wrist, middle_tip)
def process_gestures(
state: GestureState,
landmarks: list[Tuple[float, float]],
now: float
) -> Tuple[GestureState, Dict[str, Any]]:
"""
Process hand landmarks and update gesture state.
Returns: (updated_state, actions_dict)
"""
# Create new state
new_state = GestureState(
mode=state.mode,
pinch_active=state.pinch_active,
pinch_start_time=state.pinch_start_time,
last_click_time=state.last_click_time,
last_mode_toggle_time=state.last_mode_toggle_time,
scroll_active=state.scroll_active,
scroll_amount=0.0,
previous_ring_y=state.previous_ring_y,
smoothed_scroll=state.smoothed_scroll,
baseline_hand_size=state.baseline_hand_size,
smoothed_hand_size=state.smoothed_hand_size,
hand_size_change_detected=False,
pinch_start_distance=state.pinch_start_distance if hasattr(state, 'pinch_start_distance') else 0.0,
)
events = {
'toggle_mode': False,
'short_pinch': False,
}
index_pos = None
if not landmarks:
# No hand detected, reset states
new_state.pinch_active = False
new_state.pinch_start_time = None
new_state.scroll_active = False
new_state.previous_ring_y = None
new_state.smoothed_scroll = 0.0
new_state.baseline_hand_size = None
new_state.smoothed_hand_size = 0.0
new_state.hand_size_change_detected = False
else:
# Get finger positions
thumb_pos = landmarks[config.THUMB_TIP_ID]
index_pos = landmarks[config.INDEX_FINGER_TIP_ID]
middle_pos = landmarks[config.MIDDLE_FINGER_TIP_ID]
ring_pos = landmarks[config.RING_FINGER_TIP_ID] # 4ème doigt pour scroll
# Calculate hand size and track changes (inspired by handTrack)
hand_size = calculate_hand_size(landmarks)
# Update smoothed hand size
if new_state.smoothed_hand_size == 0.0:
new_state.smoothed_hand_size = hand_size
else:
new_state.smoothed_hand_size = (
new_state.smoothed_hand_size * config.HAND_SIZE_SMOOTHING +
hand_size * (1.0 - config.HAND_SIZE_SMOOTHING)
)
# Update baseline hand size when not pinching
if not new_state.pinch_active:
if new_state.baseline_hand_size is None:
new_state.baseline_hand_size = new_state.smoothed_hand_size
else:
# Gradually update baseline (allows for hand movement closer/farther)
new_state.baseline_hand_size = (
new_state.baseline_hand_size * 0.95 +
new_state.smoothed_hand_size * 0.05
)
# Detect hand size change (pinch detection inspired by handTrack)
if new_state.baseline_hand_size is not None and new_state.baseline_hand_size > 0:
relative_size_change = (
(new_state.baseline_hand_size - new_state.smoothed_hand_size) /
new_state.baseline_hand_size
)
new_state.hand_size_change_detected = (
relative_size_change >= config.HAND_SIZE_CHANGE_THRESHOLD
)
# Calculate adaptive threshold based on hand size
adaptive_threshold = config.PINCH_THRESHOLD * hand_size
# Check gestures
index_thumb_distance = calculate_distance(thumb_pos, index_pos) # For click (index + thumb)
ring_thumb_distance = calculate_distance(thumb_pos, ring_pos) # For scroll (ring + thumb)
# Enhanced pinch detection: combine distance AND hand size change (inspired by handTrack)
# Use index finger for clicking (as per README)
is_index_pinch_distance = index_thumb_distance < adaptive_threshold
is_index_pinch = is_index_pinch_distance or (
new_state.hand_size_change_detected and
index_thumb_distance < adaptive_threshold * 1.5 # Slightly more lenient when size change detected
)
is_ring_pinch = ring_thumb_distance < adaptive_threshold # Scroll: ring + thumb
# Handle scroll (ring finger + thumb) - amélioré pour stabilité
if is_ring_pinch and new_state.mode == ControlMode.ACTIVE:
if not new_state.scroll_active:
new_state.scroll_active = True
new_state.previous_ring_y = ring_pos[1]
new_state.scroll_amount = 0.0
new_state.smoothed_scroll = 0.0
else:
if new_state.previous_ring_y is not None:
delta_y = ring_pos[1] - new_state.previous_ring_y
# Filtrage: ignorer les très petits mouvements
if abs(delta_y) > config.SCROLL_THRESHOLD:
# Appliquer smoothing pour stabilité
raw_scroll = delta_y * config.SCROLL_SENSITIVITY
new_state.smoothed_scroll = (
new_state.smoothed_scroll * config.SCROLL_SMOOTHING +
raw_scroll * (1.0 - config.SCROLL_SMOOTHING)
)
new_state.scroll_amount = new_state.smoothed_scroll
else:
# Pour très petits mouvements, continuer le smoothing mais réduire
new_state.smoothed_scroll *= 0.8 # Décroissance
new_state.scroll_amount = new_state.smoothed_scroll
new_state.previous_ring_y = ring_pos[1]
else:
new_state.scroll_active = False
new_state.previous_ring_y = None
new_state.scroll_amount = 0.0
new_state.smoothed_scroll = 0.0
# Handle index+thumb pinch (click, toggle mode) - using index finger as per README
if is_index_pinch and not new_state.pinch_active:
# Pinch just started
new_state.pinch_active = True
new_state.pinch_start_time = now
# Store the distance when pinch started for later validation
new_state.pinch_start_distance = index_thumb_distance
elif not is_index_pinch and new_state.pinch_active:
# Pinch just ended
if new_state.pinch_start_time is not None:
duration = now - new_state.pinch_start_time
# Enhanced click detection: if pinch was detected, it's valid
# Hand size change is optional enhancement, not required
# This makes clicking more reliable while still benefiting from size detection when available
was_valid_pinch = True # If we got here, pinch was active, so it's valid
if duration >= config.LONG_PINCH_DURATION:
# Long pinch: toggle mode (with cooldown to prevent accidental toggles)
time_since_last_toggle = now - new_state.last_mode_toggle_time
if time_since_last_toggle >= config.MODE_TOGGLE_COOLDOWN:
new_state.mode = ControlMode.ACTIVE if new_state.mode == ControlMode.IDLE else ControlMode.IDLE
new_state.last_mode_toggle_time = now
events['toggle_mode'] = True
else:
# Short pinch: click (only in ACTIVE mode)
# If pinch was active, it's a valid click (hand size change is bonus, not required)
if new_state.mode == ControlMode.ACTIVE and was_valid_pinch:
events['short_pinch'] = True
# Reset pinch state and update baseline
new_state.pinch_active = False
new_state.pinch_start_time = None
# Reset baseline to current size after pinch ends
new_state.baseline_hand_size = new_state.smoothed_hand_size
# Build actions dictionary
actions = {
'mode': new_state.mode,
'index_pos': index_pos,
'scroll_amount': new_state.scroll_amount if new_state.scroll_active else 0.0,
'events': events,
}
return new_state, actions