File size: 8,116 Bytes
1f4e4ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
200
201
import cv2
import mediapipe as mp
import numpy as np
from typing import List, Tuple, Optional

# Try to import TensorFlow, handle gracefully if not available
try:
    import tensorflow as tf
    TF_AVAILABLE = True
except ImportError:
    TF_AVAILABLE = False
    print("Warning: TensorFlow not available. Some features may be limited.")

class MediaPipeLandmarksModel:
    def __init__(self):
        self.mp_hands = mp.solutions.hands
        self.mp_drawing = mp.solutions.drawing_utils
        self.hands = self.mp_hands.Hands(
            static_image_mode=False, 
            max_num_hands=2, 
            min_detection_confidence=0.5,
            min_tracking_confidence=0.5
        )
        self.sequence = []
        self.sequence_length = 15  # Reduced for faster response

    def predict_landmarks(self, image: np.ndarray) -> Optional[List[Tuple[float, float, float]]]:
        """Extract hand landmarks from image."""
        image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        results = self.hands.process(image_rgb)

        if results.multi_hand_landmarks:
            landmarks = []
            for hand_landmarks in results.multi_hand_landmarks:
                for landmark in hand_landmarks.landmark:
                    landmarks.append((landmark.x, landmark.y, landmark.z))
            return landmarks
        return None

    def draw_landmarks(self, image: np.ndarray, landmarks) -> np.ndarray:
        """Draw hand landmarks on image."""
        if landmarks:
            results = self.hands.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
            if results.multi_hand_landmarks:
                for hand_landmarks in results.multi_hand_landmarks:
                    self.mp_drawing.draw_landmarks(
                        image, hand_landmarks, self.mp_hands.HAND_CONNECTIONS)
        return image

    def normalize_landmarks(self, landmarks: List[Tuple[float, float, float]]) -> np.ndarray:
        """Normalize landmarks relative to wrist position."""
        if not landmarks:
            return np.zeros((21, 3))
        
        landmarks_array = np.array(landmarks)
        
        # Get wrist position (first landmark)
        wrist = landmarks_array[0]
        
        # Normalize relative to wrist
        normalized = landmarks_array - wrist
        
        # Scale by hand size (distance from wrist to middle finger tip)
        if len(landmarks_array) >= 12:
            hand_size = np.linalg.norm(landmarks_array[12] - wrist)
            if hand_size > 0:
                normalized = normalized / hand_size
        
        return normalized.flatten()

    def extract_features(self, landmarks: List[Tuple[float, float, float]]) -> np.ndarray:
        """Extract features from landmarks for model input."""
        if not landmarks:
            return np.zeros(63)  # 21 landmarks * 3 coordinates
        
        # Ensure we have exactly 21 landmarks (one hand)
        if len(landmarks) > 21:
            landmarks = landmarks[:21]  # Take first hand only
        elif len(landmarks) < 21:
            # Pad with zeros if we have fewer landmarks
            while len(landmarks) < 21:
                landmarks.append((0.0, 0.0, 0.0))
        
        # Convert to numpy array
        landmarks_array = np.array(landmarks)
        
        # Get wrist position (first landmark)
        wrist = landmarks_array[0]
        
        # Normalize relative to wrist
        normalized = landmarks_array - wrist
        
        # Scale by hand size (distance from wrist to middle finger tip)
        hand_size = np.linalg.norm(landmarks_array[12] - wrist) if len(landmarks_array) >= 12 else 1.0
        if hand_size > 0:
            normalized = normalized / hand_size
            
        # Flatten to 63 dimensions (21 landmarks * 3 coordinates)
        features = normalized.flatten()
        
        # Ensure we have exactly 63 dimensions
        if len(features) > 63:
            features = features[:63]
        elif len(features) < 63:
            features = np.pad(features, (0, 63 - len(features)))
            
        return features

    def update_sequence(self, landmarks: List[Tuple[float, float, float]]) -> np.ndarray:
        """Update sequence for temporal modeling."""
        features = self.extract_features(landmarks)
        self.sequence.append(features)
        
        # Keep only last sequence_length frames
        if len(self.sequence) > self.sequence_length:
            self.sequence.pop(0)
        
        # Pad sequence if needed
        if len(self.sequence) < self.sequence_length:
            padding = np.zeros((self.sequence_length - len(self.sequence), features.shape[0]))
            sequence_array = np.vstack([padding, np.array(self.sequence)])
        else:
            sequence_array = np.array(self.sequence)
        
        return sequence_array

    def reset_sequence(self):
        """Reset the sequence buffer."""
        self.sequence = []

class SignLanguagePredictor:
    def __init__(self, model_path: str, labels_path: str):
        self.landmark_model = MediaPipeLandmarksModel()
        
        # Load labels
        try:
            with open(labels_path, 'r') as f:
                self.labels = [line.strip() for line in f.readlines()]
        except FileNotFoundError:
            print(f"Warning: Labels file not found at {labels_path}")
            self.labels = ["Unknown"]
        
        # Initialize TFLite interpreter if available
        if TF_AVAILABLE:
            try:
                self.interpreter = tf.lite.Interpreter(model_path=model_path)
                self.interpreter.allocate_tensors()
                self.input_details = self.interpreter.get_input_details()
                self.output_details = self.interpreter.get_output_details()
                self.model_loaded = True
            except Exception as e:
                print(f"Warning: Could not load model: {e}")
                self.model_loaded = False
        else:
            print("Warning: TensorFlow not available. Running in landmark-only mode.")
            self.model_loaded = False

    def predict(self, image: np.ndarray) -> Tuple[str, float]:
        """Predict sign from image using static prediction for compatibility."""
        # Use static prediction instead of temporal for now to avoid dimension issues
        return self.predict_static(image)

    def predict_static(self, image: np.ndarray) -> Tuple[str, float]:
        """Predict sign from static image (single frame)."""
        landmarks = self.landmark_model.predict_landmarks(image)
        
        if not landmarks:
            return "No hand detected", 0.0
            
        if not self.model_loaded:
            return "Hand detected (Model not available)", 0.5
            
        try:
            # Extract features
            features = self.landmark_model.extract_features(landmarks)
            
            # Get expected input shape from model
            input_shape = self.input_details[0]['shape']
            
            # Reshape features to match expected input shape
            if len(input_shape) == 2:  # If model expects 2D input (batch_size, features)
                input_data = features.reshape(1, -1).astype(np.float32)
            elif len(input_shape) == 3:  # If model expects 3D input (batch_size, time_steps, features)
                input_data = features.reshape(1, 1, -1).astype(np.float32)
            else:
                raise ValueError(f"Unexpected input shape: {input_shape}")
            
            # Run inference
            self.interpreter.set_tensor(self.input_details[0]['index'], input_data)
            self.interpreter.invoke()
            
            # Get prediction
            output = self.interpreter.get_tensor(self.output_details[0]['index'])
            predicted_class = np.argmax(output[0])
            confidence = float(np.max(output[0]))
            
            return self.labels[predicted_class], confidence
        except Exception as e:
            print(f"Error during static prediction: {e}")
            return "Error during prediction", 0.0