File size: 6,573 Bytes
dc79c99
 
 
 
 
 
 
c072565
dc79c99
c072565
dc79c99
c072565
 
 
 
 
 
dc79c99
c072565
 
dc79c99
c072565
dc79c99
c072565
dc79c99
 
 
 
 
 
 
 
 
 
 
 
 
 
c072565
dc79c99
 
c072565
dc79c99
 
c072565
dc79c99
c072565
dc79c99
 
c072565
 
 
236535c
c072565
 
 
 
 
 
dc79c99
c072565
 
 
ac04435
236535c
c072565
dc79c99
c072565
dc79c99
 
 
c072565
236535c
 
 
c072565
236535c
 
 
ac04435
 
 
c072565
 
 
75ada83
c072565
236535c
 
c072565
236535c
c072565
 
 
ac04435
c072565
 
 
ac04435
c072565
 
 
 
 
 
236535c
 
75ada83
ea2e3bd
c072565
236535c
 
 
 
 
 
 
 
 
 
 
c072565
236535c
 
c072565
 
ac04435
236535c
c072565
236535c
 
 
c072565
ac04435
c072565
 
 
 
 
 
ac04435
 
 
ea2e3bd
 
c072565
ac04435
c072565
 
ea2e3bd
 
 
c072565
 
 
 
 
 
 
ac04435
c072565
 
 
 
 
 
 
 
 
 
 
 
 
ea2e3bd
c072565
 
 
236535c
c072565
 
 
236535c
c072565
75ada83
 
 
 
 
236535c
c072565
 
 
75ada83
c072565
ea2e3bd
75ada83
 
236535c
dc79c99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import cv2
import mediapipe as mp
import numpy as np
import torch
import torch.nn as nn
import gradio as gr

# ----------------------------
# Labels
# ----------------------------
GESTURE_LABELS = {
    0: "A", 1: "B", 2: "L", 3: "U", 4: "V", 5: "W",
    6: "Z", 7: "F", 8: "five", 9: "one", 10: "three",
    11: "two", 12: "six", 13: "seven", 14: "eight",
    15: "nine", 16: "ten", 17: "E", 18: "four",
    19: "i", 20: "k", 21: "r", 22: "zero",
    23: "m", 24: "s"
}
CONF_THRESHOLD = 0.6
BLOCK_SIZE = 10   # frames per block

# ----------------------------
# Model
# ----------------------------
class GestureNet(nn.Module):
    def __init__(self, input_size=126, num_classes=len(GESTURE_LABELS)):
        super().__init__()
        self.fc1 = nn.Linear(input_size, 256)
        self.fc2 = nn.Linear(256, 128)
        self.fc3 = nn.Linear(128, num_classes)
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(0.3)

    def forward(self, x):
        x = self.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.relu(self.fc2(x))
        x = self.dropout(x)
        return self.fc3(x)

model = GestureNet()
model.load_state_dict(torch.load("gesture_model1.pth", map_location="cpu"))
model.eval()

# ----------------------------
# MediaPipe
# ----------------------------
mp_hands = mp.solutions.hands

# ----------------------------
# Landmark extraction + prediction
# ----------------------------
def extract_coords(results):
    coords = []
    if results.multi_hand_landmarks:
        for hand_landmarks in results.multi_hand_landmarks:
            hand_coords = np.array([[lm.x, lm.y, lm.z] for lm in hand_landmarks.landmark])
            hand_coords -= hand_coords[0]
            max_val = np.max(np.linalg.norm(hand_coords, axis=1))
            if max_val > 0:
                hand_coords /= max_val
            coords.extend(hand_coords.flatten())
    return coords

def run_model(coords):
    """Return label string (either gesture or 'Unknown')"""
    if len(coords) < 126:
        coords = coords + [0.0] * (126 - len(coords))
    elif len(coords) > 126:
        coords = coords[:126]

    input_tensor = torch.tensor(coords, dtype=torch.float32).unsqueeze(0)
    with torch.no_grad():
        outputs = model(input_tensor)
        probs = torch.softmax(outputs, dim=1)
        pred_class = torch.argmax(probs, dim=1).item()
        confidence = probs[0][pred_class].item()

    if confidence >= CONF_THRESHOLD:
        return GESTURE_LABELS[pred_class]
    return "Unknown"

