Real-time hand tracking • gesture calibration • desktop automation • configurable bindings
InScroll started from a very real desk problem.
I wanted to watch Instagram Reels on desktop during breaks, but I was sitting in a relaxed position and scrolling with the arrow keys or mouse wheel felt slow and awkward. The app login flow was also inconvenient, so I leaned into the desktop experience instead.
The result was a hand-gesture controller that lets me keep sipping coffee, stay comfortable, and simply use a palm or fist to control scrolling and other actions from the webcam.
That idea became InScroll: a practical desktop gesture-control app built for fast, natural interaction.
InScroll turns a webcam into a touchless control system for the desktop.
It uses MediaPipe hand landmarks, OpenCV video capture, gesture classification, configurable bindings, calibration tools, and PyAutoGUI actions to detect gestures and trigger real keyboard or hotkey events locally.
It is designed for:
- Smooth Instagram Reels scrolling on desktop
- Productivity shortcuts
- Accessibility workflows
- Presentations
- Media control
- Computer vision experiments
- Human-computer interaction demos
The core idea behind the project is simple:
- Sit back
- Keep your hand in a relaxed position
- Show your palm to scroll down
- Make a fist to scroll up
- Tune the timing and thresholds from the UI
Everything important can be adjusted from the Configurations and Calibration tabs.
- MediaPipe Hand Landmarker integration
- Left and right hand support
- Landmark-based finger analysis
- Live skeleton overlay
- Mirrored preview for natural laptop use
Recognizes these gestures:
- Thumb Up
- Thumb Down
- Fist
- Open Palm
- Swipe Up
- Swipe Down
- Swipe Left
- Swipe Right
- Pinch
To avoid accidental actions, InScroll uses:
- Stable frame validation
- Gesture cooldown timing
- Swipe duration windows
- Distance thresholds
- Pinch thresholds
- Open-palm finger count checks
Gesture actions can trigger:
- Single key presses
- Hotkeys
- Scroll actions
- Navigation shortcuts
- Custom desktop workflows
The calibration system helps tune the app for:
- Different webcams
- Different lighting
- Different hand sizes
- Different desk distances
- Different sitting positions
If the MediaPipe hand model is missing, the app can download it automatically and store it locally in models/hand_landmarker.task.
The project includes a compatibility patch for MediaPipe so newer Python versions can keep working more smoothly.
The Preview tab is the main dashboard.
It shows:
- Live webcam feed
- Green skeleton overlay
- Realtime hand count
- Last detected gesture
- Tracking status
- FPS
- Event log
- Start/Stop controls
- Save/Reload config buttons
It also includes a center guide for calibration and short usage notes.
The Configurations tab exposes the full tuning surface:
Camera + Detection
- Camera index
- Frame width
- Frame height
- Mirror preview
- Max hands
- Model complexity
- Detection confidence
- Tracking confidence
Gesture Engine
- Stable frames
- Gesture cooldown ms
- Swipe minimum dx
- Swipe minimum dy
- Swipe minimum duration ms
- Swipe maximum duration ms
- Pinch threshold
- Open palm minimum fingers
Gesture bindings Each gesture can be configured with:
- Enabled / disabled
- Action type:
key,hotkey, orscroll - Value field for the action payload
The Calibration tab is built for tuning the app to your own movement.
It includes:
- Calibration workflow instructions
- Neutral center capture
- Swipe calibration
- Reset calibration
- Current tuning summary
- Live calibration feedback
This is where you can align the swipe timing and thresholds to your own gesture speed.
The app was made for relaxed desktop use.
When I am watching reels and sitting back, reaching for the mouse wheel or arrow keys repeatedly is annoying. InScroll replaces that with a gesture-first flow that feels natural from a desk chair.
That is why the default experience focuses on:
- simple gestures
- fast scrolling
- configurable thresholds
- reliable stabilization
- low-friction desktop interaction
Webcam
↓
OpenCV frame capture
↓
MediaPipe hand landmark detection
↓
Gesture classification
↓
Swipe / pinch / open-palm logic
↓
Stable-frame + cooldown validation
↓
PyAutoGUI key / hotkey / scroll action
↓
Desktop event
InScroll
├── main.py
├── app.py
├── patch.py
├── config.json
├── log.txt
├── requirements.txt
├── models/
│ └── hand_landmarker.task
├── assets/
│ ├── banner.png
│ ├── gesture-concept.gif
│ ├── inscroll-ui-tour.gif
│ └── screenshots/
│ ├── preview.png
│ ├── configurations.png
│ └── calibration.png
└── V1/
├── main.py
├── app.py
├── config.json
├── requirements.txt
├── static/
│ ├── app.js
│ └── style.css
└── templates/
└── index.html
The root app is the main desktop build.
V1/ is a legacy browser-based studio build that keeps the same gesture-binding idea in a Flask dashboard.
| File | Purpose | Notable contents |
|---|---|---|
main.py |
Application entry point | Starts the Tkinter desktop app |
app.py |
Core desktop application | Gesture engine, calibration, UI, config handling, camera loop |
patch.py |
Manual MediaPipe compatibility helper | apply_patch() for Python 3.14+ MediaPipe fixes |
config.json |
Persistent settings | Camera, detection, gesture, bindings, calibration |
log.txt |
Runtime log file | Useful for debugging and diagnostics |
requirements.txt |
Dependencies | OpenCV, MediaPipe, NumPy, Pillow, PyAutoGUI, Protobuf, Abseil |
models/hand_landmarker.task |
MediaPipe task model | Downloaded automatically if missing |
V1/ |
Legacy browser studio | Flask app, templates, static JS/CSS, API endpoints |
A lightweight dataclass used to store motion samples for swipe detection.
Fields:
t→ timestampx→ normalized x positiony→ normalized y position
Used by the gesture engine to compare recent hand movement with older samples.
A reusable Tkinter scroll container used for long configuration sections and gesture binding lists.
The main desktop application class.
It owns:
- the window
- the tabs
- the camera lifecycle
- the gesture pipeline
- the calibration flow
- the event log
- the status indicators
- the config save/load behavior
| Function | Purpose |
|---|---|
_auto_patch_mediapipe() |
Applies the MediaPipe compatibility fix on import when needed |
deep_merge() |
Merges saved config into defaults without losing missing fields |
load_config() |
Reads config.json and returns a safe merged config |
save_config() |
Writes the current config to disk |
normalize_key_sequence() |
Converts hotkey text like ctrl+shift+z into a PyAutoGUI-friendly sequence |
execute_binding() |
Executes key, hotkey, or scroll actions |
clamp() |
Keeps a value inside a range |
point_distance() |
Calculates distance between two normalized points |
landmarks_center() |
Finds the center point of all hand landmarks |
finger_extended() |
Checks whether a finger is extended |
thumb_extended() |
Special thumb-extension logic based on handedness |
classify_gesture() |
Classifies the current hand pose into a gesture label |
draw_text_box() |
Renders readable overlay labels on the preview frame |
_landmark_to_px() |
Converts normalized landmark coordinates to frame pixels |
draw_hand_skeleton() |
Draws the green skeleton overlay on top of the video |
get_category_label() |
Normalizes MediaPipe category labels |
ensure_model_downloaded() |
Downloads the hand-landmarker model if it is missing |
main() |
Launches the application |
_build_styles()→ configures the dark theme and widget styles_build_layout()→ creates the notebook and main app sections_build_preview_tab()→ builds the live preview dashboard_build_config_tab()→ builds camera, detection, and gesture settings_build_binding_rows()→ creates one binding row per gesture_add_labeled_entry()→ helper for text inputs_add_labeled_scale()→ helper for slider controls_build_calibration_tab()→ builds the calibration workspace
_load_ui_from_config()→ loads config values into the controls_gather_ui_to_config()→ collects UI values into a config dictionary_refresh_calibration_texts()→ updates the tuning summary and calibration labels_set_status()→ updates the status pill
toggle_camera()→ starts or stops the previewstart_camera()→ validates dependencies, saves config, and starts the capture threadstop_camera()→ stops the camera and releases resourcesreload_config()→ reloads from disk and repopulates the UIsave_config()→ saves current UI values toconfig.jsonrestore_defaults()→ resets the whole app to default settingsreset_calibration()→ resets only calibration values
capture_center_calibration()→ starts neutral-center capturecapture_swipe_calibration()→ starts swipe capture_draw_center_guide()→ draws the calibration guide box_refresh_live_feedback()→ shows live tuning values in the calibration panel
_create_landmarker()→ creates the MediaPipe Hand Landmarker_camera_loop()→ runs frame capture, landmark detection, drawing, and gesture classification_detect_swipe()→ detects swipe direction from motion history_stable_gesture_pass()→ requires repeated frames before firing_should_fire()→ enforces gesture cooldown_record_event()→ stores event log entries_refresh_event_log()→ renders the visible event list_trigger_gesture()→ executes the configured binding_ui_tick()→ refreshes the preview and stats on the UI thread_show_preview_image()→ fits and draws the latest frame into the canvason_close()→ shuts down the app cleanly
All persistent settings live in config.json.
| Key | Meaning |
|---|---|
camera_index |
Webcam device index |
camera_width |
Capture width |
camera_height |
Capture height |
preview_flip |
Mirrors the preview for a natural laptop-camera feel |
| Key | Meaning |
|---|---|
detection.max_num_hands |
Maximum hands to track at once |
detection.model_complexity |
Compatibility setting kept for the UI |
detection.min_detection_confidence |
Minimum confidence to accept a detection |
detection.min_tracking_confidence |
Minimum confidence to keep tracking |
| Key | Meaning |
|---|---|
gesture.stable_frames |
Number of repeated frames needed before a trigger |
gesture.cooldown_ms |
Minimum time between repeated triggers |
gesture.swipe_min_dx |
Minimum horizontal swipe distance |
gesture.swipe_min_dy |
Minimum vertical swipe distance |
gesture.swipe_min_duration_ms |
Minimum swipe duration |
gesture.swipe_max_duration_ms |
Maximum swipe duration |
gesture.pinch_threshold |
Distance threshold for pinch detection |
gesture.open_palm_min_extended |
Minimum extended fingers for open palm |
| Key | Meaning |
|---|---|
calibration.neutral_center |
Captured neutral hand center |
calibration.swipe_scale |
Swipe scaling values |
calibration.recent_center_samples |
Recent center samples during calibration |
calibration.recent_swipe_samples |
Recent swipe samples during calibration |
Every gesture binding uses the same structure:
{
"enabled": true,
"type": "key",
"value": "space"
}Supported types:
keyhotkeyscroll
Examples:
spacectrl,shift,z500
git clone https://github.com/BOSS294/InScroll.git
cd REPO
pip install -r requirements.txt
python main.py- Python 3.10 or newer
- Working webcam
- Windows desktop with PyAutoGUI support
- Internet access on first run if the model file needs to be downloaded
- Open the Configurations tab.
- Set the gesture you want to use for scrolling.
- Tune:
stable_framesgesture cooldown msswipe minimum durationswipe maximum durationswipe threshold dx/dypinch threshold
- Open the Calibration tab.
- Capture a neutral center pose.
- Calibrate swipe speed.
- Save the config.
That is the fastest way to make the app feel natural for your hand and your sitting position.
- Check whether another app is using the webcam
- Make sure the camera index is correct
- Try unplugging and reconnecting the camera
- Increase
stable_frames - Increase
cooldown_ms - Raise the detection confidence slightly
- Tighten swipe thresholds
- Improve lighting
- Keep your hand inside the guide during calibration
- Re-capture neutral center
- Recalibrate swipe timing
- Match the camera angle to your hand position
- Reinstall dependencies with
pip install -r requirements.txt - Run the included compatibility patch if needed
- Make sure the
models/hand_landmarker.taskfile exists
The repository also includes a browser-based version under V1/.
It contains:
V1/main.pyV1/app.pyV1/config.jsonV1/requirements.txtV1/templates/index.htmlV1/static/app.jsV1/static/style.css
That build uses a local Flask dashboard with MediaPipe Hands in the browser and local PyAutoGUI action triggering.
Possible upgrades for the next version:
- Custom gesture training
- Profile presets
- Per-app bindings
- Macro chains
- Gesture recording
- Better analytics
- Visual onboarding flow
- Export/import settings
- Cross-platform support
- Optional gesture sound feedback
- Motion smoothing improvements
- Multi-camera support
Contributions are welcome.
Good places to help:
- gesture recognition
- calibration refinement
- UI polish
- testing and bug fixes
- documentation
- cross-platform work
- packaging and installer support
MIT License





