-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New feature for removing heart artifacts from EEG or ESG data using a Principal Component Analysis - Optimal Basis Sets (PCA-OBS) algorithm #13037
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
Merged
Merged
Changes from all commits
Commits
Show all changes
92 commits
Select commit
Hold shift + click to select a range
2013fb4
feat: add initial source code
emma-bailey 6d5f5b2
Minimum working example with local data
emma-bailey 3622540
Implement testing dataset
emma-bailey 6c42f33
Feat: Update examples
emma-bailey 38b6b6a
Merge pull request #2 from mne-tools/main
steinnhauser 2fe6a88
Merge pull request #1 from emma-bailey/feat/pca-obs
steinnhauser 61f58b2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 070037d
fix: adjust import paths, add init file to module
steinnhm e1b6732
refactor: rearrange PCA_OBS arg structure, remove kwarg 'sr'
steinnhm ccd4298
refactor: move common variables to module init, further cleanup
steinnhm 61a92ea
feat/initial-cleanup: Remove custom pchip as not in use
emma-bailey a1eb6f6
refactor/initial-cleanup: answer question
emma-bailey d75e927
refactor: rename files to match other modules in preprocessing, reduc…
steinnhm 035a9f6
refactor: remove unused fit_ecgTemplate variable 'baselinerange'
steinnhm 2debb61
refactor: make main methods private, import from module __init__, rem…
steinnhm 97f9a15
refactor: add docstring templates, gather imports
steinnhm 34a2e06
test: add placeholder tests, add multiple todos to address where we g…
steinnhm 3760c02
Removing extra examples
emma-bailey 0a55712
Moving example
emma-bailey e887ace
Adding Niazy reference
emma-bailey ee3b73e
Update example, put pca-obs in a single file
emma-bailey adae352
TODO
emma-bailey b3c1b4e
Update example
emma-bailey ba36dc2
Merge pull request #4 from mne-tools/main
steinnhauser e6aa17d
refactor: change way of calling pca obs method, add comments
steinnhm b150b6c
refactor: move pca obs method out of separate python module, change l…
steinnhm 951e87c
Refactor: Docstrings, removed classes
emma-bailey c70db0a
refactor: remove unrequired index references, adjust variable namings…
steinnhm fb8c7ff
docs: minor edits in docstrings
steinnhm 4c73a96
fix: add minor sanity checks for the function inputs
steinnhm 8a0d73c
Merge pull request #3 from emma-bailey/refactor/initial-cleanup
steinnhauser e69039b
test: add initial test structure, missing validation of post-hear-art…
steinnhm 4bd3a51
style: run pre-commit hooks on test file
steinnhm 07458ba
docs: remove duplicated docstring
steinnhm b2be499
Merge branch 'mne-tools:main' into main
steinnhauser a5e68d7
Removed filter_coords from within the method
emma-bailey 1938573
Adding info to filter to example
emma-bailey 19e0802
style: run import sorter pre-commit hook
steinnhm 49a96d0
refactor,test: migrate data shape to be 1d, add sanity checks for PCA…
steinnhm a6e11fc
Merge branch 'mne-tools:main' into main
steinnhauser 925b293
example:Reshape ecg_events before pca_obs, tests:ecg_events is in sam…
emma-bailey a3d1123
example: update channel selection
emma-bailey e36e665
Merge branch 'mne-tools:main' into main
steinnhauser 2ae84b0
refactor,test: change public qrs kwarg to be more clear about being i…
steinnhm b8d8d7c
Merge pull request #5 from emma-bailey/tests/general-pca-obs
steinnhauser d19049b
style: move fit_ecg_template function to bottom of file to improve re…
steinnhm 3e9a812
Merge branch 'mne-tools:main' into main
steinnhauser 826afec
test: add pytest importorskip for pandas lib in pca obs tests
steinnhm 16937c8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 8f4dcdd
docs: add apply_pca_obs algorithm to doc/api/preprocessing.rst list
steinnhm 35ab331
Merge branch 'main' of https://github.com/emma-bailey/mne-python
steinnhm 121f8dd
Merge branch 'mne-tools:main' into main
steinnhauser 03a8544
Merge branch 'mne-tools:main' into main
steinnhauser 4a6f4e8
MAINT: Satisfy CircleCI
larsoner 28e45ca
FIX: Better
larsoner 6415d97
FIX: Wording
larsoner 7508056
FIX: Docstring
larsoner 6556418
FIX: Name
larsoner 842132d
Merge branch 'mne-tools:main' into main
emma-bailey c8eb284
example: update comments
emma-bailey 89278a6
tutorial: add ref to pca_obs in SSP tutorial section on ECG artefacts
emma-bailey 15c7c5c
Merge branch 'mne-tools:main' into main
steinnhauser 36e0ca0
Update mne/preprocessing/pca_obs.py
steinnhauser fc5bdc8
Update tutorials/preprocessing/50_artifact_correction_ssp.py
steinnhauser d586d62
Update mne/preprocessing/pca_obs.py
steinnhauser d6ae456
refactor: rework qrs_indices to qrs_times, adjust sanity checks, add …
steinnhm 420f4fc
test,example: adjust example and tests to new qrs_times changes, add …
steinnhm 91b0033
Merge branch 'mne-tools:main' into main
steinnhauser 54d67aa
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] f4082b4
feat: add copy kwarg, optionally return new instance, add verbose dec…
steinnhm 0e43130
Merge branch 'main' of https://github.com/emma-bailey/mne-python
steinnhm fd9c41b
test,example: update tests and example with new copy default behavior…
steinnhm a578a7d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 41559ff
lint: resolve pre-commit errors, format
steinnhm 0079605
Merge branch 'main' of https://github.com/emma-bailey/mne-python
steinnhm d170c39
fix: remove conditional raw return, always return raw object regardle…
steinnhm b70545b
lint: resolve linter errors
steinnhm 39c322f
FIX: Doc
larsoner f62f98b
[autofix.ci] apply automated fixes
autofix-ci[bot] ebda34e
FIX: How
larsoner 72facf0
Merge remote-tracking branch 'upstream/main' into ebm
larsoner 92fe25c
TST: Pre
larsoner d38e37f
DOC: whats new
larsoner 2472c84
FIX: Name
larsoner 49babe1
Merge branch 'mne-tools:main' into main
steinnhauser dab5623
Apply suggestions from code review
larsoner 525a8e7
Merge branch 'mne-tools:main' into main
steinnhauser a8e84ff
Update examples/preprocessing/esg_rm_heart_artefact_pcaobs.py
steinnhauser 12b3834
Update examples/preprocessing/esg_rm_heart_artefact_pcaobs.py
steinnhauser c1106c2
Apply suggestions from code review
steinnhauser a4ee593
FIX: Move
larsoner 728a010
Merge branch 'main' into main
larsoner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add PCA-OBS preprocessing for the removal of heart-artefacts from EEG or ESG datasets via :func:`mne.preprocessing.apply_pca_obs`, by :newcontrib:`Emma Bailey` and :newcontrib:`Steinn Hauser Magnusson`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -355,6 +355,7 @@ | |
| "n_frequencies", | ||
| "n_tests", | ||
| "n_samples", | ||
| "n_peaks", | ||
| "n_permutations", | ||
| "nchan", | ||
| "n_points", | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| """ | ||
| .. _ex-pcaobs: | ||
|
|
||
| ===================================================================================== | ||
| Principal Component Analysis - Optimal Basis Sets (PCA-OBS) removing cardiac artefact | ||
| ===================================================================================== | ||
|
|
||
| This script shows an example of how to use an adaptation of PCA-OBS | ||
| :footcite:`NiazyEtAl2005`. PCA-OBS was originally designed to remove | ||
| the ballistocardiographic artefact in simultaneous EEG-fMRI. Here, it | ||
| has been adapted to remove the delay between the detected R-peak and the | ||
| ballistocardiographic artefact such that the algorithm can be applied to | ||
| remove the cardiac artefact in EEG (electroencephalography) and ESG | ||
| (electrospinography) data. We will illustrate how it works by applying the | ||
| algorithm to ESG data, where the effect of removal is most pronounced. | ||
|
|
||
| See: https://www.biorxiv.org/content/10.1101/2024.09.05.611423v1 | ||
| for more details on the dataset and application for ESG data. | ||
|
|
||
| """ | ||
|
|
||
| # Authors: Emma Bailey <bailey@cbs.mpg.de>, | ||
| # Steinn Hauser Magnusson <hausersteinn@gmail.com> | ||
| # License: BSD-3-Clause | ||
| # Copyright the MNE-Python contributors. | ||
|
|
||
| import glob | ||
|
|
||
| import numpy as np | ||
|
|
||
| # %% | ||
| # Download sample subject data from OpenNeuro if you haven't already. | ||
| # This will download simultaneous EEG and ESG data from a single run of a | ||
| # single participant after median nerve stimulation of the left wrist. | ||
| import openneuro | ||
| from matplotlib import pyplot as plt | ||
|
|
||
| import mne | ||
| from mne import Epochs, events_from_annotations | ||
| from mne.io import read_raw_eeglab | ||
| from mne.preprocessing import find_ecg_events, fix_stim_artifact | ||
|
|
||
| # add the path where you want the OpenNeuro data downloaded. Each run is ~2GB of data | ||
| ds = "ds004388" | ||
| target_dir = mne.datasets.default_path() / ds | ||
| run_name = "sub-001/eeg/*median_run-03_eeg*.set" | ||
| if not glob.glob(str(target_dir / run_name)): | ||
| target_dir.mkdir(exist_ok=True) | ||
| openneuro.download(dataset=ds, target_dir=target_dir, include=run_name[:-4]) | ||
| block_files = glob.glob(str(target_dir / run_name)) | ||
| assert len(block_files) == 1 | ||
|
|
||
| # %% | ||
| # Define the esg channels (arranged in two patches over the neck and lower back). | ||
|
|
||
| esg_chans = [ | ||
| "S35", | ||
| "S24", | ||
| "S36", | ||
| "Iz", | ||
| "S17", | ||
| "S15", | ||
| "S32", | ||
| "S22", | ||
| "S19", | ||
| "S26", | ||
| "S28", | ||
| "S9", | ||
| "S13", | ||
| "S11", | ||
| "S7", | ||
| "SC1", | ||
| "S4", | ||
| "S18", | ||
| "S8", | ||
| "S31", | ||
| "SC6", | ||
| "S12", | ||
| "S16", | ||
| "S5", | ||
| "S30", | ||
| "S20", | ||
| "S34", | ||
| "S21", | ||
| "S25", | ||
| "L1", | ||
| "S29", | ||
| "S14", | ||
| "S33", | ||
| "S3", | ||
| "L4", | ||
| "S6", | ||
| "S23", | ||
| ] | ||
|
|
||
| # Interpolation window in seconds for ESG data to remove stimulation artefact | ||
| tstart_esg = -7e-3 | ||
| tmax_esg = 7e-3 | ||
|
|
||
| # Define timing of heartbeat epochs in seconds relative to R-peaks | ||
| iv_baseline = [-400e-3, -300e-3] | ||
| iv_epoch = [-400e-3, 600e-3] | ||
|
|
||
| # %% | ||
| # Next, we perform minimal preprocessing including removing the | ||
| # stimulation artefact, downsampling and filtering. | ||
|
|
||
| raw = read_raw_eeglab(block_files[0], verbose="error") | ||
| raw.set_channel_types(dict(ECG="ecg")) | ||
| # Isolate the ESG channels (include the ECG channel for R-peak detection) | ||
| raw.pick(esg_chans + ["ECG"]) | ||
| # Trim duration and downsample (from 10kHz) to improve example speed | ||
| raw.crop(0, 60).load_data().resample(2000) | ||
|
|
||
| # Find trigger timings to remove the stimulation artefact | ||
| events, event_dict = events_from_annotations(raw) | ||
| trigger_name = "Median - Stimulation" | ||
|
|
||
| fix_stim_artifact( | ||
| raw, | ||
| events=events, | ||
| event_id=event_dict[trigger_name], | ||
| tmin=tstart_esg, | ||
| tmax=tmax_esg, | ||
| mode="linear", | ||
| stim_channel=None, | ||
| ) | ||
|
|
||
| # %% | ||
| # Find ECG events and add to the raw structure as event annotations. | ||
|
|
||
| ecg_events, ch_ecg, average_pulse = find_ecg_events(raw, ch_name="ECG") | ||
| ecg_event_samples = np.asarray( | ||
| [[ecg_event[0] for ecg_event in ecg_events]] | ||
| ) # Samples only | ||
|
|
||
| qrs_event_time = [ | ||
| x / raw.info["sfreq"] for x in ecg_event_samples.reshape(-1) | ||
| ] # Divide by sampling rate to make times | ||
| duration = np.repeat(0.0, len(ecg_event_samples)) | ||
| description = ["qrs"] * len(ecg_event_samples) | ||
|
|
||
| raw.annotations.append( | ||
| qrs_event_time, duration, description, ch_names=[esg_chans] * len(qrs_event_time) | ||
| ) | ||
|
|
||
| # %% | ||
| # Create evoked response around the detected R-peaks | ||
| # before and after cardiac artefact correction. | ||
|
|
||
| events, event_ids = events_from_annotations(raw) | ||
| event_id_dict = {key: value for key, value in event_ids.items() if key == "qrs"} | ||
| epochs = Epochs( | ||
| raw, | ||
| events, | ||
| event_id=event_id_dict, | ||
| tmin=iv_epoch[0], | ||
| tmax=iv_epoch[1], | ||
| baseline=tuple(iv_baseline), | ||
| ) | ||
| evoked_before = epochs.average() | ||
|
|
||
| # Apply function - modifies the data in place. Optionally high-pass filter | ||
| # the data before applying PCA-OBS to remove low frequency drifts | ||
| raw = mne.preprocessing.apply_pca_obs( | ||
| raw, picks=esg_chans, n_jobs=5, qrs_times=raw.times[ecg_event_samples.reshape(-1)] | ||
| ) | ||
|
|
||
| epochs = Epochs( | ||
| raw, | ||
| events, | ||
| event_id=event_id_dict, | ||
| tmin=iv_epoch[0], | ||
| tmax=iv_epoch[1], | ||
| baseline=tuple(iv_baseline), | ||
| ) | ||
| evoked_after = epochs.average() | ||
|
|
||
| # %% | ||
| # Compare evoked responses to assess completeness of artefact removal. | ||
|
|
||
| fig, axes = plt.subplots(1, 1, layout="constrained") | ||
| data_before = evoked_before.get_data(units=dict(eeg="uV")).T | ||
| data_after = evoked_after.get_data(units=dict(eeg="uV")).T | ||
| hs = list() | ||
| hs.append(axes.plot(epochs.times, data_before, color="k")[0]) | ||
| hs.append(axes.plot(epochs.times, data_after, color="green", label="after")[0]) | ||
| axes.set(ylim=[-500, 1000], ylabel="Amplitude (µV)", xlabel="Time (s)") | ||
| axes.set(title="ECG artefact removal using PCA-OBS") | ||
| axes.legend(hs, ["before", "after"]) | ||
| plt.show() | ||
|
|
||
| # %% | ||
| # References | ||
| # ---------- | ||
| # .. footbibliography:: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.