Coverage for install/scipp/scipy/ndimage/__init__.py: 95%

74 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-04-28 01:28 +0000

1# SPDX-License-Identifier: BSD-3-Clause 

2# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) 

3# @author Simon Heybrock 

4"""Sub-package for multidimensional image processing. 

5 

6This subpackage provides wrappers for a subset of functions from 

7:py:mod:`scipy.ndimage`. 

8""" 

9 

10from functools import wraps 

11from typing import Callable, Dict, Optional, Union 

12 

13import scipy.ndimage 

14 

15from ...core import ( 

16 CoordError, 

17 DataArray, 

18 DimensionError, 

19 Variable, 

20 VariancesError, 

21 empty_like, 

22 islinspace, 

23 ones, 

24) 

25from ...typing import VariableLike, VariableLikeType 

26 

27 

28def _ndfilter(func: Callable) -> Callable: 

29 @wraps(func) 

30 def function(x: Union[Variable, DataArray], **kwargs) -> Union[Variable, DataArray]: 

31 if 'output' in kwargs: 

32 raise TypeError("The 'output' argument is not supported") 

33 if x.variances is not None: 

34 raise VariancesError( 

35 "Filter cannot be applied to input array with variances." 

36 ) 

37 if getattr(x, 'masks', None): 

38 raise ValueError("Filter cannot be applied to input array with masks.") 

39 return func(x, **kwargs) 

40 

41 return function 

42 

43 

44def _delta_to_positional(x: Union[Variable, DataArray], dim, index, dtype): 

45 if not isinstance(index, Variable): 

46 return index 

47 coord = x.coords[dim] 

48 if not islinspace(coord, dim).value: 

49 raise CoordError( 

50 f"Data points not regularly spaced along {dim}. To ignore this, " 

51 f"provide a plain value (convertible to {dtype.__name__}) instead of a " 

52 "scalar variable. Note that this will correspond to plain positional " 

53 "indices/offsets." 

54 ) 

55 pos = (len(coord) - 1) * (index.to(unit=coord.unit) / (coord[-1] - coord[0])).value 

56 return dtype(pos) 

57 

58 

59def _require_matching_dims(index, x, name): 

60 if set(index) != set(x.dims): 

61 raise KeyError( 

62 f"Data has dims={x.dims} but input argument '{name}' provides " 

63 f"values for {tuple(index)}" 

64 ) 

65 

66 

67def _positional_index(x: Union[Variable, DataArray], index, name=None, dtype=int): 

68 if not isinstance(index, dict): 

69 return [_delta_to_positional(x, dim, index, dtype=dtype) for dim in x.dims] 

70 _require_matching_dims(index, x, name) 

71 return [_delta_to_positional(x, dim, index[dim], dtype=dtype) for dim in x.dims] 

72 

73 

74@_ndfilter 

75def gaussian_filter( 

76 x: VariableLikeType, 

77 /, 

78 *, 

79 sigma: Union[float, Variable, Dict[str, Union[int, float, Variable]]], 

80 order: Optional[Union[int, Dict[str, int]]] = 0, 

81 **kwargs, 

82) -> VariableLikeType: 

