Skip to content
Aman SinghWorkshop
AmanSinghNp / ASL-Interpreter-AI

ASL Interpreter

A fingerspelling prototype: a desktop app that tries to read American Sign Language letters from a webcam and type them out. MediaPipe finds 21 points on the hand, those points become 42 numbers, and a 17,400-parameter network picks one of 24 letters. The pipeline runs end to end, from data capture to a 24.5 KB TensorFlow Lite model.

Tested on synthetic input only. Every number here comes from a model trained on synthetic data, and the screenshots use a blank frame or invented values. Recognition on real signing hands hasn't been validated: the repo ships no trained model or hand data.

1,990Lines of Python
24Letters · A–Y, no J or Z
17,400Model parameters
24.5 KBQuantised TFLite
35 / 35Tests, pinned deps
42Features per frame
Demo

The app's own window

OpenCV window with a grey frame, header reading Sign: No hand detected, empty word bar at the bottom
Real run, fake camera. asl_app.main() ran headless with a blank 1280 × 720 frame in place of the webcam. It loaded the TFLite model and 24 classes, and rendered the "No hand detected" state.
Same window with Sign: B (97%), a full green confidence bar, a large faded letter B and the word HELLO
Mock state, not a recognition. The app's own draw_ui_overlay called with invented values: B at 97%, word "HELLO", confirmation flash on. As it happens, the app can't type the double L in HELLO without lowering the hand.

A genuine recognition screenshot needs a model trained on real hand images and a signing hand in front of the camera. The repo ships neither: models and data are gitignored, and an earlier committed model was missing its graph file, so it could never load.

Features

From hand shape to 42 numbers

48121620517 0 · wrist = origin 9 scale = |0 → 9| x′ = (x − x₀) / scale y′ = (y − y₀) / scale 21 × (x′, y′) = 42 features · z dropped
Normalisation. Moving the wrist to the origin removes position; dividing by the wrist-to-knuckle length removes distance from the camera. Rotation is not removed.

Why landmarks instead of pixels. MediaPipe has already done the hard vision work, so the classifier only sees geometry. Background and skin tone drop out, and a small dense network is enough. Training and the live app call the same normalisation function, which keeps the two from drifting apart.

Invariance, measured. Scaling the hand 2× changed no feature. Rotating it 20° changed features by up to 0.75, and mirroring it (a left hand) by up to 1.48. A commit titled "Fix rotation-invariant normalization" made the scale Euclidean; it did not add rotation invariance.

# utils.py:84-113, condensed
wrist = landmark_list[0]
normalized = [{'x': lm['x'] - wrist['x'],
               'y': lm['y'] - wrist['y']} for lm in landmark_list]
mcp = landmark_list[9]
dist = np.sqrt((mcp['x'] - wrist['x'])**2 + (mcp['y'] - wrist['y'])**2)
scale = max(float(dist), 0.01)
features = [lm['x'] / scale for lm in normalized] + \
           [lm['y'] / scale for lm in normalized]
Pipeline

Capture, train, export, run

Offline data_collection.py100 samples / letter process_dataset.pyimage folders A/ … Y/ asl_data.csvletter + 42 floats train_model.py80/20 stratified · Adam 1e-3≤ 50 epochs · early stopping .keras255 KB+ classes.txt .tflite24.5 KBdynamic range LIVE · asl_app.py · EVERY FRAME Webcammirrored MediaPipe Hands1 hand · 21 points normalise(1, 42) classifierTFLite → .keras → SavedModel loaded at start-up conf> 0.7? yes 5 ina row? commit letterif ≠ last letter · flash 0.4 sclear buffer no "Low confidence"clear buffer, forget last letter no hand in frameclear buffer, forget last letter Keys: Space adds a space · Backspace deletes · C clears · Q quits. Only "forget last letter" lets the same letter be typed twice.
Two gates before a letter is typed. Confidence above 0.7, then five matching frames in a row. That stops flicker, but it also means a repeated letter needs the hand lowered, confidence to drop, or a space in between.
Model

A small network is enough for geometry

Parameters per layer
From model.summary(); 17,016 trainable plus 384 batch-norm statistics.
# scripts/train_model.py:78-95
model = keras.Sequential([
    layers.Input(shape=(42,)),
    layers.Dense(128, activation='relu'), layers.BatchNormalization(), layers.Dropout(0.3),
    layers.Dense(64, activation='relu'),  layers.BatchNormalization(), layers.Dropout(0.3),
    layers.Dense(32, activation='relu'),  layers.Dropout(0.2),
    layers.Dense(num_classes, activation='softmax')])
model.compile(optimizer=keras.optimizers.Adam(learning_rate=0.001),
              loss='sparse_categorical_crossentropy', metrics=['accuracy'])
255 KB.keras file
24.5 KB.tflite file
~15 msKeras predict call
~0.002 msTFLite call

Sizes and call times come from a model trained on synthetic data during this review, on an Apple Silicon Mac. They measure the classifier call only, not MediaPipe or the camera, and are indicative.

Tests

What runs, and where

Test results by environment
Skipped tests need a trained model. The pinned run used a model trained on synthetic data.