From 97907e5efcfd28de307cee2aa996c04e4f820a53 Mon Sep 17 00:00:00 2001 From: Philippe Karan Date: Wed, 19 Jun 2024 12:22:53 -0400 Subject: [PATCH 01/20] Adding script and module --- scilpy/tractanalysis/fixel_density.py | 88 +++++++++++++ scripts/scil_bundle_fixel_analysis.py | 181 ++++++++++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 scilpy/tractanalysis/fixel_density.py create mode 100644 scripts/scil_bundle_fixel_analysis.py diff --git a/scilpy/tractanalysis/fixel_density.py b/scilpy/tractanalysis/fixel_density.py new file mode 100644 index 000000000..0a1a8877e --- /dev/null +++ b/scilpy/tractanalysis/fixel_density.py @@ -0,0 +1,88 @@ +import itertools +import multiprocessing +import numpy as np + +from dipy.io.streamline import load_tractogram +from scilpy.tractanalysis.grid_intersections import grid_intersections + + +def _compute_fixel_density_parallel(args): + peaks = args[0] + max_theta = args[1] + bundle = args[2] + + sft = load_tractogram(bundle, 'same') + sft.to_vox() + sft.to_corner() + + fixel_density_maps = np.zeros((peaks.shape[:-1]) + (5,)) + + min_cos_theta = np.cos(np.radians(max_theta)) + + all_crossed_indices = grid_intersections(sft.streamlines) + for crossed_indices in all_crossed_indices: + segments = crossed_indices[1:] - crossed_indices[:-1] + seg_lengths = np.linalg.norm(segments, axis=1) + + # Remove points where the segment is zero. + # This removes numpy warnings of division by zero. + non_zero_lengths = np.nonzero(seg_lengths)[0] + segments = segments[non_zero_lengths] + seg_lengths = seg_lengths[non_zero_lengths] + + # Those starting points are used for the segment vox_idx computations + seg_start = crossed_indices[non_zero_lengths] + vox_indices = (seg_start + (0.5 * segments)).astype(int) + + normalized_seg = np.reshape(segments / seg_lengths[..., None], (-1, 3)) + + for vox_idx, seg_dir in zip(vox_indices, normalized_seg): + vox_idx = tuple(vox_idx) + peaks_at_idx = peaks[vox_idx].reshape((5, 3)) + + cos_theta = np.abs(np.dot(seg_dir.reshape((-1, 3)), + peaks_at_idx.T)) + + if (cos_theta > min_cos_theta).any(): + lobe_idx = np.argmax(np.squeeze(cos_theta), axis=0) # (n_segs) + # TODO Change that for commit weight if given + fixel_density_maps[vox_idx][lobe_idx] += 1 + + return fixel_density_maps + + +def compute_fixel_density(peaks, bundles, max_theta=45, nbr_processes=None): + """Compute the fixel density per bundle. Can use parallel processing. + + Parameters + ---------- + peaks: np.ndarray (x, y, z, 15) + Five principal fiber orientations for each voxel. + bundles : list or np.array (N) + List of (N) paths to bundles. + max_theta : int, optional + Maximum angle between streamline and peak to be associated. + nbr_processes : int, optional + The number of subprocesses to use. + Default: multiprocessing.cpu_count() + + Returns + ------- + fixel_density : np.ndarray (x, y, z, 5, N) + Density per fixel per bundle. + """ + nbr_processes = multiprocessing.cpu_count() \ + if nbr_processes is None or nbr_processes <= 0 \ + else nbr_processes + + pool = multiprocessing.Pool(nbr_processes) + results = pool.map(_compute_fixel_density_parallel, + zip(itertools.repeat(peaks), + itertools.repeat(max_theta), + bundles)) + pool.close() + pool.join() + + fixel_density = np.moveaxis(np.asarray(results), 0, -1) + + return fixel_density diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py new file mode 100644 index 000000000..8580724b2 --- /dev/null +++ b/scripts/scil_bundle_fixel_analysis.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Analyze bundles at the fixel level, producing various results: + +- +- +- + +""" + +import argparse +import nibabel as nib +import numpy as np +from pathlib import Path + +from scilpy.io.utils import (add_overwrite_arg, add_processes_arg, + add_reference_arg) +from scilpy.tractanalysis.fixel_density import compute_fixel_density + + +def _build_arg_parser(): + p = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawTextHelpFormatter) + p.add_argument('in_peaks', + help='Path of the fODF peaks. The peaks are expected to be ' + 'given as unit directions.') + p.add_argument('out_folder', + help='Path of the output folder for txt, png, masks and ' + 'measures.') + + p.add_argument('--in_bundles', nargs='+', default=[], + action='append', required=True, + help='Path to the bundle trk for where to analyze.') + + p.add_argument('--abs_thr', default=1, type=int, + help='Value of density maps threshold to obtain density ' + 'masks, in number of streamlines. Any number of ' + 'streamlines above or equal to this value will pass ' + 'the absolute threshold test [%(default)s].') + + p.add_argument('--rel_thr', default=0.01, type=float, + help='Value of density maps threshold to obtain density ' + 'masks, as a ratio of the normalized density. Any ' + 'normalized density above or equal to this value will ' + 'pass the relative threshold test [%(default)s].') + + p.add_argument('--max_theta', default=45, + help='Maximum angle between streamline and peak to be ' + 'associated [%(default)s].') + + p.add_argument('--separate_bundles', action='store_true', + help='If set, save the density maps for each bundle ' + 'separately instead of all in one file.') + + p.add_argument('--save_masks', action='store_true', + help='If set, save the density masks for each bundle.') + + p.add_argument('--select_single_bundle', action='store_true', + help='If set, select the voxels where only one bundle is ' + 'present and save the corresponding masks ' + '(if save_masks), separated by bundles.') + + p.add_argument('--norm', default="voxel", choices=["fixel", "voxel"], + help='Way of normalizing the density maps. If fixel, ' + 'will normalize the maps per fixel, in each voxel. ' + 'If voxel, will normalize the maps per voxel. ' + '[%(default)s].') + + add_overwrite_arg(p) + add_processes_arg(p) + add_reference_arg(p) + return p + + +def main(): + parser = _build_arg_parser() + args = parser.parse_args() + + out_folder = Path(args.out_folder) + + # Load the data + peaks_img = nib.load(args.in_peaks) + peaks = peaks_img.get_fdata() + affine = peaks_img.affine + + # Compute NuFo SF from peaks + if args.select_single_bundle: + is_first_peak = np.sum(peaks[..., 0:3], axis=-1) != 0 + is_second_peak = np.sum(peaks[..., 3:6], axis=-1) == 0 + nufo_sf = np.logical_and(is_first_peak, is_second_peak) + + bundles = [] + bundles_names = [] + for bundle in args.in_bundles[0]: + bundles.append(bundle) + bundles_names.append(Path(bundle).name.split(".")[0]) + + fixel_density_maps = compute_fixel_density(peaks, bundles, args.max_theta, + nbr_processes=args.nbr_processes) + + # This applies a threshold on the number of streamlines. + fixel_density_masks_abs = fixel_density_maps >= args.abs_thr + + # Normalizing the density maps + fixel_sum = np.sum(fixel_density_maps, axis=-1) + voxel_sum = np.sum(fixel_sum, axis=-1) + + for i, (bundle, bundle_name) in enumerate(zip(bundles, bundles_names)): + + if args.norm == "voxel": + fixel_density_maps[..., 0, i] /= voxel_sum + fixel_density_maps[..., 1, i] /= voxel_sum + fixel_density_maps[..., 2, i] /= voxel_sum + fixel_density_maps[..., 3, i] /= voxel_sum + fixel_density_maps[..., 4, i] /= voxel_sum + + elif args.norm == "fixel": + fixel_density_maps[..., i] /= fixel_sum + + if args.separate_bundles: + nib.save(nib.Nifti1Image(fixel_density_maps[..., i], affine), + out_folder / "fixel_density_map_{}.nii.gz".format(bundle_name)) + + if not args.separate_bundles: + nib.save(nib.Nifti1Image(fixel_density_maps, affine), + out_folder / "fixel_density_maps.nii.gz") + bundles_idx = np.arange(0, len(bundles_names), 1) + lookup_table = np.array([bundles_names, bundles_idx]) + np.savetxt(out_folder / "bundles_lookup_table.txt", + lookup_table, fmt='%s') + + # This applies a threshold on the normalized density (percentage) + fixel_density_masks_rel = fixel_density_maps >= args.rel_thr + + # Compute the fixel density masks from the rel and abs versions + fixel_density_masks = fixel_density_masks_rel * fixel_density_masks_abs + + # Compute number of bundles per fixel + nb_bundles_per_fixel = np.sum(fixel_density_masks, axis=-1) + # Compute a mask of the present of each bundle + nb_unique_bundles_per_fixel = np.where(np.sum(fixel_density_masks, + axis=-2) > 0, 1, 0) + # Compute number of bundles per fixel by taking the sum of the mask + nb_bundles_per_voxel = np.sum(nb_unique_bundles_per_fixel, axis=-1) + + if args.save_masks: + for i, (bundle, bundle_name) in enumerate(zip(bundles, bundles_names)): + + if args.separate_bundles: + nib.save(nib.Nifti1Image(fixel_density_masks[..., i].astype(np.uint8), affine), + out_folder / "fixel_density_mask_{}.nii.gz".format(bundle_name)) + + if args.select_single_bundle: + # Single-fiber single-bundle voxels + single_bundle_per_voxel = nb_bundles_per_voxel == 1 + # Making sure we also have single-fiber voxels only + single_bundle_per_voxel *= nufo_sf + + nib.save(nib.Nifti1Image(single_bundle_per_voxel.astype(np.uint8), + affine), + out_folder / "bundle_mask_only_WM.nii.gz") + + bundle_mask = fixel_density_masks[..., 0, i] * single_bundle_per_voxel + nib.save(nib.Nifti1Image(bundle_mask.astype(np.uint8), affine), + out_folder / "bundle_mask_only_{}.nii.gz".format(bundle_name)) + + if not args.separate_bundles: + nib.save(nib.Nifti1Image(fixel_density_masks.astype(np.uint8), + affine), + out_folder / "fixel_density_masks.nii.gz") + + nib.save(nib.Nifti1Image(nb_bundles_per_fixel.astype(np.uint8), + affine), out_folder / "nb_bundles_per_fixel.nii.gz") + + nib.save(nib.Nifti1Image(nb_bundles_per_voxel.astype(np.uint8), + affine), out_folder / "nb_bundles_per_voxel.nii.gz") + + +if __name__ == "__main__": + main() From 319e7273b0292a73ee0d20f0acde8c6af356a544 Mon Sep 17 00:00:00 2001 From: Philippe Karan Date: Wed, 19 Jun 2024 18:16:30 -0400 Subject: [PATCH 02/20] Cleaning the code --- scilpy/tractanalysis/fixel_density.py | 56 ++++++- scripts/scil_bundle_fixel_analysis.py | 215 +++++++++++++------------- 2 files changed, 157 insertions(+), 114 deletions(-) diff --git a/scilpy/tractanalysis/fixel_density.py b/scilpy/tractanalysis/fixel_density.py index 0a1a8877e..fcbe1e0d6 100644 --- a/scilpy/tractanalysis/fixel_density.py +++ b/scilpy/tractanalysis/fixel_density.py @@ -6,7 +6,7 @@ from scilpy.tractanalysis.grid_intersections import grid_intersections -def _compute_fixel_density_parallel(args): +def _fixel_density_parallel(args): peaks = args[0] max_theta = args[1] bundle = args[2] @@ -51,8 +51,8 @@ def _compute_fixel_density_parallel(args): return fixel_density_maps -def compute_fixel_density(peaks, bundles, max_theta=45, nbr_processes=None): - """Compute the fixel density per bundle. Can use parallel processing. +def fixel_density(peaks, bundles, max_theta=45, nbr_processes=None): + """Compute the fixel density map per bundle. Can use parallel processing. Parameters ---------- @@ -76,7 +76,7 @@ def compute_fixel_density(peaks, bundles, max_theta=45, nbr_processes=None): else nbr_processes pool = multiprocessing.Pool(nbr_processes) - results = pool.map(_compute_fixel_density_parallel, + results = pool.map(_fixel_density_parallel, zip(itertools.repeat(peaks), itertools.repeat(max_theta), bundles)) @@ -86,3 +86,51 @@ def compute_fixel_density(peaks, bundles, max_theta=45, nbr_processes=None): fixel_density = np.moveaxis(np.asarray(results), 0, -1) return fixel_density + + +def maps_to_masks(maps, abs_thr, rel_thr, norm, nb_bundles): + """Compute the fixel density masks from fixel density maps. + + Parameters + ---------- + maps : np.ndarray (x, y, z, 5, N) + Density per fixel per bundle. + abs_thr : int + Value of density maps threshold to obtain density masks, in number of + streamlines. + rel_thr : float + Value of density maps threshold to obtain density masks, as a ratio of + the normalized density. Must be between 0 and 1. + norm : string, ["fixel", "voxel"] + Way of normalizing the density maps. If fixel, will normalize the maps + per fixel, in each voxel. If voxel, will normalize the maps per voxel. + nb_bundles : int (N) + Number of bundles (N). + + Returns + ------- + masks : np.ndarray (x, y, z, 5, N) + Density masks per fixel per bundle. + """ + # Apply a threshold on the number of streamlines + masks_abs = maps >= abs_thr + + # Normalizing the density maps per voxel or fixel + fixel_sum = np.sum(maps, axis=-1) + voxel_sum = np.sum(fixel_sum, axis=-1) + for i in range(nb_bundles): + if norm == "voxel": + maps[..., 0, i] /= voxel_sum + maps[..., 1, i] /= voxel_sum + maps[..., 2, i] /= voxel_sum + maps[..., 3, i] /= voxel_sum + maps[..., 4, i] /= voxel_sum + elif norm == "fixel": + maps[..., i] /= fixel_sum + + # Apply a threshold on the normalized density (percentage) + masks_rel = maps >= rel_thr + # Compute the fixel density masks from the rel and abs versions + masks = masks_rel * masks_abs + + return masks.astype(np.uint8) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 8580724b2..4e9d2455d 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -14,62 +14,69 @@ import numpy as np from pathlib import Path -from scilpy.io.utils import (add_overwrite_arg, add_processes_arg, - add_reference_arg) -from scilpy.tractanalysis.fixel_density import compute_fixel_density +from scilpy.io.utils import (add_overwrite_arg, add_processes_arg) +from scilpy.tractanalysis.fixel_density import (fixel_density, maps_to_masks) def _build_arg_parser(): p = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawTextHelpFormatter) p.add_argument('in_peaks', - help='Path of the fODF peaks. The peaks are expected to be ' - 'given as unit directions.') - p.add_argument('out_folder', - help='Path of the output folder for txt, png, masks and ' - 'measures.') + help='Path of the peaks. The peaks are expected to be ' + 'given as unit directions. \nTo get these from fODF ' + 'or SH data, use the script scil_fodf_metrics.py ' + '\nwith the abs_peaks_and_values option.') - p.add_argument('--in_bundles', nargs='+', default=[], - action='append', required=True, - help='Path to the bundle trk for where to analyze.') + p.add_argument('--in_bundles', nargs='+', action='append', required=True, + help='List of paths of the bundles (.trk) to analyze.') - p.add_argument('--abs_thr', default=1, type=int, + p.add_argument('--in_bundles_names', nargs='+', action='append', + help='List of the names of the bundles, in the same order ' + 'as they were given. \nIf this argument is not used, ' + 'the script assumes that the name of the bundle \nis ' + 'its filename without extensions.') + + g = p.add_argument_group(title='Mask parameters') + + g.add_argument('--abs_thr', default=1, type=int, help='Value of density maps threshold to obtain density ' - 'masks, in number of streamlines. Any number of ' - 'streamlines above or equal to this value will pass ' + 'masks, in number of streamlines. \nAny number of ' + 'streamlines above or equal this value will pass ' 'the absolute threshold test [%(default)s].') - p.add_argument('--rel_thr', default=0.01, type=float, + g.add_argument('--rel_thr', default=0.01, type=float, help='Value of density maps threshold to obtain density ' - 'masks, as a ratio of the normalized density. Any ' - 'normalized density above or equal to this value will ' - 'pass the relative threshold test [%(default)s].') + 'masks, as a ratio of the normalized density ' + '\nAny normalized density above or equal to ' + 'this value will pass the relative threshold test. ' + '\nMust be between 0 and 1 [%(default)s].') + + g.add_argument('--norm', default="voxel", choices=["fixel", "voxel"], + help='Way of normalizing the density maps. If fixel, ' + 'will normalize the maps per fixel, \nin each voxel. ' + 'If voxel, will normalize the maps per voxel. ' + '[%(default)s]') p.add_argument('--max_theta', default=45, help='Maximum angle between streamline and peak to be ' 'associated [%(default)s].') - p.add_argument('--separate_bundles', action='store_true', + p.add_argument('--split_bundles', action='store_true', help='If set, save the density maps for each bundle ' - 'separately instead of all in one file.') + 'separately \ninstead of all in one file.') - p.add_argument('--save_masks', action='store_true', - help='If set, save the density masks for each bundle.') + p.add_argument('--split_fixels', action='store_true', + help='If set, save the density maps for each fixel ' + 'separately \ninstead of all in one file.') - p.add_argument('--select_single_bundle', action='store_true', - help='If set, select the voxels where only one bundle is ' - 'present and save the corresponding masks ' - '(if save_masks), separated by bundles.') - - p.add_argument('--norm', default="voxel", choices=["fixel", "voxel"], - help='Way of normalizing the density maps. If fixel, ' - 'will normalize the maps per fixel, in each voxel. ' - 'If voxel, will normalize the maps per voxel. ' - '[%(default)s].') + p.add_argument('--single_bundle', action='store_true', + help='If set, will save the single-fiber single-bundle ' + 'masks as well. \nThese are obtained by ' + 'selecting the voxels where only one bundle is ' + 'present \n(and one fiber/fixel).') add_overwrite_arg(p) add_processes_arg(p) - add_reference_arg(p) return p @@ -77,8 +84,6 @@ def main(): parser = _build_arg_parser() args = parser.parse_args() - out_folder = Path(args.out_folder) - # Load the data peaks_img = nib.load(args.in_peaks) peaks = peaks_img.get_fdata() @@ -90,91 +95,81 @@ def main(): is_second_peak = np.sum(peaks[..., 3:6], axis=-1) == 0 nufo_sf = np.logical_and(is_first_peak, is_second_peak) + # Extract bundles and names bundles = [] bundles_names = [] for bundle in args.in_bundles[0]: bundles.append(bundle) bundles_names.append(Path(bundle).name.split(".")[0]) + if args.in_bundles_names[0] != []: # If names are given + bundles_names = args.in_bundles_names[0] - fixel_density_maps = compute_fixel_density(peaks, bundles, args.max_theta, - nbr_processes=args.nbr_processes) - - # This applies a threshold on the number of streamlines. - fixel_density_masks_abs = fixel_density_maps >= args.abs_thr - - # Normalizing the density maps - fixel_sum = np.sum(fixel_density_maps, axis=-1) - voxel_sum = np.sum(fixel_sum, axis=-1) - - for i, (bundle, bundle_name) in enumerate(zip(bundles, bundles_names)): - - if args.norm == "voxel": - fixel_density_maps[..., 0, i] /= voxel_sum - fixel_density_maps[..., 1, i] /= voxel_sum - fixel_density_maps[..., 2, i] /= voxel_sum - fixel_density_maps[..., 3, i] /= voxel_sum - fixel_density_maps[..., 4, i] /= voxel_sum - - elif args.norm == "fixel": - fixel_density_maps[..., i] /= fixel_sum - - if args.separate_bundles: - nib.save(nib.Nifti1Image(fixel_density_maps[..., i], affine), - out_folder / "fixel_density_map_{}.nii.gz".format(bundle_name)) - - if not args.separate_bundles: - nib.save(nib.Nifti1Image(fixel_density_maps, affine), - out_folder / "fixel_density_maps.nii.gz") - bundles_idx = np.arange(0, len(bundles_names), 1) - lookup_table = np.array([bundles_names, bundles_idx]) - np.savetxt(out_folder / "bundles_lookup_table.txt", - lookup_table, fmt='%s') - - # This applies a threshold on the normalized density (percentage) - fixel_density_masks_rel = fixel_density_maps >= args.rel_thr - - # Compute the fixel density masks from the rel and abs versions - fixel_density_masks = fixel_density_masks_rel * fixel_density_masks_abs + # Compute fixel density maps and masks + fixel_density_maps = fixel_density(peaks, bundles, args.max_theta, + nbr_processes=args.nbr_processes) + + fixel_density_masks = maps_to_masks(fixel_density_maps, args.abs_thr, + args.rel_thr, args.norm, + len(bundles)) # Compute number of bundles per fixel nb_bundles_per_fixel = np.sum(fixel_density_masks, axis=-1) - # Compute a mask of the present of each bundle - nb_unique_bundles_per_fixel = np.where(np.sum(fixel_density_masks, - axis=-2) > 0, 1, 0) - # Compute number of bundles per fixel by taking the sum of the mask - nb_bundles_per_voxel = np.sum(nb_unique_bundles_per_fixel, axis=-1) - - if args.save_masks: - for i, (bundle, bundle_name) in enumerate(zip(bundles, bundles_names)): - - if args.separate_bundles: - nib.save(nib.Nifti1Image(fixel_density_masks[..., i].astype(np.uint8), affine), - out_folder / "fixel_density_mask_{}.nii.gz".format(bundle_name)) - - if args.select_single_bundle: - # Single-fiber single-bundle voxels - single_bundle_per_voxel = nb_bundles_per_voxel == 1 - # Making sure we also have single-fiber voxels only - single_bundle_per_voxel *= nufo_sf - - nib.save(nib.Nifti1Image(single_bundle_per_voxel.astype(np.uint8), - affine), - out_folder / "bundle_mask_only_WM.nii.gz") - - bundle_mask = fixel_density_masks[..., 0, i] * single_bundle_per_voxel - nib.save(nib.Nifti1Image(bundle_mask.astype(np.uint8), affine), - out_folder / "bundle_mask_only_{}.nii.gz".format(bundle_name)) - - if not args.separate_bundles: - nib.save(nib.Nifti1Image(fixel_density_masks.astype(np.uint8), + # Compute a mask of the presence of each bundle per voxel + # Since a bundle can be present twice in a single voxel by being associated + # with more than one fixel, we count the presence of a bundle if > 0. + presence_of_bundles_per_voxel = np.where(np.sum(fixel_density_masks, + axis=-2) > 0, 1, 0) + # Compute number of bundles per voxel by taking the sum of the mask + nb_bundles_per_voxel = np.sum(presence_of_bundles_per_voxel, axis=-1) + + # Save all results + for i, bundle_name in enumerate(bundles_names): + if args.split_bundles: # Save the maps and masks for each bundle + nib.save(nib.Nifti1Image(fixel_density_maps[..., i], affine), + "fixel_density_map_{}.nii.gz".format(bundle_name)) + nib.save(nib.Nifti1Image(fixel_density_masks[..., i], affine), + "fixel_density_mask_{}.nii.gz".format(bundle_name)) + + if args.single_bundle: + # Single-fiber single-bundle voxels + one_bundle_per_voxel = nb_bundles_per_voxel == 1 + # Making sure we also have single-fiber voxels only + one_bundle_per_voxel *= nufo_sf + # Save a single-fiber single-bundle mask for the whole WM + nib.save(nib.Nifti1Image(one_bundle_per_voxel.astype(np.uint8), affine), - out_folder / "fixel_density_masks.nii.gz") - - nib.save(nib.Nifti1Image(nb_bundles_per_fixel.astype(np.uint8), - affine), out_folder / "nb_bundles_per_fixel.nii.gz") - - nib.save(nib.Nifti1Image(nb_bundles_per_voxel.astype(np.uint8), - affine), out_folder / "nb_bundles_per_voxel.nii.gz") + "single_bundle_mask_WM.nii.gz") + # Save a single-fiber single-bundle mask for each bundle + bundle_mask = fixel_density_masks[..., 0, i] * one_bundle_per_voxel + nib.save(nib.Nifti1Image(bundle_mask.astype(np.uint8), affine), + "single_bundle_mask_{}.nii.gz".format(bundle_name)) + + if args.split_fixels: # Save the maps and masks for each fixel + for i in range(5): + nib.save(nib.Nifti1Image(fixel_density_maps[..., i, :], affine), + "fixel_density_map_f{}.nii.gz".format(i)) + nib.save(nib.Nifti1Image(fixel_density_masks[..., i, :], affine), + "fixel_density_mask_f{}.nii.gz".format(i)) + + # Save bundles lookup table to know the order of the bundles + bundles_idx = np.arange(0, len(bundles_names), 1) + lookup_table = np.array([bundles_names, bundles_idx]) + np.savetxt("bundles_lookup_table.txt", + lookup_table, fmt='%s') + + # Save full fixel density maps, all fixels and bundles combined + nib.save(nib.Nifti1Image(fixel_density_maps, affine), + "fixel_density_maps.nii.gz") + + # Save full fixel density masks, all fixels and bundles combined + nib.save(nib.Nifti1Image(fixel_density_masks, affine), + "fixel_density_masks.nii.gz") + + # Save number of bundles per fixel and per voxel + nib.save(nib.Nifti1Image(nb_bundles_per_fixel, affine), + "nb_bundles_per_fixel.nii.gz") + nib.save(nib.Nifti1Image(nb_bundles_per_voxel, affine), + "nb_bundles_per_voxel.nii.gz") if __name__ == "__main__": From f74f815f48bba253f021c1bacb6fd41075f47f90 Mon Sep 17 00:00:00 2001 From: Philippe Karan Date: Wed, 19 Jun 2024 18:51:08 -0400 Subject: [PATCH 03/20] Adding bundle names --- scripts/scil_bundle_fixel_analysis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 4e9d2455d..7b8bffce6 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -89,8 +89,8 @@ def main(): peaks = peaks_img.get_fdata() affine = peaks_img.affine - # Compute NuFo SF from peaks - if args.select_single_bundle: + # Compute NuFo single-fiber from peaks + if args.single_bundle: is_first_peak = np.sum(peaks[..., 0:3], axis=-1) != 0 is_second_peak = np.sum(peaks[..., 3:6], axis=-1) == 0 nufo_sf = np.logical_and(is_first_peak, is_second_peak) @@ -101,7 +101,7 @@ def main(): for bundle in args.in_bundles[0]: bundles.append(bundle) bundles_names.append(Path(bundle).name.split(".")[0]) - if args.in_bundles_names[0] != []: # If names are given + if args.in_bundles_names: # If names are given bundles_names = args.in_bundles_names[0] # Compute fixel density maps and masks From a56883b325d2882a414b4cdd7ed535af493af0f9 Mon Sep 17 00:00:00 2001 From: Philippe Karan Date: Wed, 19 Jun 2024 19:38:46 -0400 Subject: [PATCH 04/20] Adding tests for input/output, rel_thr check --- scilpy/tractanalysis/fixel_density.py | 2 +- scripts/scil_bundle_fixel_analysis.py | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/scilpy/tractanalysis/fixel_density.py b/scilpy/tractanalysis/fixel_density.py index fcbe1e0d6..3f51753af 100644 --- a/scilpy/tractanalysis/fixel_density.py +++ b/scilpy/tractanalysis/fixel_density.py @@ -128,7 +128,7 @@ def maps_to_masks(maps, abs_thr, rel_thr, norm, nb_bundles): elif norm == "fixel": maps[..., i] /= fixel_sum - # Apply a threshold on the normalized density (percentage) + # Apply a threshold on the normalized density masks_rel = maps >= rel_thr # Compute the fixel density masks from the rel and abs versions masks = masks_rel * masks_abs diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 7b8bffce6..77f1e645f 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -12,9 +12,12 @@ import argparse import nibabel as nib import numpy as np +import logging from pathlib import Path -from scilpy.io.utils import (add_overwrite_arg, add_processes_arg) +from scilpy.io.utils import (add_overwrite_arg, add_processes_arg, + assert_headers_compatible, assert_inputs_exist, + assert_outputs_exist) from scilpy.tractanalysis.fixel_density import (fixel_density, maps_to_masks) @@ -83,6 +86,18 @@ def _build_arg_parser(): def main(): parser = _build_arg_parser() args = parser.parse_args() + logging.getLogger().setLevel(logging.getLevelName(args.verbose)) + + assert_inputs_exist(parser, args.in_peaks + args.in_bundles[0]) + assert_outputs_exist(parser, args, ["bundles_lookup_table.txt", + "fixel_density_maps.nii.gz", + "fixel_density_masks.nii.gz", + "nb_bundles_per_fixel.nii.gz", + "nb_bundles_per_voxel.nii.gz"]) + assert_headers_compatible(parser, args.in_peaks + args.in_bundles[0]) + + if args.rel_thr < 0 or args.rel_thr > 1: + parser.error("Argument rel_thr must be a value between 0 and 1.") # Load the data peaks_img = nib.load(args.in_peaks) @@ -166,9 +181,9 @@ def main(): "fixel_density_masks.nii.gz") # Save number of bundles per fixel and per voxel - nib.save(nib.Nifti1Image(nb_bundles_per_fixel, affine), + nib.save(nib.Nifti1Image(nb_bundles_per_fixel.astype(np.uint16), affine), "nb_bundles_per_fixel.nii.gz") - nib.save(nib.Nifti1Image(nb_bundles_per_voxel, affine), + nib.save(nib.Nifti1Image(nb_bundles_per_voxel.astype(np.uint16), affine), "nb_bundles_per_voxel.nii.gz") From 8c55dc37b6fff739517099d571dad1598a660410 Mon Sep 17 00:00:00 2001 From: Philippe Karan Date: Wed, 19 Jun 2024 19:49:45 -0400 Subject: [PATCH 05/20] Fixing bugs --- scripts/scil_bundle_fixel_analysis.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 77f1e645f..9be4096b6 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -17,7 +17,7 @@ from scilpy.io.utils import (add_overwrite_arg, add_processes_arg, assert_headers_compatible, assert_inputs_exist, - assert_outputs_exist) + assert_outputs_exist, add_verbose_arg) from scilpy.tractanalysis.fixel_density import (fixel_density, maps_to_masks) @@ -53,7 +53,7 @@ def _build_arg_parser(): '\nAny normalized density above or equal to ' 'this value will pass the relative threshold test. ' '\nMust be between 0 and 1 [%(default)s].') - + g.add_argument('--norm', default="voxel", choices=["fixel", "voxel"], help='Way of normalizing the density maps. If fixel, ' 'will normalize the maps per fixel, \nin each voxel. ' @@ -80,6 +80,7 @@ def _build_arg_parser(): add_overwrite_arg(p) add_processes_arg(p) + add_verbose_arg(p) return p @@ -88,13 +89,13 @@ def main(): args = parser.parse_args() logging.getLogger().setLevel(logging.getLevelName(args.verbose)) - assert_inputs_exist(parser, args.in_peaks + args.in_bundles[0]) + assert_inputs_exist(parser, [args.in_peaks] + args.in_bundles[0]) assert_outputs_exist(parser, args, ["bundles_lookup_table.txt", "fixel_density_maps.nii.gz", "fixel_density_masks.nii.gz", "nb_bundles_per_fixel.nii.gz", "nb_bundles_per_voxel.nii.gz"]) - assert_headers_compatible(parser, args.in_peaks + args.in_bundles[0]) + assert_headers_compatible(parser, [args.in_peaks] + args.in_bundles[0]) if args.rel_thr < 0 or args.rel_thr > 1: parser.error("Argument rel_thr must be a value between 0 and 1.") @@ -116,13 +117,13 @@ def main(): for bundle in args.in_bundles[0]: bundles.append(bundle) bundles_names.append(Path(bundle).name.split(".")[0]) - if args.in_bundles_names: # If names are given + if args.in_bundles_names: # If names are given bundles_names = args.in_bundles_names[0] # Compute fixel density maps and masks fixel_density_maps = fixel_density(peaks, bundles, args.max_theta, nbr_processes=args.nbr_processes) - + fixel_density_masks = maps_to_masks(fixel_density_maps, args.abs_thr, args.rel_thr, args.norm, len(bundles)) @@ -139,7 +140,7 @@ def main(): # Save all results for i, bundle_name in enumerate(bundles_names): - if args.split_bundles: # Save the maps and masks for each bundle + if args.split_bundles: # Save the maps and masks for each bundle nib.save(nib.Nifti1Image(fixel_density_maps[..., i], affine), "fixel_density_map_{}.nii.gz".format(bundle_name)) nib.save(nib.Nifti1Image(fixel_density_masks[..., i], affine), @@ -158,19 +159,18 @@ def main(): bundle_mask = fixel_density_masks[..., 0, i] * one_bundle_per_voxel nib.save(nib.Nifti1Image(bundle_mask.astype(np.uint8), affine), "single_bundle_mask_{}.nii.gz".format(bundle_name)) - - if args.split_fixels: # Save the maps and masks for each fixel + + if args.split_fixels: # Save the maps and masks for each fixel for i in range(5): nib.save(nib.Nifti1Image(fixel_density_maps[..., i, :], affine), - "fixel_density_map_f{}.nii.gz".format(i)) + "fixel_density_map_f{}.nii.gz".format(i + 1)) nib.save(nib.Nifti1Image(fixel_density_masks[..., i, :], affine), - "fixel_density_mask_f{}.nii.gz".format(i)) + "fixel_density_mask_f{}.nii.gz".format(i + 1)) # Save bundles lookup table to know the order of the bundles bundles_idx = np.arange(0, len(bundles_names), 1) lookup_table = np.array([bundles_names, bundles_idx]) - np.savetxt("bundles_lookup_table.txt", - lookup_table, fmt='%s') + np.savetxt("bundles_lookup_table.txt", lookup_table, fmt='%s') # Save full fixel density maps, all fixels and bundles combined nib.save(nib.Nifti1Image(fixel_density_maps, affine), From a2ba9eea7b8672818dab893a47ce0abb199d3762 Mon Sep 17 00:00:00 2001 From: karp2601 Date: Thu, 20 Jun 2024 14:37:35 -0400 Subject: [PATCH 06/20] Adding doc and normalized maps --- scilpy/tractanalysis/fixel_density.py | 4 +- scripts/scil_bundle_fixel_analysis.py | 60 ++++++++++++++++++++------- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/scilpy/tractanalysis/fixel_density.py b/scilpy/tractanalysis/fixel_density.py index 3f51753af..18cfc9ebd 100644 --- a/scilpy/tractanalysis/fixel_density.py +++ b/scilpy/tractanalysis/fixel_density.py @@ -111,6 +111,8 @@ def maps_to_masks(maps, abs_thr, rel_thr, norm, nb_bundles): ------- masks : np.ndarray (x, y, z, 5, N) Density masks per fixel per bundle. + maps : np.ndarray (x, y, z, 5, N) + Normalized density maps per fixel per bundle. """ # Apply a threshold on the number of streamlines masks_abs = maps >= abs_thr @@ -133,4 +135,4 @@ def maps_to_masks(maps, abs_thr, rel_thr, norm, nb_bundles): # Compute the fixel density masks from the rel and abs versions masks = masks_rel * masks_abs - return masks.astype(np.uint8) + return masks.astype(np.uint8), maps diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 9be4096b6..8c5f33356 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -1,11 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -Analyze bundles at the fixel level, producing various results: - -- -- -- +Analyze bundles at the fixel level, producing various output: + + - bundles_LUT.txt : array of (N) bundle names + Lookup table (LUT) to know the order of the bundles in the various + outputs. When an output contains results for all bundles, it is always + the last dimension of the np.ndarray and it follows the order of the + lookup table. + + - fixel_density_maps.nii.gz : np.ndarray (x, y, z, 5, N) + For each voxel, it represents the density of bundles among the 5 fixels. + If the normalization is chosen as the voxel-type, then the sum of the + density over a voxel is 1. If the normalization is chosen as the + fixel-type, then the sum of the density over each fixel is 1, so the sum + over a voxel will be higher than 1 (except in the single-fiber case). + The density maps can be computed using the streamline count, or any + streamline weighting like COMMIT or SIF, through the data_per_streamline. + + - fixel_density_masks.nii.gz : np.ndarray (x, y, z, 5, N) + For each voxel, it represents whether or not each bundle is associated + with each of the 5 fixels. In other words, it is a masked version of + fixel_density_maps, using two different thresholds. First, the absolute + threshold (abs_thr) is applied on the maps before the normalization, + either on the number of streamlines or the custom weight. Second, after + the normalization, the relative threshold (rel_thr) is applied on the + maps as a minimal value of density to be counted as an association. """ @@ -90,9 +110,10 @@ def main(): logging.getLogger().setLevel(logging.getLevelName(args.verbose)) assert_inputs_exist(parser, [args.in_peaks] + args.in_bundles[0]) - assert_outputs_exist(parser, args, ["bundles_lookup_table.txt", + assert_outputs_exist(parser, args, ["bundles_LUT.txt", "fixel_density_maps.nii.gz", "fixel_density_masks.nii.gz", + "voxel_density_masks.nii.gz", "nb_bundles_per_fixel.nii.gz", "nb_bundles_per_voxel.nii.gz"]) assert_headers_compatible(parser, [args.in_peaks] + args.in_bundles[0]) @@ -124,17 +145,21 @@ def main(): fixel_density_maps = fixel_density(peaks, bundles, args.max_theta, nbr_processes=args.nbr_processes) - fixel_density_masks = maps_to_masks(fixel_density_maps, args.abs_thr, - args.rel_thr, args.norm, - len(bundles)) + fixel_density_masks, fixel_density_maps = maps_to_masks(fixel_density_maps, + args.abs_thr, + args.rel_thr, + args.norm, + len(bundles)) # Compute number of bundles per fixel nb_bundles_per_fixel = np.sum(fixel_density_masks, axis=-1) + # Compute voxel density maps and masks + voxel_density_maps = np.sum(fixel_density_maps, axis=-2) + voxel_density_masks = np.sum(fixel_density_masks, axis=-2) # Compute a mask of the presence of each bundle per voxel # Since a bundle can be present twice in a single voxel by being associated # with more than one fixel, we count the presence of a bundle if > 0. - presence_of_bundles_per_voxel = np.where(np.sum(fixel_density_masks, - axis=-2) > 0, 1, 0) + presence_of_bundles_per_voxel = np.where(voxel_density_masks > 0, 1, 0) # Compute number of bundles per voxel by taking the sum of the mask nb_bundles_per_voxel = np.sum(presence_of_bundles_per_voxel, axis=-1) @@ -170,7 +195,7 @@ def main(): # Save bundles lookup table to know the order of the bundles bundles_idx = np.arange(0, len(bundles_names), 1) lookup_table = np.array([bundles_names, bundles_idx]) - np.savetxt("bundles_lookup_table.txt", lookup_table, fmt='%s') + np.savetxt("bundles_LUT.txt", lookup_table, fmt='%s') # Save full fixel density maps, all fixels and bundles combined nib.save(nib.Nifti1Image(fixel_density_maps, affine), @@ -179,11 +204,18 @@ def main(): # Save full fixel density masks, all fixels and bundles combined nib.save(nib.Nifti1Image(fixel_density_masks, affine), "fixel_density_masks.nii.gz") + + # Save full voxel density maps and masks + if args.norm == "voxel": # If fixel, the voxel maps do not mean anything + nib.save(nib.Nifti1Image(voxel_density_maps, affine), + "voxel_density_maps.nii.gz") + nib.save(nib.Nifti1Image(voxel_density_masks.astype(np.uint8), affine), + "voxel_density_masks.nii.gz") # Save number of bundles per fixel and per voxel - nib.save(nib.Nifti1Image(nb_bundles_per_fixel.astype(np.uint16), affine), + nib.save(nib.Nifti1Image(nb_bundles_per_fixel.astype(np.uint8), affine), "nb_bundles_per_fixel.nii.gz") - nib.save(nib.Nifti1Image(nb_bundles_per_voxel.astype(np.uint16), affine), + nib.save(nib.Nifti1Image(nb_bundles_per_voxel.astype(np.uint8), affine), "nb_bundles_per_voxel.nii.gz") From 74211556fbba471abf9337786c127939978b4752 Mon Sep 17 00:00:00 2001 From: Philippe Karan Date: Thu, 20 Jun 2024 17:55:32 -0400 Subject: [PATCH 07/20] Finishing docstring --- scripts/scil_bundle_fixel_analysis.py | 74 ++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 8c5f33356..07f7193c5 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -1,7 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -Analyze bundles at the fixel level, producing various output: +Analyze bundles at the fixel level using peaks and bundles (.trk). If the +bundles files are names as {bundle_name}.trk, simply use the --in_bundles +argument with --in_bundles_names. If it is not the case or you want other names +to be saved, please use --in_bundles_names to provide bundles names IN THE SAME +ORDER as the inputed bundles. + +The script produces various output: - bundles_LUT.txt : array of (N) bundle names Lookup table (LUT) to know the order of the bundles in the various @@ -10,16 +16,17 @@ lookup table. - fixel_density_maps.nii.gz : np.ndarray (x, y, z, 5, N) - For each voxel, it represents the density of bundles among the 5 fixels. - If the normalization is chosen as the voxel-type, then the sum of the - density over a voxel is 1. If the normalization is chosen as the - fixel-type, then the sum of the density over each fixel is 1, so the sum - over a voxel will be higher than 1 (except in the single-fiber case). - The density maps can be computed using the streamline count, or any - streamline weighting like COMMIT or SIF, through the data_per_streamline. + For each voxel, it gives the density of bundles associated with each + of the 5 fixels. If the normalization is chosen as the voxel-type, then + the sum of the density over a voxel is 1. If the normalization is chosen + as the fixel-type, then the sum of the density over each fixel is 1, so + the sum over a voxel will be higher than 1 (except in the single-fiber + case). The density maps can be computed using the streamline count, or + any streamline weighting like COMMIT or SIF, through the + data_per_streamline. - fixel_density_masks.nii.gz : np.ndarray (x, y, z, 5, N) - For each voxel, it represents whether or not each bundle is associated + For each voxel, it gives whether or not each bundle is associated with each of the 5 fixels. In other words, it is a masked version of fixel_density_maps, using two different thresholds. First, the absolute threshold (abs_thr) is applied on the maps before the normalization, @@ -27,6 +34,45 @@ the normalization, the relative threshold (rel_thr) is applied on the maps as a minimal value of density to be counted as an association. + - voxel_density_maps.nii.gz : np.ndarray (x, y, z, N) + For each voxel, it gives the density of each bundle within the voxel, + regardless of fixels. In other words, it gives the fraction of each + bundle per voxel. This is only outputed if the normalization of the maps + is chosen as the voxel-type, because the fixel-type does not translate to + meaningful results when summed into a voxel. + + - voxel_density_masks.nii.gz : np.ndarray (x, y, z, N) + For each voxel, it gives whether or not each bundle is present. This is + computed from fixel_density_masks, so the same thresholds prevail. + + - nb_bundles_per_fixel.nii.gz : np.ndarray (x, y, z) + For each voxel, it gives the number of bundles associated with each of + the 5 fixels. + + - nb_bundles_per_voxel.nii.gz : np.ndarray (x, y, z) + For each voxel, it gives the number of bundles within the voxel. This + accounts for bundles that might be associated with more than one fixel, + so no bundle is counted more than once in a voxel. + + If the split_bundles argument is given, the script will also save the + fixel_density_maps and fixel_density_masks separated by bundles, with + names fixel_density_map_{bundle_name}.nii.gz and + fixel_density_mask_{bundle_name}.nii.gz. + These will have the shape (x, y, z, 5). + + If the split_fixels argument is given, the script will also save the + fixel_density_maps and fixel_density_masks separated by fixels, with + names fixel_density_map_f{fixel_id}.nii.gz and + fixel_density_mask_f{fixel_id}.nii.gz. + These will have the shape (x, y, z, N). + + If the single_bundle argument is given, the script will also save the + single-fiber single-bundle masks, which are obtained by selecting the + voxels where only one bundle and one fiber (fixel) are present. There will + be one single_bundle_mask_{bundle_name}.nii.gz per bundle, and a whole WM + version single_bundle_mask_WM.nii.gz. + These will have the shape (x, y, z). + """ import argparse @@ -153,15 +199,15 @@ def main(): # Compute number of bundles per fixel nb_bundles_per_fixel = np.sum(fixel_density_masks, axis=-1) - # Compute voxel density maps and masks + # Compute voxel density maps voxel_density_maps = np.sum(fixel_density_maps, axis=-2) - voxel_density_masks = np.sum(fixel_density_masks, axis=-2) - # Compute a mask of the presence of each bundle per voxel + # Compute voxel density masks # Since a bundle can be present twice in a single voxel by being associated # with more than one fixel, we count the presence of a bundle if > 0. - presence_of_bundles_per_voxel = np.where(voxel_density_masks > 0, 1, 0) + voxel_density_masks = np.where(np.sum(fixel_density_masks, axis=-2) > 0, + 1, 0) # Compute number of bundles per voxel by taking the sum of the mask - nb_bundles_per_voxel = np.sum(presence_of_bundles_per_voxel, axis=-1) + nb_bundles_per_voxel = np.sum(voxel_density_masks, axis=-1) # Save all results for i, bundle_name in enumerate(bundles_names): From 45f6015507b3b02fa23afbd93a7a6acb5b8bcd98 Mon Sep 17 00:00:00 2001 From: Philippe Karan Date: Thu, 20 Jun 2024 18:41:19 -0400 Subject: [PATCH 08/20] Adding dps key --- scilpy/tractanalysis/fixel_density.py | 20 ++++++++++++++------ scripts/scil_bundle_fixel_analysis.py | 11 ++++++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/scilpy/tractanalysis/fixel_density.py b/scilpy/tractanalysis/fixel_density.py index 18cfc9ebd..811287236 100644 --- a/scilpy/tractanalysis/fixel_density.py +++ b/scilpy/tractanalysis/fixel_density.py @@ -9,7 +9,8 @@ def _fixel_density_parallel(args): peaks = args[0] max_theta = args[1] - bundle = args[2] + dps_key = args[2] + bundle = args[3] sft = load_tractogram(bundle, 'same') sft.to_vox() @@ -20,7 +21,7 @@ def _fixel_density_parallel(args): min_cos_theta = np.cos(np.radians(max_theta)) all_crossed_indices = grid_intersections(sft.streamlines) - for crossed_indices in all_crossed_indices: + for i, crossed_indices in enumerate(all_crossed_indices): segments = crossed_indices[1:] - crossed_indices[:-1] seg_lengths = np.linalg.norm(segments, axis=1) @@ -36,6 +37,10 @@ def _fixel_density_parallel(args): normalized_seg = np.reshape(segments / seg_lengths[..., None], (-1, 3)) + weight = 1 + if dps_key: + weight = sft.data_per_streamline[dps_key][i] + for vox_idx, seg_dir in zip(vox_indices, normalized_seg): vox_idx = tuple(vox_idx) peaks_at_idx = peaks[vox_idx].reshape((5, 3)) @@ -45,21 +50,23 @@ def _fixel_density_parallel(args): if (cos_theta > min_cos_theta).any(): lobe_idx = np.argmax(np.squeeze(cos_theta), axis=0) # (n_segs) - # TODO Change that for commit weight if given - fixel_density_maps[vox_idx][lobe_idx] += 1 + fixel_density_maps[vox_idx][lobe_idx] += weight return fixel_density_maps -def fixel_density(peaks, bundles, max_theta=45, nbr_processes=None): +def fixel_density(peaks, bundles, dps_key=None, max_theta=45, nbr_processes=None): """Compute the fixel density map per bundle. Can use parallel processing. Parameters ---------- - peaks: np.ndarray (x, y, z, 15) + peaks : np.ndarray (x, y, z, 15) Five principal fiber orientations for each voxel. bundles : list or np.array (N) List of (N) paths to bundles. + dps_key : string, optional + Key to the data_per_streamline to use as weight instead of the number + of streamlines. max_theta : int, optional Maximum angle between streamline and peak to be associated. nbr_processes : int, optional @@ -79,6 +86,7 @@ def fixel_density(peaks, bundles, max_theta=45, nbr_processes=None): results = pool.map(_fixel_density_parallel, zip(itertools.repeat(peaks), itertools.repeat(max_theta), + itertools.repeat(dps_key), bundles)) pool.close() pool.join() diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 07f7193c5..c3b918bc5 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -23,7 +23,7 @@ the sum over a voxel will be higher than 1 (except in the single-fiber case). The density maps can be computed using the streamline count, or any streamline weighting like COMMIT or SIF, through the - data_per_streamline. + data_per_streamline using the --dps_key argument. - fixel_density_masks.nii.gz : np.ndarray (x, y, z, 5, N) For each voxel, it gives whether or not each bundle is associated @@ -72,7 +72,6 @@ be one single_bundle_mask_{bundle_name}.nii.gz per bundle, and a whole WM version single_bundle_mask_WM.nii.gz. These will have the shape (x, y, z). - """ import argparse @@ -104,6 +103,11 @@ def _build_arg_parser(): 'as they were given. \nIf this argument is not used, ' 'the script assumes that the name of the bundle \nis ' 'its filename without extensions.') + + p.add_argument('--dps_key', default=None, + help='Key to access the data per streamline to use as ' + 'weight when computing the maps, \ninstead of the ' + 'number of streamlines. [%(default)s].') g = p.add_argument_group(title='Mask parameters') @@ -188,7 +192,8 @@ def main(): bundles_names = args.in_bundles_names[0] # Compute fixel density maps and masks - fixel_density_maps = fixel_density(peaks, bundles, args.max_theta, + fixel_density_maps = fixel_density(peaks, bundles, args.dps_key, + args.max_theta, nbr_processes=args.nbr_processes) fixel_density_masks, fixel_density_maps = maps_to_masks(fixel_density_maps, From 6ed888d48286aeded44b3bd3f85d3d4464bbad1d Mon Sep 17 00:00:00 2001 From: Philippe Karan Date: Fri, 21 Jun 2024 10:08:07 -0400 Subject: [PATCH 09/20] Modify asb_thr for streamline weights, pep8 --- scilpy/tractanalysis/fixel_density.py | 11 ++++++----- scripts/scil_bundle_fixel_analysis.py | 27 ++++++++++++++------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/scilpy/tractanalysis/fixel_density.py b/scilpy/tractanalysis/fixel_density.py index 811287236..dbb3802e8 100644 --- a/scilpy/tractanalysis/fixel_density.py +++ b/scilpy/tractanalysis/fixel_density.py @@ -55,7 +55,8 @@ def _fixel_density_parallel(args): return fixel_density_maps -def fixel_density(peaks, bundles, dps_key=None, max_theta=45, nbr_processes=None): +def fixel_density(peaks, bundles, dps_key=None, max_theta=45, + nbr_processes=None): """Compute the fixel density map per bundle. Can use parallel processing. Parameters @@ -103,9 +104,9 @@ def maps_to_masks(maps, abs_thr, rel_thr, norm, nb_bundles): ---------- maps : np.ndarray (x, y, z, 5, N) Density per fixel per bundle. - abs_thr : int + abs_thr : float Value of density maps threshold to obtain density masks, in number of - streamlines. + streamlines or streamline weighting. rel_thr : float Value of density maps threshold to obtain density masks, as a ratio of the normalized density. Must be between 0 and 1. @@ -123,7 +124,7 @@ def maps_to_masks(maps, abs_thr, rel_thr, norm, nb_bundles): Normalized density maps per fixel per bundle. """ # Apply a threshold on the number of streamlines - masks_abs = maps >= abs_thr + masks_abs = maps > abs_thr # Normalizing the density maps per voxel or fixel fixel_sum = np.sum(maps, axis=-1) @@ -139,7 +140,7 @@ def maps_to_masks(maps, abs_thr, rel_thr, norm, nb_bundles): maps[..., i] /= fixel_sum # Apply a threshold on the normalized density - masks_rel = maps >= rel_thr + masks_rel = maps > rel_thr # Compute the fixel density masks from the rel and abs versions masks = masks_rel * masks_abs diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index c3b918bc5..ed58e2ac6 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -24,15 +24,15 @@ case). The density maps can be computed using the streamline count, or any streamline weighting like COMMIT or SIF, through the data_per_streamline using the --dps_key argument. - + - fixel_density_masks.nii.gz : np.ndarray (x, y, z, 5, N) For each voxel, it gives whether or not each bundle is associated with each of the 5 fixels. In other words, it is a masked version of fixel_density_maps, using two different thresholds. First, the absolute threshold (abs_thr) is applied on the maps before the normalization, - either on the number of streamlines or the custom weight. Second, after - the normalization, the relative threshold (rel_thr) is applied on the - maps as a minimal value of density to be counted as an association. + either on the number of streamlines or the streamline weights. Second, + after the normalization, the relative threshold (rel_thr) is applied on + the maps as a minimal value of density to be counted as an association. - voxel_density_maps.nii.gz : np.ndarray (x, y, z, N) For each voxel, it gives the density of each bundle within the voxel, @@ -57,7 +57,7 @@ If the split_bundles argument is given, the script will also save the fixel_density_maps and fixel_density_masks separated by bundles, with names fixel_density_map_{bundle_name}.nii.gz and - fixel_density_mask_{bundle_name}.nii.gz. + fixel_density_mask_{bundle_name}.nii.gz. These will have the shape (x, y, z, 5). If the split_fixels argument is given, the script will also save the @@ -103,24 +103,25 @@ def _build_arg_parser(): 'as they were given. \nIf this argument is not used, ' 'the script assumes that the name of the bundle \nis ' 'its filename without extensions.') - - p.add_argument('--dps_key', default=None, + + p.add_argument('--dps_key', default=None, type=str, help='Key to access the data per streamline to use as ' 'weight when computing the maps, \ninstead of the ' 'number of streamlines. [%(default)s].') g = p.add_argument_group(title='Mask parameters') - g.add_argument('--abs_thr', default=1, type=int, + g.add_argument('--abs_thr', default=0, type=float, help='Value of density maps threshold to obtain density ' - 'masks, in number of streamlines. \nAny number of ' - 'streamlines above or equal this value will pass ' - 'the absolute threshold test [%(default)s].') + 'masks, in number of streamlines \nor streamline ' + 'weighting if --dps_key is given. Any number of ' + 'streamlines \nor weight above this value will ' + 'pass the absolute threshold test [%(default)s].') g.add_argument('--rel_thr', default=0.01, type=float, help='Value of density maps threshold to obtain density ' 'masks, as a ratio of the normalized density ' - '\nAny normalized density above or equal to ' + '\nAny normalized density above ' 'this value will pass the relative threshold test. ' '\nMust be between 0 and 1 [%(default)s].') @@ -255,7 +256,7 @@ def main(): # Save full fixel density masks, all fixels and bundles combined nib.save(nib.Nifti1Image(fixel_density_masks, affine), "fixel_density_masks.nii.gz") - + # Save full voxel density maps and masks if args.norm == "voxel": # If fixel, the voxel maps do not mean anything nib.save(nib.Nifti1Image(voxel_density_maps, affine), From 24da64a4b05ca2eaef6181799c46313be61468f4 Mon Sep 17 00:00:00 2001 From: Philippe Karan Date: Fri, 21 Jun 2024 11:46:25 -0400 Subject: [PATCH 10/20] Adding tests --- .../tractanalysis/tests/test_fixel_density.py | 11 +++++ scripts/tests/test_bundle_fixel_analysis.py | 47 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 scilpy/tractanalysis/tests/test_fixel_density.py create mode 100644 scripts/tests/test_bundle_fixel_analysis.py diff --git a/scilpy/tractanalysis/tests/test_fixel_density.py b/scilpy/tractanalysis/tests/test_fixel_density.py new file mode 100644 index 000000000..d1cbe6a3c --- /dev/null +++ b/scilpy/tractanalysis/tests/test_fixel_density.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + + +def test_fixel_density(): + # toDO + pass + + +def test_maps_to_masks(): + # toDO + pass diff --git a/scripts/tests/test_bundle_fixel_analysis.py b/scripts/tests/test_bundle_fixel_analysis.py new file mode 100644 index 000000000..4bb09febc --- /dev/null +++ b/scripts/tests/test_bundle_fixel_analysis.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import tempfile + +from scilpy import SCILPY_HOME +from scilpy.io.fetcher import fetch_data, get_testing_files_dict + +fetch_data(get_testing_files_dict(), keys=['commit_amico.zip']) +tmp_dir = tempfile.TemporaryDirectory() + + +def test_help_option(script_runner): + ret = script_runner.run('scil_bundle_fixel_analysis.py', '--help') + assert ret.success + + +def test_default_parameters(script_runner, monkeypatch): + monkeypatch.chdir(os.path.expanduser(tmp_dir.name)) + in_peaks = os.path.join(SCILPY_HOME, 'commit_amico', 'peaks.nii.gz') + in_bundle = os.path.join(SCILPY_HOME, 'commit_amico', 'tracking.trk') + + ret = script_runner.run('scil_bundle_fixel_analysis.py', in_peaks, + '--in_bundles', in_bundle, + '--processes', '1', '-f') + assert ret.success + + +def test_all_parameters(script_runner, monkeypatch): + monkeypatch.chdir(os.path.expanduser(tmp_dir.name)) + in_peaks = os.path.join(SCILPY_HOME, 'commit_amico', 'peaks.nii.gz') + in_bundle = os.path.join(SCILPY_HOME, 'commit_amico', 'tracking.trk') + + ret = script_runner.run('scil_bundle_fixel_analysis.py', in_peaks, + '--in_bundles', in_bundle, + '--in_bundles_names', 'test', + '--abs_thr', '5', + '--rel_thr', '0.05', + '--norm', 'fixel', + '--split_bundles', '--split_fixels', + '--single_bundle', + '--processes', '1', '-f') + assert ret.success + +# We would need a tractogram with data_per_streamline to test the --dps_key +# option From 11caba863c172b4ccf7edd417bfdd4124e71344b Mon Sep 17 00:00:00 2001 From: Philippe Karan Date: Sat, 6 Jul 2024 08:32:09 -0400 Subject: [PATCH 11/20] Update doc --- scripts/scil_bundle_fixel_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index ed58e2ac6..2704571b0 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -137,11 +137,11 @@ def _build_arg_parser(): p.add_argument('--split_bundles', action='store_true', help='If set, save the density maps for each bundle ' - 'separately \ninstead of all in one file.') + 'separately \nin addition to the all in one version.') p.add_argument('--split_fixels', action='store_true', help='If set, save the density maps for each fixel ' - 'separately \ninstead of all in one file.') + 'separately \nin addition to the all in one version.') p.add_argument('--single_bundle', action='store_true', help='If set, will save the single-fiber single-bundle ' From b4a2b736db728ba8f0a0cc46834d78a2aba59279 Mon Sep 17 00:00:00 2001 From: Philippe Karan Date: Sun, 7 Jul 2024 16:01:10 -0400 Subject: [PATCH 12/20] Adding new saves --- scripts/scil_bundle_fixel_analysis.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 2704571b0..e98cbad28 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -148,6 +148,9 @@ def _build_arg_parser(): 'masks as well. \nThese are obtained by ' 'selecting the voxels where only one bundle is ' 'present \n(and one fiber/fixel).') + + p.add_argument('--bundles_mask', action='store_true', + help='If set, save the bundle mask for each bundle.') add_overwrite_arg(p) add_processes_arg(p) @@ -222,6 +225,12 @@ def main(): "fixel_density_map_{}.nii.gz".format(bundle_name)) nib.save(nib.Nifti1Image(fixel_density_masks[..., i], affine), "fixel_density_mask_{}.nii.gz".format(bundle_name)) + if args.norm == "voxel": # If fixel, voxel maps mean nothing + nib.save(nib.Nifti1Image(voxel_density_maps[..., i], affine), + "voxel_density_map_{}.nii.gz".format(bundle_name)) + bundle_mask = voxel_density_masks[..., i].astype(np.uint8) + nib.save(nib.Nifti1Image(bundle_mask, affine), + "voxel_density_mask_{}.nii.gz".format(bundle_name)) if args.single_bundle: # Single-fiber single-bundle voxels @@ -243,6 +252,13 @@ def main(): "fixel_density_map_f{}.nii.gz".format(i + 1)) nib.save(nib.Nifti1Image(fixel_density_masks[..., i, :], affine), "fixel_density_mask_f{}.nii.gz".format(i + 1)) + if args.norm == "voxel": # If fixel, voxel maps mean nothing + nib.save(nib.Nifti1Image(voxel_density_maps[..., i, :], + affine), + "voxel_density_map_f{}.nii.gz".format(i + 1)) + bundle_mask = voxel_density_masks[..., i, :].astype(np.uint8) + nib.save(nib.Nifti1Image(bundle_mask, affine), + "voxel_density_mask_f{}.nii.gz".format(i + 1)) # Save bundles lookup table to know the order of the bundles bundles_idx = np.arange(0, len(bundles_names), 1) @@ -258,7 +274,7 @@ def main(): "fixel_density_masks.nii.gz") # Save full voxel density maps and masks - if args.norm == "voxel": # If fixel, the voxel maps do not mean anything + if args.norm == "voxel": # If fixel, voxel maps mean nothing nib.save(nib.Nifti1Image(voxel_density_maps, affine), "voxel_density_maps.nii.gz") nib.save(nib.Nifti1Image(voxel_density_masks.astype(np.uint8), affine), From d0b7f4862c4b3416db7e7f9271ef93533d40aabe Mon Sep 17 00:00:00 2001 From: karp2601 Date: Tue, 16 Jul 2024 13:46:07 -0400 Subject: [PATCH 13/20] Correct pep8 --- scripts/scil_bundle_fixel_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index e98cbad28..3e0ce0a56 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -148,7 +148,7 @@ def _build_arg_parser(): 'masks as well. \nThese are obtained by ' 'selecting the voxels where only one bundle is ' 'present \n(and one fiber/fixel).') - + p.add_argument('--bundles_mask', action='store_true', help='If set, save the bundle mask for each bundle.') From d29f50675149c602f5399351e3418ccee8dbf7d6 Mon Sep 17 00:00:00 2001 From: karp2601 Date: Tue, 23 Jul 2024 10:51:52 -0400 Subject: [PATCH 14/20] Adding output filename options --- scripts/scil_bundle_fixel_analysis.py | 172 +++++++++++++++++--------- 1 file changed, 115 insertions(+), 57 deletions(-) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 3e0ce0a56..ce5c7521a 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -108,49 +108,65 @@ def _build_arg_parser(): help='Key to access the data per streamline to use as ' 'weight when computing the maps, \ninstead of the ' 'number of streamlines. [%(default)s].') - - g = p.add_argument_group(title='Mask parameters') - - g.add_argument('--abs_thr', default=0, type=float, - help='Value of density maps threshold to obtain density ' - 'masks, in number of streamlines \nor streamline ' - 'weighting if --dps_key is given. Any number of ' - 'streamlines \nor weight above this value will ' - 'pass the absolute threshold test [%(default)s].') - - g.add_argument('--rel_thr', default=0.01, type=float, - help='Value of density maps threshold to obtain density ' - 'masks, as a ratio of the normalized density ' - '\nAny normalized density above ' - 'this value will pass the relative threshold test. ' - '\nMust be between 0 and 1 [%(default)s].') - - g.add_argument('--norm', default="voxel", choices=["fixel", "voxel"], - help='Way of normalizing the density maps. If fixel, ' - 'will normalize the maps per fixel, \nin each voxel. ' - 'If voxel, will normalize the maps per voxel. ' - '[%(default)s]') - + p.add_argument('--max_theta', default=45, help='Maximum angle between streamline and peak to be ' 'associated [%(default)s].') - p.add_argument('--split_bundles', action='store_true', - help='If set, save the density maps for each bundle ' - 'separately \nin addition to the all in one version.') - - p.add_argument('--split_fixels', action='store_true', - help='If set, save the density maps for each fixel ' - 'separately \nin addition to the all in one version.') - - p.add_argument('--single_bundle', action='store_true', - help='If set, will save the single-fiber single-bundle ' - 'masks as well. \nThese are obtained by ' - 'selecting the voxels where only one bundle is ' - 'present \n(and one fiber/fixel).') - - p.add_argument('--bundles_mask', action='store_true', - help='If set, save the bundle mask for each bundle.') + g1 = p.add_argument_group(title='Mask parameters') + + g1.add_argument('--abs_thr', default=0, type=float, + help='Value of density maps threshold to obtain density ' + 'masks, in number of streamlines \nor streamline ' + 'weighting if --dps_key is given. Any number of ' + 'streamlines \nor weight above this value will ' + 'pass the absolute threshold test [%(default)s].') + + g1.add_argument('--rel_thr', default=0.01, type=float, + help='Value of density maps threshold to obtain density ' + 'masks, as a ratio of the normalized density ' + '\nAny normalized density above ' + 'this value will pass the relative threshold test. ' + '\nMust be between 0 and 1 [%(default)s].') + + g1.add_argument('--norm', default="voxel", choices=["fixel", "voxel"], + help='Way of normalizing the density maps. If fixel, ' + 'will normalize the maps per fixel, \nin each voxel. ' + 'If voxel, will normalize the maps per voxel ' + '[%(default)s].') + + g2 = p.add_argument_group(title='Output options') + + g2.add_argument('--split_bundles', action='store_true', + help='If set, save the density maps for each bundle ' + 'separately \nin addition to the all in one version.') + + g2.add_argument('--split_fixels', action='store_true', + help='If set, save the density maps for each fixel ' + 'separately \nin addition to the all in one version.') + + g2.add_argument('--single_bundle', action='store_true', + help='If set, will save the single-fiber single-bundle ' + 'masks as well. \nThese are obtained by ' + 'selecting the voxels where only one bundle is ' + 'present \n(and one fiber/fixel).') + + g2.add_argument('--bundles_mask', action='store_true', + help='If set, save the bundle mask for each bundle.') + + g2.add_argument('--out_dir', default="./", + help='Path to the output directory where all the output ' + 'files will be saved [%(default)s].') + + g2.add_argument('--prefix', default="", + help='Prefix to add to all predetermined output ' + 'filenames. We recommand finishing with an ' + 'underscore for better readability [%(default)s].') + + g2.add_argument('--suffix', default="", + help='Suffix to add to all predetermined output ' + 'filenames. We recommand starting with an underscore ' + 'for better readability [%(default)s].') add_overwrite_arg(p) add_processes_arg(p) @@ -195,6 +211,13 @@ def main(): if args.in_bundles_names: # If names are given bundles_names = args.in_bundles_names[0] + # Set up saving filename options + out_dir = args.out_dir + prefix = args.prefix + suffix = args.suffix + if out_dir[-1] != "/": + out_dir += "/" + # Compute fixel density maps and masks fixel_density_maps = fixel_density(peaks, bundles, args.dps_key, args.max_theta, @@ -219,18 +242,30 @@ def main(): nb_bundles_per_voxel = np.sum(voxel_density_masks, axis=-1) # Save all results - for i, bundle_name in enumerate(bundles_names): + for i, bundle_n in enumerate(bundles_names): if args.split_bundles: # Save the maps and masks for each bundle nib.save(nib.Nifti1Image(fixel_density_maps[..., i], affine), - "fixel_density_map_{}.nii.gz".format(bundle_name)) + "{}{}fixel_density_map_{}{}.nii.gz".format(out_dir, + prefix, + bundle_n, + suffix)) nib.save(nib.Nifti1Image(fixel_density_masks[..., i], affine), - "fixel_density_mask_{}.nii.gz".format(bundle_name)) + "{}{}fixel_density_mask_{}{}.nii.gz".format(out_dir, + prefix, + bundle_n, + suffix)) if args.norm == "voxel": # If fixel, voxel maps mean nothing nib.save(nib.Nifti1Image(voxel_density_maps[..., i], affine), - "voxel_density_map_{}.nii.gz".format(bundle_name)) + "{}{}voxel_density_map_{}{}.nii.gz".format(out_dir, + prefix, + bundle_n, + suffix)) bundle_mask = voxel_density_masks[..., i].astype(np.uint8) nib.save(nib.Nifti1Image(bundle_mask, affine), - "voxel_density_mask_{}.nii.gz".format(bundle_name)) + "{}{}voxel_density_mask_{}{}.nii.gz".format(out_dir, + prefix, + bundle_n, + suffix)) if args.single_bundle: # Single-fiber single-bundle voxels @@ -240,51 +275,74 @@ def main(): # Save a single-fiber single-bundle mask for the whole WM nib.save(nib.Nifti1Image(one_bundle_per_voxel.astype(np.uint8), affine), - "single_bundle_mask_WM.nii.gz") + "{}{}single_bundle_mask_WM{}.nii.gz".format(out_dir, + prefix, + suffix)) # Save a single-fiber single-bundle mask for each bundle bundle_mask = fixel_density_masks[..., 0, i] * one_bundle_per_voxel nib.save(nib.Nifti1Image(bundle_mask.astype(np.uint8), affine), - "single_bundle_mask_{}.nii.gz".format(bundle_name)) + "{}{}single_bundle_mask_{}{}.nii.gz".format(out_dir, + prefix, + bundle_n, + suffix)) if args.split_fixels: # Save the maps and masks for each fixel for i in range(5): nib.save(nib.Nifti1Image(fixel_density_maps[..., i, :], affine), - "fixel_density_map_f{}.nii.gz".format(i + 1)) + "{}{}fixel_density_map_f{}{}.nii.gz".format(out_dir, + prefix, + i + 1, + suffix)) nib.save(nib.Nifti1Image(fixel_density_masks[..., i, :], affine), - "fixel_density_mask_f{}.nii.gz".format(i + 1)) + "{}{}fixel_density_mask_f{}{}.nii.gz".format(out_dir, + prefix, + i + 1, + suffix)) if args.norm == "voxel": # If fixel, voxel maps mean nothing nib.save(nib.Nifti1Image(voxel_density_maps[..., i, :], affine), - "voxel_density_map_f{}.nii.gz".format(i + 1)) + "{}{}voxel_density_map_f{}{}.nii.gz".format(out_dir, + prefix, + i + 1, + suffix)) bundle_mask = voxel_density_masks[..., i, :].astype(np.uint8) nib.save(nib.Nifti1Image(bundle_mask, affine), - "voxel_density_mask_f{}.nii.gz".format(i + 1)) + "{}{}voxel_density_mask_f{}{}.nii.gz".format(out_dir, + prefix, + i + 1, + suffix)) # Save bundles lookup table to know the order of the bundles bundles_idx = np.arange(0, len(bundles_names), 1) lookup_table = np.array([bundles_names, bundles_idx]) - np.savetxt("bundles_LUT.txt", lookup_table, fmt='%s') + np.savetxt("{}{}bundles_LUT{}.txt".format(out_dir, prefix, suffix), + lookup_table, fmt='%s') # Save full fixel density maps, all fixels and bundles combined nib.save(nib.Nifti1Image(fixel_density_maps, affine), - "fixel_density_maps.nii.gz") + "{}{}fixel_density_maps{}.nii.gz".format(out_dir, prefix, suffix)) # Save full fixel density masks, all fixels and bundles combined nib.save(nib.Nifti1Image(fixel_density_masks, affine), - "fixel_density_masks.nii.gz") + "{}{}fixel_density_masks{}.nii.gz".format(out_dir, prefix, + suffix)) # Save full voxel density maps and masks if args.norm == "voxel": # If fixel, voxel maps mean nothing nib.save(nib.Nifti1Image(voxel_density_maps, affine), - "voxel_density_maps.nii.gz") + "{}{}voxel_density_maps{}.nii.gz".format(out_dir, prefix, + suffix)) nib.save(nib.Nifti1Image(voxel_density_masks.astype(np.uint8), affine), - "voxel_density_masks.nii.gz") + "{}{}voxel_density_masks{}.nii.gz".format(out_dir, prefix, + suffix)) # Save number of bundles per fixel and per voxel nib.save(nib.Nifti1Image(nb_bundles_per_fixel.astype(np.uint8), affine), - "nb_bundles_per_fixel.nii.gz") + "{}{}nb_bundles_per_fixel{}.nii.gz".format(out_dir, prefix, + suffix)) nib.save(nib.Nifti1Image(nb_bundles_per_voxel.astype(np.uint8), affine), - "nb_bundles_per_voxel.nii.gz") + "{}{}nb_bundles_per_voxel{}.nii.gz".format(out_dir, prefix, + suffix)) if __name__ == "__main__": From 76d3f7d535c14ba3be32d0d63698971eb4b44b4e Mon Sep 17 00:00:00 2001 From: karp2601 Date: Tue, 23 Jul 2024 11:04:34 -0400 Subject: [PATCH 15/20] Adding verbose and more doc --- scripts/scil_bundle_fixel_analysis.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index ce5c7521a..670d117ea 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -23,7 +23,9 @@ the sum over a voxel will be higher than 1 (except in the single-fiber case). The density maps can be computed using the streamline count, or any streamline weighting like COMMIT or SIF, through the - data_per_streamline using the --dps_key argument. + data_per_streamline using the --dps_key argument. NOTE: This is a 5D file + that will not be easily inputed to a regular viewer. Use --split_bundles + or --split_fixels options to save the 4D versions for visualization. - fixel_density_masks.nii.gz : np.ndarray (x, y, z, 5, N) For each voxel, it gives whether or not each bundle is associated @@ -33,6 +35,9 @@ either on the number of streamlines or the streamline weights. Second, after the normalization, the relative threshold (rel_thr) is applied on the maps as a minimal value of density to be counted as an association. + NOTE: This is a 5D file that will not be easily inputed to a regular + viewer. Use --split_bundles or --split_fixels options to save the 4D + versions for visualization. - voxel_density_maps.nii.gz : np.ndarray (x, y, z, N) For each voxel, it gives the density of each bundle within the voxel, @@ -192,6 +197,7 @@ def main(): parser.error("Argument rel_thr must be a value between 0 and 1.") # Load the data + logging.info("Loading data.") peaks_img = nib.load(args.in_peaks) peaks = peaks_img.get_fdata() affine = peaks_img.affine @@ -203,12 +209,16 @@ def main(): nufo_sf = np.logical_and(is_first_peak, is_second_peak) # Extract bundles and names + logging.info("Extracting bundles.") bundles = [] bundles_names = [] for bundle in args.in_bundles[0]: bundles.append(bundle) bundles_names.append(Path(bundle).name.split(".")[0]) if args.in_bundles_names: # If names are given + if len(args.in_bundles_names) != len(bundles): + parser.error("""--in_bundles_names must contain the same number of + element as in --in_bundles.""") bundles_names = args.in_bundles_names[0] # Set up saving filename options @@ -219,16 +229,19 @@ def main(): out_dir += "/" # Compute fixel density maps and masks + logging.info("Computing fixel density for all bundles.") fixel_density_maps = fixel_density(peaks, bundles, args.dps_key, args.max_theta, nbr_processes=args.nbr_processes) + logging.info("Computing density masks from density maps.") fixel_density_masks, fixel_density_maps = maps_to_masks(fixel_density_maps, args.abs_thr, args.rel_thr, args.norm, len(bundles)) + logging.info("Computing additional derivatives.") # Compute number of bundles per fixel nb_bundles_per_fixel = np.sum(fixel_density_masks, axis=-1) # Compute voxel density maps @@ -242,6 +255,7 @@ def main(): nb_bundles_per_voxel = np.sum(voxel_density_masks, axis=-1) # Save all results + logging.info("Saving all results.") for i, bundle_n in enumerate(bundles_names): if args.split_bundles: # Save the maps and masks for each bundle nib.save(nib.Nifti1Image(fixel_density_maps[..., i], affine), From 9054323ba8f49e6343850f6a889a23db94dc75ba Mon Sep 17 00:00:00 2001 From: karp2601 Date: Tue, 23 Jul 2024 11:30:34 -0400 Subject: [PATCH 16/20] Fixing prints and bundles names extraction --- scripts/scil_bundle_fixel_analysis.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 670d117ea..7b923e2e2 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -165,13 +165,13 @@ def _build_arg_parser(): g2.add_argument('--prefix', default="", help='Prefix to add to all predetermined output ' - 'filenames. We recommand finishing with an ' + 'filenames. \nWe recommand finishing with an ' 'underscore for better readability [%(default)s].') g2.add_argument('--suffix', default="", help='Suffix to add to all predetermined output ' - 'filenames. We recommand starting with an underscore ' - 'for better readability [%(default)s].') + 'filenames. \nWe recommand starting with an ' + 'underscore for better readability [%(default)s].') add_overwrite_arg(p) add_processes_arg(p) @@ -209,17 +209,17 @@ def main(): nufo_sf = np.logical_and(is_first_peak, is_second_peak) # Extract bundles and names - logging.info("Extracting bundles.") - bundles = [] - bundles_names = [] - for bundle in args.in_bundles[0]: - bundles.append(bundle) - bundles_names.append(Path(bundle).name.split(".")[0]) + bundles = args.in_bundles[0] if args.in_bundles_names: # If names are given - if len(args.in_bundles_names) != len(bundles): - parser.error("""--in_bundles_names must contain the same number of - element as in --in_bundles.""") + if len(args.in_bundles_names[0]) != len(bundles): + parser.error("--in_bundles_names must contain the same number of " + "elements as in --in_bundles.") bundles_names = args.in_bundles_names[0] + else: + logging.info("Extracting bundles names.") + bundles_names = [] + for bundle in bundles: + bundles_names.append(Path(bundle).name.split(".")[0]) # Set up saving filename options out_dir = args.out_dir From 8b55a63f63b93ecb624a831e1573c98fa603d1ad Mon Sep 17 00:00:00 2001 From: karp2601 Date: Tue, 23 Jul 2024 11:31:39 -0400 Subject: [PATCH 17/20] Fix pep8 --- scripts/scil_bundle_fixel_analysis.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 7b923e2e2..5c4a5a82a 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -113,7 +113,7 @@ def _build_arg_parser(): help='Key to access the data per streamline to use as ' 'weight when computing the maps, \ninstead of the ' 'number of streamlines. [%(default)s].') - + p.add_argument('--max_theta', default=45, help='Maximum angle between streamline and peak to be ' 'associated [%(default)s].') @@ -139,7 +139,7 @@ def _build_arg_parser(): 'will normalize the maps per fixel, \nin each voxel. ' 'If voxel, will normalize the maps per voxel ' '[%(default)s].') - + g2 = p.add_argument_group(title='Output options') g2.add_argument('--split_bundles', action='store_true', @@ -158,16 +158,16 @@ def _build_arg_parser(): g2.add_argument('--bundles_mask', action='store_true', help='If set, save the bundle mask for each bundle.') - + g2.add_argument('--out_dir', default="./", help='Path to the output directory where all the output ' 'files will be saved [%(default)s].') - + g2.add_argument('--prefix', default="", help='Prefix to add to all predetermined output ' 'filenames. \nWe recommand finishing with an ' 'underscore for better readability [%(default)s].') - + g2.add_argument('--suffix', default="", help='Suffix to add to all predetermined output ' 'filenames. \nWe recommand starting with an ' @@ -216,10 +216,10 @@ def main(): "elements as in --in_bundles.") bundles_names = args.in_bundles_names[0] else: - logging.info("Extracting bundles names.") - bundles_names = [] - for bundle in bundles: - bundles_names.append(Path(bundle).name.split(".")[0]) + logging.info("Extracting bundles names.") + bundles_names = [] + for bundle in bundles: + bundles_names.append(Path(bundle).name.split(".")[0]) # Set up saving filename options out_dir = args.out_dir From b190339dc76bc3793d7392cd1c474a1a436b77e5 Mon Sep 17 00:00:00 2001 From: Philippe Karan Date: Wed, 24 Jul 2024 09:45:02 -0400 Subject: [PATCH 18/20] Fixing out_dir stuff --- scripts/scil_bundle_fixel_analysis.py | 40 ++++++++++++++------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 5c4a5a82a..711e6a881 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -3,9 +3,14 @@ """ Analyze bundles at the fixel level using peaks and bundles (.trk). If the bundles files are names as {bundle_name}.trk, simply use the --in_bundles -argument with --in_bundles_names. If it is not the case or you want other names -to be saved, please use --in_bundles_names to provide bundles names IN THE SAME -ORDER as the inputed bundles. +argument without --in_bundles_names. If it is not the case or you want other +names to be saved, please use --in_bundles_names to provide bundles names +IN THE SAME ORDER as the inputed bundles. + +The duration of the script depends heavily on the number of bundles, the number +of streamlines in bundles and the number of processors used. For a standard +tractoflow/rbx_flow output with ~30 bundles, it should not take over 30 +minutes. The script produces various output: @@ -87,7 +92,8 @@ from scilpy.io.utils import (add_overwrite_arg, add_processes_arg, assert_headers_compatible, assert_inputs_exist, - assert_outputs_exist, add_verbose_arg) + add_verbose_arg, + assert_output_dirs_exist_and_empty) from scilpy.tractanalysis.fixel_density import (fixel_density, maps_to_masks) @@ -159,9 +165,10 @@ def _build_arg_parser(): g2.add_argument('--bundles_mask', action='store_true', help='If set, save the bundle mask for each bundle.') - g2.add_argument('--out_dir', default="./", + g2.add_argument('--out_dir', default="fixel_analysis/", help='Path to the output directory where all the output ' - 'files will be saved [%(default)s].') + 'files will be saved. \nCurrent directory by ' + 'default.') g2.add_argument('--prefix', default="", help='Prefix to add to all predetermined output ' @@ -184,13 +191,15 @@ def main(): args = parser.parse_args() logging.getLogger().setLevel(logging.getLevelName(args.verbose)) + # Set up saving filename options + out_dir = args.out_dir + prefix = args.prefix + suffix = args.suffix + if out_dir[-1] != "/": + out_dir += "/" + + assert_output_dirs_exist_and_empty(parser, args, out_dir, create_dir=True) assert_inputs_exist(parser, [args.in_peaks] + args.in_bundles[0]) - assert_outputs_exist(parser, args, ["bundles_LUT.txt", - "fixel_density_maps.nii.gz", - "fixel_density_masks.nii.gz", - "voxel_density_masks.nii.gz", - "nb_bundles_per_fixel.nii.gz", - "nb_bundles_per_voxel.nii.gz"]) assert_headers_compatible(parser, [args.in_peaks] + args.in_bundles[0]) if args.rel_thr < 0 or args.rel_thr > 1: @@ -221,13 +230,6 @@ def main(): for bundle in bundles: bundles_names.append(Path(bundle).name.split(".")[0]) - # Set up saving filename options - out_dir = args.out_dir - prefix = args.prefix - suffix = args.suffix - if out_dir[-1] != "/": - out_dir += "/" - # Compute fixel density maps and masks logging.info("Computing fixel density for all bundles.") fixel_density_maps = fixel_density(peaks, bundles, args.dps_key, From 5727d2668e3bd6057281f1df4d7a65a79403cff9 Mon Sep 17 00:00:00 2001 From: karp2601 Date: Thu, 25 Jul 2024 11:52:21 -0400 Subject: [PATCH 19/20] Removing append from parser --- scripts/scil_bundle_fixel_analysis.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 5c4a5a82a..2babead08 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -100,10 +100,10 @@ def _build_arg_parser(): 'or SH data, use the script scil_fodf_metrics.py ' '\nwith the abs_peaks_and_values option.') - p.add_argument('--in_bundles', nargs='+', action='append', required=True, + p.add_argument('--in_bundles', nargs='+', required=True, help='List of paths of the bundles (.trk) to analyze.') - p.add_argument('--in_bundles_names', nargs='+', action='append', + p.add_argument('--in_bundles_names', nargs='+', help='List of the names of the bundles, in the same order ' 'as they were given. \nIf this argument is not used, ' 'the script assumes that the name of the bundle \nis ' @@ -184,14 +184,14 @@ def main(): args = parser.parse_args() logging.getLogger().setLevel(logging.getLevelName(args.verbose)) - assert_inputs_exist(parser, [args.in_peaks] + args.in_bundles[0]) + assert_inputs_exist(parser, [args.in_peaks] + args.in_bundles) assert_outputs_exist(parser, args, ["bundles_LUT.txt", "fixel_density_maps.nii.gz", "fixel_density_masks.nii.gz", "voxel_density_masks.nii.gz", "nb_bundles_per_fixel.nii.gz", "nb_bundles_per_voxel.nii.gz"]) - assert_headers_compatible(parser, [args.in_peaks] + args.in_bundles[0]) + assert_headers_compatible(parser, [args.in_peaks] + args.in_bundles) if args.rel_thr < 0 or args.rel_thr > 1: parser.error("Argument rel_thr must be a value between 0 and 1.") @@ -209,12 +209,12 @@ def main(): nufo_sf = np.logical_and(is_first_peak, is_second_peak) # Extract bundles and names - bundles = args.in_bundles[0] + bundles = args.in_bundles if args.in_bundles_names: # If names are given - if len(args.in_bundles_names[0]) != len(bundles): + if len(args.in_bundles_names) != len(bundles): parser.error("--in_bundles_names must contain the same number of " "elements as in --in_bundles.") - bundles_names = args.in_bundles_names[0] + bundles_names = args.in_bundles_names else: logging.info("Extracting bundles names.") bundles_names = [] From 81129743a9e7c553cdf8524f744e1490b7e25564 Mon Sep 17 00:00:00 2001 From: karp2601 Date: Thu, 25 Jul 2024 13:08:26 -0400 Subject: [PATCH 20/20] Fixing merge bug --- scripts/scil_bundle_fixel_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/scil_bundle_fixel_analysis.py b/scripts/scil_bundle_fixel_analysis.py index 5eb51df35..68e69ec41 100644 --- a/scripts/scil_bundle_fixel_analysis.py +++ b/scripts/scil_bundle_fixel_analysis.py @@ -199,8 +199,8 @@ def main(): out_dir += "/" assert_output_dirs_exist_and_empty(parser, args, out_dir, create_dir=True) - assert_inputs_exist(parser, [args.in_peaks] + args.in_bundles[0]) - assert_headers_compatible(parser, [args.in_peaks] + args.in_bundles[0]) + assert_inputs_exist(parser, [args.in_peaks] + args.in_bundles) + assert_headers_compatible(parser, [args.in_peaks] + args.in_bundles) if args.rel_thr < 0 or args.rel_thr > 1: parser.error("Argument rel_thr must be a value between 0 and 1.")