Problems about volume source estimate morphing.

Dear MNE experts,

I have some spatial-temporal searchlight results and I wish to morph these volume source estimates to the volumetric fsaverage brain to get the group-level results.
However, as shown below, our source estimate is quite different from the original data.

So Iā€™m writing to ask how to fix this problem. The code we used in our analysis is shown as follows.

Environmentļ¼š

  • MNE version: 1.0.3
  • operating system: Centos (Linux version 3.10.0-1160.el7.x86_64)
  • data: CTF MEG data.

1.Firstly, we make source space for fsaverage data

import mne
from os.path import join as pjoin
subj = 'fsaverage'
subj_head = pjoin(surfDir,subj)
bem_path = pjoin(subj_head, "bem")
subject = subj 
BEM_spacing = 5 
src_spacing = 'oct6'
BEM_method = "watershed"
BEM_conductivity = [0.3]

mne.bem.make_watershed_bem(
    subject=subject,
    subjects_dir=surfDir,
    overwrite=True,
    volume="T1",
    atlas=True,
    gcaatlas=False,
    preflood=1.75,
    show=True, # use True if plot set OK
    copy=True,
    verbose=True,
)

bem_surfaces = mne.make_bem_model(
    subject=subject,
    subjects_dir=surfDir,
    ico=BEM_spacing,
    conductivity=BEM_conductivity,
    verbose=True,
)

fname_bem = pjoin(bem_path,
                    "{}_{}_ico{}_bem.fif".format(subj, BEM_method, BEM_spacing))

mne.write_bem_surfaces(fname_bem, bem_surfaces,
                        overwrite=True, verbose=True)

bem_sol = mne.make_bem_solution(surfs=bem_surfaces, verbose=True)

fname_bem_sol = pjoin(bem_path,
                        "{}_{}_ico{}_bem-sol.fif".format(subj, BEM_method, BEM_spacing))

mne.write_bem_solution(fname=fname_bem_sol, bem=bem_sol,
                        overwrite=True, verbose=True)
surface_name = "pial"

surface = pjoin(surfDir, subject, 'bem', 'inner_skull.surf')
# make and save source space for fsaverage 
surf_src_fname = pjoin(bem_path,
    "{}_{}_bem_{}_vol-src.fif".format(subj, BEM_method, surface_name))

src = mne.setup_volume_source_space(
    subject, subjects_dir=surfDir, surface=surface,
    add_interpolator=True)

mne.write_source_spaces(surf_src_fname, src, overwrite=True, verbose=True)

It returns a fsaverage source space.
<SourceSpaces: [<volume, shape=(31, 33, 25), n_used=5756>] MRI (surface RAS) coords, subject ā€˜fsaverageā€™, ~70.3 MB>

  1. Morph searchlight results to common source space (fsaverage)
# This code refers to https://mne.tools/stable/auto_examples/inverse/morph_volume_stc.html#sphx-glr-auto-examples-inverse-morph-volume-stc-py
import mne
import joblib
from os.path import join as pj
dataPath = '/meg2/'
surfDir = '/meg2/surf/'
subjList = ['subj001','subj002','subj003'] 
fullStc =[]
subjCount=0
for subj in subjList:
    # load searchlight results
    sourceName = pj(dataPath, subj,'sourced','rsa_'+subj+'.stc') 
    sourcedData = joblib.load(sourceName)
    # morgh data to a common volume space
    vol_src = pj(surfDir,subj, 'bem', subj+'_watershed_bem_pial_vol-src.fif')
    inverse_operator = mne.minimum_norm.read_inverse_operator(pj(surfDir ,subj, subj+'LCMV'+'-inv.fif'))
    # make bem model for fsaverage
    fname_src_fsaverage = pj(surfDir,'fsaverage','bem', 'fsaverage'+'_watershed_bem_pial_vol-src.fif')
    src_fs = mne.read_source_spaces(fname_src_fsaverage)
    morph = mne.compute_source_morph(
        inverse_operator['src'], subject_from=subj, subjects_dir=surfDir,
        src_to=src_fs, verbose=True)
    
    # save morphed data as stc file
    fname_stc = pj(dataPath,'bfStat','rsa_'+str(subjCount)+'.stc') # .fif  and .src
    with open(fname_stc, 'wb') as f:
        joblib.dump(stc_fsaverage, f)
    f.close() 
    stc_fsaverage = morph.apply(sourcedData)   
    subjCount = subjCount+1
    fullStc.append(stc_fsaverage)