83 """ 

84 Multidimensional Gaussian filter. 

85 

86 This is a wrapper around :py:func:`scipy.ndimage.gaussian_filter`. See there for 

87 full argument description. There are two key differences: 

88 

89 - This wrapper uses explicit dimension labels in the ``sigma`` and ``order`` 

90 arguments. For example, instead of ``sigma=[4, 6]`` use 

91 ``sigma={'time':4, 'space':6}`` 

92 (with appropriate dimension labels for the data). 

93 - Coordinate values can be used (and should be preferred) for ``sigma``. For 

94 example, instead of ``sigma=[4, 6]`` use 

95 ``sigma={'time':sc.scalar(5.0, unit='ms'), 'space':sc.scalar(1.2, unit='mm')}``. 

96 In this case it is required that the corresponding coordinates exist and form a 

97 "linspace", i.e., are evenly spaced. 

98 

99 Warning 

100 ------- 

101 If ``sigma`` is an integer or a mapping to integers then coordinate values are 

102 ignored. That is, the filter is applied even if the data points are not evenly 

103 spaced. The resulting filtered data may thus have no meaningful interpretation. 

104 

105 Parameters 

106 ---------- 

107 x: scipp.typing.VariableLike 

108 Input variable or data array. 

109 sigma: 

110 Standard deviation for Gaussian kernel. The standard deviations of the Gaussian 

111 filter are given as a mapping from dimension labels to numbers or scalar 

112 variables, or as a single number or scalar variable, in which case it is equal 

113 for all axes. 

114 order: 

115 The order of the filter along each dimension, given as mapping from dimension 

116 labels to integers, or as a single integer. An order of 0 corresponds to 

117 convolution with a Gaussian kernel. A positive order corresponds to convolution 

118 with that derivative of a Gaussian. 

119 

120 Returns 

121 ------- 

122 : scipp.typing.VariableLike 

123 Filtered variable or data array 

124 

125 Examples 

126 -------- 

127 

128 .. plot:: :context: close-figs 

129 

130 >>> from scipp.scipy.ndimage import gaussian_filter 

131 >>> da = sc.data.data_xy() 

132 >>> da.plot() 

133 

134 With sigma as integer: 

135 

136 .. plot:: :context: close-figs 

137 

138 >>> filtered = gaussian_filter(da, sigma=4) 

139 >>> filtered.plot() 

140 

141 With sigma based on input coordinate values: 

142 

143 .. plot:: :context: close-figs 

144 

145 >>> filtered = gaussian_filter(da, sigma=sc.scalar(0.1, unit='mm')) 

146 >>> filtered.plot() 

147 

148 With different sigma for different dimensions: 

149 

150 .. plot:: :context: close-figs 

151 

152 >>> filtered = gaussian_filter(da, sigma={'x':sc.scalar(0.01, unit='mm'), 

153 ... 'y':sc.scalar(1.0, unit='mm')}) 

154 >>> filtered.plot() 

155 """ 

156 sigma = _positional_index(x, sigma, name='sigma', dtype=float) 

157 if isinstance(order, dict): 

158 _require_matching_dims(order, x, 'order') 

159 order = [order[dim] for dim in x.dims] 

160 out = empty_like(x) 

161 scipy.ndimage.gaussian_filter( 

162 x.values, sigma=sigma, order=order, output=out.values, **kwargs 

163 ) 

164 return out 

165 

166 

167def _make_footprint(x: Union[Variable, DataArray], size, footprint) -> Variable: 

168 if footprint is None: 

169 size = _positional_index(x, size, name='size') 

170 footprint = ones(dims=x.dims, shape=size, dtype='bool') 

171 else: 

172 if size is not None: 

173 raise ValueError("Provide either 'size' or 'footprint', not both.") 

174 if set(footprint.dims) != set(x.dims): 

175 raise DimensionError( 

176 f"Dimensions {footprint.dims} must match data dimensions {x.dim}" 

177 ) 

178 return footprint 

179 

180 

181def _make_footprint_filter(name, example=True, extra_args=''): 

182 def footprint_filter( 

183 x: VariableLike, 

184 /, 

185 *, 

186 size: Optional[Union[int, Variable, Dict[str, Union[int, Variable]]]] = None, 

187 footprint: Optional[Variable] = None, 

188 origin: Optional[Union[int, Variable, Dict[str, Union[int, Variable]]]] = 0, 

189 **kwargs, 

190 ) -> VariableLike: 

191 footprint = _make_footprint(x, size=size, footprint=footprint) 

192 origin = _positional_index(x, origin, name='origin') 

193 out = empty_like(x) 

194 scipy_filter = getattr(scipy.ndimage, name) 

195 scipy_filter( 

196 x.values, 

197 footprint=footprint.values, 

198 origin=origin, 

199 output=out.values, 

200 **kwargs, 

201 ) 

202 return out 