# ----------------------------
# Image prediction (unchanged format)
# ----------------------------
def predict_image_from_rgb(image_rgb):
    with mp_hands.Hands(static_image_mode=True, max_num_hands=2) as hands:
        results = hands.process(image_rgb)
    coords = extract_coords(results)
    if not coords:
        return "No hand detected"
    label = run_model(coords)
    # run_model returns label or "Unknown" – we can keep the simple label
    return label

# ----------------------------
# 🆕 Video prediction → build word from 10-frame blocks
# ----------------------------
def predict_video_word(video_path):
    """
    Divides video into non-overlapping blocks of BLOCK_SIZE frames.
    If every frame in a block yields the *same* valid gesture,
    that gesture is added to the output word.
    Returns the concatenated word, or 'Unknown' if no block is valid.
    """
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        return "Cannot open video file"

    frame_labels = []   # list of strings (gesture, "Unknown", or "No hand detected")

    with mp_hands.Hands(
        static_image_mode=False,
        max_num_hands=2,
        min_detection_confidence=0.5,
        min_tracking_confidence=0.5,
    ) as hands:
        while True:
            ret, frame = cap.read()
            if not ret:
                break
            frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            results = hands.process(frame_rgb)
            coords = extract_coords(results)
            if coords:
                label = run_model(coords)  # gesture name or "Unknown"
                frame_labels.append(label)
            else:
                frame_labels.append("No hand detected")

    cap.release()

    # Build word from non-overlapping blocks
    word = []
    for i in range(0, len(frame_labels) - BLOCK_SIZE + 1, BLOCK_SIZE):
        block = frame_labels[i : i + BLOCK_SIZE]
        # Check: all labels identical AND the label is not invalid
        if (len(set(block)) == 1 and
            block[0] not in ("Unknown", "No hand detected")):
            word.append(block[0])

    if word:
        return "".join(word)
    return "Unknown"

# ----------------------------
# Universal predictor
# ----------------------------
def predict_file(file_path, is_video=False):
    if file_path is None:
        return "No file uploaded"

    # Direct numpy array (image)
    if isinstance(file_path, np.ndarray):
        if len(file_path.shape) == 3 and file_path.shape[2] == 3:
            image_rgb = cv2.cvtColor(file_path, cv2.COLOR_BGR2RGB)
            return predict_image_from_rgb(image_rgb)
        else:
            return "Invalid image format"

    file_ext = str(file_path).lower()
    is_video_file = is_video or file_ext.endswith(('.mp4', '.avi', '.mov', '.mkv', '.webm'))

    if is_video_file:
        return predict_video_word(file_path)   # returns the word
    else:
        image = cv2.imread(file_path)
        if image is None:
            return "Could not read image file"
        image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        return predict_image_from_rgb(image_rgb)

# ----------------------------
# Gradio UI
# ----------------------------
with gr.Blocks(title="Hand Gesture Recognition") as app:
    gr.Markdown("# ✋ Hand Gesture Recognition (Word‑Builder)")
    gr.Markdown(
        "Upload an image or video. For videos, non‑overlapping blocks of **10 frames** "
        "are checked. If all 10 frames in a block show the same gesture, that letter is "
        "added to the output word. Blocks with mixed or invalid gestures are ignored."
    )
    
    with gr.Row():
        input_file = gr.File(
            label="Upload Image or Video",
            file_types=["image", "video"],
            type="filepath"
        )
        output_text = gr.Textbox(label="Prediction", lines=3)
    
    predict_btn = gr.Button("Predict", variant="primary")
    predict_btn.click(
        predict_file,
        inputs=input_file,
        outputs=output_text
    )

app.launch()