Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: muscle artifact detection #7407

Merged
merged 35 commits into from
Apr 7, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7c5e680
muscle artifact detection
AdoNunes Mar 9, 2020
e42ed5c
docstrings
AdoNunes Mar 9, 2020
b705ae7
docstrings
AdoNunes Mar 9, 2020
73351db
Alex suggestions
AdoNunes Mar 9, 2020
24da9f6
example
AdoNunes Mar 9, 2020
97f6089
Merge branch 'master' of git://github.com/mne-tools/mne-python into d…
AdoNunes Mar 9, 2020
72c8c4d
example
AdoNunes Mar 9, 2020
97d3ac4
Merge branch 'detect_muscle' of https://github.com/AdoNunes/mne-pytho…
AdoNunes Mar 9, 2020
39be3d3
for eeg
AdoNunes Mar 10, 2020
ab04ad7
skip_by_annotation
AdoNunes Mar 11, 2020
2a66910
new func added
AdoNunes Mar 17, 2020
d477c60
docstring
AdoNunes Mar 17, 2020
62a00eb
cross ref
AdoNunes Mar 17, 2020
8f289c5
cross ref2
AdoNunes Mar 17, 2020
a0722fc
Alex suggestions
AdoNunes Mar 17, 2020
1eaee82
new parameters
AdoNunes Mar 17, 2020
41420b3
n_jobs
AdoNunes Mar 17, 2020
a9d5d4d
docstring
AdoNunes Mar 17, 2020
38a328b
API name change, zscore
AdoNunes Mar 20, 2020
b232e4f
test update
AdoNunes Mar 20, 2020
d9412db
sqrt nchans
AdoNunes Mar 23, 2020
b5e378e
handle nans zscore
AdoNunes Mar 23, 2020
b055b53
example note
AdoNunes Mar 23, 2020
5c36c03
Apply suggestions from code review drammock
AdoNunes Mar 24, 2020
ee8e871
revision changes
AdoNunes Mar 24, 2020
0953d9a
docstring
AdoNunes Mar 24, 2020
647b9ab
docstring
AdoNunes Mar 24, 2020
973c1d7
ch_type param
AdoNunes Mar 25, 2020
14ccd5c
Merge branch 'master' into detect_muscle
AdoNunes Mar 25, 2020
5701cb0
logger
AdoNunes Mar 26, 2020
416ab47
docstrings
AdoNunes Mar 26, 2020
269277e
ch type
AdoNunes Mar 30, 2020
d82c614
test no meeg data
AdoNunes Apr 7, 2020
2e1531d
test no meeg data
AdoNunes Apr 7, 2020
d1d448d
fix docstring
AdoNunes Apr 7, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/changes/latest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ Changelog

- Improve repr of :class:`mne.Info` (empty items are not shown anymore) by `Clemens Brunner`_

- Add function :func:`mne.preprocessing.annotate_muscle_zscore` to annotate periods with muscle artifacts. by `Adonay Nunes`_

Bug
~~~

Expand Down
10 changes: 10 additions & 0 deletions doc/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,16 @@ @article{MourtazaevEtAl1995
year = {1995}
}

@article{Muthukumaraswamy2013,
author = {Muthukumaraswamy, Suresh},
doi = {10.3389/fnhum.2013.00138},
journal = {Frontiers in Human Neuroscience},
pages = {138},
title = {High-frequency brain activity and muscle artifacts in {{MEG}}/{{EEG}}: A review and recommendations},
volume = {7},
year = {2013}
}

