autodmri.estimator

  1import numpy as np
  2
  3try:
  4    from scipy.ndimage import zoom
  5except ImportError:
  6    from scipy.ndimage.interpolation import zoom
  7
  8from scipy.special import gammaincinv
  9
 10from autodmri.gamma import get_noise_distribution
 11from autodmri.blocks import extract_patches
 12
 13from joblib import Parallel, delayed
 14from tqdm import tqdm
 15
 16###########################################
 17# These functions are for over dwis
 18###########################################
 19
 20
 21def estimate_from_dwis(data, axis=-2, return_mask=False, exclude_mask=None, ncores=-1, method='moments', verbose=False, fast_median=False):
 22    '''Given the data, splits over each slice to compute parameters of the gamma distribution
 23
 24    input
 25    ------
 26        data : array, input volume used to identify noise voxels and estimate the noise distribution
 27
 28    optional
 29    --------
 30        axis : (0, 1 or 2) the axis to consider as a slab of uniform noise profile
 31
 32        return_mask : bool, if True returns the identified noise voxels as a mask
 33
 34        exclude_mask : array, mask indicating voxels to remove from all the computations, such as those containing huge artifacts.
 35
 36        ncores : int, number of cores to use for multiprocessing
 37
 38        method='moments' or method='maxlk' : which algorithm to use to estimate sigma and N
 39
 40        verbose : bool, Shows a progress bar for parallel processing
 41
 42        fast_median : Computes the median of medians from each volume.
 43        Useful for large datasets with many volumes (e.g. HCP) since the median requires a full copy of the data and sorting.
 44
 45    output
 46    -------
 47    sigma, N, mask (optional)
 48    '''
 49
 50    # guess a gross upper bound of sigma
 51    # if it's masked, median can be zero, so use the nonzero data
 52    if fast_median:
 53        medians = np.zeros(data.shape[-1])
 54
 55        for idx in range(data.shape[-1]):
 56            chunk = data[..., idx]
 57            median = np.median(chunk)
 58
 59            if median == 0:
 60                median = np.median(chunk[chunk > 0])
 61
 62            medians[idx] = median
 63
 64        median = np.median(medians)
 65    else:
 66        median = np.median(data)
 67
 68        if median == 0:
 69            median = np.median(data[data > 0])
 70
 71    if axis < 0:
 72        axis = data.ndim + axis
 73
 74    ranger = range(data.shape[axis])
 75    swapped_data = data.swapaxes(0, axis)
 76
 77    if exclude_mask is None:
 78        exclude_mask = [None] * swapped_data.shape[0]
 79    else:
 80        exclude_mask = exclude_mask.swapaxes(0, axis)
 81
 82    if verbose:
 83        ranger = tqdm(ranger)
 84
 85    output = Parallel(n_jobs=ncores)(delayed(_inner)(swapped_data[i], median, exclude_mask[i], method) for i in ranger)
 86
 87    # output is each slice we took along axis, so the mask might be reversed
 88    sigma = np.zeros(len(output), dtype=np.float32)
 89    N = np.zeros(len(output), dtype=np.float32)
 90    mask = np.zeros(data.shape[:-1], dtype=np.int16).swapaxes(0, axis)
 91
 92    for i, s in enumerate(output):
 93        sigma[i] = s[0]
 94        N[i] = s[1]
 95        mask[i] = s[2]
 96
 97    if return_mask:
 98        return sigma, N, mask.swapaxes(0, axis)
 99    return sigma, N
