malansi commited on
Commit
f43b4be
·
verified ·
1 Parent(s): 52a6747

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

Browse files
Files changed (1) hide show
  1. code/quick_calibration.py +311 -0
code/quick_calibration.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This is the best model it worked perfect for me and safer after callibrration
2
+ # quick_calibration.py
3
+ # Fine-tune last layer only for new user — 2 min recording + 30s training
4
+
5
+ import asyncio
6
+ import myo
7
+ from myo import ClassifierMode, EMGMode, IMUMode
8
+ import torch
9
+ import torch.nn as nn
10
+ import numpy as np
11
+ from scipy import signal
12
+ from collections import deque, Counter
13
+ import time
14
+
15
+ # ── Config ──
16
+ FS = 200
17
+ WIN_SAMPLES = 150
18
+ STEP = 75
19
+ N_CHANNELS = 8
20
+ N_CLASSES = 10
21
+ DEVICE = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
22
+
23
+ MODEL_PATH = "hand_module/models/best_model_hand.pt"
24
+ NORM_MEAN = np.load("hand_module/models/hand_norm_mean.npy")
25
+ NORM_STD = np.load("hand_module/models/hand_norm_std.npy")
26
+
27
+ GESTURE_NAMES = {
28
+ 0: 'rest', 1: 'fist', 2: 'grasp',
29
+ 3: 'index', 4: 'middle', 5: 'ring',
30
+ 6: 'pinky', 7: 'thumb',
31
+ 8: 'wrist_rotate_out', 9: 'wrist_rotate_in',
32
+ }
33
+
34
+ GESTURE_INSTRUCTIONS = {
35
+ 0: 'Relax your hand completely',
36
+ 1: 'Close ALL fingers into a tight fist',
37
+ 2: 'Curl fingers — like holding a cup',
38
+ 3: 'Extend INDEX finger only',
39
+ 4: 'Extend MIDDLE finger only',
40
+ 5: 'Extend RING finger only',
41
+ 6: 'Extend PINKY finger only',
42
+ 7: 'Extend THUMB only',
43
+ 8: 'Rotate wrist — palm faces DOWN',
44
+ 9: 'Rotate wrist — palm faces UP',
45
+ }
46
+
47
+ CALIBRATION_REPS = 3
48
+ HOLD_SECONDS = 5
49
+ COUNTDOWN_SECONDS = 3
50
+ FINETUNE_EPOCHS = 30
51
+
52
+
53
+ # ── Model ──
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, n_classes)
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
+ # ── Load Model ──
86
+ model = EMG_CNN_LSTM(N_CHANNELS, N_CLASSES).to(DEVICE)
87
+ model.load_state_dict(torch.load(MODEL_PATH, map_location=DEVICE))
88
+ model.eval()
89
+ print(f"✅ Model loaded — Device: {DEVICE}")
90
+
91
+ # ── Filters ──
92
+ nyq = FS / 2
93
+ b, a = signal.butter(4, [20/nyq, 90/nyq], btype='band')
94
+ bn, an = signal.iirnotch(50, Q=30, fs=FS)
95
+
96
+
97
+ # ── State ──
98
+ class State:
99
+ emg_buffer = deque(maxlen=WIN_SAMPLES)
100
+ is_recording = False
101
+ recorded_emg = []
102
+ calibrated = False
103
+ pred_history = deque(maxlen=5)
104
+ last_pred = 0
105
+ last_print = 0
106
+
107
+ STATE = State()
108
+
109
+
110
+ def preprocess(window):
111
+ window = signal.filtfilt(b, a, window, axis=0)
112
+ window = signal.filtfilt(bn, an, window, axis=0)
113
+ window = (window - NORM_MEAN) / NORM_STD
114
+ return window
115
+
116
+
117
+ def predict(window):
118
+ w = preprocess(window.copy())
119
+ x = torch.tensor(w.T.copy(), dtype=torch.float32).unsqueeze(0).to(DEVICE)
120
+ with torch.no_grad():
121
+ probs = torch.softmax(model(x), dim=1)[0]
122
+ conf = probs.max().item()
123
+ pred = probs.argmax().item()
124
+ return pred, conf
125
+
126
+
127
+ # ── Myo Client ──
128
+ class CalibrationClient(myo.MyoClient):
129
+ async def on_emg_data(self, emg: myo.EMGData):
130
+ for sample in [emg.sample1, emg.sample2]:
131
+ STATE.emg_buffer.append(list(sample))
132
+ if STATE.is_recording:
133
+ STATE.recorded_emg.append(list(sample))
134
+
135
+ async def on_imu_data(self, _): pass
136
+ async def on_classifier_event(self, _): pass
137
+ async def on_aggregated_data(self, _): pass
138
+ async def on_emg_data_aggregated(self, _): pass
139
+ async def on_fv_data(self, _): pass
140
+ async def on_motion_event(self, _): pass
141
+
142
+
143
+ async def countdown(seconds, msg):
144
+ for i in range(seconds, 0, -1):
145
+ print(f"\r ⏳ {msg} — {i}s ", end='', flush=True)
146
+ await asyncio.sleep(1)
147
+ print(f"\r ✅ GO! ")
148
+
149
+
150
+ async def calibrate():
151
+ print("\n" + "═"*60)
152
+ print(" QUICK CALIBRATION")
153
+ print("═"*60)
154
+ print(f"\n {len(GESTURE_NAMES)} gestures × {CALIBRATION_REPS} reps × {HOLD_SECONDS}s")
155
+ print(f" Total recording: ~{len(GESTURE_NAMES)*CALIBRATION_REPS*8//60} minutes")
156
+ print(f" Fine-tuning: ~30 seconds\n")
157
+ print(" Starting in 5 seconds...")
158
+ await asyncio.sleep(5)
159
+
160
+ all_X, all_y = [], []
161
+
162
+ for gesture_id in range(N_CLASSES):
163
+ name = GESTURE_NAMES[gesture_id]
164
+ instruction = GESTURE_INSTRUCTIONS[gesture_id]
165
+
166
+ print(f"\n{'─'*60}")
167
+ print(f" GESTURE: {name.upper()}")
168
+ print(f" {instruction}")
169
+
170
+ for rep in range(1, CALIBRATION_REPS + 1):
171
+ print(f"\n Rep {rep}/{CALIBRATION_REPS}")
172
+ await countdown(COUNTDOWN_SECONDS, f"Prepare for {name}")
173
+ print(f" 🟢 HOLD STEADY!\n")
174
+
175
+ STATE.recorded_emg = []
176
+ STATE.is_recording = True
177
+
178
+ start = time.time()
179
+ while time.time() - start < HOLD_SECONDS:
180
+ await asyncio.sleep(0.1)
181
+ elapsed = time.time() - start
182
+ bar = '█' * int(elapsed/HOLD_SECONDS*20) + '░' * (20-int(elapsed/HOLD_SAMPLES*20)) if False else ''
183
+ print(f"\r Recording... {elapsed:.1f}s/{HOLD_SECONDS}s "
184
+ f"({len(STATE.recorded_emg)} samples)",
185
+ end='', flush=True)
186
+
187
+ STATE.is_recording = False
188
+ print()
189
+
190
+ emg = np.array(STATE.recorded_emg, dtype=np.float32)
191
+ if len(emg) < WIN_SAMPLES:
192
+ continue
193
+
194
+ # Extract windows
195
+ j = 0
196
+ while j + WIN_SAMPLES <= len(emg):
197
+ window = preprocess(emg[j:j+WIN_SAMPLES].copy())
198
+ all_X.append(window.T.copy())
199
+ all_y.append(gesture_id)
200
+ j += STEP
201
+
202
+ print(f" ✅ {len(all_X)} total windows collected")
203
+ await asyncio.sleep(1)
204
+
205
+ # ── Fine-tune last layer only ──
206
+ print(f"\n{'═'*60}")
207
+ print(f" FINE-TUNING on your data...")
208
+ print(f" Windows: {len(all_X)}")
209
+
210
+ # Freeze all layers except last fc layer
211
+ for param in model.parameters():
212
+ param.requires_grad = False
213
+ for param in model.fc[-1].parameters():
214
+ param.requires_grad = True
215
+
216
+ model.train()
217
+
218
+ X_tensor = torch.tensor(np.array(all_X), dtype=torch.float32).to(DEVICE)
219
+ y_tensor = torch.tensor(np.array(all_y), dtype=torch.long).to(DEVICE)
220
+
221
+ optimizer = torch.optim.Adam(
222
+ filter(lambda p: p.requires_grad, model.parameters()),
223
+ lr=1e-3
224
+ )
225
+ criterion = nn.CrossEntropyLoss()
226
+
227
+ dataset = torch.utils.data.TensorDataset(X_tensor, y_tensor)
228
+ loader = torch.utils.data.DataLoader(dataset, batch_size=32, shuffle=True)
229
+
230
+ for epoch in range(1, FINETUNE_EPOCHS + 1):
231
+ epoch_loss = 0
232
+ correct = total = 0
233
+ for xb, yb in loader:
234
+ optimizer.zero_grad()
235
+ out = model(xb)
236
+ loss = criterion(out, yb)
237
+ loss.backward()
238
+ optimizer.step()
239
+ epoch_loss += loss.item()
240
+ correct += (out.argmax(1) == yb).sum().item()
241
+ total += len(yb)
242
+
243
+ if epoch % 10 == 0:
244
+ acc = correct / total
245
+ print(f" Epoch {epoch:2d}/{FINETUNE_EPOCHS} | "
246
+ f"Loss: {epoch_loss/len(loader):.4f} | Acc: {acc:.3f}")
247
+
248
+ model.eval()
249
+ STATE.calibrated = True
250
+
251
+ print(f"\n ✅ Calibration complete!")
252
+ print(f" Model fine-tuned on YOUR data")
253
+ print("═"*60)
254
+
255
+
256
+ async def realtime():
257
+ print("\n" + "═"*60)
258
+ print(" REAL-TIME INFERENCE")
259
+ print(" Try any gesture!")
260
+ print(" Press Ctrl+C to stop")
261
+ print("═"*60 + "\n")
262
+
263
+ count = 0
264
+ while True:
265
+ await asyncio.sleep(0.05)
266
+ count += 1
267
+ if count % 10 != 0:
268
+ continue
269
+ if len(STATE.emg_buffer) < WIN_SAMPLES:
270
+ continue
271
+
272
+ window = np.array(STATE.emg_buffer, dtype=np.float32)
273
+ pred, conf = predict(window)
274
+
275
+ STATE.pred_history.append(pred)
276
+ top_pred = Counter(STATE.pred_history).most_common(1)[0][0]
277
+
278
+ now = time.time()
279
+ if top_pred != STATE.last_pred or (now - STATE.last_print) > 1.5:
280
+ name = GESTURE_NAMES[top_pred]
281
+ print(f"\r 🖐 {name:<22} (conf: {conf:.0%}) ",
282
+ end='', flush=True)
283
+ STATE.last_pred = top_pred
284
+ STATE.last_print = now
285
+
286
+
287
+ async def main():
288
+ print("🔍 Scanning for Myo Armband...")
289
+ client = await CalibrationClient.with_device()
290
+ print(f"✅ Connected: {client.device.name}")
291
+
292
+ await client.setup(
293
+ classifier_mode=ClassifierMode.DISABLED,
294
+ emg_mode=EMGMode.SEND_EMG,
295
+ imu_mode=IMUMode.SEND_DATA,
296
+ )
297
+ await client.start()
298
+
299
+ try:
300
+ await calibrate()
301
+ await realtime()
302
+ except (KeyboardInterrupt, EOFError):
303
+ pass
304
+ finally:
305
+ print("\n\n Stopping...")
306
+ await client.stop()
307
+ await client.disconnect()
308
+
309
+
310
+ if __name__ == "__main__":
311
+ asyncio.run(main())