Body surface potential mapping of the cortico-muscular axis using smart textile electrode arrays.
The 19 matches
- [1] § Methods › Feature extraction ↔ Wireless_BSPM.ipynb, lines 1290–1370 · score 0.95 · overlapping windows, power spectral density, 0.5–4 Hz, 30–100 Hz, 8–13 Hz, 0.5–100 Hz
- [2] § Methods › EEG signal processing and analytical procedures › Spectral decomposition and periodic vs. aperiodic components ↔ Additional_figures.ipynb, lines 560–637 · score 0.93 · peak width limits, 1–100 Hz, 1–12 Hz, peak height, Power spectral densities, aperiodic
- [3] § Methods › Preprocessing of recorded electrophysiological signals ↔ Wireless_BSPM.ipynb, lines 1290–1370 · score 0.85 · power spectral density, 8–13 Hz, 13–30 Hz, band power, EEG signals, frequency bands
- [4] § Methods › Classification and performance evaluation ↔ Wireless_BSPM.ipynb, lines 288–344 · score 0.83 · Confusion matrices, Logistic regression, F1 score, cross validation, single channel, precision
- [5] § Methods › PLS regression (EEG → EMG mapping) ↔ Wireless_BSPM.ipynb, lines 3107–3136 · score 0.80 · PLSRegression, EMG maps, EEG features, RMSE, squares, components
- [6] § Methods › EEG signal processing and analytical procedures › Evoked vs. induced activity ↔ Additional_figures.ipynb, lines 825–954 · score 0.80 · flattened ERP, confusion matrices, logistic regression, lbfgs, multinomial, solver
- [7] § Results and Discussion › Spatiotemporal features for enhanced classification ↔ Wireless_BSPM.ipynb, lines 1434–1505 · score 0.77 · logistic regression, F1 score, Event related, motor activity, precision, recall
- [8] § Methods › Feature importance analysis using correlation circles ↔ Wireless_BSPM.ipynb, lines 399–516 · score 0.76 · correlation circle coordinates, unit circle, standardised feature matrix, PCA, variance, component
- [9] § Results and Discussion › Spatiotemporal features for enhanced classification ↔ Wireless_BSPM.ipynb, lines 288–344 · score 0.76 · repeated cross validation, logistic regression, F1 score, single channel, precision, recall
- [10] § Methods › EEG signal processing and analytical procedures › Event-Related Desynchronisation/Synchronisation (ERD/ERS) ↔ Additional_figures.ipynb, lines 1030–1148 · score 0.75 · movement onset, 8–12 Hz, 13–30 Hz, Hilbert, ERD, ERS
- [11] § Methods › Classification and performance evaluation ↔ Wireless_BSPM.ipynb, lines 3138–3185 · score 0.69 · Spearman rank correlation, Pearson correlation, standard deviation, cosine, predictions
- [12] § Methods › Machine learning model descriptions ↔ Wireless_BSPM.ipynb, lines 953–1009 · score 0.69 · repeated stratified, F1 score, cross validation, precision, recall, fold
- [13] § Methods › EEG signal processing and analytical procedures › Event-Related Desynchronisation/Synchronisation (ERD/ERS) ↔ Additional_figures.ipynb, lines 1030–1148 · score 0.69 · broadband envelope, ERD, ERS, Gaussian, Epochs, onset
- [14] § Methods › Machine learning model training ↔ Wireless_BSPM.ipynb, lines 3138–3185 · score 0.67 · Spearman rank correlation, Pearson correlation, cosine, Predictive
- [15] § Methods › Feature importance analysis using correlation circles ↔ Wireless_BSPM.ipynb, lines 399–516 · score 0.62 · correlation circles, feature matrices, PCA, variance, component, SNR
- [16] § Results and Discussion › Mapping and prediction of cortico-muscular axis dynamics ↔ Wireless_BSPM.ipynb, lines 2691–2744 · score 0.61 · Gaussian fits, hand flexion, wrist extension, Histograms, Spatio temporal, movement
- [17] § Methods › EEG signal processing and analytical procedures › Evoked Response Potentials (ERPs) ↔ Additional_figures.ipynb, lines 439–557 · score 0.60 · Anterior Frontal, SEM, Parietal, Occipital, ERPs, Evoked
- [18] § Results and Discussion › Mapping and prediction of cortico-muscular axis dynamics ↔ Wireless_BSPM.ipynb, lines 2804–2809 · score 0.53 · 15–30 Hz, beta band, cortico muscular, components, Event, 15 Hz
- [19] § Results and Discussion › Mapping and prediction of cortico-muscular axis dynamics ↔ Wireless_BSPM.ipynb, lines 2691–2744 · score 0.52 · hand flexion, wrist extension, Gaussian, 600 ms, movements, reaction
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Jupyter notebook · 3,675 lines · 148 KB · no license · 14 matches
- # %% [markdown]
- # # Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology
- # %% [markdown]
- # Ruben Ruiz-Mateos Serrano <br>
- # Start date : 10/01/2025
- # %% [markdown]
- # ## BSPM & ML
- # %% [markdown]
- # ### Test 16 channel BSPM recordings (with INTAN)
- # %% [markdown]
- # #### Data import
- # %%
- import os
- import re
- from biolab.bspm import BSPM,image2layout
- from biolab.utils import apply2all
- # Save new recordings into .bspm files
- # Define parameters
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings'
- # Convert electrode array image into layout
- ECG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\ECG_lyt.jpg',seq='cw')
- EMG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EMG_lyt.jpg',seq='cw')
- #EEG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EEG_lyt.jpg',seq='cw')
- # Define operation to be applied to each file
- def func(filepath):
- # Extract filename from filepath
- filename = os.path.splitext(os.path.basename(filepath))[0]
- # Define the regex pattern to match the required values
- pattern = r'^(([^\s]+)_M(\d+))_\d{6}_\d{6}$'
- # Search for the pattern in the given filename
- match = re.search(pattern,filename)
- if match:
- filename = match.group(1)
- mode = match.group(2) # The type of signal (i.e. ECG, EMG, EEG)
- measurement = match.group(3) # The number following 'M'
- else:
- raise ValueError("Filename \"{}\" does not match the expected pattern.".format(filename))
- meta = {
- 'mode': mode.split('_')[0],
- 'measurement': measurement
- }
- # Load BSPM data from .rhs file
- if mode == 'ECG_BSPM':
- MP = BSPM.from_file(filepath=filepath,layout=ECG_lyt,metadata=meta,refchs=[])
- elif mode == 'EMG_BSPM':
- MP = BSPM.from_file(filepath=filepath,layout=EMG_lyt,metadata=meta,refchs=[])
- #elif mode == 'EEG_BSPM':
- #MP = BSPM.from_file(filepath=filepath,layout=EEG_lyt,metadata=meta,refchs=[])
- MP.save(savepath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\\'+filename+'.bspm')
- # Call DataSaver function
- apply2all(read_directory=readpath,extension='.rhs',func=func)
- # %% [markdown]
- # #### Electrode array impedance analysis
- # %% [markdown]
- # Average electrode impedance across all measurements and channels for each signal mode
- # %%
- import pandas as pd
- import matplotlib.pyplot as plt
- # Import .bspm files
- ECG = BSPM.load(loadpath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_BSPM_M2.bspm')
- df_ECG = ECG.channel_data.loc[:,['electrode_impedance_magnitude','custom_order']]
- EMG = BSPM.load(loadpath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_BSPM_M2.bspm')
- df_EMG = EMG.channel_data.loc[:,['electrode_impedance_magnitude','custom_order']]
- # Add a 'mode' column to each DataFrame
- df_ECG['mode'] = 'ECG'
- df_EMG['mode'] = 'EMG'
- # Concatenate the DataFrames
- df = pd.concat([df_ECG,df_EMG],ignore_index=True)
- # %% [markdown]
- # Violin plot of impedance distribution for all modes
- # %%
- import seaborn as sns
- sns.violinplot(x='mode',y='electrode_impedance_magnitude',data=df,cut=0)
- plt.title('')
- plt.xlabel('Mode')
- plt.ylabel('Impedance [$\Omega$]')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Impedance_distribution_by_mode.pdf')
- plt.show()
- # %% [markdown]
- # #### Body Surface Potential Mapping
- # %%
- from biolab.bspm import BSPM
- # Import .bspm files
- ECG = BSPM.load(loadpath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_BSPM_M1.bspm')
- EMG = BSPM.load(loadpath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_BSPM_M2.bspm')
- # %%
- import matplotlib.pyplot as plt
- from biolab.bspm import interpolate
- vmap = ECG.potential(time=10)
- frame = interpolate(valmap=vmap)
- plt.imshow(frame,vmin=-2000,vmax=2000)
- cbar = plt.colorbar()
- cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- # %% [markdown]
- # ### Shape classification from muscular BSPM
- # %% [markdown]
- # #### Data import
- # %%
- import os
- import re
- from biolab.bspm import BSPM,image2layout
- from biolab.utils import apply2all
- # Save new recordings into .bspm files
- # Define parameters
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings\EMG_classification'
- # Convert electrode array image into layout
- EMG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EMG_lyt.jpg',seq='cw')
- # Define operation to be applied to each file
- def func(filepath):
- # Extract filename from filepath
- filename = os.path.splitext(os.path.basename(filepath))[0]
- # Define the regex pattern to match the required values
- pattern = r'^([A-Za-z]+)_([A-Za-z]+)_(\d+)_\d{6}_\d{6}$'
- # Search for the pattern in the given filename
- match = re.search(pattern,filename)
- if match:
- filename = match.group(1)+'_'+match.group(2)+'_'+match.group(3)
- mode = match.group(1) # type of signal (i.e. ECG, EMG, EEG)
- shape = match.group(2) # shape of the object
- measurement = match.group(3) # measurement
- else:
- raise ValueError("Filename \"{}\" does not match the expected pattern.".format(filename))
- meta = {
- 'mode': mode,
- 'measurement': measurement,
- 'shape': {'ball':'sphere','cylinder':'cylinder','emptyhand':'no object'}.get(shape,None)
- }
- # Load BSPM data from .rhs file
- MP = BSPM.from_file(filepath=filepath,layout=EMG_lyt,metadata=meta,refchs=[])
- # Save BSPM object
- MP.save(savepath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification\\'+filename+'.bspm')
- # Call DataSaver function
- apply2all(read_directory=readpath,extension='.rhs',func=func)
- # %% [markdown]
- # #### SNR computation
- # %%
- import pandas as pd
- import numpy as np
- from biolab.utils import apply2all,snr
- from biolab.bspm import BSPM
- # Define read directory
- rdir = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification'
- # Define `apply2all` function
- def fun(filepath):
- # Load BSPM files
- MP = BSPM.load(loadpath=filepath)
- # Resample data
- newfs = 800
- rMP = MP.resample(newfs=newfs)
- # Preprocess data
- rMP.preprocess(bw=(5,399))
- # Read DataFrame with existing data
- excelpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification\SNR.xlsx'
- with pd.ExcelWriter(excelpath,engine='openpyxl',mode='a',if_sheet_exists='overlay') as writer:
- existing_data = pd.read_excel(excelpath)
- updated_data = existing_data
- # Compute SNR for each channel (5s baseline + 5s contraction)
- for col in rMP.preprocessed_data.columns:
- acc = 0
- # Get the slices as lists of absolute values
- arrays = [
- np.abs(rMP.preprocessed_data[col][0.5:4.5].values),
- np.abs(rMP.preprocessed_data[col][10.5:14.5].values),
- np.abs(rMP.preprocessed_data[col][20.5:24.5].values),
- np.abs(rMP.preprocessed_data[col][30.5:34.5].values),
- np.abs(rMP.preprocessed_data[col][40.5:44.5].values),
- np.abs(rMP.preprocessed_data[col][50.5:54.5].values),
- np.abs(rMP.preprocessed_data[col][60.5:64.5].values),
- np.abs(rMP.preprocessed_data[col][70.5:74.5].values),
- np.abs(rMP.preprocessed_data[col][80.5:84.5].values),
- np.abs(rMP.preprocessed_data[col][90.5:94.5].values)
- ]
- # Find the minimum length of all arrays
- min_length = min(len(arr) for arr in arrays)
- # Trim each array to the minimum length
- arrays_trimmed = [arr[:min_length] for arr in arrays]
- # Compute the mean
- baseline = np.mean(arrays_trimmed,axis=0)
- while (acc+10)*newfs<len(rMP.preprocessed_data[col].values):
- data = np.abs(rMP.preprocessed_data[col][5+acc:10+acc].values)
- snrval = snr(s=data,n=baseline)
- new_data = {'SNR':snrval,'shape':rMP.metadata['shape'],'measurement':rMP.metadata['measurement'],'envelope':acc/10+1,'channel':col}
- updated_data = updated_data.append(new_data,ignore_index=True)
- acc = acc+10
- # Write the updated DataFrame back to the Excel file, overwriting the old data
- updated_data.to_excel(writer,index=False)
- apply2all(read_directory=rdir,extension='.bspm',func=fun)
- # %% [markdown]
- # #### SNR values inspection
- # %%
- import pandas as pd
- import seaborn as sns
- import matplotlib.pyplot as plt
- # Import SNR data from Excel
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification\SNR.xlsx'
- df = pd.read_excel(io=readpath)
- # Display SNR as a function of shape, channel and measurement
- # Create a figure with subplots
- fig, axes = plt.subplots(1,3,figsize=(18,6))
- # Plot 1: Index by shape
- df = df.sort_values(by='shape')
- sns.violinplot(x='shape',y='SNR',data=df,order=df['shape'].unique(),ax=axes[0])
- axes[0].set_title('')
- axes[0].set_xlabel('Shape')
- axes[0].set_ylabel('SNR')
- # Plot 2: Index by channel
- df = df.sort_values(by='channel')
- df['channel'] = df['channel'].astype(int)
- sns.violinplot(x='channel',y='SNR',data=df,order=df['channel'].unique(),ax=axes[1])
- axes[1].set_title('')
- axes[1].set_xlabel('Channel')
- axes[1].set_ylabel('SNR')
- # Plot 3: Index by measurement
- df = df.sort_values(by='measurement')
- sns.violinplot(x='measurement',y='SNR',data=df,order=df['measurement'].unique(),ax=axes[2])
- axes[2].set_title('')
- axes[2].set_xlabel('Measurement')
- axes[2].set_ylabel('SNR')
- # Adjust layout to prevent overlap of titles and labels
- plt.tight_layout()
- # Display SNR as a function of channel for each shape
- # Create a figure with subplots
- fig, axes = plt.subplots(1,3,figsize=(18,6))
- # Plot 1: Index by shape
- df_sphere = df[df['shape']=='sphere'].sort_values(by='channel')
- sns.violinplot(x='channel',y='SNR',data=df_sphere,order=df_sphere['channel'].unique(),ax=axes[0])
- axes[0].set_title('Sphere')
- axes[0].set_xlabel('Channel')
- axes[0].set_ylabel('SNR')
- # Plot 2: Index by channel
- df_cylinder = df[df['shape']=='cylinder'].sort_values(by='channel')
- df_cylinder['channel'] = df_cylinder['channel'].astype(int)
- sns.violinplot(x='channel',y='SNR',data=df_cylinder,order=df_cylinder['channel'].unique(),ax=axes[1])
- axes[1].set_title('Cylinder')
- axes[1].set_xlabel('Channel')
- axes[1].set_ylabel('SNR')
- # Plot 3: Index by measurement
- df_noobject = df[df['shape']=='no object'].sort_values(by='channel')
- sns.violinplot(x='channel',y='SNR',data=df_noobject,order=df_noobject['channel'].unique(),ax=axes[2])
- axes[2].set_title('No object')
- axes[2].set_xlabel('Channel')
- axes[2].set_ylabel('SNR')
- # Adjust layout to prevent overlap of titles and labels
- plt.tight_layout()
- # %% [markdown]
- # *There doesn't seem to be a significant difference in SNR across different shapes. This probably indicates inability for a single electrode to perform accurate classification. When displaying SNR as a function of channel for different shapes, a difference in the distribution of SNR can be observed.*
- # %% [markdown]
- # #### Machine learning classification of shapes
- # %% [markdown]
- # Single electrode (control - middle electrode)
- # %%
- import pandas as pd
- from sklearn.model_selection import train_test_split
- from sklearn.linear_model import LogisticRegression
- from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
- from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, make_scorer,precision_score,recall_score,f1_score
- import matplotlib.pyplot as plt
- # Import SNR data from Excel
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\EMG_classification\SNR.xlsx'
- df = pd.read_excel(io=readpath)
- # Generate feature matrix from data
- fmatrix = df[df['channel']==9].drop(['measurement','envelope','channel'],axis=1)
- # Separate features and labels
- X = fmatrix.drop('shape',axis=1) # feature matrix
- y = fmatrix['shape'] # labels
- # Split the data into training and testing sets
- X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=fmatrix['shape'])
- # Initialise and fit the logistic regression
- clf = LogisticRegression()
- clf.fit(X_train,y_train)
- # Predict the labels for the test set
- y_pred = clf.predict(X_test)
- # Calculate the confusion matrix
- cm = confusion_matrix(y_test,y_pred)
- # Display the confusion matrix
- disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=clf.classes_)
- fig,ax = plt.subplots(figsize=(6,6)) # Adjust figure size if needed
- # Set the color range limits
- vmin,vmax = 0,20 # Adjust these values as needed
- disp.plot(cmap="Reds",ax=ax,colorbar=False,values_format="d")
- disp.im_.set_clim(vmin,vmax) # Set the colorbar limits
- # Adjust the colorbar size
- cbar = ax.figure.colorbar(disp.im_,fraction=0.046,pad=0.04)
- cbar.set_label("Count") # Label for better readability
- #plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Results\Confusion_matrices\Single_channel_EMG',dpi=1000)
- # Repeated Cross-Validation Accuracy
- rkf = RepeatedStratifiedKFold(n_splits=10,n_repeats=5)
- accuracy_scores = cross_val_score(clf,X,y,cv=rkf)
- precision_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(precision_score, average='macro'))
- recall_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(recall_score, average='macro'))
- f1_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(f1_score, average='macro'))
- # Print results
- print(f'Repeated Cross-Validation Accuracy: {accuracy_scores.mean():.2f} ± {accuracy_scores.std():.2f}')
- print(f"Repeated Cross-Validation Precision: {precision_scores.mean():.2f} ± {precision_scores.std():.2f}")
- print(f"Repeated Cross-Validation Recall: {recall_scores.mean():.2f} ± {recall_scores.std():.2f}")
- print(f"Repeated Cross-Validation F1 Score: {f1_scores.mean():.2f} ± {f1_scores.std():.2f}")
- # %% [markdown]
- # Electrode array
- # %%
- import pandas as pd
- from sklearn.model_selection import train_test_split
- from sklearn.linear_model import LogisticRegression
- from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
- from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
- import matplotlib.pyplot as plt
- # Import SNR data from Excel
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\EMG_classification\SNR.xlsx'
- df = pd.read_excel(io=readpath)
- # Generate feature matrix from data
- # Pivot the DataFrame so each channel becomes its own column
- fmatrix = df.pivot(index=['shape','envelope','measurement'],columns='channel',values='SNR')
- # Rename the columns to match the desired format
- fmatrix.columns = [f'snr_channel_{col}' for col in fmatrix.columns]
- # Reset index for a tidy DataFrame
- fmatrix.reset_index(inplace=True)
- # Remove unnecessary columns
- fmatrix = fmatrix.drop(columns=['envelope','measurement'])
- # Separate features and labels
- X = fmatrix.drop('shape',axis=1) # feature matrix
- y = fmatrix['shape'] # labels
- # Split the data into training and testing sets
- X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=fmatrix['shape'])
- # Initialise and fit the logistic regression
- clf = LogisticRegression()
- clf.fit(X_train,y_train)
- # Predict the labels for the test set
- y_pred = clf.predict(X_test)
- # Calculate the confusion matrix
- cm = confusion_matrix(y_test,y_pred)
- # Display the confusion matrix
- disp = ConfusionMatrixDisplay(confusion_matrix=cm,display_labels=clf.classes_)
- fig,ax = plt.subplots(figsize=(6,6)) # Adjust figure size if needed
- # Set the color range limits
- vmin,vmax = 0,20 # Adjust these values as needed
- disp.plot(cmap="Reds",ax=ax,colorbar=False,values_format="d")
- disp.im_.set_clim(vmin,vmax) # Set the colorbar limits
- # Adjust the colorbar size
- cbar = ax.figure.colorbar(disp.im_,fraction=0.046,pad=0.04)
- cbar.set_label("Count") # Label for better readability
- #plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Results\Confusion_matrices\BSPM_EMG',dpi=1000)
- # Repeated Cross-Validation Accuracy
- rkf = RepeatedStratifiedKFold(n_splits=10,n_repeats=5)
- scores = cross_val_score(clf,X,y,cv=rkf)
- print(f'Repeated Cross-Validation Accuracy: {scores.mean():.2f} ± {scores.std():.2f}')
- # %% [markdown]
- # Correlation circle for feature importance
- # %%
- import pandas as pd
- import numpy as np
- import matplotlib.pyplot as plt
- import matplotlib.cm as cm
- from sklearn.preprocessing import StandardScaler
- from sklearn.decomposition import PCA
- # ------------------------------------------------
- # Load data
- # ------------------------------------------------
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\EMG_classification\SNR.xlsx'
- df = pd.read_excel(io=readpath)
- # ------------------------------------------------
- # Build feature matrix (same as your ML pipeline)
- # ------------------------------------------------
- fmatrix = df.pivot(
- index=['shape','envelope','measurement'],
- columns='channel',
- values='SNR'
- )
- # Rename columns
- fmatrix.columns = [f'snr_channel_{col}' for col in fmatrix.columns]
- # Reset index
- fmatrix.reset_index(inplace=True)
- # Remove unnecessary columns
- fmatrix = fmatrix.drop(columns=['envelope','measurement'])
- # Separate features and labels
- X = fmatrix.drop('shape', axis=1)
- y = fmatrix['shape']
- # ------------------------------------------------
- # Standardise features (IMPORTANT for PCA)
- # ------------------------------------------------
- scaler = StandardScaler()
- X_scaled = scaler.fit_transform(X)
- # ------------------------------------------------
- # PCA
- # ------------------------------------------------
- pca = PCA(n_components=2)
- X_pca = pca.fit_transform(X_scaled)
- # Compute loadings (correlation circle coordinates)
- loadings = pca.components_.T * np.sqrt(pca.explained_variance_)
- # ------------------------------------------------
- # Plot correlation circle
- # ------------------------------------------------
- fig, ax = plt.subplots(figsize=(8,8), constrained_layout=True)
- # Draw unit circle
- circle = plt.Circle((0,0),1,fill=False,linestyle='--', color='black')
- ax.add_artist(circle)
- # Create colour mapping by channel index
- num_features = len(X.columns)
- colors = cm.viridis(np.linspace(0,1,num_features))
- channel_numbers = [int(col.split('_')[-1]) for col in X.columns]
- # Plot arrows (THICKER)
- for i, feature in enumerate(X.columns):
- ax.arrow(
- 0, 0,
- loadings[i,0],
- loadings[i,1],
- color=colors[i],
- linewidth=2.0, # <-- thicker arrows
- head_width=0.03, # slightly larger arrowhead
- length_includes_head=True
- )
- # Add colorbar
- norm = plt.Normalize(min(channel_numbers), max(channel_numbers))
- sm = plt.cm.ScalarMappable(cmap='viridis', norm=norm)
- sm.set_array([])
- cbar = fig.colorbar(sm, ax=ax)
- cbar.set_label('Channel number')
- # Axis formatting (BLACK axes)
- ax.axhline(0, color='black', linewidth=1.5)
- ax.axvline(0, color='black', linewidth=1.5)
- # Make frame/spines black
- for spine in ax.spines.values():
- spine.set_color('black')
- spine.set_linewidth(1.5)
- # Tick colors
- ax.tick_params(colors='black')
- ax.set_xlim(-1.1,1.1)
- ax.set_ylim(-1.1,1.1)
- ax.set_aspect('equal')
- ax.set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]*100:.1f}% variance)')
- ax.set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]*100:.1f}% variance)')
- ax.set_title('Correlation Circle (Muscular BSPM)')
- plt.show()
- # %% [markdown]
- # #### Muscular BSPM
- # %%
- import matplotlib.pyplot as plt
- import numpy as np
- from biolab.bspm import interpolate,BSPM
- # Read .bspm data for sphere
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification\EMG_ball_1.bspm'
- EMG = BSPM.load(loadpath=readpath)
- rEMG_sphere = EMG.resample(newfs=800)
- # Display muscular BSPM
- vmap = rEMG_sphere.potential(time=78.70023333333333)
- abmap = np.abs(vmap)
- frame = interpolate(valmap=abmap,method='cubic')
- plt.imshow(frame,vmin=0,vmax=1500,cmap='viridis')
- cbar = plt.colorbar()
- cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_3\Sphere_viridis.pdf',dpi=1000)
- plt.show()
- # Read .bspm data for cylinder
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification\EMG_cylinder_1.bspm'
- EMG = BSPM.load(loadpath=readpath)
- rEMG_cylinder = EMG.resample(newfs=800)
- # Display muscular BSPM
- vmap = rEMG_cylinder.potential(time=50.11526666666666)
- abmap = np.abs(vmap)
- frame = interpolate(valmap=abmap,method='cubic')
- plt.imshow(frame,vmin=0,vmax=1500,cmap='viridis')
- cbar = plt.colorbar()
- cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_3\Cylinder_viridis.pdf',dpi=1000)
- plt.show()
- # Read .bspm data for no object
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification\EMG_emptyhand_1.bspm'
- EMG = BSPM.load(loadpath=readpath)
- rEMG_noobject = EMG.resample(newfs=800)
- # Display muscular BSPM
- vmap = rEMG_noobject.potential(time=35.5237)
- abmap = np.abs(vmap)
- frame = interpolate(valmap=abmap,method='cubic')
- plt.imshow(frame,vmin=0,vmax=1500,cmap='viridis')
- cbar = plt.colorbar()
- cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_3\No_object_viridis.pdf',dpi=1000)
- plt.show()
- # %% [markdown]
- # ### Position classification from cardiac BSPM
- # %% [markdown]
- # #### Data import
- # %%
- import os
- import re
- from biolab.bspm import BSPM,image2layout
- from biolab.utils import apply2all
- # Save new recordings into .bspm files
- # Define parameters
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings\ECG_classification'
- # Convert electrode array image into layout
- ECG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\ECG_lyt.jpg',seq='cw')
- # Define operation to be applied to each file
- def func(filepath):
- # Extract filename from filepath
- filename = os.path.splitext(os.path.basename(filepath))[0]
- # Define the regex pattern to match the required values
- pattern = r'^([A-Za-z]+)_([A-Za-z-]+)_M(\d+)_\d{6}_\d{6}$'
- # Search for the pattern in the given filename
- match = re.search(pattern,filename)
- if match:
- filename = match.group(1)+'_'+match.group(2)
- mode = match.group(1) # type of signal (i.e. ECG, EMG, EEG)
- position = match.group(2) # position of the subject
- measurement = match.group(3) # measurement
- else:
- raise ValueError("Filename \"{}\" does not match the expected pattern.".format(filename))
- meta = {
- 'mode': mode,
- 'measurement': measurement,
- 'position': {'standing':'standing','sitting':'sitting','lying-side':'sideways','lying-up':'lying down'}.get(position,None)
- }
- # Load BSPM data from .rhs file
- MP = BSPM.from_file(filepath=filepath,layout=ECG_lyt,metadata=meta,refchs=[])
- # Save BSPM object
- MP.save(savepath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\\'+filename+'.bspm')
- # Call DataSaver function
- apply2all(read_directory=readpath,extension='.rhs',func=func)
- # %% [markdown]
- # #### Feature extraction
- # %% [markdown]
- # RR intervals from all channels
- # %%
- import wfdb.processing
- import numpy as np
- import pandas as pd
- from biolab.bspm import BSPM
- from biolab.utils import apply2all
- # Define read directory
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification'
- # Define apply2all function
- def fun(filepath):
- # Define new dataframe
- df = pd.DataFrame()
- # Load .bspm data
- ECG = BSPM.load(loadpath=filepath)
- # Resample and preprocess the data
- rECG = ECG.resample(newfs=1000)
- rECG.preprocess(bw=(0.5,20))
- for col in rECG.preprocessed_data.columns:
- # Find R-R intervals
- rpeaks = wfdb.processing.xqrs_detect(rECG.preprocessed_data[col][0:119.75].values,fs=1000,verbose=False)
- rrintv = wfdb.processing.calc_rr(qrs_locs=rpeaks)
- # Check the current number of rows in the dataframe
- current_len = len(df)
- new_len = len(rrintv)
- # If the DataFrame has more rows than rrintv and the DataFrame is not empty
- if (current_len<new_len) & (current_len!=0):
- # Calculate the mean of rrintv
- rrintv_mean = np.mean(rrintv)
- # Calculate the absolute deviation from the mean
- deviations = np.abs(rrintv-rrintv_mean)
- # Get the indices of the most different (largest deviations) values
- indices_to_remove = np.argsort(deviations)[:(new_len-current_len)] # Select top largest deviations
- # Remove excess values by excluding them
- rrintv = np.delete(rrintv,indices_to_remove)
- # If rrintv is shorter than the DataFrame, truncate the DataFrame to match the length of rrintv
- elif current_len>new_len:
- # Remove rows from the dataframe to match rrintv length
- df = df.iloc[:new_len]
- df['RRinterval_ch{}'.format(col)] = rrintv
- df['cycle'] = np.arange(1,len(rrintv)+1)
- df['position'] = rECG.metadata['position']
- # Append the DataFrame to an existing Excel file
- excel_path = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\RR_interval_feature_matrix.xlsx'
- # Load the existing data from the Excel file
- try:
- existing_df = pd.read_excel(excel_path)
- # Append the new data to the existing data
- combined_df = pd.concat([existing_df,df],ignore_index=True)
- except FileNotFoundError:
- # If the file doesn't exist, just use the new data
- combined_df = df
- # Write the combined data back to the Excel file
- with pd.ExcelWriter(excel_path,mode='w',engine='openpyxl') as writer:
- combined_df.to_excel(writer,index=False)
- # Apply function to all files in read directory
- apply2all(read_directory=readpath,extension='.bspm',func=fun)
- # %% [markdown]
- # Delays from all channels
- # %%
- import wfdb.processing
- import numpy as np
- import pandas as pd
- from biolab.bspm import BSPM
- from biolab.utils import apply2all
- # Define read directory
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification'
- # Define apply2all function
- def fun(filepath):
- # Load .bspm data
- ECG = BSPM.load(loadpath=filepath)
- # Resample and preprocess the data
- rECG = ECG.resample(newfs=2000)
- rECG.preprocess(bw=(0.5,20))
- # Find R peaks
- peaks_per_channel = {}
- for col in rECG.preprocessed_data.columns:
- peaks_per_channel[col] = wfdb.processing.xqrs_detect(rECG.preprocessed_data[col][0:119.75].values,fs=2000,verbose=False)
- # Get the peak indices for channel 1
- channel_1_peaks = peaks_per_channel[rECG.preprocessed_data.columns[0]]
- # Initialise list to store delays for valid cycles
- delays = []
- # Define ECG peak delay tolerance
- tolerance = 0.01
- # Calculate delays for each cycle
- for peak_1 in channel_1_peaks:
- cycle_delays = []
- valid_cycle = True
- for col in rECG.preprocessed_data.columns[1:]: # Skip channel 1, compare all others to it
- channel_peaks = peaks_per_channel[col]
- # Find the closest peak in the current channel within the tolerance window
- valid_peaks = np.abs(channel_peaks-peak_1)<=tolerance*rECG.metadata['amplifier_sample_rate']
- if np.any(valid_peaks): # If there's a valid peak
- closest_peak = channel_peaks[valid_peaks][0] # Get the first valid peak
- delay = (closest_peak-peak_1)/rECG.metadata['amplifier_sample_rate'] # Convert to seconds
- cycle_delays.append(delay)
- else:
- valid_cycle = False
- break # If any channel does not have a valid peak for this cycle, skip it
- # If the cycle was valid, store the delays
- if valid_cycle:
- delays.append(cycle_delays)
- # Convert delays to DataFrame
- if delays: # Only create DataFrame if there are valid cycles
- delay_df = pd.DataFrame(delays)
- delay_df.columns = [f'Delay_ch{col}' for col in rECG.preprocessed_data.columns[1:]]
- else:
- delay_df = pd.DataFrame() # Return an empty DataFrame if no valid cycles are found
- delay_df['position'] = rECG.metadata['position']
- # Append the DataFrame to an existing Excel file
- excel_path = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\Delay_feature_matrix.xlsx'
- # Load the existing data from the Excel file
- try:
- existing_df = pd.read_excel(excel_path)
- # Append the new data to the existing data
- combined_df = pd.concat([existing_df,delay_df],ignore_index=True)
- except FileNotFoundError:
- # If the file doesn't exist, just use the new data
- combined_df = delay_df
- # Write the combined data back to the Excel file
- with pd.ExcelWriter(excel_path,mode='w',engine='openpyxl') as writer:
- combined_df.to_excel(writer,index=False)
- # Apply function to all files in read directory
- apply2all(read_directory=readpath,extension='.bspm',func=fun)
- # %% [markdown]
- # #### Feature values inspection
- # %% [markdown]
- # RR intervals
- # %%
- import pandas as pd
- import seaborn as sns
- import matplotlib.pyplot as plt
- # Import SNR data from Excel
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\RR_interval_feature_matrix.xlsx'
- df = pd.read_excel(io=readpath)
- # Display RR interval as a function of position
- df = df.sort_values(by='position')
- sns.violinplot(x='position',y='RRinterval_ch9',data=df,order=df['position'].unique())
- plt.title('')
- plt.xlabel('Position')
- plt.ylabel('RR Interval [$\mu$s]')
- # Adjust layout to prevent overlap of titles and labels
- plt.tight_layout()
- plt.show()
- # %% [markdown]
- # *Mild differences between lying and upward positions can be observed. No clear distinction can be observed between sitting and standing or lying down or sideways.*
- # %% [markdown]
- # Delays
- # %%
- import pandas as pd
- import seaborn as sns
- import numpy as np
- import matplotlib.pyplot as plt
- # Import SNR data from Excel
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\Delay_feature_matrix.xlsx'
- df = pd.read_excel(io=readpath)
- # Display RR interval as a function of position
- df = df.sort_values(by='position')
- sns.violinplot(x='position',y='Delay_ch16',data=df,order=df['position'].unique())
- plt.title('')
- plt.xlabel('Position')
- plt.ylabel('Delay CH1 - CH9 [s]')
- # Adjust layout to prevent overlap of titles and labels
- plt.tight_layout()
- plt.show()
- # Generate delay plots
- # Group by position and compute average delay and standard deviation
- avg = df.groupby(by='position').mean()
- stdv = df.groupby(by='position').std()
- avg = avg[['Delay_ch2','Delay_ch3','Delay_ch4','Delay_ch5','Delay_ch6','Delay_ch7','Delay_ch8','Delay_ch9','Delay_ch10','Delay_ch11','Delay_ch12','Delay_ch13','Delay_ch14','Delay_ch15','Delay_ch16']]
- stdv = stdv[['Delay_ch2','Delay_ch3','Delay_ch4','Delay_ch5','Delay_ch6','Delay_ch7','Delay_ch8','Delay_ch9','Delay_ch10','Delay_ch11','Delay_ch12','Delay_ch13','Delay_ch14','Delay_ch15','Delay_ch16']]
- plt.plot(np.arange(2,17),avg.iloc[0,:].values)
- plt.fill_between(np.arange(2,17),avg.iloc[0,:].values-stdv.iloc[0,:].values,avg.iloc[0,:].values+stdv.iloc[0,:].values,color='blue',alpha=0.2)
- plt.plot(np.arange(2,17),avg.iloc[1,:].values)
- plt.fill_between(np.arange(2,17),avg.iloc[1,:].values-stdv.iloc[1,:].values,avg.iloc[1,:].values+stdv.iloc[1,:].values,color='red',alpha=0.2)
- plt.plot(np.arange(2,17),avg.iloc[2,:].values)
- plt.fill_between(np.arange(2,17),avg.iloc[2,:].values-stdv.iloc[2,:].values,avg.iloc[2,:].values+stdv.iloc[2,:].values,color='green',alpha=0.2)
- plt.plot(np.arange(2,17),avg.iloc[3,:].values)
- plt.fill_between(np.arange(2,17),avg.iloc[3,:].values-stdv.iloc[3,:].values,avg.iloc[3,:].values+stdv.iloc[3,:].values,color='orange',alpha=0.2)
- plt.show()
- # %% [markdown]
- # *Clear differences can be observed between delays for all four positions and across channels.*
- # %%
- import numpy as np
- import pandas as pd
- import matplotlib.pyplot as plt
- # Import SNR data from Excel
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\Delay_feature_matrix.xlsx'
- df = pd.read_excel(io=readpath)
- # Set 'Position' as the index
- df.set_index('position',inplace=True)
- # Group by 'Position' and calculate mean and std for each channel
- grouped = df.groupby('position').agg(['mean','std'])
- # Channels as columns (without multi-indexing)
- channels = df.columns
- num_channels = len(channels)
- # Prepare the angle array for the radar chart (evenly distributed for each channel)
- angles = np.linspace(0,2*np.pi,num_channels,endpoint=False).tolist()
- # Close the loop by repeating the first angle
- angles += angles[:1]
- # Create a figure for the radar chart
- fig, ax = plt.subplots(figsize=(6,6),subplot_kw=dict(polar=True))
- # Loop through each position and plot the radar chart
- color = {'lying down':'blue','sideways':'red','sitting':'green','standing':'orange'}
- for position in grouped.index:
- # Calculate the mean and standard deviation for each channel
- mean_delays = grouped.loc[position,(channels,'mean')].values
- std_delays = grouped.loc[position,(channels,'std')].values
- # Close the loop by appending the first value of the mean and std
- mean_delays = np.append(mean_delays,mean_delays[0])
- std_delays = np.append(std_delays,std_delays[0])
- # Plot the shaded standard deviation area
- ax.fill(angles,mean_delays+std_delays,color=color[position],alpha=0.2) # Shaded area
- ax.fill(angles, mean_delays - std_delays, color=color[position], alpha=0.2) # Shaded area
- # Plot the mean delay
- ax.plot(angles, mean_delays, label=f'Position {position}', linewidth=2, marker='o')
- # Remove radial ticks
- ax.set_yticklabels([])
- # Set axis labels for the channels
- ax.set_xticks(angles[:-1]) # Set the angles for the channels
- ax.set_xticklabels(channels)
- # Set the title of the radar chart
- ax.set_title("Radar Chart: Average Delay Between ECG Peaks with Std Dev", size=16, color='black', y=1.1)
- # Add a legend
- ax.legend(title="Positions", loc='upper right')
- # Show the chart
- plt.show()
- # %% [markdown]
- # #### Machine learning classification of positions
- # %% [markdown]
- # Single electrode (middle - control electrode)
- # %%
- import pandas as pd
- from sklearn.model_selection import train_test_split
- from sklearn.linear_model import LogisticRegression
- from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
- from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay,make_scorer,precision_score,recall_score,f1_score
- import matplotlib.pyplot as plt
- # Import SNR data from Excel
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\RR_interval_feature_matrix.xlsx'
- df = pd.read_excel(io=readpath)
- # Generate feature matrix from data
- fmatrix = df[['RRinterval_ch9', 'position']]
- # Separate features and labels
- X = fmatrix.drop('position', axis=1) # feature matrix
- y = fmatrix['position'] # labels
- # Split the data into training and testing sets
- X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=fmatrix['position'])
- # Initialise and fit the logistic regression
- clf = LogisticRegression()
- clf.fit(X_train,y_train)
- # Predict the labels for the test set
- y_pred = clf.predict(X_test)
- # Calculate the confusion matrix
- cm = confusion_matrix(y_test,y_pred)
- # Display the confusion matrix
- disp = ConfusionMatrixDisplay(confusion_matrix=cm,display_labels=clf.classes_)
- fig,ax = plt.subplots(figsize=(6,6)) # Adjust figure size if needed
- # Set the color range limits
- vmin,vmax = 0,40 # Adjust these values as needed
- disp.plot(cmap="Blues",ax=ax,colorbar=False,values_format="d")
- disp.im_.set_clim(vmin,vmax) # Set the colorbar limits
- # Adjust the colorbar size
- cbar = ax.figure.colorbar(disp.im_,fraction=0.046,pad=0.04)
- cbar.set_label("Count") # Label for better readability
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Confusion_matrices\Single_channel_ECG',dpi=1000)
- # Repeated Cross-Validation Accuracy
- rkf = RepeatedStratifiedKFold(n_splits=10,n_repeats=5)
- accuracy_scores = cross_val_score(clf,X,y,cv=rkf)
- precision_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(precision_score, average='macro'))
- recall_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(recall_score, average='macro'))
- f1_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(f1_score, average='macro'))
- # Print results
- print(f'Repeated Cross-Validation Accuracy: {accuracy_scores.mean():.2f} ± {accuracy_scores.std():.2f}')
- print(f"Repeated Cross-Validation Precision: {precision_scores.mean():.2f} ± {precision_scores.std():.2f}")
- print(f"Repeated Cross-Validation Recall: {recall_scores.mean():.2f} ± {recall_scores.std():.2f}")
- print(f"Repeated Cross-Validation F1 Score: {f1_scores.mean():.2f} ± {f1_scores.std():.2f}")
- # %% [markdown]
- # Electrode array
- # %%
- import pandas as pd
- from sklearn.model_selection import train_test_split
- from sklearn.linear_model import LogisticRegression
- from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
- from sklearn.preprocessing import StandardScaler
- from sklearn.pipeline import Pipeline
- from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay,make_scorer,precision_score,recall_score,f1_score
- import matplotlib.pyplot as plt
- # Import SNR data from Excel
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\Delay_feature_matrix.xlsx'
- df = pd.read_excel(io=readpath)
- # Generate feature matrix from data
- fmatrix = df
- # Separate features and labels
- X = fmatrix.drop('position',axis=1) # feature matrix
- y = fmatrix['position'] # labels
- # Split the data into training and testing sets
- X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=fmatrix['position'])
- # Define a pipeline with scaling and logistic regression
- pipeline = Pipeline([
- ('scaler',StandardScaler()), # Standardizes the data
- ('classifier',LogisticRegression()) # Logistic Regression model
- ])
- # Fit the model using only training data
- pipeline.fit(X_train,y_train)
- # Predict the labels for the test set
- y_pred = pipeline.predict(X_test)
- # Calculate the confusion matrix
- cm = confusion_matrix(y_test,y_pred)
- # Display the confusion matrix
- disp = ConfusionMatrixDisplay(confusion_matrix=cm,display_labels=pipeline.classes_)
- fig,ax = plt.subplots(figsize=(6,6)) # Adjust figure size if needed
- # Set the color range limits
- vmin,vmax = 0,40 # Adjust these values as needed
- disp.plot(cmap="Blues",ax=ax,colorbar=False,values_format="d")
- disp.im_.set_clim(vmin,vmax) # Set the colorbar limits
- # Adjust the colorbar size
- cbar = ax.figure.colorbar(disp.im_,fraction=0.046,pad=0.04)
- cbar.set_label("Count") # Label for better readability
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Confusion_matrices\BSPM_ECG',dpi=1000)
- # Repeated Cross-Validation Accuracy
- rkf = RepeatedStratifiedKFold(n_splits=10,n_repeats=5)
- accuracy_scores = cross_val_score(pipeline,X,y,cv=rkf)
- precision_scores = cross_val_score(pipeline, X, y, cv=rkf, scoring=make_scorer(precision_score, average='macro'))
- recall_scores = cross_val_score(pipeline, X, y, cv=rkf, scoring=make_scorer(recall_score, average='macro'))
- f1_scores = cross_val_score(pipeline, X, y, cv=rkf, scoring=make_scorer(f1_score, average='macro'))
- # Print results
- print(f'Repeated Cross-Validation Accuracy: {accuracy_scores.mean():.2f} ± {accuracy_scores.std():.2f}')
- print(f"Repeated Cross-Validation Precision: {precision_scores.mean():.2f} ± {precision_scores.std():.2f}")
- print(f"Repeated Cross-Validation Recall: {recall_scores.mean():.2f} ± {recall_scores.std():.2f}")
- print(f"Repeated Cross-Validation F1 Score: {f1_scores.mean():.2f} ± {f1_scores.std():.2f}")
- # %% [markdown]
- # #### Cardiac BSPM
- # %%
- import matplotlib.pyplot as plt
- import numpy as np
- from biolab.bspm import interpolate,BSPM
- # Read .bspm data for sphere
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\ECG_lying-side.bspm'
- ECG = BSPM.load(loadpath=readpath)
- rECG_sideways = ECG.resample(newfs=200)
- rECG_sideways.preprocess(bw=(0.5,99))
- # Display cardiac BSPM
- vmap = rECG_sideways.potential(time=10.385)
- frame = interpolate(valmap=np.where(np.isnan(-vmap),-vmap,-vmap/np.nanmax(-vmap)),method='cubic')
- plt.imshow(frame,vmin=0.5,vmax=1)
- cbar = plt.colorbar()
- cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Sideways.pdf',dpi=1000)
- plt.show()
- # Read .bspm data for sphere
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\ECG_lying-up.bspm'
- ECG = BSPM.load(loadpath=readpath)
- rECG_lying_down = ECG.resample(newfs=200)
- rECG_lying_down.preprocess(bw=(0.5,99))
- # Display cardiac BSPM
- vmap = rECG_lying_down.potential(time=10.47)
- frame = interpolate(valmap=np.where(np.isnan(-vmap),-vmap,-vmap/np.nanmax(-vmap)),method='cubic')
- plt.imshow(frame,vmin=0.5,vmax=1)
- cbar = plt.colorbar()
- cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Lying down.pdf',dpi=1000)
- plt.show()
- # Read .bspm data for sphere
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\ECG_sitting.bspm'
- ECG = BSPM.load(loadpath=readpath)
- rECG_sitting = ECG.resample(newfs=200)
- rECG_sitting.preprocess(bw=(0.5,99))
- # Display cardiac BSPM
- vmap = rECG_sitting.potential(time=10.235)
- frame = interpolate(valmap=np.where(np.isnan(-vmap),-vmap,-vmap/np.nanmax(-vmap)),method='cubic')
- plt.imshow(frame,vmin=0.5,vmax=1)
- cbar = plt.colorbar()
- cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Sitting.pdf',dpi=1000)
- plt.show()
- # Read .bspm data for sphere
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\ECG_standing.bspm'
- ECG = BSPM.load(loadpath=readpath)
- rECG_standing = ECG.resample(newfs=200)
- rECG_standing.preprocess(bw=(0.5,99))
- # Display cardiac BSPM
- vmap = rECG_standing.potential(time=10.515)
- frame = interpolate(valmap=np.where(np.isnan(-vmap),-vmap,-vmap/np.nanmax(-vmap)),method='cubic')
- plt.imshow(frame,vmin=0.5,vmax=1)
- cbar = plt.colorbar()
- cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Standing.pdf',dpi=1000)
- plt.show()
- # %%
- import pandas as pd
- def delay(bspm,delays) -> np.ndarray:
- # Create copy of mapped layout
- dmap = np.copy(bspm.layout).astype(float)
- dmap[dmap == 0] = np.NaN
- # Convert channel_data to numpy arrays
- locations = bspm.channel_data['location'].values
- # Change value of label in layout to potential taking into account reference channels
- # Create a mapping from location to voltage channel
- location_to_voltage = dict(zip(locations,delays))
- # Replace values in zmap based on location_to_impedance mapping
- mask = np.isin(bspm.layout,locations)
- dmap[mask] = np.vectorize(location_to_voltage.get)(bspm.layout[mask])
- return dmap
- data = pd.read_excel(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\Delay_feature_matrix.xlsx')
- data_avg = data.groupby('position',as_index=False).mean().drop(columns='position')
- # Display cardiac delay maps
- dmap = delay(rECG_sideways,np.insert(data_avg.iloc[0,:].values,13,0.0)*1000)
- frame = interpolate(valmap=dmap,method='cubic')
- plt.imshow(frame,vmin=0,vmax=10)
- cbar = plt.colorbar()
- cbar.set_label('Delay [ms]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Sideways_delay.pdf',dpi=1000)
- plt.show()
- dmap = delay(rECG_lying_down,np.insert(data_avg.iloc[1,:].values,13,0.0)*1000)
- frame = interpolate(valmap=dmap,method='cubic')
- plt.imshow(frame,vmin=0,vmax=10)
- cbar = plt.colorbar()
- cbar.set_label('Delay [ms]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Lying_down_delay.pdf',dpi=1000)
- plt.show()
- dmap = delay(rECG_sitting,np.insert(data_avg.iloc[2,:].values,13,0.0)*1000)
- frame = interpolate(valmap=dmap,method='cubic')
- plt.imshow(frame,vmin=0,vmax=10)
- cbar = plt.colorbar()
- cbar.set_label('Delay [ms]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Sitting_delay.pdf',dpi=1000)
- plt.show()
- dmap = delay(rECG_standing,np.insert(data_avg.iloc[3,:].values,13,0.0)*1000)
- frame = interpolate(valmap=dmap,method='cubic')
- plt.imshow(frame,vmin=0,vmax=10)
- cbar = plt.colorbar()
- cbar.set_label('Delay [ms]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Standing_delay.pdf',dpi=1000)
- plt.show()
- # %% [markdown]
- # ### Sensorimotor classification from cerebral BSPM
- # %% [markdown]
- # #### Data import
- # %%
- import os
- import re
- from biolab.bspm import BSPM,image2layout
- from biolab.utils import apply2all
- # Save new recordings into .bspm files
- # Define parameters
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings\EEG_classification'
- # Convert electrode array image into layout
- ECG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EEG_lyt.jpg',seq='cw')
- # Define operation to be applied to each file
- def func(filepath):
- # Extract filename from filepath
- filename = os.path.splitext(os.path.basename(filepath))[0]
- # Define the regex pattern to match the required values
- pattern = r'^([A-Za-z_]+)_\d{6}_\d{6}$'
- # Search for the pattern in the given filename
- match = re.search(pattern,filename)
- if match:
- filename = match.group(1)
- if filename == 'muscle activity':
- mode = 'EEG/EMG'
- else:
- mode = 'EEG'
- sensorimotor = filename
- measurement = 'M1'
- else:
- raise ValueError("Filename \"{}\" does not match the expected pattern.".format(filename))
- meta = {
- 'mode': mode,
- 'measurement': measurement,
- 'sensorimotor': {'auditory_stimulus':'auditory','eyes_closed_open':'eye closing','mental_calculus':'mental calculus','muscle_activity':'motor','visual_stimulus':'visual'}.get(sensorimotor,None)
- }
- # Load BSPM data from .rhs file
- MP = BSPM.from_file(filepath=filepath,layout=ECG_lyt,metadata=meta,refchs=[])
- # Save BSPM object
- MP.save(savepath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\\'+filename+'.bspm')
- # Call DataSaver function
- apply2all(read_directory=readpath,extension='.rhs',func=func)
- # %% [markdown]
- # #### Data visualisation
- # %% [markdown]
- # Changes in alpha wave activity when opening and closing eyes
- # %%
- import pandas as pd
- import plotly.express as px
- from scipy.signal import butter,filtfilt
- from biolab.bspm import BSPM
- # Define read directory
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\Testing\eyes_closed_open.bspm'
- # Load .bspm data
- EEG = BSPM.load(loadpath=readpath)
- # Resample and preprocess the data
- rEEG = EEG.resample(newfs=1000)
- rEEG.preprocess(bw=(0.5,100))
- # Define bandpass filter function
- def bandpass_filter(data,lowcut,highcut,sf,order=4):
- return filtfilt(*butter(order,[lowcut,highcut],fs=sf,btype='band'),data)
- # Define EEG bands
- bands = {'Delta (0.5-4 Hz)': (1,4), 'Theta (4-8 Hz)': (4,8),
- 'Alpha (8-13 Hz)': (8,13), 'Beta (13-30 Hz)': (13,30),
- 'Gamma (30-100 Hz)': (30,100)}
- # Select one channel (e.g., 'A-000') and apply filters
- channel = 'A-000'
- filtered_signals = {band: bandpass_filter(rEEG.preprocessed_data[channel],low,high,1000) for band,(low,high) in bands.items()}
- # Create DataFrame for Plotly
- df_plot = pd.DataFrame({'Time (s)': rEEG.preprocessed_data.index.values})
- for band, signal in filtered_signals.items():
- df_plot[band] = signal
- # Melt DataFrame for Plotly Express
- df_melted = df_plot.melt(id_vars=['Time (s)'],var_name='Band',value_name='Amplitude')
- # Plot with Plotly Express
- fig = px.line(df_melted,x='Time (s)',y='Amplitude',color='Band',title=f'EEG Trace of {channel} Across Frequency Bands')
- fig.show()
- # %% [markdown]
- # Changes in theta wave activity when performing mental calculus
- # %%
- import pandas as pd
- import plotly.express as px
- from scipy.signal import butter,filtfilt
- from biolab.bspm import BSPM
- # Define read directory
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\Testing\mental_calculus.bspm'
- # Load .bspm data
- EEG = BSPM.load(loadpath=readpath)
- # Resample and preprocess the data
- rEEG = EEG.resample(newfs=1000)
- rEEG.preprocess(bw=(0.5,100))
- # Define bandpass filter function
- def bandpass_filter(data,lowcut,highcut,sf,order=4):
- return filtfilt(*butter(order,[lowcut,highcut],fs=sf,btype='band'),data)
- # Define EEG bands
- bands = {'Delta (0.5-4 Hz)': (1,4), 'Theta (4-8 Hz)': (4,8),
- 'Alpha (8-13 Hz)': (8,13), 'Beta (13-30 Hz)': (13,30),
- 'Gamma (30-100 Hz)': (30,100)}
- # Select one channel (e.g., 'A-000') and apply filters
- channel = 'A-000'
- filtered_signals = {band: bandpass_filter(rEEG.preprocessed_data[channel],low,high,1000) for band,(low,high) in bands.items()}
- # Create DataFrame for Plotly
- df_plot = pd.DataFrame({'Time (s)': rEEG.preprocessed_data.index.values})
- for band, signal in filtered_signals.items():
- df_plot[band] = signal
- # Melt DataFrame for Plotly Express
- df_melted = df_plot.melt(id_vars=['Time (s)'],var_name='Band',value_name='Amplitude')
- # Plot with Plotly Express
- fig = px.line(df_melted,x='Time (s)',y='Amplitude',color='Band',title=f'EEG Trace of {channel} Across Frequency Bands')
- fig.show()
- # %% [markdown]
- # #### Feature extraction
- # %%
- import numpy as np
- import pandas as pd
- import scipy.signal as signal
- from biolab.utils import apply2all
- from biolab.bspm import BSPM
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification'
- def compute_band_power(eeg_signal,sf,band,method='welch'):
- low,high = band
- # Compute Power Spectral Density (PSD)
- if method == 'welch':
- freqs,psd = signal.welch(eeg_signal,sf,nperseg=sf*2) # 2-sec windows
- else: # FFT method
- freqs = np.fft.rfftfreq(len(eeg_signal),d=1/sf)
- psd = np.abs(np.fft.rfft(eeg_signal))**2/len(eeg_signal)
- # Integrate PSD within the frequency band
- band_power = np.trapz(psd[(freqs>=low)&(freqs<=high)],freqs[(freqs>=low)&(freqs<=high)])
- return band_power
- # Define bands
- bands = {'Delta':(0.5,4),'Theta':(4,8),'Alpha':(8,13),'Beta':(13,30),'Gamma':(30,100)}
- def f(filepath):
- EEG = BSPM.load(loadpath=filepath)
- rEEG = EEG.resample(newfs=1000)
- rEEG.preprocess(bw=(0.5,100))
- sf = 1000 # Sampling frequency (Hz)
- window_size = 2*sf # 2-second window
- gap_size = 2*sf # 2-second window
- start_idx = 10*sf # Start after 10 seconds
- num_samples = len(rEEG.preprocessed_data)
- # Create an empty list to store computed band power values
- band_power_list = []
- # Iterate over 2-second non-overlapping windows after the 10-second mark
- for start in range(start_idx,num_samples,window_size+gap_size):
- end = start+window_size
- if end>num_samples: # Ensure the window does not exceed data length
- break
- # Extract windowed EEG data (2-sec segment)
- segment = rEEG.preprocessed_data.iloc[start:end]
- # Compute band power for each channel-band combination
- band_power_dict = {}
- for band_name,band_range in bands.items():
- band_power_values = segment.apply(lambda x: compute_band_power(x.values,sf,band_range),axis=0)
- # Rename columns to match channel + band (e.g., Ch1_Delta, Ch1_Theta)
- for ch in band_power_values.index:
- band_power_dict[f"{ch}_{band_name}"] = band_power_values[ch]
- # Append result as a row in the list
- band_power_list.append(band_power_dict)
- # Convert list to DataFrame
- df_band_power = pd.DataFrame(band_power_list)
- df_band_power['action'] = EEG.metadata['sensorimotor']
- excel_path = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\EEG_feature_matrix.xlsx'
- # Load the existing data from the Excel file
- try:
- existing_df = pd.read_excel(excel_path)
- # Append the new data to the existing data
- combined_df = pd.concat([existing_df,df_band_power],ignore_index=True)
- except FileNotFoundError:
- # If the file doesn't exist, just use the new data
- combined_df = df_band_power
- # Write the combined data back to the Excel file
- with pd.ExcelWriter(excel_path,mode='w',engine='openpyxl') as writer:
- combined_df.to_excel(writer,index=False)
- apply2all(read_directory=readpath,extension='.bspm',exclude=['Testing'],func=f)
- # %% [markdown]
- # #### Feature values inspection
- # %%
- import matplotlib.pyplot as plt
- import pandas as pd
- import seaborn as sns
- # Import EEG waves data
- df = pd.read_excel(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\EEG_feature_matrix.xlsx')
- # Reshaping DataFrame: Convert from Wide to Long Format
- df_long = df.melt(id_vars=["action"], var_name="Channel_Band", value_name="Power")
- # Splitting "Channel_Band" into "Channel" and "Band"
- df_long[['Channel', 'Band']] = df_long['Channel_Band'].str.extract(r"(A-\d{3})_(\w+)")
- # Function to Remove Outliers using IQR
- def remove_outliers(group):
- Q1 = group["Power"].quantile(0.25)
- Q3 = group["Power"].quantile(0.75)
- IQR = Q3 - Q1
- lower_bound = Q1 - 1.5 * IQR
- upper_bound = Q3 + 1.5 * IQR
- return group[(group["Power"] >= lower_bound) & (group["Power"] <= upper_bound)]
- # Apply outlier removal
- df_filtered = df_long.groupby(["Channel", "Band"]).apply(remove_outliers).reset_index(drop=True)
- # Drop high-impedance channels
- channels_to_remove = ["A-004", "A-025", "A-030", "A-031"]
- df_filtered = df_filtered[~df_filtered["Channel"].isin(channels_to_remove)]
- # Get unique EEG bands
- bands = df_filtered["Band"].unique()
- # Create subplots (one for each band)
- fig, axes = plt.subplots(nrows=len(bands), figsize=(15, 3 * len(bands)), sharex=True)
- for i, band in enumerate(bands):
- ax = axes[i] if len(bands) > 1 else axes # Adjust for single-band case
- band_data = df_filtered[df_filtered["Band"] == band] # Filter for current band
- sns.violinplot(x="Channel", y="Power", hue="action", data=band_data, dodge=True,palette="muted", ax=ax)
- ax.set_title(f"{band} Band Power Distribution Across Channels")
- ax.set_ylabel("Power")
- ax.set_xlabel("")
- ax.tick_params(axis='x', rotation=90) # Rotate x-axis labels for readability
- # Add x-axis label
- plt.xlabel("Channel")
- # Adjust layout & legend placement
- plt.tight_layout()
- plt.legend(title="Action",loc='upper left') # Move legend outside
- # Show the plot
- plt.show()
- # %% [markdown]
- # 1. Auditory Activity:
- # - Theta (4-8 Hz): Involved in auditory processing, especially during tasks requiring attention or memory.
- # - Gamma (30-100 Hz): Associated with auditory perception and integration, especially in response to complex sounds or language.
- #
- # 2. Visual Activity:
- # - Alpha (8-12 Hz): Typically decreases (desynchronizes) in occipital regions during visual processing, indicating engagement.
- # - Gamma (30-100 Hz): Related to visual perception, feature binding, and conscious awareness.
- #
- # 3. Motor Activity:
- # - Beta (13-30 Hz): In sensorimotor cortex, beta activity often decreases (event-related desynchronization, ERD) during movement or motor preparation and increases (event-related synchronization, ERS) after movement.
- # - Mu Rhythm (8-13 Hz): Similar to alpha but localized over motor cortex; suppressed during actual or imagined movement.
- #
- # Observation
- # - Auditory vs. Visual: Focus on Alpha (visual engagement) and Gamma (auditory processing).
- # - Auditory vs. Motor: Compare Theta/Gamma (auditory) with Beta/Mu (motor).
- # - Visual vs. Motor: Contrast Alpha (visual) with Beta/Mu (motor).
- # %% [markdown]
- # #### Machine learning classification of brain activity
- # %% [markdown]
- # Single electrode (middle - control electrode)
- # %%
- import pandas as pd
- from sklearn.model_selection import train_test_split
- from sklearn.linear_model import LogisticRegression
- from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
- from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, make_scorer, precision_score, recall_score, f1_score
- import matplotlib.pyplot as plt
- # Import SNR data from Excel
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\EEG_feature_matrix.xlsx'
- df = pd.read_excel(io=readpath)
- # Generate feature matrix from data
- fmatrix = df[['A-003_Delta','A-003_Theta','A-003_Alpha','A-003_Beta','A-003_Gamma','action']]
- # Separate features and labels
- X = fmatrix.drop('action', axis=1) # feature matrix
- y = fmatrix['action'] # labels
- # Split the data into training and testing sets
- X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=fmatrix['action'])
- # Initialise and fit the logistic regression
- clf = LogisticRegression()
- clf.fit(X_train,y_train)
- # Predict the labels for the test set
- y_pred = clf.predict(X_test)
- # Calculate the confusion matrix
- cm = confusion_matrix(y_test,y_pred)
- # Display the confusion matrix
- disp = ConfusionMatrixDisplay(confusion_matrix=cm,display_labels=clf.classes_)
- fig,ax = plt.subplots(figsize=(6,6)) # Adjust figure size if needed
- # Set the color range limits
- vmin,vmax = 0,25 # Adjust these values as needed
- disp.plot(cmap="Greens",ax=ax,colorbar=False,values_format="d")
- disp.im_.set_clim(vmin,vmax) # Set the colorbar limits
- # Adjust the colorbar size
- cbar = ax.figure.colorbar(disp.im_,fraction=0.046,pad=0.04)
- cbar.set_label("Count") # Label for better readability
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Confusion_matrices\Single_channel_EEG',dpi=1000)
- # Repeated Cross-Validation Accuracy
- rkf = RepeatedStratifiedKFold(n_splits=10,n_repeats=5)
- accuracy_scores = cross_val_score(clf,X,y,cv=rkf)
- precision_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(precision_score, average='macro'))
- recall_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(recall_score, average='macro'))
- f1_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(f1_score, average='macro'))
- # Print results
- print(f'Repeated Cross-Validation Accuracy: {accuracy_scores.mean():.2f} ± {accuracy_scores.std():.2f}')
- print(f"Repeated Cross-Validation Precision: {precision_scores.mean():.2f} ± {precision_scores.std():.2f}")
- print(f"Repeated Cross-Validation Recall: {recall_scores.mean():.2f} ± {recall_scores.std():.2f}")
- print(f"Repeated Cross-Validation F1 Score: {f1_scores.mean():.2f} ± {f1_scores.std():.2f}")
- # %% [markdown]
- # Electrode array
- # %%
- import pandas as pd
- from sklearn.model_selection import train_test_split
- from sklearn.linear_model import LogisticRegression
- from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
- from sklearn.preprocessing import StandardScaler
- from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, make_scorer, precision_score, recall_score, f1_score
- import matplotlib.pyplot as plt
- # Import SNR data from Excel
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\EEG_classification\EEG_feature_matrix.xlsx'
- df = pd.read_excel(io=readpath)
- # Generate feature matrix from data
- fmatrix = df
- # Separate features and labels
- X = fmatrix.drop('action',axis=1) # feature matrix
- y = fmatrix['action'] # labels
- # Split the data into training and testing sets
- X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=fmatrix['action'])
- # Initialise and fit the logistic regression
- clf = LogisticRegression()
- clf.fit(X_train,y_train)
- # Predict the labels for the test set
- y_pred = clf.predict(X_test)
- # Calculate the confusion matrix
- cm = confusion_matrix(y_test,y_pred)
- # Display the confusion matrix
- disp = ConfusionMatrixDisplay(confusion_matrix=cm,display_labels=clf.classes_)
- fig,ax = plt.subplots(figsize=(6,6)) # Adjust figure size if needed
- # Set the color range limits
- vmin,vmax = 0,25 # Adjust these values as needed
- disp.plot(cmap="Greens",ax=ax,colorbar=False,values_format="d")
- disp.im_.set_clim(vmin,vmax) # Set the colorbar limits
- # Adjust the colorbar size
- cbar = ax.figure.colorbar(disp.im_,fraction=0.046,pad=0.04)
- cbar.set_label("Count") # Label for better readability
- #plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Results\Confusion_matrices\BSPM_EEG',dpi=1000)
- # Repeated Cross-Validation Accuracy
- rkf = RepeatedStratifiedKFold(n_splits=10,n_repeats=5)
- accuracy_scores = cross_val_score(clf,X,y,cv=rkf)
- precision_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(precision_score, average='macro'))
- recall_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(recall_score, average='macro'))
- f1_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(f1_score, average='macro'))
- # Print results
- print(f'Repeated Cross-Validation Accuracy: {accuracy_scores.mean():.2f} ± {accuracy_scores.std():.2f}')
- print(f"Repeated Cross-Validation Precision: {precision_scores.mean():.2f} ± {precision_scores.std():.2f}")
- print(f"Repeated Cross-Validation Recall: {recall_scores.mean():.2f} ± {recall_scores.std():.2f}")
- print(f"Repeated Cross-Validation F1 Score: {f1_scores.mean():.2f} ± {f1_scores.std():.2f}")
- # %% [markdown]
- # Correlation circle for feature importance
- # %%
- import numpy as np
- import matplotlib.pyplot as plt
- from matplotlib.colors import to_rgba, Normalize
- from matplotlib.cm import ScalarMappable
- from sklearn.preprocessing import StandardScaler
- from sklearn.decomposition import PCA
- # ------------------------------------------------
- # Extract frequency and channel info from feature names
- # ------------------------------------------------
- feature_names = X.columns
- # Map canonical frequency bands to base colors
- freq_colors = {
- 'delta': 'blue',
- 'theta': 'green',
- 'alpha': 'red',
- 'beta': 'orange',
- 'gamma': 'purple'
- }
- frequencies = []
- channels = []
- for feat in feature_names:
- channel_part, freq_part = feat.split('_')
- channel_num = int(channel_part.split('-')[1])
- frequencies.append(freq_part.lower())
- channels.append(channel_num)
- # Normalize channels for alpha mapping
- chan_min = min(channels)
- chan_max = max(channels)
- channels_norm = [(c - chan_min)/(chan_max - chan_min) for c in channels] # 0 → 1
- # ------------------------------------------------
- # Standardize features and run PCA
- # ------------------------------------------------
- scaler = StandardScaler()
- X_scaled = scaler.fit_transform(X)
- pca = PCA(n_components=2)
- X_pca = pca.fit_transform(X_scaled)
- loadings = pca.components_.T * np.sqrt(pca.explained_variance_)
- # ------------------------------------------------
- # Plot correlation circle
- # ------------------------------------------------
- fig, ax = plt.subplots(figsize=(10,10), constrained_layout=True)
- # Unit circle
- circle = plt.Circle((0,0), 1, fill=False, linestyle='--', color='black')
- ax.add_artist(circle)
- # Plot arrows with frequency color + channel brightness
- for i, feat in enumerate(feature_names):
- base_color = freq_colors.get(frequencies[i], 'gray')
- # Use alpha for channel: 0 = min channel → white overlay, 1 = max channel → base color
- rgba = to_rgba(base_color, alpha=0.4 + 0.6*channels_norm[i])
- ax.annotate(
- "",
- xy=(loadings[i,0], loadings[i,1]),
- xytext=(0,0),
- arrowprops=dict(
- arrowstyle="->",
- lw=2.5,
- color=rgba
- )
- )
- # ------------------------------------------------
- # Legend for frequency bands
- # ------------------------------------------------
- for freq, color in freq_colors.items():
- ax.plot([], [], color=color, lw=2, label=freq.capitalize())
- ax.legend(title='Frequency band', loc='upper right')
- # ------------------------------------------------
- # Colorbar for channel brightness (white → black)
- # ------------------------------------------------
- # We'll create a grayscale colormap from white → black
- from matplotlib.colors import LinearSegmentedColormap
- grayscale = LinearSegmentedColormap.from_list('gray', ['white', 'black'])
- norm = Normalize(vmin=chan_min, vmax=chan_max)
- sm = ScalarMappable(cmap=grayscale, norm=norm)
- sm.set_array([])
- cbar = fig.colorbar(sm, ax=ax)
- cbar.set_label('Channel number')
- # ------------------------------------------------
- # Axis formatting
- # ------------------------------------------------
- ax.axhline(0, color='black', linewidth=1.5)
- ax.axvline(0, color='black', linewidth=1.5)
- for spine in ax.spines.values():
- spine.set_color('black')
- spine.set_linewidth(1.5)
- ax.tick_params(colors='black')
- ax.set_xlim(-1.1,1.1)
- ax.set_ylim(-1.1,1.1)
- ax.set_aspect('equal')
- ax.set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]*100:.1f}% variance)')
- ax.set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]*100:.1f}% variance)')
- ax.set_title('Correlation Circle (Cerebral BSPM)')
- plt.show()
- # %% [markdown]
- # #### Cerebral BSPM
- # %% [markdown]
- # ##### BSPM data import
- # %%
- import os
- import re
- from biolab.bspm import BSPM,image2layout
- from biolab.utils import apply2all
- # Save new recordings into .bspm files
- # Define parameters
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings\EEG_classification\BSPM'
- # Convert electrode array image into layout
- ECG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EEG_lyt.jpg',seq='cw')
- # Define operation to be applied to each file
- def func(filepath):
- # Extract filename from filepath
- filename = os.path.splitext(os.path.basename(filepath))[0]
- # Define the regex pattern to match the required values
- pattern = r'^([A-Za-z_]+)_\d{6}_\d{6}$'
- # Search for the pattern in the given filename
- match = re.search(pattern,filename)
- if match:
- filename = match.group(1)
- if filename == 'muscle activity':
- mode = 'EEG/EMG'
- else:
- mode = 'EEG'
- sensorimotor = filename
- measurement = 'M1'
- else:
- raise ValueError("Filename \"{}\" does not match the expected pattern.".format(filename))
- meta = {
- 'mode': mode,
- 'measurement': measurement,
- 'sensorimotor': {'auditory':'auditory','motor':'motor','visual':'visual'}.get(sensorimotor,None)
- }
- # Load BSPM data from .rhs file
- MP = BSPM.from_file(filepath=filepath,layout=ECG_lyt,metadata=meta,refchs=[])
- # Save BSPM object
- MP.save(savepath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\\'+filename+'.bspm')
- # Call DataSaver function
- apply2all(read_directory=readpath,extension='.rhs',func=func)
- # %% [markdown]
- # ##### Extract EEG features
- # %%
- import numpy as np
- import pandas as pd
- import scipy.signal as signal
- from biolab.utils import apply2all
- from biolab.bspm import BSPM
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM'
- # Function to normalize EEG signals (Z-score normalization)
- def normalize_signals(eeg_data):
- return (eeg_data - eeg_data.mean()) / eeg_data.std()
- def compute_band_power(eeg_signal, sf, band, method='welch'):
- """Compute power spectral density (PSD) or average magnitude for a given EEG signal."""
- low, high = band
- if method == 'welch':
- freqs, psd = signal.welch(eeg_signal, sf, nperseg=sf*2) # 2-sec windows
- band_power = np.trapz(psd[(freqs >= low) & (freqs <= high)], freqs[(freqs >= low) & (freqs <= high)])
- return band_power
- elif method == 'fft':
- freqs = np.fft.rfftfreq(len(eeg_signal), d=1/sf)
- psd = np.abs(np.fft.rfft(eeg_signal))**2 / len(eeg_signal)
- band_power = np.trapz(psd[(freqs >= low) & (freqs <= high)], freqs[(freqs >= low) & (freqs <= high)])
- return band_power
- elif method == 'magnitude': # Compute average magnitude using RMS
- filtered_signal = bandpass_filter(eeg_signal, low, high, sf)
- return np.sqrt(np.mean(filtered_signal**2)) # RMS magnitude
- else:
- raise ValueError("Invalid method. Choose 'welch', 'fft', or 'magnitude'.")
- def bandpass_filter(data, lowcut, highcut, sf, order=4):
- """Apply a bandpass filter to the EEG signal."""
- b, a = signal.butter(order, [lowcut, highcut], fs=sf, btype='band')
- return signal.filtfilt(b, a, data)
- # Define bands
- bands = {'Theta': (4, 8), 'Alpha': (8, 13), 'Beta': (13, 30), 'Gamma': (30, 100)}
- def f(filepath, method='power'): # Choose 'power' or 'magnitude'
- EEG = BSPM.load(loadpath=filepath)
- rEEG = EEG.resample(newfs=1000)
- rEEG.preprocess(bw=(0.5, 100))
- sf = 1000 # Sampling frequency (Hz)
- # Define start & end times based on the file type
- segment_map = {
- 'auditory.bspm': (8.5,9.5),
- 'motor.bspm': (10.2,11.2),
- 'visual.bspm': (6.5,7.5)
- }
- for key, (start, end) in segment_map.items():
- if key in filepath:
- break
- # Extract the EEG segment
- segment = rEEG.preprocessed_data.iloc[int(start*sf):int(end*sf)]
- # Normalize EEG data
- segment_normalized = segment.apply(normalize_signals, axis=0)
- # Compute feature (power or magnitude) for each band & channel
- band_feature_list = []
- band_feature_dict = {}
- for band_name, band_range in bands.items():
- feature_values = segment_normalized.apply(lambda x: compute_band_power(x.values, sf, band_range, method=method), axis=0)
- for ch in feature_values.index:
- band_feature_dict[f"{ch}_{band_name}"] = feature_values[ch]
- band_feature_list.append(band_feature_dict)
- # Convert list to DataFrame
- df_band_feature = pd.DataFrame(band_feature_list)
- df_band_feature['action'] = EEG.metadata['sensorimotor']
- # Save to Excel
- excel_path = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\EEG_bands_features.xlsx'
- try:
- existing_df = pd.read_excel(excel_path)
- combined_df = pd.concat([existing_df, df_band_feature], ignore_index=True)
- except FileNotFoundError:
- combined_df = df_band_feature
- with pd.ExcelWriter(excel_path, mode='w', engine='openpyxl') as writer:
- combined_df.to_excel(writer, index=False)
- # Apply to all files
- apply2all(read_directory=readpath, extension='.bspm', exclude=['Testing'], func=lambda fp: f(fp, method='fft')) # Change method here
- # %% [markdown]
- # ##### Generate BSPM
- # %%
- import matplotlib.pyplot as plt
- import numpy as np
- from biolab.bspm import interpolate,BSPM
- limit = 50
- # Read .bspm data for sphere
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\auditory.bspm'
- EEG = BSPM.load(loadpath=readpath)
- rEEG_auditory = EEG.resample(newfs=1000)
- rEEG_auditory.preprocess(bw=(0.5,100))
- # Display cardiac BSPM
- vmap = rEEG_auditory.potential(time=16.4)
- frame = interpolate(valmap=vmap,method='cubic')
- plt.imshow(frame,vmin=-limit,vmax=limit)
- cbar = plt.colorbar()
- cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Auditory.pdf',dpi=1000)
- plt.show()
- # Read .bspm data for sphere
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\motor.bspm'
- EEG = BSPM.load(loadpath=readpath)
- rEEG_motor = EEG.resample(newfs=1000)
- rEEG_motor.preprocess(bw=(0.5,100))
- # Display cardiac BSPM
- vmap = rEEG_motor.potential(time=8.2)
- frame = interpolate(valmap=vmap,method='cubic')
- plt.imshow(frame,vmin=-limit,vmax=limit)
- cbar = plt.colorbar()
- cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Motor.pdf',dpi=1000)
- plt.show()
- # Read .bspm data for sphere
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\visual.bspm'
- EEG = BSPM.load(loadpath=readpath)
- rEEG_visual = EEG.resample(newfs=200)
- rEEG_visual.preprocess(bw=(0.5,99))
- # Display cardiac BSPM
- vmap = rEEG_visual.potential(time=18.2)
- frame = interpolate(valmap=vmap,method='cubic')
- plt.imshow(frame,vmin=-limit,vmax=limit)
- cbar = plt.colorbar()
- cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Visual.pdf',dpi=1000)
- plt.show()
- # %%
- import pandas as pd
- import plotly.express as px
- from scipy.signal import butter, filtfilt
- from biolab.bspm import BSPM
- # Define read directory
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\motor.bspm'
- # Load .bspm data
- EEG = BSPM.load(loadpath=readpath)
- # Resample and preprocess the data
- rEEG = EEG.resample(newfs=1000)
- rEEG.preprocess(bw=(0.5, 100))
- # Define bandpass filter function
- def bandpass_filter(data, lowcut, highcut, sf, order=4):
- return filtfilt(*butter(order, [lowcut, highcut], fs=sf, btype='band'), data)
- # Define EEG bands
- bands = {
- #'Delta (0.5-4 Hz)': (0.5, 4),
- 'Theta (4-8 Hz)': (4, 8),
- 'Alpha (8-13 Hz)': (8, 13),
- 'Beta (13-30 Hz)': (13, 30),
- 'Gamma (30-100 Hz)': (30, 100)
- }
- # Initialize DataFrame for plotting
- filtered_data = []
- for channel in rEEG.preprocessed_data.columns:
- for band, (low, high) in bands.items():
- filtered_signal = bandpass_filter(rEEG.preprocessed_data[channel], low, high, 1000)
- temp_df = pd.DataFrame({
- 'Time (s)': rEEG.preprocessed_data.index.values,
- 'Amplitude': filtered_signal,
- 'Band': band,
- 'Channel': channel
- })
- filtered_data.append(temp_df)
- # Combine all data into a single DataFrame
- df_melted = pd.concat(filtered_data, ignore_index=True)
- # Plot with Plotly Express (all in one figure)
- fig = px.line(
- df_melted,
- x='Time (s)',
- y='Amplitude',
- color='Band', # Different colors for bands
- line_dash='Channel', # Different line styles for channels
- title='EEG Traces Across Channels and Frequency Bands'
- )
- fig.show()
- # %% [markdown]
- # ##### Generate maps of anything but BSP
- # %%
- import pandas as pd
- import numpy as np
- import matplotlib.pyplot as plt
- from biolab.bspm import BSPM,interpolate
- def mapping(bspm,values) -> np.ndarray:
- # Create copy of mapped layout
- pmap = np.copy(bspm.layout).astype(float)
- pmap[pmap == 0] = np.NaN
- # Convert channel_data to numpy arrays
- locations = bspm.channel_data['location'].values
- # Change value of label in layout to potential taking into account reference channels
- # Create a mapping from location to voltage channel
- location_to_voltage = dict(zip(locations,values))
- # Replace values in zmap based on location_to_impedance mapping
- mask = np.isin(bspm.layout,locations)
- pmap[mask] = np.vectorize(location_to_voltage.get)(bspm.layout[mask])
- return pmap
- # Load data
- file_path = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\EEG_bands_features.xlsx'
- data = pd.read_excel(file_path)
- # Get the lowest value for each action
- data_min = data.groupby('action',as_index=False).min()
- # Select only Gamma band columns
- band = 'Theta'
- signal = 'Power_reg'
- units = {'Power':'$\mu$V$^2$','RMS':'$\mu$V','Power_reg':'$\mu$V$^2$'}
- unit = units[signal]
- limits = {'Theta':500,'Alpha':200,'Beta':150,'Gamma':15} # RMS -> {'Theta':0.75,'Alpha':0.5,'Beta':0.5,'Gamma':0.1}
- limit = limits[band]
- data_min = data_min.loc[:,data_min.columns.str.contains(band,case=False,na=False)]
- # Display power maps
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\auditory.bspm'
- EEG = BSPM.load(loadpath=readpath)
- rEEG_auditory = EEG.resample(newfs=1000)
- pmap = mapping(rEEG_auditory,data_min.iloc[0,:].values)
- frame = interpolate(valmap=pmap,method='cubic')
- plt.imshow(frame,vmin=0,vmax=limit,cmap='viridis')
- cbar = plt.colorbar()
- cbar.set_label('{} [{}]'.format(signal,unit)) # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\\'+'{}_auditory_{}'.format(band,signal)+'_viridis.pdf',dpi=1000)
- plt.show()
- # Display power maps
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\motor.bspm'
- EEG = BSPM.load(loadpath=readpath)
- rEEG_motor = EEG.resample(newfs=1000)
- pmap = mapping(rEEG_motor,data_min.iloc[1,:].values)
- frame = interpolate(valmap=pmap,method='cubic')
- plt.imshow(frame,vmin=0,vmax=limit,cmap='viridis')
- cbar = plt.colorbar()
- cbar.set_label('{} [{}]'.format(signal,unit)) # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\\'+'{}_motor_{}'.format(band,signal)+'_viridis.pdf',dpi=1000)
- plt.show()
- # Display power maps
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\visual.bspm'
- EEG = BSPM.load(loadpath=readpath)
- rEEG_visual = EEG.resample(newfs=1000)
- pmap = mapping(rEEG_visual,data_min.iloc[2,:].values)
- frame = interpolate(valmap=pmap,method='cubic')
- plt.imshow(frame,vmin=0,vmax=limit,cmap='viridis')
- cbar = plt.colorbar()
- cbar.set_label('{} [{}]'.format(signal,unit)) # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\\'+'{}_visual_{}'.format(band,signal)+'_viridis.pdf',dpi=1000)
- plt.show()
- # %% [markdown]
- # ## Multi-modal BSPM
- # %% [markdown]
- # ### Data import
- # %%
- import os
- import re
- from biolab.bspm import BSPM,image2layout
- from biolab.utils import apply2all
- # Save new recordings into .bspm files
- # Define parameters
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings\Multi-modal_recording'
- # Convert electrode array image into layout
- EMG_EEG_lyt = image2layout(image_path=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EEG-EMG_lyt.jpg',seq='cw')
- # Define operation to be applied to each file
- def func(filepath):
- # Extract filename from filepath
- filename = os.path.splitext(os.path.basename(filepath))[0]
- # Define the regex pattern to match the required values
- pattern = r'^([A-Za-z_]+)_\d{6}_\d{6}$'
- # Search for the pattern in the given filename
- match = re.search(pattern,filename)
- if match:
- filename = match.group(1)
- mode = 'EMG/ECG'
- movement = filename
- measurement = 'M1'
- else:
- raise ValueError("Filename \"{}\" does not match the expected pattern.".format(filename))
- meta = {
- 'mode': mode,
- 'measurement': measurement,
- 'movement': movement
- }
- savepath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Multi-modal\\'+filename+'.bspm'
- if not os.path.isfile(savepath):
- # Load BSPM data from .rhs file
- MP = BSPM.from_file(filepath=filepath,layout=EMG_EEG_lyt,metadata=meta,refchs=[])
- # Save BSPM object
- MP.save(savepath=savepath)
- # Call DataSaver function
- apply2all(read_directory=readpath,extension='.rhs',exclude=['Individual_recordings'],func=func)
- # %% [markdown]
- # ### Joint cerebral and muscular BSPM
- # %% [markdown]
- # Import multimodal data
- # %%
- from biolab.bspm import BSPM
- # Read .bspm data for fist
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\Multi-modal\fist.bspm'
- FIST = BSPM.load(loadpath=readpath)
- rEMGFIST = FIST.resample(newfs=1000)
- rEEGFIST = FIST.resample(newfs=1000)
- rEMGFIST.preprocess(bw=(5,400))
- rEEGFIST.preprocess(bw=(0.5,100))
- # Read .bspm data for up
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\Multi-modal\up.bspm'
- UP = BSPM.load(loadpath=readpath)
- rEMGUP = UP.resample(newfs=1000)
- rEEGUP = UP.resample(newfs=1000)
- rEMGUP.preprocess(bw=(5,400))
- rEEGUP.preprocess(bw=(0.5,100))
- # %% [markdown]
- # Display EEG power bands
- # %%
- import pandas as pd
- import plotly.express as px
- from scipy.signal import butter, filtfilt
- # Define data to be displayed
- data = rEEGUP.preprocessed_data
- # Define bandpass filter function
- def bandpass_filter(data, lowcut, highcut, sf, order=4):
- return filtfilt(*butter(order, [lowcut, highcut], fs=sf, btype='band'), data)
- # Define EEG bands
- bands = {
- #'Delta (0.5-4 Hz)': (0.5, 4),
- 'Theta (4-8 Hz)': (4, 8),
- 'Alpha (8-13 Hz)': (8, 13),
- 'Beta (13-30 Hz)': (13, 30),
- 'Gamma (30-100 Hz)': (30, 100)
- }
- # Initialize DataFrame for plotting
- filtered_data = []
- for channel in data.filter(like="A").columns:
- for band, (low, high) in bands.items():
- filtered_signal = bandpass_filter(data[channel], low, high, 1000)
- temp_df = pd.DataFrame({
- 'Time (s)': data.index.values,
- 'Amplitude': filtered_signal,
- 'Band': band,
- 'Channel': channel
- })
- filtered_data.append(temp_df)
- # Combine all data into a single DataFrame
- df_melted = pd.concat(filtered_data, ignore_index=True)
- # Plot with Plotly Express (all in one figure)
- fig = px.line(
- df_melted,
- x='Time (s)',
- y='Amplitude',
- color='Band', # Different colors for bands
- line_dash='Channel', # Different line styles for channels
- title='EEG Traces Across Channels and Frequency Bands'
- )
- fig.show()
- # %% [markdown]
- # Display EMG envelopes
- # %%
- import plotly.express as px
- data = rEMGFIST.preprocessed_data
- # Ensure the DataFrame is properly referenced
- fig = px.line(
- data.abs(),#.rolling(window=500).mean(),
- x=data.index,
- y=data.columns # Specify the column(s) to plot
- )
- fig.show()
- # %% [markdown]
- # Generate EMG BSPM
- # %%
- import copy
- from biolab.bspm import interpolate,layout2map
- data = copy.deepcopy(rEMGFIST.preprocessed_data.head(30000).abs().rolling(window=500).mean())
- chdata = copy.deepcopy(rEMGFIST.channel_data)
- # Display cardiac BSPM
- chdata['custom_channel_name'] = ['A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','B','B','B','B','B','B','B','B','B','B','B','B','B','B','B','B']+chdata['custom_channel_name']
- # Step 1: Create a mapping from 'locations' to 'custom_channel_names'
- location_to_channel = dict(zip(chdata['location'],chdata['custom_channel_name']))
- # Step 2: Rearrange the recdata columns based on the locations order
- # Match the locations to the recdata columns
- reordered_columns = [location_to_channel[loc] for loc in chdata['location'] if location_to_channel[loc] in rEMGFIST.preprocessed_data.columns]
- # Step 3: Get the values at the specific index from recdata with the correct column order
- values_at_index = data.loc[15.045,reordered_columns] # FIST = PRE (14) PERI (15.045) # UP = PRE (9.68) PERI (10.218)
- vmap = layout2map(layout=rEMGFIST.layout,mapping=dict(zip(chdata['location'].values,values_at_index)))
- frame = interpolate(valmap=vmap,method='cubic')
- # %% [markdown]
- # Generate EEG BSPM
- # %%
- import copy
- from biolab.bspm import interpolate,layout2map
- import numpy as np
- import pandas as pd
- import scipy.signal as signal
- # Data to be used
- data = copy.deepcopy(rEMGUP.preprocessed_data.head(30000))
- method = 'fft'
- # Function to normalize EEG signals (Z-score normalization)
- def normalize_signals(eeg_data):
- return (eeg_data - eeg_data.mean()) / eeg_data.std()
- def compute_band_power(eeg_signal, sf, band, method='welch'):
- """Compute power spectral density (PSD) or average magnitude for a given EEG signal."""
- low, high = band
- if method == 'welch':
- freqs, psd = signal.welch(eeg_signal, sf, nperseg=sf*2) # 2-sec windows
- band_power = np.trapz(psd[(freqs >= low) & (freqs <= high)], freqs[(freqs >= low) & (freqs <= high)])
- return band_power
- elif method == 'fft':
- freqs = np.fft.rfftfreq(len(eeg_signal), d=1/sf)
- psd = np.abs(np.fft.rfft(eeg_signal))**2 / len(eeg_signal)
- band_power = np.trapz(psd[(freqs >= low) & (freqs <= high)], freqs[(freqs >= low) & (freqs <= high)])
- return band_power
- elif method == 'magnitude': # Compute average magnitude using RMS
- filtered_signal = bandpass_filter(eeg_signal, low, high, sf)
- return np.sqrt(np.mean(filtered_signal**2)) # RMS magnitude
- else:
- raise ValueError("Invalid method. Choose 'welch', 'fft', or 'magnitude'.")
- def bandpass_filter(data, lowcut, highcut, sf, order=4):
- """Apply a bandpass filter to the EEG signal."""
- b, a = signal.butter(order, [lowcut, highcut], fs=sf, btype='band')
- return signal.filtfilt(b, a, data)
- # Define bands
- bands = {'Theta': (4, 8), 'Alpha': (8, 13), 'Beta': (13, 30), 'Gamma': (30, 100)}
- sf = 1000 # Sampling frequency (Hz)
- segment_map = (14.4,15.3) # FIST = PRE (12.2,14.2) PERI (14.2,16.2) # UP = PRE (13.5,14.4) PERI (14.4,15.3)
- # Extract the EEG segment
- segment = data.iloc[int(segment_map[0]*sf):int(segment_map[1]*sf)]
- # Normalize EEG data
- segment_normalized = segment.apply(normalize_signals, axis=0)
- # Compute feature (power or magnitude) for each band & channel
- band_feature_list = []
- band_feature_dict = {}
- for band_name, band_range in bands.items():
- feature_values = segment_normalized.apply(lambda x: compute_band_power(x.values, sf, band_range, method=method), axis=0)
- for ch in feature_values.index:
- band_feature_dict[f"{ch}_{band_name}"] = feature_values[ch]
- band_feature_list.append(band_feature_dict)
- # Convert list to DataFrame
- df_band_feature = pd.DataFrame(band_feature_list)
- # %%
- chdata = copy.deepcopy(rEEGUP.channel_data)
- wave = 'Beta'
- # Display BSPM
- chdata['custom_channel_name'] = ['A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','B','B','B','B','B','B','B','B','B','B','B','B','B','B','B','B']+chdata['custom_channel_name']
- # Step 1: Create a mapping from 'locations' to 'custom_channel_names'
- location_to_channel = dict(zip(chdata['location'],chdata['custom_channel_name']+'_{}'.format(wave)))
- # Step 2: Rearrange the recdata columns based on the locations order
- # Match the locations to the recdata columns
- reordered_columns = [location_to_channel[loc] for loc in chdata['location'] if location_to_channel[loc] in df_band_feature.filter(like=wave)]
- # Step 3: Get the values at the specific index from recdata with the correct column order
- values_at_index = df_band_feature.loc[0,reordered_columns]
- vmap = layout2map(layout=rEEGUP.layout,mapping=dict(zip(chdata['location'].values,values_at_index)))
- frame = interpolate(valmap=vmap,method='cubic')
- # %%
- import matplotlib.pyplot as plt
- limit = 100
- plt.imshow(frame,vmin=0,vmax=limit,cmap='viridis')
- cbar = plt.colorbar()
- cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
- # Remove the axes
- plt.axis('off')
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\Multimodal_EMG_peri_fist_viridis.pdf',dpi=1000)
- plt.show()
- # %% [markdown]
- # ### Reaction time
- # %% [markdown]
- # Import multimodal data
- # %%
- from biolab.bspm import BSPM
- # Read .bspm data for fist
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Multi-modal\fist.bspm'
- FIST = BSPM.load(loadpath=readpath)
- rEMGFIST = FIST.resample(newfs=1000)
- rEEGFIST = FIST.resample(newfs=1000)
- rEMGFIST.preprocess(bw=(5,400))
- rEEGFIST.preprocess(bw=(0.5,100))
- # Read .bspm data for up
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Multi-modal\up.bspm'
- UP = BSPM.load(loadpath=readpath)
- rEMGUP = UP.resample(newfs=1000)
- rEEGUP = UP.resample(newfs=1000)
- rEMGUP.preprocess(bw=(5,400))
- rEEGUP.preprocess(bw=(0.5,100))
- # %% [markdown]
- # Display EEG power bands
- # %%
- import pandas as pd
- import plotly.express as px
- from scipy.signal import butter, filtfilt
- import copy
- # Define data to be displayed
- data = copy.deepcopy(rEEGUP.preprocessed_data.head(30000))
- # Define bandpass filter function
- def bandpass_filter(data, lowcut, highcut, sf, order=4):
- return filtfilt(*butter(order, [lowcut, highcut], fs=sf, btype='band'), data)
- # Define EEG bands
- bands = {
- #'Delta (0.5-4 Hz)': (0.5, 4),
- 'Theta (4-8 Hz)': (4, 8),
- 'Alpha (8-13 Hz)': (8, 13),
- 'Beta (13-30 Hz)': (13, 30),
- 'Gamma (30-100 Hz)': (30, 100)
- }
- # Initialize DataFrame for plotting
- filtered_data = []
- for channel in data.filter(like="A").columns:
- for band, (low, high) in bands.items():
- filtered_signal = bandpass_filter(data[channel], low, high, 1000)
- temp_df = pd.DataFrame({
- 'Time (s)': np.lib.stride_tricks.sliding_window_view(np.abs(data.index.values),500).mean(axis=1),
- 'Amplitude': np.lib.stride_tricks.sliding_window_view(np.abs(filtered_signal),500).mean(axis=1),
- 'Band': band,
- 'Channel': channel
- })
- filtered_data.append(temp_df)
- # Combine all data into a single DataFrame
- df_melted = pd.concat(filtered_data, ignore_index=True)
- # Plot with Plotly Express (all in one figure)
- fig = px.line(
- df_melted,
- x='Time (s)',
- y='Amplitude',
- color='Band', # Different colors for bands
- line_dash='Channel', # Different line styles for channels
- title='EEG Traces Across Channels and Frequency Bands'
- )
- fig.show()
- # %% [markdown]
- # Display EMG envelopes
- # %%
- import plotly.express as px
- data = rEMGUP.preprocessed_data.head(30000)
- # Ensure the DataFrame is properly referenced
- fig = px.line(
- data.abs().rolling(window=500).mean(), #.sub(data.iloc[0],axis=1),
- x=data.index,
- y=data.columns[data.columns.str.contains("B")] # Specify the column(s) to plot
- )
- fig.show()
- # %% [markdown]
- # Compute peaks for EMG data for display
- # %%
- import plotly.graph_objects as go
- from scipy.signal import find_peaks
- import copy
- data = copy.deepcopy(rEMGUP.preprocessed_data.head(90000).abs().rolling(window=500).mean().dropna())
- df_emg = data[[col for col in data.columns if "B" in col]]
- # Function to find peaks
- def get_peaks(df):
- peaks_dict = {}
- for col in df.columns:
- peaks, _ = find_peaks(df[col],height=20, distance=1000) # Adjust height threshold as needed
- peaks_dict[col] = peaks
- return peaks_dict
- # Compute the peaks
- peaks_dict_emg = get_peaks(df_emg)
- # Plot with Plotly
- fig = go.Figure()
- # Add the channel signals
- for col in df_emg.columns:
- fig.add_trace(go.Scatter(
- x=df_emg.index,
- y=df_emg[col],
- mode='lines',
- name=col
- ))
- # Add the peaks as scatter points
- for col, peaks in peaks_dict_emg.items():
- fig.add_trace(go.Scatter(
- x=df_emg.index[peaks], # Use time for x-axis
- y=df_emg[col].iloc[peaks],
- mode='markers',
- name=f"{col} Peaks",
- marker=dict(size=8, symbol='circle', color='red')
- ))
- # Layout adjustments
- fig.update_layout(
- title="Interactive Peaks Plot with Time Axis",
- xaxis_title="Time (seconds)",
- yaxis_title="Amplitude",
- legend=dict(title="Channels & Peaks"),
- height=600,
- width=1000
- )
- # Show the interactive plot
- fig.show()
- # %% [markdown]
- # Compute peaks for EEG data (theta wave, motor planning) for display
- # %%
- import pandas as pd
- import numpy as np
- import plotly.graph_objects as go
- from scipy.signal import butter, filtfilt, find_peaks
- import copy
- # Define bandpass filter function
- def bandpass_filter(data, lowcut, highcut, sf, order=4):
- """Applies a bandpass filter to the data."""
- return filtfilt(*butter(order, [lowcut, highcut], fs=sf, btype='band'), data)
- # Sampling frequency
- sf = 1000
- # Define EEG bands
- bands = {
- 'Theta (4-8 Hz)': (4, 8),
- 'Alpha (8-13 Hz)': (8, 13),
- 'Beta (13-30 Hz)': (13, 30),
- 'Gamma (30-100 Hz)': (30, 100)
- }
- # Load and preprocess the data
- data = copy.deepcopy(rEEGFIST.preprocessed_data.head(90000))
- # Filter for theta waves across all channels
- filtered_data = []
- for channel in data.filter(like="A").columns:
- filtered_signal = bandpass_filter(data[channel], bands['Theta (4-8 Hz)'][0], bands['Theta (4-8 Hz)'][1], sf)
- # Apply rolling amplitude extraction
- amplitude = np.lib.stride_tricks.sliding_window_view(np.abs(filtered_signal), 500).mean(axis=1)
- # Align time index with the amplitude array
- time_aligned = data.index.values[499:]
- # Store filtered data
- temp_df = pd.DataFrame({
- 'Time (s)': time_aligned,
- 'Amplitude': amplitude,
- 'Channel': channel
- })
- filtered_data.append(temp_df)
- # Combine all theta-filtered data into a single DataFrame
- df_eeg = pd.concat(filtered_data, ignore_index=True)
- # Function to detect peaks
- def get_peaks(df, height=0.5, distance=1000):
- """Detect peaks across all channels."""
- peaks_dict = {}
- for channel in df['Channel'].unique():
- channel_data = df[df['Channel'] == channel]
- peaks, _ = find_peaks(channel_data['Amplitude'], height=height, distance=distance)
- peaks_dict[channel] = peaks
- return peaks_dict
- # Detect peaks
- peaks_dict_eeg = get_peaks(df_eeg, height=5, distance=1000) # Adjust height and distance as needed
- # Create interactive plot
- fig = go.Figure()
- # Plot the theta amplitude traces
- for channel in df_eeg['Channel'].unique():
- channel_data = df_eeg[df_eeg['Channel'] == channel]
- fig.add_trace(go.Scatter(
- x=channel_data['Time (s)'],
- y=channel_data['Amplitude'],
- mode='lines',
- name=f"{channel} Amplitude"
- ))
- # Plot the peaks as scatter points
- for channel, peaks in peaks_dict_eeg.items():
- channel_data = df_eeg[df_eeg['Channel'] == channel]
- fig.add_trace(go.Scatter(
- x=channel_data['Time (s)'].iloc[peaks], # Use correct time index
- y=channel_data['Amplitude'].iloc[peaks],
- mode='markers',
- name=f"{channel} Peaks",
- marker=dict(size=8, symbol='circle', color='red')
- ))
- # Layout adjustments
- fig.update_layout(
- title="Theta Wave Peaks Across Channels",
- xaxis_title="Time (seconds)",
- yaxis_title="Amplitude",
- legend=dict(title="Channels & Peaks"),
- height=600,
- width=1000
- )
- # Show the interactive plot
- fig.show()
- # %% [markdown]
- # Apply to all data without visualising
- # %%
- import pandas as pd
- import numpy as np
- import copy
- from scipy.signal import find_peaks, butter, filtfilt
- DATA = (rEMGUP,rEEGUP)
- # Load and preprocess the EMG data
- data = copy.deepcopy(DATA[0].preprocessed_data.abs().rolling(window=900).mean().dropna())
- df_emg = data[[col for col in data.columns if "B" in col]]
- # Define bandpass filter function
- def bandpass_filter(data, lowcut, highcut, sf, order=4):
- """Applies a bandpass filter to the data."""
- return filtfilt(*butter(order, [lowcut, highcut], fs=sf, btype='band'), data)
- # Function to find peaks in EMG
- def get_peaks(df):
- peaks_dict = {}
- for col in df.columns:
- peaks, _ = find_peaks(df[col], height=20, distance=1000) # Adjust height threshold as needed
- peaks_dict[col] = peaks
- return peaks_dict
- # Get peaks for EMG channels
- peaks_dict_emg = get_peaks(df_emg)
- # Load and preprocess the EEG data (filtered for theta band)
- data = copy.deepcopy(DATA[1].preprocessed_data)
- sf = 1000 # Sampling frequency
- filtered_data = []
- # Filter for theta waves across all channels (only A4 in EEG for delay calculation)
- for channel in data.filter(like="A").columns:
- filtered_signal = bandpass_filter(data[channel], 4, 8, sf) # Filtering for theta band (4-8 Hz)
- amplitude = np.lib.stride_tricks.sliding_window_view(np.abs(filtered_signal), 500).mean(axis=1)
- time_aligned = data.index.values[499:] # Align time with amplitude
- temp_df = pd.DataFrame({'Time (s)': time_aligned, 'Amplitude': amplitude, 'Channel': channel})
- filtered_data.append(temp_df)
- df_eeg = pd.concat(filtered_data, ignore_index=True)
- # Function to detect peaks in EEG
- def get_peaks_eeg(df, height=0.5, distance=1000):
- peaks_dict = {}
- for channel in df['Channel'].unique():
- channel_data = df[df['Channel'] == channel]
- peaks, _ = find_peaks(channel_data['Amplitude'], height=height, distance=distance)
- peaks_dict[channel] = peaks
- return peaks_dict
- # Get peaks for EEG (across all channels)
- peaks_dict_eeg = get_peaks_eeg(df_eeg, height=5, distance=1000)
- # Extract the time values for peaks from each EEG channel
- eeg_peaks_times = {}
- for channel in df_eeg['Channel'].unique():
- eeg_peaks_times[channel] = df_eeg[df_eeg['Channel'] == channel]['Time (s)'].iloc[peaks_dict_eeg[channel]].values
- # Extract the time values for peaks in channel A4 specifically
- a4_peaks_times = eeg_peaks_times.get('A4', [])
- # Function to calculate delay between EEG and EMG peaks with tolerance check and no reuse of EEG peaks
- def calculate_delays_with_tolerance(a4_peaks_times, emg_peaks_dict, df_emg, eeg_peaks_times, min_tolerance=0.1, max_tolerance=0.6):
- delays = {}
- # Initialize a dictionary to track the matched peaks for each EEG channel
- matched_eeg_peaks = {channel: set() for channel in eeg_peaks_times}
- # Calculate the maximum number of peaks across all EMG channels
- max_emg_peaks = max(len(peaks) for peaks in emg_peaks_dict.values())
- for emg_channel, emg_peaks in emg_peaks_dict.items():
- emg_peaks_times = df_emg[emg_channel].index[emg_peaks].values # Get time indices for EMG peaks
- emg_delays = []
- for emg_time in emg_peaks_times:
- # For each EMG peak, try to match it with the EEG peaks of each channel
- matched_delay = False
- for eeg_channel, a4_peaks_times in eeg_peaks_times.items():
- # Find the closest EEG peak in the current EEG channel within the tolerance range, excluding already matched peaks
- time_diffs = emg_time - a4_peaks_times # Time differences
- valid_peaks_times = a4_peaks_times[np.where((time_diffs >= min_tolerance) & (time_diffs <= max_tolerance))[0]]
- # Exclude matched EEG peaks for the current EEG channel
- valid_peaks_times = [t for t in valid_peaks_times if t not in matched_eeg_peaks[eeg_channel]]
- if valid_peaks_times:
- # Find the closest peak within the valid range
- closest_eeg_peak_time = valid_peaks_times[np.argmin(np.abs(valid_peaks_times - emg_time))]
- delay = emg_time - closest_eeg_peak_time
- emg_delays.append(delay)
- # Mark this EEG peak as matched (add to the set for this EEG channel)
- matched_eeg_peaks[eeg_channel].add(closest_eeg_peak_time)
- matched_delay = True
- break
- if not matched_delay:
- # If no valid peaks are found within the tolerance range, append NaN to indicate no match
- emg_delays.append(np.nan)
- # Pad the emg_delays array with NaN if its length is less than the maximum number of EMG peaks
- if len(emg_delays) < max_emg_peaks:
- emg_delays.extend([np.nan] * (max_emg_peaks - len(emg_delays)))
- delays[emg_channel] = emg_delays
- return pd.DataFrame(delays)
- # Calculate delays between EEG and all EMG channels with tolerance check and no reuse of EEG peaks for each channel
- delay_df = calculate_delays_with_tolerance(a4_peaks_times, peaks_dict_emg, df_emg, eeg_peaks_times, min_tolerance=0.1, max_tolerance=0.6)
- # %% [markdown]
- # Display histogram of reaction times
- # %%
- import matplotlib.pyplot as plt
- import pandas as pd
- import numpy as np
- from scipy.stats import norm
- # Import data
- delaymatrix = pd.read_excel(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\Reaction_time.xlsx')
- # Flatten all numerical data into a single series
- all_data_up = delaymatrix[delaymatrix['movement'] == 'UP'].select_dtypes(include='number').values.flatten()
- all_data_fist = delaymatrix[delaymatrix['movement'] == 'FIST'].select_dtypes(include='number').values.flatten()
- # Remove non-finite values (NaN and inf)
- all_data_up = all_data_up[np.isfinite(all_data_up)]
- all_data_fist = all_data_fist[np.isfinite(all_data_fist)]
- # Calculate the Gaussian fitting parameters (mean and std) for both datasets
- mu_up, std_up = norm.fit(all_data_up)
- mu_fist, std_fist = norm.fit(all_data_fist)
- # Plot the histogram
- plt.figure(figsize=(6,6))
- # Plot histogram for 'UP' data
- count_up, bins_up, patches_up = plt.hist(all_data_up, bins=10, color='blue', edgecolor='black', alpha=0.6, label='Wrist extension')
- # Plot Gaussian fit for 'UP' data
- xmin_up, xmax_up = plt.xlim()
- x_up = np.linspace(xmin_up, xmax_up, 100)
- p_up = norm.pdf(x_up, mu_up, std_up)
- plt.plot(x_up, p_up * len(all_data_up) * (bins_up[1] - bins_up[0]), 'blue')
- # Plot histogram for 'FIST' data
- count_fist, bins_fist, patches_fist = plt.hist(all_data_fist, bins=10, color='green', edgecolor='black', alpha=0.6, label='Hand flexion')
- # Plot Gaussian fit for 'FIST' data
- xmin_fist, xmax_fist = plt.xlim()
- x_fist = np.linspace(xmin_fist, xmax_fist, 100)
- p_fist = norm.pdf(x_fist, mu_fist, std_fist)
- plt.plot(x_fist, p_fist * len(all_data_fist) * (bins_fist[1] - bins_fist[0]), 'green')
- # Title and labels
- plt.title('Histogram of All Numerical Data with Gaussian Fit')
- plt.xlabel('Time [s]')
- plt.ylabel('Frequency')
- plt.legend(frameon=False)
- # Show the plot
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\Reaction_time_histogram.pdf',dpi=1000)
- plt.show()
- # %% [markdown]
- # Display spatially the difference between reaction time across channels
- # %%
- from biolab.bspm import image2layout
- lyt = image2layout(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EMG_lyt.jpg',seq='cw')
- # %%
- from biolab.bspm import layout2map
- delaymatrix = pd.read_excel(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\Reaction_time.xlsx')
- delaymatrixFIST = delaymatrix[delaymatrix['movement']=='FIST'].drop(columns=['movement'])
- delaymatrixUP = delaymatrix[delaymatrix['movement']=='UP'].drop(columns=['movement'])
- column_meansFIST = delaymatrixFIST.mean()
- mean_dictFIST = {int(col[1:]): column_meansFIST[col] for col in delaymatrixFIST.columns}
- vmapFIST = layout2map(layout=lyt,mapping=mean_dictFIST)
- column_meansUP = delaymatrixUP.mean()
- mean_dictUP = {int(col[1:]): column_meansUP[col] for col in delaymatrixUP.columns}
- vmapUP = layout2map(layout=lyt,mapping=mean_dictUP)
- # %%
- from biolab.bspm import interpolate
- intmapFIST = interpolate(valmap=vmapFIST,method='cubic')
- intmapUP = interpolate(valmap=vmapUP,method='cubic')
- # %%
- plt.imshow(intmapFIST,vmin=0.275,vmax=0.34, cmap='Greens')
- plt.colorbar()
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\Reaction_time_mapping_FIST_greens.pdf')
- plt.show()
- # %% [markdown]
- # ### CMC computation
- # %% [markdown]
- # Import multimodal data
- # %%
- # =====================================================
- # COMPLETE CORTICO-MUSCULAR COHERENCE PIPELINE
- # USING EVENTS.XLSX INTERVALS (FIST ONLY)
- # REMOVES LOW-FREQUENCY PEAKS
- # =====================================================
- import numpy as np
- import pandas as pd
- import matplotlib.pyplot as plt
- from scipy.signal import butter, filtfilt, coherence
- from biolab.bspm import BSPM
- # =====================================================
- # PARAMETERS
- # =====================================================
- FS = 1000
- BETA_BAND = (15,30)
- LOW_FREQ_CUTOFF = 8 # remove <8 Hz slow components
- FIST_PATH = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\Multi-modal\fist.bspm'
- EVENTS_PATH = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\Multi-modal\events.xlsx'
- # =====================================================
- # LOAD BSPM
- # =====================================================
- def load_bspm(path):
- obj = BSPM.load(loadpath=path)
- r = obj.resample(newfs=FS)
- r.preprocess(bw=(0.5,100))
- return r
- # =====================================================
- # SPLIT EEG / EMG
- # =====================================================
- def split_eeg_emg(bspm_obj):
- ch_info = bspm_obj.channel_data
- data = bspm_obj.preprocessed_data
- eeg_cols, emg_cols = [], []
- for idx, row in ch_info.iterrows():
- col_name = data.columns[idx]
- if row["port_prefix"] == "A":
- eeg_cols.append(col_name)
- elif row["port_prefix"] == "B":
- emg_cols.append(col_name)
- eeg = data[eeg_cols].values
- emg = data[emg_cols].values
- return eeg, emg
- # =====================================================
- # PREPROCESS EMG (BANDPASS + RECTIFY + HIGH-PASS)
- # =====================================================
- def preprocess_emg(emg):
- # Bandpass 5–400 Hz
- b,a = butter(4, [5/(FS/2),400/(FS/2)], btype='band')
- emg_filt = filtfilt(b,a, emg, axis=0)
- emg_rect = np.abs(emg_filt)
- # High-pass to remove <LOW_FREQ_CUTOFF Hz drift
- b,a = butter(4, LOW_FREQ_CUTOFF/(FS/2), btype='high')
- emg_hp = filtfilt(b,a, emg_rect, axis=0)
- return emg_hp
- # =====================================================
- # LOAD EVENTS + EXTRACT SEGMENTS
- # =====================================================
- def extract_event_segments(eeg, emg, events_path):
- events = pd.read_excel(events_path)
- events = events[events["movement"]=="FIST"]
- eeg_segments = []
- emg_segments = []
- for _, row in events.iterrows():
- start_idx = int(row["start"] * FS)
- end_idx = int(row["end"] * FS)
- if end_idx > len(eeg):
- continue
- eeg_segments.append(eeg[start_idx:end_idx])
- emg_segments.append(emg[start_idx:end_idx])
- eeg_segments = np.array(eeg_segments, dtype=object)
- emg_segments = np.array(emg_segments, dtype=object)
- print("Number of event segments:", len(eeg_segments))
- return eeg_segments, emg_segments
- # =====================================================
- # COMPUTE CMC
- # =====================================================
- def compute_cmc(eeg_segments, emg_segments, fmin=5, fmax=100, fstep=1):
- # Define common frequency grid
- common_freqs = np.arange(fmin, fmax+fstep, fstep)
- coh_trials = []
- NPERSEG = 512
- for eeg_seg, emg_seg in zip(eeg_segments, emg_segments):
- if len(eeg_seg) < NPERSEG:
- continue
- emg_mean = emg_seg.mean(axis=1)
- for e in range(eeg_seg.shape[1]):
- eeg_trial = eeg_seg[:,e] - np.mean(eeg_seg[:,e])
- emg_trial = emg_mean - np.mean(emg_mean)
- f, cxy = coherence(eeg_trial, emg_trial, fs=FS, nperseg=NPERSEG)
- # Keep only fmin–fmax
- mask = (f >= fmin) & (f <= fmax)
- f_sel = f[mask]
- cxy_sel = cxy[mask]
- # Interpolate onto common grid
- cxy_interp = np.interp(common_freqs, f_sel, cxy_sel)
- coh_trials.append(cxy_interp)
- coh_trials = np.vstack(coh_trials)
- return common_freqs, coh_trials
- # =====================================================
- # MAIN PIPELINE
- # =====================================================
- print("\nProcessing FIST using event intervals")
- obj = load_bspm(FIST_PATH)
- eeg, emg = split_eeg_emg(obj)
- emg = preprocess_emg(emg)
- eeg_segments, emg_segments = extract_event_segments(
- eeg,
- emg,
- EVENTS_PATH
- )
- freqs, coh_all = compute_cmc(eeg_segments, emg_segments)
- # =====================================================
- # BETA STRENGTH
- # =====================================================
- idx = (freqs>=BETA_BAND[0]) & (freqs<=BETA_BAND[1])
- beta_strength = coh_all[:,idx].mean(axis=1)
- # =====================================================
- # DEBUG PLOT
- # =====================================================
- plt.figure()
- plt.plot(freqs, np.mean(coh_all, axis=0))
- plt.axvspan(*BETA_BAND, alpha=0.2)
- plt.title("Mean CMC Spectrum (FIST — Event based, low-freq removed)")
- plt.xlabel("Frequency (Hz)")
- plt.ylabel("Coherence")
- plt.show()
- # %% [markdown]
- # ### Muscular BSPM prediction from cerebral BSPM
- # %% [markdown]
- # Import multimodal data
- # %%
- from biolab.bspm import BSPM
- # Read .bspm data for fist
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Multi-modal\fist.bspm'
- FIST = BSPM.load(loadpath=readpath)
- rEMGFIST = FIST.resample(newfs=1000)
- rEEGFIST = FIST.resample(newfs=1000)
- rEMGFIST.preprocess(bw=(5,400))
- rEEGFIST.preprocess(bw=(0.5,100))
- # Read .bspm data for up
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Multi-modal\up.bspm'
- UP = BSPM.load(loadpath=readpath)
- rEMGUP = UP.resample(newfs=1000)
- rEEGUP = UP.resample(newfs=1000)
- rEMGUP.preprocess(bw=(5,400))
- rEEGUP.preprocess(bw=(0.5,100))
- # %% [markdown]
- # Define activity locations from EMG envelopes and reaction time
- # %%
- import copy
- from scipy.signal import find_peaks
- DATA = (rEMGFIST,rEEGFIST)
- window_size = 500
- # Load and preprocess EMG data
- emgdata = copy.deepcopy(DATA[0].preprocessed_data.abs().rolling(window=window_size).mean().dropna())
- emgdata = emgdata[[col for col in emgdata.columns if "B" in col]]
- # Function to find peaks in EMG
- def emgpeaks(df,window_size):
- peaks_dict = {}
- for col in df.columns:
- peaks, _ = find_peaks(df[col], height=20, distance=1000) # Adjust height threshold as needed
- # Adjust peak indices by the rolling window offset
- adjusted_peaks = peaks + (window_size // 2)
- peaks_dict[col] = adjusted_peaks
- return peaks_dict
- # Get peaks for EMG channels
- emg_peak_locs = emgpeaks(emgdata,window_size)
- # %% [markdown]
- # - DEFINE WINDOW LENGTH 1s FOR BOTH EMG AND EEG ACTIVITY
- # - DEFINE DELAY BETWEEN EMG AND EEG SIGNAL TO BE 300ms
- # %% [markdown]
- # Compute feature matrix from EEG bands power
- # %%
- import numpy as np
- import pandas as pd
- import scipy.signal as signal
- eegdata = copy.deepcopy(DATA[0].preprocessed_data.abs().rolling(window=window_size).mean().dropna())
- eegdata = eegdata[[col for col in eegdata.columns if "A" in col]]
- sf = 1000 # Sampling frequency
- eegbands = {
- 'Delta':(0.5,4),
- 'Theta':(4,8),
- 'Alpha':(8,13),
- 'Beta':(13,30),
- 'Gamma':(30,100)
- }
- # Function to normalize EEG signals (Z-score normalization)
- def normalize_signals(eeg_data):
- return (eeg_data - eeg_data.mean()) / eeg_data.std()
- def compute_band_power(eeg_signal, sf, band):
- """Compute power spectral density (PSD) or average magnitude for a given EEG signal."""
- low, high = band
- freqs = np.fft.rfftfreq(len(eeg_signal), d=1/sf)
- psd = np.abs(np.fft.rfft(eeg_signal))**2 / len(eeg_signal)
- band_power = np.trapz(psd[(freqs >= low) & (freqs <= high)], freqs[(freqs >= low) & (freqs <= high)])
- return band_power
- def bandpass_filter(data, lowcut, highcut, sf, order=4):
- """Apply a bandpass filter to the EEG signal."""
- b, a = signal.butter(order, [lowcut, highcut], fs=sf, btype='band')
- return signal.filtfilt(b, a, data)
- emg_segments = [(int(peak - 0.5*sf), int(peak + 0.5*sf)) for peak in emg_peak_locs['B1']] # 1 second
- eeg_segments = [(int(peak - 0.8*sf), int(peak + 0.2*sf)) for peak in emg_peak_locs['B1']] # 1 second, with 300ms delay
- # Use filter with zip and unpacking
- filtered = [(t1, t2) for t1, t2 in zip(emg_segments, eeg_segments) if all(x >= 0 for x in t1 + t2)]
- # Unzip the filtered pairs
- emg_segments, eeg_segments = zip(*filtered) if filtered else ((), ())
- band_feature_list = []
- for seg in eeg_segments:
- segment = eegdata.iloc[seg[0]:seg[1]]
- # Normalize EEG data
- segment_normalized = segment.apply(normalize_signals, axis=0)
- # Compute feature (power or magnitude) for each band & channel
- band_feature_dict = {}
- for band_name, band_range in eegbands.items():
- feature_values = segment_normalized.apply(lambda x: compute_band_power(x.values, sf, band_range), axis=0)
- for ch in feature_values.index:
- band_feature_dict[f"{ch}_{band_name}"] = feature_values[ch]
- band_feature_list.append(band_feature_dict)
- # Convert list to DataFrame
- feature_matrix = pd.DataFrame(band_feature_list)
- # %% [markdown]
- # Compute output matrix from EMG data
- # %%
- # Deadjust peaks for rolling averaged EMG data
- window_size = 500
- emg_segments = [(int(peak - 0.5*sf), int(peak + 0.5*sf)) for peak in emg_peak_locs['B1']-(window_size // 2)] # 1 second
- eeg_segments = [(int(peak - 0.8*sf), int(peak + 0.2*sf)) for peak in emg_peak_locs['B1']] # 1 second, with 300ms delay
- # Use filter with zip and unpacking
- filtered = [(t1, t2) for t1, t2 in zip(emg_segments, eeg_segments) if all(x >= 0 for x in t1 + t2)]
- # Unzip the filtered pairs
- emg_segments, eeg_segments = zip(*filtered) if filtered else ((), ())
- emg_feature_list = []
- for seg in emg_segments:
- segment = emgdata.iloc[seg[0]:seg[1]].mean()
- emg_feature_list.append(segment)
- # Convert list to DataFrame
- output_matrix = pd.DataFrame(emg_feature_list)
- # %% [markdown]
- # Perform multimodal prediction
- # %%
- import numpy as np
- import copy
- from sklearn.cross_decomposition import PLSRegression
- from sklearn.model_selection import train_test_split
- from sklearn.metrics import r2_score, mean_squared_error
- # Example data (replace with your EEG & EMG features)
- X = copy.deepcopy(feature_matrix.to_numpy()) # EEG features
- y = copy.deepcopy(output_matrix.to_numpy()) # EMG maps
- # Train-test split
- X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
- # PLSR model
- n_components = 10 # Tune this hyperparameter
- plsr = PLSRegression(n_components=n_components)
- plsr.fit(X_train, y_train)
- # Predictions
- y_pred = plsr.predict(X_test)
- # Evaluation
- r2 = r2_score(y_test, y_pred)
- rmse = np.sqrt(mean_squared_error(y_test, y_pred))
- print(f'R²: {r2:.4f}, RMSE: {rmse:.4f}')
- # %%
- import numpy as np
- from scipy.stats import pearsonr, spearmanr
- from sklearn.metrics.pairwise import cosine_similarity
- # Assuming y_test and y_pred are already defined
- # Initialize an array to store Pearson correlation for each test sample
- pearson_corr = np.zeros(y_test.shape[0])
- # Compute Pearson correlation for each test sample (across 16 channels)
- for i in range(y_test.shape[0]):
- corr, _ = pearsonr(y_test[i], y_pred[i]) # Pearson correlation per sample
- pearson_corr[i] = corr
- # Average correlation across all test samples (optional)
- average_pearson_corr = np.mean(pearson_corr)
- print(f"Average Pearson Correlation: {average_pearson_corr}")
- std_pearson_corr = np.std(pearson_corr)
- print(f"Standard deviation Pearson Correlation: {std_pearson_corr}")
- # Initialize an array to store Spearman correlation for each test sample
- spearman_corr = np.zeros(y_test.shape[0])
- # Compute Spearman rank correlation for each test sample (across 16 channels)
- for i in range(y_test.shape[0]):
- corr, _ = spearmanr(y_test[i], y_pred[i]) # Spearman correlation per sample
- spearman_corr[i] = corr
- # Average rank correlation across all test samples (optional)
- average_spearman_corr = np.mean(spearman_corr)
- print(f"Average Spearman Rank Correlation: {average_spearman_corr}")
- std_spearman_corr = np.std(spearman_corr)
- print(f"Standard deviation Spearman Rank Correlation: {std_spearman_corr}")
- # Initialize an array to store Cosine similarity for each test sample
- cosine_sim = np.zeros(y_test.shape[0])
- # Compute Cosine similarity for each test sample (across 16 channels)
- for i in range(y_test.shape[0]):
- cos_sim = cosine_similarity([y_test[i]], [y_pred[i]]) # Cosine similarity per sample
- cosine_sim[i] = cos_sim[0][0]
- # Average Cosine similarity across all test samples (optional)
- average_cosine_sim = np.mean(cosine_sim)
- print(f"Average Cosine Similarity: {average_cosine_sim}")
- std_cosine_sim = np.std(cosine_sim)
- print(f"Standard deviation Cosine Similarity: {std_cosine_sim}")
- # %% [markdown]
- # Display predicted vs expected maps
- # %%
- from biolab.bspm import image2layout
- lyt = image2layout(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EMG_lyt.jpg',seq='cw')
- # %%
- from biolab.bspm import layout2map
- test = pd.DataFrame(y_test,columns=output_matrix.columns)
- pred = pd.DataFrame(y_pred,columns=output_matrix.columns)
- mapstest = {int(col[1:]): test[col][2] for col in output_matrix.columns}
- mapspred = {int(col[1:]): pred[col][2] for col in output_matrix.columns}
- vmaptest = layout2map(layout=lyt,mapping=mapstest)
- vmappred = layout2map(layout=lyt,mapping=mapspred)
- # %%
- plt.imshow(intmappred,vmin=28,vmax=36,cmap='jet')
- plt.colorbar()
- #plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\EMG_BSPM_regression_pred_FIST.pdf')
- plt.show()
- # %% [markdown]
- # ## Scalability
- # %% [markdown]
- # ### Import Matlab format data from 128CH wireless system
- # %% [markdown]
- # Generate BSPM
- # %%
- import os
- import h5py
- import pandas as pd
- from biolab.bspm import BSPM,image2layout
- from biolab.utils import apply2all
- fs = 1954
- # Save new recordings into .bspm files
- # Define parameters
- readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings\Scalability'
- # Convert electrode array image into layout
- EMG_126CH_lyt = image2layout(image_path=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EMG_126CH_lyt.jpg',seq='cw')
- # Define operation to be applied to each file
- def func(filepath):
- # Extract filename from filepath
- filename = os.path.splitext(os.path.basename(filepath))[0]
- # Split filename into sections
- _,_,muscle,_ = filename.split('_',3)
- meta = {
- 'mode': 'EMG',
- 'measurement': 'M1',
- 'muscle': muscle,
- 'amplifier_sample_rate': fs
- }
- with h5py.File(filepath, 'r') as f:
- # Access the dataset inside the file
- emgdata = np.delete(f['EMG_DATA'][:], [0, 64], axis=0) # remove not-connected channels
- t = np.linspace(0,emgdata.shape[1]/fs,emgdata.shape[1],endpoint=False)
- # Create channel names
- channel_names = [f'A{i+1}' for i in range(emgdata.shape[0])]
- # Create the recorded data
- recdata = pd.DataFrame(emgdata.T, columns=channel_names, index=t)
- # Store channel information
- chns = {}
- chns['custom_channel_name'] = channel_names
- chns['custom_order'] = range(126)
- chns['electrode_impedance_magnitude'] = np.zeros(shape=126,dtype='float')
- chns['electrode_impedance_phase'] = np.zeros(shape=126,dtype='float')
- chns['location'] = range(1,127)
- chns['native_channel_name'] = channel_names
- chns['native_order'] = range(126)
- chns['port_prefix'] = np.full(126,'A')
- chns['ref_channel'] = np.zeros(shape=126,dtype='float')
- # Create the channel information data
- chdata = pd.DataFrame(chns)
- MP = BSPM(recorded_data=recdata,channel_data=chdata,layout=EMG_126CH_lyt,metadata=meta)
- # Save BSPM object
- MP.save(savepath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Scalability\\'+filename+'.bspm')
- # Call DataSaver function
- apply2all(read_directory=readpath,extension='.mat',func=func)
- # %% [markdown]
- # ### Preprocess data and obtain relevant EMG envelopes
- # %% [markdown]
- # Loading and data preprocessing
- # %%
- from biolab.bspm import BSPM
- DATA = {
- 'BICEPS': r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Scalability\Ruben_electrode_biceps_ordered.bspm',
- 'BTD': r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Scalability\Ruben_electrode_btd_ordered.bspm',
- 'FIST1': r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Scalability\Ruben_electrode_fist_ordered.bspm',
- 'FIST2': r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Scalability\Ruben_electrode_fist_ordered2.bspm'
- }
- # Load BSPM data
- MP = BSPM.load(loadpath=DATA['BTD'])
- # Preprocess data
- rMP = MP.resample(newfs=800)
- rMP.preprocess(bw=(5,399))
- # %% [markdown]
- # EMG envelope
- # %%
- from scipy.signal import find_peaks
- window_size = 500
- envelopes = rMP.preprocessed_data.abs().rolling(window=window_size,min_periods=1).mean().loc[70:,:]
- channel = 'A126'
- peaks_dict = {}
- for channel in envelopes.columns:
- # Find peaks in the current channel data
- peaks, _ = find_peaks(envelopes[channel],distance=9*800)
- peaks_dict[channel] = peaks
- plt.plot(rMP.preprocessed_data[channel][70:].abs())
- plt.plot(envelopes[channel])
- # Mark the peaks with vertical lines (vlines) on the signal
- plt.vlines(envelopes.index[peaks_dict[channel]], ymin=envelopes[channel].min(), ymax=envelopes[channel].max(),
- colors='r', linestyles='--', label='Peaks')
- plt.show()
- # %% [markdown]
- # Compute average EMG values and generate mapping
- # %%
- average_peaks_dict = {}
- for channel, indices in peaks_dict.items():
- # Get the values at the specified peak indices for the current channel
- peak_values = envelopes[channel].iloc[indices]
- # Compute the average of those values
- average_peaks_dict[channel] = peak_values.mean()
- # %% [markdown]
- # Generate mapping
- # %%
- from biolab.bspm import layout2map
- new_keys = rMP.channel_data['location'].values
- # Modify dictionary to relate to locations
- updated_dict = {new_keys[i]: value for i, (_,value) in enumerate(average_peaks_dict.items())}
- vmap = layout2map(layout=rMP.layout,mapping=updated_dict)
- # %% [markdown]
- # Display BSPM
- # %%
- plt.imshow(vmap[:,:,0])
- plt.colorbar()
- # %% [markdown]
- # Interpolate BSPM and save as figure
- # %%
- from biolab.bspm import interpolate
- intmap = interpolate(valmap=vmap,method='cubic')
- # %%
- import matplotlib.pyplot as plt
- plt.imshow(intmap,cmap='viridis')
- plt.colorbar()
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_5\Fist1_viridis.pdf')
- plt.show()
- # %% [markdown]
- # Study differences between biceps, triceps and deltoid activity
- # %%
- from biolab.bspm import layout2map
- muscle_peaks_dict = {}
- for channel, indices in peaks_dict.items():
- # Get the values at the specified peak indices for the current channel
- peak_values = envelopes[channel].iloc[indices]
- muscle_peaks_dict[channel] = peak_values.values[0] # 0 - Biceps, 1 - Triceps, 2 - Deltoids
- new_keys = rMP.channel_data['location'].values
- # Modify dictionary to relate to locations
- updated_dict = {new_keys[i]: value for i, (_,value) in enumerate(muscle_peaks_dict.items())}
- vmap = layout2map(layout=rMP.layout,mapping=updated_dict)
- # %%
- plt.imshow(vmap[:,:,0])
- plt.colorbar()
- # %% [markdown]
- # Interpolate BSPM and save as figure
- # %%
- from biolab.bspm import interpolate
- intmap = interpolate(valmap=vmap,method='cubic')
- # %%
- import matplotlib.pyplot as plt
- plt.imshow(intmap,vmin=0,vmax=200,cmap='viridis')
- plt.colorbar()
- plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_5\Deltoid_BTD_viridis.pdf',dpi=1000)
- plt.show()
- # %%
- import numpy as np
- import plotly.graph_objects as go
- import matplotlib
- import matplotlib.pyplot as plt
- # --- Example Data ---
- testdata = intmap
- # --- Downsampling the Array for Efficiency ---
- downsample_factor = (10, 5) # (rows, cols)
- data_ds = testdata[::downsample_factor[0], ::downsample_factor[1]]
- # --- Circular Loop Parameters ---
- rows, cols = data_ds.shape
- # Limit the angle range to 300 degrees, which is approximately 5.24 radians
- theta = np.linspace(0, np.deg2rad(30), cols) # 300 degrees in radians
- base_radius = 10 # Base radius of the loop
- # --- Create 2D Grid for Circular Loop ---
- theta_grid, z_grid = np.meshgrid(theta, np.arange(rows))
- # --- Apply Height Scaling ---
- height_factor = 0.3 # Make the shape 30% as tall
- z_scaled = z_grid * height_factor
- # --- Apply Width Scaling ---
- width_factor = 3 # Make the shape 50% wider
- bulge_factor = 0.1 # How much wider it gets at the center
- # Bulging radius with width scaling applied
- radius_grid = base_radius * (1 + bulge_factor * np.sin(np.pi * z_grid / rows))
- radius_scaled = radius_grid * width_factor # Scale the width
- # Circular coordinates with varying radius and width scaling
- x_grid = radius_scaled * np.cos(theta_grid)
- y_grid = radius_scaled * np.sin(theta_grid)
- # --- Preprocess Data ---
- # Handle NaN values by replacing them with -1
- data_with_nan_replacement = np.copy(data_ds)
- data_with_nan_replacement[np.isnan(data_with_nan_replacement)] = -1 # Replace NaNs with -1
- # --- Create a custom jet colormap ---
- jet = matplotlib.colormaps['viridis'] # Use the new Matplotlib API
- # --- Create a colorscale (with -1 mapped to transparent) ---
- jet_colors = jet(np.linspace(0, 1, 256)) # Get the jet colormap's RGBA values
- jet_colors[0] = [0, 0, 0, 0] # Set the first color to fully transparent (for NaNs)
- # Convert the RGBA colors to Plotly's 'rgba' format
- jet_colors_rgba = [f'rgba({int(c[0]*255)}, {int(c[1]*255)}, {int(c[2]*255)}, {c[3]})' for c in jet_colors]
- # --- Plotting with Plotly (3D Surface) ---
- fig = go.Figure(data=[go.Surface(
- z=z_scaled, # Apply the scaled height
- x=x_grid, # Circular x coordinates with bulge and width effect
- y=y_grid, # Circular y coordinates with bulge and width effect
- surfacecolor=data_with_nan_replacement, # Color data
- colorscale=jet_colors_rgba, # Custom colorscale with NaN handling
- colorbar=dict(title="Intensity"), # Colorbar for intensity values
- )])
- # --- Adjusting Aspect Ratio and Axis Range ---
- fig.update_layout(
- scene=dict(
- xaxis=dict(title='X', range=[-base_radius * width_factor * (1 + bulge_factor), base_radius * width_factor * (1 + bulge_factor)]),
- yaxis=dict(title='Y', range=[-base_radius * width_factor * (1 + bulge_factor), base_radius * width_factor * (1 + bulge_factor)]),
- zaxis=dict(title='Layer', range=[0, rows * height_factor]), # Adjust z-axis range for the scaled height
- camera=dict(eye=dict(x=1.5, y=1.5, z=1.5)), # Adjust camera angle for better visualization
- ),
- margin=dict(l=0, r=0, b=0, t=50), # Adjust margins for a clean view
- )
- # --- Display the plot ---
- fig.show()
- # %%
- import numpy as np
- import plotly.graph_objects as go
- import matplotlib
- import matplotlib.pyplot as plt
- # --- Example Data ---
- testdata = intmap.T
- # --- Downsampling the Array for Efficiency ---
- downsample_factor = (5, 5) # (rows, cols)
- data_ds = testdata[::downsample_factor[0], ::downsample_factor[1]]
- # --- Circular Loop Parameters ---
- rows, cols = data_ds.shape
- # Limit the angle range to 300 degrees, which is approximately 5.24 radians
- theta = np.linspace(0, np.deg2rad(300), cols) # 300 degrees in radians
- base_radius = 10 # Base radius of the loop
- # --- Create 2D Grid for Circular Loop ---
- theta_grid, z_grid = np.meshgrid(theta, np.arange(rows))
- # --- Apply Height Scaling ---
- height_factor = 0.15 # Make the shape 30% as tall
- z_scaled = z_grid * height_factor
- # --- Apply Width Scaling ---
- width_factor = 3 # Make the shape 50% wider
- bulge_factor = 0.15 # How much wider it gets at the center
- # Bulging radius with width scaling applied
- radius_grid = base_radius * (1 + bulge_factor * np.sin(np.pi * z_grid / rows))
- radius_scaled = radius_grid * width_factor # Scale the width
- # Circular coordinates with varying radius and width scaling
- x_grid = radius_scaled * np.cos(theta_grid)
- y_grid = radius_scaled * np.sin(theta_grid)
- # --- Preprocess Data ---
- # Handle NaN values by replacing them with -1
- data_with_nan_replacement = np.copy(data_ds)
- data_with_nan_replacement[np.isnan(data_with_nan_replacement)] = -1 # Replace NaNs with -1
- # --- Create a custom jet colormap ---
- jet = matplotlib.colormaps['viridis'] # Use the new Matplotlib API
- # --- Create a colorscale (with -1 mapped to transparent) ---
- jet_colors = jet(np.linspace(0, 1, 256)) # Get the jet colormap's RGBA values
- jet_colors[0] = [0, 0, 0, 0] # Set the first color to fully transparent (for NaNs)
- # Convert the RGBA colors to Plotly's 'rgba' format
- jet_colors_rgba = [f'rgba({int(c[0]*255)}, {int(c[1]*255)}, {int(c[2]*255)}, {c[3]})' for c in jet_colors]
- # --- Plotting with Plotly (3D Surface) ---
- fig = go.Figure(data=[go.Surface(
- z=z_scaled, # Apply the scaled height
- x=x_grid, # Circular x coordinates with bulge and width effect
- y=y_grid, # Circular y coordinates with bulge and width effect
- surfacecolor=data_with_nan_replacement, # Color data
- colorscale=jet_colors_rgba, # Custom colorscale with NaN handling
- colorbar=dict(title="Intensity"), # Colorbar for intensity values
- )])
- # --- Adjusting Aspect Ratio and Axis Range ---
- fig.update_layout(
- scene=dict(
- xaxis=dict(title='X', range=[-base_radius * width_factor * (1 + bulge_factor), base_radius * width_factor * (1 + bulge_factor)]),
- yaxis=dict(title='Y', range=[-base_radius * width_factor * (1 + bulge_factor), base_radius * width_factor * (1 + bulge_factor)]),
- zaxis=dict(title='Layer', range=[0, rows * height_factor]), # Adjust z-axis range for the scaled height
- camera=dict(eye=dict(x=1.5, y=1.5, z=1.5)), # Adjust camera angle for better visualization
- ),
- margin=dict(l=0, r=0, b=0, t=50), # Adjust margins for a clean view
- )
- # --- Display the plot ---
- fig.show()
- # %%
- from biolab.bspm import interpolate
- intmaptest = interpolate(valmap=vmaptest,method='cubic')
- intmappred = interpolate(valmap=vmappred,method='cubic')
- # %% [markdown]
- # ## Convert `jet` colorspace to `viridis`
- # %%
- import fitz # PyMuPDF
- import numpy as np
- import cv2
- import matplotlib.pyplot as plt
- import matplotlib.cm as cm
- from matplotlib.colors import Normalize
- from PIL import Image
- def extract_image_from_pdf(pdf_path):
- doc = fitz.open(pdf_path)
- for page_index in range(len(doc)):
- page = doc.load_page(page_index)
- images = page.get_images(full=True)
- for img_index, img in enumerate(images):
- xref = images[img_index][0]
- base_image = doc.extract_image(xref)
- image_bytes = base_image["image"]
- img = cv2.imdecode(np.frombuffer(image_bytes, np.uint8), cv2.IMREAD_COLOR)
- return img
- return None
- def convert_jet_to_viridis(image):
- image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
- # Convert to float32 and normalize
- img_float = image_rgb.astype(np.float32) / 255.0
- # Simulate inverse of `jet` colormap
- jet = cm.get_cmap('jet', 256)
- jet_colors = (jet(np.linspace(0, 1, 256))[:, :3]) # ignore alpha
- # Reshape image and jet LUT
- pixels = img_float.reshape(-1, 3)
- distances = np.linalg.norm(pixels[:, None] - jet_colors[None, :], axis=2)
- jet_indices = np.argmin(distances, axis=1)
- # Normalize back to data values
- normed_data = jet_indices / 255.0
- # Apply viridis colormap
- viridis = cm.get_cmap('viridis')
- viridis_img = viridis(normed_data)[:, :3]
- viridis_img = (viridis_img.reshape(image.shape[0], image.shape[1], 3) * 255).astype(np.uint8)
- return cv2.cvtColor(viridis_img, cv2.COLOR_RGB2BGR)
- def save_image_as_pdf(image, output_pdf_path):
- img_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
- img_pil.save(output_pdf_path, "PDF")
- # === USAGE ===
- filepath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\EMG_BSPM_regression_test_UP'
- input_pdf = filepath+'.pdf'
- output_pdf = filepath+'_viridis.pdf'
- image = extract_image_from_pdf(input_pdf)
- if image is not None:
- viridis_image = convert_jet_to_viridis(image)
- save_image_as_pdf(viridis_image, output_pdf)
- print(f"Saved converted PDF as {output_pdf}")
- else:
- print("No image found in PDF.")
- # %%
- import numpy as np
- import matplotlib.pyplot as plt
- import seaborn as sns
- def plot_confusion_matrix(cm, labels=None, title='Confusion Matrix', pdf_path='confusion_matrix.pdf'):
- fig, ax = plt.subplots(figsize=(6, 5), constrained_layout=True)
- sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
- xticklabels=labels, yticklabels=labels,
- cbar=True, square=True, linewidths=0.5, vmin=0, vmax=20, ax=ax)
- ax.set_title(title)
- ax.set_xlabel('Predicted')
- ax.set_ylabel('Actual')
- # Save to PDF
- fig.savefig(pdf_path, format='pdf')
- print(f"Saved confusion matrix to: {pdf_path}")
- # === Example Usage ===
- conf_matrix = np.array([[13, 4, 3],
- [10, 11, 0],
- [8, 7, 5]])
- class_labels = ['Class A', 'Class B', 'Class C']
- plot_confusion_matrix(conf_matrix, labels=class_labels, pdf_path='my_confusion_matrix.pdf')
Wireless_BSPM.ipynb at commit c1ccfee, no license · at the source
Overview
- Institute for Biomedical Innovation, University of Cambridge,Cambridge, UK
- Electrical Engineering Division, Department of Engineering, University of Cambridge,Cambridge, UK
- Department of Bioengineering, Faculty of Engineering, Imperial College London,London, UK
- Present Address: Instituto de Microelectrónica, IMSE-CNM, (CSIC Universidad de Sevilla),Av. Américo Vespucio 28, 41092 Sevilla, Spain
- IKERBASQUE, Basque Foundation for Science,Bilbao, Spain
- POLYMAT, Department of Mining-Metallurgy Engineering and Materials Science, School of Engineering, University of the Basque Country (UPV/EHU),Bilbao, Spain
- POLYMAT, University of the Basque Country UPV/EHU,Av.Tolosa 72, 20018 Donostia-San Sebastian, Gipuzkoa Spain
Abstract
Cutaneous electrophysiology is a fundamental non-invasive technique for assessing electrically active organs such as the brain, heart, and muscles. Standard approaches, however, are limited in spatial resolution, reducing sensitivity to certain pathological features. The development of body surface potential mapping using electrode arrays has helped overcome these limitations, enhancing the diagnostic power of cutaneous recordings, yet clinical adoption remains constrained by challenges in electrode performance, wiring complexity, wearability, data transmission, and interpretability. Here, we present a hybrid e-textile electrode array system that overcomes these barriers, enabling simultaneous mapping of electrical activity along the cortico-muscular axis. The system combines application-specific conducting polymer coatings to improve electrode performance, a flexible fabrication process for robust connectivity and wearability, and interpretable machine learning algorithms for data analysis. In controlled single-subject experiments, we demonstrate reliable muscle and brain recordings, enabling classification of grasped object shapes and somatosensory stimuli. Simultaneous multi-site recordings along the cortico-muscular axis provide spatial maps of reaction time distributions and allow prediction of muscle activation patterns from cortical activity. This platform establishes a framework for wearable, multi-modal electrophysiological mapping and non-invasive study of cortico-muscular dynamics, representing a step towards practical brain–body interfaces with applications in neurorehabilitation, prosthetics, and human–machine interaction.
Reproduced under the paper's license (CC BY), from the paper cited above.
Repositories
Its files are read in the Code ↔ Paper reader above, with 19 matches between paragraphs and lines of code.
Zenodo 20041637
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
- 27 September 2026: the link answers (HTTP 200)
3 files
- Additional_figures.ipynb
, Jupyter, 1,149 lines - External stimuli.ipynb, Jupyter, 167 lines
- Wireless_BSPM.ipynb, Jupyter, 3,675 lines
rr1017/cortico-muscular-axis-mapping
c1ccfee5ca3314ef5c3c2c2fb8b7aa6a14008166, 28 April 2026Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
3 files
- Additional_figures.ipynb
, Jupyter, 1,149 lines, 5 matches - External stimuli.ipynb, Jupyter, 167 lines
- Wireless_BSPM.ipynb, Jupyter, 3,675 lines, 14 matches
Code availability
The codes used for collecting data are available from the corresponding author upon reasonable request.
Reproduced under the paper's license (CC BY), from the paper cited above.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 6 scripts, each with its path and the digest of its content;
- 19 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Data availability
All data supporting the findings of this study are available within the article and its supplementary files. Any additional requests for information can be directed to, and will be fulfilled by, the corresponding author. Source data are provided with this paper. The code used to process and analyse the data in this study is publicly available at 10.5281/
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 27 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 10 authors, 4 keywords, 8 MeSH terms, 3 funders, 58 references.
Cite
This paper
Serrano, R. R.-M., Brunt, C., Tao, X., Zajaczkowski, M., Dominguez-Alfaro, A., Picchio, M. L., Mantione, D., Drakakis, E. M., Mecerreyes, D., & Malliaras, G. G. (2026). Body surface potential mapping of the cortico-muscular axis using smart textile electrode arrays. Nature communications, 17(1), 8441. https://
BibTeX
@article{serrano2026body
author = {Serrano, Ruben Ruiz-Mateos and Brunt, Charlie and Tao, Xudong and Zajaczkowski, Maciej and Dominguez-Alfaro, Antonio and Picchio, Matias L. and Mantione, Daniele and Drakakis, Emmanuel M. and Mecerreyes, David and Malliaras, George G.},
title = {{Body surface potential mapping of the cortico-muscular axis using smart textile electrode arrays}},
journal = {Nature communications},
year = {2026},
month = jul,
volume = {17},
number = {1},
pages = {8441},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/
url = {https://
pmid = {42420293},
pmcid = {PMC13478180}
}
RIS
TY - JOUR
AU - Serrano, Ruben Ruiz-Mateos
AU - Brunt, Charlie
AU - Tao, Xudong
AU - Zajaczkowski, Maciej
AU - Dominguez-Alfaro, Antonio
AU - Picchio, Matias L.
AU - Mantione, Daniele
AU - Drakakis, Emmanuel M.
AU - Mecerreyes, David
AU - Malliaras, George G.
TI - Body surface potential mapping of the cortico-muscular axis using smart textile electrode arrays
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/
VL - 17
IS - 1
SP - 8441
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1038/
"type": "article-journal",
"title": "Body surface potential mapping of the cortico-muscular axis using smart textile electrode arrays",
"container-title": "Nature communications",
"author": [
{
"family": "Serrano",
"given": "Ruben Ruiz-Mateos"
},
{
"family": "Brunt",
"given": "Charlie"
},
{
"family": "Tao",
"given": "Xudong"
},
{
"family": "Zajaczkowski",
"given": "Maciej"
},
{
"family": "Dominguez-Alfaro",
"given": "Antonio"
},
{
"family": "Picchio",
"given": "Matias L."
},
{
"family": "Mantione",
"given": "Daniele"
},
{
"family": "Drakakis",
"given": "Emmanuel M."
},
{
"family": "Mecerreyes",
"given": "David"
},
{
"family": "Malliaras",
"given": "George G."
}
],
"container-title-short":
"volume": "17",
"issue": "1",
"page": "8441",
"DOI": "10.1038/
"PMID": "42420293",
"PMCID": "PMC13478180",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
8
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1002/advs.202519893 [code]
- NeuroSuite for Long-Term Functional and Structural Studies of Air-Liquid Interface Cerebral Organoids.Journal: Advanced science (Weinheim, Baden-Wurttemberg, Germany)In common: specparam (formerly FOOOF), Pillow, pandas, 3 other tools, 2 authors
- [2] doi:10.7554/elife.100605 [code]
- Age-related changes in ‘cortical’ 1/
f dynamics are linked to cardiac activity Journal: n/aIn common: WFDB Python, specparam (formerly FOOOF), h5py, 6 other tools, other - [3] doi:10.1038/s42003-026-10957-8 [code]
- Brain defence by the extracellular matrix protein Cochlin.Journal: Communications biologyIn common: WFDB Python, Plotly, OpenCV, 7 other tools
- [4] doi:10.1016/j.xcrm.2026.102766 [code]
- A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.Journal: Cell reports. MedicineIn common: Plotly, OpenCV, h5py, 7 other tools
- [5] doi:10.1038/s41597-025-05174-7 [code]
- A large-scale MEG and EEG dataset for object recognition in naturalistic scenesJournal: n/aIn common: Plotly, OpenCV, h5py, 7 other tools
- [6] doi:10.1016/j.isci.2026.116825 [code]
- Social hierarchy shapes behavioral and transcriptional responses to chronic stress and ketamine in male mice.Journal: iScienceIn common: Plotly, OpenCV, h5py, 7 other tools
- [7] doi:10.1162/imag.a.1269 [code]
- From early to contemporary normative modeling: Mapping individual differences in neurophysiological signals.Journal: Imaging neuroscience (Cambridge, Mass.)In common: specparam (formerly FOOOF), Plotly, seaborn, 5 other tools, 1 reference
- [8] doi:10.3389/fncom.2026.1786996 [code]
- Schumann-anchored golden ratio organization of human neural oscillations.Journal: Frontiers in computational neuroscienceIn common: specparam (formerly FOOOF), h5py, Pillow, 6 other tools
- [9] doi:10.1371/journal.pone.0348866 [code]
- Using deep learning to identify inherited retinal diseases based on wide-field retinal imaging data.Journal: PloS oneIn common: Plotly, OpenCV, Pillow, 6 other tools, other
- [10] doi:10.1038/s41467-026-75128-z
- Surface circumferential spinal cord recording in freely moving rodents.Journal: Nature communicationsIn common: 2 authors
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 2 repositories of the authors' code, each at its verified commit and with its license, 6 scripts, and 19 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:bdf2f5104d14934b…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