Another question is, how to read labels of this morphed source estimate? I tried ā€™ read_labels_from_annotā€™, but it seems that labels in ā€˜fsaverage/labelsā€™ are not suited for morphed volume estimates (5756 voxels).

Best wishes!
Jinhua

hi @Clemens

can you replicate this using the mne sample data so we can investigate?

Alex

Thank you for your suggestion! I replicate this data analysis on example data (referred to
Morph volumetric source estimate ā€” MNE 1.6.0 documentation) using my reconstructed fsaverage source space (the first step showed above). As shown as follows, a similar problem occurred, the morphed source data was also different from the examplar result.

So do I do something wrong when I make the source space for fsaverage data? By the way, fsaverage folder was copied from freesurfer path ā€˜freesurfer/subject/fsaverageā€™

Best!
Jinhua

can you share the code that replicates the problem on sample data so we can also test?

Alex

Hi, Alexandre
We have uploaded the data to the Google Drive cloud MNE_morph.zip - Google Drive

Code we used for fsaverage source space reconstruction and source estimate morphing was provided as follows:

  1. make source space for fsaverage data
import mne
from os.path import join as pjoin
subj = 'fsaverage'
subj_head = pjoin(surfDir,subj)
bem_path = pjoin(subj_head, "bem")
subject = subj 
BEM_spacing = 5 
src_spacing = 'oct6'
BEM_method = "watershed"
BEM_conductivity = [0.3]

mne.bem.make_watershed_bem(
    subject=subject,
    subjects_dir=surfDir,
    overwrite=True,
    volume="T1",
    atlas=True,
    gcaatlas=False,
    preflood=1.75,
    show=True, # use True if plot set OK
    copy=True,
    verbose=True,
)

bem_surfaces = mne.make_bem_model(
    subject=subject,
    subjects_dir=surfDir,
    ico=BEM_spacing,
    conductivity=BEM_conductivity,
    verbose=True,
)

fname_bem = pjoin(bem_path,
                    "{}_{}_ico{}_bem.fif".format(subj, BEM_method, BEM_spacing))

mne.write_bem_surfaces(fname_bem, bem_surfaces,
                        overwrite=True, verbose=True)

bem_sol = mne.make_bem_solution(surfs=bem_surfaces, verbose=True)

fname_bem_sol = pjoin(bem_path,
                        "{}_{}_ico{}_bem-sol.fif".format(subj, BEM_method, BEM_spacing))

mne.write_bem_solution(fname=fname_bem_sol, bem=bem_sol,
                        overwrite=True, verbose=True)
surface_name = "pial"

surface = pjoin(surfDir, subject, 'bem', 'inner_skull.surf')
# make and save source space for fsaverage 
surf_src_fname = pjoin(bem_path,
    "{}_{}_bem_{}_vol-src.fif".format(subj, BEM_method, surface_name))

src = mne.setup_volume_source_space(
    subject, subjects_dir=surfDir, surface=surface,
    add_interpolator=True)

mne.write_source_spaces(surf_src_fname, src, overwrite=True, verbose=True)
  1. Morphing sample data to common source space
