├── .gitignore ├── data ├── emg_signal.xlsx ├── multichannel_emg_signals.mat └── multichannel_emg_signals.xlsx ├── feature_extraction_scheme.py ├── README.md ├── digital_processing.py └── feature_extraction.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .git 3 | __pycache__ -------------------------------------------------------------------------------- /data/emg_signal.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastianRestrepoA/EMG-pattern-recognition/HEAD/data/emg_signal.xlsx -------------------------------------------------------------------------------- /data/multichannel_emg_signals.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastianRestrepoA/EMG-pattern-recognition/HEAD/data/multichannel_emg_signals.mat -------------------------------------------------------------------------------- /data/multichannel_emg_signals.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastianRestrepoA/EMG-pattern-recognition/HEAD/data/multichannel_emg_signals.xlsx -------------------------------------------------------------------------------- /feature_extraction_scheme.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from digital_processing import bp_filter, notch_filter, plot_signal 3 | from feature_extraction import features_estimation 4 | 5 | # Load data 6 | signal_path = 'data/emg_signal.xlsx' 7 | emg_signal = pd.read_excel(signal_path).values 8 | channel_name = 'Right Masseter' 9 | sampling_frequency = 2e3 10 | frame = 500 11 | step = 250 12 | 13 | 14 | # Plot raw sEMG signal 15 | plot_signal(emg_signal, sampling_frequency, channel_name) 16 | 17 | # Biomedical Signal Processing 18 | emg_signal = emg_signal.reshape((emg_signal.size,)) 19 | filtered_signal = notch_filter(emg_signal, sampling_frequency, 20 | True) 21 | filtered_signal = bp_filter(filtered_signal, 10, 500, 22 | sampling_frequency, True) 23 | 24 | # EMG Feature Extraction 25 | emg_features, features_names = features_estimation(filtered_signal, channel_name, 26 | sampling_frequency, frame, step) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## EMG Feature Extraction 2 | 3 | This project explains how to apply digital filters above a raw EMG signal and then extract time and frequency features using 4 | the sliding window method. This characterization can be used as input to train a machine learning model that recognizes muscular patterns. 5 | 6 | 7 | ### Prerequisites 8 | You must have NumPy, Pandas, Matplotlib, Scipy, and Pyyawt installed. 9 | 10 | ### Project Structure 11 | 12 | 1. digital_processing.py - It contains the digital filters (notch and band pass) configuration to eliminate signal noise and artifacts. 13 | 2. feature_extraction.py - It allows to compute time and frequency features above an EMG signal. 14 | 3. feature_extraction_scheme.py - It contains the feature extraction scheme: load data, biomedical signal processing and feature extraction. 15 | 4. data - This folder contains the EMG data to be analyzed. 16 | 17 | ### Running the project 18 | 19 | 1. Clone emg pattern recognition project in a local directory. 20 | ``` 21 | git clone https://github.com/SebastianRestrepoA/EMG-pattern-recognition.git 22 | ``` 23 | 24 | 2. Create enviroment for run EMG pattern recognition project. In your cloned folder run the following commands: 25 | ``` 26 | virtualenv env 27 | env\Scripts\activate 28 | pip install pandas 29 | pip install matplotlib 30 | pip install numpy 31 | pip install pyyawt 32 | ``` 33 | 34 | 4. Run feature_extraction_scheme.py using below command to see EMG characterization. 35 | ``` 36 | python feature_extraction_scheme.py 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /digital_processing.py: -------------------------------------------------------------------------------- 1 | from scipy import signal 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | import pyyawt 5 | 6 | 7 | def notch_filter(x, samplerate, plot=False): 8 | x = x - np.mean(x) 9 | 10 | high_cutoff_notch = 59 / (samplerate / 2) 11 | low_cutoff_notch = 61 / (samplerate / 2) 12 | 13 | [b, a] = signal.butter(4, [high_cutoff_notch, low_cutoff_notch], btype='stop') 14 | 15 | x_filt = signal.filtfilt(b, a, x.T) 16 | 17 | if plot: 18 | t = np.arange(0, len(x) / samplerate, 1 / samplerate) 19 | plt.plot(t, x) 20 | plt.plot(t, x_filt.T, 'k') 21 | plt.autoscale(tight=True) 22 | plt.xlabel('Time') 23 | plt.ylabel('Amplitude (mV)') 24 | plt.show() 25 | 26 | return x_filt 27 | 28 | 29 | def bp_filter(x, low_f, high_f, samplerate, plot=False): 30 | # x = x - np.mean(x) 31 | 32 | low_cutoff_bp = low_f / (samplerate / 2) 33 | high_cutoff_bp = high_f / (samplerate / 2) 34 | 35 | [b, a] = signal.butter(5, [low_cutoff_bp, high_cutoff_bp], btype='bandpass') 36 | 37 | x_filt = signal.filtfilt(b, a, x) 38 | 39 | if plot: 40 | t = np.arange(0, len(x) / samplerate, 1 / samplerate) 41 | plt.plot(t, x) 42 | plt.plot(t, x_filt, 'k') 43 | plt.autoscale(tight=True) 44 | plt.xlabel('Time') 45 | plt.ylabel('Amplitude (mV)') 46 | plt.show() 47 | 48 | return x_filt 49 | 50 | 51 | def plot_signal(x, samplerate, chname): 52 | t = np.arange(0, len(x) / samplerate, 1 / samplerate) 53 | plt.plot(t, x) 54 | plt.autoscale(tight=True) 55 | plt.xlabel('Time') 56 | plt.ylabel('Amplitude (mV)') 57 | plt.title(chname) 58 | plt.show() 59 | 60 | 61 | def denoisewavelet(x1, level=5): 62 | xd, cxd, lxd = pyyawt.wden(x1, 'minimaxi', 's', 'mln', level, 'db5') 63 | return xd 64 | 65 | -------------------------------------------------------------------------------- /feature_extraction.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import math 4 | import matplotlib.pyplot as plt 5 | import pywt 6 | 7 | 8 | def features_estimation(signal, channel_name, fs, frame, step, plot=False): 9 | """ 10 | Compute time, frequency and time-frequency features from signal. 11 | :param signal: numpy array signal. 12 | :param channel_name: string variable with the EMG channel name in analysis. 13 | :param fs: int variable with the sampling frequency used to acquire the signal 14 | :param frame: sliding window size 15 | :param step: sliding window step size 16 | :param plot: bolean variable to plot estimated features. 17 | 18 | :return: total_feature_matrix -- python Dataframe with . 19 | :return: features_names -- python list with 20 | 21 | """ 22 | 23 | features_names = ['VAR', 'RMS', 'IEMG', 'MAV', 'LOG', 'WL', 'ACC', 'DASDV', 'ZC', 'WAMP', 'MYOP', "FR", "MNP", "TP", 24 | "MNF", "MDF", "PKF", "WENT"] 25 | 26 | time_matrix = time_features_estimation(signal, frame, step) 27 | frequency_matrix = frequency_features_estimation(signal, fs, frame, step) 28 | time_frequency_matrix = time_frequency_features_estimation(signal, frame, step) 29 | total_feature_matrix = pd.DataFrame(np.column_stack((time_matrix, frequency_matrix, time_frequency_matrix)).T, 30 | index=features_names) 31 | 32 | print('EMG features were from channel {} extracted successfully'.format(channel_name)) 33 | 34 | if plot: 35 | plot_features(signal, channel_name, fs, total_feature_matrix, step) 36 | 37 | return total_feature_matrix, features_names 38 | 39 | 40 | def time_features_estimation(signal, frame, step): 41 | """ 42 | Compute time features from signal using sliding window method. 43 | :param signal: numpy array signal. 44 | :param frame: sliding window size. 45 | :param step: sliding window step size. 46 | 47 | :return: time_features_matrix: narray matrix with the time features stacked by columns. 48 | """ 49 | 50 | variance = [] 51 | rms = [] 52 | iemg = [] 53 | mav = [] 54 | log_detector = [] 55 | wl = [] 56 | aac = [] 57 | dasdv = [] 58 | zc = [] 59 | wamp = [] 60 | myop = [] 61 | 62 | th = np.mean(signal) + 3 * np.std(signal) 63 | 64 | for i in range(frame, signal.size, step): 65 | x = signal[i - frame:i] 66 | 67 | variance.append(np.var(x)) 68 | rms.append(np.sqrt(np.mean(x ** 2))) 69 | iemg.append(np.sum(abs(x))) # Integral 70 | mav.append(np.sum(np.absolute(x)) / frame) # Mean Absolute Value 71 | log_detector.append(np.exp(np.sum(np.log10(np.absolute(x))) / frame)) 72 | wl.append(np.sum(abs(np.diff(x)))) # Wavelength 73 | aac.append(np.sum(abs(np.diff(x))) / frame) # Average Amplitude Change 74 | dasdv.append( 75 | math.sqrt((1 / (frame - 1)) * np.sum((np.diff(x)) ** 2))) # Difference absolute standard deviation value 76 | zc.append(zcruce(x, th)) # Zero-Crossing 77 | wamp.append(wilson_amplitude(x, th)) # Willison amplitude 78 | myop.append(myopulse(x, th)) # Myopulse percentage rate 79 | 80 | time_features_matrix = np.column_stack((variance, rms, iemg, mav, log_detector, wl, aac, dasdv, zc, wamp, myop)) 81 | return time_features_matrix 82 | 83 | 84 | def frequency_features_estimation(signal, fs, frame, step): 85 | """ 86 | Compute frequency features from signal using sliding window method. 87 | :param signal: numpy array signal. 88 | :param fs: sampling frequency of the signal. 89 | :param frame: sliding window size 90 | :param step: sliding window step size 91 | 92 | :return: frequency_features_matrix: narray matrix with the frequency features stacked by columns. 93 | """ 94 | 95 | fr = [] 96 | mnp = [] 97 | tot = [] 98 | mnf = [] 99 | mdf = [] 100 | pkf = [] 101 | 102 | for i in range(frame, signal.size, step): 103 | x = signal[i - frame:i] 104 | frequency, power = spectrum(x, fs) 105 | 106 | fr.append(frequency_ratio(frequency, power)) # Frequency ratio 107 | mnp.append(np.sum(power) / len(power)) # Mean power 108 | tot.append(np.sum(power)) # Total power 109 | mnf.append(mean_freq(frequency, power)) # Mean frequency 110 | mdf.append(median_freq(frequency, power)) # Median frequency 111 | pkf.append(frequency[power.argmax()]) # Peak frequency 112 | 113 | frequency_features_matrix = np.column_stack((fr, mnp, tot, mnf, mdf, pkf)) 114 | 115 | return frequency_features_matrix 116 | 117 | 118 | def time_frequency_features_estimation(signal, frame, step): 119 | """ 120 | Compute time-frequency features from signal using sliding window method. 121 | :param signal: numpy array signal. 122 | :param frame: sliding window size 123 | :param step: sliding window step size 124 | 125 | :return: h_wavelet: list 126 | """ 127 | h_wavelet = [] 128 | 129 | for i in range(frame, signal.size, step): 130 | x = signal[i - frame:i] 131 | 132 | E_a, E = wavelet_energy(x, 'db2', 4) 133 | E.insert(0, E_a) 134 | E = np.asarray(E) / 100 135 | 136 | h_wavelet.append(-np.sum(E * np.log2(E))) 137 | 138 | return h_wavelet 139 | 140 | 141 | def wilson_amplitude(signal, th): 142 | x = abs(np.diff(signal)) 143 | umbral = x >= th 144 | return np.sum(umbral) 145 | 146 | 147 | def myopulse(signal, th): 148 | umbral = signal >= th 149 | return np.sum(umbral) / len(signal) 150 | 151 | 152 | def spectrum(signal, fs): 153 | m = len(signal) 154 | n = next_power_of_2(m) 155 | y = np.fft.fft(signal, n) 156 | yh = y[0:int(n / 2 - 1)] 157 | fh = (fs / n) * np.arange(0, n / 2 - 1, 1) 158 | power = np.real(yh * np.conj(yh) / n) 159 | 160 | return fh, power 161 | 162 | 163 | def frequency_ratio(frequency, power): 164 | power_low = power[(frequency >= 30) & (frequency <= 250)] 165 | power_high = power[(frequency > 250) & (frequency <= 500)] 166 | ULC = np.sum(power_low) 167 | UHC = np.sum(power_high) 168 | 169 | return ULC / UHC 170 | 171 | 172 | def shannon(x): 173 | N = len(x) 174 | nb = 19 175 | hist, bin_edges = np.histogram(x, bins=nb) 176 | counts = hist / N 177 | nz = np.nonzero(counts) 178 | 179 | return np.sum(counts[nz] * np.log(counts[nz]) / np.log(2)) 180 | 181 | 182 | def zcruce(X, th): 183 | th = 0 184 | cruce = 0 185 | for cont in range(len(X) - 1): 186 | can = X[cont] * X[cont + 1] 187 | can2 = abs(X[cont] - X[cont + 1]) 188 | if can < 0 and can2 > th: 189 | cruce = cruce + 1 190 | return cruce 191 | 192 | 193 | def mean_freq(frequency, power): 194 | num = 0 195 | den = 0 196 | for i in range(int(len(power) / 2)): 197 | num += frequency[i] * power[i] 198 | den += power[i] 199 | 200 | return num / den 201 | 202 | 203 | def median_freq(frequency, power): 204 | power_total = np.sum(power) / 2 205 | temp = 0 206 | tol = 0.01 207 | errel = 1 208 | i = 0 209 | 210 | while abs(errel) > tol: 211 | temp += power[i] 212 | errel = (power_total - temp) / power_total 213 | i += 1 214 | if errel < 0: 215 | errel = 0 216 | i -= 1 217 | 218 | return frequency[i] 219 | 220 | 221 | def wavelet_energy(x, mother, nivel): 222 | coeffs = pywt.wavedecn(x, wavelet=mother, level=nivel) 223 | arr, _ = pywt.coeffs_to_array(coeffs) 224 | Et = np.sum(arr ** 2) 225 | cA = coeffs[0] 226 | Ea = 100 * np.sum(cA ** 2) / Et 227 | Ed = [] 228 | 229 | for k in range(1, len(coeffs)): 230 | cD = list(coeffs[k].values()) 231 | cD = np.asarray(cD) 232 | Ed.append(100 * np.sum(cD ** 2) / Et) 233 | 234 | return Ea, Ed 235 | 236 | 237 | def next_power_of_2(x): 238 | return 1 if x == 0 else 2 ** (x - 1).bit_length() 239 | 240 | 241 | def med_freq(f, P): 242 | Ptot = np.sum(P) / 2 243 | temp = 0 244 | tol = 0.01 245 | errel = 1 246 | i = 0 247 | 248 | while abs(errel) > tol: 249 | temp += P[i] 250 | errel = (Ptot - temp) / Ptot 251 | i += 1 252 | if errel < 0: 253 | errel = 0 254 | i -= 1 255 | 256 | return f[i] 257 | 258 | 259 | def plot_features(signal, channel_name, fs, feature_matrix, step): 260 | """ 261 | xxxs 262 | 263 | Argument: 264 | signal -- python numpy array representing recording of a signal. 265 | channel_name -- string variable with the EMG channel name in analysis. 266 | fs -- int variable with the sampling frequency used to acquire the signal. 267 | feature_matrix -- python Dataframe ... 268 | step -- int variable with the step size used in the sliding window method. 269 | """ 270 | 271 | ts = np.arange(0, len(signal) / fs, 1 / fs) 272 | # for idx, f in enumerate(tfeatures.T): 273 | for key in feature_matrix.T: 274 | tf = step * (np.arange(0, len(feature_matrix.T[key]) / fs, 1 / fs)) 275 | fig = plt.figure() 276 | 277 | ax = fig.add_subplot(111, label="1") 278 | ax2 = fig.add_subplot(111, label="2", frame_on=False) 279 | ax.plot(ts, signal, color="C0") 280 | ax.autoscale(tight=True) 281 | plt.title(channel_name + ": " + key) 282 | ax.set_xlabel("Time") 283 | ax.set_ylabel("mV") 284 | 285 | ax2.plot(tf, feature_matrix.T[key], color="red") 286 | ax2.yaxis.tick_right() 287 | ax2.autoscale(tight=True) 288 | ax2.set_xticks([]) 289 | ax2.set_yticks([]) 290 | mng = plt.get_current_fig_manager() 291 | mng.window.state('zoomed') 292 | plt.show() 293 | 294 | 295 | 296 | --------------------------------------------------------------------------------