From 4924ce93409c1ff7da495d52ec54137b5fd134c4 Mon Sep 17 00:00:00 2001 From: frheault Date: Thu, 22 Aug 2024 14:34:16 -0400 Subject: [PATCH 1/7] working draft --- scripts/scil_lesions_generate_nawm.py | 81 +++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100755 scripts/scil_lesions_generate_nawm.py diff --git a/scripts/scil_lesions_generate_nawm.py b/scripts/scil_lesions_generate_nawm.py new file mode 100755 index 000000000..87c921407 --- /dev/null +++ b/scripts/scil_lesions_generate_nawm.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +TODO +""" + +import argparse +import json +import logging +import os + +import nibabel as nib +import numpy as np + +from scilpy.image.labels import get_data_as_labels +from scilpy.io.image import get_data_as_mask +from scilpy.io.streamlines import load_tractogram_with_reference +from scilpy.io.utils import (add_overwrite_arg, assert_inputs_exist, + add_json_args, assert_outputs_exist, + add_verbose_arg, add_reference_arg, + assert_headers_compatible) +from scilpy.segment.streamlines import filter_grid_roi +from scilpy.tractanalysis.streamlines_metrics import compute_tract_counts_map +from scilpy.utils.filenames import split_name_with_nii +from scilpy.utils.metrics_tools import compute_lesion_stats + + +def _build_arg_parser(): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawTextHelpFormatter) + + p.add_argument('in_image', + help='Lesions file as mask OR labels (.nii.gz).') + p.add_argument('out_image', + help='TODO') + + p.add_argument('--nb_ring', type=int, + help='Integer representing the number of rings to be ' + 'created.') + p.add_argument('--ring_thickness', type=int, + help='Integer representing the thickness of the rings to be ' + 'created. Used for voxel dilation passes.') + # TODO split 4D into many files + + add_verbose_arg(p) + add_overwrite_arg(p) + + return p + + +def main(): + parser = _build_arg_parser() + args = parser.parse_args() + logging.getLogger().setLevel(logging.getLevelName(args.verbose)) + + + assert_inputs_exist(parser, args.in_image) + assert_outputs_exist(parser, args, args.out_image) + + lesion_img = nib.load(args.in_image) + lesion_atlas = get_data_as_labels(lesion_img) + + if np.unique(lesion_atlas).size == 1: + raise ValueError('Input lesion map is empty.') + is_binary = True if np.unique(lesion_atlas).size == 2 else False + print(is_binary) + labels = np.unique(lesion_atlas)[1:] + nawm = np.zeros(lesion_atlas.shape + (len(labels),), dtype=np.uint16) + for i, label in enumerate(labels): + nawm[..., i] = i + 1 + + if is_binary: + nawm = np.squeeze(nawm) + + nib.save(nib.Nifti1Image(nawm, lesion_img.affine), args.out_image) + + + +if __name__ == "__main__": + main() From 5d4a5be76ce0e9144e46ad890c17b1403dc85dcd Mon Sep 17 00:00:00 2001 From: frheault Date: Thu, 22 Aug 2024 14:55:29 -0400 Subject: [PATCH 2/7] Working example with distance map --- scripts/scil_lesions_generate_nawm.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/scripts/scil_lesions_generate_nawm.py b/scripts/scil_lesions_generate_nawm.py index 87c921407..4bbb6be57 100755 --- a/scripts/scil_lesions_generate_nawm.py +++ b/scripts/scil_lesions_generate_nawm.py @@ -14,6 +14,7 @@ import numpy as np from scilpy.image.labels import get_data_as_labels +from scilpy.image.volume_operations import compute_distance_map from scilpy.io.image import get_data_as_mask from scilpy.io.streamlines import load_tractogram_with_reference from scilpy.io.utils import (add_overwrite_arg, assert_inputs_exist, @@ -35,13 +36,14 @@ def _build_arg_parser(): p.add_argument('out_image', help='TODO') - p.add_argument('--nb_ring', type=int, + p.add_argument('--nb_ring', type=int, default=4, help='Integer representing the number of rings to be ' 'created.') - p.add_argument('--ring_thickness', type=int, + p.add_argument('--ring_thickness', type=int, default=4, help='Integer representing the thickness of the rings to be ' 'created. Used for voxel dilation passes.') # TODO split 4D into many files + # TODO Lesions as 1 (default) vs lesions as 0 add_verbose_arg(p) add_overwrite_arg(p) @@ -54,7 +56,6 @@ def main(): args = parser.parse_args() logging.getLogger().setLevel(logging.getLevelName(args.verbose)) - assert_inputs_exist(parser, args.in_image) assert_outputs_exist(parser, args, args.out_image) @@ -66,9 +67,14 @@ def main(): is_binary = True if np.unique(lesion_atlas).size == 2 else False print(is_binary) labels = np.unique(lesion_atlas)[1:] - nawm = np.zeros(lesion_atlas.shape + (len(labels),), dtype=np.uint16) + nawm = np.zeros(lesion_atlas.shape + (len(labels),), dtype=float) + inverse_mask = np.ones(lesion_atlas.shape, dtype=np.uint8) for i, label in enumerate(labels): - nawm[..., i] = i + 1 + curr_mask = np.zeros(lesion_atlas.shape, dtype=np.uint8) + curr_mask[lesion_atlas == label] = 1 + nawm[..., i] = compute_distance_map(inverse_mask, + curr_mask, + max_distance=args.nb_ring * args.ring_thickness) if is_binary: nawm = np.squeeze(nawm) @@ -76,6 +82,5 @@ def main(): nib.save(nib.Nifti1Image(nawm, lesion_img.affine), args.out_image) - if __name__ == "__main__": main() From 6f1f990f7aa9ec6d623c473330f453247686a29c Mon Sep 17 00:00:00 2001 From: frheault Date: Mon, 26 Aug 2024 12:22:53 -0400 Subject: [PATCH 3/7] added a test and some modifications in the script --- scilpy/image/tests/test_volume_operations.py | 20 +++++++- scilpy/image/volume_operations.py | 48 ++++++++++++++++++++ scripts/scil_lesions_generate_nawm.py | 23 ++-------- 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/scilpy/image/tests/test_volume_operations.py b/scilpy/image/tests/test_volume_operations.py index 3772167e7..e05e2d3f7 100644 --- a/scilpy/image/tests/test_volume_operations.py +++ b/scilpy/image/tests/test_volume_operations.py @@ -14,7 +14,8 @@ merge_metrics, normalize_metric, resample_volume, register_image, mask_data_with_default_cube, - compute_distance_map) + compute_distance_map, + compute_nawm) from scilpy.io.fetcher import fetch_data, get_testing_files_dict from scilpy.image.utils import compute_nifti_bounding_box @@ -298,3 +299,20 @@ def test_compute_distance_map_wrong_shape(): assert False except ValueError: assert True + +def test_compute_nawm(): + lesion_img = np.zeros((3, 3, 3)) + lesion_img[1, 1, 1] = 1 + + nawm = compute_nawm(lesion_img, nb_ring=1, ring_thickness=1) + print(np.sum(nawm)) + assert np.sum(nawm) == 26 + + nawm = compute_nawm(lesion_img, nb_ring=2, ring_thickness=1) + print(np.sum(nawm)) + assert np.sum(nawm) == 98 + + nawm = compute_nawm(lesion_img, nb_ring=1, ring_thickness=2) + print(np.sum(nawm)) + assert np.sum(nawm) == 80 + diff --git a/scilpy/image/volume_operations.py b/scilpy/image/volume_operations.py index 56b2731e1..f90e4946a 100644 --- a/scilpy/image/volume_operations.py +++ b/scilpy/image/volume_operations.py @@ -713,6 +713,8 @@ def compute_distance_map(mask_1, mask_2, symmetric=False, If True, compute the symmetric distance map. Default is np.inf max_distance: float, optional Maximum distance to consider for kdtree exploration. Default is None. + If you put any value, coordinates further than this value will be + considered as np.inf. Returns ------- @@ -736,3 +738,49 @@ def compute_distance_map(mask_1, mask_2, symmetric=False, distance_map[np.where(mask_2)] = distance return distance_map + + +def compute_nawm(lesion_atlas, nb_ring, ring_thickness): + """ + TODO + + le centre de la lesion est toujours a 1 donc les rings vont de 2 jusqu'a nb_ring+1 + + Parameters + ---------- + + Returns + ------- + + """ + if np.unique(lesion_atlas).size == 1: + raise ValueError('Input lesion map is empty.') + is_binary = True if np.unique(lesion_atlas).size == 2 else False + labels = np.unique(lesion_atlas)[1:] + nawm = np.zeros(lesion_atlas.shape + (len(labels),), dtype=float) + inverse_mask = np.ones(lesion_atlas.shape, dtype=np.uint8) + for i, label in enumerate(labels): + curr_mask = np.zeros(lesion_atlas.shape, dtype=np.uint8) + curr_mask[lesion_atlas == label] = 1 + curr_dist_map = compute_distance_map(inverse_mask, + curr_mask, + max_distance=nb_ring * ring_thickness) + curr_dist_map[np.isinf(curr_dist_map)] = 0 + # Mask to remember where values were computed + to_increase_mask = np.zeros(lesion_atlas.shape, dtype=np.uint8) + to_increase_mask[curr_dist_map > 0] = 1 + to_increase_mask[curr_mask > 0] = 1 + + # Faire le round juste dans la distance map? + # Avec gros parametres la lesions semble grossir + curr_dist_map[to_increase_mask > 0] += 1 + curr_dist_map = np.round(curr_dist_map / ring_thickness) + curr_dist_map[to_increase_mask > 0] += 1 + + print(curr_dist_map.min(), curr_dist_map.max()) + nawm[..., i] = curr_dist_map + + if is_binary: + nawm = np.squeeze(nawm) + + return nawm.astype(np.uint16) \ No newline at end of file diff --git a/scripts/scil_lesions_generate_nawm.py b/scripts/scil_lesions_generate_nawm.py index 4bbb6be57..30792163e 100755 --- a/scripts/scil_lesions_generate_nawm.py +++ b/scripts/scil_lesions_generate_nawm.py @@ -14,7 +14,7 @@ import numpy as np from scilpy.image.labels import get_data_as_labels -from scilpy.image.volume_operations import compute_distance_map +from scilpy.image.volume_operations import compute_distance_map, compute_nawm from scilpy.io.image import get_data_as_mask from scilpy.io.streamlines import load_tractogram_with_reference from scilpy.io.utils import (add_overwrite_arg, assert_inputs_exist, @@ -36,10 +36,10 @@ def _build_arg_parser(): p.add_argument('out_image', help='TODO') - p.add_argument('--nb_ring', type=int, default=4, + p.add_argument('--nb_ring', type=int, default=3, help='Integer representing the number of rings to be ' 'created.') - p.add_argument('--ring_thickness', type=int, default=4, + p.add_argument('--ring_thickness', type=int, default=2, help='Integer representing the thickness of the rings to be ' 'created. Used for voxel dilation passes.') # TODO split 4D into many files @@ -62,22 +62,7 @@ def main(): lesion_img = nib.load(args.in_image) lesion_atlas = get_data_as_labels(lesion_img) - if np.unique(lesion_atlas).size == 1: - raise ValueError('Input lesion map is empty.') - is_binary = True if np.unique(lesion_atlas).size == 2 else False - print(is_binary) - labels = np.unique(lesion_atlas)[1:] - nawm = np.zeros(lesion_atlas.shape + (len(labels),), dtype=float) - inverse_mask = np.ones(lesion_atlas.shape, dtype=np.uint8) - for i, label in enumerate(labels): - curr_mask = np.zeros(lesion_atlas.shape, dtype=np.uint8) - curr_mask[lesion_atlas == label] = 1 - nawm[..., i] = compute_distance_map(inverse_mask, - curr_mask, - max_distance=args.nb_ring * args.ring_thickness) - - if is_binary: - nawm = np.squeeze(nawm) + nawm = compute_nawm(lesion_atlas, args.nb_ring, args.ring_thickness) nib.save(nib.Nifti1Image(nawm, lesion_img.affine), args.out_image) From d422ff222ade5be9d25bfc308f7e213258609972 Mon Sep 17 00:00:00 2001 From: frheault Date: Wed, 4 Sep 2024 10:58:44 -0400 Subject: [PATCH 4/7] All done: test + documentation + split 4D --- scilpy/image/tests/test_volume_operations.py | 30 +++++--- scilpy/image/volume_operations.py | 55 ++++++++++----- scripts/scil_lesions_generate_nawm.py | 72 +++++++++++++++----- 3 files changed, 114 insertions(+), 43 deletions(-) diff --git a/scilpy/image/tests/test_volume_operations.py b/scilpy/image/tests/test_volume_operations.py index e05e2d3f7..2fc4473b0 100644 --- a/scilpy/image/tests/test_volume_operations.py +++ b/scilpy/image/tests/test_volume_operations.py @@ -300,19 +300,31 @@ def test_compute_distance_map_wrong_shape(): except ValueError: assert True -def test_compute_nawm(): + +def test_compute_nawm_3D(): lesion_img = np.zeros((3, 3, 3)) lesion_img[1, 1, 1] = 1 - nawm = compute_nawm(lesion_img, nb_ring=1, ring_thickness=1) - print(np.sum(nawm)) - assert np.sum(nawm) == 26 + nawm = compute_nawm(lesion_img, nb_ring=0, ring_thickness=2) + assert np.sum(nawm) == 1 - nawm = compute_nawm(lesion_img, nb_ring=2, ring_thickness=1) - print(np.sum(nawm)) - assert np.sum(nawm) == 98 + try: + nawm = compute_nawm(lesion_img, nb_ring=2, ring_thickness=0) + assert False + except ValueError: + assert True nawm = compute_nawm(lesion_img, nb_ring=1, ring_thickness=2) - print(np.sum(nawm)) - assert np.sum(nawm) == 80 + assert np.sum(nawm) == 53 + +def test_compute_nawm_4D(): + lesion_img = np.zeros((10, 10, 10)) + lesion_img[4, 4, 4] = 1 + lesion_img[2, 2, 2] = 2 + + nawm = compute_nawm(lesion_img, nb_ring=2, ring_thickness=1) + assert nawm.shape == (10, 10, 10, 2) + val, count = np.unique(nawm[..., 0], return_counts=True) + assert np.array_equal(val, [0, 1, 2, 3]) + assert np.array_equal(count, [967, 1, 6, 26]) diff --git a/scilpy/image/volume_operations.py b/scilpy/image/volume_operations.py index f90e4946a..33c14e1c9 100644 --- a/scilpy/image/volume_operations.py +++ b/scilpy/image/volume_operations.py @@ -740,47 +740,70 @@ def compute_distance_map(mask_1, mask_2, symmetric=False, return distance_map -def compute_nawm(lesion_atlas, nb_ring, ring_thickness): +def compute_nawm(lesion_atlas, nb_ring, ring_thickness, mask=None): """ - TODO + Compute the NAWM (Normal Appearing White Matter) from a lesion map. + The rings go from 2 to nb_ring + 2, with the lesion being 1. - le centre de la lesion est toujours a 1 donc les rings vont de 2 jusqu'a nb_ring+1 + The optional mask is used to compute the rings only in the mask + region. This can be useful to avoid useless computation. + + If the lesion_atlas is binary, the output will be 3D. If the lesion_atlas + is a label map, the output will be 4D, with each label having its own NAWM. Parameters ---------- + lesion_atlas: np.ndarray + Lesion map. Can be binary or label map. + nb_ring: int + Number of rings to compute. + ring_thickness: int + Thickness of the rings. + mask: np.ndarray, optional + Mask where to compute the NAWM. Default is None. Returns ------- + nawm: np.ndarray + NAWM volume(s), 3D if binary lesion map, 4D if label lesion map. """ + if ring_thickness < 1: + raise ValueError("Ring thickness must be at least 1.") + if np.unique(lesion_atlas).size == 1: raise ValueError('Input lesion map is empty.') is_binary = True if np.unique(lesion_atlas).size == 2 else False labels = np.unique(lesion_atlas)[1:] nawm = np.zeros(lesion_atlas.shape + (len(labels),), dtype=float) - inverse_mask = np.ones(lesion_atlas.shape, dtype=np.uint8) + + if mask is None: + mask = np.ones(lesion_atlas.shape, dtype=np.uint8) + + max_distance = (nb_ring * ring_thickness) + 1 + for i, label in enumerate(labels): curr_mask = np.zeros(lesion_atlas.shape, dtype=np.uint8) curr_mask[lesion_atlas == label] = 1 - curr_dist_map = compute_distance_map(inverse_mask, - curr_mask, - max_distance=nb_ring * ring_thickness) + curr_dist_map = compute_distance_map(mask, + curr_mask, + max_distance=max_distance) curr_dist_map[np.isinf(curr_dist_map)] = 0 + # Mask to remember where values were computed to_increase_mask = np.zeros(lesion_atlas.shape, dtype=np.uint8) to_increase_mask[curr_dist_map > 0] = 1 - to_increase_mask[curr_mask > 0] = 1 - - # Faire le round juste dans la distance map? - # Avec gros parametres la lesions semble grossir - curr_dist_map[to_increase_mask > 0] += 1 - curr_dist_map = np.round(curr_dist_map / ring_thickness) + + # Compute the rings. The lesion should be 1, and the first ring + # should be 2, and the max ring should be nb_ring + 1. + curr_dist_map = np.ceil(curr_dist_map / ring_thickness) curr_dist_map[to_increase_mask > 0] += 1 - - print(curr_dist_map.min(), curr_dist_map.max()) + curr_dist_map[curr_mask > 0] += 1 + curr_dist_map[curr_dist_map > nb_ring + 1] = 0 + nawm[..., i] = curr_dist_map if is_binary: nawm = np.squeeze(nawm) - return nawm.astype(np.uint16) \ No newline at end of file + return nawm.astype(np.uint16) diff --git a/scripts/scil_lesions_generate_nawm.py b/scripts/scil_lesions_generate_nawm.py index 30792163e..8e7c8b32a 100755 --- a/scripts/scil_lesions_generate_nawm.py +++ b/scripts/scil_lesions_generate_nawm.py @@ -2,11 +2,21 @@ # -*- coding: utf-8 -*- """ -TODO +The NAWM (Normal Appearing White Matter) is the white matter that is +neighboring a lesion. It is used to compute metrics in the white matter +surrounding lesions. + +This script will generate concentric rings around the lesions, with the rings +going from 2 to nb_ring + 2, with the lesion being 1. + +The optional mask is used to compute the rings only in the mask +region. This can be useful to avoid useless computation. + +If the lesion_atlas is binary, the output will be 3D. If the lesion_atlas +is a label map, the output will be 4D, with each label having its own NAWM. """ import argparse -import json import logging import os @@ -14,17 +24,13 @@ import numpy as np from scilpy.image.labels import get_data_as_labels -from scilpy.image.volume_operations import compute_distance_map, compute_nawm +from scilpy.image.volume_operations import compute_nawm from scilpy.io.image import get_data_as_mask -from scilpy.io.streamlines import load_tractogram_with_reference from scilpy.io.utils import (add_overwrite_arg, assert_inputs_exist, - add_json_args, assert_outputs_exist, - add_verbose_arg, add_reference_arg, - assert_headers_compatible) -from scilpy.segment.streamlines import filter_grid_roi -from scilpy.tractanalysis.streamlines_metrics import compute_tract_counts_map + assert_outputs_exist, + assert_output_dirs_exist_and_empty, + add_verbose_arg) from scilpy.utils.filenames import split_name_with_nii -from scilpy.utils.metrics_tools import compute_lesion_stats def _build_arg_parser(): @@ -34,7 +40,7 @@ def _build_arg_parser(): p.add_argument('in_image', help='Lesions file as mask OR labels (.nii.gz).') p.add_argument('out_image', - help='TODO') + help='Output NAWM file (.nii.gz).') p.add_argument('--nb_ring', type=int, default=3, help='Integer representing the number of rings to be ' @@ -42,8 +48,11 @@ def _build_arg_parser(): p.add_argument('--ring_thickness', type=int, default=2, help='Integer representing the thickness of the rings to be ' 'created. Used for voxel dilation passes.') - # TODO split 4D into many files - # TODO Lesions as 1 (default) vs lesions as 0 + p.add_argument('--mask', + help='Mask where to compute the NAWM (e.g WM mask).') + p.add_argument('--split_4D', metavar='OUT_DIR', + help='Provided lesions will be split into multiple files.\n' + 'The output files will be named using out_image as a prefix.') add_verbose_arg(p) add_overwrite_arg(p) @@ -56,15 +65,42 @@ def main(): args = parser.parse_args() logging.getLogger().setLevel(logging.getLevelName(args.verbose)) - assert_inputs_exist(parser, args.in_image) - assert_outputs_exist(parser, args, args.out_image) + if args.nb_ring < 1: + parser.error('The number of rings must be at least 1.') + if args.ring_thickness < 1: + parser.error('The ring thickness must be at least 1.') + + assert_inputs_exist(parser, args.in_image, args.mask) + if not args.split_4D: + assert_outputs_exist(parser, args, args.out_image) lesion_img = nib.load(args.in_image) lesion_atlas = get_data_as_labels(lesion_img) - nawm = compute_nawm(lesion_atlas, args.nb_ring, args.ring_thickness) - - nib.save(nib.Nifti1Image(nawm, lesion_img.affine), args.out_image) + if args.split_4D and np.unique(lesion_atlas).size <= 2: + raise ValueError('Split only works with multiple lesion labels') + else: + assert_output_dirs_exist_and_empty(parser, args, args.split_4D) + + if args.mask: + mask_img = nib.load(args.mask) + mask_data = get_data_as_mask(mask_img) + else: + mask_data = None + + nawm = compute_nawm(lesion_atlas, args.nb_ring, args.ring_thickness, + mask=mask_data) + + if args.split_4D: + for i in range(nawm.shape[-1]): + base, ext = split_name_with_nii(args.in_image) + base = os.path.basename(base) + lesion_name = os.path.join(args.split_4D, + f'{base}_nawm_{i+1}{ext}') + nib.save(nib.Nifti1Image(nawm[..., i], lesion_img.affine), + lesion_name) + else: + nib.save(nib.Nifti1Image(nawm, lesion_img.affine), args.out_image) if __name__ == "__main__": From 500ad946a5e130d8e81bf221bf7c9fb8175597ab Mon Sep 17 00:00:00 2001 From: frheault Date: Wed, 4 Sep 2024 11:09:25 -0400 Subject: [PATCH 5/7] Detailled the --split_4D --- scripts/scil_lesions_generate_nawm.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/scil_lesions_generate_nawm.py b/scripts/scil_lesions_generate_nawm.py index 8e7c8b32a..b295af1c0 100755 --- a/scripts/scil_lesions_generate_nawm.py +++ b/scripts/scil_lesions_generate_nawm.py @@ -6,14 +6,16 @@ neighboring a lesion. It is used to compute metrics in the white matter surrounding lesions. -This script will generate concentric rings around the lesions, with the rings +This script will generate concentric rings around the lesions, with the rings going from 2 to nb_ring + 2, with the lesion being 1. The optional mask is used to compute the rings only in the mask region. This can be useful to avoid useless computation. If the lesion_atlas is binary, the output will be 3D. If the lesion_atlas -is a label map, the output will be 4D, with each label having its own NAWM. +is a label map, the output will be either: + - 4D, with each label having its own NAWM. + - 3D, if using --split_4D and saved into a folder as multiple 3D files. """ import argparse From b032765c8695107c3d0074d8b1c31d625029081f Mon Sep 17 00:00:00 2001 From: frheault Date: Mon, 16 Sep 2024 09:25:31 -0400 Subject: [PATCH 6/7] Fix Stan comments and add tests --- scripts/scil_lesions_generate_nawm.py | 30 ++++++++++++++++----- scripts/tests/test_lesions_generate_nawm.py | 29 ++++++++++++++++++++ 2 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 scripts/tests/test_lesions_generate_nawm.py diff --git a/scripts/scil_lesions_generate_nawm.py b/scripts/scil_lesions_generate_nawm.py index b295af1c0..1407796a7 100755 --- a/scripts/scil_lesions_generate_nawm.py +++ b/scripts/scil_lesions_generate_nawm.py @@ -16,6 +16,8 @@ is a label map, the output will be either: - 4D, with each label having its own NAWM. - 3D, if using --split_4D and saved into a folder as multiple 3D files. + +WARNING: Voxels must be isotropic. """ import argparse @@ -40,21 +42,25 @@ def _build_arg_parser(): formatter_class=argparse.RawTextHelpFormatter) p.add_argument('in_image', - help='Lesions file as mask OR labels (.nii.gz).') + help='Lesions file as mask OR labels (.nii.gz).\n' + '(must be uint8 for mask, uint16 for labels).') p.add_argument('out_image', - help='Output NAWM file (.nii.gz).') + help='Output NAWM file (.nii.gz).\n' + 'If using --split_4D, this will be the prefix of the ' + 'output files.') p.add_argument('--nb_ring', type=int, default=3, help='Integer representing the number of rings to be ' 'created.') p.add_argument('--ring_thickness', type=int, default=2, - help='Integer representing the thickness of the rings to be ' - 'created. Used for voxel dilation passes.') + help='Integer representing the thickness (in voxels) of ' + 'the rings to be created.') p.add_argument('--mask', help='Mask where to compute the NAWM (e.g WM mask).') p.add_argument('--split_4D', metavar='OUT_DIR', help='Provided lesions will be split into multiple files.\n' - 'The output files will be named using out_image as a prefix.') + 'The output files will be named using out_image as ' + 'a prefix.') add_verbose_arg(p) add_overwrite_arg(p) @@ -78,12 +84,21 @@ def main(): lesion_img = nib.load(args.in_image) lesion_atlas = get_data_as_labels(lesion_img) + voxel_size = lesion_img.header.get_zooms() + + if not np.allclose(voxel_size, np.mean(voxel_size)): + raise ValueError('Voxels must be isotropic.') if args.split_4D and np.unique(lesion_atlas).size <= 2: raise ValueError('Split only works with multiple lesion labels') - else: + elif args.split_4D: assert_output_dirs_exist_and_empty(parser, args, args.split_4D) + if not args.split_4D and np.unique(lesion_atlas).size > 2: + logging.warning('The input lesion atlas has multiple labels. ' + 'Converting to binary.') + lesion_atlas[lesion_atlas > 0] = 1 + if args.mask: mask_img = nib.load(args.mask) mask_data = get_data_as_mask(mask_img) @@ -95,10 +110,11 @@ def main(): if args.split_4D: for i in range(nawm.shape[-1]): + label = np.unique(lesion_atlas)[i+1] base, ext = split_name_with_nii(args.in_image) base = os.path.basename(base) lesion_name = os.path.join(args.split_4D, - f'{base}_nawm_{i+1}{ext}') + f'{base}_nawm_{label}{ext}') nib.save(nib.Nifti1Image(nawm[..., i], lesion_img.affine), lesion_name) else: diff --git a/scripts/tests/test_lesions_generate_nawm.py b/scripts/tests/test_lesions_generate_nawm.py new file mode 100644 index 000000000..9c3134d7d --- /dev/null +++ b/scripts/tests/test_lesions_generate_nawm.py @@ -0,0 +1,29 @@ +#!/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 + + +# If they already exist, this only takes 5 seconds (check md5sum) +fetch_data(get_testing_files_dict(), keys=['atlas.zip']) +tmp_dir = tempfile.TemporaryDirectory() + + +def test_help_option(script_runner): + ret = script_runner.run('scil_lesions_generate_nawm.py', '--help') + assert ret.success + + +def test_execution_atlas(script_runner, monkeypatch): + monkeypatch.chdir(os.path.expanduser(tmp_dir.name)) + in_atlas = os.path.join(SCILPY_HOME, 'atlas', + 'atlas_freesurfer_v2_single_brainstem.nii.gz') + ret = script_runner.run('scil_lesions_generate_nawm.py', in_atlas, + 'nawm.nii.gz', '--nb_ring', '3', + '--ring_thickness', '2') + assert ret.success From 829788d0e523d24dcbaaee932e4c80e817e28f89 Mon Sep 17 00:00:00 2001 From: frheault Date: Fri, 11 Oct 2024 11:06:40 -0400 Subject: [PATCH 7/7] Fix errors in tests --- requirements.txt | 2 +- scripts/tests/test_labels_from_mask.py | 2 +- scripts/tests/test_volume_stats_in_labels.py | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index a9b9d3207..6458a393c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ docopt==0.6.* dvc==3.48.* dvc-http==2.32.* formulaic==0.3.* -fury==0.10.* +fury==0.11.* future==0.18.* GitPython==3.1.* h5py==3.10.* diff --git a/scripts/tests/test_labels_from_mask.py b/scripts/tests/test_labels_from_mask.py index 335abc195..dc6cb53d7 100644 --- a/scripts/tests/test_labels_from_mask.py +++ b/scripts/tests/test_labels_from_mask.py @@ -8,7 +8,7 @@ from scilpy.io.fetcher import fetch_data, get_testing_files_dict # If they already exist, this only takes 5 seconds (check md5sum) -fetch_data(get_testing_files_dict(), keys=['atlas.zip']) +fetch_data(get_testing_files_dict(), keys=['tractograms.zip']) tmp_dir = tempfile.TemporaryDirectory() diff --git a/scripts/tests/test_volume_stats_in_labels.py b/scripts/tests/test_volume_stats_in_labels.py index 6190f3d46..83bd07b8f 100644 --- a/scripts/tests/test_volume_stats_in_labels.py +++ b/scripts/tests/test_volume_stats_in_labels.py @@ -1,8 +1,13 @@ #!/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=['plot.zip']) +tmp_dir = tempfile.TemporaryDirectory() def test_help_option(script_runner): @@ -10,7 +15,8 @@ def test_help_option(script_runner): assert ret.success -def test_execution(script_runner): +def test_execution(script_runner, monkeypatch): + monkeypatch.chdir(os.path.expanduser(tmp_dir.name)) in_map = os.path.join(SCILPY_HOME, 'plot', 'fa.nii.gz') in_atlas = os.path.join(SCILPY_HOME, 'plot', 'atlas_brainnetome.nii.gz') atlas_lut = os.path.join(SCILPY_HOME, 'plot', 'atlas_brainnetome.json')