Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite or Binder
ATAR: Automatic and Tunable Artifact Removal Algorithm¶
ATAR: Automatic and Tunable Artifact Removal Algorithm - Tuning
The algorithm is based on wavelet packet decomposion (WPD), the full description of algorithm can be found here Automatic and Tunable Artifact Removal Algorithm for EEG from the article [1]. Figure 1 shows the the block diagram and operating mode of filtering.
The algorithm is applied on the given multichannel signal X (n,nch), window wise and reconstructed with overall add method. The defualt window size is set to 1 sec (128 samples). For each window, the threshold \(\theta_\alpha\) is computed and applied to filter the wavelet coefficients.
There is manily one parameter that can be tuned \(\beta\) with different operating modes and other settings. Here is the list of parameters and there simplified meaning given:
Parameters:
\(\beta\) : This is a main parameter to tune, highher the value, more aggressive the algorithm to remove the artifacts. By default it is set to 0.1. \(\beta\) is postive float value.
*OptMode*: This sets the mode of operation, which decides hoe to remove the artifact. By default it is set to ‘soft’, which means Soft Thresholding, in this mode, rather than removing the pressumed artifact, it is suppressed to the threshold, softly.
OptMode='linAtten'
, suppresses the pressumed artifact depending on how far it is from threshold. Finally, the most common mode - Elimination (OptMode=’elim’), which remove the pressumed artifact.Soft Thresholding and Linear Attenuation require addition parameters to set the associated thresholds which are by default set to bf=2, gf=0.8.
*wv=db3*: Wavelet funtion, by default set to db3, could be any of [‘db3’…..’db38’, ‘sym2…..sym20’, ‘coif1…..coif17’, ‘bior1.1….bior6.8’, ‘rbio1.1…rbio6.8’, ‘dmey’]
\(k_1\), \(k_2\): Lower and upper bounds on threshold \(\theta_\alpha\).
*IPR=[25,75]*: interpercentile range, range used to compute threshold
Figure below, shows the affect of \(\beta\) on a segment of signal with three different modes.
Reference:
- [1] Bajaj, Nikesh, et al. “Automatic and tunable algorithm for EEG artifact removal using
wavelet decomposition with applications in predictive modeling during auditory tasks.” Biomedical Signal Processing and Control 55 (2020): 101624.
There are three functions in spkit.eeg for ATAR algorithm
spkit.eeg.ATAR(…)
spkit.eeg.ATAR_1Ch(…)
spkit.eeg.ATAR_mCh(…)
*spkit.eeg.ATAR_1Ch* is for single channel input signal x of shape (n,), where as, *spkit.eeg.ATAR_mCh* is for multichannel signal X with shape (n,ch), which uses joblib for parallel processing of multi channels. For some OS, joblib raise an error of *BrokenProcessPool*, in that case use *spkit.eeg.ATAR_mCh_noParallel*, which is same as *spkit.eeg.ATAR_mCh*, except parallel processing. Alternatively, use *spkit.eeg.ATAR_1Ch* with for loop for each channel.
*spkit.eeg.ATAR* is generalized function, this will call *spkit.eeg.ATAR_1Ch* is single channel is passed else *spkit.eeg.ATAR_mCh* and with use_joblib agrument, it can be set to try parallel processing, else will process each channel individually.
We recommed to use *spkit.eeg.ATAR*.
# Importing libraries/spkit
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
import spkit as sp
print('spkit version :', sp.__version__)
# # Import EEG sample data
X, fs, ch_names = sp.data.eeg_sample_14ch()
#X = sp.filterDC_sGolay(X, window_length=fs//3+1)
# ## Filter with highpass
Xf = sp.filter_X(X.copy(),fs=fs, band=[0.5], btype='highpass',ftype='SOS')
t = np.arange(Xf.shape[0])/fs
plt.figure(figsize=(12,5))
plt.plot(t,Xf+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('Xf: 14 channel - EEG Signal (filtered)')
plt.show()
spkit version : 0.0.9.7
Applying ATAR Algorithm¶
Soft Thresholding:¶
# default is set to: OptMode='soft' and :math:`\beta=0.1`
XR = sp.eeg.ATAR(Xf.copy(),verbose=0)
XR.shape
plt.figure(figsize=(12,5))
plt.plot(t,XR+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('XR: Corrected Signal')
plt.show()
plt.figure(figsize=(12,5))
plt.plot(t,(Xf-XR)+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('Xf - XR: Difference (removed signal)')
plt.show()
Linear Attenuation:¶
XR = sp.eeg.ATAR(Xf.copy(),verbose=0,OptMode='linAtten')
print(XR.shape)
plt.figure(figsize=(12,5))
plt.plot(t,XR+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('XR: Corrected Signal')
plt.show()
plt.figure(figsize=(12,5))
plt.plot(t,(Xf-XR)+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('Xf - XR: Difference (removed signal)')
plt.show()
(2048, 14)
Elimination:¶
XR = sp.eeg.ATAR(Xf.copy(),verbose=0,OptMode='elim')
XR.shape
plt.figure(figsize=(12,5))
plt.plot(t,XR+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('XR: Corrected Signal')
plt.show()
plt.figure(figsize=(12,5))
plt.plot(t,(Xf-XR)+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('Xf - XR: Difference (removed signal)')
plt.show()
Tuning \(\beta\) with ‘soft’ : Controlling the aggressiveness¶
betas = np.r_[np.arange(0.01,0.1,0.02), np.arange(0.1,1, 0.1)].round(2)
for b in betas:
XR = sp.eeg.ATAR(Xf.copy(),verbose=0,beta=b,OptMode='soft')
XR.shape
plt.figure(figsize=(15,5))
plt.subplot(121)
plt.plot(t,XR+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('XR: Corrected Signal: '+r'$\beta=$' + f'{b}')
plt.subplot(122)
plt.plot(t,(Xf-XR)+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('Xf - XR: Difference (removed signal)')
plt.show()
Tuning \(\beta\) with ‘elim’¶
betas = np.r_[np.arange(0.01,0.1,0.02), np.arange(0.1,1, 0.1)].round(2)
for b in betas:
XR = sp.eeg.ATAR(Xf.copy(),verbose=0,beta=b,OptMode='elim')
XR.shape
plt.figure(figsize=(15,5))
plt.subplot(121)
plt.plot(t,XR+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('XR: Corrected Signal: '+r'$\beta=$' + f'{b}')
plt.subplot(122)
plt.plot(t,(Xf-XR)+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('Xf - XR: Difference (removed signal)')
plt.show()
Other settings¶
Changing wavelet function¶
XR = sp.eeg.ATAR(Xf.copy(),wv='db8',beta=0.01,OptMode='elim',verbose=0,)
XR.shape
plt.figure(figsize=(15,5))
plt.subplot(121)
plt.plot(t,XR+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('XR: Corrected Signal: '+r'$wv=db8$')
plt.subplot(122)
plt.plot(t,(Xf-XR)+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('Xf - XR: Difference (removed signal)')
plt.show()
XR = sp.eeg.ATAR_mCh_noParallel(Xf.copy(),wv='db32',beta=0.01,OptMode='elim',verbose=0,)
XR.shape
plt.figure(figsize=(15,5))
plt.subplot(121)
plt.plot(t,XR+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('XR: Corrected Signal: '+r'$wv=db32$')
plt.subplot(122)
plt.plot(t,(Xf-XR)+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('Xf - XR: Difference (removed signal)')
plt.show()
Changing upper and lower bounds: \(k_1\), \(k_2\)¶
# :math:`k_1` and :math:`k_2` are lower and upper bound on the threshold :math:`\theta_\alpha`. :math:`k_1` is set to 10,
# which means, the lowest threshold value will be 10, this helps to prevent the removal of entire signal (zeroing out) due
# to present of high magnitute of artifact. :math:`k_2` is largest threshold value, which in terms set
# the decaying curve of threshold :math:`\theta_\alpha`. Increasing k2 will make the removal less aggressive
XR = sp.eeg.ATAR(Xf.copy(),wv='db3',beta=0.1,OptMode='elim',verbose=0,k1=10, k2=200)
XR.shape
plt.figure(figsize=(15,5))
plt.subplot(121)
plt.plot(t,XR+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('XR: Corrected Signal: '+r'$k_2=200$')
plt.subplot(122)
plt.plot(t,(Xf-XR)+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('Xf - XR: Difference (removed signal)')
plt.show()
Changing IPR - Interpercentile range¶
# **IPR is interpercentile range**, which is set to 50% (IPR=[25,75]) as default (inter-quartile range), incresing the range increses the aggressiveness of removing artifacts.
XR = sp.eeg.ATAR(Xf.copy(),wv='db3',beta=0.1,OptMode='elim',verbose=0,k1=10, k2=200, IPR=[15,85])
plt.figure(figsize=(15,5))
plt.subplot(121)
plt.plot(t,XR+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('XR: Corrected Signal: '+r'$IPR=[15,85]$~ 70%')
plt.subplot(122)
plt.plot(t,(Xf-XR)+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('Xf - XR: Difference (removed signal)')
plt.show()
Using the fix threshold \(\theta_\alpha\)¶
# Using the fix threshold $\theta_\alpha=300$, to all the windows
XR = sp.eeg.ATAR(Xf.copy(),wv='db3',thr_method=None,theta_a=300,OptMode='elim',verbose=0)
plt.figure(figsize=(15,5))
plt.subplot(121)
plt.plot(t,XR+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('XR: Corrected Signal: '+r'$\theta_\alpha=300$')
plt.subplot(122)
plt.plot(t,(Xf-XR)+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('Xf - XR: Difference (removed signal)')
plt.show()
Changing window length (5 sec)¶
# **winsize** is be default set to 128 (1 sec), assuming 128 sampling rate, which can be changed as needed. In following example it is changed to 5 sec.
XR = sp.eeg.ATAR(Xf.copy(),winsize=128*5,beta=0.01,OptMode='elim',verbose=0,)
plt.figure(figsize=(15,5))
plt.subplot(121)
plt.plot(t,XR+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('XR: Corrected Signal: '+r'$winsize=5sec$')
plt.subplot(122)
plt.plot(t,(Xf-XR)+np.arange(-7,7)*200)
plt.xlim([t[0],t[-1]])
plt.xlabel('time (sec)')
plt.yticks(np.arange(-7,7)*200,ch_names)
plt.grid()
plt.title('Xf - XR: Difference (removed signal)')
plt.show()
Total running time of the script: (0 minutes 14.437 seconds)
Related examples
EEG Artifact removal using ATAR
EEG Artifact removal using ICA
ATAR Algorithm with MNE RAW Object