autodmri.scripts

  1import numpy as np
  2import nibabel as nib
  3
  4import os
  5import argparse
  6import logging
  7
  8from autodmri.estimator import estimate_from_dwis, estimate_from_nmaps
  9
 10
 11DESCRIPTION = """
 12Main script for automatically characterizing noise distributions.
 13"""
 14
 15EPILOG = """
 16Reference:
 17
 18MICCAI version
 19---------------
 20St-Jean S., De Luca A., Viergever M.A., Leemans A. (2018)
 21Automatic, Fast and Robust Characterization of Noise Distributions for Diffusion MRI, MICCAI 2018.
 22Springer International Publishing, pp. 304-312. doi: 10.1007/978-3-030-00928-1_35.
 23Available at: https://arxiv.org/abs/1805.12071
 24
 25Journal version
 26-----------------
 27Samuel St-Jean, Alberto De Luca, Chantal M.W. Tax, Max A. Viergever, Alexander Leemans,
 28Automated characterization of noise distributions in diffusion MRI data,
 29Medical Image Analysis, 2020, 101758, ISSN 1361-8415,
 30doi: 10.1016/j.media.2020.101758
 31Available at: http://www.sciencedirect.com/science/article/pii/S1361841520301225
 32"""
 33
 34
 35class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawTextHelpFormatter):
 36    pass
 37
 38
 39def buildArgsParser():
 40
 41    p = argparse.ArgumentParser(description=DESCRIPTION,
 42                                epilog=EPILOG,
 43                                formatter_class=CustomFormatter)
 44
 45    p.add_argument('data', metavar='input',
 46                   help='Path of the input data.')
 47
 48    p.add_argument('sigma', metavar='sigma',
 49                   help='Path of the output sigma volume.')
 50
 51    p.add_argument('N', metavar='N',
 52                   help='Path of the output N volume.')
 53
 54    p.add_argument('mask', metavar='mask',
 55                   help='Path of the output mask for voxels identified as noise.')
 56
 57    p.add_argument('-a', '--axis', type=int, default=-2, choices=[0, 1, 2],
 58                   help='Axis (0, 1 or 2 typically) which is assumed to contain uniform noise.')
 59
 60    p.add_argument('-m', '--method', default='moments', choices=['moments', 'maxlk'], metavar='string',
 61                   help='Method to use for estimating parameters, either "moments" or "maxlk".')
 62
 63    p.add_argument('--ncores', metavar='int', type=int, default=-1,
 64                   help='Number of cores to use for multithreading.')
 65
 66    p.add_argument('--exclude', metavar='file',
 67                   help='Mask indicating which voxels to exclude from the computation. Useful to remove gross artifacts.')
 68
 69    p.add_argument('--noise_maps', action='store_true',
 70                   help='Estimate in small windows instead of whole slices over the input volume. Only valid in theory for noise maps.')
 71
 72    p.add_argument('--subsample', action='store_true',
 73                   help='If supplied, estimate in non-overlapping windows with option --noise_maps.')
 74
 75    p.add_argument('--fast_median', action='store_true',
 76                   help='If supplied, computes the median of medians from each volume instead of one median value.\n'
 77                      'Useful for large datasets with many volumes (e.g. HCP) since the median requires a full copy of the data and sorting.')
 78
 79    p.add_argument('--size', metavar='int', type=int, default=5,
 80                   help='Size of the window for local noise maps estimation.')
 81
 82    p.add_argument('-f', '--force', action='store_true', dest='overwrite',
 83                   help='If set, overwrites the output text file if it already exists.')
 84
 85    p.add_argument('-v', '--verbose', action='store_true', dest='verbose',
 86                   help='If set, shows a progress bar during processing and prints useful information.')
 87
 88    p.add_argument('-l', '--log', dest='logfile', metavar='file',
 89                   help='Save the logging output to this file. Implies verbose output.')
 90
 91    return p
 92
 93
 94def main():
 95    parser = buildArgsParser()
 96    args = parser.parse_args()
 97
 98    logger = logging.getLogger('autodmri')
 99
