Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 136 additions & 90 deletions camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
import numpy as np
import math
import time
from key_emulator import set_key, release_all
from pynput.mouse import Button, Controller
Expand All @@ -18,25 +17,25 @@
"inventory_key": "e",

# --- ZONES ---
"run_start_offset": 0.15, # Chest Level
"run_stop_offset": -0.10, # Waist Level
"click_trigger_offset": 0.02,
"run_start_offset": 0.15, # Chest Level

# CHANGE: Lowered to 0.30 (Mid-Thigh) to give more running room
"run_stop_offset": 0.00,

"click_trigger_offset": 0.02, # Eye Level
"hold_threshold_s": 0.5,

# --- Sensitivity ---
"jump_threshold": 0.06,
"tpose_extension": 0.12,
"tpose_vertical_tol": 0.20,
# --- STRICT ALIGNMENT SETTINGS ---
"click_vertical_align": 0.15,
"scroll_horizontal_align": 0.15,
"tpose_extension": 0.15,
"scroll_cooldown_s": 0.5,

# --- RUNNING TIMING (TIGHTENED) ---
# --- RUNNING TIMING ---
"switches_required": 1,

# CHANGED: You must pump both hands within 0.3s to start.
# This prevents accidental running when just reaching for clicks.
"start_switch_window_s": 0.3,

# CHANGED: Reset counter faster so old movements don't linger
"switch_count_reset_s": 0.5,
"jump_threshold": 0.06,
}
# ====================================================================

Expand All @@ -55,6 +54,9 @@ def force_up(button='left'):
btn = Button.left if button == 'left' else Button.right
mouse.release(btn)

def force_scroll(dy):
mouse.scroll(0, dy)

# Mediapipe Setup
BaseOptions = python.BaseOptions
PoseLandmarker = vision.PoseLandmarker
Expand Down Expand Up @@ -109,6 +111,9 @@ def force_up(button='left'):
_right_feedback_text = ""
_right_feedback_timer = 0.0

# Scroll State
_last_scroll_time = 0.0

# Feedback Overlay State
_overlay_msg = ""
_overlay_timer = 0.0
Expand Down Expand Up @@ -138,8 +143,7 @@ def _safe_set_key(key, pressed, now):
HandLandmarker.create_from_options(hand_options) as hand_landmarker:

print("SYSTEM READY.")
print(" - START RUN: SNAP both hands up (Chest) quickly!")
print(" - STOP RUN: Drop hands below Waist.")
print(" - STOP LINE LOWERED (Mid-Thigh).")

while cap.isOpened():
ret, frame = cap.read()
Expand All @@ -151,69 +155,93 @@ def _safe_set_key(key, pressed, now):
timestamp_ms = int(time.time() * 1000)

pose_res = pose_landmarker.detect_for_video(mp_image, timestamp_ms)
hand_res = hand_landmarker.detect_for_video(mp_image, timestamp_ms)
now = time.time()

if pose_res.pose_landmarks:
lm = pose_res.pose_landmarks[0]

# Key Landmarks
left_eye, right_eye = lm[5], lm[2]
ls, rs = lm[11], lm[12]
lw, rw = lm[15], lm[16]
ls, rs = lm[11], lm[12] # Shoulders
lw, rw = lm[15], lm[16] # Wrists
lh, rh = lm[23], lm[24] # Hips

# --- CALCULATE THRESHOLDS ---
# --- ZONES ---
start_thresh_y = ((ls.y + rs.y) / 2.0) + CONFIG["run_start_offset"]
stop_thresh_y = ((lh.y + rh.y) / 2.0) + CONFIG["run_stop_offset"]
eye_level_y = (left_eye.y + right_eye.y) / 2.0
click_thresh_y = eye_level_y - CONFIG["click_trigger_offset"]

# --- DRAW LINES ---
y_start_px = int(start_thresh_y * h)
cv2.line(frame, (0, y_start_px), (w_px, y_start_px), (255, 200, 0), 2)
cv2.putText(frame, "START (SNAP UP)", (10, y_start_px - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 200, 0), 1)