# This code refers to https://mne.tools/stable/auto_examples/inverse/morph_volume_stc.html#sphx-glr-auto-examples-inverse-morph-volume-stc-py
import os
import nibabel as nib
import mne
from mne.datasets import sample, fetch_fsaverage
from mne.minimum_norm import apply_inverse, read_inverse_operator
from nilearn.plotting import plot_glass_brain
sample_dir = os.path.join('Path/sample')
subjects_dir = 'Path'
fname_evoked = os.path.join(sample_dir, 'sample_audvis-ave.fif')
fname_inv = os.path.join(sample_dir, 'sample_audvis-meg-vol-7-meg-inv.fif')
fname_t1_fsaverage = os.path.join(subjects_dir, 'fsaverage', 'mri',
                                  'brain.mgz')
fname_src_fsaverage = subjects_dir + '/fsaverage/bem/fsaverage_watershed_bem_pial_vol-src.fif'

evoked = mne.read_evokeds(fname_evoked, condition=0, baseline=(None, 0))
inverse_operator = read_inverse_operator(fname_inv)

# Apply inverse operator
stc = apply_inverse(evoked, inverse_operator, 1.0 / 3.0 ** 2, "dSPM")

# To save time
stc.crop(0.09, 0.09)

src_fs = mne.read_source_spaces(fname_src_fsaverage)
morph = mne.compute_source_morph(
    inverse_operator['src'], subject_from='sample', subjects_dir=subjects_dir,
    #niter_affine=[10, 10, 5], niter_sdr=[10, 10, 5],  # just for speed
    src_to=src_fs, verbose=True)

stc_fsaverage = morph.apply(stc)

# Create mri-resolution volume of results
img_fsaverage = morph.apply(stc, mri_resolution=2, output='nifti1')

# Load fsaverage anatomical image
t1_fsaverage = nib.load(fname_t1_fsaverage)

# Plot glass brain (change to plot_anat to display an overlaid anatomical T1)
display = plot_glass_brain(t1_fsaverage,
                           title='subject results to fsaverage',
                           draw_cross=False,
                           annotate=True)

# Add functional data as overlay
display.add_overlay(img_fsaverage, alpha=0.75)

Best!
Jinhua

thanks !

I should not need any data I donā€™t already have to run your code. What code did you use to
obtain : fsaverage/bem/fsaverage_watershed_bem_pial_vol-src.fif ?

Alex

Hi, Alex,

I used the first part code ( 1. make source space for fsaverage data) provided above to obtain this fsaverage source space.

Jinhua

can you provide a self contained script that runs from top to bottom on a machine that is not yours?

thanks
Alex

Sorry for the late reply, it takes some time to download sample data. Our data processing using sample data was shown as follows.

# This code refers to https://mne.tools/stable/auto_examples/inverse/morph_volume_stc.html#sphx-glr-auto-examples-inverse-morph-volume-stc-py
import os
import nibabel as nib
import mne
from mne.datasets import sample, fetch_fsaverage
from mne.minimum_norm import apply_inverse, read_inverse_operator
from nilearn.plotting import plot_glass_brain
from os.path import join as pjoin
# load sample data
sample_dir_raw = sample.data_path()
sample_dir = os.path.join(sample_dir_raw, 'MEG', 'sample')
subjects_dir = os.path.join(sample_dir_raw, 'subjects')
fname_evoked = os.path.join(sample_dir, 'sample_audvis-ave.fif')
fname_inv = os.path.join(sample_dir, 'sample_audvis-meg-vol-7-meg-inv.fif')
fname_t1_fsaverage = os.path.join(subjects_dir, 'fsaverage', 'mri',
                                  'brain.mgz')

subj = 'fsaverage'
subj_head = pjoin(subjects_dir,subj)
bem_path = pjoin(subj_head, "bem")
subject = subj 
BEM_spacing = 5 
src_spacing = 'oct6'
BEM_method = "watershed"
BEM_conductivity = [0.3]

mne.bem.make_watershed_bem(
    subject=subject,
    subjects_dir=subjects_dir,
    overwrite=True,
    volume="T1",
    atlas=True,
    gcaatlas=False,
    preflood=1.75,
    show=True, # use True if plot set OK
    copy=True,
    verbose=True,
)