100
101
102def _inner(data, median, exclude_mask=None, method='moments', l=50, N_min=1, N_max=12, max_iter=100, eps=1e-3):
103
104    def lambda_cdf(N, alpha_prob):
105        out = gammaincinv(N, alpha_prob)
106        out = np.nan_to_num(out).clip(min=1e-7)
107        return out
108
109    def get_mask(data, N_min, N_max, phi, alpha_prob=0.05):
110        if alpha_prob <= 0 or alpha_prob >= 1:
111            raise ValueError(f'alpha_prob is a parameter in ]0, 1[ with default value 0.05, but has value {alpha_prob}')
112
113        K = np.sum(data > 0, axis=-1)
114        sum_data2 = np.sum(data**2, axis=-1)
115
116        lambda_minus = lambda_cdf(N_min*K, alpha_prob/2)
117        lambda_plus = lambda_cdf(N_max*K, 1 - alpha_prob/2)
118
119        mask_current = np.zeros(data.shape[:-1], dtype=bool)
120        mask_loop = np.zeros(data.shape[:-1], dtype=bool)
121
122        for sigma in phi:
123            s = sum_data2 / (2*sigma**2)
124            mask_current[:] = np.logical_and(lambda_minus < s, s < lambda_plus)
125
126            if mask_current.sum() > mask_loop.sum():
127                mask_loop[:] = mask_current
128
129        return mask_loop
130
131    # Explicitly remove known artifacts
132    if exclude_mask is None:
133        exclude_mask = np.zeros(data.shape[:-1], dtype=bool)
134
135    # we don't know N, so guess parameters iteratively
136    data = data.astype(np.float64) # prevent data**4 overflow
137    data = np.nan_to_num(data).clip(min=0) # cleanup in case we had bad data somewhere to prevent nan propagation
138
139    sigma_prev = -1
140    N_prev = -1
141    sigma_init = median / np.sqrt(2 * lambda_cdf(N_max, 0.5))
142    phi = np.arange(1, l+1) * sigma_init / l
143
144    for _ in range(max_iter):
145
146        mask = get_mask(data, N_min, N_max, phi)
147        mask *= np.logical_not(exclude_mask)
148
149        # empty slice -> mask is zero
150        if mask.sum() == 0:
151            return 0, 0, np.zeros_like(mask)
152
153        datam = data[np.broadcast_to(mask[..., None], data.shape)]
154        sigma, N = get_noise_distribution(datam, method=method)
155
156        if sigma == 0 or N == 0:
157            return 0, 0, np.zeros_like(mask)
158
159        # abs error is small?
160        if (np.abs(N - N_prev) < eps) and (np.abs(sigma - sigma_prev) < eps):
161            break
162
163        # relative error is small?
164        if ((np.abs(N - N_prev) / N) < eps) and ((np.abs(sigma - sigma_prev) / sigma) < eps):
165            break
166
167        N_prev = N
168        sigma_prev = sigma
169
170        N_min = N
171        N_max = N
172
173        phi = np.linspace(.95, 1.05, num=11) * sigma
174
175    return sigma, N, mask
176
177
178###########################################
179# These functions are for over noise maps
180###########################################
181
182
183def estimate_from_nmaps(data, size=5, return_mask=True, method='moments', full=False, ncores=-1, use_rejection=False, verbose=False):
184    '''Given the data, estimates parameters of the gamma distribution in small 3D windows.
185
186    input
187    ------
188        data : noise maps to use for parameter estimation.
189
190    optional
191    --------
192        size : size of the 3D windows (default 5)
193
194        return_mask : bool, if True returns the identified noise voxels as a mask
195
196        method='moments' or method='maxlk' : which algorithm to use to estimate sigma and N
197
198        full : bool, if True estimates are made in overlapping windows
199
200        ncores : int, number of cores to use for multiprocessing
201
202        use_rejection : if True, iterate to reject voxels in each estimated window, but this is much slower than just using all of the data.
203
204        verbose : bool, Shows a progress bar for parallel processing
205
206    output
207    -------
208    sigma, N, mask (optional)
209    '''
210    m_out = np.zeros(data.shape[:-1], dtype=bool)
211    median = np.median(data)
212
213    if median == 0:
214        median = np.median(data[data > 0])
215
216    if full:
217        reshaped_maps = extract_patches(data, (size, size, size, data.shape[-1]), (1, 1, 1, data.shape[-1]), flatten=False)
218
219        sigma = np.zeros(data.shape[:-1], dtype=np.float32)
220        N = np.zeros(data.shape[:-1], dtype=np.float32)
221        count = np.zeros(data.shape[:-1], dtype=np.int32)
222        mask = np.zeros(data.shape[:-1], dtype=np.int32)
223
224        indexer = list(np.ndindex(reshaped_maps.shape[:reshaped_maps.ndim//2 - 1]))
225
226        if verbose:
227            ranger = tqdm(indexer)
228        else:
229            ranger = indexer
230
231        output = Parallel(n_jobs=ncores)(delayed(proc_inner)(reshaped_maps[i], median, size, method, use_rejection) for i in ranger)
232
233        indexer = (np.index_exp[idx[0]:idx[0] + size, idx[1]:idx[1] + size, idx[2]:idx[2] + size] for idx in indexer)
234
235        # We accumulate the value at each voxel then take the average over the overlap
236        for idx, (s, n, m) in zip(indexer, output):
237            sigma[idx] += s
238            N[idx] += n
239
240            mask[idx] = m.sum()
241            count[idx] += 1
242
243        sigma /= count
244        N /= count
245
246        if return_mask:
247            return sigma, N, mask
248        return sigma, N
249
250    else:
251        reshaped_maps = extract_patches(data, (size, size, size, data.shape[-1]), (size, size, size, data.shape[-1]))
252
253        sigma = np.zeros(reshaped_maps.shape[0], dtype=np.float32)
254        N = np.zeros(reshaped_maps.shape[0], dtype=np.float32)
255        mask = np.zeros((reshaped_maps.shape[0], size**3), dtype=bool)
256
257        ranger = range(reshaped_maps.shape[0])
258
259        if verbose:
260            ranger = tqdm(ranger)
261
262        output = Parallel(n_jobs=ncores)(delayed(proc_inner)(reshaped_maps[i], median, size, method, use_rejection) for i in ranger)
263
264        for i, (s, n, m) in enumerate(output):
265            sigma[i] = s
266            N[i] = n
267            mask[i] = np.squeeze(m).sum(axis=-1)
268
269        s_out = sigma.reshape(data.shape[0] // size, data.shape[1] // size, data.shape[2] // size)
270        N_out = N.reshape(data.shape[0] // size, data.shape[1] // size, data.shape[2] // size)
271
272        for n, i in enumerate(np.ndindex(s_out.shape)):
273            i = np.array(i) * size
274            j = i + size
275            m_out[i[0]:j[0], i[1]:j[1], i[2]:j[2]] = mask[n].reshape(size, size, size)
276
277        x, y, z = np.array(s_out.shape) * size
278        interpolated_sigma = np.zeros_like(data[..., 0], dtype=np.float32)
279        interpolated_N = np.zeros_like(data[..., 0], dtype=np.float32)
280
281        interpolated_sigma[:x, :y, :z] = zoom(s_out, size, order=1)
282        interpolated_N[:x, :y, :z] = zoom(N_out, size, order=1)
283
284        if return_mask:
285            return interpolated_sigma, interpolated_N, m_out
286        return interpolated_sigma, interpolated_N
287
288
289def proc_inner(cur_map, median, size, method, use_rejection):
290
291    if use_rejection:
292        cur_map = cur_map.reshape(-1, 1, 1)
293        output = _inner(cur_map, median, method=method)
294    else:
295        cur_map = cur_map.reshape(size**3, 1, -1)
296        sigma, N = get_noise_distribution(cur_map, method=method)
297        mask = np.ones_like(cur_map, dtype=bool)
298
299        if np.isnan(sigma) or np.isnan(N):
300            sigma = 0
301            N = 0
302
303        output = sigma, N, mask
304    return output
def estimate_from_dwis( data, axis=-2, return_mask=False, exclude_mask=None, ncores=-1, method='moments', verbose=False, fast_median=False):
 22def estimate_from_dwis(data, axis=-2, return_mask=False, exclude_mask=None, ncores=-1, method='moments', verbose=False, fast_median=False):
 23    '''Given the data, splits over each slice to compute parameters of the gamma distribution
 24
 25    input
 26    ------
 27        data : array, input volume used to identify noise voxels and estimate the noise distribution
 28
 29    optional
 30    --------
 31        axis : (0, 1 or 2) the axis to consider as a slab of uniform noise profile
 32
 33        return_mask : bool, if True returns the identified noise voxels as a mask
 34
 35        exclude_mask : array, mask indicating voxels to remove from all the computations, such as those containing huge artifacts.
 36
 37        ncores : int, number of cores to use for multiprocessing
 38
 39        method='moments' or method='maxlk' : which algorithm to use to estimate sigma and N
 40
 41        verbose : bool, Shows a progress bar for parallel processing
 42
 43        fast_median : Computes the median of medians from each volume.
 44        Useful for large datasets with many volumes (e.g. HCP) since the median requires a full copy of the data and sorting.
 45
 46    output
 47    -------
 48    sigma, N, mask (optional)
 49    '''
 50
 51    # guess a gross upper bound of sigma
 52    # if it's masked, median can be zero, so use the nonzero data
 53    if fast_median:
 54        medians = np.zeros(data.shape[-1])
 55
 56        for idx in range(data.shape[-1]):
 57            chunk = data[..., idx]
 58            median = np.median(chunk)
 59
 60            if median == 0:
 61                median = np.median(chunk[chunk > 0])
 62
 63            medians[idx] = median
 64
 65        median = np.median(medians)
 66    else:
 67        median = np.median(data)
 68
 69        if median == 0:
 70            median = np.median(data[data > 0])
 71
 72    if axis < 0:
 73        axis = data.ndim + axis
 74
 75    ranger = range(data.shape[axis])
 76    swapped_data = data.swapaxes(0, axis)
 77
 78    if exclude_mask is None:
 79        exclude_mask = [None] * swapped_data.shape[0]
 80    else:
 81        exclude_mask = exclude_mask.swapaxes(0, axis)
 82
 83    if verbose:
 84        ranger = tqdm(ranger)
 85
 86    output = Parallel(n_jobs=ncores)(delayed(_inner)(swapped_data[i], median, exclude_mask[i], method) for i in ranger)
 87
 88    # output is each slice we took along axis, so the mask might be reversed
 89    sigma = np.zeros(len(output), dtype=np.float32)
 90    N = np.zeros(len(output), dtype=np.float32)
 91    mask = np.zeros(data.shape[:-1], dtype=np.int16).swapaxes(0, axis)
 92
 93    for i, s in enumerate(output):
 94        sigma[i] = s[0]
 95        N[i] = s[1]
 96        mask[i] = s[2]
 97
 98    if return_mask:
 99        return sigma, N, mask.swapaxes(0, axis)
100    return sigma, N

Given the data, splits over each slice to compute parameters of the gamma distribution

input

data : array, input volume used to identify noise voxels and estimate the noise distribution

optional

axis : (0, 1 or 2) the axis to consider as a slab of uniform noise profile

return_mask : bool, if True returns the identified noise voxels as a mask

exclude_mask : array, mask indicating voxels to remove from all the computations, such as those containing huge artifacts.

ncores : int, number of cores to use for multiprocessing

method='moments' or method='maxlk' : which algorithm to use to estimate sigma and N

verbose : bool, Shows a progress bar for parallel processing

fast_median : Computes the median of medians from each volume.
Useful for large datasets with many volumes (e.g. HCP) since the median requires a full copy of the data and sorting.

output

sigma, N, mask (optional)

def estimate_from_nmaps( data, size=5, return_mask=True, method='moments', full=False, ncores=-1, use_rejection=False, verbose=False):
184def estimate_from_nmaps(data, size=5, return_mask=True, method='moments', full=False, ncores=-1, use_rejection=False, verbose=False):
185    '''Given the data, estimates parameters of the gamma distribution in small 3D windows.
186
187    input
188    ------
189        data : noise maps to use for parameter estimation.
190
191    optional
192    --------
193        size : size of the 3D windows (default 5)
194
195        return_mask : bool, if True returns the identified noise voxels as a mask
196
197        method='moments' or method='maxlk' : which algorithm to use to estimate sigma and N
198
199        full : bool, if True estimates are made in overlapping windows
200
201        ncores : int, number of cores to use for multiprocessing
202
203        use_rejection : if True, iterate to reject voxels in each estimated window, but this is much slower than just using all of the data.
204
205        verbose : bool, Shows a progress bar for parallel processing
206
207    output
208    -------
209    sigma, N, mask (optional)
210    '''
211    m_out = np.zeros(data.shape[:-1], dtype=bool)
212    median = np.median(data)
213
214    if median == 0:
215        median = np.median(data[data > 0])
216
217    if full:
218        reshaped_maps = extract_patches(data, (size, size, size, data.shape[-1]), (1, 1, 1, data.shape[-1]), flatten=False)
219
220        sigma = np.zeros(data.shape[:-1], dtype=np.float32)
221        N = np.zeros(data.shape[:-1], dtype=np.float32)
222        count = np.zeros(data.shape[:-1], dtype=np.int32)
223        mask = np.zeros(data.shape[:-1], dtype=np.int32)
224
225        indexer = list(np.ndindex(reshaped_maps.shape[:reshaped_maps.ndim//2 - 1]))
226
227        if verbose:
228            ranger = tqdm(indexer)
229        else:
230            ranger = indexer
231
232        output = Parallel(n_jobs=ncores)(delayed(proc_inner)(reshaped_maps[i], median, size, method, use_rejection) for i in ranger)
233
234        indexer = (np.index_exp[idx[0]:idx[0] + size, idx[1]:idx[1] + size, idx[2]:idx[2] + size] for idx in indexer)
235
236        # We accumulate the value at each voxel then take the average over the overlap
237        for idx, (s, n, m) in zip(indexer, output):
238            sigma[idx] += s
239            N[idx] += n
240
241            mask[idx] = m.sum()
242            count[idx] += 1
243
244        sigma /= count
245        N /= count
246
247        if return_mask:
248            return sigma, N, mask
249        return sigma, N
250
251    else:
252        reshaped_maps = extract_patches(data, (size, size, size, data.shape[-1]), (size, size, size, data.shape[-1]))
253
254        sigma = np.zeros(reshaped_maps.shape[0], dtype=np.float32)
255        N = np.zeros(reshaped_maps.shape[0], dtype=np.float32)
256        mask = np.zeros((reshaped_maps.shape[0], size**3), dtype=bool)
257
258        ranger = range(reshaped_maps.shape[0])
259
260        if verbose:
261            ranger = tqdm(ranger)
262
263        output = Parallel(n_jobs=ncores)(delayed(proc_inner)(reshaped_maps[i], median, size, method, use_rejection) for i in ranger)
264
265        for i, (s, n, m) in enumerate(output):
266            sigma[i] = s
267            N[i] = n
268            mask[i] = np.squeeze(m).sum(axis=-1)
269
270        s_out = sigma.reshape(data.shape[0] // size, data.shape[1] // size, data.shape[2] // size)
271        N_out = N.reshape(data.shape[0] // size, data.shape[1] // size, data.shape[2] // size)
272
273        for n, i in enumerate(np.ndindex(s_out.shape)):
274            i = np.array(i) * size
275            j = i + size
276            m_out[i[0]:j[0], i[1]:j[1], i[2]:j[2]] = mask[n].reshape(size, size, size)
277
278        x, y, z = np.array(s_out.shape) * size
279        interpolated_sigma = np.zeros_like(data[..., 0], dtype=np.float32)
280        interpolated_N = np.zeros_like(data[..., 0], dtype=np.float32)
281
282        interpolated_sigma[:x, :y, :z] = zoom(s_out, size, order=1)
283        interpolated_N[:x, :y, :z] = zoom(N_out, size, order=1)
284
285        if return_mask:
286            return interpolated_sigma, interpolated_N, m_out
287        return interpolated_sigma, interpolated_N

Given the data, estimates parameters of the gamma distribution in small 3D windows.

input

data : noise maps to use for parameter estimation.

optional

size : size of the 3D windows (default 5)

return_mask : bool, if True returns the identified noise voxels as a mask

method='moments' or method='maxlk' : which algorithm to use to estimate sigma and N

full : bool, if True estimates are made in overlapping windows

ncores : int, number of cores to use for multiprocessing

use_rejection : if True, iterate to reject voxels in each estimated window, but this is much slower than just using all of the data.

verbose : bool, Shows a progress bar for parallel processing

output

sigma, N, mask (optional)

def proc_inner(cur_map, median, size, method, use_rejection):
290def proc_inner(cur_map, median, size, method, use_rejection):
291
292    if use_rejection:
293        cur_map = cur_map.reshape(-1, 1, 1)
294        output = _inner(cur_map, median, method=method)
295    else:
296        cur_map = cur_map.reshape(size**3, 1, -1)
297        sigma, N = get_noise_distribution(cur_map, method=method)
298        mask = np.ones_like(cur_map, dtype=bool)
299
300        if np.isnan(sigma) or np.isnan(N):
301            sigma = 0
302            N = 0
303
304        output = sigma, N, mask
305    return output