def main(): path_warp = sys.argv[ 1] # {StudyDir}/{SubjectID}/MNINonLinear/xfms/acpc_dc2standard.nii.gz path_to_T1_space_ASL_variable = sys.argv[ 2] # {StudyDir}/{SubjectID}/T1w/ASL/TIs/OxfordASL/native_space/<perfusion_variable> path_T1 = sys.argv[ 3] # {StudyDir}/{SubjectID}/T1w/T1w_acpc_dc_restore.nii.gz path_MNI = sys.argv[4] # /usr/local/fsl/data/standard/MNI152_T1_2mm.nii.gz path_to_lowres_MNI = sys.argv[ 5] # {StudyDir}/{SubjectID}/MNINonLinear/ASL/OutputtoCIFTI/asl_grid_mni.nii.gz path_to_MNI_space_ASL_variable = sys.argv[ 6] # {StudyDir}/{SubjectID}/MNINonLinear/ASL/Results/<folder/perfusion_variable_MNI> # Make the ASL-grid MNI space target image for registration (if needed) if not op.isfile(path_to_lowres_MNI): print("Creating ASL-grid MNI-space MNI image") perfusion_spc = rt.ImageSpace(path_to_T1_space_ASL_variable) mni_spc = rt.ImageSpace(path_MNI) mni_asl_grid = mni_spc.resize_voxels(perfusion_spc.vox_size / mni_spc.vox_size) nb.save( rt.Registration.identity().apply_to_image(path_MNI, mni_asl_grid), path_to_lowres_MNI) else: print("ASL-grid MNI-space MNI image already exists") # Warp ASL variable to newly prepared ASL-gridded MNI-space print("Transforming ASL Variable to ASL-gridded MNI-space ASL") the_warp = rt.NonLinearRegistration.from_fnirt(path_warp, path_T1, path_MNI) asl_mni_mniaslgrid = the_warp.apply_to_image(path_to_T1_space_ASL_variable, path_to_lowres_MNI) nb.save(asl_mni_mniaslgrid, path_to_MNI_space_ASL_variable)
def main(): # argument handling parser = argparse.ArgumentParser( description= "Create T1-ASL space, estimate PVs, generate CSF ventricle mask") parser.add_argument("study_dir", help="Path of the base study directory.") parser.add_argument("sub_number", help="Subject number.") args = parser.parse_args() study_dir = args.study_dir sub_id = args.sub_number # for debug, re-use intermediate results force_refresh = True sub_base = op.abspath(op.join(study_dir, sub_id)) t1_dir = op.join(sub_base, "T1w") asl = op.join(sub_base, "ASL", "TIs", "tis.nii.gz") struct = op.join(t1_dir, "T1w_acpc_dc_restore.nii.gz") # Create ASL-gridded version of T1 image t1_asl_grid = op.join(t1_dir, "ASL", "reg", "ASL_grid_T1w_acpc_dc_restore.nii.gz") if not op.exists(t1_asl_grid) or force_refresh: asl_spc = rt.ImageSpace(asl) t1_spc = rt.ImageSpace(struct) t1_asl_grid_spc = t1_spc.resize_voxels(asl_spc.vox_size / t1_spc.vox_size) nib.save( rt.Registration.identity().apply_to_image(struct, t1_asl_grid_spc), t1_asl_grid) # Create a ventricle CSF mask in T1 ASL space ventricle_mask = op.join(sub_base, "T1w", "ASL", "PVEs", "vent_csf_mask.nii.gz") if not op.exists(ventricle_mask) or force_refresh: aparc_aseg = op.join(t1_dir, "aparc+aseg.nii.gz") vmask = generate_ventricle_mask(aparc_aseg, t1_asl_grid) rt.ImageSpace.save_like(t1_asl_grid, vmask, ventricle_mask) # Estimate PVs in T1 ASL space pv_gm = op.join(sub_base, "T1w", "ASL", "PVEs", "pve_GM.nii.gz") if not op.exists(pv_gm) or force_refresh: aparc_seg = op.join(t1_dir, "aparc+aseg.nii.gz") pvs_stacked = estimate_pvs(t1_dir, t1_asl_grid) # Save output with tissue suffix fileroot = op.join(sub_base, "T1w", "ASL", "PVEs", "pve") for idx, suffix in enumerate(['GM', 'WM', 'CSF']): p = "{}_{}.nii.gz".format(fileroot, suffix) rt.ImageSpace.save_like(t1_asl_grid, pvs_stacked.dataobj[..., idx], p)
def main(): desc = """ Generate PV estimates for a reference voxel grid. FS' volumetric segmentation is used for the subcortex and a surface-based method is used for the cortex (toblerone). """ parser = argparse.ArgumentParser(description=desc) parser.add_argument( "--aparcseg", required=True, help="path to volumetric FS segmentation (aparc+aseg.mgz)") parser.add_argument("--LWS", required=True, help="path to left white surface") parser.add_argument("--LPS", required=True, help="path to left pial surface") parser.add_argument("--RPS", required=True, help="path to right pial surface") parser.add_argument("--RWS", required=True, help="path to right white surface") parser.add_argument("--ref", required=True, help="path to image defining reference grid for PVs") parser.add_argument("--out", required=True, help="path to save output") parser.add_argument("--stack", action="store_true", help="stack output into single 4D volume") parser.add_argument("--cores", default=1, type=int, help="CPU cores to use") args = parser.parse_args() surf_dict = dict([(k, getattr(args, k)) for k in ['LWS', 'LPS', 'RPS', 'RWS']]) pvs = extract_fs_pvs(args.aparcseg, surf_dict, args.ref, args.cores) if args.stack: nib.save(pvs, args.out) else: opath = pathlib.Path(args.out) spc = rt.ImageSpace(pvs) pvs = pvs.dataobj for idx, tiss in enumerate(["GM", "WM", "CSF"]): n = opath.parent.joinpath( opath.stem.rsplit('.', 1)[0] + f"_{tiss}" + "".join(opath.suffixes)) spc.save_image(pvs[..., idx], n.as_posix())
def generate_fmaps(pa_ap_sefms, params, config, distcorr_dir): """ Generate fieldmaps via topup for use with asl_reg. Args: asl_vol0: path to image of stacked blipped images (ie, PEdir as vol0, (oPEdir as vol1), in this case stacked as pa then ap) params: path to text file for topup --datain, PE directions/times config: path to text file for topup --config, other args distcorr_dir: directory in which to put output Returns: n/a, files 'fmap, fmapmag, fmapmagbrain.nii.gz' will be created in output dir """ pwd = os.getcwd() os.chdir(distcorr_dir) # Run topup to get fmap in Hz topup_fmap = op.join(distcorr_dir, 'topup_fmap_hz.nii.gz') cmd = (("topup --imain={} --datain={}".format(pa_ap_sefms, params) + " --config={} --out=topup".format(config)) + " --fout={} --iout={}".format( topup_fmap, op.join(distcorr_dir, 'corrected_sefms.nii.gz'))) sp.run(cmd, shell=True) fmap, fmapmag, fmapmagbrain = [ op.join(distcorr_dir, '{}.nii.gz'.format(s)) for s in ['fmap', 'fmapmag', 'fmapmagbrain'] ] # Convert fmap from Hz to rad/s fmap_spc = rt.ImageSpace(topup_fmap) fmap_arr_hz = nb.load(topup_fmap).get_data() fmap_arr = fmap_arr_hz * 2 * np.pi fmap_spc.save_image(fmap_arr, fmap) # Mean across volumes of corrected sefms to get fmapmag fmapmag_arr = nb.load(op.join(distcorr_dir, "corrected_sefms.nii.gz")).get_data() fmapmag_arr = fmapmag_arr.mean(-1) fmap_spc.save_image(fmapmag_arr, fmapmag) # Run BET on fmapmag to get brain only version bet(fmap_spc.make_nifti(fmapmag_arr), output=fmapmagbrain) os.chdir(pwd)
def generate_ventricle_mask(aparc_aseg, t1_asl): ref_spc = rt.ImageSpace(t1_asl) # get ventricles mask from aparc+aseg image aseg = nib.load(aparc_aseg).get_data() vent_mask = np.logical_or( aseg == 43, # left ventricle aseg == 4 # right ventricle ) # erosion in t1 space for safety vent_mask = scipy.ndimage.morphology.binary_erosion(vent_mask) # Resample to target space, re-threshold output = rt.Registration.identity().apply_to_array(vent_mask, aparc_aseg, ref_spc) output = (output > 0.8) return output
def create_ti_image(asl, tis, sliceband, slicedt, outname): """ Create a 4D series of actual TIs at each voxel. Args: asl: path to image in the space we wish to create the TI series tis: list of TIs in the acquisition sliceband: number of slices per band in the acquisition slicedt: time taken to acquire each slice outname: path to which the ti image is saved Returns: n/a, file outname is created in output directory """ asl_spc = rt.ImageSpace(asl) n_slice = asl_spc.size[2] slice_in_band = np.tile(np.arange(0, sliceband), n_slice // sliceband).reshape(1, 1, n_slice, 1) ti_array = np.array([np.tile(x, asl_spc.size) for x in tis]).transpose(1, 2, 3, 0) ti_array = ti_array + (slice_in_band * slicedt) rt.ImageSpace.save_like(asl, ti_array, outname)
def generate_fmaps(pa_ap_sefms, params, config, distcorr_dir, gdc_warp, interpolation=3): """ Generate fieldmaps via topup for use with asl_reg. Args: asl_vol0: path to image of stacked blipped images (ie, PEdir as vol0, (oPEdir as vol1), in this case stacked as pa then ap) params: path to text file for topup --datain, PE directions/times config: path to text file for topup --config, other args distcorr_dir: directory in which to put output gdc_warp: path to gradient_unwarp's gradient distortion correction warp interpolation: order of interpolation to be used when applying registrations, default=3 Returns: n/a, files 'fmap, fmapmag, fmapmagbrain.nii.gz' will be created in output dir """ # set up logger logger = logging.getLogger("HCPASL.distortion_estimation") logger.info("Running generate_fmaps()") logger.info(f"Spin Echo Field Maps: {pa_ap_sefms}") logger.info(f"Topup param file: {params}") logger.info(f"Topup config file: {config}") logger.info(f"Topup output directory: {distcorr_dir}") logger.info(f"Gradient distortion correction warp: {gdc_warp}") logger.info(f"Interpolation order: {interpolation}") pwd = os.getcwd() os.chdir(distcorr_dir) # apply gradient distortion correction to stacked SEFMs gdc = rt.NonLinearRegistration.from_fnirt(coefficients=str(gdc_warp), src=str(pa_ap_sefms), ref=str(pa_ap_sefms), intensity_correct=True) gdc_corr_pa_ap_sefms = gdc.apply_to_image(src=str(pa_ap_sefms), ref=str(pa_ap_sefms), order=interpolation, cores=1) gdc_corr_pa_ap_sefms_name = distcorr_dir / "merged_sefms_gdc.nii.gz" nb.save(gdc_corr_pa_ap_sefms, gdc_corr_pa_ap_sefms_name) # Run topup to get fmap in Hz topup_fmap = op.join(distcorr_dir, 'topup_fmap_hz.nii.gz') topup_cmd = [ "topup", f"--imain={gdc_corr_pa_ap_sefms_name}", f"--datain={params}", f"--config={config}", f"--out=topup", f"--iout={op.join(distcorr_dir, 'corrected_sefms.nii.gz')}", f"--fout={topup_fmap}", f"--dfout={op.join(distcorr_dir, 'WarpField')}", f"--rbmout={op.join(distcorr_dir, 'MotionMatrix')}", f"--jacout={op.join(distcorr_dir, 'Jacobian')}", "--verbose" ] logger.info(f"Topup command: {' '.join(topup_cmd)}") process = sp.Popen(topup_cmd, stdout=sp.PIPE) while 1: retcode = process.poll() line = process.stdout.readline().decode("utf-8") logger.info(line) if line == "" and retcode is not None: break if retcode != 0: logger.info(f"retcode={retcode}") logger.exception("Process failed.") fmap, fmapmag, fmapmagbrain = [ op.join(distcorr_dir, '{}.nii.gz'.format(s)) for s in ['fmap', 'fmapmag', 'fmapmagbrain'] ] # Convert fmap from Hz to rad/s logger.info("Converting fieldmap from Hz to rad/s.") fmap_spc = rt.ImageSpace(topup_fmap) fmap_arr_hz = nb.load(topup_fmap).get_data() fmap_arr = fmap_arr_hz * 2 * np.pi fmap_spc.save_image(fmap_arr, fmap) # Apply gdc warp from gradient_unwarp and topup's EPI-DC # warp (just generated) in one interpolation step logger.info( "Applying gdc and epi-dc to fieldmap images in one interpolation step." ) pa_ap_sefms_gdc_dc = apply_gdc_and_topup(pa_ap_sefms, distcorr_dir, gdc_warp, interpolation=interpolation) # Mean across volumes of corrected sefms to get fmapmag logger.info( "Taking mean of corrected fieldmap images to get fmapmag.nii.gz") fmapmag_img = nb.nifti1.Nifti1Image( pa_ap_sefms_gdc_dc.get_fdata().mean(-1), affine=pa_ap_sefms_gdc_dc.affine) nb.save(fmapmag_img, fmapmag) # Run BET on fmapmag to get brain only version logger.info("Running BET on fmapmag for brain-extracted version.") bet(fmapmag, output=fmapmagbrain) os.chdir(pwd)
def main(): # argument handling parser = argparse.ArgumentParser() parser.add_argument("study_dir", help="Path of the base study directory.") parser.add_argument("sub_number", help="Subject number.") parser.add_argument( "-g", "--grads", help="Filename of the gradient coefficients for gradient" + "distortion correction (optional).") args = parser.parse_args() study_dir = args.study_dir sub_num = args.sub_number grad_coeffs = args.grads oph = (study_dir + "/" + sub_num + "/ASL/TIs/DistCorr") outdir = (study_dir + "/" + sub_num + "/T1w/ASL/reg") pve_path = (study_dir + "/" + sub_num + "/T1w/ASL/PVEs") T1w_oph = (study_dir + "/" + sub_num + "/T1w/ASL/TIs/DistCorr") T1w_cal_oph = (study_dir + "/" + sub_num + "/T1w/ASL/Calib/Calib0/DistCorr") need_dirs = [oph, outdir, pve_path, T1w_oph, T1w_cal_oph] for req_dir in need_dirs: Path(req_dir).mkdir(parents=True, exist_ok=True) initial_wd = os.getcwd() print("Pre-distortion correction working directory was: " + initial_wd) print("Changing working directory to: " + oph) os.chdir(oph) # Generate ASL-gridded T1-aligned T1w image for use as a reg reference t1 = (study_dir + "/" + sub_num + "/T1w/T1w_acpc_dc_restore.nii.gz") t1_brain = (study_dir + "/" + sub_num + "/T1w/T1w_acpc_dc_restore_brain.nii.gz") asl = (study_dir + "/" + sub_num + "/ASL/TIs/STCorr/SecondPass/tis_stcorr.nii.gz") t1_asl_res = (study_dir + "/" + sub_num + "/T1w/ASL/reg/ASL_grid_T1w_acpc_dc_restore.nii.gz") asl_v1 = (study_dir + "/" + sub_num + "/ASL/TIs/STCorr/SecondPass/tis_stcorr_vol1.nii.gz") first_asl_call = ("fslroi " + asl + " " + asl_v1 + " 0 1") # print(first_asl_call) sp.run(first_asl_call.split(), check=True, stderr=sp.PIPE, stdout=sp.PIPE) print("Running regtricks bit") t1_spc = rt.ImageSpace(t1) asl_spc = rt.ImageSpace(asl_v1) t1_spc_asl = t1_spc.resize_voxels(asl_spc.vox_size / t1_spc.vox_size) r = rt.Registration.identity() t1_asl = r.apply_to_image(t1, t1_spc_asl) nb.save(t1_asl, t1_asl_res) # Check .grad coefficients are available and call function to generate # GDC warp if they are: if os.path.isfile(grad_coeffs): calc_gdc_warp(asl_v1, grad_coeffs, oph) else: print("Gradient coefficients not available") print("Changing back to original working directory: " + initial_wd) os.chdir(initial_wd) # output file of topup parameters to subject's distortion correction dir pars_filepath = (oph + "/topup_params.txt") produce_topup_params(pars_filepath) # generate EPI distortion correction fieldmaps for use in asl_reg pa_sefm, ap_sefm = find_field_maps(study_dir, sub_num) pa_ap_sefms = (oph + "/merged_sefms.nii.gz") cnf_file = "b02b0.cnf" out_basename = (oph + "/topup_result") topup_fmap = (oph + "/topup_result_fmap_hz.nii.gz") fmap_rads = (oph + "/fmap_rads.nii.gz") fmapmag = (oph + "/fmapmag.nii.gz") fmapmagbrain = (oph + "/fmapmag_brain.nii.gz") calc_fmaps(pa_sefm, ap_sefm, pa_ap_sefms, pars_filepath, cnf_file, oph, out_basename, topup_fmap, fmap_rads, fmapmag, fmapmagbrain) # Calculate initial linear transformation from ASL-space to T1w-space asl_v1_brain = (study_dir + "/" + sub_num + "/ASL/TIs/STCorr/SecondPass/tis_stcorr_vol1_brain.nii.gz") bet_regfrom_call = ("bet " + asl_v1 + " " + asl_v1_brain) # print(bet_regfrom_call) sp.run(bet_regfrom_call.split(), check=True, stderr=sp.PIPE, stdout=sp.PIPE) gen_initial_trans(asl_v1_brain, outdir, t1, t1_brain) # Generate a brain mask in the space of the 1st ASL volume asl2struct = (outdir + "/asl2struct.mat") t1_brain_mask = (outdir + "/T1w_acpc_dc_restore_brain_mask.nii.gz") asl_mask = (outdir + "/asl_vol1_mask.nii.gz") struct2asl = (outdir + "/struct2asl.mat") gen_asl_mask(t1_brain, t1_brain_mask, asl_v1_brain, asl2struct, asl_mask, struct2asl) # brain mask t1_mask = (study_dir + "/" + sub_num + "/T1w/ASL/reg/T1w_acpc_dc_restore_brain_mask.nii.gz") t1_asl_mask_name = ( study_dir + "/" + sub_num + "/T1w/ASL/reg/ASL_grid_T1w_acpc_dc_restore_brain_mask.nii.gz") t1_mask_spc = rt.ImageSpace(t1_mask) t1_mask_spc_asl = t1_mask_spc.resize_voxels(asl_spc.vox_size / t1_mask_spc.vox_size) r = rt.Registration.identity() t1_mask_asl = r.apply_to_image(t1_mask, t1_mask_spc_asl) fslmaths(t1_mask_asl).thr(0.5).bin().run(t1_asl_mask_name) # Generate PVEs aparc_aseg = (study_dir + "/" + sub_num + "/T1w/aparc+aseg.nii.gz") pve_files = (study_dir + "/" + sub_num + "/T1w/ASL/PVEs/pve") gen_pves(Path(t1).parent, asl, pve_files) # Generate WM mask pvwm = (pve_files + "_WM.nii.gz") tissseg = (study_dir + "/" + sub_num + "/T1w/ASL/PVEs/wm_mask.nii.gz") gen_wm_mask(pvwm, tissseg) # Calculate the overall distortion correction warp asl2str_trans = (outdir + "/asl2struct.mat") gdc_warp = (oph + "/gdc_warp.nii.gz") calc_distcorr_warp(asl_v1_brain, oph, t1, t1_brain, asl_mask, tissseg, asl2str_trans, fmap_rads, fmapmag, fmapmagbrain, t1_asl_res, gdc_warp) # Calculate the Jacobian of the distortion correction warp calc_warp_jacobian(oph) # apply the combined distortion correction warp with motion correction # to move asl data, calibrationn images, and scaling factors into # ASL-gridded T1w-aligned space asl_distcorr = (T1w_oph + "/tis_distcorr.nii.gz") moco_xfms = (study_dir + "/" + sub_num + "/ASL/TIs/MoCo/asln2asl0.mat" ) #will this work? concat_xfms = str(Path(moco_xfms).parent / f'{Path(moco_xfms).stem}.cat') # concatenate xfms like in oxford_asl concat_call = f'cat {moco_xfms}/MAT* > {concat_xfms}' sp.run(concat_call, shell=True) # only correcting and transforming the 1st of the calibration images at the moment calib_orig = (study_dir + "/" + sub_num + "/ASL/Calib/Calib0/MTCorr/calib0_mtcorr.nii.gz") calib_distcorr = (study_dir + "/" + sub_num + "/T1w/ASL/Calib/Calib0/DistCorr/calib0_dcorr.nii.gz") calib_inv_xfm = (study_dir + "/" + sub_num + "/ASL/TIs/MoCo/asln2m0.mat/MAT_0000") calib_xfm = (study_dir + "/" + sub_num + "/ASL/TIs/MoCo/calibTOasl1.mat") sfacs_orig = (study_dir + "/" + sub_num + "/ASL/TIs/STCorr/SecondPass/combined_scaling_factors.nii.gz") sfacs_distcorr = (T1w_oph + "/combined_scaling_factors.nii.gz") invert_call = ("convert_xfm -omat " + calib_xfm + " -inverse " + calib_inv_xfm) # print(invert_call) sp.run(invert_call.split(), check=True, stderr=sp.PIPE, stdout=sp.PIPE) apply_distcorr_warp(asl, t1_asl_res, asl_distcorr, oph, concat_xfms, calib_orig, calib_distcorr, calib_xfm, sfacs_orig, sfacs_distcorr)
def main(): # argument handling parser = argparse.ArgumentParser( description= "Create T1-ASL space, estimate PVs, generate CSF ventricle mask") parser.add_argument("study_dir", help="Path of the base study directory.") parser.add_argument("sub_number", help="Subject number.") parser.add_argument( "-c", "--cores", help="Number of cores to use when applying motion correction and " + "other potentially multi-core operations. Default is 1", default=1, type=int, choices=range(1, mp.cpu_count() + 1)) parser.add_argument( "--outdir", help="Name of the directory within which we will store all of the " + "pipeline's outputs in sub-directories. Default is 'hcp_asl'", default="hcp_asl") parser.add_argument( "--interpolation", help="Interpolation order for registrations. This can be any " + "integer from 0-5 inclusive. Default is 3. See scipy's " + "map_coordinates for more details.", default=3, type=int, choices=range(0, 5 + 1)) args = parser.parse_args() study_dir = args.study_dir sub_id = args.sub_number # for debug, re-use intermediate results force_refresh = True sub_base = op.abspath(op.join(study_dir, sub_id)) t1_dir = op.join(sub_base, "T1w") t1_asl_dir = op.join(sub_base, args.outdir, "T1w", "ASL") asl = op.join(sub_base, args.outdir, "ASL", "TIs", "tis.nii.gz") struct = op.join(t1_dir, "T1w_acpc_dc_restore.nii.gz") # Create ASL-gridded version of T1 image t1_asl_grid = op.join(t1_asl_dir, "reg", "ASL_grid_T1w_acpc_dc_restore.nii.gz") if not op.exists(t1_asl_grid) or force_refresh: asl_spc = rt.ImageSpace(asl) t1_spc = rt.ImageSpace(struct) t1_asl_grid_spc = t1_spc.resize_voxels(asl_spc.vox_size / t1_spc.vox_size) nib.save( rt.Registration.identity().apply_to_image(struct, t1_asl_grid_spc), t1_asl_grid) # Create PVEs directory pve_dir = op.join(sub_base, args.outdir, "T1w", "ASL", "PVEs") os.makedirs(pve_dir, exist_ok=True) # Create a ventricle CSF mask in T1 ASL space ventricle_mask = op.join(pve_dir, "vent_csf_mask.nii.gz") if not op.exists(ventricle_mask) or force_refresh: aparc_aseg = op.join(t1_dir, "aparc+aseg.nii.gz") vmask = generate_ventricle_mask(aparc_aseg, t1_asl_grid) rt.ImageSpace.save_like(t1_asl_grid, vmask, ventricle_mask) # Estimate PVs in ASL0 space then register them to ASLT1w space asl2struct = op.join(t1_asl_dir, "TIs", "reg", "asl2struct.mat") pv_gm = op.join(pve_dir, "pve_GM.nii.gz") if not op.exists(pv_gm) or force_refresh: aparc_seg = op.join(t1_dir, "aparc+aseg.nii.gz") pvs_stacked = estimate_pvs(t1_dir, asl, ref2struct=asl2struct, cores=args.cores) # register the PVEs from ASL0 space to ASLT1w space with the # same order of interpolation used to register the ASL series asl2struct = rt.Registration.from_flirt(asl2struct, asl, struct) pvs_stacked = asl2struct.apply_to_image(src=pvs_stacked, ref=t1_asl_grid, order=args.interpolation, cores=args.cores) # Save output with tissue suffix fileroot = op.join(pve_dir, "pve") for idx, suffix in enumerate(['GM', 'WM', 'CSF']): p = "{}_{}.nii.gz".format(fileroot, suffix) rt.ImageSpace.save_like(t1_asl_grid, pvs_stacked.dataobj[..., idx], p)
def main(): # argument handling parser = argparse.ArgumentParser() parser.add_argument("study_dir", help="Path of the base study directory.") parser.add_argument("sub_number", help="Subject number.") parser.add_argument( "-g", "--grads", help="Filename of the gradient coefficients for gradient" + "distortion correction (optional).") parser.add_argument( "-t", "--target", help="Which space we want to register to. Can be either 'asl' for " + "registration to the first volume of the ASL series or " + "'structural' for registration to the T1w image. Default " + " is 'asl'.", default="asl") args = parser.parse_args() study_dir = args.study_dir sub_id = args.sub_number grad_coefficients = args.grads target = args.target # For debug, re-use existing intermediate files force_refresh = True # Input, output and intermediate directories # Create if they do not already exist. sub_base = op.abspath(op.join(study_dir, sub_id)) grad_coefficients = op.abspath(grad_coefficients) pvs_dir = op.join(sub_base, "T1w", "ASL", "PVEs") t1_asl_dir = op.join(sub_base, "T1w", "ASL") distcorr_dir = op.join(sub_base, "ASL", "TIs", "SecondPass", "DistCorr") reg_dir = op.join(sub_base, 'T1w', 'ASL', 'reg') t1_dir = op.join(sub_base, "T1w") asl_dir = op.join(sub_base, "ASL", "TIs", "SecondPass", "STCorr2") asl_out_dir = op.join(t1_asl_dir, "TIs", "DistCorr") calib_out_dir = op.join(t1_asl_dir, "Calib", "Calib0", "DistCorr") [ os.makedirs(d, exist_ok=True) for d in [ pvs_dir, t1_asl_dir, distcorr_dir, reg_dir, asl_out_dir, calib_out_dir ] ] # Images required for processing asl = op.join(asl_dir, "tis_stcorr.nii.gz") struct = op.join(t1_dir, "T1w_acpc_dc_restore.nii.gz") struct_brain = op.join(t1_dir, "T1w_acpc_dc_restore_brain.nii.gz") struct_brain_mask = op.join(t1_dir, "T1w_acpc_dc_restore_brain_mask.nii.gz") asl_vol0 = op.join(asl_dir, "tis_stcorr_vol1.nii.gz") if (not op.exists(asl_vol0) or force_refresh) and target == 'asl': cmd = "fslroi {} {} 0 1".format(asl, asl_vol0) sp.run(cmd.split(" "), check=True) # Create ASL-gridded version of T1 image t1_asl_grid = op.join(t1_dir, "ASL", "reg", "ASL_grid_T1w_acpc_dc_restore.nii.gz") if (not op.exists(t1_asl_grid) or force_refresh) and target == 'asl': asl_spc = rt.ImageSpace(asl) t1_spc = rt.ImageSpace(struct) t1_asl_grid_spc = t1_spc.resize_voxels(asl_spc.vox_size / t1_spc.vox_size) nb.save( rt.Registration.identity().apply_to_image(struct, t1_asl_grid_spc), t1_asl_grid) # Create ASL-gridded version of T1 image t1_asl_grid_mask = op.join( reg_dir, "ASL_grid_T1w_acpc_dc_restore_brain_mask.nii.gz") if (not op.exists(t1_asl_grid_mask) or force_refresh) and target == 'asl': asl_spc = rt.ImageSpace(asl) t1_spc = rt.ImageSpace(struct_brain) t1_asl_grid_spc = t1_spc.resize_voxels(asl_spc.vox_size / t1_spc.vox_size) t1_mask = binarise_image(struct_brain) t1_mask_asl_grid = rt.Registration.identity().apply_to_array( t1_mask, t1_spc, t1_asl_grid_spc) # Re-binarise downsampled mask and save t1_asl_grid_mask_array = binary_fill_holes( t1_mask_asl_grid > 0.25).astype(np.float32) t1_asl_grid_spc.save_image(t1_asl_grid_mask_array, t1_asl_grid_mask) # MCFLIRT ASL using the calibration as reference calib = op.join(sub_base, 'ASL', 'Calib', 'Calib0', 'MTCorr', 'calib0_mtcorr.nii.gz') asl = op.join(sub_base, 'ASL', 'TIs', 'tis.nii.gz') mcdir = op.join(sub_base, 'ASL', 'TIs', 'SecondPass', 'MoCo', 'asln2m0.mat') asl2calib_mc = rt.MotionCorrection.from_mcflirt(mcdir, asl, calib) # Rebase the motion correction to target volume 0 of ASL # The first registration in the series gives us ASL-calibration transform calib2asl0 = asl2calib_mc[0].inverse() asl_mc = rt.chain(asl2calib_mc, calib2asl0) # Generate the gradient distortion correction warp gdc_path = op.join(distcorr_dir, 'fullWarp_abs.nii.gz') if (not op.exists(gdc_path) or force_refresh) and target == 'asl': generate_gdc_warp(asl_vol0, grad_coefficients, distcorr_dir) gdc = rt.NonLinearRegistration.from_fnirt(gdc_path, asl_vol0, asl_vol0, intensity_correct=True, constrain_jac=(0.01, 100)) # Stack the cblipped images together for use with topup pa_sefm, ap_sefm = find_field_maps(study_dir, sub_id) pa_ap_sefms = op.join(distcorr_dir, 'merged_sefms.nii.gz') if (not op.exists(pa_ap_sefms) or force_refresh) and target == 'asl': rt.ImageSpace.save_like( pa_sefm, np.stack( (nb.load(pa_sefm).get_data(), nb.load(ap_sefm).get_data()), axis=-1), pa_ap_sefms) topup_params = op.join(distcorr_dir, 'topup_params.txt') generate_topup_params(topup_params) topup_config = "b02b0.cnf" # Note this file doesn't exist in scope, # but topup knows where to find it # Generate fieldmaps for use with asl_reg (via topup) fmap, fmapmag, fmapmagbrain = [ op.join(distcorr_dir, '{}.nii.gz'.format(s)) for s in ['fmap', 'fmapmag', 'fmapmagbrain'] ] if ((not all([op.exists(p) for p in [fmap, fmapmag, fmapmagbrain]])) or force_refresh) and target == 'asl': generate_fmaps(pa_ap_sefms, topup_params, topup_config, distcorr_dir) # get linear registration from asl to structural if target == 'asl': unreg_img = asl_vol0 elif target == 'structural': # register perfusion-weighted image to structural instead of asl 0 unreg_img = op.join(sub_base, "ASL", "TIs", "SecondPass", "OxfordASL", "native_space", "perfusion.nii.gz") # apply gdc to unreg_img before getting registration to structural # only apply to asl_vol0 as perfusion image has already had gdc applied distcorr_out_dir = asl_out_dir if target == 'structural' else distcorr_dir gdc_tis_vol1_name = op.join(distcorr_out_dir, "gdc_tis_vol1.nii.gz") if (not op.exists(gdc_tis_vol1_name) or force_refresh) and target == 'asl': gdc_tis_vol1 = gdc.apply_to_image(src=unreg_img, ref=unreg_img) unreg_img = gdc_tis_vol1_name nb.save(gdc_tis_vol1, unreg_img) # Initial (linear) asl to structural registration, via first round of asl_reg asl2struct_initial_path = op.join( reg_dir, 'asl2struct_init.mat' if target == 'asl' else 'asl2struct_final.mat') if not op.exists(asl2struct_initial_path) or force_refresh: generate_asl2struct_initial(unreg_img, reg_dir, struct, struct_brain) asl2struct_initial_path_temp = op.join(reg_dir, 'asl2struct.mat') os.replace(asl2struct_initial_path_temp, asl2struct_initial_path) asl2struct_initial = rt.Registration.from_flirt(asl2struct_initial_path, src=unreg_img, ref=struct) # Get brain mask in asl space if target == 'asl': mask_name = op.join(reg_dir, "asl_vol1_mask_init.nii.gz") else: mask_name = op.join(reg_dir, "asl_vol1_mask_final.nii.gz") if not op.exists(mask_name) or force_refresh: asl_mask = generate_asl_mask(struct_brain, unreg_img, asl2struct_initial) rt.ImageSpace.save_like(unreg_img, asl_mask, mask_name) # Brain extract volume 0 of asl series gdc_unreg_img_brain = op.join(sub_base, "ASL", "TIs", "SecondPass", "DistCorr", "gdc_tis_vol1_brain.nii.gz") if (not op.exists(gdc_unreg_img_brain) or force_refresh) and target == 'asl': bet(unreg_img, gdc_unreg_img_brain) unreg_img = gdc_unreg_img_brain # Generate a binary WM mask in the space of the T1 (using FS' aparc+aseg) wmmask = op.join(sub_base, "T1w", "wmmask.nii.gz") if (not op.exists(wmmask) or force_refresh) and target == 'asl': aparc_seg = op.join(t1_dir, "aparc+aseg.nii.gz") wmmask_img = generate_wmmask(aparc_seg) rt.ImageSpace.save_like(struct, wmmask_img, wmmask) # Generate the EPI distortion correction warp via asl_reg --final epi_dc_path = op.join( distcorr_dir, 'asl2struct_warp_init.nii.gz' if target == 'asl' else 'asl2struct_warp_final.nii.gz') if not op.exists(epi_dc_path) or force_refresh: epi_dc_path_temp = op.join(distcorr_dir, 'asl2struct_warp.nii.gz') generate_epidc_warp(unreg_img, struct, struct_brain, mask_name, wmmask, asl2struct_initial, fmap, fmapmag, fmapmagbrain, distcorr_dir) # rename warp so it isn't overwritten os.replace(epi_dc_path_temp, epi_dc_path) epi_dc = rt.NonLinearRegistration.from_fnirt(epi_dc_path, mask_name, struct, intensity_correct=True, constrain_jac=(0.01, 100)) # if ending in asl space, chain struct2asl transformation if target == 'asl': struct2asl_aslreg = op.join(distcorr_out_dir, "struct2asl.mat") struct2asl_aslreg = rt.Registration.from_flirt(struct2asl_aslreg, src=struct, ref=asl) epi_dc = rt.chain(epi_dc, struct2asl_aslreg) # Final ASL transforms: moco, grad dc, # epi dc (incorporating asl->struct reg) asl = op.join(asl_dir, "tis_stcorr.nii.gz") reference = t1_asl_grid if target == 'structural' else asl asl_outpath = op.join(distcorr_out_dir, "tis_distcorr.nii.gz") if not op.exists(asl_outpath) or force_refresh: asl2struct_mc_dc = rt.chain(asl_mc, gdc, epi_dc) asl_corrected = asl2struct_mc_dc.apply_to_image(src=asl, ref=reference, cores=mp.cpu_count()) nb.save(asl_corrected, asl_outpath) # Final calibration transforms: calib->asl, grad dc, # epi dc (incorporating asl->struct reg) calib_outpath = op.join(calib_out_dir, "calib0_dcorr.nii.gz") if (not op.exists(calib_outpath) or force_refresh) and target == 'structural': calib2struct_dc = rt.chain(calib2asl0, gdc, epi_dc) calib_corrected = calib2struct_dc.apply_to_image(src=calib, ref=reference) nb.save(calib_corrected, calib_outpath) # Final scaling factors transforms: moco, grad dc, # epi dc (incorporating asl->struct reg) sfs_name = op.join(asl_dir, "combined_scaling_factors.nii.gz") sfs_outpath = op.join(distcorr_out_dir, "combined_scaling_factors.nii.gz") if not op.exists(sfs_outpath) or force_refresh: # don't chain transformations together if we don't have to try: asl2struct_mc_dc except NameError: asl2struct_mc_dc = rt.chain(asl_mc, gdc, epi_dc) sfs_corrected = asl2struct_mc_dc.apply_to_image(src=sfs_name, ref=reference, cores=mp.cpu_count()) nb.save(sfs_corrected, sfs_outpath) # create ti image in asl space slicedt = 0.059 tis = [1.7, 2.2, 2.7, 3.2, 3.7] sliceband = 10 ti_asl = op.join(sub_base, "ASL", "TIs", "timing_img.nii.gz") if (not op.exists(ti_asl) or force_refresh) and target == 'asl': create_ti_image(asl, tis, sliceband, slicedt, ti_asl) # transform ti image into t1 space ti_t1 = op.join(t1_asl_dir, "timing_img.nii.gz") if (not op.exists(ti_t1) or force_refresh) and target == 'structural': asl2struct = op.join(distcorr_dir, "asl2struct.mat") asl2struct = rt.Registration.from_flirt(asl2struct, src=asl, ref=struct) ti_t1_img = asl2struct.apply_to_image(src=ti_asl, ref=reference) nb.save(ti_t1_img, ti_t1)
def enforcer(ref, struct2ref, **kwargs): # Reference image path if (not isinstance(ref, rt.ImageSpace)): ref = rt.ImageSpace(ref) # If given a fslanat dir we can load the structural image in if kwargs.get('fslanat'): if not check_anat_dir(kwargs['fslanat']): raise RuntimeError( "fslanat is not complete: it must contain " "FAST output and a first_results subdirectory") kwargs['fastdir'] = kwargs['fslanat'] kwargs['firstdir'] = op.join(kwargs['fslanat'], 'first_results') # If no struct image given, try and pull it out from the anat dir # But, if it has been cropped relative to original T1, then give # warning (as we will not be able to convert FLIRT to world-world) if not kwargs.get('struct'): if kwargs.get('flirt'): matpath = glob.glob( op.join(kwargs['fslanat'], '*nonroi2roi.mat'))[0] nonroi2roi = np.loadtxt(matpath) if np.any(np.abs(nonroi2roi[0:3, 3])): print( "Warning: T1 was cropped relative to T1_orig within" + " fslanat dir.\n Please ensure the struct2ref FLIRT" + " matrix is referenced to T1, not T1_orig.") s = op.join(kwargs['fslanat'], 'T1.nii.gz') kwargs['struct'] = s if not op.isfile(s): raise RuntimeError( "Could not find T1.nii.gz in the fslanat dir") # Structural to reference transformation. Either as array, path # to file containing matrix, or regtricks Registration object if not any([ type(struct2ref) is str, type(struct2ref) is np.ndarray, type(struct2ref) is rt.Registration ]): raise RuntimeError( "struct2ref transform must be given (either path,", " np.array or regtricks Registration object)") else: s2r = struct2ref if (type(s2r) is str): if s2r == 'I': matrix = np.identity(4) else: _, matExt = op.splitext(s2r) try: if matExt in ['.txt', '.mat']: matrix = np.loadtxt(s2r, dtype=NP_FLOAT) elif matExt in ['.npy', 'npz', '.pkl']: matrix = np.load(s2r) else: matrix = np.fromfile(s2r, dtype=NP_FLOAT) except Exception as e: warnings.warn("""Could not load struct2ref matrix. File should be any type valid with numpy.load().""" ) raise e struct2ref = matrix # If FLIRT transform we need to do some clever preprocessing # We then set the flirt flag to false again (otherwise later steps will # repeat the tricks and end up reverting to the original - those steps don't # need to know what we did here, simply that it is now world-world again) if kwargs.get('flirt'): if not kwargs.get('struct'): raise RuntimeError( "If using a FLIRT transform, the path to the" " structural image must also be given") struct2ref = rt.Registration.from_flirt(struct2ref, kwargs['struct'], ref).src2ref kwargs['flirt'] = False elif isinstance(struct2ref, rt.Registration): struct2ref = struct2ref.src2ref assert isinstance(struct2ref, np.ndarray), 'should have cast struc2ref to np.array' # Processor cores if not kwargs.get('cores'): kwargs['cores'] = multiprocessing.cpu_count() # Supersampling factor sup = kwargs.get('super') if sup is not None: try: if (type(sup) is list) and (len(sup) == 3): sup = np.array([int(s) for s in sup]) else: sup = int(sup[0]) sup = np.array([sup for _ in range(3)]) if type(sup) is not np.ndarray: raise RuntimeError() except: raise RuntimeError("-super must be a value or list of 3" + " values of int type") return ref, struct2ref, kwargs
def extract_fs_pvs(aparcseg, surf_dict, ref_spc, ref2struct=None, cores=1): """ Extract and layer PVs according to tissue type, taken from a FS aparc+aseg. Results are stored in ASL-gridded T1 space. Args: aparcseg: path to aparc+aseg file surf_dict: dict with LWS/LPS/RWS/RPS keys, paths to those surfaces ref_spc: space in which to estimate (ie, ASL-gridded T1) superfactor: supersampling factor for intermediate steps cores: number CPU cores to use, default is 1 Returns: nibabel Nifti object """ ref_spc = rt.ImageSpace(ref_spc) aseg_spc = nib.load(aparcseg) aseg = aseg_spc.dataobj aseg_spc = rt.ImageSpace(aseg_spc) # Estimate cortical PVs, loading a struct2ref registration if provided # If not provided, an identity transform is used if ref2struct: struct2ref_reg = rt.Registration.from_flirt(ref2struct, ref_spc, aseg_spc).inverse() cortex = estimate_cortex(ref=ref_spc, struct2ref=struct2ref_reg.src2ref, cores=cores, **surf_dict) else: struct2ref_reg = rt.Registration.identity() cortex = estimate_cortex(ref=ref_spc, struct2ref='I', cores=cores, **surf_dict) # Extract PVs from aparcseg segmentation. Subcortical structures go into # a dict keyed according to their name, whereas general WM/GM are # grouped into the vol_pvs array to_stack = {} vol_pvs = np.zeros((aseg_spc.size.prod(), 3), dtype=np.float32) for label in np.unique(aseg): tissue = SUBCORT_LUT.get(label) if not tissue: tissue = CTX_LUT(label) if tissue: # print(f"Label {label} assigned to {tissue}.") mask = (aseg == label) if tissue == "WM": vol_pvs[mask.flatten(), 1] = 1 elif tissue == "GM": vol_pvs[mask.flatten(), 0] = 1 elif tissue == "CSF": pass else: to_stack[tissue] = mask.astype(np.float32) elif label not in IGNORE: print("Did not assign tissue for aseg/aparc label:", label) # Super-resolution resampling for the vol_pvs, a la applywarp. # 0: GM, 1: WM, 2: CSF, always in the LAST dimension of an array vol_pvs = struct2ref_reg.apply_to_array(vol_pvs.reshape(*aseg.shape, 3), aseg_spc, ref_spc, order=1, cores=cores) vol_pvs = vol_pvs.reshape(-1, 3) vol_pvs[:, 2] = np.maximum(0, 1 - vol_pvs[:, :2].sum(1)) vol_pvs[vol_pvs[:, 2] < 1e-2, 2] = 0 vol_pvs /= vol_pvs.sum(1)[:, None] vol_pvs = vol_pvs.reshape(*ref_spc.size, 3) # Stack the subcortical structures into a big 4D volume to resample them # all at once, then put them back into a dict keys, values = list(to_stack.keys()), list(to_stack.values()) del to_stack subcorts = struct2ref_reg.apply_to_array(np.stack(values, axis=-1), aseg_spc, ref_spc, order=1, cores=cores) subcorts = np.moveaxis(subcorts, 3, 0) to_stack = dict(zip(keys, subcorts)) # Add the cortical and vol PV estimates into the dict, stack them in # a sneaky way (see the stack_images function) to_stack['cortex_GM'] = cortex[..., 0] to_stack['cortex_WM'] = cortex[..., 1] to_stack['cortex_nonbrain'] = cortex[..., 2] to_stack['vol_GM'] = vol_pvs[..., 0] to_stack['vol_WM'] = vol_pvs[..., 1] to_stack['vol_CSF'] = vol_pvs[..., 2] result = stack_images(to_stack) return ref_spc.make_nifti(result.reshape((*ref_spc.size, 3)))
def extract_fs_pvs(aparcseg, surf_dict, t1, asl, superfactor=2, cores=mp.cpu_count()): """ Extract and layer PVs according to tissue type, taken from a FS aparc+aseg. Results are stored in ASL-gridded T1 space. Args: aparcseg: path to aparc+aseg file surf_dict: dict with LWS/LPS/RWS/RPS keys, paths to those surfaces t1: path to T1 file asl: path to ASL file (for setting resolution) superfactor: supersampling factor for intermediate steps cores: number CPU cores to use Returns: nibabel Nifti object """ asl_spc = rt.ImageSpace(asl) t1_spc = rt.ImageSpace(t1) ref_spc = t1_spc.resize_voxels(asl_spc.vox_size / t1_spc.vox_size) high_spc = ref_spc.resize_voxels(1/superfactor, 'ceil') aseg_spc = nib.load(aparcseg) aseg = aseg_spc.dataobj aseg_spc = rt.ImageSpace(aseg_spc) # Estimate cortical PVs # FIXME: allow tob to accept imagespace directly here with tempfile.TemporaryDirectory() as td: ref_path = op.join(td, 'ref.nii.gz') ref_spc.touch(ref_path) cortex = estimate_cortex(ref=ref_path, struct2ref='I', superfactor=1, cores=cores, **surf_dict) # Extract PVs from aparcseg segmentation. Subcortical structures go into # a dict keyed according to their name, whereas general WM/GM are # grouped into the vol_pvs array to_stack = {} vol_pvs = np.zeros((aseg_spc.size.prod(), 3), dtype=np.float32) for label in np.unique(aseg): tissue = SUBCORT_LUT.get(label) if not tissue: tissue = CTX_LUT(label) if tissue: mask = (aseg == label) if tissue == "WM": vol_pvs[mask.flatten(),1] = 1 elif tissue == "GM": vol_pvs[mask.flatten(),0] = 1 elif tissue == "CSF": pass else: to_stack[tissue] = mask.astype(np.float32) elif label not in IGNORE: print("Did not assign aseg/aparc label:", label) # Super-resolution resampling for the vol_pvs, a la applywarp. # We use an identity transform as we don't actually want to shift the data # 0: GM, 1: WM, 2: CSF, always in the LAST dimension of an array reg = rt.Registration.identity() vol_pvs = reg.apply_to_array(vol_pvs.reshape(*aseg.shape, 3), aseg_spc, high_spc, order=1, cores=cores) vol_pvs = (_sum_array_blocks(vol_pvs, 3 * [superfactor] + [1]) / (superfactor ** 3)) vol_pvs = vol_pvs.reshape(-1,3) vol_pvs[:,2] = np.maximum(0, 1 - vol_pvs[:,:2].sum(1)) vol_pvs[vol_pvs[:,2] < 1e-2, 2] = 0 vol_pvs /= vol_pvs.sum(1)[:,None] vol_pvs = vol_pvs.reshape(*ref_spc.size, 3) # Stack the subcortical structures into a big 4D volume to resample them # all at once, then put them back into a dict keys, values = list(to_stack.keys()), list(to_stack.values()) del to_stack subcorts = reg.apply_to_array(np.stack(values, axis=-1), aseg_spc, high_spc, order=1, cores=cores) subcorts = (_sum_array_blocks(subcorts, 3 * [superfactor] + [1]) / (superfactor ** 3)) subcorts = np.moveaxis(subcorts, 3, 0) to_stack = dict(zip(keys, subcorts)) # Add the cortical and vol PV estimates into the dict, stack them in # a sneaky way (see the stack_images function) to_stack['cortex_GM'] = cortex[...,0] to_stack['cortex_WM'] = cortex[...,1] to_stack['cortex_nonbrain'] = cortex[...,2] to_stack['vol_GM'] = vol_pvs[...,0] to_stack['vol_WM'] = vol_pvs[...,1] to_stack['vol_CSF'] = vol_pvs[...,2] result = stack_images(to_stack) # Squash small values (looks like dodgy output otherwise, but # is actually just an artefact of resampling) pvmin = result.min() if pvmin > 1e-9: result[result < 1e-6] = 0 return ref_spc.make_nifti(result.reshape((*ref_spc.size, 3)))
sys.argv[1:] = cmd.split() parser = argparse.ArgumentParser(usage=usage) parser.add_argument("--aparcseg", required=True) parser.add_argument("--LWS", required=True) parser.add_argument("--LPS", required=True) parser.add_argument("--RPS", required=True) parser.add_argument("--RWS", required=True) parser.add_argument("--t1", required=True) parser.add_argument("--asl", required=True) parser.add_argument("--out", required=True) parser.add_argument("--stack", action="store_true") parser.add_argument("--super", default=2, type=int) parser.add_argument("--cores", default=mp.cpu_count(), type=int) args = parser.parse_args(sys.argv[1:]) surf_dict = dict([ (k, getattr(args, k)) for k in ['LWS', 'LPS', 'RPS', 'RWS'] ]) pvs = extract_fs_pvs(args.aparcseg, surf_dict, args.t1, args.asl, args.super, args.cores) if args.stack: nib.save(pvs, args.out) else: opath = pathlib.Path(args.out) spc = rt.ImageSpace(pvs) pvs = pvs.dataobj for idx,tiss in enumerate(["GM", "WM", "CSF"]): n = opath.parent.joinpath(opath.stem.rsplit('.', 1)[0] + f"_{tiss}" + "".join(opath.suffixes)) spc.save_image(pvs[...,idx], n.as_posix())