bem_surfaces = mne.make_bem_model(
    subject=subject,
    subjects_dir=subjects_dir,
    ico=BEM_spacing,
    conductivity=BEM_conductivity,
    verbose=True,
)

fname_bem = pjoin(bem_path,
                    "{}_{}_ico{}_bem.fif".format(subj, BEM_method, BEM_spacing))

mne.write_bem_surfaces(fname_bem, bem_surfaces,
                        overwrite=True, verbose=True)

bem_sol = mne.make_bem_solution(surfs=bem_surfaces, verbose=True)

fname_bem_sol = pjoin(bem_path,
                        "{}_{}_ico{}_bem-sol.fif".format(subj, BEM_method, BEM_spacing))

mne.write_bem_solution(fname=fname_bem_sol, bem=bem_sol,
                        overwrite=True, verbose=True)
surface_name = "pial"

surface = pjoin(subjects_dir, subject, 'bem', 'inner_skull.surf')
# make and save source space for fsaverage 
surf_src_fname = pjoin(bem_path,
    "{}_{}_bem_{}_vol-src.fif".format(subj, BEM_method, surface_name))

src = mne.setup_volume_source_space(
    subject, subjects_dir=subjects_dir, surface=surface,
    add_interpolator=True)
# save source estimate data
mne.write_source_spaces(surf_src_fname, src, overwrite=True, verbose=True)

fname_src_fsaverage = subjects_dir + '/fsaverage/bem/fsaverage_watershed_bem_pial_vol-src.fif'
src_fs = mne.read_source_spaces(fname_src_fsaverage)

evoked = mne.read_evokeds(fname_evoked, condition=0, baseline=(None, 0))
inverse_operator = read_inverse_operator(fname_inv)

# Apply inverse operator
stc = apply_inverse(evoked, inverse_operator, 1.0 / 3.0 ** 2, "dSPM")

# To save time
stc.crop(0.09, 0.09)

morph = mne.compute_source_morph(
    inverse_operator['src'], subject_from='sample', subjects_dir=subjects_dir,
    #niter_affine=[10, 10, 5], niter_sdr=[10, 10, 5],  # just for speed
    src_to=src_fs, verbose=True)

stc_fsaverage = morph.apply(stc)

# Create mri-resolution volume of results
img_fsaverage = morph.apply(stc, mri_resolution=2, output='nifti1')

# Load fsaverage anatomical image
t1_fsaverage = nib.load(fname_t1_fsaverage)

# Plot glass brain (change to plot_anat to display an overlaid anatomical T1)
display = plot_glass_brain(t1_fsaverage,
                           title='subject results to fsaverage',
                           draw_cross=False,
                           annotate=True)

# Add functional data as overlay
display.add_overlay(img_fsaverage, alpha=0.75)

Best!
Jinhua

it seems you mess up the files between fsaverage and sample. Each bem surface must be different for each subject. If you define a source space you must recompute the forward accordingly etc.

Here is a simplified notebook that shows that it should work for you.

Hope this helps

Alex

2 Likes

Hi Alex,

Thank you so much for your help!
I think the mistake I made was that I use the wrong parameters when I create BEM surfaces using ā€˜make_watershed_bemā€™. (I used ā€˜atlas=None, preflood = 1.75ā€™ which I copied othersā€™ code). When I use the default parameters, the results look correct.
Iā€™d like to confirm that is it okay to use default parameters ā€œatlas=False, preflood=Noneā€ to create BEM surfaces?

  mne.bem.make_watershed_bem(
      subject=subject,
      subjects_dir=surfDir,
      overwrite=True,
      volume="T1",
      atlas=False,
      gcaatlas=False,
      preflood=None,
      show=True, 
      copy=True,
      verbose=True,
  )

Best!
Jinhua