From 89c056a3b153b5aea0204e28508b861348c0e356 Mon Sep 17 00:00:00 2001 From: Lucius <30386169+LucSam@users.noreply.github.com> Date: Thu, 2 Jan 2025 14:28:57 +0100 Subject: [PATCH 01/11] 5ttgen: Add Deep Atropos algorithm --- cmd/5ttgen/deep_atropos.py | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 cmd/5ttgen/deep_atropos.py diff --git a/cmd/5ttgen/deep_atropos.py b/cmd/5ttgen/deep_atropos.py new file mode 100644 index 0000000000..665795a238 --- /dev/null +++ b/cmd/5ttgen/deep_atropos.py @@ -0,0 +1,45 @@ +import os +from mrtrix3 import MRtrixError +from mrtrix3 import app, image, path, run + +def usage(base_parser, subparsers): #pylint: disable=unused-variable + parser = subparsers.add_parser('deep_atropos', parents=[base_parser]) + parser.set_author('Lucius S. Fekonja (lucius.fekonja[at]charite.de)') + parser.set_synopsis('Generate the 5TT image based on a Deep Atropos segmentation image') + parser.add_argument('input', help='The input Deep Atropos segmentation image') + parser.add_argument('output', help='The output 5TT image') + +def check_output_paths(): #pylint: disable=unused-variable + app.check_output_path(app.ARGS.output) + +def check_deep_atropos_input(image_path): + dim = image.Header(image_path).size() + if len(dim) != 3: + raise MRtrixError('Image \'' + image_path + '\' does not look like Deep Atropos segmentation (number of spatial dimensions is not 3)') + +def get_inputs(): #pylint: disable=unused-variable + check_deep_atropos_input(path.from_user(app.ARGS.input, False)) + run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('input.mif')) + +def execute(): #pylint: disable=unused-variable + # Generate the images related to each tissue + run.command('mrcalc input.mif 1 -eq CSF.mif') + run.command('mrcalc input.mif 2 -eq cGM.mif') + run.command('mrcalc input.mif 3 -eq WM1.mif') + run.command('mrcalc input.mif 5 -eq WM2.mif') + run.command('mrcalc input.mif 6 -eq WM3.mif') + run.command('mrmath WM1.mif WM2.mif WM3.mif sum WM.mif') + run.command('mrcalc input.mif 4 -eq sGM.mif') + + # Create an empty lesion image + run.command('mrcalc WM.mif 0 -mul lsn.mif') + + # Convert into the 5tt format + run.command('mrcat cGM.mif sGM.mif WM.mif CSF.mif lsn.mif 5tt.mif -axis 3') + + if app.ARGS.nocrop: + run.function(os.rename, '5tt.mif', 'result.mif') + else: + run.command('mrmath 5tt.mif sum - -axis 3 | mrthreshold - - -abs 0.5 | mrgrid 5tt.mif crop result.mif -mask -') + + run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) From 25a2fecaf91a79c0020575d1289d2ef79008a13c Mon Sep 17 00:00:00 2001 From: Lucius <30386169+LucSam@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:08:39 +0100 Subject: [PATCH 02/11] 5ttgen: Update Deep Atropos algorithm implementation --- python/mrtrix3/commands/5ttgen/__init__.py | 2 +- .../mrtrix3/commands/5ttgen/deep_atropos.py | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 python/mrtrix3/commands/5ttgen/deep_atropos.py diff --git a/python/mrtrix3/commands/5ttgen/__init__.py b/python/mrtrix3/commands/5ttgen/__init__.py index 3f72263f4c..4fe461562e 100644 --- a/python/mrtrix3/commands/5ttgen/__init__.py +++ b/python/mrtrix3/commands/5ttgen/__init__.py @@ -14,4 +14,4 @@ # For more details, see http://www.mrtrix.org/. # pylint: disable=unused-variable -ALGORITHMS = ['freesurfer', 'fsl', 'gif', 'hsvs'] +ALGORITHMS = ['freesurfer', 'fsl', 'gif', 'hsvs', 'deep_atropos'] diff --git a/python/mrtrix3/commands/5ttgen/deep_atropos.py b/python/mrtrix3/commands/5ttgen/deep_atropos.py new file mode 100644 index 0000000000..8d043c7e67 --- /dev/null +++ b/python/mrtrix3/commands/5ttgen/deep_atropos.py @@ -0,0 +1,68 @@ +import os.path, shutil +from mrtrix3 import MRtrixError +from mrtrix3 import app, image, path, run + +def usage(base_parser, subparsers): + parser = subparsers.add_parser('deep_atropos', parents=[base_parser]) + parser.set_author('Lucius S. Fekonja (lucius.fekonja[at]charite.de)') + parser.set_synopsis('Generate the 5TT image based on a Deep Atropos segmentation image') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input Deep Atropos segmentation image') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output 5TT image') + +def check_deep_atropos_input(image_path): + dim = image.Header(image_path).size() + if len(dim) != 3: + raise MRtrixError(f'Image \'{str(image_path)}\' does not look like Deep Atropos segmentation (number of spatial dimensions is not 3)') + +def execute(): + check_deep_atropos_input(app.ARGS.input) + run.command(['mrconvert', app.ARGS.input, 'input.mif']) + + # Generate initial tissue-specific maps + run.command('mrcalc input.mif 1 -eq CSF.mif') + run.command('mrcalc input.mif 2 -eq cGM.mif') + run.command('mrcalc input.mif 3 -eq WM1.mif') + run.command('mrcalc input.mif 5 -eq WM2.mif') + run.command('mrcalc input.mif 6 -eq WM3.mif') + run.command('mrmath WM1.mif WM2.mif WM3.mif sum WM.mif') + run.command('mrcalc input.mif 4 -eq sGM.mif') + + # Run connected components on WM for cleanup + run.command('mrthreshold WM.mif - -abs 0.001 | ' + 'maskfilter - connect - -connectivity | ' + 'mrcalc 1 - 1 -gt -sub remove_unconnected_wm_mask.mif -datatype bit') + + # Preserve CSF and handle volume fractions + run.command('mrcalc CSF.mif remove_unconnected_wm_mask.mif -mult csf_clean.mif') + run.command('mrcalc 1.0 csf_clean.mif -sub sGM.mif -min sgm_clean.mif') + + # Calculate multiplier for volume fraction correction + run.command('mrcalc 1.0 csf_clean.mif sgm_clean.mif -add -sub cGM.mif WM.mif -add -div multiplier.mif') + run.command('mrcalc multiplier.mif -finite multiplier.mif 0.0 -if multiplier_noNAN.mif') + + # Apply corrections + run.command('mrcalc cGM.mif multiplier_noNAN.mif -mult remove_unconnected_wm_mask.mif -mult cgm_clean.mif') + run.command('mrcalc WM.mif multiplier_noNAN.mif -mult remove_unconnected_wm_mask.mif -mult wm_clean.mif') + + # Create empty pathological tissue map + run.command('mrcalc wm_clean.mif 0 -mul path.mif') + + # Combine into 5TT format + run.command('mrcat cgm_clean.mif sgm_clean.mif wm_clean.mif csf_clean.mif path.mif - -axis 3 | ' + 'mrconvert - combined_precrop.mif -strides +2,+3,+4,+1') + + # Apply cropping unless disabled + if app.ARGS.nocrop: + run.function(os.rename, 'combined_precrop.mif', 'result.mif') + else: + run.command('mrmath combined_precrop.mif sum - -axis 3 | ' + 'mrthreshold - - -abs 0.5 | ' + 'mrgrid combined_precrop.mif crop result.mif -mask -') + + run.command(['mrconvert', 'result.mif', app.ARGS.output], + mrconvert_keyval=app.ARGS.input, + force=app.FORCE_OVERWRITE) \ No newline at end of file From c9f746aeb9cb02a1601fc66f019a3c25e9a83f01 Mon Sep 17 00:00:00 2001 From: Lucius <30386169+LucSam@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:05:41 +1100 Subject: [PATCH 03/11] Remove vestigial file cmd/5ttgen/deep_atropos.py File erroneously preserved in 25a2fecaf91a79c0020575d1289d2ef79008a13c as part of #3057. --- cmd/5ttgen/deep_atropos.py | 45 -------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 cmd/5ttgen/deep_atropos.py diff --git a/cmd/5ttgen/deep_atropos.py b/cmd/5ttgen/deep_atropos.py deleted file mode 100644 index 665795a238..0000000000 --- a/cmd/5ttgen/deep_atropos.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -from mrtrix3 import MRtrixError -from mrtrix3 import app, image, path, run - -def usage(base_parser, subparsers): #pylint: disable=unused-variable - parser = subparsers.add_parser('deep_atropos', parents=[base_parser]) - parser.set_author('Lucius S. Fekonja (lucius.fekonja[at]charite.de)') - parser.set_synopsis('Generate the 5TT image based on a Deep Atropos segmentation image') - parser.add_argument('input', help='The input Deep Atropos segmentation image') - parser.add_argument('output', help='The output 5TT image') - -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.output) - -def check_deep_atropos_input(image_path): - dim = image.Header(image_path).size() - if len(dim) != 3: - raise MRtrixError('Image \'' + image_path + '\' does not look like Deep Atropos segmentation (number of spatial dimensions is not 3)') - -def get_inputs(): #pylint: disable=unused-variable - check_deep_atropos_input(path.from_user(app.ARGS.input, False)) - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('input.mif')) - -def execute(): #pylint: disable=unused-variable - # Generate the images related to each tissue - run.command('mrcalc input.mif 1 -eq CSF.mif') - run.command('mrcalc input.mif 2 -eq cGM.mif') - run.command('mrcalc input.mif 3 -eq WM1.mif') - run.command('mrcalc input.mif 5 -eq WM2.mif') - run.command('mrcalc input.mif 6 -eq WM3.mif') - run.command('mrmath WM1.mif WM2.mif WM3.mif sum WM.mif') - run.command('mrcalc input.mif 4 -eq sGM.mif') - - # Create an empty lesion image - run.command('mrcalc WM.mif 0 -mul lsn.mif') - - # Convert into the 5tt format - run.command('mrcat cGM.mif sGM.mif WM.mif CSF.mif lsn.mif 5tt.mif -axis 3') - - if app.ARGS.nocrop: - run.function(os.rename, '5tt.mif', 'result.mif') - else: - run.command('mrmath 5tt.mif sum - -axis 3 | mrthreshold - - -abs 0.5 | mrgrid 5tt.mif crop result.mif -mask -') - - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) From c411b2587e85599aa2a1445c112327d1ce628d3a Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 8 Jan 2025 12:37:39 +1100 Subject: [PATCH 04/11] 5ttgen deep_atropos: Multiple changes - Add capability take as input the concatenated tissue probability images as an alternative to the segmentation label image. - Change default allocation of brain stem to be the 5th volume, in line with 5ttgen hsvs behaviour; this can be overridden using the -white-stem option. - Change allocation of cerebellum to be subcortical grey matter. - Import updated script test data, and add tests for the new command. - Add RS to command author list. --- python/mrtrix3/commands/5ttgen/__init__.py | 2 +- .../mrtrix3/commands/5ttgen/deep_atropos.py | 92 ++++++++++--------- testing/CMakeLists.txt | 8 +- testing/scripts/CMakeLists.txt | 4 + .../tests/5ttgen/deepatropos_fromprob_default | 6 ++ .../5ttgen/deepatropos_fromprob_whitestem | 7 ++ .../tests/5ttgen/deepatropos_fromseg_default | 6 ++ .../5ttgen/deepatropos_fromseg_whitestem | 7 ++ 8 files changed, 86 insertions(+), 46 deletions(-) create mode 100644 testing/scripts/tests/5ttgen/deepatropos_fromprob_default create mode 100644 testing/scripts/tests/5ttgen/deepatropos_fromprob_whitestem create mode 100644 testing/scripts/tests/5ttgen/deepatropos_fromseg_default create mode 100644 testing/scripts/tests/5ttgen/deepatropos_fromseg_whitestem diff --git a/python/mrtrix3/commands/5ttgen/__init__.py b/python/mrtrix3/commands/5ttgen/__init__.py index 4fe461562e..942601933a 100644 --- a/python/mrtrix3/commands/5ttgen/__init__.py +++ b/python/mrtrix3/commands/5ttgen/__init__.py @@ -14,4 +14,4 @@ # For more details, see http://www.mrtrix.org/. # pylint: disable=unused-variable -ALGORITHMS = ['freesurfer', 'fsl', 'gif', 'hsvs', 'deep_atropos'] +ALGORITHMS = ['deep_atropos', 'freesurfer', 'fsl', 'gif', 'hsvs'] diff --git a/python/mrtrix3/commands/5ttgen/deep_atropos.py b/python/mrtrix3/commands/5ttgen/deep_atropos.py index 8d043c7e67..00182b9efe 100644 --- a/python/mrtrix3/commands/5ttgen/deep_atropos.py +++ b/python/mrtrix3/commands/5ttgen/deep_atropos.py @@ -1,10 +1,10 @@ -import os.path, shutil +import os from mrtrix3 import MRtrixError -from mrtrix3 import app, image, path, run +from mrtrix3 import app, image, run def usage(base_parser, subparsers): parser = subparsers.add_parser('deep_atropos', parents=[base_parser]) - parser.set_author('Lucius S. Fekonja (lucius.fekonja[at]charite.de)') + parser.set_author('Lucius S. Fekonja (lucius.fekonja[at]charite.de) and Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Generate the 5TT image based on a Deep Atropos segmentation image') parser.add_argument('input', type=app.Parser.ImageIn(), @@ -12,47 +12,57 @@ def usage(base_parser, subparsers): parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') - -def check_deep_atropos_input(image_path): - dim = image.Header(image_path).size() - if len(dim) != 3: - raise MRtrixError(f'Image \'{str(image_path)}\' does not look like Deep Atropos segmentation (number of spatial dimensions is not 3)') + parser.add_argument('-white_stem', + action='store_true', + default=None, + help='Classify the brainstem as white matter') def execute(): - check_deep_atropos_input(app.ARGS.input) - run.command(['mrconvert', app.ARGS.input, 'input.mif']) - # Generate initial tissue-specific maps - run.command('mrcalc input.mif 1 -eq CSF.mif') - run.command('mrcalc input.mif 2 -eq cGM.mif') - run.command('mrcalc input.mif 3 -eq WM1.mif') - run.command('mrcalc input.mif 5 -eq WM2.mif') - run.command('mrcalc input.mif 6 -eq WM3.mif') - run.command('mrmath WM1.mif WM2.mif WM3.mif sum WM.mif') - run.command('mrcalc input.mif 4 -eq sGM.mif') + if app.ARGS.sgm_amyg_hipp: + app.warn('Option -sgm_amyg_hipp has no effect on deep_atropos algorithm') - # Run connected components on WM for cleanup - run.command('mrthreshold WM.mif - -abs 0.001 | ' - 'maskfilter - connect - -connectivity | ' - 'mrcalc 1 - 1 -gt -sub remove_unconnected_wm_mask.mif -datatype bit') + dim = image.Header(app.ARGS.input).size() + if not(len(dim) == 3 or (len(dim) == 4 and dim[3] == 7)): + raise MRtrixError(f'Image \'{str(app.ARGS.input)}\' does not look like Deep Atropos segmentation' + f' (expected either a 3D image, or a 4D image with 7 volumes; input image is {dim})') - # Preserve CSF and handle volume fractions - run.command('mrcalc CSF.mif remove_unconnected_wm_mask.mif -mult csf_clean.mif') - run.command('mrcalc 1.0 csf_clean.mif -sub sGM.mif -min sgm_clean.mif') - - # Calculate multiplier for volume fraction correction - run.command('mrcalc 1.0 csf_clean.mif sgm_clean.mif -add -sub cGM.mif WM.mif -add -div multiplier.mif') - run.command('mrcalc multiplier.mif -finite multiplier.mif 0.0 -if multiplier_noNAN.mif') - - # Apply corrections - run.command('mrcalc cGM.mif multiplier_noNAN.mif -mult remove_unconnected_wm_mask.mif -mult cgm_clean.mif') - run.command('mrcalc WM.mif multiplier_noNAN.mif -mult remove_unconnected_wm_mask.mif -mult wm_clean.mif') - - # Create empty pathological tissue map - run.command('mrcalc wm_clean.mif 0 -mul path.mif') + run.command(['mrconvert', app.ARGS.input, 'input.mif']) + + if len(dim) == 3: + # Generate tissue-specific masks + run.command('mrcalc input.mif 2 -eq cGM.mif') + run.command('mrcalc input.mif 4 -eq input.mif 6 -eq -add sGM.mif') + run.command(f'mrcalc input.mif 3 -eq{" input.mif 5 -eq -add" if app.ARGS.white_stem else ""} WM.mif') + run.command('mrcalc input.mif 1 -eq CSF.mif') + run.command(f'mrcalc input.mif {"0 -mult" if app.ARGS.white_stem else "5 -eq"} path.mif') + else: + # Brain mask = non-brain probability is <50% + run.command('mrconvert input.mif -coord 3 0 -axes 0,1,2 - | ' + 'mrthreshold - -abs 0.5 -comparison le mask.mif') + # Need to rescale model probabilities so that, excluding the non-brain component, + # the sum across all tissues will be 1.0 + run.command('mrconvert input.mif -coord 3 1:end - | ' + 'mrmath - sum -axis 3 - | ' + 'mrcalc 1 - -div multiplier.mif') + # Generate tissue-specific probability maps + run.command('mrconvert input.mif -coord 3 2 -axes 0,1,2 cGM.mif') + run.command('mrconvert input.mif -coord 3 4,6 - | ' + 'mrmath - sum -axis 3 sGM.mif') + if app.ARGS.white_stem: + run.command(f'mrconvert input.mif -coord 3 3,5 - | ' + 'mrmath - sum -axis 3 WM.mif') + else: + run.command('mrconvert input.mif -coord 3 3 -axes 0,1,2 WM.mif') + run.command('mrconvert input.mif -coord 3 1 -axes 0,1,2 CSF.mif') + if app.ARGS.white_stem: + run.command('mrcalc cGM.mif 0 -mult path.mif') + else: + run.command('mrconvert input.mif -coord 3 5 -axes 0,1,2 path.mif') - # Combine into 5TT format - run.command('mrcat cgm_clean.mif sgm_clean.mif wm_clean.mif csf_clean.mif path.mif - -axis 3 | ' + # Concatenate into the 5TT image + run.command('mrcat cGM.mif sGM.mif WM.mif CSF.mif path.mif - -axis 3 | ' + f'{"mrcalc - multiplier.mif -mult mask.mif -mult - | " if len(dim) == 4 else ""}' 'mrconvert - combined_precrop.mif -strides +2,+3,+4,+1') # Apply cropping unless disabled @@ -60,9 +70,9 @@ def execute(): run.function(os.rename, 'combined_precrop.mif', 'result.mif') else: run.command('mrmath combined_precrop.mif sum - -axis 3 | ' - 'mrthreshold - - -abs 0.5 | ' - 'mrgrid combined_precrop.mif crop result.mif -mask -') + 'mrthreshold - - -abs 0.5 | ' + 'mrgrid combined_precrop.mif crop result.mif -mask -') run.command(['mrconvert', 'result.mif', app.ARGS.output], mrconvert_keyval=app.ARGS.input, - force=app.FORCE_OVERWRITE) \ No newline at end of file + force=app.FORCE_OVERWRITE) diff --git a/testing/CMakeLists.txt b/testing/CMakeLists.txt index ed43c4ed09..9e77b79374 100644 --- a/testing/CMakeLists.txt +++ b/testing/CMakeLists.txt @@ -16,7 +16,7 @@ include(ExternalProject) ExternalProject_Add(BinariesTestData PREFIX ${CMAKE_CURRENT_BINARY_DIR}/binaries_data GIT_REPOSITORY ${mrtrix_binaries_data_url} - GIT_TAG 2169ebc06040a0b1380017f5f2a11d6380c69922 + GIT_TAG 2169ebc06040a0b1380017f5f2a11d6380c69922 GIT_PROGRESS TRUE CONFIGURE_COMMAND "" BUILD_COMMAND "" @@ -27,7 +27,7 @@ ExternalProject_Add(BinariesTestData ExternalProject_Add(ScriptsTestData PREFIX ${CMAKE_CURRENT_BINARY_DIR}/scripts_data GIT_REPOSITORY ${mrtrix_scripts_data_url} - GIT_TAG 76f47633cd0a37e901c42320f4540ecaffd51367 + GIT_TAG 7f3dae1e1bbbb383d710c0db66f469b5f812a298 GIT_PROGRESS TRUE CONFIGURE_COMMAND "" BUILD_COMMAND "" @@ -35,11 +35,11 @@ ExternalProject_Add(ScriptsTestData LOG_DOWNLOAD ON ) -set(BINARY_DATA_DIR +set(BINARY_DATA_DIR ${CMAKE_CURRENT_BINARY_DIR}/binaries_data/src/BinariesTestData ) -set(SCRIPT_DATA_DIR +set(SCRIPT_DATA_DIR ${CMAKE_CURRENT_BINARY_DIR}/scripts_data/src/ScriptsTestData ) diff --git a/testing/scripts/CMakeLists.txt b/testing/scripts/CMakeLists.txt index 9a0b9fad51..30e5817250 100644 --- a/testing/scripts/CMakeLists.txt +++ b/testing/scripts/CMakeLists.txt @@ -27,6 +27,10 @@ function(add_bash_script_test file_path) ) endfunction() +add_bash_script_test(5ttgen/deepatropos_fromseg_default "pythonci") +add_bash_script_test(5ttgen/deepatropos_fromseg_whitestem) +add_bash_script_test(5ttgen/deepatropos_fromprob_default "pythonci") +add_bash_script_test(5ttgen/deepatropos_fromprob_whitestem) add_bash_script_test(5ttgen/freesurfer_default "pythonci") add_bash_script_test(5ttgen/freesurfer_nocrop) add_bash_script_test(5ttgen/freesurfer_piping) diff --git a/testing/scripts/tests/5ttgen/deepatropos_fromprob_default b/testing/scripts/tests/5ttgen/deepatropos_fromprob_default new file mode 100644 index 0000000000..5778dd6eb3 --- /dev/null +++ b/testing/scripts/tests/5ttgen/deepatropos_fromprob_default @@ -0,0 +1,6 @@ +#!/bin/bash +# Verify default operation of "5ttgen deep_atropos" +# where the input is the concatenation of tissue probability images +# Outcome is compared to that generated using a prior software version +5ttgen deep_atropos 5ttgen/deep_atropos/probability_images.nii.gz tmp.mif -force +testing_diff_image tmp.mif 5ttgen/deep_atropos/fromprob_default.mif.gz diff --git a/testing/scripts/tests/5ttgen/deepatropos_fromprob_whitestem b/testing/scripts/tests/5ttgen/deepatropos_fromprob_whitestem new file mode 100644 index 0000000000..2ec776a17b --- /dev/null +++ b/testing/scripts/tests/5ttgen/deepatropos_fromprob_whitestem @@ -0,0 +1,7 @@ +#!/bin/bash +# Verify default operation of "5ttgen deep_atropos" +# where the input is the concatenation of tissue probability images +# and the brain stem is allocated to WM rather than 5th volume +# Outcome is compared to that generated using a prior software version +5ttgen deep_atropos 5ttgen/deep_atropos/probability_images.nii.gz tmp.mif -white_stem -force +testing_diff_image tmp.mif 5ttgen/deep_atropos/fromprob_whitestem.mif.gz diff --git a/testing/scripts/tests/5ttgen/deepatropos_fromseg_default b/testing/scripts/tests/5ttgen/deepatropos_fromseg_default new file mode 100644 index 0000000000..d8d1133536 --- /dev/null +++ b/testing/scripts/tests/5ttgen/deepatropos_fromseg_default @@ -0,0 +1,6 @@ +#!/bin/bash +# Verify default operation of "5ttgen deep_atropos" +# where input image is the segmentation label image +# Outcome is compared to that generated using a prior software version +5ttgen deep_atropos 5ttgen/deep_atropos/segmentation_image.nii.gz tmp.mif -force +testing_diff_image tmp.mif 5ttgen/deep_atropos/fromseg_default.mif.gz diff --git a/testing/scripts/tests/5ttgen/deepatropos_fromseg_whitestem b/testing/scripts/tests/5ttgen/deepatropos_fromseg_whitestem new file mode 100644 index 0000000000..5c6994d9a1 --- /dev/null +++ b/testing/scripts/tests/5ttgen/deepatropos_fromseg_whitestem @@ -0,0 +1,7 @@ +#!/bin/bash +# Verify default operation of "5ttgen deep_atropos" +# where input image is the segmentation label image +# and the brain stem is allocated to WM rather than 5th volume +# Outcome is compared to that generated using a prior software version +5ttgen deep_atropos 5ttgen/deep_atropos/segmentation_image.nii.gz tmp.mif -white_stem -force +testing_diff_image tmp.mif 5ttgen/deep_atropos/fromseg_whitestem.mif.gz From a210c24356f80f8d987f14587f57df3594f76cb3 Mon Sep 17 00:00:00 2001 From: Lucius <30386169+LucSam@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:46:19 +0100 Subject: [PATCH 05/11] 5ttgen deep_atropos: Further changes and testing - Added MRtrix3 copyright license header (2025) - Fixed indentation issues throughout file to conform with MRtrix3's two-space standardz - Fixed pylint warnings: - Corrected bad indentation (changed 4/8 spaces to 2/4 spaces) - Added pylint disable statement for unused variables in usage and execute - Addressed f-string error - Verified functionality through testing: - All deep_atropos tests passing (fromseg and fromprob with default and whitestem) Related to #3057 --- .../mrtrix3/commands/5ttgen/deep_atropos.py | 146 ++++++++++-------- 1 file changed, 80 insertions(+), 66 deletions(-) diff --git a/python/mrtrix3/commands/5ttgen/deep_atropos.py b/python/mrtrix3/commands/5ttgen/deep_atropos.py index 00182b9efe..3e07d7711a 100644 --- a/python/mrtrix3/commands/5ttgen/deep_atropos.py +++ b/python/mrtrix3/commands/5ttgen/deep_atropos.py @@ -1,78 +1,92 @@ +# Copyright (c) 2008-2024 the MRtrix3 contributors. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# Covered Software is provided under this License on an "as is" +# basis, without warranty of any kind, either expressed, implied, or +# statutory, including, without limitation, warranties that the +# Covered Software is free of defects, merchantable, fit for a +# particular purpose or non-infringing. +# See the Mozilla Public License v. 2.0 for more details. +# +# For more details, see http://www.mrtrix.org/. + import os from mrtrix3 import MRtrixError from mrtrix3 import app, image, run -def usage(base_parser, subparsers): - parser = subparsers.add_parser('deep_atropos', parents=[base_parser]) - parser.set_author('Lucius S. Fekonja (lucius.fekonja[at]charite.de) and Robert E. Smith (robert.smith@florey.edu.au)') - parser.set_synopsis('Generate the 5TT image based on a Deep Atropos segmentation image') - parser.add_argument('input', - type=app.Parser.ImageIn(), - help='The input Deep Atropos segmentation image') - parser.add_argument('output', - type=app.Parser.ImageOut(), - help='The output 5TT image') - parser.add_argument('-white_stem', - action='store_true', - default=None, - help='Classify the brainstem as white matter') - -def execute(): +def usage(base_parser, subparsers): #pylint: disable=unused-variable + parser = subparsers.add_parser('deep_atropos', parents=[base_parser]) + parser.set_author('Lucius S. Fekonja (lucius.fekonja[at]charite.de) and Robert E. Smith (robert.smith@florey.edu.au)') + parser.set_synopsis('Generate the 5TT image based on a Deep Atropos segmentation image') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input Deep Atropos segmentation image') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output 5TT image') + parser.add_argument('-white_stem', + action='store_true', + default=None, + help='Classify the brainstem as white matter') - if app.ARGS.sgm_amyg_hipp: - app.warn('Option -sgm_amyg_hipp has no effect on deep_atropos algorithm') +def execute(): #pylint: disable=unused-variable + if app.ARGS.sgm_amyg_hipp: + app.warn('Option -sgm_amyg_hipp has no effect on deep_atropos algorithm') - dim = image.Header(app.ARGS.input).size() - if not(len(dim) == 3 or (len(dim) == 4 and dim[3] == 7)): - raise MRtrixError(f'Image \'{str(app.ARGS.input)}\' does not look like Deep Atropos segmentation' - f' (expected either a 3D image, or a 4D image with 7 volumes; input image is {dim})') + dim = image.Header(app.ARGS.input).size() + if not(len(dim) == 3 or (len(dim) == 4 and dim[3] == 7)): + raise MRtrixError(f'Image \'{str(app.ARGS.input)}\' does not look like Deep Atropos segmentation' + f' (expected either a 3D image, or a 4D image with 7 volumes; input image is {dim})') - run.command(['mrconvert', app.ARGS.input, 'input.mif']) + run.command(['mrconvert', app.ARGS.input, 'input.mif']) - if len(dim) == 3: - # Generate tissue-specific masks - run.command('mrcalc input.mif 2 -eq cGM.mif') - run.command('mrcalc input.mif 4 -eq input.mif 6 -eq -add sGM.mif') - run.command(f'mrcalc input.mif 3 -eq{" input.mif 5 -eq -add" if app.ARGS.white_stem else ""} WM.mif') - run.command('mrcalc input.mif 1 -eq CSF.mif') - run.command(f'mrcalc input.mif {"0 -mult" if app.ARGS.white_stem else "5 -eq"} path.mif') + if len(dim) == 3: + # Generate tissue-specific masks + run.command('mrcalc input.mif 2 -eq cGM.mif') + run.command('mrcalc input.mif 4 -eq input.mif 6 -eq -add sGM.mif') + run.command(f'mrcalc input.mif 3 -eq{" input.mif 5 -eq -add" if app.ARGS.white_stem else ""} WM.mif') + run.command('mrcalc input.mif 1 -eq CSF.mif') + run.command(f'mrcalc input.mif {("0 -mult" if app.ARGS.white_stem else "5 -eq")} path.mif') + else: + # Brain mask = non-brain probability is <50% + run.command('mrconvert input.mif -coord 3 0 -axes 0,1,2 - | ' + 'mrthreshold - -abs 0.5 -comparison le mask.mif') + # Need to rescale model probabilities so that, excluding the non-brain component, + # the sum across all tissues will be 1.0 + run.command('mrconvert input.mif -coord 3 1:end - | ' + 'mrmath - sum -axis 3 - | ' + 'mrcalc 1 - -div multiplier.mif') + # Generate tissue-specific probability maps + run.command('mrconvert input.mif -coord 3 2 -axes 0,1,2 cGM.mif') + run.command('mrconvert input.mif -coord 3 4,6 - | ' + 'mrmath - sum -axis 3 sGM.mif') + if app.ARGS.white_stem: + run.command('mrconvert input.mif -coord 3 3,5 - | ' + 'mrmath - sum -axis 3 WM.mif') else: - # Brain mask = non-brain probability is <50% - run.command('mrconvert input.mif -coord 3 0 -axes 0,1,2 - | ' - 'mrthreshold - -abs 0.5 -comparison le mask.mif') - # Need to rescale model probabilities so that, excluding the non-brain component, - # the sum across all tissues will be 1.0 - run.command('mrconvert input.mif -coord 3 1:end - | ' - 'mrmath - sum -axis 3 - | ' - 'mrcalc 1 - -div multiplier.mif') - # Generate tissue-specific probability maps - run.command('mrconvert input.mif -coord 3 2 -axes 0,1,2 cGM.mif') - run.command('mrconvert input.mif -coord 3 4,6 - | ' - 'mrmath - sum -axis 3 sGM.mif') - if app.ARGS.white_stem: - run.command(f'mrconvert input.mif -coord 3 3,5 - | ' - 'mrmath - sum -axis 3 WM.mif') - else: - run.command('mrconvert input.mif -coord 3 3 -axes 0,1,2 WM.mif') - run.command('mrconvert input.mif -coord 3 1 -axes 0,1,2 CSF.mif') - if app.ARGS.white_stem: - run.command('mrcalc cGM.mif 0 -mult path.mif') - else: - run.command('mrconvert input.mif -coord 3 5 -axes 0,1,2 path.mif') + run.command('mrconvert input.mif -coord 3 3 -axes 0,1,2 WM.mif') + run.command('mrconvert input.mif -coord 3 1 -axes 0,1,2 CSF.mif') + if app.ARGS.white_stem: + run.command('mrcalc cGM.mif 0 -mult path.mif') + else: + run.command('mrconvert input.mif -coord 3 5 -axes 0,1,2 path.mif') - # Concatenate into the 5TT image - run.command('mrcat cGM.mif sGM.mif WM.mif CSF.mif path.mif - -axis 3 | ' - f'{"mrcalc - multiplier.mif -mult mask.mif -mult - | " if len(dim) == 4 else ""}' - 'mrconvert - combined_precrop.mif -strides +2,+3,+4,+1') + # Concatenate into the 5TT image + run.command('mrcat cGM.mif sGM.mif WM.mif CSF.mif path.mif - -axis 3 | ' + f'{"mrcalc - multiplier.mif -mult mask.mif -mult - | " if len(dim) == 4 else ""}' + 'mrconvert - combined_precrop.mif -strides +2,+3,+4,+1') - # Apply cropping unless disabled - if app.ARGS.nocrop: - run.function(os.rename, 'combined_precrop.mif', 'result.mif') - else: - run.command('mrmath combined_precrop.mif sum - -axis 3 | ' - 'mrthreshold - - -abs 0.5 | ' - 'mrgrid combined_precrop.mif crop result.mif -mask -') + # Apply cropping unless disabled + if app.ARGS.nocrop: + run.function(os.rename, 'combined_precrop.mif', 'result.mif') + else: + run.command('mrmath combined_precrop.mif sum - -axis 3 | ' + 'mrthreshold - - -abs 0.5 | ' + 'mrgrid combined_precrop.mif crop result.mif -mask -') - run.command(['mrconvert', 'result.mif', app.ARGS.output], - mrconvert_keyval=app.ARGS.input, - force=app.FORCE_OVERWRITE) + run.command(['mrconvert', 'result.mif', app.ARGS.output], + mrconvert_keyval=app.ARGS.input, + force=app.FORCE_OVERWRITE) From 81f5d21f7aa55d87a2da6912ad76e01ac24ae6ae Mon Sep 17 00:00:00 2001 From: Lucius <30386169+LucSam@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:12:12 +0100 Subject: [PATCH 06/11] Add antspynet description to 5ttgen deep_atropos #3057 --- .../mrtrix3/commands/5ttgen/deep_atropos.py | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/python/mrtrix3/commands/5ttgen/deep_atropos.py b/python/mrtrix3/commands/5ttgen/deep_atropos.py index 3e07d7711a..e90c917a18 100644 --- a/python/mrtrix3/commands/5ttgen/deep_atropos.py +++ b/python/mrtrix3/commands/5ttgen/deep_atropos.py @@ -14,13 +14,42 @@ # For more details, see http://www.mrtrix.org/. import os +import argparse from mrtrix3 import MRtrixError from mrtrix3 import app, image, run def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('deep_atropos', parents=[base_parser]) parser.set_author('Lucius S. Fekonja (lucius.fekonja[at]charite.de) and Robert E. Smith (robert.smith@florey.edu.au)') - parser.set_synopsis('Generate the 5TT image based on a Deep Atropos segmentation image') + parser.set_synopsis('Generate the 5TT image based on a Deep Atropos segmentation or probabilities image') + parser.add_citation('Use of the ANTsX ecosystem should be accompanied by the following citation:\n' + 'N.J. Tustison, P.A. Cook, A.J. Holbrook, H.J. Johnson, J. Muschelli, G.A. Devenyi, J.T. Duda, S.R. Das, ' + 'N.C. Cullen, D.L. Gillen, M.A. Yassa, J.R. Stone, J.C. Gee, and B.B. Avants. ' + 'The ANTsX ecosystem for quantitative biological and medical imaging. ' + 'Scientific Reports, 11(1):9068 (2021), pp. 1-13.', + is_external=True) + parser.add_description('This algorithm accepts either:') + parser.add_description('1. A 3D segmentation image from Deep Atropos with labels:') + parser.add_description(' - 0: Background - 1: CSF - 2: Gray Matter - 3: White Matter - 4: Deep Gray Matter - 5: Brain Stem - 6: Cerebellum') + parser.add_description('2. A 4D probability image (dimensions [x,y,z,7]) containing probability maps for each tissue class in the same order as above') + parser.add_description('To generate these input images using antspynet\'s deep_atropos function:') + parser.add_description('1. Install required packages: pip install antspynet nibabel') + parser.add_description('2. Use Python commands:') + parser.add_description(' # Load the T1-weighted image') + parser.add_description(' >>> import ants, antspynet') + parser.add_description(' >>> import nibabel as nib') + parser.add_description(' >>> import numpy as np') + parser.add_description(' >>> t1_image = ants.image_read(\'input_t1.nii.gz\')') + parser.add_description(' # Run Deep Atropos segmentation') + parser.add_description(' >>> segments = antspynet.deep_atropos(t1_image)') + parser.add_description(' # Save the segmentation') + parser.add_description(' >>> ants.image_write(segments[\'segmentation_image\'], \'segmentation.nii.gz\')') + parser.add_description(' # Stack and save probability maps') + parser.add_description(' >>> prob_maps = np.stack([np.array(img.numpy()) for img in segments[\'probability_images\']], axis=-1)') + parser.add_description(' >>> nib.save(nib.Nifti1Image(prob_maps, t1_image.affine), \'probabilities.nii.gz\')') + parser.add_description('The generated files can then be used with MRtrix3:') + parser.add_description(' 5ttgen deep_atropos segmentation.nii.gz 5tt.mif') + parser.add_description(' 5ttgen deep_atropos probabilities.nii.gz 5tt.mif') parser.add_argument('input', type=app.Parser.ImageIn(), help='The input Deep Atropos segmentation image') From 7c5d34b909e37b0bd3257934107a1cb201d31284 Mon Sep 17 00:00:00 2001 From: Lucius <30386169+LucSam@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:00:29 +0100 Subject: [PATCH 07/11] 5ttgen deep_atropos: Improve help text formatting --- .../mrtrix3/commands/5ttgen/deep_atropos.py | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/python/mrtrix3/commands/5ttgen/deep_atropos.py b/python/mrtrix3/commands/5ttgen/deep_atropos.py index e90c917a18..46e875f4e3 100644 --- a/python/mrtrix3/commands/5ttgen/deep_atropos.py +++ b/python/mrtrix3/commands/5ttgen/deep_atropos.py @@ -14,7 +14,6 @@ # For more details, see http://www.mrtrix.org/. import os -import argparse from mrtrix3 import MRtrixError from mrtrix3 import app, image, run @@ -30,26 +29,26 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable is_external=True) parser.add_description('This algorithm accepts either:') parser.add_description('1. A 3D segmentation image from Deep Atropos with labels:') - parser.add_description(' - 0: Background - 1: CSF - 2: Gray Matter - 3: White Matter - 4: Deep Gray Matter - 5: Brain Stem - 6: Cerebellum') + parser.add_description('0: Background, 1: CSF, 2: Gray Matter, 3: White Matter, 4: Deep Gray Matter, 5: Brain Stem, 6: Cerebellum') parser.add_description('2. A 4D probability image (dimensions [x,y,z,7]) containing probability maps for each tissue class in the same order as above') parser.add_description('To generate these input images using antspynet\'s deep_atropos function:') parser.add_description('1. Install required packages: pip install antspynet nibabel') parser.add_description('2. Use Python commands:') - parser.add_description(' # Load the T1-weighted image') - parser.add_description(' >>> import ants, antspynet') - parser.add_description(' >>> import nibabel as nib') - parser.add_description(' >>> import numpy as np') - parser.add_description(' >>> t1_image = ants.image_read(\'input_t1.nii.gz\')') - parser.add_description(' # Run Deep Atropos segmentation') - parser.add_description(' >>> segments = antspynet.deep_atropos(t1_image)') - parser.add_description(' # Save the segmentation') - parser.add_description(' >>> ants.image_write(segments[\'segmentation_image\'], \'segmentation.nii.gz\')') - parser.add_description(' # Stack and save probability maps') - parser.add_description(' >>> prob_maps = np.stack([np.array(img.numpy()) for img in segments[\'probability_images\']], axis=-1)') - parser.add_description(' >>> nib.save(nib.Nifti1Image(prob_maps, t1_image.affine), \'probabilities.nii.gz\')') + parser.add_description('>>> # Load the T1-weighted image') + parser.add_description('>>> import ants, antspynet') + parser.add_description('>>> import nibabel as nib') + parser.add_description('>>> import numpy as np') + parser.add_description('>>> t1_image = ants.image_read(\'input_t1.nii.gz\')') + parser.add_description('>>> # Run Deep Atropos segmentation') + parser.add_description('>>> segments = antspynet.deep_atropos(t1_image)') + parser.add_description('>>> # Save the segmentation') + parser.add_description('>>> ants.image_write(segments[\'segmentation_image\'], \'segmentation.nii.gz\')') + parser.add_description('>>> # Stack and save probability maps') + parser.add_description('>>> prob_maps = np.stack([np.array(img.numpy()) for img in segments[\'probability_images\']], axis=-1)') + parser.add_description('>>> nib.save(nib.Nifti1Image(prob_maps, t1_image.affine), \'probabilities.nii.gz\')') parser.add_description('The generated files can then be used with MRtrix3:') - parser.add_description(' 5ttgen deep_atropos segmentation.nii.gz 5tt.mif') - parser.add_description(' 5ttgen deep_atropos probabilities.nii.gz 5tt.mif') + parser.add_description('5ttgen deep_atropos segmentation.nii.gz 5tt.mif') + parser.add_description('5ttgen deep_atropos probabilities.nii.gz 5tt.mif') parser.add_argument('input', type=app.Parser.ImageIn(), help='The input Deep Atropos segmentation image') From c75609dfeb93bfe386d5d4275d15ec44daa7b881 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 21 Jan 2025 10:10:14 +1100 Subject: [PATCH 08/11] 5ttgen deep_atropos: Reformat help documentation --- .../mrtrix3/commands/5ttgen/deep_atropos.py | 63 +++++++++++-------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/python/mrtrix3/commands/5ttgen/deep_atropos.py b/python/mrtrix3/commands/5ttgen/deep_atropos.py index 46e875f4e3..1b899b5939 100644 --- a/python/mrtrix3/commands/5ttgen/deep_atropos.py +++ b/python/mrtrix3/commands/5ttgen/deep_atropos.py @@ -22,33 +22,42 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Lucius S. Fekonja (lucius.fekonja[at]charite.de) and Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Generate the 5TT image based on a Deep Atropos segmentation or probabilities image') parser.add_citation('Use of the ANTsX ecosystem should be accompanied by the following citation:\n' - 'N.J. Tustison, P.A. Cook, A.J. Holbrook, H.J. Johnson, J. Muschelli, G.A. Devenyi, J.T. Duda, S.R. Das, ' - 'N.C. Cullen, D.L. Gillen, M.A. Yassa, J.R. Stone, J.C. Gee, and B.B. Avants. ' - 'The ANTsX ecosystem for quantitative biological and medical imaging. ' - 'Scientific Reports, 11(1):9068 (2021), pp. 1-13.', - is_external=True) - parser.add_description('This algorithm accepts either:') - parser.add_description('1. A 3D segmentation image from Deep Atropos with labels:') - parser.add_description('0: Background, 1: CSF, 2: Gray Matter, 3: White Matter, 4: Deep Gray Matter, 5: Brain Stem, 6: Cerebellum') - parser.add_description('2. A 4D probability image (dimensions [x,y,z,7]) containing probability maps for each tissue class in the same order as above') - parser.add_description('To generate these input images using antspynet\'s deep_atropos function:') - parser.add_description('1. Install required packages: pip install antspynet nibabel') - parser.add_description('2. Use Python commands:') - parser.add_description('>>> # Load the T1-weighted image') - parser.add_description('>>> import ants, antspynet') - parser.add_description('>>> import nibabel as nib') - parser.add_description('>>> import numpy as np') - parser.add_description('>>> t1_image = ants.image_read(\'input_t1.nii.gz\')') - parser.add_description('>>> # Run Deep Atropos segmentation') - parser.add_description('>>> segments = antspynet.deep_atropos(t1_image)') - parser.add_description('>>> # Save the segmentation') - parser.add_description('>>> ants.image_write(segments[\'segmentation_image\'], \'segmentation.nii.gz\')') - parser.add_description('>>> # Stack and save probability maps') - parser.add_description('>>> prob_maps = np.stack([np.array(img.numpy()) for img in segments[\'probability_images\']], axis=-1)') - parser.add_description('>>> nib.save(nib.Nifti1Image(prob_maps, t1_image.affine), \'probabilities.nii.gz\')') - parser.add_description('The generated files can then be used with MRtrix3:') - parser.add_description('5ttgen deep_atropos segmentation.nii.gz 5tt.mif') - parser.add_description('5ttgen deep_atropos probabilities.nii.gz 5tt.mif') + 'N.J. Tustison, P.A. Cook, A.J. Holbrook, H.J. Johnson, J. Muschelli, G.A. Devenyi, J.T. Duda, S.R. Das, ' + 'N.C. Cullen, D.L. Gillen, M.A. Yassa, J.R. Stone, J.C. Gee, and B.B. Avants. ' + 'The ANTsX ecosystem for quantitative biological and medical imaging. ' + 'Scientific Reports, 11(1):9068 (2021), pp. 1-13.', + is_external=True) + parser.add_description('This algorithm can accept the outputs of Deep Atropos in one of two forms. ' + 'The "segmentation image" is a 3D image, of integer datatype, ' + 'with indices mapping to discrete tissue classes as follows: ' + '0: Background; 1: CSF; 2: Gray Matter; 3: White Matter; 4: Deep Gray Matter; 5: Brain Stem; 6: Cerebellum. ' + 'The "probabilities images" are a set of seven 3D volumes, ' + 'each corresponding to the posterior probability of one of the seven tissue classes above. ' + 'These can be provided as input to this command by concatenating into a 4D image series with 7 volumes ' + '(the order of which must match that above).') + parser.add_description('The example usages provided in this help page, ' + 'which include execution of Deep Atropos itself within a Python environment, ' + 'require that "ants" and "antspynet" be installed via Python\'s "pip"; ' + 'use of the "probability images" also requires that nibabel and numpy be installed.') + parser.add_example_usage('To utilise the "segmentation" image', + 'python3 -c \'import ants, antspynet; ' + 't1w = ants.image_read(\'T1w.nii.gz\'); ' + 'result = antspynet.deep_atropos(t1w); ' + 'ants.image_write\'result[\'segmentation_image\'], \'segmentation.nii.gz\')\'; ' + '5ttgen deep_atropos segmentation.nii.gz 5tt_segmentation.mif', + 'Because the input segmentation here is an integer image, ' + 'where each voxel just contains an index corresponding to the maximal tissue class, ' + 'the output 5TT image will not possess any fractional partial volumes; ' + 'it will just contain the value 1.0 in whichever 5TT volume corresponds to the singular assigned tissue class.') + parser.add_example_usage('To utilise the "probability images"', + 'python3 -c \'import ants, antspynet, nibabel, numpy; ' + 't1w = ants.image_read(\'T1w.nii.gz\'); ' + 'result = antspynet.deep_atropos(t1w); ' + 'prob_maps = numpy.stack([numpy.array(img.numpy()) for img in result[\'probability_images\']], axis=-1); ' + 'nibabel.save(nib.Nifti1Image(prob_maps, t1w.affine), \'probabilities.nii.gz\')\'; ' + '5ttgen deep_atropos probabilities.nii.gz 5tt_probabilities.mif', + 'In this use case, the poerior probabilities of these tissue classes are interpreted as partial volume fractions ' + 'and imported into the derivative 5TT image appropriately.') parser.add_argument('input', type=app.Parser.ImageIn(), help='The input Deep Atropos segmentation image') From fd3aa5bf067cefa804672cc64d5579c0c90e18c5 Mon Sep 17 00:00:00 2001 From: Lucius <30386169+LucSam@users.noreply.github.com> Date: Tue, 21 Jan 2025 10:11:09 +1100 Subject: [PATCH 09/11] 5ttgen: Propagate addition of deep_atropos to help page --- docs/reference/commands/5ttgen.rst | 115 ++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/docs/reference/commands/5ttgen.rst b/docs/reference/commands/5ttgen.rst index 950fd9c9af..a7592167cc 100644 --- a/docs/reference/commands/5ttgen.rst +++ b/docs/reference/commands/5ttgen.rst @@ -15,7 +15,7 @@ Usage 5ttgen algorithm [ options ] ... -- *algorithm*: Select the algorithm to be used; additional details and options become available once an algorithm is nominated. Options are: freesurfer, fsl, gif, hsvs +- *algorithm*: Select the algorithm to be used; additional details and options become available once an algorithm is nominated. Options are: deep_atropos, freesurfer, fsl, gif, hsvs Description ----------- @@ -90,6 +90,119 @@ See the Mozilla Public License v. 2.0 for more details. For more details, see http://www.mrtrix.org/. +.. _5ttgen_deep_atropos: + +5ttgen deep_atropos +=================== + +Synopsis +-------- + +Generate the 5TT image based on a Deep Atropos segmentation or probabilities image + +Usage +----- + +:: + + 5ttgen deep_atropos input output [ options ] + +- *input*: The input Deep Atropos segmentation image +- *output*: The output 5TT image + +Description +----------- + +This algorithm can accept the outputs of Deep Atropos in one of two forms. The "segmentation image" is a 3D image, of integer datatype, with indices mapping to discrete tissue classes as follows: 0: Background; 1: CSF; 2: Gray Matter; 3: White Matter; 4: Deep Gray Matter; 5: Brain Stem; 6: Cerebellum. The "probabilities images" are a set of seven 3D volumes, each corresponding to the posterior probability of one of the seven tissue classes above. These can be provided as input to this command by concatenating into a 4D image series with 7 volumes (the order of which must match that above). + +The example usages provided in this help page, which include execution of Deep Atropos itself within a Python environment, require that "ants" and "antspynet" be installed via Python's "pip"; use of the "probability images" also requires that nibabel and numpy be installed. + +Example usages +-------------- + +- *To utilise the "segmentation" image*:: + + $ python3 -c 'import ants, antspynet; t1w = ants.image_read('T1w.nii.gz'); result = antspynet.deep_atropos(t1w); ants.image_write'result['segmentation_image'], 'segmentation.nii.gz')'; 5ttgen deep_atropos segmentation.nii.gz 5tt_segmentation.mif + + Because the input segmentation here is an integer image, where each voxel just contains an index corresponding to the maximal tissue class, the output 5TT image will not possess any fractional partial volumes; it will just contain the value 1.0 in whichever 5TT volume corresponds to the singular assigned tissue class. + +- *To utilise the "probability images"*:: + + $ python3 -c 'import ants, antspynet, nibabel, numpy; t1w = ants.image_read('T1w.nii.gz'); result = antspynet.deep_atropos(t1w); prob_maps = numpy.stack([numpy.array(img.numpy()) for img in result['probability_images']], axis=-1); nibabel.save(nib.Nifti1Image(prob_maps, t1w.affine), 'probabilities.nii.gz')'; 5ttgen deep_atropos probabilities.nii.gz 5tt_probabilities.mif + + In this use case, the poerior probabilities of these tissue classes are interpreted as partial volume fractions and imported into the derivative 5TT image appropriately. + +Options +------- + +- **-white_stem** Classify the brainstem as white matter + +Options common to all 5ttgen algorithms +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **-nocrop** Do NOT crop the resulting 5TT image to reduce its size (keep the same dimensions as the input image) + +- **-sgm_amyg_hipp** Represent the amygdalae and hippocampi as sub-cortical grey matter in the 5TT image + +Additional standard options for Python scripts +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. + +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. + +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. + +Standard options +^^^^^^^^^^^^^^^^ + +- **-info** display information messages. + +- **-quiet** do not display information messages or progress status. Alternatively, this can be achieved by setting the MRTRIX_QUIET environment variable to a non-empty string. + +- **-debug** display debugging messages. + +- **-force** force overwrite of output files. + +- **-nthreads number** use this number of threads in multi-threaded applications (set to 0 to disable multi-threading). + +- **-config key value** *(multiple uses permitted)* temporarily set the value of an MRtrix config file entry. + +- **-help** display this information page and exit. + +- **-version** display version information and exit. + +References +^^^^^^^^^^ + +* Smith, R. E.; Tournier, J.-D.; Calamante, F. & Connelly, A. Anatomically-constrained tractography: Improved diffusion MRI streamlines tractography through effective use of anatomical information. NeuroImage, 2012, 62, 1924-1938 + +* Use of the ANTsX ecosystem should be accompanied by the following citation: +N.J. Tustison, P.A. Cook, A.J. Holbrook, H.J. Johnson, J. Muschelli, G.A. Devenyi, J.T. Duda, S.R. Das, N.C. Cullen, D.L. Gillen, M.A. Yassa, J.R. Stone, J.C. Gee, and B.B. Avants. The ANTsX ecosystem for quantitative biological and medical imaging. Scientific Reports, 11(1):9068 (2021), pp. 1-13. + +Tournier, J.-D.; Smith, R. E.; Raffelt, D.; Tabbara, R.; Dhollander, T.; Pietsch, M.; Christiaens, D.; Jeurissen, B.; Yeh, C.-H. & Connelly, A. MRtrix3: A fast, flexible and open software framework for medical image processing and visualisation. NeuroImage, 2019, 202, 116137 + +-------------- + + + +**Author:** Lucius S. Fekonja (lucius.fekonja[at]charite.de) and Robert E. Smith (robert.smith@florey.edu.au) + +**Copyright:** Copyright (c) 2008-2024 the MRtrix3 contributors. + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Covered Software is provided under this License on an "as is" +basis, without warranty of any kind, either expressed, implied, or +statutory, including, without limitation, warranties that the +Covered Software is free of defects, merchantable, fit for a +particular purpose or non-infringing. +See the Mozilla Public License v. 2.0 for more details. + +For more details, see http://www.mrtrix.org/. + .. _5ttgen_freesurfer: 5ttgen freesurfer From ee9059685fc5af3f04633196e47b851e30743b52 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 21 Jan 2025 13:30:19 +1100 Subject: [PATCH 10/11] 5ttgen deep_atropos: Fixes to example usages - Fix use of single vs double quotes so that examples can be copy-pasted into a terminal and executed successfully. - Load the input image a second time using nibabel to get the affine transformation; the ANTsImage class modifies both the content and the representation of this information, and does not contain a member variable ".affine" as utilised in the original example. --- docs/reference/commands/5ttgen.rst | 4 ++-- .../mrtrix3/commands/5ttgen/deep_atropos.py | 20 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/reference/commands/5ttgen.rst b/docs/reference/commands/5ttgen.rst index a7592167cc..72f5925ef5 100644 --- a/docs/reference/commands/5ttgen.rst +++ b/docs/reference/commands/5ttgen.rst @@ -122,13 +122,13 @@ Example usages - *To utilise the "segmentation" image*:: - $ python3 -c 'import ants, antspynet; t1w = ants.image_read('T1w.nii.gz'); result = antspynet.deep_atropos(t1w); ants.image_write'result['segmentation_image'], 'segmentation.nii.gz')'; 5ttgen deep_atropos segmentation.nii.gz 5tt_segmentation.mif + $ python3 -c "import ants, antspynet; t1w = ants.image_read('T1w.nii.gz'); result = antspynet.deep_atropos(t1w); ants.image_write(result['segmentation_image'], 'segmentation.nii.gz')"; 5ttgen deep_atropos segmentation.nii.gz 5tt_segmentation.mif Because the input segmentation here is an integer image, where each voxel just contains an index corresponding to the maximal tissue class, the output 5TT image will not possess any fractional partial volumes; it will just contain the value 1.0 in whichever 5TT volume corresponds to the singular assigned tissue class. - *To utilise the "probability images"*:: - $ python3 -c 'import ants, antspynet, nibabel, numpy; t1w = ants.image_read('T1w.nii.gz'); result = antspynet.deep_atropos(t1w); prob_maps = numpy.stack([numpy.array(img.numpy()) for img in result['probability_images']], axis=-1); nibabel.save(nib.Nifti1Image(prob_maps, t1w.affine), 'probabilities.nii.gz')'; 5ttgen deep_atropos probabilities.nii.gz 5tt_probabilities.mif + $ python3 -c "import ants, antspynet, nibabel, numpy; inpath = 'T1w.nii.gz'; t1w_ants = ants.image_read(inpath); t1w_nib = nibabel.load(inpath); result = antspynet.deep_atropos(t1w_ants); prob_maps = numpy.stack([numpy.array(img.numpy()) for img in result['probability_images']], axis=-1); nibabel.save(nibabel.Nifti1Image(prob_maps, t1w_nib.affine), 'probabilities.nii.gz')"; 5ttgen deep_atropos probabilities.nii.gz 5tt_probabilities.mif In this use case, the poerior probabilities of these tissue classes are interpreted as partial volume fractions and imported into the derivative 5TT image appropriately. diff --git a/python/mrtrix3/commands/5ttgen/deep_atropos.py b/python/mrtrix3/commands/5ttgen/deep_atropos.py index 1b899b5939..88b129fca7 100644 --- a/python/mrtrix3/commands/5ttgen/deep_atropos.py +++ b/python/mrtrix3/commands/5ttgen/deep_atropos.py @@ -40,21 +40,23 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable 'require that "ants" and "antspynet" be installed via Python\'s "pip"; ' 'use of the "probability images" also requires that nibabel and numpy be installed.') parser.add_example_usage('To utilise the "segmentation" image', - 'python3 -c \'import ants, antspynet; ' - 't1w = ants.image_read(\'T1w.nii.gz\'); ' - 'result = antspynet.deep_atropos(t1w); ' - 'ants.image_write\'result[\'segmentation_image\'], \'segmentation.nii.gz\')\'; ' + 'python3 -c "import ants, antspynet; ' + 't1w = ants.image_read(\'T1w.nii.gz\'); ' + 'result = antspynet.deep_atropos(t1w); ' + 'ants.image_write(result[\'segmentation_image\'], \'segmentation.nii.gz\')"; ' '5ttgen deep_atropos segmentation.nii.gz 5tt_segmentation.mif', 'Because the input segmentation here is an integer image, ' 'where each voxel just contains an index corresponding to the maximal tissue class, ' 'the output 5TT image will not possess any fractional partial volumes; ' 'it will just contain the value 1.0 in whichever 5TT volume corresponds to the singular assigned tissue class.') parser.add_example_usage('To utilise the "probability images"', - 'python3 -c \'import ants, antspynet, nibabel, numpy; ' - 't1w = ants.image_read(\'T1w.nii.gz\'); ' - 'result = antspynet.deep_atropos(t1w); ' - 'prob_maps = numpy.stack([numpy.array(img.numpy()) for img in result[\'probability_images\']], axis=-1); ' - 'nibabel.save(nib.Nifti1Image(prob_maps, t1w.affine), \'probabilities.nii.gz\')\'; ' + 'python3 -c "import ants, antspynet, nibabel, numpy; ' + 'inpath = \'T1w.nii.gz\'; ' + 't1w_ants = ants.image_read(inpath); ' + 't1w_nib = nibabel.load(inpath); ' + 'result = antspynet.deep_atropos(t1w_ants); ' + 'prob_maps = numpy.stack([numpy.array(img.numpy()) for img in result[\'probability_images\']], axis=-1); ' + 'nibabel.save(nibabel.Nifti1Image(prob_maps, t1w_nib.affine), \'probabilities.nii.gz\')"; ' '5ttgen deep_atropos probabilities.nii.gz 5tt_probabilities.mif', 'In this use case, the poerior probabilities of these tissue classes are interpreted as partial volume fractions ' 'and imported into the derivative 5TT image appropriately.') From 4470cf1a4eefca371810ee2f59e26ff140018d0a Mon Sep 17 00:00:00 2001 From: Lucius <30386169+LucSam@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:16:23 +1100 Subject: [PATCH 11/11] 5ttgen deep_atropos: fix typo in documentation --- docs/reference/commands/5ttgen.rst | 2 +- python/mrtrix3/commands/5ttgen/deep_atropos.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/commands/5ttgen.rst b/docs/reference/commands/5ttgen.rst index 72f5925ef5..3a8faa7d62 100644 --- a/docs/reference/commands/5ttgen.rst +++ b/docs/reference/commands/5ttgen.rst @@ -130,7 +130,7 @@ Example usages $ python3 -c "import ants, antspynet, nibabel, numpy; inpath = 'T1w.nii.gz'; t1w_ants = ants.image_read(inpath); t1w_nib = nibabel.load(inpath); result = antspynet.deep_atropos(t1w_ants); prob_maps = numpy.stack([numpy.array(img.numpy()) for img in result['probability_images']], axis=-1); nibabel.save(nibabel.Nifti1Image(prob_maps, t1w_nib.affine), 'probabilities.nii.gz')"; 5ttgen deep_atropos probabilities.nii.gz 5tt_probabilities.mif - In this use case, the poerior probabilities of these tissue classes are interpreted as partial volume fractions and imported into the derivative 5TT image appropriately. + In this use case, the posterior probabilities of these tissue classes are interpreted as partial volume fractions and imported into the derivative 5TT image appropriately. Options ------- diff --git a/python/mrtrix3/commands/5ttgen/deep_atropos.py b/python/mrtrix3/commands/5ttgen/deep_atropos.py index 88b129fca7..a3cf5871bf 100644 --- a/python/mrtrix3/commands/5ttgen/deep_atropos.py +++ b/python/mrtrix3/commands/5ttgen/deep_atropos.py @@ -58,7 +58,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable 'prob_maps = numpy.stack([numpy.array(img.numpy()) for img in result[\'probability_images\']], axis=-1); ' 'nibabel.save(nibabel.Nifti1Image(prob_maps, t1w_nib.affine), \'probabilities.nii.gz\')"; ' '5ttgen deep_atropos probabilities.nii.gz 5tt_probabilities.mif', - 'In this use case, the poerior probabilities of these tissue classes are interpreted as partial volume fractions ' + 'In this use case, the posterior probabilities of these tissue classes are interpreted as partial volume fractions ' 'and imported into the derivative 5TT image appropriately.') parser.add_argument('input', type=app.Parser.ImageIn(),