Source code for intan.ml._ml_utilities

# intan/ml_utilities.py

import numpy as np
import time
import threading
import queue

[docs] class EMGRealTimePredictor: def __init__( self, device, model, pca, mean, std, label_names, cue_df=None, buffer_sec=1, window_ms=250, step_sec=1, window_offset_samples=0, feature_list=None ): self.device = device self.model = model self.pca = pca self.mean = mean self.std = std self.label_names = label_names self.cue_df = cue_df self.buffer_sec = buffer_sec self.window_ms = window_ms self.step_sec = step_sec self.window_offset_samples = window_offset_samples self.feature_list = feature_list or [ 'mean_absolute_value', 'zero_crossings', 'slope_sign_changes', 'waveform_length', 'root_mean_square' ] self.fs = self.device.sample_rate self.total = 0 self.correct = 0 self.stop_event = threading.Event() self.print_queue = queue.Queue()
[docs] def apply_filters(self, emg): from intan.processing import notch_filter, bandpass_filter, lowpass_filter, rectify emg = notch_filter(emg, self.fs, 60) emg = bandpass_filter(emg, 20, 450, self.fs) emg = rectify(emg) return lowpass_filter(emg, 5, self.fs)
[docs] def extract_and_classify(self, window, already_filtered=True): from intan.processing import extract_features if not already_filtered: window = self.apply_filters(window) feats = extract_features(window, self.feature_list) feats = (feats - self.mean) / self.std if np.any(np.isnan(feats)): return "NaN", 0.0, -1 feats = self.pca.transform(feats.reshape(1, -1)) pred = self.model.predict(feats, verbose=0) gesture_idx = np.argmax(pred) gesture_name = self.label_names[gesture_idx] confidence = np.max(pred) return gesture_name, confidence, gesture_idx
[docs] def get_label_for_sample(self, sample_idx): if self.cue_df is None: return None for i in range(len(self.cue_df) - 1): start = self.cue_df.loc[i, "Sample"] end = self.cue_df.loc[i + 1, "Sample"] label = str(self.cue_df.loc[i, "Label"]) if label.lower() != "none" and start <= sample_idx < end: return label return None
[docs] def run_prediction_loop(self, background=True): if background: thread = threading.Thread(target=self._prediction_worker, daemon=True) thread.start() return thread else: self._prediction_worker()
def _prediction_worker(self): buffer_samples = int(self.buffer_sec * self.fs) window_samples = int(self.window_ms * self.fs / 1000) while not self.stop_event.is_set(): time.sleep(self.step_sec) full_buffer = self.device.get_latest_window(self.buffer_sec * 1000) if full_buffer.shape[1] != buffer_samples: self.print_queue.put(f"Buffer size mismatch: expected {buffer_samples}, got {full_buffer.shape[1]}") continue filtered_buffer = self.apply_filters(full_buffer) start = self.window_offset_samples end = start + window_samples if start < 0 or end > buffer_samples: self.print_queue.put(f"Window out of bounds: start {start}, end {end}, buffer size {buffer_samples}") continue window = filtered_buffer[:, start:end] if window.shape[1] != window_samples: self.print_queue.put(f"Window size mismatch: expected {window_samples}, got {window.shape[1]}") continue if np.any(np.isnan(window)): self.print_queue.put("NaN detected in window, skipping prediction.") continue gesture_name, confidence, gesture_idx = self.extract_and_classify(window, already_filtered=True) if gesture_name == "NaN": self.print_queue.put("NaN detected in prediction, skipping.") continue # === Ground truth label from notes === label = self.get_label_for_sample(start) msg = "" if label and str(label).lower() != "nan": label = label.strip().lower() self.total += 1 try: true_idx = next(i for i, name in enumerate(self.label_names) if label in name.lower()) except StopIteration: true_idx = None if true_idx is not None and gesture_idx == true_idx: self.correct += 1 status = "[OK]" else: status = "[X]" msg = (f"{status} [{start / self.fs:.2f}-{end / self.fs:.2f}s] → {gesture_name} | True: " f"{self.label_names[true_idx] if true_idx is not None else label} | Conf: {confidence:.2f}") else: msg = (f"[WARN] [{start / self.fs:.2f}-{end / self.fs:.2f}s] → {gesture_name} | No ground truth | Conf: {confidence:.2f}") self.print_queue.put(msg) # On exit if self.total > 0: self.print_queue.put(f"\nFinal accuracy: {self.correct}/{self.total} ({100 * self.correct / self.total:.2f}%)")
[docs] def stop(self): self.stop_event.set()
[docs] def get_message(self, timeout=0.5): try: return self.print_queue.get(timeout=timeout) except queue.Empty: return None