-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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: add interpolate_to method #13044
Open
antoinecollas
wants to merge
27
commits into
mne-tools:main
Choose a base branch
from
antoinecollas:interpolate_to
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
824457d
working implem
antoinecollas 5e5f16b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 4236efb
pre-commit
antoinecollas 36981ca
Merge branch 'interpolate_to' of https://github.com/antoinecollas/mne…
antoinecollas e7cefd8
Merge branch 'main' into interpolate_to
antoinecollas 3a0383a
minor changes
antoinecollas 3ea4cad
fix nested imports
antoinecollas 29f7ce4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 8a05713
fix comment
antoinecollas de0ddc4
add test
antoinecollas 254fdac
fix docstring
antoinecollas 4bf3f1d
fix docstring
antoinecollas 2f27599
rm plt.tight_layout
antoinecollas 872ba1d
taller figure
antoinecollas 045496c
[autofix.ci] apply automated fixes
autofix-ci[bot] 568656d
Merge branch 'main' into interpolate_to
antoinecollas 6fb3ae5
fix figure layout
antoinecollas dd093a8
improve test
antoinecollas 9551016
simplify getting original data
antoinecollas fa326ad
simplify setting interpolated data
antoinecollas 1a5ca89
merge two lines
antoinecollas 4333f08
add spline method
antoinecollas 4e25616
add splive vs mne to doc
antoinecollas a249321
keep all modalities in test
antoinecollas fb8804f
use self.info instead of old_info
antoinecollas 7ff7f51
fix info when MNE interpolation
antoinecollas 1334fb3
fix origin in spline method
antoinecollas 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 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 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,76 @@ | ||
""" | ||
.. _ex-interpolate-to-any-montage: | ||
|
||
====================================================== | ||
Interpolate EEG data to any montage | ||
====================================================== | ||
|
||
This example demonstrates how to interpolate EEG channels to match a given montage. | ||
This can be useful for standardizing | ||
EEG channel layouts across different datasets (see :footcite:`MellotEtAl2024`). | ||
|
||
- Using the field interpolation for EEG data. | ||
- Using the target montage "biosemi16". | ||
|
||
In this example, the data from the original EEG channels will be | ||
interpolated onto the positions defined by the "biosemi16" montage. | ||
""" | ||
|
||
# Authors: Antoine Collas <[email protected]> | ||
# License: BSD-3-Clause | ||
# Copyright the MNE-Python contributors. | ||
|
||
import matplotlib.pyplot as plt | ||
|
||
import mne | ||
from mne.channels import make_standard_montage | ||
from mne.datasets import sample | ||
|
||
print(__doc__) | ||
|
||
# %% | ||
# Load EEG data | ||
data_path = sample.data_path() | ||
eeg_file_path = data_path / "MEG" / "sample" / "sample_audvis-ave.fif" | ||
evoked = mne.read_evokeds(eeg_file_path, condition="Left Auditory", baseline=(None, 0)) | ||
|
||
# Select only EEG channels | ||
evoked.pick("eeg") | ||
|
||
# Plot the original EEG layout | ||
evoked.plot(exclude=[], picks="eeg") | ||
|
||
# %% | ||
# Define the target montage | ||
standard_montage = make_standard_montage("biosemi16") | ||
|
||
# %% | ||
# Use interpolate_to to project EEG data to the standard montage | ||
evoked_interpolated_spline = evoked.copy().interpolate_to( | ||
standard_montage, method="spline" | ||
) | ||
|
||
# Plot the interpolated EEG layout | ||
evoked_interpolated_spline.plot(exclude=[], picks="eeg") | ||
|
||
# %% | ||
# Use interpolate_to to project EEG data to the standard montage | ||
evoked_interpolated_mne = evoked.copy().interpolate_to(standard_montage, method="MNE") | ||
|
||
# Plot the interpolated EEG layout | ||
evoked_interpolated_mne.plot(exclude=[], picks="eeg") | ||
|
||
# %% | ||
# Comparing before and after interpolation | ||
fig, axs = plt.subplots(3, 1, figsize=(8, 6), constrained_layout=True) | ||
evoked.plot(exclude=[], picks="eeg", axes=axs[0], show=False) | ||
axs[0].set_title("Original EEG Layout") | ||
evoked_interpolated_spline.plot(exclude=[], picks="eeg", axes=axs[1], show=False) | ||
axs[1].set_title("Interpolated to Standard 1020 Montage using spline interpolation") | ||
evoked_interpolated_mne.plot(exclude=[], picks="eeg", axes=axs[2], show=False) | ||
axs[2].set_title("Interpolated to Standard 1020 Montage using MNE interpolation") | ||
|
||
# %% | ||
# References | ||
# ---------- | ||
# .. footbibliography:: |
This file contains 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 | ||||
---|---|---|---|---|---|---|
|
@@ -950,6 +950,111 @@ def interpolate_bads( | |||||
|
||||||
return self | ||||||
|
||||||
def interpolate_to(self, montage, origin="auto", method="spline", reg=0.0): | ||||||
"""Interpolate EEG data onto a new montage. | ||||||
|
||||||
Parameters | ||||||
---------- | ||||||
montage : DigMontage | ||||||
The target montage containing channel positions to interpolate onto. | ||||||
origin : array-like, shape (3,) | str | ||||||
Origin of the sphere in the head coordinate frame and in meters. | ||||||
Can be ``'auto'`` (default), which means a head-digitization-based | ||||||
origin fit. | ||||||
method : str | ||||||
Method to use for EEG channels. | ||||||
Supported methods are 'spline' (default) and 'MNE'. | ||||||
|
||||||
.. warning:: | ||||||
Be careful, only EEG channels are interpolated. Other channel types are | ||||||
not interpolated. | ||||||
|
||||||
reg : float | ||||||
The regularization parameter for the interpolation method (if applicable). | ||||||
antoinecollas marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
Returns | ||||||
------- | ||||||
inst : instance of Raw, Epochs, or Evoked | ||||||
The instance with updated channel locations and data. | ||||||
|
||||||
Notes | ||||||
----- | ||||||
This method is useful for standardizing EEG layouts across datasets. | ||||||
|
||||||
.. versionadded:: 1.10.0 | ||||||
""" | ||||||
from ..forward._field_interpolation import _map_meg_or_eeg_channels | ||||||
from .interpolation import _make_interpolation_matrix | ||||||
|
||||||
# Get target positions from the montage | ||||||
ch_pos = montage.get_positions()["ch_pos"] | ||||||
target_ch_names = list(ch_pos.keys()) | ||||||
if len(target_ch_names) == 0: | ||||||
raise ValueError( | ||||||
"The provided montage does not contain any channel positions." | ||||||
) | ||||||
antoinecollas marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
# Check the method is valid | ||||||
_check_option("method", method, ["spline", "MNE"]) | ||||||
|
||||||
# Ensure data is loaded | ||||||
_check_preload(self, "interpolation") | ||||||
|
||||||
# Extract positions and data for EEG channels | ||||||
picks_from = pick_types(self.info, meg=False, eeg=True, exclude=[]) | ||||||
if len(picks_from) == 0: | ||||||
raise ValueError("No EEG channels available for interpolation.") | ||||||
|
||||||
# Create a new info structure | ||||||
sfreq = self.info["sfreq"] | ||||||
ch_types = ["eeg"] * len(target_ch_names) | ||||||
new_info = create_info(ch_names=target_ch_names, sfreq=sfreq, ch_types=ch_types) | ||||||
new_info.set_montage(montage) | ||||||
|
||||||
# Compute mapping from current montage to target montage | ||||||
if method == "spline": | ||||||
# pos_from = np.array( | ||||||
# [self.info["chs"][idx]["loc"][:3] for idx in picks_from] | ||||||
# ) | ||||||
|
||||||
origin = _check_origin(origin, self.info) | ||||||
pos_from = self.info._get_channel_positions(picks_from) | ||||||
pos_from = pos_from - origin | ||||||
pos_to = np.stack(list(ch_pos.values()), axis=0) | ||||||
|
||||||
def _check_pos_sphere(pos): | ||||||
distance = np.linalg.norm(pos, axis=-1) | ||||||
distance = np.mean(distance / np.mean(distance)) | ||||||
if np.abs(1.0 - distance) > 0.1: | ||||||
warn( | ||||||
"Your spherical fit is poor, interpolation results are " | ||||||
"likely to be inaccurate." | ||||||
) | ||||||
|
||||||
_check_pos_sphere(pos_from) | ||||||
_check_pos_sphere(pos_to) | ||||||
|
||||||
mapping = _make_interpolation_matrix(pos_from, pos_to, alpha=reg) | ||||||
|
||||||
elif method == "MNE": | ||||||
info_eeg = pick_info(self.info, picks_from) | ||||||
mapping = _map_meg_or_eeg_channels( | ||||||
info_eeg, new_info, mode="accurate", origin="auto" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't this be:
Suggested change
|
||||||
) | ||||||
|
||||||
# Apply the interpolation mapping | ||||||
data_orig = self.get_data(picks=picks_from) | ||||||
data_interp = mapping.dot(data_orig) | ||||||
|
||||||
# Update bad channels | ||||||
new_info["bads"] = [ch for ch in self.info["bads"] if ch in target_ch_names] | ||||||
|
||||||
# Update the instance's info and data | ||||||
self.info = new_info | ||||||
self._data = data_interp | ||||||
|
||||||
return self | ||||||
|
||||||
|
||||||
@verbose | ||||||
def rename_channels(info, mapping, allow_duplicates=False, *, verbose=None): | ||||||
|
This file contains 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.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this warning applies to the entire function, not the
method
parameter specifically. Perhaps move it to the main text, so above theParameters
line.