-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
304 lines (251 loc) · 10.5 KB
/
Copy pathmain.py
File metadata and controls
304 lines (251 loc) · 10.5 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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
"""
GESTURE MAP
☝ Index only → Move mouse
✌ Index+Middle open → Click-ready
✌ Index+Middle pinch → Left click
👌 Thumb+Index pinch → Right click
🤘 Index+Pinky (horns) → Scroll UP (fixed, tap gesture)
🤙 Thumb+Pinky → Scroll DOWN (fixed, tap gesture)
✊ Fist → PAUSE (failsafe)
🖐 All 5 fingers → PAUSE (failsafe)
👋 Wave (wrist L→R→L) → Double click
🤞 Index+Middle crossed → Drag lock toggle
"""
import cv2
import numpy as np
import HandTrackingModule as htm
import time
import ctypes
from pynput.mouse import Button, Controller
# Config
wCam, hCam = 640, 480
frameR = 60 # smaller margin = more reach incl. taskbar
smoothening = 5
CLICK_DIST = 38
SCROLL_AMOUNT = 3 # scroll clicks per gesture trigger
WAVE_THRESH = 50
WAVE_TIMEOUT = 0.7
DRAG_DIST = 35 # pinch dist to activate drag
# Screen
user32 = ctypes.windll.user32
user32.SetProcessDPIAware()
wScr = user32.GetSystemMetrics(0)
hScr = user32.GetSystemMetrics(1)
mouse = Controller()
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, wCam)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, hCam)
cap.set(cv2.CAP_PROP_FPS, 30)
detector = htm.handDetector(maxHands=1, detectionCon=0.78, trackCon=0.65)
pTime = 0
plocX, plocY = 0, 0
clocX, clocY = 0, 0
click_cooldown = 0
scroll_cooldown = 0 # prevents scroll spam
# Wave state
wave_stage = 0
wave_last_x = None
wave_timer = 0.0
# Drag state
drag_active = False
# Status
status_text = "Ready"
status_color = (0, 255, 120)
print(f"[INFO] Screen: {wScr}x{hScr} | Cam: {wCam}x{hCam} | Q to quit")
# Gestures
def is_fist(fingers):
return fingers == [0, 0, 0, 0, 0]
def is_open_palm(fingers):
return fingers == [1, 1, 1, 1, 1]
def is_move(fingers):
# Index only — middle, ring, pinky down
return fingers[1] == 1 and fingers[2] == 0 and fingers[3] == 0 and fingers[4] == 0
def is_left_click_mode(fingers):
return fingers[1] == 1 and fingers[2] == 1 and fingers[3] == 0 and fingers[4] == 0
def is_right_click_mode(fingers):
return fingers[0] == 1 and fingers[1] == 1 and fingers[2] == 0 and fingers[3] == 0 and fingers[4] == 0
def is_scroll_up(fingers):
# Horns: index + pinky up, middle + ring down — \\m/
return fingers[1] == 1 and fingers[4] == 1 and fingers[2] == 0 and fingers[3] == 0
def is_scroll_down(fingers):
# Thumb + pinky up, others down
return fingers[0] == 1 and fingers[4] == 1 and fingers[1] == 0 and fingers[2] == 0 and fingers[3] == 0
def is_drag_gesture(fingers):
# Middle + index crossed/together — all three top fingers up
return fingers[1] == 1 and fingers[2] == 1 and fingers[3] == 1 and fingers[4] == 0
def detect_wave(wrist_x, now):
global wave_stage, wave_last_x, wave_timer
if wrist_x is None:
return False
if wave_last_x is None:
wave_last_x = wrist_x
return False
delta = wrist_x - wave_last_x
if now - wave_timer > WAVE_TIMEOUT:
wave_stage = 0
if wave_stage == 0 and delta > WAVE_THRESH:
wave_stage = 1
wave_timer = now
elif wave_stage == 1 and delta < -WAVE_THRESH:
wave_stage = 2
wave_timer = now
elif wave_stage == 2 and delta > WAVE_THRESH:
wave_stage = 0
wave_last_x = wrist_x
return True
wave_last_x = wrist_x
return False
# UI
def draw_ui(img, status, color, fps, drag_on):
# Bottom legend bar
overlay = img.copy()
cv2.rectangle(overlay, (0, hCam-88), (wCam, hCam), (15,15,15), -1)
cv2.addWeighted(overlay, 0.6, img, 0.4, 0, img)
lines = [
"☝ Move ✌ L-Click(pinch) 👌 R-Click(pinch) ✊/🖐 PAUSE",
"🤘 Scroll UP 🤙 Scroll DOWN 👋 Wave=DblClick ✋ Drag",
]
for i, txt in enumerate(lines):
cv2.putText(img, txt, (6, hCam-66+i*26),
cv2.FONT_HERSHEY_SIMPLEX, 0.40, (160,160,160), 1, cv2.LINE_AA)
# Top bar
overlay2 = img.copy()
cv2.rectangle(overlay2, (0,0), (wCam, 48), (15,15,15), -1)
cv2.addWeighted(overlay2, 0.6, img, 0.4, 0, img)
cv2.putText(img, f"MODE: {status}", (10, 32),
cv2.FONT_HERSHEY_SIMPLEX, 0.72, color, 2, cv2.LINE_AA)
cv2.putText(img, f"FPS {int(fps)}", (wCam-85, 32),
cv2.FONT_HERSHEY_SIMPLEX, 0.60, (80,220,80), 2, cv2.LINE_AA)
if drag_on:
cv2.putText(img, "DRAG ON", (wCam//2-45, 32),
cv2.FONT_HERSHEY_SIMPLEX, 0.62, (0,200,255), 2, cv2.LINE_AA)
# Active zone — mapped to FULL screen including taskbar
cv2.rectangle(img, (frameR, frameR), (wCam-frameR, hCam-frameR), (60,60,180), 1)
while True:
success, img = cap.read()
if not success:
print("[ERROR] Camera read failed. Try VideoCapture(1)")
break
img = cv2.flip(img, 1)
img = detector.findHands(img, draw=True)
lmList, _ = detector.findPosition(img, draw=False)
now = time.time()
if not detector.handPresent() or not lmList:
status_text = "No Hand"
status_color = (90, 90, 90)
# Release drag if hand lost
if drag_active:
mouse.release(Button.left)
drag_active = False
else:
fingers = detector.fingersUp(mirrored=True)
wrist_x = detector.getWristX()
# FAILSAFE
if is_fist(fingers) or is_open_palm(fingers):
if drag_active:
mouse.release(Button.left)
drag_active = False
status_text = "PAUSED"
status_color = (0, 80, 220)
# WAVE → double click
elif detect_wave(wrist_x, now) and click_cooldown == 0:
mouse.click(Button.left, 2)
click_cooldown = 25
status_text = "DOUBLE CLICK"
status_color = (255, 220, 0)
# SCROLL UP
elif is_scroll_up(fingers):
if scroll_cooldown == 0:
mouse.scroll(0, SCROLL_AMOUNT)
scroll_cooldown = 12 # ~12 frames between scroll ticks
status_text = "SCROLL UP 🤘"
status_color = (0, 240, 180)
cv2.putText(img, "▲▲▲", (wCam//2-30, hCam//2),
cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0,240,180), 3, cv2.LINE_AA)
# Scroll Down
elif is_scroll_down(fingers):
if scroll_cooldown == 0:
mouse.scroll(0, -SCROLL_AMOUNT)
scroll_cooldown = 12
status_text = "SCROLL DOWN 🤙"
status_color = (0, 180, 255)
cv2.putText(img, "▼▼▼", (wCam//2-30, hCam//2),
cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0,180,255), 3, cv2.LINE_AA)
# Drag
elif is_drag_gesture(fingers):
x1, y1 = lmList[8][1], lmList[8][2]
x3 = np.interp(x1, (frameR, wCam-frameR), (0, wScr))
y3 = np.interp(y1, (frameR, hCam-frameR), (0, hScr))
clocX = plocX + (x3-plocX) / smoothening
clocY = plocY + (y3-plocY) / smoothening
mouse.position = (int(clocX), int(clocY))
length, img, _ = detector.findDistance(8, 12, img, draw=False)
if length < DRAG_DIST and not drag_active:
mouse.press(Button.left)
drag_active = True
elif length >= DRAG_DIST and drag_active:
mouse.release(Button.left)
drag_active = False
plocX, plocY = clocX, clocY
status_text = "DRAG"
status_color = (255, 140, 0)
cv2.circle(img, (x1,y1), 14, (255,140,0), cv2.FILLED)
else:
# Release drag if leaving drag gesture
if drag_active:
mouse.release(Button.left)
drag_active = False
# Move
if is_move(fingers):
x1, y1 = lmList[8][1], lmList[8][2]
# Map full camera zone → full screen (0,0) to (wScr, hScr)
# including taskbar — no clipping
x3 = np.interp(x1, (frameR, wCam-frameR), (0, wScr))
y3 = np.interp(y1, (frameR, hCam-frameR), (0, hScr))
clocX = plocX + (x3-plocX) / smoothening
clocY = plocY + (y3-plocY) / smoothening
# Clamp to allow reaching every pixel including taskbar
clocX = max(0, min(clocX, wScr-1))
clocY = max(0, min(clocY, hScr-1))
mouse.position = (int(clocX), int(clocY))
cv2.circle(img, (x1,y1), 12, (80,255,180), cv2.FILLED)
plocX, plocY = clocX, clocY
status_text = "MOVE"
status_color = (80, 255, 180)
# Left Click
if is_left_click_mode(fingers):
length, img, lineInfo = detector.findDistance(8, 12, img)
if length < CLICK_DIST and click_cooldown == 0:
cv2.circle(img, (lineInfo[4], lineInfo[5]), 14, (0,255,80), cv2.FILLED)
mouse.click(Button.left, 1)
click_cooldown = 18
status_text = "LEFT CLICK"
status_color = (0, 255, 80)
else:
status_text = "CLICK READY"
status_color = (0, 200, 255)
# Right Click
elif is_right_click_mode(fingers):
length, img, lineInfo = detector.findDistance(4, 8, img)
if length < CLICK_DIST and click_cooldown == 0:
cv2.circle(img, (lineInfo[4], lineInfo[5]), 14, (0,120,255), cv2.FILLED)
mouse.click(Button.right, 1)
click_cooldown = 18
status_text = "RIGHT CLICK"
status_color = (0, 120, 255)
# Cooldowns
if click_cooldown > 0: click_cooldown -= 1
if scroll_cooldown > 0: scroll_cooldown -= 1
# Fps and ui
cTime = time.time()
fps = 1 / (cTime - pTime) if pTime else 0
pTime = cTime
draw_ui(img, status_text, status_color, fps, drag_active)
cv2.imshow("Virtual Mouse | Q to quit", img)
if cv2.waitKey(1) & 0xFF == ord('q'):
if drag_active:
mouse.release(Button.left)
break
cap.release()
cv2.destroyAllWindows()
print("[INFO] Exited cleanly.")