Source code for intan.processing._filters

"""
intan.processing._filters

Comprehensive EMG signal preprocessing module.

Includes:

- Bandpass, lowpass, and notch filters
- Hilbert envelope extraction
- RMS and windowed RMS computation
- Common average referencing (CAR)
- Sliding windows and PCA-based dimensionality reduction
- CNN-ECA compatible preprocessing pipeline

This module supports feature extraction pipelines for real-time classification
and pre-training EMG datasets with overlapping or fixed windows.
"""

import time
import numpy as np
from scipy.signal import butter, filtfilt, hilbert, iirnotch, lfilter, lfilter_zi
from ._features import compute_rms, window_rms, common_average_reference, envelope_extraction

#from sklearn.decomposition import PCA
#from sklearn.preprocessing import StandardScaler

[docs] def preprocess_emg(emg_data, sample_rate): """ Applies filtering and extracts RMS features. Parameters: emg_data: 2D numpy array of EMG data (channels, samples). sample_rate: Sampling rate of the EMG data. Returns: rms_features: 2D numpy array of RMS features (channels, windows). """ filtered_data = notch_filter(emg_data, fs=sample_rate, f0=60) filtered_data = bandpass_filter(filtered_data, lowcut=20, highcut=400, fs=sample_rate, order=2, axis=1) rms_features = compute_rms(filtered_data, int(0.1 * sample_rate)) return rms_features
[docs] def parse_channel_ranges(channel_arg): """ Parses a channel range string (e.g., [1:8, 64:72]) and returns a flat list of integers. Parameters: channel_arg (str): The string containing channel ranges (e.g., "[1:8, 64:72]"). Returns: list: A flat list of integers. """ # Remove square brackets and split by commas channel_arg = channel_arg.strip("[]") ranges = channel_arg.split(",") channel_list = [] for r in ranges: if ":" in r: start, end = map(int, r.split(":")) # channel_list.extend(range(start - 1, end)) # Convert to 0-based indexing channel_list.extend(range(start, end)) else: # channel_list.append(int(r) - 1) # Convert single channel to 0-based indexing channel_list.append(int(r)) return channel_list
[docs] def notch_filter(data, fs=4000, f0=60.0, Q=10, axis=1): """ Applies a notch filter to the data to remove 60 Hz interference. Assumes data shape (n_channels, n_samples). A bandwidth of 10 Hz is recommended for 50 or 60 Hz notch filters; narrower bandwidths lead to poor time-domain properties with an extended ringing response to transient disturbances. Parameters: data (ndarray): Input data to be filtered. fs (float): Sampling frequency of the data. f0 (float): Frequency to be removed from the data (60 Hz). Q (float): Quality factor of the notch filter. Returns: nn.array: Example: out = notch_filter(signal_in, 30000, 60, 10); """ b, a = iirnotch(f0, Q, fs) return filtfilt(b, a, data, axis=axis)
[docs] def lowpass_filter(data, cutoff, fs, order=4, axis=1): """ Applies a lowpass filter to the data using a Butterworth filter. Parameters: data (ndarray): Input data to be filtered. cutoff (float): Cutoff frequency. fs (float): Sampling frequency of the data. order (int): Order of the filter. axis (int): Axis along which to apply the filter. Returns: ndarray: Filtered data. """ b, a = butter(order, cutoff, btype="low", fs=fs) y = filtfilt(b, a, data, axis=axis) return y
[docs] def bandpass_filter(data, lowcut=10, highcut=500, fs=4000, order=4, axis=1, verbose=False): """ Applies a bandpass filter to the data using a Butterworth filter. Parameters: data (ndarray): Input data to be filtered. lowcut (float): Low cutoff frequency. highcut (float): High cutoff frequency. fs (float): Sampling frequency of the data. order (int): Order of the filter. axis (int): Axis along which to apply the filter. verbose (bool): Whether to print filter parameters. Returns: ndarray: Filtered data. """ b, a = butter(order, [lowcut, highcut], btype="bandpass", fs=fs) y = filtfilt(b, a, data, axis=axis) return y
[docs] def filter_emg(emg_data, filter_type='bandpass', lowcut=30, highcut=500, fs=1259, order=5, verbose=False): """ Applies a bandpass or lowpass filter to EMG data using numpy arrays. Parameters: emg_data: Numpy array of shape (num_samples, num_channels) with EMG data. filter_type: Type of filter to apply ('bandpass' or 'lowpass'). lowcut: Low cutoff frequency for the bandpass filter. highcut: High cutoff frequency for the bandpass filter. fs: Sampling rate of the EMG data. order: Filter order. verbose: Whether to print progress. Returns: Filtered data as a numpy array (same shape as input data). """ tic = time.process_time() if filter_type == 'bandpass': if verbose: print(f"| Applying butterworth bandpass filter: {lowcut}-{highcut} Hz {order} order") filtered_data = bandpass_filter(emg_data, lowcut, highcut, fs, order, axis=0) elif filter_type == 'lowpass': if verbose: print(f"| Applying butterworth lowpass filter: {lowcut} Hz {order} order") filtered_data = lowpass_filter(emg_data, lowcut, fs, order, axis=0) toc = time.process_time() if verbose: print(f"| | Filtering time = {1000 * (toc - tic):.2f} ms") # Convert list of arrays to a single 2D numpy array filtered_data = np.stack(filtered_data, axis=0) # Stack along axis 0 (channels) return filtered_data
[docs] def process_emg_pipeline(data, lowcut=30, highcut=500, order=5, window_size=400, verbose=False): """ Processing steps to match the CNN-ECA methodology https://pmc.ncbi.nlm.nih.gov/articles/PMC10669079/ Input data is assumed to have shape (N_channels, N_samples) Parameters: data: 2D numpy array of EMG data (channels, samples). lowcut: Low cutoff frequency for the bandpass filter. highcut: High cutoff frequency for the bandpass filter. order: Order of the Butterworth filter. window_size: Window size for RMS calculation. verbose: Whether to print progress. Returns: smoothed: 2D numpy array of processed EMG data (channels, samples). """ emg_data = data['amplifier_data'] # Extract EMG data sample_rate = int(data['frequency_parameters']['board_dig_in_sample_rate']) # Extract sampling rate # Overwrite the first and last second of the data with 0 to remove edge effects # emg_data[:, :sample_rate] = 0.0 emg_data[:, -sample_rate:] = 0.0 # Just first second # Apply bandpass filter bandpass_filtered = filter_emg(emg_data, 'bandpass', lowcut, highcut, sample_rate, order) # Rectify # rectified = rectify_emg(bandpass_filtered) rectified = bandpass_filtered # Apply Smoothing # smoothed = window_rms(rectified, window_size=window_size) smoothed = envelope_extraction(rectified, method='hilbert') return smoothed
[docs] def sliding_window(data, window_size, step_size): """ Splits the data into overlapping windows. Parameters: data: 2D numpy array of shape (channels, samples). window_size: Window size in number of samples. step_size: Step size in number of samples. Returns: windows: List of numpy arrays, each representing a window of data. """ num_channels, num_samples = data.shape windows = [] for start in range(0, num_samples - window_size + 1, step_size): window = data[:, start:start + window_size] windows.append(window) return windows
# def apply_pca(data, num_components=8, verbose=False): # """ # Applies PCA to reduce the number of EMG channels to the desired number of components. # # Parameters: # data: 2D numpy array of EMG data (channels, samples) -> (128, 500,000). # num_components: Number of principal components to reduce to (e.g., 8). # # Returns: # pca_data: 2D numpy array of reduced EMG data (num_components, samples). # explained_variance_ratio: Percentage of variance explained by each of the selected components. # """ # # Step 1: Standardize the data across the channels # scaler = StandardScaler() # features_std = scaler.fit_transform(data) # Standardizing along the channels # # # Step 2: Apply PCA # pca = PCA(n_components=num_components) # pca_data = pca.fit_transform(features_std) # Apply PCA on the transposed data # # if verbose: # print("Original shape:", data.shape) # print("PCA-transformed data shape:", pca_data.shape) # # # Step 3: Get the explained variance ratio (useful for understanding how much variance is retained) # explained_variance_ratio = pca.explained_variance_ratio_ # # return pca_data, explained_variance_ratio