Spaces:
Sleeping
Sleeping
| import json | |
| import os | |
| import urllib.request | |
| from collections import deque | |
| import cv2 | |
| import gradio as gr | |
| import numpy as np | |
| import onnxruntime as ort | |
| ONNX_PATH = "mlp_asl.onnx" | |
| CLASSES_PATH = "mlp_classes.json" | |
| HAND_MODEL_PATH = "hand_landmarker.task" | |
| HAND_MODEL_URL = ("https://storage.googleapis.com/mediapipe-models/hand_landmarker/" | |
| "hand_landmarker/float16/1/hand_landmarker.task") | |
| SMOOTH_WINDOW = 10 | |
| CONF_THRESH = 0.65 | |
| def ensure_hand_model(path=HAND_MODEL_PATH): | |
| if not os.path.exists(path): | |
| urllib.request.urlretrieve(HAND_MODEL_URL, path) | |
| return path | |
| def build_landmarker(model_path, det_conf=0.75, track_conf=0.6): | |
| import mediapipe as mp | |
| from mediapipe.tasks.python import BaseOptions | |
| from mediapipe.tasks.python.vision import ( | |
| HandLandmarker, HandLandmarkerOptions, RunningMode, | |
| ) | |
| options = HandLandmarkerOptions( | |
| base_options=BaseOptions(model_asset_path=model_path), | |
| running_mode=RunningMode.IMAGE, | |
| num_hands=1, | |
| min_hand_detection_confidence=det_conf, | |
| min_hand_presence_confidence=det_conf, | |
| min_tracking_confidence=track_conf, | |
| ) | |
| return HandLandmarker.create_from_options(options), mp | |
| with open(CLASSES_PATH) as f: | |
| CLASS_NAMES = json.load(f) | |
| SESSION = ort.InferenceSession(ONNX_PATH, providers=["CPUExecutionProvider"]) | |
| INPUT_NAME = SESSION.get_inputs()[0].name | |
| LANDMARKER, MP = build_landmarker(ensure_hand_model()) | |
| HISTORY = deque(maxlen=SMOOTH_WINDOW) | |
| def normalize_landmarks(pts): | |
| wrist = pts[0] | |
| centered = pts - wrist | |
| scale = np.linalg.norm(centered[9]) | |
| if scale < 1e-6: | |
| scale = 1.0 | |
| return (centered / scale).flatten().astype(np.float32) | |
| def softmax(logits): | |
| z = logits - logits.max() | |
| e = np.exp(z) | |
| return e / e.sum() | |
| def smooth_prediction(history): | |
| scores = {} | |
| for label, conf in history: | |
| scores[label] = scores.get(label, 0.0) + conf | |
| total = sum(scores.values()) | |
| if total <= 0: | |
| return "...", 0.0 | |
| winner = max(scores, key=scores.get) | |
| smooth_conf = scores[winner] / total | |
| if smooth_conf < CONF_THRESH: | |
| return "...", smooth_conf | |
| return winner, smooth_conf | |
| def draw_landmarks(frame, lms, w, h, color=(0, 255, 0)): | |
| for lm in lms: | |
| cv2.circle(frame, (int(lm.x * w), int(lm.y * h)), 4, color, -1) | |
| def draw_confidence_bar(frame, conf, x, y, width=240, height=24): | |
| cv2.rectangle(frame, (x, y), (x + width, y + height), (60, 60, 60), -1) | |
| fill = int(width * conf) | |
| green = int(80 + 175 * conf) | |
| cv2.rectangle(frame, (x, y), (x + fill, y + height), (0, green, 0), -1) | |
| cv2.rectangle(frame, (x, y), (x + width, y + height), (200, 200, 200), 1) | |
| cv2.putText(frame, f"{conf * 100:.1f}%", (x + width + 10, y + height - 4), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) | |
| def predict(frame): | |
| if frame is None: | |
| return None | |
| frame = np.asarray(frame) | |
| if frame.ndim == 2: | |
| frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2RGB) | |
| if frame.shape[2] == 4: | |
| frame = frame[:, :, :3] | |
| frame = np.ascontiguousarray(frame, dtype=np.uint8).copy() | |
| h, w = frame.shape[:2] | |
| try: | |
| mp_image = MP.Image(image_format=MP.ImageFormat.SRGB, data=frame) | |
| result = LANDMARKER.detect(mp_image) | |
| except Exception as exc: | |
| cv2.putText(frame, f"err: {type(exc).__name__}", (20, 44), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 80, 80), 2) | |
| return frame | |
| if not result.hand_landmarks: | |
| HISTORY.clear() | |
| cv2.putText(frame, "No hand detected", (20, 44), | |
| cv2.FONT_HERSHEY_SIMPLEX, 1.1, (255, 80, 80), 3) | |
| return frame | |
| lms = result.hand_landmarks[0] | |
| pts = np.array([[lm.x, lm.y, lm.z] for lm in lms], dtype=np.float32) | |
| logits = SESSION.run(None, {INPUT_NAME: normalize_landmarks(pts).reshape(1, -1)})[0][0] | |
| probs = softmax(logits) | |
| idx = int(probs.argmax()) | |
| HISTORY.append((CLASS_NAMES[idx], float(probs[idx]))) | |
| label, conf = smooth_prediction(HISTORY) | |
| draw_landmarks(frame, lms, w, h) | |
| color = (0, 255, 0) if label != "..." else (255, 180, 0) | |
| cv2.putText(frame, label, (20, 70), | |
| cv2.FONT_HERSHEY_SIMPLEX, 2.2, color, 5) | |
| draw_confidence_bar(frame, conf, 20, 90) | |
| return frame | |
| DESCRIPTION = ( | |
| "Show an ASL letter (A–Z) or digit (0–9) to your webcam and hold steady. " | |
| "Dataset: ASL-HG by Pranto et al. (2026) — " | |
| "https://www.sciencedirect.com/science/article/pii/S2352340926000454 | " | |
| "Model by Doruk Doğular (nocontextdoruk)." | |
| ) | |
| with gr.Blocks(title="ASL Hand Gesture Recognizer") as demo: | |
| gr.Markdown("# ASL Hand Gesture Recognizer") | |
| gr.Markdown(DESCRIPTION) | |
| with gr.Row(): | |
| webcam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="Webcam") | |
| output = gr.Image(label="Prediction", type="numpy") | |
| webcam.stream(predict, inputs=webcam, outputs=output, | |
| stream_every=0.15, concurrency_limit=30) | |
| demo.queue() | |
| if __name__ == "__main__": | |
| demo.launch() | |