Skip to content

prem-2006/CS2-HandGesture-control

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 

Repository files navigation

s - toggle control ON/OFF m - toggle aim mode (ABSOLUTE <-> RELATIVE) f - cycle firing mode (tap -> hold -> burst) c - calibrate (current finger becomes center) l - toggle Windows low-level mouse (Windows only) + / - - increase / decrease sensitivity q / ESC - quit """ import time import math import platform import sys from collections import deque

import cv2 import numpy as np import mediapipe as mp import pyautogui

CAMERA_INDEX = 0 CAMERA_FLIP = True # mirror camera MAX_HANDS = 1

EST_FPS = 60.0

DEFAULT_SENS = 1.0
DEFAULT_DEADZONE = 0.02
DEFAULT_SMOOTH = 0.18 DEFAULT_PINCH_THRESH = 0.045
DEFAULT_BURST_COUNT = 3 DEFAULT_BURST_INTERVAL_MS = 60

SCREEN_W, SCREEN_H = pyautogui.size()

mp_hands = mp.solutions.hands mp_drawing = mp.solutions.drawing_utils INDEX_TIP = 8 THUMB_TIP = 4

class LowPass: def init(self, alpha=1.0, init_val=0.0): self.alpha = float(alpha) self.s = float(init_val) self.initialized = False

def filter(self, x):
    if not self.initialized:
        self.s = float(x)
        self.initialized = True
        return self.s
    self.s = self.alpha * float(x) + (1.0 - self.alpha) * self.s
    return self.s

def alpha(cutoff, dt): tau = 1.0 / (2.0 * math.pi * cutoff) return 1.0 / (1.0 + tau / dt)

class OneEuro: def init(self, freq=EST_FPS, min_cutoff=1.0, beta=0.007): self.freq = float(freq) self.min_cutoff = float(min_cutoff) self.beta = float(beta) self.x_prev = None self.dx_filter = None self.x_filter = None self.last_t = None

def __call__(self, x, t=None):
    if t is None:
        t = time.time()
    if self.last_t is None:
        dt = 1.0 / self.freq
    else:
        dt = max(1e-6, t - self.last_t)
    self.last_t = t

    if self.x_prev is None:
        self.x_prev = x
        self.dx_filter = LowPass(alpha=alpha(self.min_cutoff, dt), init_val=0.0)
        self.x_filter = LowPass(alpha=alpha(self.min_cutoff, dt), init_val=x)
        return x

    dx = (x - self.x_prev) / dt
    self.x_prev = x

    dx_hat = self.dx_filter.filter(dx)
    cutoff = self.min_cutoff + self.beta * abs(dx_hat)
    a = alpha(cutoff, dt)
    self.x_filter.alpha = a
    return self.x_filter.filter(x)

IS_WINDOWS = platform.system().lower().startswith("win") low_level_enabled = False

if IS_WINDOWS: import ctypes from ctypes import wintypes PUL = ctypes.POINTER(ctypes.c_ulong) class MOUSEINPUT(ctypes.Structure): fields = [("dx", ctypes.c_long), ("dy", ctypes.c_long), ("mouseData", ctypes.c_ulong), ("dwFlags", ctypes.c_ulong), ("time", ctypes.c_ulong), ("dwExtraInfo", PUL)] class INPUT(ctypes.Structure): class _I(ctypes.Union): fields = [("mi", MOUSEINPUT)] anonymous = ("i",) fields = [("type", ctypes.c_ulong), ("i", _I)] SendInput = ctypes.windll.user32.SendInput MOUSEEVENTF_LEFTDOWN = 0x0002 MOUSEEVENTF_LEFTUP = 0x0004

def win_mouse_down():
    inp = INPUT()
    inp.type = 0  # INPUT_MOUSE
    inp.mi = MOUSEINPUT(0, 0, 0, MOUSEEVENTF_LEFTDOWN, 0, None)
    SendInput(1, ctypes.byref(inp), ctypes.sizeof(inp))

def win_mouse_up():
    inp = INPUT()
    inp.type = 0
    inp.mi = MOUSEINPUT(0, 0, 0, MOUSEEVENTF_LEFTUP, 0, None)
    SendInput(1, ctypes.byref(inp), ctypes.sizeof(inp))

else: def win_mouse_down(): raise RuntimeError("Not Windows") def win_mouse_up(): raise RuntimeError("Not Windows")

def mouse_down(): global low_level_enabled try: if IS_WINDOWS and low_level_enabled: win_mouse_down() else: pyautogui.mouseDown() except Exception: # ignore failures; permission issues may occur pass

def mouse_up(): global low_level_enabled try: if IS_WINDOWS and low_level_enabled: win_mouse_up() else: pyautogui.mouseUp() except Exception: pass

def norm_to_screen(nx, ny): sx = int(np.clip(nx, 0.0, 1.0) * SCREEN_W) sy = int(np.clip(ny, 0.0, 1.0) * SCREEN_H) return sx, sy

def dist_norm(a, b): return math.hypot(a[0] - b[0], a[1] - b[1])

WIN_NAME = "HandAim - Fixed (PRACTICE ONLY)" cv2.namedWindow(WIN_NAME, cv2.WINDOW_NORMAL) cv2.resizeWindow(WIN_NAME, 960, 540)

def nothing(x): pass

cv2.createTrackbar("Sensitivity x100", WIN_NAME, int(DEFAULT_SENS100), 500, nothing) cv2.createTrackbar("Deadzone x1000", WIN_NAME, int(DEFAULT_DEADZONE1000), 200, nothing) cv2.createTrackbar("Smoothing x100", WIN_NAME, int(DEFAULT_SMOOTH100), 90, nothing) cv2.createTrackbar("PinchThresh x1000", WIN_NAME, int(DEFAULT_PINCH_THRESH1000), 200, nothing) cv2.createTrackbar("BurstCount", WIN_NAME, DEFAULT_BURST_COUNT, 10, nothing) cv2.createTrackbar("BurstInterval ms", WIN_NAME, DEFAULT_BURST_INTERVAL_MS, 500, nothing) cv2.createTrackbar("FireMode (0 tap,1 hold,2 burst)", WIN_NAME, 0, 2, nothing)

def main(): global low_level_enabled

cap = cv2.VideoCapture(CAMERA_INDEX)
if not cap.isOpened():
    print("ERROR: cannot open camera index", CAMERA_INDEX)
    sys.exit(1)

hands = mp_hands.Hands(static_image_mode=False, max_num_hands=MAX_HANDS,
                       min_detection_confidence=0.6, min_tracking_confidence=0.6)

one_x = OneEuro(freq=EST_FPS)
one_y = OneEuro(freq=EST_FPS)

trail = deque(maxlen=12)

control_on = False
absolute_mode = True
firing_mode = 'tap'  # tap / hold / burst
hold_active = False
last_click_time = 0.0
burst_remaining = 0
burst_last_time = 0.0

calib_offset_x = 0.0
calib_offset_y = 0.0

prev_norm = None

print("Started. Keys: s toggle, m mode, f cycle fire, c calibrate, l toggle low-level (Win), q quit")

try:
    while True:
        ret, frame = cap.read()
        if not ret:
            print("Camera read failed. Exiting.")
            break

        if CAMERA_FLIP:
            frame = cv2.flip(frame, 1)

        h, w, _ = frame.shape
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        res = hands.process(frame_rgb)

        
        sens = cv2.getTrackbarPos("Sensitivity x100", WIN_NAME) / 100.0
        deadzone = cv2.getTrackbarPos("Deadzone x1000", WIN_NAME) / 1000.0
        smoothing = cv2.getTrackbarPos("Smoothing x100", WIN_NAME) / 100.0
        pinch_thresh = cv2.getTrackbarPos("PinchThresh x1000", WIN_NAME) / 1000.0
        burst_count = cv2.getTrackbarPos("BurstCount", WIN_NAME)
        burst_interval = cv2.getTrackbarPos("BurstInterval ms", WIN_NAME) / 1000.0
        firemode_tb = cv2.getTrackbarPos("FireMode (0 tap,1 hold,2 burst)", WIN_NAME)
        if firemode_tb == 0:
            firing_mode = 'tap'
        elif firemode_tb == 1:
            firing_mode = 'hold'
        else:
            firing_mode = 'burst'

        index_pt = None
        thumb_pt = None
        pinch = False
        pinch_dist = 999.0

        if res.multi_hand_landmarks:
            lm = res.multi_hand_landmarks[0]
            idx = lm.landmark[INDEX_TIP]
            th = lm.landmark[THUMB_TIP]

            nx = idx.x + calib_offset_x
            ny = idx.y + calib_offset_y
            tx = th.x + calib_offset_x
            ty = th.y + calib_offset_y

            index_pt = (nx, ny)
            thumb_pt = (tx, ty)
            pinch_dist = math.hypot(nx - tx, ny - ty)
            if pinch_dist < pinch_thresh:
                pinch = True

            mp_drawing.draw_landmarks(frame, lm, mp_hands.HAND_CONNECTIONS)
            # small dots
            cv2.circle(frame, (int(idx.x * w), int(idx.y * h)), 4, (0, 255, 0), -1)
            cv2.circle(frame, (int(th.x * w), int(th.y * h)), 4, (0, 0, 255), -1)

        now = time.time()

        
        if control_on and index_pt is not None:
            nx, ny = index_pt

            
            cx = nx - 0.5
            cy = ny - 0.5
            if abs(cx) < deadzone:
                cx = 0.0
            if abs(cy) < deadzone:
                cy = 0.0
            
            if absolute_mode:
                
                tx_n = (nx * sens) - (0.5 * (sens - 1.0))
                ty_n = (ny * sens) - (0.5 * (sens - 1.0))
                tx_n = float(np.clip(tx_n, 0.0, 1.0))
                ty_n = float(np.clip(ty_n, 0.0, 1.0))

                
                fx = one_x(tx_n, now)
                fy = one_y(ty_n, now)
                sx, sy = norm_to_screen(fx, fy)

                
                try:
                    cur_x, cur_y = pyautogui.position()
                    new_x = cur_x + (sx - cur_x) * smoothing
                    new_y = cur_y + (sy - cur_y) * smoothing
                    pyautogui.moveTo(int(new_x), int(new_y), _pause=False)
                except Exception:
                    cv2.putText(frame, "Mouse move blocked / requires permissions", (10, 60),
                                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
            else:
                
                if prev_norm is None:
                    prev_norm = (nx, ny)
                dx = (nx - prev_norm[0]) * SCREEN_W * sens
                dy = (ny - prev_norm[1]) * SCREEN_H * sens
                prev_norm = (nx, ny)
                # apply basic smoothing scale for movement
                dx = dx * (1.0 - smoothing)
                dy = dy * (1.0 - smoothing)
                try:
                    pyautogui.moveRel(int(dx), int(dy), _pause=False)
                except Exception:
                    cv2.putText(frame, "Mouse rel blocked / requires permissions", (10, 60),
                                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)

            
            fx_frame_x = int(nx * w)
            fy_frame_y = int(ny * h)
            trail.appendleft((fx_frame_x, fy_frame_y))
            for i, p in enumerate(trail):
                radius = max(1, 6 - i)
                alpha = 1.0 - (i / max(1, len(trail)))
                color = (0, int(200 * alpha) + 30, int(255 * alpha))
                cv2.circle(frame, p, radius, color, -1)
        else:
            # when control off, reset prev_norm (so relative mode doesn't jump)
            prev_norm = None

        # Firing logic
        if control_on and pinch:
            if firing_mode == 'tap':
                if (now - last_click_time) > 0.08:
                    last_click_time = now
                    try:
                        pyautogui.click()
                    except Exception:
                        pass
            elif firing_mode == 'hold':
                if not hold_active:
                    mouse_down()
                    hold_active = True
            elif firing_mode == 'burst':
                if burst_remaining == 0 and (now - last_click_time) > 0.08:
                    burst_remaining = max(1, burst_count)
                    burst_last_time = 0.0  # allow immediate first click
                    last_click_time = now
        else:
            # pinch released -> release hold if active
            if hold_active:
                mouse_up()
                hold_active = False

        # Burst processing (timed)
        if burst_remaining > 0:
            if burst_last_time == 0.0 or (now - burst_last_time) >= burst_interval:
                try:
                    pyautogui.click()
                except Exception:
                    pass
                burst_remaining -= 1
                burst_last_time = now

        # Overlay UI text
        mode_text = "ABSOLUTE" if absolute_mode else "RELATIVE"
        status_text = f"Control: {'ON' if control_on else 'OFF'}  Mode: {mode_text}  Fire: {firing_mode.upper()}"
        cv2.putText(frame, status_text, (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 255, 200), 2)

        cv2.putText(frame, f"Sens:{sens:.2f} Dead:{deadzone:.3f} Smooth:{smoothing:.2f} Pinch:{pinch_thresh:.3f}",
                    (10, 44), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 255, 200), 1)
        cv2.putText(frame, f"Burst:{burst_count}x {int(burst_interval*1000)}ms  PinchDist:{pinch_dist:.3f}",
                    (10, 64), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 255), 1)

        # small center crosshair
        cv2.drawMarker(frame, (int(w/2), int(h/2)), (255, 255, 255), markerType=cv2.MARKER_CROSS, markerSize=12, thickness=1)

        cv2.imshow(WIN_NAME, frame)
        key = cv2.waitKey(1) & 0xFF

        if key == 27 or key == ord('q'):
            break
        elif key == ord('s'):
            control_on = not control_on
            prev_norm = None
            # ensure any held mouse button is released
            mouse_up()
            print("Control toggled:", control_on)
        elif key == ord('m'):
            absolute_mode = not absolute_mode
            prev_norm = None
            print("Aiming mode:", "ABSOLUTE" if absolute_mode else "RELATIVE")
        elif key == ord('f'):
            # cycle firing mode
            if firing_mode == 'tap':
                firing_mode = 'hold'
            elif firing_mode == 'hold':
                firing_mode = 'burst'
            else:
                firing_mode = 'tap'
            # update trackbar to reflect selection
            tb_val = 0 if firing_mode == 'tap' else (1 if firing_mode == 'hold' else 2)
            cv2.setTrackbarPos("FireMode (0 tap,1 hold,2 burst)", WIN_NAME, tb_val)
            print("Firing mode:", firing_mode)
        elif key == ord('c'):
            if index_pt is not None:
                nx, ny = index_pt
                calib_offset_x = 0.5 - nx
                calib_offset_y = 0.5 - ny
                print("Calibrated offsets:", calib_offset_x, calib_offset_y)
            else:
                print("No hand detected to calibrate.")
        elif key == ord('l'):
            if IS_WINDOWS:
                low_level_enabled = not low_level_enabled
                print("Windows low-level mouse enabled:", low_level_enabled)
            else:
                print("Low-level mouse only available on Windows.")
        elif key == ord('+') or key == ord('='):
            cur = cv2.getTrackbarPos("Sensitivity x100", WIN_NAME)
            cv2.setTrackbarPos("Sensitivity x100", WIN_NAME, min(500, cur + 5))
        elif key == ord('-') or key == ord('_'):
            cur = cv2.getTrackbarPos("Sensitivity x100", WIN_NAME)
            cv2.setTrackbarPos("Sensitivity x100", WIN_NAME, max(1, cur - 5))

finally:
    # ensure mouse button up and cleanup
    try:
        mouse_up()
    except Exception:
        pass
    cap.release()
    cv2.destroyAllWindows()

if name == "main": main()

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages