I spent a bit more time getting a more reasonable skull surface with this code:
import os.path as op
import numpy as np
import mne
import nibabel as nib
from tqdm import tqdm
misc_path = mne.datasets.misc.data_path()
sample_path = mne.datasets.sample.data_path()
subjects_dir = op.join(sample_path, 'subjects')
# align CT
T1 = nib.load(op.join(misc_path, 'seeg', 'sample_seeg', 'mri', 'T1.mgz'))
CT_orig = nib.load(op.join(misc_path, 'seeg', 'sample_seeg_CT.mgz'))
reg_affine = np.array([
[0.99270756, -0.03243313, 0.11610254, -133.094156],
[0.04374389, 0.99439665, -0.09623816, -97.58320673],
[-0.11233068, 0.10061512, 0.98856381, -84.45551601],
[0., 0., 0., 1.]])
CT_aligned = mne.transforms.apply_volume_registration(CT_orig, T1, reg_affine)
CT_data = np.array(CT_aligned.dataobj).astype(np.int64)
CT_data[CT_data < 1000] = 0
CT_data[CT_data > 0] = 1
coords, faces = mne.surface._marching_cubes(CT_data, level=[1], smooth=0.95)[0]
coords = mne.transforms.apply_trans(T1.header.get_vox2ras_tkr(), coords)
brain = mne.viz.Brain(
'sample_seeg', subjects_dir=op.join(misc_path, 'seeg'), alpha=0.8)
brain._renderer.mesh(*coords.T, faces, 'red', opacity=0.5)
# load icosahedron and fit it to the surface
# could change to 5 for smoother but slower
ico = mne.surface._get_ico_surface(grade=2)
# find large radius from coords
coords_sph = mne.transforms._cart_to_sph(coords)
r = np.quantile(coords_sph[0], 0.95)
ico_rr = ico['rr'] * r + coords.mean(axis=0)
ico_sph = mne.transforms._cart_to_sph(ico_rr)
brain._renderer.mesh(*ico_rr.T, ico['tris'], 'blue', opacity=0.5)
ico_rr_shrunk = list()
delta_r = 0.5
# divide by 2 to roughly account for doubling of inner skull surface
n_closest = np.round(coords.size / 2 / ico_rr.size).astype(int)
for sph_pt in tqdm(ico_sph):
pt = mne.transforms._sph_to_cart(np.array([sph_pt]))[0]
dists = np.linalg.norm(pt - coords, axis=1)
closest = np.argsort(dists)[:n_closest] # could change this 100 too
error = None # fencepost
next_error = np.quantile(dists[closest], 0.05)
while error is None or next_error < error:
error = next_error
sph_pt[0] -= delta_r
pt = mne.transforms._sph_to_cart(np.array([sph_pt]))[0]
dists = np.linalg.norm(pt - coords, axis=1)
next_error = np.quantile(dists[closest], 0.05)
ico_rr_shrunk.append(pt)
ico_rr_shrunk = np.array(ico_rr_shrunk)
brain = mne.viz.Brain(
'sample_seeg', subjects_dir=op.join(misc_path, 'seeg'), alpha=0.8)
brain._renderer.mesh(*coords.T, faces, 'red', opacity=0.5)
brain._renderer.mesh(*ico_rr_shrunk.T, ico['tris'], 'green', opacity=0.5)
# mne.write_surface(op.join(misc_path, 'seeg', 'sample_seeg',
# 'bem', 'outer_skull.surf'),
# ico_rr_shrunk, ico['tris'])
which might be helpful to you.
I think it’s maybe a bit hacky to go in the MNE codebase unless the full watershed algorithm were reimplemented which involves a smoothness constraint and a template deformation as described here http://surfer.nmr.mgh.harvard.edu/ftp/articles/segonne05.pdf but this is an okay start and could perhaps be useable for your case. If it were me and I had this data
I would probably go into freeview and quickly edit the CT to remove the very large artifacts and then rerun with a finer icosahedron (it took 2.5 minutes on a macbook laptop to run the code above).
Anyway, hope that helps.