100    if args.logfile is not None:
101        handler = logging.FileHandler(args.logfile)
102        args.verbose = True
103    else:
104        handler = logging.StreamHandler(args.logfile)
105
106    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s', "%Y-%m-%d %H:%M:%S")
107    handler.setFormatter(formatter)
108    logger.addHandler(handler)
109
110    if args.verbose:
111        logger.setLevel(logging.INFO)
112        logger.info('Verbosity is on')
113
114    overwritable_files = [args.sigma,
115                          args.N,
116                          args.mask]
117
118    for f in overwritable_files:
119        if f is not None and os.path.isfile(f):
120            if args.overwrite:
121                logger.warning(f'Overwriting {os.path.realpath(f)}')
122            else:
123                parser.error(f'{f} already exists! Use -f or --force to overwrite it.')
124
125    vol = nib.load(args.data)
126    data = vol.get_fdata(dtype=np.float32)
127    aff = vol.affine
128    hdr = vol.header
129
130    ncores = args.ncores
131    method = args.method
132    axis = args.axis
133    full = not args.subsample
134    size = args.size
135    noise_maps = args.noise_maps
136
137    if args.exclude is not None:
138        exclude_mask = nib.load(args.exclude).get_fdata().astype(bool)
139        logger.info(f'Excluding voxels from file {args.exclude}')
140    else:
141        exclude_mask = None
142
143    logger.info(f'Now estimating over file {args.data} with method = {method} and axis = {axis}')
144
145    if noise_maps:
146        if full:
147            overlap = 'overlapping windows'
148        else:
149            overlap = 'non-overlapping windows'
150
151        if data.ndim == 3:
152            data = data[..., None]
153
154        logger.info(f'Estimation will be done over noise maps with a window of size {size} and {overlap}')
155        sigma, N, mask = estimate_from_nmaps(data, size=size, return_mask=True, method=method, full=full, ncores=ncores, use_rejection=False, verbose=args.verbose)
156
157    else:
158        if axis < 0:
159            axis = data.ndim + axis
160
161        if args.fast_median:
162            logger.info('Estimation of the medians will be done over each volume, then on the median of the medians.')
163        elif data.shape[-1] > 100:
164            logger.warning(f'Estimation of the median will be done over the whole volume, but you have {data.shape[-1]} volumes.\n' +
165                           '\tConsider the option --fast_median if memory usage is high and startup time is too long.')
166
167        sigma, N, mask = estimate_from_dwis(data, axis=axis, return_mask=True, exclude_mask=exclude_mask, ncores=ncores,
168                                            method=method, verbose=args.verbose, fast_median=args.fast_median)
169
170        # Broadcast the 1D arrays to full 3D
171        if axis == 0:
172            sigma = sigma[:, None, None]
173            N = N[:, None, None]
174        elif axis == 1:
175            sigma = sigma[None, :, None]
176            N = N[None, :, None]
177        elif axis == 2:
178            sigma = sigma[None, None, :]
179            N = N[None, None, :]
180        else:
181            raise ValueError(f'axis = {axis} is not 0, 1 or 2, but that should never happen!')
182
183        sigma = np.ones(mask.shape) * sigma
184        N = np.ones(mask.shape) * N
185
186    # Save the data
187    logger.info(f'Output files are {args.sigma}, {args.N} and {args.mask}')
188    mask = mask.astype(np.int16)
189    sigma = sigma.astype(np.float32)
190    N = N.astype(np.float32)
191
192    nib.Nifti1Image(sigma, aff).to_filename(args.sigma)
193    nib.Nifti1Image(N, aff).to_filename(args.N)
194    nib.Nifti1Image(mask, aff).to_filename(args.mask)
DESCRIPTION = '\nMain script for automatically characterizing noise distributions.\n'
EPILOG = '\nReference:\n\nMICCAI version\n---------------\nSt-Jean S., De Luca A., Viergever M.A., Leemans A. (2018)\nAutomatic, Fast and Robust Characterization of Noise Distributions for Diffusion MRI, MICCAI 2018.\nSpringer International Publishing, pp. 304-312. doi: 10.1007/978-3-030-00928-1_35.\nAvailable at: https://arxiv.org/abs/1805.12071\n\nJournal version\n-----------------\nSamuel St-Jean, Alberto De Luca, Chantal M.W. Tax, Max A. Viergever, Alexander Leemans,\nAutomated characterization of noise distributions in diffusion MRI data,\nMedical Image Analysis, 2020, 101758, ISSN 1361-8415,\ndoi: 10.1016/j.media.2020.101758\nAvailable at: http://www.sciencedirect.com/science/article/pii/S1361841520301225\n'
class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawTextHelpFormatter):
36class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawTextHelpFormatter):
37    pass

Help message formatter which adds default values to argument help.

Only the name of this class is considered a public API. All the methods provided by the class are considered an implementation detail.