203 

204 footprint_filter.__name__ = name 

205 if extra_args: 

206 extra_args = f', {extra_args}' 

207 doc = f""" 

208 Calculate a multidimensional {name.replace('_', ' ')}. 

209 

210 This is a wrapper around :py:func:`scipy.ndimage.{name}`. See there for full 

211 argument description. There are two key differences: 

212 

213 - This wrapper uses explicit dimension labels in the ``size``, ``footprint``, and 

214 ``origin`` arguments. For example, instead of ``size=[4, 6]`` use 

215 ``size={{'time':4, 'space':6}}`` (with appropriate dimension labels for the data). 

216 - Coordinate values can be used (and should be preferred) for ``size`` and 

217 ``origin``. For example, instead of ``size=[4, 6]`` use 

218 ``size={{'time':sc.scalar(5.0, unit='ms'), 'space':sc.scalar(1.2, unit='mm')}}``. 

219 In this case it is required that the corresponding coordinates exist and form a 

220 "linspace", i.e., are evenly spaced. 

221 

222 Warning 

223 ------- 

224 When ``size`` is an integer or a mapping to integers or when ``footprint`` is 

225 given, coordinate values are ignored. That is, the filter is applied even if the 

226 data points are not evenly spaced. The resulting filtered data may thus have no 

227 meaningful interpretation. 

228 

229 Parameters 

230 ---------- 

231 x: scipp.typing.VariableLike 

232 Input variable or data array. 

233 size: 

234 Integer or scalar variable or mapping from dimension labels to integers or 

235 scalar variables. Defines the footprint (see below). 

236 footprint: 

237 Variable with same dimension labels (but different shape) as the input data. 

238 The boolean values specify (implicitly) a shape, but also which of the elements 

239 within this shape will get passed to the filter function. 

240 origin: 

241 Integer or scalar variable or mapping from dimension labels to integers or 

242 scalar variables. Controls the placement of the filter on the input array. 

243 

244 Returns 

245 ------- 

246 : scipp.typing.VariableLike 

247 Filtered variable or data array 

248 """ 

249 if example: 

250 doc += f""" 

251 Examples 

252 -------- 

253 

254 .. plot:: :context: close-figs 

255 

256 >>> from scipp.scipy.ndimage import {name} 

257 >>> da = sc.data.data_xy() 

258 >>> da.plot() 

259 

260 With size as integer: 

261 

262 .. plot:: :context: close-figs 

263 

264 >>> filtered = {name}(da, size=4{extra_args}) 

265 >>> filtered.plot() 

266 

267 With size based on input coordinate values: 

268 

269 .. plot:: :context: close-figs 

270 

271 >>> filtered = {name}(da, size=sc.scalar(0.2, unit='mm'){extra_args}) 

272 >>> filtered.plot() 

273 

274 With different size for different dimensions: 

275 

276 .. plot:: :context: close-figs 

277 

278 >>> filtered = {name}(da, size={{'x':sc.scalar(0.2, unit='mm'), 

279 ... {' ' * len(name)}'y':sc.scalar(1.1, unit='mm')}}{extra_args}) 

280 >>> filtered.plot() 

281 """ # noqa: E501 

282 footprint_filter.__doc__ = doc 

283 return _ndfilter(footprint_filter) 

284 

285 

286generic_filter = _make_footprint_filter('generic_filter', example=False) 

287maximum_filter = _make_footprint_filter('maximum_filter') 

288median_filter = _make_footprint_filter('median_filter') 

289minimum_filter = _make_footprint_filter('minimum_filter') 

290percentile_filter = _make_footprint_filter( 

291 'percentile_filter', extra_args='percentile=80' 

292) 

293rank_filter = _make_footprint_filter('rank_filter', extra_args='rank=3') 

294 

295__all__ = [ 

296 'gaussian_filter', 

297 'generic_filter', 

298 'maximum_filter', 

299 'median_filter', 

300 'minimum_filter', 

301 'percentile_filter', 

302 'rank_filter', 

303]