nabu 2023.2.1__py3-none-any.whl → 2024.1.0rc3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. doc/conf.py +1 -1
  2. doc/doc_config.py +32 -0
  3. nabu/__init__.py +2 -1
  4. nabu/app/bootstrap_stitching.py +1 -1
  5. nabu/app/cli_configs.py +122 -2
  6. nabu/app/composite_cor.py +27 -2
  7. nabu/app/correct_rot.py +70 -0
  8. nabu/app/create_distortion_map_from_poly.py +42 -18
  9. nabu/app/diag_to_pix.py +358 -0
  10. nabu/app/diag_to_rot.py +449 -0
  11. nabu/app/generate_header.py +4 -3
  12. nabu/app/histogram.py +2 -2
  13. nabu/app/multicor.py +6 -1
  14. nabu/app/parse_reconstruction_log.py +151 -0
  15. nabu/app/prepare_weights_double.py +83 -22
  16. nabu/app/reconstruct.py +5 -1
  17. nabu/app/reconstruct_helical.py +7 -0
  18. nabu/app/reduce_dark_flat.py +6 -3
  19. nabu/app/rotate.py +4 -4
  20. nabu/app/stitching.py +16 -2
  21. nabu/app/tests/test_reduce_dark_flat.py +18 -2
  22. nabu/app/validator.py +4 -4
  23. nabu/cuda/convolution.py +8 -376
  24. nabu/cuda/fft.py +4 -0
  25. nabu/cuda/kernel.py +4 -4
  26. nabu/cuda/medfilt.py +5 -158
  27. nabu/cuda/padding.py +5 -71
  28. nabu/cuda/processing.py +23 -2
  29. nabu/cuda/src/ElementOp.cu +78 -0
  30. nabu/cuda/src/backproj.cu +28 -2
  31. nabu/cuda/src/fourier_wavelets.cu +2 -2
  32. nabu/cuda/src/normalization.cu +23 -0
  33. nabu/cuda/src/padding.cu +2 -2
  34. nabu/cuda/src/transpose.cu +16 -0
  35. nabu/cuda/utils.py +39 -0
  36. nabu/estimation/alignment.py +10 -1
  37. nabu/estimation/cor.py +808 -38
  38. nabu/estimation/cor_sino.py +7 -9
  39. nabu/estimation/tests/test_cor.py +85 -3
  40. nabu/io/reader.py +26 -18
  41. nabu/io/tests/test_cast_volume.py +3 -3
  42. nabu/io/tests/test_detector_distortion.py +3 -3
  43. nabu/io/tiffwriter_zmm.py +2 -2
  44. nabu/io/utils.py +14 -4
  45. nabu/io/writer.py +5 -3
  46. nabu/misc/fftshift.py +6 -0
  47. nabu/misc/histogram.py +5 -285
  48. nabu/misc/histogram_cuda.py +8 -104
  49. nabu/misc/kernel_base.py +3 -121
  50. nabu/misc/padding_base.py +5 -69
  51. nabu/misc/processing_base.py +3 -107
  52. nabu/misc/rotation.py +5 -62
  53. nabu/misc/rotation_cuda.py +5 -65
  54. nabu/misc/transpose.py +6 -0
  55. nabu/misc/unsharp.py +3 -78
  56. nabu/misc/unsharp_cuda.py +5 -52
  57. nabu/misc/unsharp_opencl.py +8 -85
  58. nabu/opencl/fft.py +6 -0
  59. nabu/opencl/kernel.py +21 -6
  60. nabu/opencl/padding.py +5 -72
  61. nabu/opencl/processing.py +27 -5
  62. nabu/opencl/src/backproj.cl +3 -3
  63. nabu/opencl/src/fftshift.cl +65 -12
  64. nabu/opencl/src/padding.cl +2 -2
  65. nabu/opencl/src/roll.cl +96 -0
  66. nabu/opencl/src/transpose.cl +16 -0
  67. nabu/pipeline/config_validators.py +63 -3
  68. nabu/pipeline/dataset_validator.py +2 -2
  69. nabu/pipeline/estimators.py +193 -35
  70. nabu/pipeline/fullfield/chunked.py +34 -17
  71. nabu/pipeline/fullfield/chunked_cuda.py +7 -5
  72. nabu/pipeline/fullfield/computations.py +48 -13
  73. nabu/pipeline/fullfield/nabu_config.py +13 -13
  74. nabu/pipeline/fullfield/processconfig.py +10 -5
  75. nabu/pipeline/fullfield/reconstruction.py +1 -2
  76. nabu/pipeline/helical/fbp.py +5 -0
  77. nabu/pipeline/helical/filtering.py +12 -9
  78. nabu/pipeline/helical/gridded_accumulator.py +179 -33
  79. nabu/pipeline/helical/helical_chunked_regridded.py +262 -151
  80. nabu/pipeline/helical/helical_chunked_regridded_cuda.py +4 -11
  81. nabu/pipeline/helical/helical_reconstruction.py +56 -18
  82. nabu/pipeline/helical/span_strategy.py +1 -1
  83. nabu/pipeline/helical/tests/test_accumulator.py +4 -0
  84. nabu/pipeline/params.py +23 -2
  85. nabu/pipeline/processconfig.py +3 -8
  86. nabu/pipeline/tests/test_chunk_reader.py +78 -0
  87. nabu/pipeline/tests/test_estimators.py +120 -2
  88. nabu/pipeline/utils.py +25 -0
  89. nabu/pipeline/writer.py +2 -0
  90. nabu/preproc/ccd_cuda.py +9 -7
  91. nabu/preproc/ctf.py +21 -26
  92. nabu/preproc/ctf_cuda.py +25 -25
  93. nabu/preproc/double_flatfield.py +14 -2
  94. nabu/preproc/double_flatfield_cuda.py +7 -11
  95. nabu/preproc/flatfield_cuda.py +23 -27
  96. nabu/preproc/phase.py +19 -24
  97. nabu/preproc/phase_cuda.py +21 -21
  98. nabu/preproc/shift_cuda.py +58 -28
  99. nabu/preproc/tests/test_ctf.py +5 -5
  100. nabu/preproc/tests/test_double_flatfield.py +2 -2
  101. nabu/preproc/tests/test_vshift.py +13 -2
  102. nabu/processing/__init__.py +0 -0
  103. nabu/processing/convolution_cuda.py +375 -0
  104. nabu/processing/fft_base.py +163 -0
  105. nabu/processing/fft_cuda.py +256 -0
  106. nabu/processing/fft_opencl.py +54 -0
  107. nabu/processing/fftshift.py +134 -0
  108. nabu/processing/histogram.py +286 -0
  109. nabu/processing/histogram_cuda.py +103 -0
  110. nabu/processing/kernel_base.py +126 -0
  111. nabu/processing/medfilt_cuda.py +159 -0
  112. nabu/processing/muladd.py +29 -0
  113. nabu/processing/muladd_cuda.py +68 -0
  114. nabu/processing/padding_base.py +71 -0
  115. nabu/processing/padding_cuda.py +75 -0
  116. nabu/processing/padding_opencl.py +77 -0
  117. nabu/processing/processing_base.py +123 -0
  118. nabu/processing/roll_opencl.py +64 -0
  119. nabu/processing/rotation.py +63 -0
  120. nabu/processing/rotation_cuda.py +66 -0
  121. nabu/processing/tests/__init__.py +0 -0
  122. nabu/processing/tests/test_fft.py +268 -0
  123. nabu/processing/tests/test_fftshift.py +71 -0
  124. nabu/{misc → processing}/tests/test_histogram.py +2 -4
  125. nabu/{cuda → processing}/tests/test_medfilt.py +1 -1
  126. nabu/processing/tests/test_muladd.py +54 -0
  127. nabu/{cuda → processing}/tests/test_padding.py +119 -75
  128. nabu/processing/tests/test_roll.py +63 -0
  129. nabu/{misc → processing}/tests/test_rotation.py +3 -2
  130. nabu/processing/tests/test_transpose.py +72 -0
  131. nabu/{misc → processing}/tests/test_unsharp.py +41 -8
  132. nabu/processing/transpose.py +126 -0
  133. nabu/processing/unsharp.py +79 -0
  134. nabu/processing/unsharp_cuda.py +53 -0
  135. nabu/processing/unsharp_opencl.py +75 -0
  136. nabu/reconstruction/fbp.py +34 -10
  137. nabu/reconstruction/fbp_base.py +35 -16
  138. nabu/reconstruction/fbp_opencl.py +7 -12
  139. nabu/reconstruction/filtering.py +2 -2
  140. nabu/reconstruction/filtering_cuda.py +13 -14
  141. nabu/reconstruction/filtering_opencl.py +3 -4
  142. nabu/reconstruction/projection.py +2 -0
  143. nabu/reconstruction/rings.py +158 -1
  144. nabu/reconstruction/rings_cuda.py +218 -58
  145. nabu/reconstruction/sinogram_cuda.py +16 -12
  146. nabu/reconstruction/tests/test_deringer.py +116 -14
  147. nabu/reconstruction/tests/test_fbp.py +22 -31
  148. nabu/reconstruction/tests/test_filtering.py +11 -2
  149. nabu/resources/dataset_analyzer.py +89 -26
  150. nabu/resources/nxflatfield.py +2 -2
  151. nabu/resources/tests/test_nxflatfield.py +1 -1
  152. nabu/resources/utils.py +9 -2
  153. nabu/stitching/alignment.py +184 -0
  154. nabu/stitching/config.py +241 -39
  155. nabu/stitching/definitions.py +6 -0
  156. nabu/stitching/frame_composition.py +4 -2
  157. nabu/stitching/overlap.py +99 -3
  158. nabu/stitching/sample_normalization.py +60 -0
  159. nabu/stitching/slurm_utils.py +10 -10
  160. nabu/stitching/tests/test_alignment.py +99 -0
  161. nabu/stitching/tests/test_config.py +16 -1
  162. nabu/stitching/tests/test_overlap.py +68 -2
  163. nabu/stitching/tests/test_sample_normalization.py +49 -0
  164. nabu/stitching/tests/test_slurm_utils.py +5 -5
  165. nabu/stitching/tests/test_utils.py +3 -33
  166. nabu/stitching/tests/test_z_stitching.py +391 -22
  167. nabu/stitching/utils.py +144 -202
  168. nabu/stitching/z_stitching.py +309 -126
  169. nabu/testutils.py +18 -0
  170. nabu/thirdparty/tomocupy_remove_stripe.py +586 -0
  171. nabu/utils.py +32 -6
  172. {nabu-2023.2.1.dist-info → nabu-2024.1.0rc3.dist-info}/LICENSE +1 -1
  173. {nabu-2023.2.1.dist-info → nabu-2024.1.0rc3.dist-info}/METADATA +5 -5
  174. nabu-2024.1.0rc3.dist-info/RECORD +296 -0
  175. {nabu-2023.2.1.dist-info → nabu-2024.1.0rc3.dist-info}/WHEEL +1 -1
  176. {nabu-2023.2.1.dist-info → nabu-2024.1.0rc3.dist-info}/entry_points.txt +5 -1
  177. nabu/conftest.py +0 -14
  178. nabu/opencl/fftshift.py +0 -92
  179. nabu/opencl/tests/test_fftshift.py +0 -55
  180. nabu/opencl/tests/test_padding.py +0 -84
  181. nabu-2023.2.1.dist-info/RECORD +0 -252
  182. /nabu/cuda/src/{fftshift.cu → dfi_fftshift.cu} +0 -0
  183. {nabu-2023.2.1.dist-info → nabu-2024.1.0rc3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,286 @@
1
+ from math import log2, ceil
2
+ import numpy as np
3
+ from silx.math import Histogramnd
4
+ from tomoscan.io import HDF5File
5
+ from ..utils import check_supported
6
+ from ..resources.logger import LoggerOrPrint
7
+
8
+
9
+ class PartialHistogram:
10
+ """
11
+ A class for computing histogram progressively.
12
+
13
+ In certain cases, it is cumbersome to compute a histogram directly on a big chunk of
14
+ data (ex. data not fitting in memory, disk access too slow) while some parts of the
15
+ data are readily available in-memory.
16
+ """
17
+
18
+ histogram_methods = ["fixed_bins_width", "fixed_bins_number"]
19
+ bin_width_policies = ["uint16"]
20
+ backends = ["numpy", "silx"]
21
+
22
+ def __init__(self, method="fixed_bins_width", bin_width="uint16", num_bins=None, min_bins=None, backend="silx"):
23
+ """
24
+ Initialize a PartialHistogram class.
25
+
26
+ Parameters
27
+ ----------
28
+ method: str, optional
29
+ Partial histogram computing method. Available are:
30
+ - `fixed_bins_width`: all the histograms are computed with the same bin
31
+ width. The class adapts to the data range and computes the number of
32
+ bins accordingly.
33
+ - `fixed_bins_number`: all the histograms are computed with the same
34
+ number of bins. The class adapts to the data range and computes the
35
+ bin width accordingly.
36
+ Default is "fixed_bins_width"
37
+ bin_width: str or float, optional
38
+ Policy for histogram bins when method="fixed_bins_width". Available are:
39
+ - "uint16": The bin width is computed so that floating-point elements
40
+ `f1` and `f2` satisfying `|f1 - f2| < bin_width` implies
41
+ `f1_converted - f2_converted < 1` once cast to uint16.
42
+ - A number: all the bins have this fixed width.
43
+
44
+ Default is "uint16"
45
+ num_bins: int, optional
46
+ Number of bins when method = 'fixed_bins_number'.
47
+ min_bins: int, optional
48
+ Minimum number of bins when method = 'fixed_bins_width'.
49
+ backend: str, optional
50
+ Which histogram backend to use for computations. Available are "silx", "numpy".
51
+ Fastest is "silx".
52
+ """
53
+ check_supported(method, self.histogram_methods, "histogram computing method")
54
+ self.method = method
55
+ check_supported(backend, self.backends, "histogram backend")
56
+ self.backend = backend
57
+ self._set_bin_width(bin_width)
58
+ self._set_num_bins(num_bins)
59
+ self.min_bins = min_bins
60
+ self._set_histogram_methods()
61
+
62
+ def _set_bin_width(self, bin_width):
63
+ if self.method == "fixed_bins_number":
64
+ self.bin_width = None
65
+ return
66
+ if isinstance(bin_width, str):
67
+ check_supported(bin_width, self.bin_width_policies, "bin width policy")
68
+ self._fixed_bw = False
69
+ else:
70
+ bin_width = float(bin_width)
71
+ self._fixed_bw = True
72
+ self.bin_width = bin_width
73
+
74
+ def _set_num_bins(self, num_bins):
75
+ if self.method == "fixed_bins_width":
76
+ self.num_bins = None
77
+ return
78
+ if self.method == "fixed_bins_number" and num_bins is None:
79
+ raise ValueError("Need to specify num_bins for method='fixed_bins_number'")
80
+ self.num_bins = int(num_bins)
81
+
82
+ def _set_histogram_methods(self):
83
+ self._histogram_methods = {
84
+ "fixed_bins_number": {
85
+ "compute": self._compute_histogram_fixed_nbins,
86
+ "merge": self._merge_histograms_fixed_nbins,
87
+ },
88
+ "fixed_bins_width": {
89
+ "compute": self._compute_histogram_fixed_bw,
90
+ "merge": self._merge_histograms_fixed_bw,
91
+ },
92
+ }
93
+ assert set(self._histogram_methods.keys()) == set(self.histogram_methods)
94
+
95
+ @staticmethod
96
+ def _get_histograms_and_bins(histograms, center=False, dont_truncate_bins=False):
97
+ histos = [h[0] for h in histograms]
98
+ if dont_truncate_bins:
99
+ bins = [h[1] for h in histograms]
100
+ else:
101
+ if center:
102
+ bins = [0.5 * (h[1][1:] + h[1][:-1]) for h in histograms]
103
+ else:
104
+ bins = [h[1][:-1] for h in histograms]
105
+ return histos, bins
106
+
107
+ #
108
+ # Histogram with fixed number of bins
109
+ #
110
+
111
+ def _compute_histogram_fixed_nbins(self, data, data_range=None):
112
+ if data.ndim > 1:
113
+ data = data.ravel()
114
+ dmin, dmax = data.min(), data.max() if data_range is None else data_range
115
+ if self.backend == "numpy":
116
+ res = np.histogram(data, bins=self.num_bins)
117
+ elif self.backend == "silx":
118
+ histogrammer = Histogramnd(data, n_bins=self.num_bins, histo_range=(dmin, dmax), last_bin_closed=True)
119
+ res = histogrammer.histo, histogrammer.edges[0] # pylint: disable=E1136
120
+ return res
121
+
122
+ def _merge_histograms_fixed_nbins(self, histograms, dont_truncate_bins=False):
123
+ histos, bins = self._get_histograms_and_bins(histograms, dont_truncate_bins=dont_truncate_bins)
124
+ res = np.histogram(
125
+ np.hstack(bins),
126
+ weights=np.hstack(histos),
127
+ bins=self.num_bins,
128
+ )
129
+ return res
130
+
131
+ #
132
+ # Histogram with fixed bin width
133
+ #
134
+
135
+ def _bin_width_u16(self, dmin, dmax):
136
+ return (dmax - dmin) / 65535.0
137
+
138
+ def _bin_width_fixed(self, dmin, dmax):
139
+ return self.bin_width
140
+
141
+ def get_bin_width(self, dmin, dmax):
142
+ if self._fixed_bw:
143
+ return self._bin_width_fixed(dmin, dmax)
144
+ elif self.bin_width == "uint16":
145
+ return self._bin_width_u16(dmin, dmax)
146
+ else:
147
+ raise ValueError()
148
+
149
+ def _compute_histogram_fixed_bw(self, data, data_range=None):
150
+ dmin, dmax = data.min(), data.max() if data_range is None else data_range
151
+ min_bins = self.min_bins or 1
152
+ bw_max = self.get_bin_width(dmin, dmax)
153
+ nbins = 0
154
+ bw_factor = 1
155
+ while nbins < min_bins:
156
+ bw = 2 ** round(log2(bw_max)) / bw_factor
157
+ nbins = int((dmax - dmin) / bw)
158
+ bw_factor *= 2
159
+ res = np.histogram(data, bins=nbins)
160
+ return res
161
+
162
+ def _merge_histograms_fixed_bw(self, histograms, **kwargs):
163
+ histos, bins = self._get_histograms_and_bins(histograms, center=False)
164
+ dmax = max([b[-1] for b in bins])
165
+ dmin = min([b[0] for b in bins])
166
+ bw_max = max([b[1] - b[0] for b in bins])
167
+ res = np.histogram(np.hstack(bins), weights=np.hstack(histos), bins=int((dmax - dmin) / bw_max))
168
+ return res
169
+
170
+ #
171
+ # Dispatch methods
172
+ #
173
+
174
+ def compute_histogram(self, data, data_range=None):
175
+ compute_hist_func = self._histogram_methods[self.method]["compute"]
176
+ return compute_hist_func(data, data_range=data_range)
177
+
178
+ def merge_histograms(self, histograms, **kwargs):
179
+ merge_hist_func = self._histogram_methods[self.method]["merge"]
180
+ return merge_hist_func(histograms, **kwargs)
181
+
182
+
183
+ class VolumeHistogram:
184
+ """
185
+ A class for computing the histogram of an entire volume.
186
+ Unless explicitly specified, histogram is computed in several passes so that not
187
+ all the volume is loaded in memory.
188
+ """
189
+
190
+ def __init__(self, data_url, chunk_size_slices=100, chunk_size_GB=None, nbins=1e6, logger=None):
191
+ """
192
+ Initialize a VolumeHistogram object.
193
+
194
+ Parameters
195
+ ----------
196
+ fname: DataUrl
197
+ DataUrl to the HDF5 file.
198
+ chunk_size_slices: int, optional
199
+ Compute partial histograms of groups of slices. This is the default behavior,
200
+ where the groups size is 100 slices.
201
+ This parameter is mutually exclusive with 'chunk_size_GB'.
202
+ chunk_size_GB: float, optional
203
+ Maximum memory (in GB) to use when computing the histogram by group of slices.
204
+ This parameter is mutually exclusive with 'chunk_size_slices'.
205
+ nbins: int, optional
206
+ Histogram number of bins. Default is 1e6.
207
+ """
208
+ self.data_url = data_url
209
+ self.logger = LoggerOrPrint(logger)
210
+ self._get_data_info()
211
+ self._set_chunk_size(chunk_size_slices, chunk_size_GB)
212
+ self.nbins = int(nbins)
213
+ self._init_histogrammer()
214
+
215
+ def _get_data_info(self):
216
+ self.fname = self.data_url.file_path()
217
+ self.data_path = self.data_url.data_path()
218
+ with HDF5File(self.fname, "r") as fid:
219
+ try:
220
+ data_ptr = fid[self.data_path]
221
+ except KeyError:
222
+ msg = str(
223
+ "Could not access HDF5 path %s in file %s. Please check that this file \
224
+ actually contains a reconstruction and that the HDF5 path is correct"
225
+ % (self.data_path, self.fname)
226
+ )
227
+ self.logger.fatal(msg)
228
+ raise ValueError(msg)
229
+ if data_ptr.ndim != 3:
230
+ msg = "Expected data to have 3 dimensions, got %d" % data_ptr.ndim
231
+ raise ValueError(msg)
232
+ self.data_shape = data_ptr.shape
233
+ self.data_dtype = data_ptr.dtype
234
+ self.data_nbytes_GB = np.prod(data_ptr.shape) * data_ptr.dtype.itemsize / 1e9
235
+
236
+ def _set_chunk_size(self, chunk_size_slices, chunk_size_GB):
237
+ if not ((chunk_size_slices is not None) ^ (chunk_size_GB is not None)):
238
+ raise ValueError("Please specify either chunk_size_slices or chunk_size_GB")
239
+ if chunk_size_slices is None:
240
+ chunk_size_slices = int(chunk_size_GB / (np.prod(self.data_shape[1:]) * self.data_dtype.itemsize / 1e9))
241
+ self.chunk_size = chunk_size_slices
242
+ self.logger.debug("Computing histograms by groups of %d slices" % self.chunk_size)
243
+
244
+ def _init_histogrammer(self):
245
+ self.histogrammer = PartialHistogram(method="fixed_bins_number", num_bins=self.nbins)
246
+
247
+ def _compute_histogram(self, data):
248
+ return self.histogrammer.compute_histogram(data.ravel()) # 1D
249
+
250
+ def compute_volume_histogram(self):
251
+ n_z = self.data_shape[0]
252
+ histograms = []
253
+ n_steps = ceil(n_z / self.chunk_size)
254
+ with HDF5File(self.fname, "r") as fid:
255
+ for chunk_id in range(n_steps):
256
+ self.logger.debug("Computing histogram %d/%d" % (chunk_id + 1, n_steps))
257
+ z_slice = slice(chunk_id * self.chunk_size, (chunk_id + 1) * self.chunk_size)
258
+ images_stack = fid[self.data_path][z_slice, :, :]
259
+ hist = self._compute_histogram(images_stack)
260
+ histograms.append(hist)
261
+ res = self.histogrammer.merge_histograms(histograms)
262
+ return res
263
+
264
+
265
+ def hist_as_2Darray(hist, center=True, dtype="f"):
266
+ hist, bins = hist
267
+ if bins.size != hist.size:
268
+ # assert bins.size == hist.size +1
269
+ if center:
270
+ bins = 0.5 * (bins[1:] + bins[:-1])
271
+ else:
272
+ bins = bins[:-1]
273
+ res = np.zeros((2, hist.size), dtype=dtype)
274
+ res[0] = hist
275
+ res[1] = bins.astype(dtype)
276
+ return res
277
+
278
+
279
+ def add_last_bin(histo_bins):
280
+ """
281
+ Add the last bin (max value) to a list of bin edges.
282
+ """
283
+ res = np.zeros(histo_bins.size + 1, dtype=histo_bins.dtype)
284
+ res[:-1] = histo_bins[:]
285
+ res[-1] = res[-2] + (res[1] - res[0])
286
+ return res
@@ -0,0 +1,103 @@
1
+ import numpy as np
2
+ from ..utils import get_cuda_srcfile, updiv
3
+ from ..cuda.utils import __has_pycuda__
4
+ from .histogram import PartialHistogram, VolumeHistogram
5
+
6
+ if __has_pycuda__:
7
+ import pycuda.gpuarray as garray
8
+ from ..cuda.processing import CudaProcessing
9
+
10
+
11
+ class CudaPartialHistogram(PartialHistogram):
12
+ def __init__(
13
+ self,
14
+ method="fixed_bins_number",
15
+ bin_width="uint16",
16
+ num_bins=None,
17
+ min_bins=None,
18
+ cuda_options=None,
19
+ ):
20
+ if method == "fixed_bins_width":
21
+ raise NotImplementedError("Histogram with fixed bins width is not implemented with the Cuda backend")
22
+ super().__init__(
23
+ method=method,
24
+ bin_width=bin_width,
25
+ num_bins=num_bins,
26
+ min_bins=min_bins,
27
+ )
28
+ self.cuda_processing = CudaProcessing(**(cuda_options or {}))
29
+ self._init_cuda_histogram()
30
+
31
+ def _init_cuda_histogram(self):
32
+ self.cuda_hist = self.cuda_processing.kernel(
33
+ "histogram",
34
+ filename=get_cuda_srcfile("histogram.cu"),
35
+ signature="PiiiffPi",
36
+ )
37
+ self.d_hist = self.cuda_processing.allocate_array("d_hist", self.num_bins, dtype=np.uint32)
38
+
39
+ def _compute_histogram_fixed_nbins(self, data, data_range=None):
40
+ if isinstance(data, np.ndarray):
41
+ data = self.cuda_processing.to_device("data", data)
42
+ if data_range is None:
43
+ # Should be possible to do both in one single pass with ReductionKernel
44
+ # and garray.vec.float2, but the last step in volatile shared memory
45
+ # still gives errors. To be investigated...
46
+ data_min = garray.min(data).get()[()]
47
+ data_max = garray.max(data).get()[()]
48
+ else:
49
+ data_min, data_max = data_range
50
+ Nz, Ny, Nx = data.shape
51
+ block = (16, 16, 4)
52
+ grid = (
53
+ updiv(Nx, block[0]),
54
+ updiv(Ny, block[1]),
55
+ updiv(Nz, block[2]),
56
+ )
57
+ self.d_hist.fill(0)
58
+ self.cuda_hist(
59
+ data,
60
+ Nx,
61
+ Ny,
62
+ Nz,
63
+ data_min,
64
+ data_max,
65
+ self.d_hist,
66
+ self.num_bins,
67
+ grid=grid,
68
+ block=block,
69
+ )
70
+ # Return a result in the same format as numpy.histogram
71
+ res_hist = self.d_hist.get()
72
+ res_bins = np.linspace(data_min, data_max, num=self.num_bins + 1, endpoint=True)
73
+ return res_hist, res_bins
74
+
75
+
76
+ class CudaVolumeHistogram(VolumeHistogram):
77
+ def __init__(
78
+ self,
79
+ data_url,
80
+ chunk_size_slices=100,
81
+ chunk_size_GB=None,
82
+ nbins=1e6,
83
+ logger=None,
84
+ cuda_options=None,
85
+ ):
86
+ self.cuda_options = cuda_options
87
+ super().__init__(
88
+ data_url,
89
+ chunk_size_slices=chunk_size_slices,
90
+ chunk_size_GB=chunk_size_GB,
91
+ nbins=nbins,
92
+ logger=logger,
93
+ )
94
+
95
+ def _init_histogrammer(self):
96
+ self.histogrammer = CudaPartialHistogram(
97
+ method="fixed_bins_number",
98
+ num_bins=self.nbins,
99
+ cuda_options=self.cuda_options,
100
+ )
101
+
102
+ def _compute_histogram(self, data):
103
+ return self.histogrammer.compute_histogram(data) # 3D
@@ -0,0 +1,126 @@
1
+ """
2
+ Base class for CudaKernel and OpenCLKernel
3
+ Should not be used directly
4
+ """
5
+ from ..utils import updiv
6
+
7
+
8
+ class KernelBase:
9
+ """
10
+ A base class for OpenCL and Cuda kernels.
11
+
12
+ Parameters
13
+ -----------
14
+ kernel_name: str
15
+ Name of the CUDA kernel.
16
+ filename: str, optional
17
+ Path to the file name containing kernels definitions
18
+ src: str, optional
19
+ Source code of kernels definitions
20
+ automation_params: dict, optional
21
+ Automation parameters, see below
22
+
23
+ Automation parameters
24
+ ----------------------
25
+ automation_params is a dictionary with the following keys and default values.
26
+ guess_block: bool (True)
27
+ If block is not specified during calls, choose a block size based on
28
+ the size/dimensions of the first array.
29
+ Mind that it is unlikely to be the optimal choice.
30
+ guess_grid: bool (True):
31
+ If the grid size is not specified during calls, choose a grid size
32
+ based on the size of the first array.
33
+ follow_device_ptr: bool (True)
34
+ specify gpuarray.gpudata for all cuda GPUArrays (and pyopencl.array.data for pyopencl arrays).
35
+ Otherwise, raise an error.
36
+ """
37
+
38
+ _default_automation_params = {
39
+ "guess_block": True,
40
+ "guess_grid": True,
41
+ "follow_device_ptr": True,
42
+ }
43
+
44
+ def __init__(
45
+ self,
46
+ kernel_name,
47
+ filename=None,
48
+ src=None,
49
+ automation_params=None,
50
+ ):
51
+ self.check_filename_src(filename, src)
52
+ self.set_automation_params(automation_params)
53
+
54
+ def check_filename_src(self, filename, src):
55
+ err_msg = "Please provide either filename or src"
56
+ if filename is None and src is None:
57
+ raise ValueError(err_msg)
58
+ if filename is not None and src is not None:
59
+ raise ValueError(err_msg)
60
+ if filename is not None:
61
+ with open(filename) as fid:
62
+ src = fid.read()
63
+ self.filename = filename
64
+ self.src = src
65
+
66
+ def set_automation_params(self, automation_params):
67
+ self.automation_params = self._default_automation_params.copy()
68
+ self.automation_params.update(automation_params or {})
69
+
70
+ @staticmethod
71
+ def guess_grid_size(shape, block_size):
72
+ # python: (z, y, x) -> cuda: (x, y, z)
73
+ res = tuple(map(lambda x: updiv(x[0], x[1]), zip(shape[::-1], block_size)))
74
+ if len(res) == 2:
75
+ res += (1,)
76
+ return res
77
+
78
+ @staticmethod
79
+ def guess_block_size(shape):
80
+ """
81
+ Guess a block size based on the shape of an array.
82
+ """
83
+ ndim = len(shape)
84
+ if ndim == 1:
85
+ return (128, 1, 1)
86
+ if ndim == 2:
87
+ return (32, 32, 1)
88
+ else:
89
+ return (16, 8, 8)
90
+
91
+ def get_block_grid(self, *args, **kwargs):
92
+ block = None
93
+ grid = None
94
+ if ("block" not in kwargs) or (kwargs["block"] is None):
95
+ if self.automation_params["guess_block"]:
96
+ block = self.guess_block_size(args[0].shape)
97
+ else:
98
+ raise ValueError("Please provide block size")
99
+ else:
100
+ block = kwargs["block"]
101
+ if ("grid" not in kwargs) or (kwargs["grid"] is None):
102
+ if self.automation_params["guess_grid"]:
103
+ grid = self.guess_grid_size(args[0].shape, block)
104
+ else:
105
+ raise ValueError("Please provide block grid")
106
+ else:
107
+ grid = kwargs["grid"]
108
+ self.last_block_size = block
109
+ self.last_grid_size = grid
110
+ return block, grid
111
+
112
+ def follow_device_arr(self, args):
113
+ raise ValueError("Base class")
114
+
115
+ def _prepare_call(self, *args, **kwargs):
116
+ block, grid = self.get_block_grid(*args, **kwargs)
117
+ # pycuda crashes when any element of block/grid is not a python int (ex. numpy.int64).
118
+ # A weird behavior once observed is "data.shape" returning (np.int64, int, int) (!).
119
+ # Ensure that everything is a python integer.
120
+ grid = tuple(int(x) for x in grid)
121
+ if block is not None:
122
+ block = tuple(int(x) for x in block)
123
+ #
124
+ args = self.follow_device_arr(args)
125
+
126
+ return grid, block, args, kwargs
@@ -0,0 +1,159 @@
1
+ from os.path import dirname
2
+ import numpy as np
3
+ from pycuda.compiler import SourceModule
4
+ from ..utils import updiv, get_cuda_srcfile
5
+ from ..cuda.processing import CudaProcessing
6
+
7
+
8
+ class MedianFilter:
9
+ """
10
+ A class for performing median filter on GPU with CUDA
11
+ """
12
+
13
+ def __init__(
14
+ self,
15
+ shape,
16
+ footprint=(3, 3),
17
+ mode="reflect",
18
+ threshold=None,
19
+ cuda_options=None,
20
+ abs_diff=False,
21
+ ):
22
+ """Constructor of Cuda Median Filter.
23
+
24
+ Parameters
25
+ ----------
26
+ shape: tuple
27
+ Shape of the array, in the format (n_rows, n_columns)
28
+ footprint: tuple
29
+ Size of the median filter, in the format (y, x).
30
+ mode: str
31
+ Boundary handling mode. Available modes are:
32
+ - "reflect": cba|abcd|dcb
33
+ - "nearest": aaa|abcd|ddd
34
+ - "wrap": bcd|abcd|abc
35
+ - "constant": 000|abcd|000
36
+
37
+ Default is "reflect".
38
+ threshold: float, optional
39
+ Threshold for the "thresholded median filter".
40
+ A thresholded median filter only replaces a pixel value by the median
41
+ if this pixel value is greater or equal than median + threshold.
42
+ abs_diff: bool, optional
43
+ Whether to perform conditional threshold as abs(value - median)
44
+
45
+ Notes
46
+ ------
47
+ Please refer to the documentation of the CudaProcessing class for
48
+ the other parameters.
49
+ """
50
+ self.cuda_processing = CudaProcessing(**(cuda_options or {}))
51
+ self._set_params(shape, footprint, mode, threshold, abs_diff)
52
+ self.cuda_processing.init_arrays_to_none(["d_input", "d_output"])
53
+ self._init_kernels()
54
+
55
+ def _set_params(self, shape, footprint, mode, threshold, abs_diff):
56
+ self.data_ndim = len(shape)
57
+ if self.data_ndim == 2:
58
+ ny, nx = shape
59
+ nz = 1
60
+ elif self.data_ndim == 3:
61
+ nz, ny, nx = shape
62
+ else:
63
+ raise ValueError("Expected 2D or 3D data")
64
+ self.shape = shape
65
+ self.Nx = np.int32(nx)
66
+ self.Ny = np.int32(ny)
67
+ self.Nz = np.int32(nz)
68
+ if len(footprint) != 2:
69
+ raise ValueError("3D median filter is not implemented yet")
70
+ if not ((footprint[0] & 1) and (footprint[1] & 1)):
71
+ raise ValueError("Must have odd-sized footprint")
72
+ self.footprint = footprint
73
+ self._set_boundary_mode(mode)
74
+ self.do_threshold = False
75
+ self.abs_diff = abs_diff
76
+ if threshold is not None:
77
+ self.threshold = np.float32(threshold)
78
+ self.do_threshold = True
79
+ else:
80
+ self.threshold = np.float32(0)
81
+
82
+ def _set_boundary_mode(self, mode):
83
+ self.mode = mode
84
+ # Some code duplication from convolution
85
+ self._c_modes_mapping = {
86
+ "periodic": 2,
87
+ "wrap": 2,
88
+ "nearest": 1,
89
+ "replicate": 1,
90
+ "reflect": 0,
91
+ "constant": 3,
92
+ }
93
+ mp = self._c_modes_mapping
94
+ if self.mode.lower() not in mp:
95
+ raise ValueError(
96
+ """
97
+ Mode %s is not available. Available modes are:
98
+ %s
99
+ """
100
+ % (self.mode, str(mp.keys()))
101
+ )
102
+ if self.mode.lower() == "constant":
103
+ raise NotImplementedError("mode='constant' is not implemented yet")
104
+ self._c_conv_mode = mp[self.mode]
105
+
106
+ def _init_kernels(self):
107
+ # Compile source module
108
+ compile_options = [
109
+ "-DUSED_CONV_MODE=%d" % self._c_conv_mode,
110
+ "-DMEDFILT_X=%d" % self.footprint[1],
111
+ "-DMEDFILT_Y=%d" % self.footprint[0],
112
+ "-DDO_THRESHOLD=%d" % (int(self.do_threshold) + int(self.abs_diff)),
113
+ ]
114
+ fname = get_cuda_srcfile("medfilt.cu")
115
+ nabu_cuda_dir = dirname(fname)
116
+ include_dirs = [nabu_cuda_dir]
117
+ self.sourcemodule_kwargs = {}
118
+ self.sourcemodule_kwargs["options"] = compile_options
119
+ self.sourcemodule_kwargs["include_dirs"] = include_dirs
120
+ with open(fname) as fid:
121
+ cuda_src = fid.read()
122
+ self._module = SourceModule(cuda_src, **self.sourcemodule_kwargs)
123
+ self.cuda_kernel_2d = self._module.get_function("medfilt2d")
124
+ # Blocks, grid
125
+ self._block_size = {2: (32, 32, 1), 3: (16, 8, 8)}[self.data_ndim] # TODO tune
126
+ self._n_blocks = tuple([updiv(a, b) for a, b in zip(self.shape[::-1], self._block_size)])
127
+
128
+ def medfilt2(self, image, output=None):
129
+ """
130
+ Perform a median filter on an image (or batch of images).
131
+
132
+ Parameters
133
+ -----------
134
+ images: numpy.ndarray or pycuda.gpuarray
135
+ 2D image or 3D stack of 2D images
136
+ output: numpy.ndarray or pycuda.gpuarray, optional
137
+ Output of filtering. If provided, it must have the same shape
138
+ as the input array.
139
+ """
140
+ self.cuda_processing.set_array("d_input", image)
141
+ if output is not None:
142
+ self.cuda_processing.set_array("d_output", output)
143
+ else:
144
+ self.cuda_processing.allocate_array("d_output", self.shape)
145
+ self.cuda_kernel_2d(
146
+ self.cuda_processing.d_input,
147
+ self.cuda_processing.d_output,
148
+ self.Nx,
149
+ self.Ny,
150
+ self.Nz,
151
+ self.threshold,
152
+ grid=self._n_blocks,
153
+ block=self._block_size,
154
+ )
155
+ self.cuda_processing.recover_arrays_references(["d_input", "d_output"])
156
+ if output is None:
157
+ return self.cuda_processing.d_output.get()
158
+ else:
159
+ return output