def buildArgsParser():
40def buildArgsParser():
41
42    p = argparse.ArgumentParser(description=DESCRIPTION,
43                                epilog=EPILOG,
44                                formatter_class=CustomFormatter)
45
46    p.add_argument('data', metavar='input',
47                   help='Path of the input data.')
48
49    p.add_argument('sigma', metavar='sigma',
50                   help='Path of the output sigma volume.')
51
52    p.add_argument('N', metavar='N',
53                   help='Path of the output N volume.')
54
55    p.add_argument('mask', metavar='mask',
56                   help='Path of the output mask for voxels identified as noise.')
57
58    p.add_argument('-a', '--axis', type=int, default=-2, choices=[0, 1, 2],
59                   help='Axis (0, 1 or 2 typically) which is assumed to contain uniform noise.')
60
61    p.add_argument('-m', '--method', default='moments', choices=['moments', 'maxlk'], metavar='string',
62                   help='Method to use for estimating parameters, either "moments" or "maxlk".')
63
64    p.add_argument('--ncores', metavar='int', type=int, default=-1,
65                   help='Number of cores to use for multithreading.')
66
67    p.add_argument('--exclude', metavar='file',
68                   help='Mask indicating which voxels to exclude from the computation. Useful to remove gross artifacts.')
69
70    p.add_argument('--noise_maps', action='store_true',
71                   help='Estimate in small windows instead of whole slices over the input volume. Only valid in theory for noise maps.')
72
73    p.add_argument('--subsample', action='store_true',
74                   help='If supplied, estimate in non-overlapping windows with option --noise_maps.')
75
76    p.add_argument('--fast_median', action='store_true',
77                   help='If supplied, computes the median of medians from each volume instead of one median value.\n'
78                      'Useful for large datasets with many volumes (e.g. HCP) since the median requires a full copy of the data and sorting.')
79
80    p.add_argument('--size', metavar='int', type=int, default=5,
81                   help='Size of the window for local noise maps estimation.')
82
83    p.add_argument('-f', '--force', action='store_true', dest='overwrite',
84                   help='If set, overwrites the output text file if it already exists.')
85
86    p.add_argument('-v', '--verbose', action='store_true', dest='verbose',
87                   help='If set, shows a progress bar during processing and prints useful information.')
88
89    p.add_argument('-l', '--log', dest='logfile', metavar='file',
90                   help='Save the logging output to this file. Implies verbose output.')
91
92    return p
def main():
 95def main():
 96    parser = buildArgsParser()
 97    args = parser.parse_args()
 98
 99    logger = logging.getLogger('autodmri')
100
101    if args.logfile is not None:
102        handler = logging.FileHandler(args.logfile)
103        args.verbose = True
104    else:
105        handler = logging.StreamHandler(args.logfile)
106
107    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s', "%Y-%m-%d %H:%M:%S")
108    handler.setFormatter(formatter)
109    logger.addHandler(handler)
110
111    if args.verbose:
112        logger.setLevel(logging.INFO)
113        logger.info('Verbosity is on')
114
115    overwritable_files = [args.sigma,
116                          args.N,
117                          args.mask]
118
119    for f in overwritable_files:
120        if f is not None and os.path.isfile(f):
121            if args.overwrite:
122                logger.warning(f'Overwriting {os.path.realpath(f)}')
123            else:
124                parser.error(f'{f} already exists! Use -f or --force to overwrite it.')
125
126    vol = nib.load(args.data)
127    data = vol.get_fdata(dtype=np.float32)
128    aff = vol.affine
129    hdr = vol.header
130
131    ncores = args.ncores
132    method = args.method
133    axis = args.axis
134    full = not args.subsample
135    size = args.size
136    noise_maps = args.noise_maps
137
138    if args.exclude is not None:
139        exclude_mask = nib.load(args.exclude).get_fdata().astype(bool)
140        logger.info(f'Excluding voxels from file {args.exclude}')
141    else:
142        exclude_mask = None
143
144    logger.info(f'Now estimating over file {args.data} with method = {method} and axis = {axis}')
145
146    if noise_maps:
147        if full:
148            overlap = 'overlapping windows'
149        else:
150            overlap = 'non-overlapping windows'
151
152        if data.ndim == 3:
153            data = data[..., None]
154
155        logger.info(f'Estimation will be done over noise maps with a window of size {size} and {overlap}')
156        sigma, N, mask = estimate_from_nmaps(data, size=size, return_mask=True, method=method, full=full, ncores=ncores, use_rejection=False, verbose=args.verbose)
157
158    else:
159        if axis < 0:
160            axis = data.ndim + axis
161
162        if args.fast_median:
163            logger.info('Estimation of the medians will be done over each volume, then on the median of the medians.')
164        elif data.shape[-1] > 100:
165            logger.warning(f'Estimation of the median will be done over the whole volume, but you have {data.shape[-1]} volumes.\n' +
166                           '\tConsider the option --fast_median if memory usage is high and startup time is too long.')
167
168        sigma, N, mask = estimate_from_dwis(data, axis=axis, return_mask=True, exclude_mask=exclude_mask, ncores=ncores,
169                                            method=method, verbose=args.verbose, fast_median=args.fast_median)
170
171        # Broadcast the 1D arrays to full 3D
172        if axis == 0:
173            sigma = sigma[:, None, None]
174            N = N[:, None, None]
175        elif axis == 1:
176            sigma = sigma[None, :, None]
177            N = N[None, :, None]
178        elif axis == 2:
179            sigma = sigma[None, None, :]
180            N = N[None, None, :]
181        else:
182            raise ValueError(f'axis = {axis} is not 0, 1 or 2, but that should never happen!')
183
184        sigma = np.ones(mask.shape) * sigma
185        N = np.ones(mask.shape) * N
186
187    # Save the data
188    logger.info(f'Output files are {args.sigma}, {args.N} and {args.mask}')
189    mask = mask.astype(np.int16)
190    sigma = sigma.astype(np.float32)
191    N = N.astype(np.float32)
192
193    nib.Nifti1Image(sigma, aff).to_filename(args.sigma)
194    nib.Nifti1Image(N, aff).to_filename(args.N)
195    nib.Nifti1Image(mask, aff).to_filename(args.mask)