malansi commited on
Commit
2796429
·
verified ·
1 Parent(s): 37046a6

Update hand_module/guided_test_hand.py — 2026-07-07 18:04

Browse files
Files changed (1) hide show
  1. code/guided_test_hand.py +263 -0
code/guided_test_hand.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # guided_test_hand.py
2
+ # Real-time accuracy test for prosthetic hand model
3
+
4
+ import asyncio
5
+ import myo
6
+ from myo import ClassifierMode, EMGMode, IMUMode
7
+ import torch
8
+ import torch.nn as nn
9
+ import numpy as np
10
+ from scipy import signal
11
+ from collections import deque, Counter
12
+ import time
13
+ import json
14
+
15
+ FS = 200
16
+ WIN_SAMPLES = 150
17
+
18
+ GESTURE_NAMES = {
19
+ 0: 'rest',
20
+ 1: 'fist',
21
+ 2: 'grasp',
22
+ 3: 'index',
23
+ 4: 'middle',
24
+ 5: 'ring',
25
+ 6: 'pinky',
26
+ 7: 'thumb',
27
+ 8: 'wrist_rotate_out',
28
+ 9: 'wrist_rotate_in',
29
+ }
30
+
31
+ GESTURE_INSTRUCTIONS = {
32
+ 0: 'Relax your hand completely — do not move anything',
33
+ 1: 'Close ALL fingers into a tight fist',
34
+ 2: 'Curl fingers into a C-shape — like holding a cup',
35
+ 3: 'Extend INDEX finger only — others closed',
36
+ 4: 'Extend MIDDLE finger only — others closed',
37
+ 5: 'Extend RING finger only — others closed',
38
+ 6: 'Extend PINKY finger only — others closed',
39
+ 7: 'Extend THUMB only — others closed',
40
+ 8: 'Rotate wrist so palm faces DOWN toward table',
41
+ 9: 'Rotate wrist so palm faces UP toward you',
42
+ }
43
+
44
+ TEST_SEQUENCE = [
45
+ 0, 1, 0, 2, 0, 3, 0, 4, 0, 5,
46
+ 0, 6, 0, 7, 0, 8, 0, 9, 0, 1,
47
+ 0, 3, 0, 5, 0, 7, 0, 2, 0, 4,
48
+ ]
49
+
50
+ HOLD_SECONDS = 5
51
+ COUNTDOWN_SECONDS = 3
52
+
53
+
54
+ class EMG_CNN_LSTM(nn.Module):
55
+ def __init__(self, n_channels=8, n_classes=10):
56
+ super().__init__()
57
+ self.cnn = nn.Sequential(
58
+ nn.Conv1d(n_channels, 64, kernel_size=3, padding=1),
59
+ nn.BatchNorm1d(64), nn.ReLU(),
60
+ nn.Conv1d(64, 128, kernel_size=3, padding=1),
61
+ nn.BatchNorm1d(128), nn.ReLU(),
62
+ nn.MaxPool1d(2), nn.Dropout(0.3),
63
+ nn.Conv1d(128, 256, kernel_size=3, padding=1),
64
+ nn.BatchNorm1d(256), nn.ReLU(),
65
+ nn.MaxPool1d(2), nn.Dropout(0.3),
66
+ )
67
+ self.lstm = nn.LSTM(
68
+ input_size=256, hidden_size=128,
69
+ num_layers=2, batch_first=True,
70
+ dropout=0.3, bidirectional=True
71
+ )
72
+ self.fc = nn.Sequential(
73
+ nn.Linear(256, 128), nn.ReLU(),
74
+ nn.Dropout(0.4),
75
+ nn.Linear(128, 10)
76
+ )
77
+ def forward(self, x):
78
+ x = self.cnn(x)
79
+ x = x.permute(0, 2, 1)
80
+ x, _ = self.lstm(x)
81
+ x = x[:, -1, :]
82
+ return self.fc(x)
83
+
84
+
85
+ DEVICE = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
86
+ model = EMG_CNN_LSTM().to(DEVICE)
87
+ model.load_state_dict(torch.load('hand_module/models/best_model_hand.pt',
88
+ map_location=DEVICE))
89
+ model.eval()
90
+
91
+ NORM_MEAN = np.load('hand_module/models/hand_norm_mean.npy')
92
+ NORM_STD = np.load('hand_module/models/hand_norm_std.npy')
93
+ print(f"✅ Model + normalization loaded")
94
+
95
+ nyq = FS / 2
96
+ b, a = signal.butter(4, [20/nyq, 90/nyq], btype='band')
97
+ bn, an = signal.iirnotch(50, Q=30, fs=FS)
98
+
99
+
100
+ class State:
101
+ emg_buffer = deque(maxlen=WIN_SAMPLES)
102
+ current_truth = None
103
+ predictions_log = []
104
+ is_recording = False
105
+
106
+ STATE = State()
107
+
108
+
109
+ def predict():
110
+ if len(STATE.emg_buffer) < WIN_SAMPLES:
111
+ return None, 0.0
112
+
113
+ window = np.array(STATE.emg_buffer, dtype=np.float32)
114
+ window = signal.filtfilt(b, a, window, axis=0)
115
+ window = signal.filtfilt(bn, an, window, axis=0)
116
+ window = (window - NORM_MEAN) / NORM_STD
117
+ window = window.T.copy()
118
+
119
+ x = torch.tensor(window, dtype=torch.float32).unsqueeze(0).to(DEVICE)
120
+ with torch.no_grad():
121
+ probs = torch.softmax(model(x), dim=1)[0]
122
+ confidence = probs.max().item()
123
+ pred_label = probs.argmax().item()
124
+
125
+ return pred_label, confidence
126
+
127
+
128
+ class TestClassifier(myo.MyoClient):
129
+
130
+ async def on_emg_data(self, emg: myo.EMGData):
131
+ for sample in [emg.sample1, emg.sample2]:
132
+ STATE.emg_buffer.append(list(sample))
133
+
134
+ if not STATE.is_recording:
135
+ return
136
+
137
+ pred_label, confidence = predict()
138
+ if pred_label is None:
139
+ return
140
+
141
+ STATE.predictions_log.append({
142
+ 'truth': STATE.current_truth,
143
+ 'pred': pred_label,
144
+ 'conf': confidence,
145
+ })
146
+
147
+ async def on_imu_data(self, _): pass
148
+ async def on_classifier_event(self, _): pass
149
+ async def on_aggregated_data(self, _): pass
150
+ async def on_emg_data_aggregated(self, _): pass
151
+ async def on_fv_data(self, _): pass
152
+ async def on_motion_event(self, _): pass
153
+
154
+
155
+ async def countdown(seconds, message):
156
+ for i in range(seconds, 0, -1):
157
+ print(f"\r ⏳ {message} — {i}s ", end='', flush=True)
158
+ await asyncio.sleep(1)
159
+ print(f"\r ✅ GO! ")
160
+
161
+
162
+ async def run_test():
163
+ print("\n" + "═" * 64)
164
+ print(" GUIDED TEST — Prosthetic Hand Model")
165
+ print("═" * 64)
166
+ print(f"\n {len(TEST_SEQUENCE)} gestures | {HOLD_SECONDS}s each")
167
+ print(f" Keep your arm still — only hand/wrist moves\n")
168
+ print(" Starting in 5 seconds...")
169
+ await asyncio.sleep(5)
170
+
171
+ all_results = []
172
+
173
+ for idx, gesture_id in enumerate(TEST_SEQUENCE, 1):
174
+ name = GESTURE_NAMES[gesture_id]
175
+ instruction = GESTURE_INSTRUCTIONS[gesture_id]
176
+
177
+ print("\n" + "─" * 64)
178
+ print(f" [{idx}/{len(TEST_SEQUENCE)}] {name.upper()}")
179
+ print(f" 👉 {instruction}")
180
+
181
+ await countdown(COUNTDOWN_SECONDS, f"Prepare for {name}")
182
+ print(f"\n 🟢 Hold steady!\n")
183
+
184
+ STATE.current_truth = gesture_id
185
+ STATE.predictions_log = []
186
+ STATE.is_recording = True
187
+
188
+ start = time.time()
189
+ last_shown = None
190
+ while time.time() - start < HOLD_SECONDS:
191
+ await asyncio.sleep(0.1)
192
+ if STATE.predictions_log:
193
+ latest = STATE.predictions_log[-1]
194
+ pred = latest['pred']
195
+ if pred != last_shown:
196
+ correct = "✅" if pred == gesture_id else "❌"
197
+ print(f" {correct} {GESTURE_NAMES[pred]:<20} "
198
+ f"(conf: {latest['conf']:.0%})")
199
+ last_shown = pred
200
+
201
+ STATE.is_recording = False
202
+
203
+ preds = [p['pred'] for p in STATE.predictions_log]
204
+ if preds:
205
+ correct_count = sum(1 for p in preds if p == gesture_id)
206
+ acc = correct_count / len(preds) * 100
207
+ top3 = Counter(preds).most_common(3)
208
+ print(f"\n 📊 Accuracy: {acc:.0f}% ({correct_count}/{len(preds)})")
209
+ print(f" 📊 Top predictions: "
210
+ f"{[(GESTURE_NAMES[k], v) for k,v in top3]}")
211
+
212
+ all_results.append({'gesture': name, 'id': gesture_id, 'predictions': preds})
213
+
214
+ # ── Final Summary ──
215
+ print("\n" + "═" * 64)
216
+ print(" FINAL SUMMARY")
217
+ print("═" * 64)
218
+
219
+ gesture_stats = {}
220
+ for r in all_results:
221
+ g = r['gesture']
222
+ gid = r['id']
223
+ if g not in gesture_stats:
224
+ gesture_stats[g] = {'correct': 0, 'total': 0, 'confusions': []}
225
+ for p in r['predictions']:
226
+ gesture_stats[g]['total'] += 1
227
+ if p == gid:
228
+ gesture_stats[g]['correct'] += 1
229
+ else:
230
+ gesture_stats[g]['confusions'].append(GESTURE_NAMES[p])
231
+
232
+ print(f"\n {'Gesture':<22} {'Accuracy':<12} {'Most Confused With'}")
233
+ print(" " + "─" * 55)
234
+ for g, stats in gesture_stats.items():
235
+ acc = stats['correct'] / stats['total'] * 100 if stats['total'] else 0
236
+ confusion = Counter(stats['confusions']).most_common(1)
237
+ conf_str = f"{confusion[0][0]} ({confusion[0][1]}x)" if confusion else "—"
238
+ print(f" {g:<22} {acc:>5.0f}% {conf_str}")
239
+
240
+ with open('hand_module/test_results_hand.json', 'w') as f:
241
+ json.dump(all_results, f, indent=2)
242
+ print(f"\n 💾 Saved: hand_module/test_results_hand.json")
243
+ print("═" * 64)
244
+
245
+
246
+ async def main():
247
+ print("🔍 Scanning for Myo Armband...")
248
+ client = await TestClassifier.with_device()
249
+ print(f"✅ Connected: {client.device.name}")
250
+
251
+ await client.setup(
252
+ classifier_mode=ClassifierMode.DISABLED,
253
+ emg_mode=EMGMode.SEND_EMG,
254
+ imu_mode=IMUMode.SEND_DATA,
255
+ )
256
+ await client.start()
257
+ await run_test()
258
+ await client.stop()
259
+ await client.disconnect()
260
+
261
+
262
+ if __name__ == "__main__":
263
+ asyncio.run(main())