@inproceedings{NdiayeEtAl2016,
author = {Ndiaye, Eugene and Fercoq, Olivier and Gramfort, Alexandre and Salmon, Joseph},
booktitle = {Advances in Neural Information Processing Systems 29},
Expand Down
32 changes: 14 additions & 18 deletions examples/preprocessing/plot_muscle_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@

Muscle contractions produce high frequency activity that can mask brain signal
of interest. Muscle artifacts can be produced when clenching the jaw,
swallowing, or twitching a head muscle. Muscle artifacts are most noticeable
in the range of 110-140 Hz.
swallowing, or twitching a cranial muscle. Muscle artifacts are most
noticeable in the range of 110-140 Hz.

This example uses :func:`~mne.preprocessing.annotate_muscle_zscore` to annotate
segments where muscle activity is likely present. This is done by band-pass
filtering the data in the 110-140 Hz range. Then, the envelope is taken using
the hilbert analytical signal to only consider the absolute amplitude and not
the phase of the high frequency signal. The envelope is z-scored and summed
across channels and divided by the square root of the number of channels.
Because muscle artifacts last several hundred milliseconds, we then low-pass
filter the averaged z-scores at 4 Hz, to remove transient peaks. Segments above
a set threshold are annotated as ``BAD_muscle``. In addition, the
``min_length_good`` parameter determines the cutoff for whether short spans of
"good data" in between muscle artifacts are included in the surrounding "BAD"
annotation.
Because muscle artifacts last several hundred milliseconds, a low-pass filter
is applied on the averaged z-scores at 4 Hz, to remove transient peaks.
Segments above a set threshold are annotated as ``BAD_muscle``. In addition,
the ``min_length_good`` parameter determines the cutoff for whether short
spans of "good data" in between muscle artifacts are included in the
surrounding "BAD" annotation.

"""
# Authors: Adonay Nunes <[email protected]>
Expand All @@ -29,7 +29,6 @@
import os.path as op
import matplotlib.pyplot as plt
import numpy as np
from mne import pick_types
from mne.datasets.brainstorm import bst_auditory
from mne.io import read_raw_ctf
from mne.preprocessing import annotate_muscle_zscore
Expand All @@ -45,14 +44,7 @@
raw.resample(300, npad="auto")

###############################################################################
# The inputted raw data has to be from a single channel type as there might be
# different channel type magnitudes.
# If the MEG has axial gradiometers and magnetometers, select magnetometers as
# they are more sensitive to muscle activity.
picks = pick_types(raw.info, meg=True, ref_meg=False)

###############################################################################
# Notch filter the data.
# Notch filter the data:
#
# .. note::
# If line noise is present, you should perform notch-filtering *before*
Expand All @@ -61,10 +53,14 @@

raw.notch_filter([50, 100])

###############################################################################

# The threshold is data dependent, check the optimal threshold by plotting
# ``scores_muscle``.
threshold_muscle = 5 # z-score
out = annotate_muscle_zscore(raw, picks=picks, threshold=threshold_muscle,
# Choose one channel type, if there are axial gradiometers and magnetometers,
# select magnetometers as they are more sensitive to muscle activity.
out = annotate_muscle_zscore(raw, ch_type="mag", threshold=threshold_muscle,
min_length_good=0.2, filter_freq=[110, 140])
annot_muscle, scores_muscle = out

Expand Down
58 changes: 33 additions & 25 deletions mne/preprocessing/artifact_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,19 @@


@verbose
def annotate_muscle_zscore(raw, threshold=4, picks=None, min_length_good=0.1,
def annotate_muscle_zscore(raw, threshold=4, ch_type=None, min_length_good=0.1,
filter_freq=(110, 140), n_jobs=1, verbose=None):
"""Detect segments that likely contain muscle artifacts.
"""Create annotations for segments that likely contain muscle artifacts.

Detects data segments containing activity in the frequency range given by
``filter_freq``, that exceeds the specified z-score threshold.

Raw data is band-pass filtered between ``filter_freq`` especified
frequencies (default is 110 - 140 Hz), the signal envelope computed,
z-scored across samples, channel summation and division by the square root
of the channel number, and low-pass filtered to better capture beginning
and end of muscle activity and false positive transient peaks.

.. note::
Use a single channel type.
`filter_freq` whose envelope magnitude exceeds the specified z-score
threshold (when summed across channels and divided by `sqrt(n_channels)`).
False-positive transient peaks are prevented by low-pass filtering the
resulting z-score time series at 4 Hz. Only operates on a single channel
type, if `ch_type` is ``None`` it will select the first type in the list
``mag`` ``grad`` ``eeg``.
See :footcite:`Muthukumaraswamy2013` for background on choosing
`filter_freq` and `threshold`.

Parameters
----------
Expand All @@ -36,7 +34,9 @@ def annotate_muscle_zscore(raw, threshold=4, picks=None, min_length_good=0.1,
threshold : float
The threshold in z-scores for marking segments as containing muscle
activity artifacts.
%(picks_all)s
ch_type : 'mag' | 'grad' | 'eeg' | None
The type of sensors to use. If ``None`` it will take the first type in
``mag`` ``grad`` ``eeg``.
min_length_good : float | None
The shortest allowed duration of "good data" (in seconds) between
adjacent annotations; shorter segments will be incorporated into the
Expand All @@ -54,31 +54,39 @@ def annotate_muscle_zscore(raw, threshold=4, picks=None, min_length_good=0.1,
Periods with muscle artifacts annotated as BAD_muscle.
scores_muscle : array
Z-score values averaged across channels for each sample.

References
----------
.. footbibliography::
"""
from scipy.stats import zscore
from scipy.ndimage.measurements import label

raw_copy = raw.copy()
raw_copy.pick(picks)
ch_type = raw_copy.get_channel_types()

# Remove ref chans if MEG data just in case
meg = [True for e in ['mag', 'grad'] if (e in ch_type)]
if bool(meg) is True:
raw_copy.pick_types(ref_meg=False)
ch_type = raw_copy.get_channel_types()

# Only one type of channel, otherwise z-score will be biased
assert(len(np.unique(ch_type)) == 1), \
'Different channel types, pick one type'
if ch_type is None:
raw_ch_type = raw_copy.get_channel_types()
if 'mag' in raw_ch_type:
ch_type = 'mag'
elif 'grad' in raw_ch_type:
ch_type = 'grad'
elif 'eeg' in raw_ch_type:
ch_type = 'eeg'
logger.info(u'Using %s sensors for muscle artifact detection'
% (ch_type))

if ch_type in ('mag', 'grad'):
raw_copy.pick_types(meg=ch_type, ref_meg=False)
elif ch_type == 'eeg':
raw_copy.pick_types(meg=False, eeg=True)

raw_copy.filter(filter_freq[0], filter_freq[1], fir_design='firwin',
pad="reflect_limited", n_jobs=n_jobs)
raw_copy.apply_hilbert(envelope=True, n_jobs=n_jobs)
sfreq = raw_copy.info['sfreq']

data = raw_copy.get_data(reject_by_annotation="NaN")
nan_mask = ~np.isnan(data[0])
sfreq = raw_copy.info['sfreq']

art_scores = zscore(data[:, nan_mask], axis=1)
art_scores = art_scores.sum(axis=0) / np.sqrt(art_scores.shape[0])
Expand Down
4 changes: 2 additions & 2 deletions mne/preprocessing/tests/test_artifact_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ def test_movement_annotation_head_correction():
def test_muscle_annotation():
"""Test correct detection muscle artifacts."""
raw = read_raw_fif(raw_fname, allow_maxshield='yes').load_data()
raw.pick_types(meg='mag', ref_meg=False)
raw.notch_filter([50, 110, 150])
# Check 2 muscle segments are detected
annot_muscle, scores = annotate_muscle_zscore(raw, threshold=10)
annot_muscle, scores = annotate_muscle_zscore(raw, ch_type='mag',
threshold=10)
onset = annot_muscle.onset * raw.info['sfreq']
onset = onset.astype(int)
np.testing.assert_array_equal(scores[onset].astype(int), np.array([23,
Expand Down