Quantitative MRI Uncovers Subtle Cortical Damage in Myelin Oligodendrocyte Glycoprotein Antibody-Associated Disease.
The 1 match
- [1] § Methods › MRI Acquisition and Processing ↔ myelin_map_funcs.py, lines 648–788 · score 0.54 · myelin_map, pipeline, calibrated, eye, brain, masks
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Python · 788 lines · 30 KB · no license · 1 match
- from __future__ import division
- import numpy as np
- import nibabel as nb
- import scipy.stats as stats
- from scipy.ndimage.morphology import binary_erosion as be
- from statsmodels import robust
- from nibabel import processing as nbproc
- import matplotlib.pyplot as plt
- import seaborn as sns
- from nipype.interfaces import ants, dcm2nii
- import fnmatch
- import glob
- import os
- def dcm_convert(scan_dict, output_dir):
- subj = os.path.split(output_dir)[-1]
- dir_files = os.listdir(output_dir)
- if fnmatch.filter(dir_files, 't1.nii.gz') and fnmatch.filter(dir_files, 't2.nii.gz'):
- print('DCM to nifti conversion already run for {}. Not re-running.'.format(subj))
- #Get output from previous run, return in dictionary.
- dcm_out = glob.glob(os.path.join(output_dir, 't?.nii.gz'))
- t1_subj_nii = dcm_out[0]
- t2_subj_nii = dcm_out[1]
- else:
- scan_list = list(scan_dict.keys())
- anat_convert_output = []
- for scan in scan_list:
- anat_convert = dcm2nii.Dcm2niix()
- anat_convert.inputs.source_names = scan_dict[scan]
- anat_convert.inputs.out_filename = scan
- anat_convert.inputs.output_dir = output_dir
- anat_convert.inputs.compress = 'y'
- results = anat_convert.run()
- anat_convert_output.append(results.outputs.get()['converted_files'])
- t1_subj_nii, t2_subj_nii = anat_convert_output
- return(t1_subj_nii, t2_subj_nii)
- ###PLOTTING###
- #Plot histograms of data
- def plot_mask_dist(t1_fn, t2_fn, eye_mask_fn, temp_bone_mask_fn, stat = None):
- t1 = nb.load(t1_fn).get_data().ravel()
- t2 = nb.load(t2_fn).get_data().ravel()
- eye_mask = nb.load(eye_mask_fn).get_data().ravel()
- temp_mask = nb.load(temp_bone_mask_fn).get_data().ravel()
- if stat == 'mean':
- t1_eye_stat = np.mean(t1[eye_mask])
- t1_temp_stat = np.mean(t1[temp_mask])
- t2_eye_stat = np.mean(t2[eye_mask])
- t2_temp_stat = np.mean(t2[temp_mask])
- elif stat == 'median':
- t1_eye_stat = np.median(t1[eye_mask])
- t1_temp_stat = np.median(t1[temp_mask])
- t2_eye_stat = np.median(t2[eye_mask])
- t2_temp_stat = np.median(t2[temp_mask])
- elif stat == 'mode' or stat == None:
- t1_eye_stat = stats.mode(t1[eye_mask][t1[eye_mask] > 0])[0][0]
- t1_temp_stat = stats.mode(t1[temp_mask][t1[temp_mask] > 0])[0][0]
- t2_eye_stat = stats.mode(t2[eye_mask][t2[eye_mask] > 0])[0][0]
- t2_temp_stat = stats.mode(t2[temp_mask][t2[temp_mask] > 0])[0][0]
- fig = plt.figure(figsize = [15, 10]);
- plt.subplot(2,1,1);
- plt.title('Eye ' + stat)
- sns.distplot(t1[eye_mask], label = 'T1');
- ymin, ymax = fig.gca().axes.get_ybound()
- plt.vlines(x = t1_eye_stat, ymin = ymin, ymax = ymax)
- sns.distplot(t2[eye_mask], label = 'T2');
- plt.vlines(x = t2_eye_stat, ymin = ymin, ymax = ymax)
- plt.legend()
- #plt.show()
- fig = plt.figure(figsize = [15, 10]);
- plt.subplot(2,1,2);
- plt.title('Temporal Bone ' + stat)
- sns.distplot(t1[temp_mask], label = 'T1');
- ymin, ymax = fig.gca().axes.get_ybound()
- plt.vlines(x = t1_temp_stat, ymin = ymin, ymax = ymax)
- sns.distplot(t2[temp_mask], label = 'T2');
- plt.vlines(x = t2_temp_stat, ymin = ymin, ymax = ymax)
- plt.legend()
- #plt.show()
- plt.savefig('modes.png')
- print('T1 eye: {}\nT2 eye: {}\nT1 temp: {}\nT2 temp: {}'.format(t1_eye_stat, t2_eye_stat, t1_temp_stat, t2_temp_stat))
- #Plot image overlays to assess warp quality
- def plot_ants_warp(fixed, moving, nslices, output_name = None):
- """
- Plots nslices axial images of the fixed and moving images from the ants transform
- inputs:
- anat - Fixed image (image the moving was warped to)
- moving - Moving image (Transformed image)
- nslices - Number of slices to plot
- output_name - filename to save png image to (optional)
- """
- fixed_data = nb.load(fixed).get_data()
- moving_data = nb.load(moving).get_data()
- view_slices = np.linspace(100, fixed_data.shape[1] - 1, num = nslices).astype(int)
- fig = plt.figure(figsize = [50, 25])
- for n, view_slice in enumerate(view_slices):
- plt.subplot(1, nslices, n + 1)
- plt.imshow(fixed_data[:, :, view_slice], cmap = 'Greys_r')
- plt.imshow(moving_data[:, :, view_slice], cmap = 'Reds', alpha = 0.25)
- plt.axis('off')
- plt.text(1,1, 'z = ' + str(view_slice), color = [1,0,0], bbox=dict(facecolor=[0,0,0]), fontsize = 20)
- plt.tight_layout()
- plt.show()
- if output_name != None:
- fig.savefig(fname = output_name + '.png')
- ###SPACIAL TRANSORMS###
- #Warp MNI to subj
- def ants_reg(fixed = None, moving = None, prefix = None, fixed_mask = None, moving_mask = None, output_dir = None):
- '''
- Uses ANTs to warp the moving image to the space of the fixed.
- Both fixed and moving images can have masks.
- If warping subjects with lesion damage, it is recommended to warp from MNI to subj space
- (fixed = subj, moving = mni template, fixed_mask = lesion mask) then
- use the inverse transform to warp from subj to MNI.
- '''
- subj = os.path.split(output_dir)[-1]
- if 'output_warped_image.nii.gz' in os.listdir(output_dir):
- print("Ants registration already run. Not re-running for subj {}. You're welcome.".format(subj))
- #Get output from previous run, return in dictionary.
- reg_trans = glob.glob(output_dir + '/reg_trans_*')
- reg_trans.append(glob.glob(output_dir + '/output_warped_image*')[0])
- reg_trans.sort()
- reg_labels = ['warped_image', 'trans_mat', 'composite_transform', 'inverse_composite_transform']
- reg_out_files = {k: reg_trans[n] for n, k in enumerate(reg_labels)}
- print(subj, reg_out_files)
- else:
- reg = ants.Registration()
- reg.inputs.fixed_image = fixed
- reg.inputs.moving_image = moving
- if fixed_mask != None:
- reg.inputs.fixed_mask = fixed_mask
- if moving_mask != None:
- reg.inputs.moving_mask = fixed_mask
- if prefix != None:
- reg.inputs.output_transform_prefix = prefix
- else:
- reg.inputs.output_transform_prefix = os.path.join(output_dir, 'reg_trans_')
- reg.inputs.transforms = ['Rigid', 'Affine', 'SyN']
- reg.inputs.transform_parameters = [(0.1,),(0.1,),(0.1, 3.0, 0.0)] #Size of movement for registration (Optimal values are 0.1-0.25.)
- reg.inputs.number_of_iterations =[[1000, 500, 250, 100],[1000, 500, 250, 100],[100, 70, 50, 20]]
- reg.inputs.dimension = 3
- # '''
- # Align the moving_image and fixed_image before registration using the geometric
- # center of the images (=0), the image intensities (=1),or the origin of the images (=2)
- # '''
- reg.inputs.initial_moving_transform_com = 0
- reg.inputs.write_composite_transform = True
- reg.inputs.collapse_output_transforms = False
- reg.inputs.initialize_transforms_per_stage = False
- reg.inputs.metric = ['MI', 'MI', 'CC']
- reg.inputs.radius_or_number_of_bins = [32, 32, 4]
- reg.inputs.sampling_strategy = ['Regular','Regular','None']
- reg.inputs.sampling_percentage = [0.25, 0.25, 1]
- reg.inputs.convergence_threshold = [1e-06]
- reg.inputs.convergence_window_size = [10]
- reg.inputs.smoothing_sigmas = [[3, 2, 1, 0]] * 3
- reg.inputs.sigma_units = ['vox'] * 3
- reg.inputs.shrink_factors = [[8, 4, 2, 1]] * 3
- reg.inputs.use_estimate_learning_rate_once = [True, True, True]
- reg.inputs.use_histogram_matching = True
- if output_dir != None:
- reg.inputs.output_warped_image = os.path.join(output_dir, 'output_warped_image.nii.gz')
- else:
- reg.inputs.output_warped_image = './output_warped_image.nii.gz'
- reg.inputs.num_threads = 6
- reg.inputs.metric_weight = [1.0] * 3
- reg.inputs.winsorize_lower_quantile = 0.005
- reg.inputs.winsorize_upper_quantile = 0.995
- reg.inputs.verbose = True
- reg_results = reg.run()
- reg_out_files = reg_results.outputs.get()
- return(reg_out_files)
- #Transform eye + temporal mask + brain mask from MNI to subj
- def mask_transform(mask_list, ref, transmat, output_dir):
- '''
- Transforms masks from MNI to subj space
- masks = list of eye + temporal mask images
- transmat = mapping from MNI > subj space
- outputs:
- '''
- subj = os.path.split(output_dir)[-1]
- dir_files = os.listdir(output_dir)
- if fnmatch.filter(dir_files, '*mask_subj*'):
- print('Mask transforms already run. Not re-running for subj {}'.format(subj))
- subj_trans_masks = glob.glob(os.path.join(output_dir, '*mask_subj.nii.gz'))
- subj_trans_masks.sort() #Sure, this COULD have been a dict, but eh.
- print(subj, 'subj masks', subj_trans_masks)
- else:
- subj_trans_masks = []
- for mask in mask_list:
- image_file = os.path.split(mask)[1]
- image_name = image_file.split('.')[0]
- print(image_name)
- mni2subj = ants.ApplyTransforms()
- mni2subj.inputs.input_image = mask
- mni2subj.inputs.reference_image = ref
- mni2subj.inputs.transforms = transmat
- mni2subj.inputs.interpolation = 'NearestNeighbor'
- mni2subj.inputs.output_image = os.path.join(output_dir, image_name + '_subj.nii.gz')
- mni2subj_results = mni2subj.run()
- output_image = mni2subj_results.outputs.get()['output_image']
- subj_trans_masks.append(output_image)
- subj_trans_masks.sort() #Ditto
- return(subj_trans_masks)
- #Register T2 to T1
- def ants_rigid(fixed = None, moving = None, prefix = None, fixed_mask = None, moving_mask = None, output_dir = None):
- subj = os.path.split(output_dir)[-1]
- dir_files = os.listdir(output_dir)
- if fnmatch.filter(dir_files, 'output_rigid*'):
- print("Ants rigid transformation already run. Not re-running for subj {}".format(subj))
- rigid_out_files = {'warped_image': os.path.join(output_dir, 'output_rigid_image.nii.gz')}
- print('Rigid out', rigid_out_files)
- else:
- print("Performing rigid registration of T1 and T2 images for subj {}".format(subj))
- rigid = ants.Registration()
- rigid.inputs.fixed_image = fixed
- rigid.inputs.moving_image = moving
- if fixed_mask != None:
- rigid.inputs.fixed_mask = fixed_mask
- if moving_mask != None:
- rigid.inputs.moving_mask = fixed_mask
- if prefix != None:
- rigid.inputs.output_transform_prefix = prefix
- else:
- rigid.inputs.output_transform_prefix = os.path.join(output_dir, 'rigid_trans_')
- rigid.inputs.transforms = ['Affine']
- rigid.inputs.transform_parameters = [(0.1,)] #Size of movement for registration (Optimal values are 0.1-0.25.)
- rigid.inputs.number_of_iterations = [[1000, 500, 250, 100]]
- rigid.inputs.dimension = 3
- #
- # Align the moving_image and fixed_image before registration using the geometric
- # center of the images (=0), the image intensities (=1),or the origin of the images (=2)
- #
- rigid.inputs.initial_moving_transform_com = 0
- rigid.inputs.write_composite_transform = True
- rigid.inputs.collapse_output_transforms = False
- rigid.inputs.initialize_transforms_per_stage = False
- rigid.inputs.metric = ['MI']
- rigid.inputs.radius_or_number_of_bins = [32]
- rigid.inputs.sampling_strategy = ['Regular']
- rigid.inputs.sampling_percentage = [0.25]
- rigid.inputs.convergence_threshold = [1e-06]
- rigid.inputs.convergence_window_size = [10]
- rigid.inputs.smoothing_sigmas = [[3, 2, 1, 0]]
- rigid.inputs.sigma_units = ['vox']
- rigid.inputs.shrink_factors = [[8, 4, 2, 1]]
- rigid.inputs.use_estimate_learning_rate_once = [True]
- rigid.inputs.use_histogram_matching = [True]
- if output_dir != None:
- rigid.inputs.output_warped_image = os.path.join(output_dir, 'output_rigid_image.nii.gz')
- else:
- rigid.inputs.output_warped_image = './output_rigid_image.nii.gz'
- rigid.inputs.num_threads = 6
- rigid.inputs.metric_weight = [1.0]
- rigid.inputs.winsorize_lower_quantile = 0.005
- rigid.inputs.winsorize_upper_quantile = 0.995
- rigid.inputs.verbose = True
- rigid.inputs.interpolation = 'NearestNeighbor' #Started with NN, gave good output.
- rigid_results = rigid.run()
- rigid_out_files = rigid_results.outputs.get()
- print('Rigid out', rigid_out_files)
- return(rigid_out_files)
- #Myelin map to MNI
- def subj2mni(moving = None, ref = None, transmat = None, output_dir = None):
- subj = os.path.split(output_dir)[-1]
- print('Warping {} to MNI space'.format(subj))
- subj2mni = ants.ApplyTransforms()
- subj2mni.inputs.dimension = 3
- subj2mni.inputs.input_image = moving
- subj2mni.inputs.reference_image = ref
- subj2mni.inputs.transforms = transmat
- if output_dir != None:
- subj2mni.inputs.output_image = os.path.join(output_dir, 'myelin_map_mni.nii.gz')
- else:
- subj2mni.inputs.output_image = ('myelin_map_mni.nii.gz')
- subj2mni.inputs.interpolation = 'NearestNeighbor'
- subj2mni_results = subj2mni.run()
- subj2mni_output = subj2mni_results.outputs.get()
- subj2mni_im = subj2mni_output['output_image']
- return(subj2mni_output, subj2mni_im)
- ###INTENSITY MANIPULATION###
- #Bias correction
- def bias_corr(images, output_dir):
- '''
- Uses N4 bias correction to remove intensity inhomogeneities
- Input:
- images = List of images (w/ paths) to be corrected
- '''
- subj = os.path.split(output_dir)[-1]
- dir_files = os.listdir(output_dir)
- if fnmatch.filter(dir_files, '*bias*'):
- print("Bias correction already run. Not re-running for subj {}".format(subj))
- bias_output = glob.glob(os.path.join(output_dir, '*_bias_corr.nii.gz'))
- bias_output.sort(key = len) #Assumes T2 image has been rigidly transformed to t1
- else:
- print("Running bias correction for subj {}".format(subj))
- bias_output = []
- for n, image in enumerate(images):
- image_file = os.path.split(image)[1]
- image_name = image_file.split('.')[0]
- print(image, image_name)
- n4 = ants.N4BiasFieldCorrection()
- n4.inputs.dimension = 3
- n4.inputs.input_image = image
- n4.inputs.bspline_fitting_distance = 300
- n4.inputs.shrink_factor = 2
- n4.inputs.n_iterations = [50,50,30,20]
- n4.inputs.save_bias = True
- n4.inputs.bias_image = os.path.join(output_dir, image_name + '_bias_field.nii.gz')
- n4.inputs.output_image = os.path.join(output_dir, image_name + '_bias_corr.nii.gz')
- n4.inputs.num_threads = 6
- n4_results = n4.run()
- output_image = n4_results.outputs.get()['output_image']
- bias_output.append(output_image)
- return(bias_output)
- def image_smooth(image_fn, fwhm = None, output_dir = None):
- subj = os.path.split(output_dir)[-1]
- print('\nSmoothing {} with kernel size: {}'.format(subj, fwhm))
- im_name = os.path.split(image_fn)[-1].split('.')[0]
- im_hdr = nb.load(image_fn)
- smoothed = nbproc.smooth_image(img = im_hdr, fwhm = fwhm)
- output_fn = os.path.join(output_dir, im_name + '_smoothed_' + str(fwhm) + '_mm.nii.gz')
- nb.Nifti1Image(smoothed.get_data(), affine = im_hdr.affine, header = im_hdr.header).to_filename(output_fn)
- return(output_fn)
- #Calibration stage
- def image_calibration(t1_subj_bias_corr = None,
- t2_subj_bias_corr = None,
- t1_mni_bias_corr = None,
- t2_mni_bias_corr = None,
- eye_subj_mask = None,
- temp_bone_subj_mask = None,
- brain_subj_mask = None,
- eye_mni_mask = None,
- temp_bone_mni_mask = None,
- brain_mni_mask = None, output_dir = None):
- subj = os.path.split(output_dir)[-1]
- #load images
- t1_subj_hdr = nb.load(t1_subj_bias_corr)
- t1_subj = t1_subj_hdr.get_data()
- t2_subj = nb.load(t2_subj_bias_corr).get_data()
- t1_mni = nb.load(t1_mni_bias_corr).get_data()
- t2_mni = nb.load(t2_mni_bias_corr).get_data()
- #Load masks
- eye_subj_mask = nb.load(eye_subj_mask).get_data().astype(bool)
- temp_bone_subj_mask = nb.load(temp_bone_subj_mask).get_data().astype(bool)
- brain_subj_mask = nb.load(brain_subj_mask).get_data().astype(bool)
- eye_mni_mask = nb.load(eye_mni_mask).get_data().astype(bool)
- temp_bone_mni_mask = nb.load(temp_bone_mni_mask).get_data().astype(bool)
- brain_mni_mask = nb.load(brain_mni_mask).get_data().astype(bool)
- #Extract stats
- t1_mni_eye = t1_mni[eye_mni_mask]
- t1_mni_eye_stat = stats.mode(t1_mni_eye)[0][0]
- t1_mni_temp = t1_mni[temp_bone_mni_mask]
- t1_mni_temp_stat = stats.mode(t1_mni_temp)[0][0]
- t2_mni_eye = t2_mni[eye_mni_mask]
- t2_mni_eye_stat = stats.mode(t2_mni_eye)[0][0]
- t2_mni_temp =t2_mni[temp_bone_mni_mask]
- t2_mni_temp_stat = stats.mode(t2_mni_temp)[0][0]
- print('\nMNI mask values:\nT1 eye = {} T1 temp bone = {}\nT2 eye = {} T2 temp bone = {}\n'.format(t1_mni_eye_stat, t1_mni_temp_stat, t2_mni_eye_stat, t2_mni_temp_stat))
- t1_subj_eye = t1_subj[eye_subj_mask]
- t1_subj_eye_stat = stats.mode(t1_subj_eye)[0][0]
- t1_subj_temp = t1_subj[temp_bone_subj_mask]
- t1_subj_temp_stat = stats.mode(t1_subj_temp)[0][0]
- t2_subj_eye = t2_subj[eye_subj_mask]
- t2_subj_eye_stat = stats.mode(t2_subj_eye)[0][0]
- t2_subj_temp =t2_subj[temp_bone_subj_mask]
- t2_subj_temp_stat = stats.mode(t2_subj_temp)[0][0]
- print('\n{} mask values:\nT1 eye = {} T1 temp bone = {}\nT2 eye = {} T2 temp bone = {}'.format(subj, t1_subj_eye_stat, t1_subj_temp_stat, t2_subj_eye_stat, t2_subj_temp_stat))
- with open(os.path.join(output_dir, subj + '_mask_values.txt'), 'w') as text_file:
- text_file.write('{} mask values:\nT1 eye = {} T1 temp bone = {}\nT2 eye = {} T2 temp bone = {}'.format(subj, t1_subj_eye_stat, t1_subj_temp_stat, t2_subj_eye_stat, t2_subj_temp_stat))
- #FOR FUTURE: SPLIT INTO 2 FUNCTIONS
- #Shorten linear equation for easier troubleshooting
- t1_a = (t1_mni_temp_stat - t1_mni_eye_stat) / (t1_subj_temp_stat - t1_subj_eye_stat)
- t1_b = ((t1_subj_temp_stat * t1_mni_eye_stat) - (t1_mni_temp_stat * t1_subj_eye_stat)) / (t1_subj_temp_stat - t1_subj_eye_stat)
- t2_a = (t2_mni_temp_stat - t2_mni_eye_stat) / (t2_subj_temp_stat - t2_subj_eye_stat)
- t2_b = ((t2_subj_temp_stat * t2_mni_eye_stat) - (t2_mni_temp_stat * t2_subj_eye_stat)) / (t2_subj_temp_stat - t2_subj_eye_stat)
- #Intensity correction
- t1_corr = (t1_a * t1_subj[brain_subj_mask]) + t1_b
- t2_corr = (t2_a * t2_subj[brain_subj_mask]) + t2_b
- print('\n{} bias corrected T1: {} {}'.format(subj, t1_subj.min(), t1_subj.max()))
- print('{} bias corrected T2: {} {}'.format(subj, t1_subj.min(), t1_subj.max()))
- print('{} calibrated T1: {} {}'.format(subj, t1_corr.min(), t1_corr.max()))
- print('{} calibrated T2:{} {}'.format(subj, t2_corr.min(), t2_corr.max()))
- t1_corr_out = np.zeros_like(t1_subj)
- t2_corr_out = np.zeros_like(t2_subj)
- t1_corr_out[brain_subj_mask] = t1_corr
- t2_corr_out[brain_subj_mask] = t2_corr
- if output_dir != None:
- t1_fn = os.path.join(output_dir, 't1_calibrated.nii.gz')
- t2_fn = os.path.join(output_dir, 't2_calibrated.nii.gz')
- else:
- t1_fn = 't1_calibrated.nii.gz'
- t2_fn = 't2_calibrated.nii.gz'
- nb.Nifti1Image(t1_corr_out, affine = t1_subj_hdr.affine, header = t1_subj_hdr.header).to_filename(t1_fn)
- nb.Nifti1Image(t2_corr_out, affine = t1_subj_hdr.affine, header = t1_subj_hdr.header).to_filename(t2_fn)
- return(t1_fn, t2_fn)
- ###Myelin Map###
- def create_mm_func(corrected_t1, corrected_t2, output_dir):
- """
- The final step. The hard yard. Creation of the myelin map
- through division of two matrices. Calculation of this in the pre-computer
- era would have been a nightmare. Thankfully we don't live in those dark times...
- Inputs:
- corrected_t1 - Bias corrected + calibrated T1 image
- corrected_t2 - Bias corrected + calibrated T1 image
- Output:
- mm_image - The myelin map in subject space.
- """
- subj = os.path.split(output_dir)[-1]
- t1_hdr = nb.load(corrected_t1)
- t1_im = t1_hdr.get_data()
- t1_mask = t1_im > 0
- t2_im = nb.load(corrected_t2).get_data()
- t2_mask = t2_im > 0
- # n_iters = 1
- # print('\nRemoving {} voxels at zero boundaries'.format(1 + n_iters))
- # t2_mask = be(t2_mask, iterations = n_iters)
- # dif_mask = (t1_mask.astype(int) - t2_mask.astype(int)).astype(bool)
- # dif_mask = t1_mask.astype(int) - t2_mask.astype(int)
- mm = t1_im[t2_mask] / t2_im[t2_mask]
- im_mm = np.zeros_like(t1_im)
- im_mm[t2_mask] = mm
- im_mm[np.isnan(im_mm)] = 0
- im_mm[im_mm < 0] = 0
- hi = np.percentile(im_mm.ravel(), [99.95]) #Remove .05% extreme values caused by masking issues
- im_mm[im_mm > hi] = 0
- print('\n{} myelin map values: \nmin = {}\nmax = {}\nmean = {} ({})\nmedian = {}'.format(subj, im_mm.min(),
- im_mm.max(),
- im_mm[im_mm > 0].mean(),
- np.std(im_mm[im_mm > 0]),
- np.median(im_mm[im_mm > 0])))
- out_im_fn = os.path.join(output_dir, 'myelin_map_subj.nii.gz')
- t1_hdr.header['cal_min'] = im_mm.min()
- t1_hdr.header['cal_max'] = im_mm.max()
- print('HEADER MIN: {}\nHEADER MAX: {}'.format(t1_hdr.header['cal_min'], t1_hdr.header['cal_max']))
- nb.Nifti1Image(im_mm, affine = t1_hdr.affine, header = t1_hdr.header).to_filename(out_im_fn)
- return(out_im_fn)
- def mm_percentile(mmap, mask, output_dir):
- """
- Takes the raw myelin map output from create_mm_func and zeros values < 5th percentile and > 9th percentile.
- NOTE: This is for visualisation purposes ONLY
- """
- subj = os.path.split(output_dir)[-1]
- basename = os.path.split(mmap)[1].split('.')[0]
- outname = basename + '_percentile'
- print(subj, outname, output_dir)
- print('\nConverting myelin map to percentage for {}'.format(subj))
- mmap_hdr = nb.load(mmap)
- mmap_data = mmap_hdr.get_data()
- mask = nb.load(mask).get_data().astype(bool)
- mmap_data = ((mmap_data - mmap_data.min() * (1 - 0)) / (mmap_data.max() - mmap_data.min()) + 0)
- out_im_fn = os.path.join(output_dir, outname + '.nii.gz')
- mmap_hdr.header['cal_min'] = 0
- mmap_hdr.header['cal_max'] = 1
- nb.Nifti1Image(mmap_data, affine = mmap_hdr.affine, header = mmap_hdr.header).to_filename(out_im_fn)
- return(out_im_fn)
- ###FUNCTION FOR PARALLEL PROCESSING###
- def myelin_map_proc(subj, n_cores, raw_dir, output_dir, patterns, n_scans, dcm_suffix, fwhm_list):
- """
- Wrapper function that is needed for parallel processing but can also be used for individual subjects.
- Inputs:
- subj - name of participant to be processed (str)
- n_cores - number of cores to be used for parallel processing (int).
- note: for single participants only a single core is used).
- raw_dir - path to root directory which houses subj/dicoms (str).
- output_dir - path to write output to (note: output_dir is the root. Individual participant directories will be created during the process (str).
- patterns - strings for how to recognise T1 and T2 dicom directories with the participant directory (dict).
- n_scans - Number of expected dicom files in t1 and t2 directories (dict).
- dcm_suffix - Strings of file endings for how to recognise dicom files (str).
- fwhm_list - Values for size of smoothing kernel (in mm) (list).
- Note: Can be single value or multiple.
- Output:
- Everything.
- """
- print('Processing data for subj {}'.format(subj))
- try:
- os.mkdir(output_dir)
- except:
- print('Directory {} exists. Not creating'.format(output_dir))
- out_subj_dir = os.path.join(output_dir, subj)
- print()
- try:
- os.mkdir(out_subj_dir)
- except:
- print('Directory {} already exists. Not creating.'.format(out_subj_dir))
- print("\nWorking dir: {}".format(out_subj_dir))
- ##Read in masks
- t1_im_mni_fn = os.path.join('.', 'resources','mni_t1_template.nii.gz')
- t2_im_mni_fn = os.path.join('.', 'resources','mni_t2_template.nii.gz')
- eye_mni_mask_fn = os.path.join('.', 'resources','mni_eye_mask.nii.gz')
- temp_bone_mni_mask_fn = os.path.join('.', 'resources','mni_temp_bone_mask.nii.gz')
- brain_mni_mask_fn = os.path.join('.', 'resources','mni_brain_mask.nii.gz')
- subj_raw_dirs = os.listdir(os.path.join(raw_dir, subj))
- t1_dir = [raw_dir for raw_dir in subj_raw_dirs if patterns[0] in raw_dir][0]
- t2_dir = [raw_dir for raw_dir in subj_raw_dirs if patterns[1] in raw_dir][0]
- scan_dict = {'t1': glob.glob(os.path.join(raw_dir, subj, t1_dir, '*{}'.format(dcm_suffix))),
- 't2': glob.glob(os.path.join(raw_dir, subj, t2_dir, '*{}'.format(dcm_suffix)))
- }
- #Count number of scans for t1 and t2, print and write to file.
- im_count = {k: len(list(scan_dict.values())[n]) for n, k in enumerate(scan_dict)}
- print(subj, im_count)
- with open(os.path.join(out_subj_dir, subj + '_input_files_n.txt'), 'w') as text_file:
- text_file.write('{} - {}'.format(subj, im_count))
- #Generate error if data not matching criteria
- if len(scan_dict['t1']) < n_scans['t1']:
- with open(os.path.join(out_subj_dir, subj + '_t1_error.txt'), 'w') as text_file:
- text_file.write('Number of T1 scans < {}}: {}'.format(n_scans['t1'], len(scan_dict['t1'])))
- raise ValueError('Error: Number of DICOMS in T1 directory less than expected for subj {}\n# of Dicoms = {}'.format(subj, len(scan_dict['t1'])))
- if len(scan_dict['t2']) < n_scans['t2']:
- with open(os.path.join(out_subj_dir, subj + '_t2_error.txt'), 'w') as text_file:
- text_file.write('Number of T2 scans < {}: {}'.format(n_scans['t2'], len(scan_dict['t2'])))
- raise ValueError('Error: Number of DICOMS in T2 directory less than expected for subj {}\n# of Dicoms = {}'.format(subj, len(scan_dict['t2'])))
- #Start pipeline
- t1_subj_nii, t2_subj_nii = dcm_convert(scan_dict, out_subj_dir)
- reg_output = ants_reg(fixed = t1_subj_nii, moving = t1_im_mni_fn, output_dir = out_subj_dir)
- mask_list = [eye_mni_mask_fn, temp_bone_mni_mask_fn, brain_mni_mask_fn]
- brain_subj_mask, eye_subj_mask, temp_bone_subj_mask, = mask_transform(mask_list = mask_list,
- ref = t1_subj_nii,
- transmat = reg_output['composite_transform'],
- output_dir = out_subj_dir)
- # plot_ants_warp(fixed = t1_subj_nii, moving = brain_subj_mask, nslices = 10, output_name = os.path.join(out_subj_dir, 'brain_mask'))
- rigid_output = ants_rigid(fixed = t1_subj_nii, moving = t2_subj_nii, output_dir = out_subj_dir)
- print(t1_subj_nii, rigid_output['warped_image'])
- t1_bias, t2_bias = bias_corr([t1_subj_nii, rigid_output['warped_image']], output_dir = out_subj_dir)
- #Calibration - TO DO: Split function into mode calculation + linear correction
- print('Brain = {}\nEye = {}\nTemp = {}'.format(brain_subj_mask, eye_subj_mask, temp_bone_subj_mask))
- t1_cal, t2_cal = image_calibration(t1_subj_bias_corr = t1_bias,
- t2_subj_bias_corr = t2_bias,
- t1_mni_bias_corr = t1_im_mni_fn,
- t2_mni_bias_corr = t2_im_mni_fn,
- eye_subj_mask = eye_subj_mask,
- temp_bone_subj_mask = temp_bone_subj_mask,
- brain_subj_mask = brain_subj_mask,
- eye_mni_mask = eye_mni_mask_fn,
- temp_bone_mni_mask = temp_bone_mni_mask_fn,
- brain_mni_mask = brain_mni_mask_fn,
- output_dir = out_subj_dir)
- #Calculate myelin maps
- myelin_map_subj = create_mm_func(t1_cal, t2_cal, output_dir = out_subj_dir)
- #Warp myelin maps to MNI space
- subj2mni_output, subj2mni_im = subj2mni(moving = myelin_map_subj, ref = t1_im_mni_fn, transmat = reg_output['inverse_composite_transform'], output_dir = out_subj_dir)
- #Smooth
- if len(fwhm_list) > 1:
- for im in [myelin_map_subj, subj2mni_im]:
- for fwhm in fwhm_list:
- smoothed = image_smooth(image_fn = im, fwhm = fwhm, output_dir = out_subj_dir)
- else:
- for im in [myelin_map_subj, subj2mni_im]:
- smoothed = image_smooth(image_fn = im, fwhm = fwhm_list[0], output_dir = out_subj_dir)
- #Percentile
- mmap_list = glob.glob(os.path.join(out_subj_dir, 'myelin_map*'))
- mmap_list = [im for im in mmap_list if 'percentile' not in im]
- for im in mmap_list:
- if 'mni' in im:
- mm_percentile(mmap = im, mask = brain_mni_mask_fn, output_dir = out_subj_dir)
- else:
- mm_percentile(mmap = im, mask = brain_subj_mask, output_dir = out_subj_dir)
- for im in mmap_list:
- if 'mni' in im:
- mm_percentage(mmap = im, mask = brain_mni_mask_fn, output_dir = out_subj_dir)
- else:
- mm_percentage(mmap = im, mask = brain_subj_mask, output_dir = out_subj_dir)
myelin_map_funcs.py at commit 17d42f9, no license · at the source
Overview
- Department of Neuroscience, Biomedicine and Movement Sciences University of Verona Verona Italy
- Nuffield Department of Clinical Neurosciences University of Oxford Oxford UK
- Department of Clinical Neurology, John Radcliffe Hospital Oxford University Hospitals Foundation Trust Oxford UK
- Department of Engineering for Innovation Medicine University of Verona Verona Italy
- Santa Maria Delle Croci Hospital, Department of Neuroscience MS Center Ravenna Italy
- Department of Biotechnological and Applied Clinical Sciences University of L'Aquila L'Aquila Italy
- Department of Neurosciences Azienda Ospedaliero‐Universitaria di Modena, Baggiovara Civil Hospital Modena Italy
- Neurology Unit Mater Salutis Hospital Legnago VR Italy
- Neurology Unit Santa Chiara Hospital Trento Italy
- Department of Brain Sciences, Faculty of Medicine Imperial College London London UK
- Oxford Autoimmune Neurology Diagnostic Laboratory, Nuffield Department of Clinical Neurosciences University of Oxford Oxford UK
Abstract
Objective: To determine whether myelin‐sensitive quantitative MRI reveals microstructural abnormalities in normal‐appearing cortex (NACtx) in myelin oligodendrocyte glycoprotein antibody–associated disease (MOGAD), indicating that conventional MRI underestimates remission residual cortical injury.
Methods: Forty‐two patients with MOGAD in remission and 42 age‐ and sex‐matched healthy controls (HCs) underwent cognitive testing (Rao Brief Repeatable Battery), disability rating (Expanded Disability Status Scale) and 3‐T MRI, including three‐dimensional T1‐weighted, T2‐weighted and double‐inversion‐recover
Results: Sixteen of 42 MOGAD patients had a cortical phenotype; cortical lesions at remission were present in 4 of 42 (9.5%), all with a cortical phenotype. MTsat metric, but not T1/
Interpretation: Myelin‐sensitive MTsat reveals persistent, regionally specific abnormalities in normal‐appearing cortex in cortical MOGAD and is a promising marker of residual cortical damage linked to cognitive dysfunction.
Reproduced under the paper's license (CC BY), from the paper cited above.
Repository
Its files are read in the Code ↔ Paper reader above, with 1 match between paragraphs and lines of code.
petergoodin/myelin_map
17d42f983f7b6da8aecad3013b1ca9c6d7e68774, 11 March 2019Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
4 files
- myelin_map_funcs.py, Python, 788 lines, 1 match
- myelin_map_prototype.ipy
nb , Jupyter, 466 lines - myelin_map_run.py, Python, 57 lines
- README.md, Text, 48 lines
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 3 scripts, each with its path and the digest of its content;
- 1 match between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Data Availability Statement
The data that support the findings of this study are available on request from the corresponding author. The data are not publicly available due to privacy or ethical restrictions.
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 27 September 2026: the first record
Recorded: type, language, journal, pages, dates, 21 authors, 5 keywords, 1 funder, 44 references.
Cite
This paper
Camera, V., Tamanti, A., Messina, S., Dall'Osto, N., Maltempo, T., Ziccardi, S., Foschi, M., Piscaglia, M. G., Ferraro, D., Crescenzo, F., Rossi, F., Bajrami, A., Marangoni, S., Marastoni, D., Pizzini, F. B., Leite, M. I., Magliozzi, R., Waters, P., Calabrese, M., . . . Geraldes, R. (2026). Quantitative MRI Uncovers Subtle Cortical Damage in Myelin Oligodendrocyte Glycoprotein Antibody-Associated Disease. Annals of clinical and translational neurology, 10.1002/
BibTeX
@article{camera2026quant
author = {Camera, Valentina and Tamanti, Agnese and Messina, Silvia and Dall'Osto, Nicola and Maltempo, Teresa and Ziccardi, Stefano and Foschi, Matteo and Piscaglia, Maria Grazia and Ferraro, Diana and Crescenzo, Francesco and Rossi, Francesca and Bajrami, Albulena and Marangoni, Sabrina and Marastoni, Damiano and Pizzini, Francesca Benedetta and Leite, Maria Isabel and Magliozzi, Roberta and Waters, Patrick and Calabrese, Massimiliano and Palace, Jacqueline and Geraldes, Ruth},
title = {{Quantitative MRI Uncovers Subtle Cortical Damage in Myelin Oligodendrocyte Glycoprotein Antibody-Associated Disease}},
journal = {Annals of clinical and translational neurology},
year = {2026},
month = jul,
pages = {10.1002/
publisher = {Wiley},
issn = {2328-9503},
doi = {10.1002/
url = {https://
pmid = {42444075},
pmcid = {PMC13394544}
}
RIS
TY - JOUR
AU - Camera, Valentina
AU - Tamanti, Agnese
AU - Messina, Silvia
AU - Dall'Osto, Nicola
AU - Maltempo, Teresa
AU - Ziccardi, Stefano
AU - Foschi, Matteo
AU - Piscaglia, Maria Grazia
AU - Ferraro, Diana
AU - Crescenzo, Francesco
AU - Rossi, Francesca
AU - Bajrami, Albulena
AU - Marangoni, Sabrina
AU - Marastoni, Damiano
AU - Pizzini, Francesca Benedetta
AU - Leite, Maria Isabel
AU - Magliozzi, Roberta
AU - Waters, Patrick
AU - Calabrese, Massimiliano
AU - Palace, Jacqueline
AU - Geraldes, Ruth
TI - Quantitative MRI Uncovers Subtle Cortical Damage in Myelin Oligodendrocyte Glycoprotein Antibody-Associated Disease
T2 - Annals of clinical and translational neurology
J2 - Ann Clin Transl Neurol
PY - 2026
DA - 2026/
SP - 10.1002/
SN - 2328-9503
PB - Wiley
DO - 10.1002/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1002/
"type": "article-journal",
"title": "Quantitative MRI Uncovers Subtle Cortical Damage in Myelin Oligodendrocyte Glycoprotein Antibody-Associated Disease",
"container-title": "Annals of clinical and translational neurology",
"author": [
{
"family": "Camera",
"given": "Valentina"
},
{
"family": "Tamanti",
"given": "Agnese"
},
{
"family": "Messina",
"given": "Silvia"
},
{
"family": "Dall'Osto",
"given": "Nicola"
},
{
"family": "Maltempo",
"given": "Teresa"
},
{
"family": "Ziccardi",
"given": "Stefano"
},
{
"family": "Foschi",
"given": "Matteo"
},
{
"family": "Piscaglia",
"given": "Maria Grazia"
},
{
"family": "Ferraro",
"given": "Diana"
},
{
"family": "Crescenzo",
"given": "Francesco"
},
{
"family": "Rossi",
"given": "Francesca"
},
{
"family": "Bajrami",
"given": "Albulena"
},
{
"family": "Marangoni",
"given": "Sabrina"
},
{
"family": "Marastoni",
"given": "Damiano"
},
{
"family": "Pizzini",
"given": "Francesca Benedetta"
},
{
"family": "Leite",
"given": "Maria Isabel"
},
{
"family": "Magliozzi",
"given": "Roberta"
},
{
"family": "Waters",
"given": "Patrick"
},
{
"family": "Calabrese",
"given": "Massimiliano"
},
{
"family": "Palace",
"given": "Jacqueline"
},
{
"family": "Geraldes",
"given": "Ruth"
}
],
"container-title-short":
"page": "10.1002/
"DOI": "10.1002/
"PMID": "42444075",
"PMCID": "PMC13394544",
"ISSN": "2328-9503",
"publisher": "Wiley",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
13
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1038/s41467-026-73366-9 [code]
- Cortical and white matter myelination proceed in concert during early infancy.Journal: Nature communicationsIn common: Nipype, statsmodels, NiBabel, 4 other tools, 3 references
- [2] doi:10.1038/s42003-026-10276-y [code]
- The cellular correlates and adolescent reorganisation of cortical myelination networks in the common marmoset.Journal: Communications biologyIn common: Nipype, statsmodels, NiBabel, 4 other tools, structural MRI / diffusion, 2 references
- [3] doi:10.1371/journal.pbio.3003856 [code]
- Aging and metabolism contribute separately to brain-body health.Journal: PLoS biologyIn common: ANTs, statsmodels, NiBabel, 4 other tools, structural MRI / diffusion, 2 references
- [4] doi:10.1093/braincomms/fcag129 [code]
- Longitudinal changes of choroid plexus volumes and MRI ratios in multiple sclerosis.Journal: Brain communicationsIn common: ANTs, NiBabel, SciPy, 1 other tool, structural MRI / diffusion, 4 references
- [5] doi:10.1038/s41398-026-04157-5 [code]
- Association of glymphatic function with 40-Hz neural oscillations, systemic metabolic markers, and cognitive performance in healthy aging adults: An EEG and MRI study.Journal: Translational psychiatryIn common: Nipype, ANTs, statsmodels, 5 other tools, structural MRI / diffusion
- [6] doi:10.1162/imag.a.1362 [code]
- Human fMRI at 11.7T: Assessing feasibility, stability, and reliability on the Iseult scanner.Journal: Imaging neuroscience (Cambridge, Mass.)In common: Nipype, ANTs, statsmodels, 5 other tools
- [7] doi:10.1371/journal.pbio.3003755 [code]
- Action information is integrated into entorhinal representations of conceptual space and is reflected in eye movements.Journal: PLoS biologyIn common: Nipype, ANTs, statsmodels, 5 other tools
- [8] doi:10.1038/s41467-026-71151-2 [code]
- Common and distinct neural correlates of social interaction processing and theory of mind in narratives.Journal: Nature communicationsIn common: Nipype, ANTs, statsmodels, 5 other tools
- [9] doi:10.1002/alz.71649 [code]
- Postmortem brain MRI reveals differential associations of subcortical and limbic volumes with cortical thinning and neurodegenerative pathologies.Journal: Alzheimer's & dementia : the journal of the Alzheimer's AssociationIn common: ANTs, statsmodels, NiBabel, 4 other tools, structural MRI / diffusion, other condition, cellular / molecular, 1 reference
- [10] doi:10.1162/imag.a.1245 [code]
- Towards precision EEG connectomics: Evaluating the benefits of dense sampling.Journal: Imaging neuroscience (Cambridge, Mass.)In common: Nipype, ANTs, statsmodels, 5 other tools
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 3 scripts, and 1 match between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:8432e5063169616d…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
