Real-Time Hand Gesture Gamepad
Hand gestures from a webcam become macOS arrow key presses through a Python to C socket bridge.
4 min read
Hand landmarks -> heuristic gesture classifier -> Unix socket -> C process that fires real macOS keyboard events. Four gestures act as four gamepad buttons.
| Gesture | Action | Key |
|---|---|---|
| open palm (4 fingers extended) | accelerate | Up arrow |
| fist (0 fingers extended) | brake | Down arrow |
| point left (only index, tip left of wrist) | steer left | Left arrow |
| point right (only index, tip right of wrist) | steer right | Right arrow |
Architecture
producer.py keybridge.c
landmark source Unix socket client
(live webcam+MediaPipe, JSON reads NDJSON lines
or synthetic replay) --> socket --> tracks gesture changes
gesture_classify.py CGEventCreateKeyboardEvent
(shared, real code path) + CGEventPost (keyDown/keyUp)
producer.py binds a Unix domain socket, waits for keybridge to connect,
then for every frame runs gesture_classify.classify() on a 21-point hand
landmark list and sends {"frame", "ts", "gesture", "confidence"}\n.
The classifier is the same function regardless of where the landmarks came
from. Only the frame source changes between live and replay.
keybridge.c connects to that socket, reads lines, maps gesture to a key,
and posts a keyDown when a gesture starts and a keyUp when it ends (so a
held gesture behaves like a held button, not a key repeated every frame).
Every attempted key press is logged regardless of whether the OS actually
delivered it.
Build & run
# 1. build the C consumer
make
# 2. start the producer (binds the socket, then waits for a consumer)
.venv10projects/bin/python producer.py --source replay
# 3. in another terminal, start the consumer
./keybridge
producer.py --source live tries the webcam via MediaPipe’s HandLandmarker
task first, and automatically falls back to --source replay if the camera
can’t be opened (no device, no permission), so the same command works
whether or not a camera is available. --source replay (the default) skips
the camera entirely and streams a hand-crafted 21-landmark sequence that
cycles through all four gestures, so the whole pipeline is runnable with
zero external permissions.
Useful flags: --fps, --hold (frames per gesture in replay), --cycles,
--socket (defaults to /tmp/hand_gesture_gamepad.sock).
Self-checks: python gesture_classify.py asserts the classifier against
hand-built landmark sets for each gesture.
Captured run (synthetic replay, real producer -> real consumer)
Producer (producer.py --source replay --fps 15 --hold 8 --cycles 2):
[producer] listening on /tmp/hand_gesture_gamepad.sock, waiting for consumer to connect...
[producer] consumer connected, streaming gestures
[producer] frame=000 gesture=open_palm conf=0.90
...
[producer] frame=063 gesture=point_right conf=0.85
[producer] done, 64 frames sent
Consumer (./keybridge), same run:
[keybridge] connecting to /tmp/hand_gesture_gamepad.sock ...
[keybridge] connected, reading gesture events
>>> KEYDOWN key=UpArrow (gesture=open_palm, action=accelerate)
frame=001 gesture=open_palm (holding UpArrow)
...
>>> KEYUP key=UpArrow (gesture=open_palm, action=accelerate)
>>> KEYDOWN key=DownArrow (gesture=fist, action=brake)
...
>>> KEYUP key=DownArrow (gesture=fist, action=brake)
>>> KEYDOWN key=LeftArrow (gesture=point_left, action=steer_left)
...
>>> KEYUP key=LeftArrow (gesture=point_left, action=steer_left)
>>> KEYDOWN key=RightArrow (gesture=point_right, action=steer_right)
...
>>> KEYUP key=RightArrow (gesture=point_right, action=steer_right)
[keybridge] stream closed. 64 events, 8 key presses.
(Full logs cycle through the four gestures twice, 64 frames / 8 key transitions total. The excerpt above is trimmed for length.)
--source live was also exercised in this sandbox: OpenCV reported
not authorized to capture video, producer.py caught that and fell back
to replay automatically, and the consumer still fired the correct
keyDown/keyUp sequence. This proves the fallback path, not just the demo path.
Simplified vs full spec
- No live camera / no Accessibility permission in this sandbox. The
demo runs on synthetic landmark data and
CGEventPostcalls that are logged whether or not the OS actually delivers them (no Accessibility grant here). Add when: running on a Mac with a camera and this app added to System Settings > Privacy & Security > Accessibility and Camera. - Heuristic finger-extension classifier instead of a trained CNN.
gesture_classify.pycompares fingertip vs. PIP-joint y-coordinates, which is standard practice for landmark-based gesture recognition and needs no training data. Add when: gestures beyond simple finger counting/pointing are needed (e.g. distinguishing OK-sign from thumbs-up): train a small sklearn KNN/SVM on landmark feature vectors. - User-space
CGEventPostinstead of a kernel HID driver. A true virtual HID device needs a signed DriverKit extension (Apple developer account, notarization, kernel entitlements), infeasible in a sandbox. Add when: an unsigned/no-permission-dialog input path is required: ship a DriverKit HID driver extension instead.
Support
Submit issues, questions, or bug reports to the GitHub issue tracker: https://github.com/ADM1SH/hand-gesture-gamepad/issues
Roadmap
- Done: Core architecture and baseline implementation.
- Done: Functional verification and test coverage.
- To do: Add two-hand gesture chord combinations
- To do: Support dynamic sensitivity and deadzone calibration
Contributing
Contributions are welcome.
- Fork the repository.
- Create a feature branch:
git checkout -b feature/improvement. - Commit your changes:
git commit -m "feat: enhance functionality". - Push to the branch:
git push origin feature/improvement. - Open a Pull Request.
Authors and Acknowledgment
- Adam Anwar (ADM1SH) - Lead architect and developer.
- Authored by Adam Anwar. Uses Google MediaPipe Hands and macOS CoreGraphics event taps.
License
Licensed under the MIT License. See LICENSE for details.
Project Status
Real-time computer vision gamepad bridge.