11#!/usr/bin/env python
22# -*- coding: utf-8 -*-
3- """Change directory to provide relative paths for doctests
3+ """
4+ Change directory to provide relative paths for doctests
45 >>> import os
56 >>> filepath = os.path.dirname( os.path.realpath( __file__ ) )
67 >>> datadir = os.path.realpath(os.path.join(filepath, '../../testing/data'))
78 >>> os.chdir(datadir)
89"""
910import os .path as op
10- import warnings
11-
1211import nibabel as nb
1312import numpy as np
1413
15- from ..base import (traits , TraitedSpec , BaseInterface , File , isdefined )
16- from ...utils .filemanip import split_filename
17- from ...utils .misc import package_check
14+ from ..base import (traits , TraitedSpec , File , isdefined )
15+ from .base import DipyBaseInterface
1816from ... import logging
19- iflogger = logging .getLogger ('interface' )
20-
21- have_dipy = True
22- try :
23- package_check ('dipy' , version = '0.6.0' )
24- except Exception as e :
25- have_dipy = False
17+ IFLOGGER = logging .getLogger ('interface' )
2618
2719
2820class ResampleInputSpec (TraitedSpec ):
@@ -33,15 +25,17 @@ class ResampleInputSpec(TraitedSpec):
3325 ' is set, then isotropic regridding will '
3426 'be performed, with spacing equal to the '
3527 'smallest current zoom.' ))
36- interp = traits .Int (1 , mandatory = True , usedefault = True , desc = ('order of '
37- 'the interpolator (0 = nearest, 1 = linear, etc.' ))
28+ interp = traits .Int (
29+ 1 , mandatory = True , usedefault = True ,
30+ desc = ('order of the interpolator (0 = nearest, 1 = linear, etc.' ))
3831
3932
4033class ResampleOutputSpec (TraitedSpec ):
4134 out_file = File (exists = True )
4235
4336
44- class Resample (BaseInterface ):
37+ class Resample (DipyBaseInterface ):
38+
4539 """
4640 An interface to reslicing diffusion datasets.
4741 See
@@ -69,7 +63,7 @@ def _run_interface(self, runtime):
6963 resample_proxy (self .inputs .in_file , order = order ,
7064 new_zooms = vox_size , out_file = out_file )
7165
72- iflogger .info ('Resliced image saved as {i}' .format (i = out_file ))
66+ IFLOGGER .info ('Resliced image saved as {i}' .format (i = out_file ))
7367 return runtime
7468
7569 def _list_outputs (self ):
@@ -92,17 +86,21 @@ class DenoiseInputSpec(TraitedSpec):
9286 noise_model = traits .Enum ('rician' , 'gaussian' , mandatory = True ,
9387 usedefault = True ,
9488 desc = ('noise distribution model' ))
89+ signal_mask = File (desc = ('mask in which the mean signal '
90+ 'will be computed' ), exists = True )
9591 noise_mask = File (desc = ('mask in which the standard deviation of noise '
9692 'will be computed' ), exists = True )
9793 patch_radius = traits .Int (1 , desc = 'patch radius' )
9894 block_radius = traits .Int (5 , desc = 'block_radius' )
95+ snr = traits .Float (desc = 'manually set an SNR' )
9996
10097
10198class DenoiseOutputSpec (TraitedSpec ):
10299 out_file = File (exists = True )
103100
104101
105- class Denoise (BaseInterface ):
102+ class Denoise (DipyBaseInterface ):
103+
106104 """
107105 An interface to denoising diffusion datasets [Coupe2008]_.
108106 See
@@ -140,16 +138,24 @@ def _run_interface(self, runtime):
140138 if isdefined (self .inputs .block_radius ):
141139 settings ['block_radius' ] = self .inputs .block_radius
142140
141+ snr = None
142+ if isdefined (self .inputs .snr ):
143+ snr = self .inputs .snr
144+
145+ signal_mask = None
146+ if isdefined (self .inputs .signal_mask ):
147+ signal_mask = nb .load (self .inputs .signal_mask ).get_data ()
143148 noise_mask = None
144- if isdefined (self .inputs .in_mask ):
149+ if isdefined (self .inputs .noise_mask ):
145150 noise_mask = nb .load (self .inputs .noise_mask ).get_data ()
146151
147- _ , s = nlmeans_proxy (self .inputs .in_file ,
148- settings ,
149- noise_mask = noise_mask ,
152+ _ , s = nlmeans_proxy (self .inputs .in_file , settings ,
153+ snr = snr ,
154+ smask = signal_mask ,
155+ nmask = noise_mask ,
150156 out_file = out_file )
151- iflogger .info (('Denoised image saved as {i}, estimated '
152- 'sigma ={s}' ).format (i = out_file , s = s ))
157+ IFLOGGER .info (('Denoised image saved as {i}, estimated '
158+ 'SNR ={s}' ).format (i = out_file , s = str ( s ) ))
153159 return runtime
154160
155161 def _list_outputs (self ):
@@ -169,7 +175,7 @@ def resample_proxy(in_file, order=3, new_zooms=None, out_file=None):
169175 """
170176 Performs regridding of an image to set isotropic voxel sizes using dipy.
171177 """
172- from dipy .align .aniso2iso import resample
178+ from dipy .align .reslice import reslice
173179
174180 if out_file is None :
175181 fname , fext = op .splitext (op .basename (in_file ))
@@ -191,7 +197,7 @@ def resample_proxy(in_file, order=3, new_zooms=None, out_file=None):
191197 if np .all (im_zooms == new_zooms ):
192198 return in_file
193199
194- data2 , affine2 = resample (data , affine , im_zooms , new_zooms , order = order )
200+ data2 , affine2 = reslice (data , affine , im_zooms , new_zooms , order = order )
195201 tmp_zooms = np .array (hdr .get_zooms ())
196202 tmp_zooms [:3 ] = new_zooms [0 ]
197203 hdr .set_zooms (tuple (tmp_zooms ))
@@ -203,12 +209,16 @@ def resample_proxy(in_file, order=3, new_zooms=None, out_file=None):
203209
204210
205211def nlmeans_proxy (in_file , settings ,
206- noise_mask = None , out_file = None ):
212+ snr = None ,
213+ smask = None ,
214+ nmask = None ,
215+ out_file = None ):
207216 """
208217 Uses non-local means to denoise 4D datasets
209218 """
210- package_check ('dipy' , version = '0.8.0.dev' )
211219 from dipy .denoise .nlmeans import nlmeans
220+ from scipy .ndimage .morphology import binary_erosion
221+ from scipy import ndimage
212222
213223 if out_file is None :
214224 fname , fext = op .splitext (op .basename (in_file ))
@@ -222,13 +232,69 @@ def nlmeans_proxy(in_file, settings,
222232 data = img .get_data ()
223233 aff = img .affine
224234
225- nmask = data [..., 0 ] > 80
226- if noise_mask is not None :
227- nmask = noise_mask > 0
228-
229- sigma = np .std (data [nmask == 1 ])
230- den = nlmeans (data , sigma , ** settings )
235+ if data .ndim < 4 :
236+ data = data [..., np .newaxis ]
237+
238+ data = np .nan_to_num (data )
239+
240+ if data .max () < 1.0e-4 :
241+ raise RuntimeError ('There is no signal in the image' )
242+
243+ df = 1.0
244+ if data .max () < 1000.0 :
245+ df = 1000. / data .max ()
246+ data *= df
247+
248+ b0 = data [..., 0 ]
249+
250+ if smask is None :
251+ smask = np .zeros_like (b0 )
252+ smask [b0 > np .percentile (b0 , 85. )] = 1
253+
254+ smask = binary_erosion (
255+ smask .astype (np .uint8 ), iterations = 2 ).astype (np .uint8 )
256+
257+ if nmask is None :
258+ nmask = np .ones_like (b0 , dtype = np .uint8 )
259+ bmask = settings ['mask' ]
260+ if bmask is None :
261+ bmask = np .zeros_like (b0 )
262+ bmask [b0 > np .percentile (b0 [b0 > 0 ], 10 )] = 1
263+ label_im , nb_labels = ndimage .label (bmask )
264+ sizes = ndimage .sum (bmask , label_im , range (nb_labels + 1 ))
265+ maxidx = np .argmax (sizes )
266+ bmask = np .zeros_like (b0 , dtype = np .uint8 )
267+ bmask [label_im == maxidx ] = 1
268+ nmask [bmask > 0 ] = 0
269+ else :
270+ nmask = np .squeeze (nmask )
271+ nmask [nmask > 0.0 ] = 1
272+ nmask [nmask < 1 ] = 0
273+ nmask = nmask .astype (bool )
274+
275+ nmask = binary_erosion (nmask , iterations = 1 ).astype (np .uint8 )
276+
277+ den = np .zeros_like (data )
278+
279+ est_snr = True
280+ if snr is not None :
281+ snr = [snr ] * data .shape [- 1 ]
282+ est_snr = False
283+ else :
284+ snr = []
285+
286+ for i in range (data .shape [- 1 ]):
287+ d = data [..., i ]
288+ if est_snr :
289+ s = np .mean (d [smask > 0 ])
290+ n = np .std (d [nmask > 0 ])
291+ snr .append (s / n )
292+
293+ den [..., i ] = nlmeans (d , snr [i ], ** settings )
294+
295+ den = np .squeeze (den )
296+ den /= df
231297
232298 nb .Nifti1Image (den .astype (hdr .get_data_dtype ()), aff ,
233299 hdr ).to_filename (out_file )
234- return out_file , sigma
300+ return out_file , snr
0 commit comments