Spherical spline interpolation to use subject data on template head model

Hello everyone,

I am working on inverse problem in EEG. As seen in a paper ( Sun, R., Sohrabpour et al. Deep neural networks constrained by neural mass models improve electrophysiological source imaging of spatiotemporal brain dynamics. https://doi.org/10.1073/pnas.2201128119 ), I want to “map” EEG data from a subject on a template montage to solve the inverse problem on fsaverage head model using spherical spline interpolation.

I am using the sample dataset, more specifically the visual right stimuli data. To check the process I perform the inverse problem resolution on the subject specific head model and morph the solution on fsaverage. I then use spherical spline interpolation to interpolate the EEG data of the subject to standard_1020 electrode montage and solve the inverse problem on fsaverage head model.

When I visualize the results (see image), they are not as expected :cry: . It looks like the activity is moved upward in the brain and I do not understand why.

My questions are :

  • Is there something I am missing in the process or an explanation for those results ?
  • Is it a common technique to interpolate subject specific data to a template head model ?

Any information on this topic would be amazing, thank you very much !
Sarah

Results :

Below is the code I am running (a bit long sorry) :

  • MNE version: 1.5.1
  • operating system : Ubuntu 22.4.3 LTS
import os
import mne
import numpy as np

from mne.datasets import sample
from mne.channels.interpolation import _make_interpolation_matrix


#### load the subject data ####
data_path = sample.data_path()
fname_raw = data_path / "MEG" / "sample" / "sample_audvis_raw.fif"
raw = mne.io.read_raw_fif(fname_raw)
raw.pick_types(meg=False, eeg=True, stim=False, exclude=["EEG 053"]).load_data() # exclude bad channel

event_file = data_path / "MEG" / "sample" / "sample_audvis_raw-eve.fif"
events = mne.read_events(event_file)

epochs = mne.Epochs(raw, events, baseline=(None, 0.0), preload=True)
epochs.set_eeg_reference(projection=True)
reject_criteria = dict(eeg=100e-6)
epochs.drop_bad(reject=reject_criteria)


## we work on the right visual stimuli
vis_right = epochs[f"{4}"].average() 
_, time_p100 = vis_right.get_peak(mode = 'pos',time_as_index=True)


#### load template data ####
standard_montage_kind = "standard_1020"
standard_montage = mne.channels.make_standard_montage(standard_montage_kind)
# remove duplicate electrodes (modified combinatorial nomenclature)
exclude_mdn = ["T3", "T4", "T5", "T6"]
ids_duplicate = [ np.where([ch == e for ch in standard_montage.ch_names])[0][0]
for e in exclude_mdn ]
ch_names = list(np.delete(standard_montage.ch_names, ids_duplicate))


ssi_info = mne.create_info(
ch_names, raw.info["sfreq"], ch_types="eeg", verbose=0
)
ssi_info.set_montage(standard_montage)


### electrode loactions ###
epos_subj = np.array([ch["loc"][:3] for ch in raw.info["chs"]])
epos_templ = np.array([ch["loc"][:3] for ch in ssi_info["chs"]])

### INTERPOLATION ###
interp_mat = _make_interpolation_matrix(epos_s, epos_t)
epoch_data_ssi = np.matmul(interp_mat, epochs._data[...,:,:]) 
epochs_ssi = mne.EpochsArray(epoch_data_ssi, ssi_info, events = epochs.events, tmin = epochs.tmin, event_id = epochs.event_id, baseline=epochs.baseline)
vis_right_ssi = epochs_ssi[f"{4}"].average()


### inverse problem ###
# on subject 
subject = "sample"
subjects_dir = data_path / "subjects"

# build forward model
if not os.path.exists( f"./fsaverage_ori_sample_ico3-{standard_montage}-fwd.fif"):
    trans = f"{data_path}/MEG/sample/sample_audvis_raw-trans.fif"
    conductivity = (0.3, 0.006, 0.3)
    sampling = "ico3"
 
    model = mne.make_bem_model(
        subject=subject, ico=4, conductivity=conductivity, subjects_dir=subjects_dir
    )

    bem = mne.make_bem_solution(model)
    src = mne.setup_source_space(
        subject,
        spacing=sampling,
        surface="white",
        subjects_dir=subjects_dir,
        add_dist=False,
        n_jobs=-1,
        verbose=0,
    )

    fwd = mne.make_forward_solution(
        raw.info,
        trans=trans,
        src=src,
        bem=bem,
        eeg=True,
        mindist=5.0,
        n_jobs=-1,
        verbose=0,
    )

    mne.write_forward_solution(
        f"./fsaverage_ori_sample_ico3-{standard_montage}-fwd.fif",
        fwd,
    )

else:
    fwd = mne.read_forward_solution(
        f"./fsaverage_ori_sample_ico3-{standard_montage}-fwd.fif"
    )

fwd = mne.convert_forward_solution(
    fwd, surf_ori=True, force_fixed=True, use_cps=True, verbose=0
)

# for the template head model
subject = "fsaverage"
subjects_dir = os.path.dirname(mne.datasets.fetch_fsaverage(verbose=0))
if not os.path.exists( f"./fsaverage_{standard_montage_kind}_ico3-fwd.fif"):
    conductivity = (0.3, 0.006, 0.3)
    sampling = "ico3"
    model = mne.make_bem_model(
        subject="fsaverage", ico=4, conductivity=conductivity, subjects_dir=subjects_dir
    )
    bem = mne.make_bem_solution(model)
    src = mne.setup_source_space(
        "fsaverage",
        spacing=sampling,
        surface="white",
        subjects_dir=subjects_dir,
        add_dist=False,
        n_jobs=-1,
        verbose=0,
    )


    fwd_templ = mne.make_forward_solution(
        ssi_info,
        trans="fsaverage",
        src=src,
        bem=bem,
        eeg=True,
        mindist=5.0,
        n_jobs=-1,
        verbose=0,
    )

    mne.write_forward_solution(
        f"./fsaverage_{standard_montage_kind}_ico3-fwd.fif",
        fwd_templ, overwrite=True
    )

else:
    fwd_templ = mne.read_forward_solution(
        f"./fsaverage_{standard_montage_kind}_ico3-fwd.fif"
    )

fwd_templ = mne.convert_forward_solution(
    fwd_templ, surf_ori=True, force_fixed=True, use_cps=True, verbose=0
)

### inverse solution ###
method = "sLORETA"
snr = 3.0
lambda2 = 1.0 / snr**2

cov = mne.compute_covariance(epochs, tmax=0.0)
inv = mne.minimum_norm.make_inverse_operator(
    vis_right.info, fwd, cov, loose=0, depth=0, fixed=True, verbose=True
    )
stc = mne.minimum_norm.apply_inverse(
    vis_right,
    inv,
    lambda2, 
    method=method,
    pick_ori=None,
    return_residual=False,
    verbose=True,
    )

vis_right_ssi.set_eeg_reference(projection=True)
epochs_ssi.set_eeg_reference(projection=True)
cov_ssi = mne.compute_covariance(epochs_ssi, tmax=0.0)
inv_ssi = mne.minimum_norm.make_inverse_operator(
    vis_right_ssi.info, fwd_templ, cov_ssi, loose=0, depth=0, fixed=True, verbose=False
    )

stc_ssi = mne.minimum_norm.apply_inverse(
    vis_right_ssi,
    inv_ssi,
    lambda2,
    method=method,
    pick_ori=None,
    return_residual=False,
    verbose=False,
    )

can you use plot_alignment https://mne.tools/dev/generated/mne.viz.plot_alignment.html to see electrode locations
before and after the mapping to template locations?

how does the topography on sensor space look like before and after?

I used plot_alignment to see the electrode positions but the first are on the sample subject and the one on which I want to interpolate are on the fsaverage subject, so I am not sure how to visualize that.

Here are the topography before and after spherical spline interpolation, at the time of the P100 for visual right stimuli (with default parameters of the function except for the time of visualisation).

Before interpolation :
topomap_before_ssi

After interpolation :
topomap_after_ssi

Thank you !

ok so it appears pretty different so it’s clear that sources are going to move.
one issue you have is that the dipolar pattern is very much on the edge of the helmet
where interpolation becomes a problem.

what this tells me is that the error from mapping to the template are going to dominate the
errors due to inverse modeling and I don’t see an easy way around it.

Alex

Ok, thank you very much for your answer and your time !

Sarah