From fa778a3367161005a5ddc4fa474be85cb08e0336 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 3 Jun 2026 21:34:34 +1000 Subject: [PATCH] Refactor dwi2response brain mask derivation with shell subsets Resolve a conflict in dwi2response between restricting response function estimation to a subset of shells and deriving a brain mask. Previously, when a shell subset was requested (or the chosen algorithm operates on a single shell) and no mask was supplied, dwi2mask was run on the already shell-filtered data. The command now imports the whole DWI series, derives the mask from all available data, and only then extracts the requested shells for response function estimation; when a mask is provided, the subset continues to be extracted directly on import. A paragraph is added to the command documentation explaining this behaviour and noting that, to mask from the same subset, the user should either supply that subset as input or provide a mask generated from it. Session prompts: 1. > Python command dwi2response currently contains a conflict regarding the use of a subset of shells for response function estimation vs. derivation of a brain mask. If either the user specifies that they wish to compute response functions for only a subset of shells, or an algorithm nominates that it can only operate on a single b>0 shell---possibly including the b=0 shell also---then this filtering will be applied at initial import of data into the scratch directory. If this imported image is then used to derive a brain mask, because the user did not provide one themselves, then the derivation of that mask will be performed using only that particular subset of shells. The command is to be refactored as follows. > 1. If the user specifies a subset of shells, but does not provide a mask: > 1.1. The whole input DWI series will be imported into the scratch directory. > 1.2. Command dwi2mask will be executed on this complete DWI series. > 1.3. The user-requested subset of shells will be extracted using dwiextract; app.cleanup() can then be executed on the initial imported DWI. > 1.4. The requested dwi2response algorithm is executed taking as input the shell-filtered DWI. > 2. If the user specifies a subset of shells, but also provides a mask: > 2.1. The input DWI series will be filtered using dwiextract immediately upon import. > 2.2. THe requested dwi2response algorithm is executed taking this DWI series as input. > A paragraph shall be added to the command documentation describing that in the absence of a user-specified mask, the whole DWI series will be used for brain mask derivation; if the user wishes for the same subset of shells used for response function estimation to also be used for brain mask estimation, then they should generate that subset using eg. the dwiextract command and then provide that result as the input to dwi2response. Generated-by: Claude Opus 4.8 --- docs/reference/commands/dwi2response.rst | 2 + .../commands/dwi2response/dwi2response.py | 66 +++++++++++++++---- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/docs/reference/commands/dwi2response.rst b/docs/reference/commands/dwi2response.rst index 60039a4337..8144093bf1 100644 --- a/docs/reference/commands/dwi2response.rst +++ b/docs/reference/commands/dwi2response.rst @@ -30,6 +30,8 @@ https://mrtrix.readthedocs.io/en/3.0.8/constrained_spherical_deconvolution/respo Note that if the -mask command-line option is not specified, the MRtrix3 command dwi2mask will automatically be called to derive an initial voxel exclusion mask. More information on mask derivation from DWI data can be found at: https://mrtrix.readthedocs.io/en/3.0.8/dwi_preprocessing/masking.html +In the absence of a user-specified mask (option -mask), the whole DWI series will be used for derivation of the brain mask, even where only a subset of the DWI volumes is used for response function estimation (whether because the -shells option has been specified, or because the nominated algorithm operates on only a single b-value shell). If it is instead desired that the same subset of shells used for response function estimation also be used for brain mask derivation, then the user has two alternatives: either generate that subset of shells themselves---e.g. using the dwiextract command---and provide the result as the input to dwi2response; or generate a brain mask from that subset of shells and provide that mask via the -mask option. + Options ------- diff --git a/python/mrtrix3/commands/dwi2response/dwi2response.py b/python/mrtrix3/commands/dwi2response/dwi2response.py index 72be7ae297..ea5eab8a4d 100644 --- a/python/mrtrix3/commands/dwi2response/dwi2response.py +++ b/python/mrtrix3/commands/dwi2response/dwi2response.py @@ -38,6 +38,19 @@ def usage(cmdline): #pylint: disable=unused-variable ' derive an initial voxel exclusion mask.' ' More information on mask derivation from DWI data can be found at: \n' f'https://mrtrix.readthedocs.io/en/{version.TAG}/dwi_preprocessing/masking.html') + cmdline.add_description('In the absence of a user-specified mask (option -mask),' + ' the whole DWI series will be used for derivation of the brain mask,' + ' even where only a subset of the DWI volumes is used for response function estimation' + ' (whether because the -shells option has been specified,' + ' or because the nominated algorithm operates on only a single b-value shell).' + ' If it is instead desired that the same subset of shells used for response function estimation' + ' also be used for brain mask derivation,' + ' then the user has two alternatives:' + ' either generate that subset of shells themselves---' + 'e.g. using the dwiextract command---' + 'and provide the result as the input to dwi2response;' + ' or generate a brain mask from that subset of shells' + ' and provide that mask via the -mask option.') # General options common_options = cmdline.add_argument_group('General dwi2response options') @@ -95,14 +108,48 @@ def execute(): #pylint: disable=unused-variable 'either in image header, or using -grad / -fslgrad option') app.activate_scratch_dir() + + # Determine whether only a subset of the DWI volumes is to be used for response function estimation; + # this is the case if the user has explicitly requested a subset of shells, + # or if the nominated algorithm operates on only a single b-value shell. + need_to_extract = bool(alg.NEEDS_SINGLE_SHELL or shells_option) + extract_option = shells_option + singleshell_option + + # Import the user-provided mask, if one was given + if app.ARGS.mask: + app.console(f'Importing mask ({app.ARGS.mask})...') + run.command(['mrconvert', app.ARGS.mask, 'mask.mif', '-datatype', 'bit'], + show=False, + preserve_pipes=True) + # Get standard input data into the scratch directory - if alg.NEEDS_SINGLE_SHELL or shells_option: + if need_to_extract and alg.SUPPORTS_MASK and not app.ARGS.mask: + # The user has requested that only a subset of the data be used for response function estimation + # (or the nominated algorithm operates on only a single shell), + # but no mask has been provided and therefore one must be derived. + # Import the whole DWI series so that brain mask derivation can make use of all available data, + # derive the mask, and only then extract the requested subset of shells + # for the purpose of response function estimation. + app.console(f'Importing DWI data ({app.ARGS.input})...') + run.command(['mrconvert', app.ARGS.input, 'dwi_full.mif', '-strides', '0,0,0,1'] + + grad_import_option, + show=False, + preserve_pipes=True) + dwi2mask_algo = CONFIG['Dwi2maskAlgorithm'] + app.console(f'Computing brain mask (dwi2mask {dwi2mask_algo})...') + run.command(f'dwi2mask {dwi2mask_algo} dwi_full.mif mask.mif', show=False) + app.console('Extracting requested b-value shells for response function estimation...') + run.command(['dwiextract', 'dwi_full.mif', 'dwi.mif'] + extract_option, + show=False) + app.cleanup('dwi_full.mif') + elif need_to_extract: + # Either a mask has been provided by the user, or the nominated algorithm makes no use of a mask; + # the requested subset of shells can therefore be extracted immediately upon import. app.console(f'Importing DWI data ({app.ARGS.input}) and selecting b-values...') run.command(['mrconvert', app.ARGS.input, '-', '-strides', '0,0,0,1'] + grad_import_option + ['|', 'dwiextract', '-', 'dwi.mif'] - + shells_option - + singleshell_option, + + extract_option, show=False, preserve_pipes=True) else: # Don't discard b=0 in multi-shell algorithms @@ -111,11 +158,10 @@ def execute(): #pylint: disable=unused-variable + grad_import_option, show=False, preserve_pipes=True) - if app.ARGS.mask: - app.console(f'Importing mask ({app.ARGS.mask})...') - run.command(['mrconvert', app.ARGS.mask, 'mask.mif', '-datatype', 'bit'], - show=False, - preserve_pipes=True) + if alg.SUPPORTS_MASK and not app.ARGS.mask: + dwi2mask_algo = CONFIG['Dwi2maskAlgorithm'] + app.console(f'Computing brain mask (dwi2mask {dwi2mask_algo})...') + run.command(f'dwi2mask {dwi2mask_algo} dwi.mif mask.mif', show=False) if alg.SUPPORTS_MASK: if app.ARGS.mask: @@ -125,10 +171,6 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('Dimensions of provided mask image do not match DWI') if not (len(mask_header.size()) == 3 or (len(mask_header.size()) == 4 and mask_header.size()[3] == 1)): raise MRtrixError('Provided mask image needs to be a 3D image') - else: - dwi2mask_algo = CONFIG['Dwi2maskAlgorithm'] - app.console(f'Computing brain mask (dwi2mask {dwi2mask_algo})...') - run.command(f'dwi2mask {dwi2mask_algo} dwi.mif mask.mif', show=False) if not image.statistics('mask.mif', mask='mask.mif').count: raise MRtrixError(('Provided' if app.ARGS.mask else 'Generated') + ' mask image does not contain any voxels')