y_stop_px = int(stop_thresh_y * h)
cv2.line(frame, (0, y_stop_px), (w_px, y_stop_px), (0, 255, 0), 2)
cv2.putText(frame, "STOP (DROP)", (10, y_stop_px + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)

# Click Height Limit
click_y_limit = ((lm[5].y + lm[2].y)/2.0) - CONFIG["click_trigger_offset"]

y_click_px = int(click_thresh_y * h)
cv2.line(frame, (0, y_click_px), (w_px, y_click_px), (0, 0, 255), 2)
# --- DRAW VISUAL GUIDES (RESTORED) ---
# 1. Start/Stop Lines
y_start = int(start_thresh_y * h)
y_stop = int(stop_thresh_y * h)
cv2.line(frame, (0, y_start), (w_px, y_start), (255, 200, 0), 1)
cv2.line(frame, (0, y_stop), (w_px, y_stop), (0, 0, 255), 2)
cv2.putText(frame, "STOP LINE (THIGHS)", (10, y_stop - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)

# --- RUNNING LOGIC ---
l_above_start = lw.y < start_thresh_y
r_above_start = rw.y < start_thresh_y
l_below_stop = lw.y > stop_thresh_y
r_below_stop = rw.y > stop_thresh_y

_last_left_above = _update_switch(l_above_start, _last_left_above, "left", now)
_last_right_above = _update_switch(r_above_start, _last_right_above, "right", now)
# 2. CLICK ZONES (Vertical Boxes above shoulders)
v_tol_px = int(CONFIG["click_vertical_align"] * w_px)
ls_px = int(ls.x * w_px)
rs_px = int(rs.x * w_px)

# Reset counters if too slow (now 0.5s)
if _last_any_switch_time and (now - _last_any_switch_time) > CONFIG["switch_count_reset_s"]:
_left_switch_count = _right_switch_count = 0

# START Condition
req = CONFIG["switches_required"]
if _left_switch_count >= req and _right_switch_count >= req:
t_max = max(_last_left_switch_time, _last_right_switch_time)
t_min = min(_last_left_switch_time, _last_right_switch_time)
# STRICT WINDOW (0.3s)
if (t_max - t_min) <= CONFIG["start_switch_window_s"] and (now - t_max) <= CONFIG["start_switch_window_s"]:
if now >= _insta_stop_until and not _running_holding:
_running_holding = True
_left_switch_count = _right_switch_count = 0
cv2.rectangle(frame, (ls_px - v_tol_px, 0), (ls_px + v_tol_px, int(click_y_limit*h)), (50, 50, 255), 1)
cv2.rectangle(frame, (rs_px - v_tol_px, 0), (rs_px + v_tol_px, int(click_y_limit*h)), (50, 50, 255), 1)

# 3. SCROLL ZONES (Horizontal Boxes beside shoulders)
h_tol_px = int(CONFIG["scroll_horizontal_align"] * h)
ls_y_px = int(ls.y * h)
rs_y_px = int(rs.y * h)

# STOP Condition
if l_below_stop and r_below_stop:
_running_holding = False
cv2.rectangle(frame, (0, ls_y_px - h_tol_px), (ls_px - 100, ls_y_px + h_tol_px), (255, 0, 255), 1)
cv2.rectangle(frame, (rs_px + 100, rs_y_px - h_tol_px), (w_px, rs_y_px + h_tol_px), (255, 0, 255), 1)


# --- GEOMETRY CHECKS (STRICT) ---
# 1. Click Check
l_is_vertical = abs(lw.x - ls.x) < CONFIG["click_vertical_align"]
r_is_vertical = abs(rw.x - rs.x) < CONFIG["click_vertical_align"]
l_click_active = l_is_vertical and (lw.y < click_y_limit)
r_click_active = r_is_vertical and (rw.y < click_y_limit)

# 2. Scroll Check
l_is_horizontal = abs(lw.y - ls.y) < CONFIG["scroll_horizontal_align"]
r_is_horizontal = abs(rw.y - rs.y) < CONFIG["scroll_horizontal_align"]
l_is_extended = abs(lw.x - ls.x) > CONFIG["tpose_extension"]
r_is_extended = abs(rw.x - rs.x) > CONFIG["tpose_extension"]

is_left_scroll = l_is_horizontal and l_is_extended and not l_click_active
is_right_scroll = r_is_horizontal and r_is_extended and not r_click_active

# --- SCROLL & INVENTORY ---
if is_left_scroll and is_right_scroll:
cv2.putText(frame, "INVENTORY (E)", (int(w_px/2)-60, 150), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 3)
if not prev_tpose:
_safe_set_key(CONFIG["inventory_key"], True, now)
time.sleep(0.05)
_safe_set_key(CONFIG["inventory_key"], False, now)
_overlay_msg = "INVENTORY"
_overlay_timer = now + 1.5
prev_tpose = True
elif is_left_scroll:
prev_tpose = False
if now - _last_scroll_time > CONFIG["scroll_cooldown_s"]:
force_scroll(-1)
_last_scroll_time = now
_overlay_msg = "SCROLL RIGHT -->"
_overlay_timer = now + 0.5
elif is_right_scroll:
prev_tpose = False
if now - _last_scroll_time > CONFIG["scroll_cooldown_s"]:
force_scroll(1)
_last_scroll_time = now
_overlay_msg = "<-- SCROLL LEFT"
_overlay_timer = now + 0.5
else:
prev_tpose = False

# --- CLICK LOGIC ---
l_click_active = lw.y < click_thresh_y
r_click_active = rw.y < click_thresh_y
hold_time = CONFIG["hold_threshold_s"]

if l_click_active:
if not _prev_left_raised: _left_raise_start_time = now
dur = now - _left_raise_start_time
Expand All @@ -228,7 +256,6 @@ def _safe_set_key(key, pressed, now):
force_up('left')
_left_is_dragging = False
_left_feedback_text = "RELEASED"
_left_feedback_timer = now + 1.0
else:
force_click('left')
_left_feedback_text = "CLICK!"
Expand All @@ -248,7 +275,6 @@ def _safe_set_key(key, pressed, now):
force_up('right')
_right_is_dragging = False
_right_feedback_text = "RELEASED"
_right_feedback_timer = now + 1.0
else:
force_click('right')
_right_feedback_text = "CLICK!"
Expand All @@ -257,26 +283,33 @@ def _safe_set_key(key, pressed, now):
_prev_left_raised = l_click_active
_prev_right_raised = r_click_active

# --- T-POSE (PRESS E) ---
ext_thresh = CONFIG["tpose_extension"]
vert_thresh = CONFIG["tpose_vertical_tol"]
arms_wide = (abs(lw.x - ls.x) > ext_thresh) and (abs(rw.x - rs.x) > ext_thresh)
arms_level = (abs(lw.y - ls.y) < vert_thresh) and (abs(rw.y - rs.y) < vert_thresh)
is_tpose = arms_wide and arms_level

if is_tpose:
cv2.rectangle(frame, (100, 100), (w_px-100, h-100), (0, 255, 0), 4)
cv2.putText(frame, "T-POSE", (int(w_px/2)-60, 90), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)

if not prev_tpose:
_safe_set_key(CONFIG["inventory_key"], True, now)
time.sleep(0.05)
_safe_set_key(CONFIG["inventory_key"], False, now)
_overlay_msg = "INVENTORY (E)"
_overlay_timer = now + 1.5

prev_tpose = is_tpose
# --- RUNNING LOGIC (UPDATED) ---
l_below_stop = lw.y > stop_thresh_y
r_below_stop = rw.y > stop_thresh_y

if l_below_stop or r_below_stop:
_running_holding = False
else:
is_clicking_any = l_click_active or r_click_active
if not is_clicking_any:
l_above_start = lw.y < start_thresh_y
r_above_start = rw.y < start_thresh_y

_last_left_above = _update_switch(l_above_start, _last_left_above, "left", now)
_last_right_above = _update_switch(r_above_start, _last_right_above, "right", now)

if _last_any_switch_time and (now - _last_any_switch_time) > CONFIG["switch_count_reset_s"]:
_left_switch_count = _right_switch_count = 0

req = CONFIG["switches_required"]
if _left_switch_count >= req and _right_switch_count >= req:
t_max = max(_last_left_switch_time, _last_right_switch_time)
t_min = min(_last_left_switch_time, _last_right_switch_time)
if (t_max - t_min) <= CONFIG["start_switch_window_s"] and (now - t_max) <= CONFIG["start_switch_window_s"]:
if now >= _insta_stop_until and not _running_holding:
_running_holding = True
_left_switch_count = _right_switch_count = 0

# --- JUMP LOGIC ---
avg_hip_y = (lm[23].y + lm[24].y) / 2
if _is_jumping:
Expand All @@ -295,7 +328,25 @@ def _safe_set_key(key, pressed, now):

prev_hip_y = avg_hip_y

# --- VISUALS ---
# --- VISUAL FEEDBACK ---
_safe_set_key(CONFIG["running_key"], _running_holding, now)

# Status Bar
status_text = "RUNNING (W)" if _running_holding else "STOPPED"
status_color = (0, 255, 0) if _running_holding else (0, 0, 255)

box_width = 300
box_height = 60
top_x = int(w_px/2 - box_width/2)
cv2.rectangle(frame, (top_x, 10), (top_x + box_width, 10 + box_height), (50, 50, 50), -1)
cv2.rectangle(frame, (top_x, 10), (top_x + box_width, 10 + box_height), status_color, 3)

text_size = cv2.getTextSize(status_text, cv2.FONT_HERSHEY_SIMPLEX, 1, 2)[0]
text_x = int(w_px/2 - text_size[0]/2)
text_y = int(10 + box_height/2 + text_size[1]/2)
cv2.putText(frame, status_text, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 1, status_color, 2)

# Other Feedback
if now < _left_feedback_timer:
cv2.putText(frame, _left_feedback_text, (50, 250), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2)
if now < _right_feedback_timer:
Expand All @@ -304,19 +355,14 @@ def _safe_set_key(key, pressed, now):
if now < _overlay_timer:
cv2.putText(frame, _overlay_msg, (int(w_px/2)-100, int(h/2)), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 255, 255), 4)

# Progress Bars
if l_click_active and not _left_is_dragging:
pct = min(1.0, (now - _left_raise_start_time)/hold_time)
cv2.rectangle(frame, (50, 200), (50+int(100*pct), 210), (0,255,255), -1)
if r_click_active and not _right_is_dragging:
pct = min(1.0, (now - _right_raise_start_time)/hold_time)
cv2.rectangle(frame, (w_px-150, 200), (w_px-150+int(100*pct), 210), (0,255,255), -1)

_safe_set_key(CONFIG["running_key"], _running_holding, now)

status = "RUNNING" if _running_holding else "STOPPED"
col = (0, 255, 0) if _running_holding else (0, 0, 255)
cv2.putText(frame, f"Mode: {status}", (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, col, 2)

cv2.imshow('Action Recognition', frame)
if cv2.waitKey(1) & 0xFF == ord('q'): break

Expand Down