nabu 2024.2.14__py3-none-any.whl → 2025.1.0__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 (197) hide show
  1. doc/doc_config.py +32 -0
  2. nabu/__init__.py +1 -1
  3. nabu/app/bootstrap_stitching.py +4 -2
  4. nabu/app/cast_volume.py +16 -14
  5. nabu/app/cli_configs.py +102 -9
  6. nabu/app/compare_volumes.py +1 -1
  7. nabu/app/composite_cor.py +2 -4
  8. nabu/app/diag_to_pix.py +5 -6
  9. nabu/app/diag_to_rot.py +10 -11
  10. nabu/app/double_flatfield.py +18 -5
  11. nabu/app/estimate_motion.py +75 -0
  12. nabu/app/multicor.py +28 -15
  13. nabu/app/parse_reconstruction_log.py +1 -0
  14. nabu/app/pcaflats.py +122 -0
  15. nabu/app/prepare_weights_double.py +1 -2
  16. nabu/app/reconstruct.py +1 -7
  17. nabu/app/reconstruct_helical.py +5 -9
  18. nabu/app/reduce_dark_flat.py +5 -4
  19. nabu/app/rotate.py +3 -1
  20. nabu/app/stitching.py +7 -2
  21. nabu/app/tests/test_reduce_dark_flat.py +2 -2
  22. nabu/app/validator.py +1 -4
  23. nabu/cuda/convolution.py +1 -1
  24. nabu/cuda/fft.py +1 -1
  25. nabu/cuda/medfilt.py +1 -1
  26. nabu/cuda/padding.py +1 -1
  27. nabu/cuda/src/backproj.cu +6 -6
  28. nabu/cuda/src/cone.cu +4 -0
  29. nabu/cuda/src/hierarchical_backproj.cu +14 -0
  30. nabu/cuda/utils.py +2 -2
  31. nabu/estimation/alignment.py +17 -31
  32. nabu/estimation/cor.py +27 -33
  33. nabu/estimation/cor_sino.py +2 -8
  34. nabu/estimation/focus.py +4 -8
  35. nabu/estimation/motion.py +557 -0
  36. nabu/estimation/tests/test_alignment.py +2 -0
  37. nabu/estimation/tests/test_motion_estimation.py +471 -0
  38. nabu/estimation/tests/test_tilt.py +1 -1
  39. nabu/estimation/tilt.py +6 -5
  40. nabu/estimation/translation.py +47 -1
  41. nabu/io/cast_volume.py +108 -18
  42. nabu/io/detector_distortion.py +5 -6
  43. nabu/io/reader.py +45 -6
  44. nabu/io/reader_helical.py +5 -4
  45. nabu/io/tests/test_cast_volume.py +2 -2
  46. nabu/io/tests/test_readers.py +41 -38
  47. nabu/io/tests/test_remove_volume.py +152 -0
  48. nabu/io/tests/test_writers.py +2 -2
  49. nabu/io/utils.py +8 -4
  50. nabu/io/writer.py +1 -2
  51. nabu/misc/fftshift.py +1 -1
  52. nabu/misc/fourier_filters.py +1 -1
  53. nabu/misc/histogram.py +1 -1
  54. nabu/misc/histogram_cuda.py +1 -1
  55. nabu/misc/padding_base.py +1 -1
  56. nabu/misc/rotation.py +1 -1
  57. nabu/misc/rotation_cuda.py +1 -1
  58. nabu/misc/tests/test_binning.py +1 -1
  59. nabu/misc/transpose.py +1 -1
  60. nabu/misc/unsharp.py +1 -1
  61. nabu/misc/unsharp_cuda.py +1 -1
  62. nabu/misc/unsharp_opencl.py +1 -1
  63. nabu/misc/utils.py +1 -1
  64. nabu/opencl/fft.py +1 -1
  65. nabu/opencl/padding.py +1 -1
  66. nabu/opencl/src/backproj.cl +6 -6
  67. nabu/opencl/utils.py +8 -8
  68. nabu/pipeline/config.py +2 -2
  69. nabu/pipeline/config_validators.py +46 -46
  70. nabu/pipeline/datadump.py +3 -3
  71. nabu/pipeline/estimators.py +271 -11
  72. nabu/pipeline/fullfield/chunked.py +103 -67
  73. nabu/pipeline/fullfield/chunked_cuda.py +5 -2
  74. nabu/pipeline/fullfield/computations.py +4 -1
  75. nabu/pipeline/fullfield/dataset_validator.py +0 -1
  76. nabu/pipeline/fullfield/get_double_flatfield.py +147 -0
  77. nabu/pipeline/fullfield/nabu_config.py +36 -17
  78. nabu/pipeline/fullfield/processconfig.py +41 -7
  79. nabu/pipeline/fullfield/reconstruction.py +14 -10
  80. nabu/pipeline/helical/dataset_validator.py +3 -4
  81. nabu/pipeline/helical/fbp.py +4 -4
  82. nabu/pipeline/helical/filtering.py +5 -4
  83. nabu/pipeline/helical/gridded_accumulator.py +10 -11
  84. nabu/pipeline/helical/helical_chunked_regridded.py +1 -0
  85. nabu/pipeline/helical/helical_reconstruction.py +12 -9
  86. nabu/pipeline/helical/helical_utils.py +1 -2
  87. nabu/pipeline/helical/nabu_config.py +2 -1
  88. nabu/pipeline/helical/span_strategy.py +1 -0
  89. nabu/pipeline/helical/weight_balancer.py +2 -3
  90. nabu/pipeline/params.py +20 -3
  91. nabu/pipeline/tests/__init__.py +0 -0
  92. nabu/pipeline/tests/test_estimators.py +240 -3
  93. nabu/pipeline/utils.py +1 -1
  94. nabu/pipeline/writer.py +1 -1
  95. nabu/preproc/alignment.py +0 -10
  96. nabu/preproc/ccd.py +53 -3
  97. nabu/preproc/ctf.py +8 -8
  98. nabu/preproc/ctf_cuda.py +1 -1
  99. nabu/preproc/double_flatfield_cuda.py +2 -2
  100. nabu/preproc/double_flatfield_variable_region.py +0 -1
  101. nabu/preproc/flatfield.py +307 -2
  102. nabu/preproc/flatfield_cuda.py +1 -2
  103. nabu/preproc/flatfield_variable_region.py +3 -3
  104. nabu/preproc/phase.py +2 -4
  105. nabu/preproc/phase_cuda.py +2 -2
  106. nabu/preproc/shift.py +4 -2
  107. nabu/preproc/shift_cuda.py +0 -1
  108. nabu/preproc/tests/test_ctf.py +4 -4
  109. nabu/preproc/tests/test_double_flatfield.py +1 -1
  110. nabu/preproc/tests/test_flatfield.py +1 -1
  111. nabu/preproc/tests/test_paganin.py +1 -3
  112. nabu/preproc/tests/test_pcaflats.py +154 -0
  113. nabu/preproc/tests/test_vshift.py +4 -1
  114. nabu/processing/azim.py +9 -5
  115. nabu/processing/convolution_cuda.py +6 -4
  116. nabu/processing/fft_base.py +7 -3
  117. nabu/processing/fft_cuda.py +25 -164
  118. nabu/processing/fft_opencl.py +28 -6
  119. nabu/processing/fftshift.py +1 -1
  120. nabu/processing/histogram.py +1 -1
  121. nabu/processing/muladd.py +0 -1
  122. nabu/processing/padding_base.py +1 -1
  123. nabu/processing/padding_cuda.py +0 -2
  124. nabu/processing/processing_base.py +12 -6
  125. nabu/processing/rotation_cuda.py +3 -1
  126. nabu/processing/tests/test_fft.py +2 -64
  127. nabu/processing/tests/test_fftshift.py +1 -1
  128. nabu/processing/tests/test_medfilt.py +1 -3
  129. nabu/processing/tests/test_padding.py +1 -1
  130. nabu/processing/tests/test_roll.py +1 -1
  131. nabu/processing/tests/test_rotation.py +4 -2
  132. nabu/processing/unsharp_opencl.py +1 -1
  133. nabu/reconstruction/astra.py +245 -0
  134. nabu/reconstruction/cone.py +39 -9
  135. nabu/reconstruction/fbp.py +7 -0
  136. nabu/reconstruction/fbp_base.py +36 -5
  137. nabu/reconstruction/filtering.py +59 -25
  138. nabu/reconstruction/filtering_cuda.py +22 -21
  139. nabu/reconstruction/filtering_opencl.py +10 -14
  140. nabu/reconstruction/hbp.py +26 -13
  141. nabu/reconstruction/mlem.py +55 -16
  142. nabu/reconstruction/projection.py +3 -5
  143. nabu/reconstruction/sinogram.py +1 -1
  144. nabu/reconstruction/sinogram_cuda.py +0 -1
  145. nabu/reconstruction/tests/test_cone.py +37 -2
  146. nabu/reconstruction/tests/test_deringer.py +4 -4
  147. nabu/reconstruction/tests/test_fbp.py +36 -15
  148. nabu/reconstruction/tests/test_filtering.py +27 -7
  149. nabu/reconstruction/tests/test_halftomo.py +28 -2
  150. nabu/reconstruction/tests/test_mlem.py +94 -64
  151. nabu/reconstruction/tests/test_projector.py +7 -2
  152. nabu/reconstruction/tests/test_reconstructor.py +1 -1
  153. nabu/reconstruction/tests/test_sino_normalization.py +0 -1
  154. nabu/resources/dataset_analyzer.py +210 -24
  155. nabu/resources/gpu.py +4 -4
  156. nabu/resources/logger.py +4 -4
  157. nabu/resources/nxflatfield.py +103 -37
  158. nabu/resources/tests/test_dataset_analyzer.py +37 -0
  159. nabu/resources/tests/test_extract.py +11 -0
  160. nabu/resources/tests/test_nxflatfield.py +5 -5
  161. nabu/resources/utils.py +16 -10
  162. nabu/stitching/alignment.py +8 -11
  163. nabu/stitching/config.py +44 -35
  164. nabu/stitching/definitions.py +2 -2
  165. nabu/stitching/frame_composition.py +8 -10
  166. nabu/stitching/overlap.py +4 -4
  167. nabu/stitching/sample_normalization.py +5 -5
  168. nabu/stitching/slurm_utils.py +2 -2
  169. nabu/stitching/stitcher/base.py +2 -0
  170. nabu/stitching/stitcher/dumper/base.py +0 -1
  171. nabu/stitching/stitcher/dumper/postprocessing.py +1 -1
  172. nabu/stitching/stitcher/post_processing.py +11 -9
  173. nabu/stitching/stitcher/pre_processing.py +37 -31
  174. nabu/stitching/stitcher/single_axis.py +2 -3
  175. nabu/stitching/stitcher_2D.py +2 -1
  176. nabu/stitching/tests/test_config.py +10 -11
  177. nabu/stitching/tests/test_sample_normalization.py +1 -1
  178. nabu/stitching/tests/test_slurm_utils.py +1 -2
  179. nabu/stitching/tests/test_y_preprocessing_stitching.py +11 -8
  180. nabu/stitching/tests/test_z_postprocessing_stitching.py +3 -3
  181. nabu/stitching/tests/test_z_preprocessing_stitching.py +27 -24
  182. nabu/stitching/utils/tests/__init__.py +0 -0
  183. nabu/stitching/utils/tests/test_post-processing.py +1 -0
  184. nabu/stitching/utils/utils.py +16 -18
  185. nabu/tests.py +0 -3
  186. nabu/testutils.py +62 -9
  187. nabu/utils.py +50 -20
  188. {nabu-2024.2.14.dist-info → nabu-2025.1.0.dist-info}/METADATA +7 -7
  189. nabu-2025.1.0.dist-info/RECORD +328 -0
  190. {nabu-2024.2.14.dist-info → nabu-2025.1.0.dist-info}/WHEEL +1 -1
  191. {nabu-2024.2.14.dist-info → nabu-2025.1.0.dist-info}/entry_points.txt +2 -1
  192. nabu/app/correct_rot.py +0 -70
  193. nabu/io/tests/test_detector_distortion.py +0 -178
  194. nabu-2024.2.14.dist-info/RECORD +0 -317
  195. /nabu/{stitching → app}/tests/__init__.py +0 -0
  196. {nabu-2024.2.14.dist-info → nabu-2025.1.0.dist-info}/licenses/LICENSE +0 -0
  197. {nabu-2024.2.14.dist-info → nabu-2025.1.0.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
1
+ import warnings
1
2
  import numpy as np
2
3
  from ..utils import updiv, nextpow2, convert_index, deprecation_warning
3
4
  from ..processing.processing_base import ProcessingBase
@@ -6,6 +7,19 @@ from .sinogram import SinoMult
6
7
  from .sinogram import get_extended_sinogram_width
7
8
 
8
9
 
10
+ def rot_center_is_in_middle_of_roi(rot_center, roi, tol=2.0):
11
+ # NB. tolerance should be at least 2,
12
+ # because in halftomo the extended sinogram width is 2*sino_width - int(2 * XXXX)
13
+ # (where XXX depends on whether the CoR is on the left or on the right)
14
+ # because of the int(2 * stuff), we can have a jump of at most two pixels.
15
+ #
16
+ start_x, end_x, start_y, end_y = roi
17
+ return (
18
+ abs((start_x + end_x - 1) / 2 - rot_center) - 0.5 < tol
19
+ and abs((start_y + end_y - 1) / 2 - rot_center) - 0.5 < tol
20
+ )
21
+
22
+
9
23
  class BackprojectorBase:
10
24
  """
11
25
  Base class for backprojectors.
@@ -22,6 +36,7 @@ class BackprojectorBase:
22
36
  "scale_factor": None,
23
37
  "filter_cutoff": 1.0,
24
38
  "outer_circle_value": 0.0,
39
+ "crop_filtered_data": True,
25
40
  }
26
41
 
27
42
  kernel_filename = None
@@ -82,7 +97,7 @@ class BackprojectorBase:
82
97
  backend_options: dict, optional
83
98
  OpenCL/Cuda options passed to the OpenCLProcessing or CudaProcessing class.
84
99
 
85
- Other parameters
100
+ Other Parameters
86
101
  -----------------
87
102
  extra_options: dict, optional
88
103
  Dictionary with a set of advanced options. The default are the following:
@@ -160,9 +175,6 @@ class BackprojectorBase:
160
175
  self.axis_pos = self.rot_center
161
176
  self._set_angles(angles, n_angles)
162
177
  self._set_slice_roi(slice_roi)
163
- #
164
- # offset = start - move
165
- # move = 0 if not(centered_axis) else start + (n-1)/2. - c
166
178
  if self.extra_options["centered_axis"]:
167
179
  self.offsets = {
168
180
  "x": self.rot_center - (self.n_x - 1) / 2.0,
@@ -208,6 +220,19 @@ class BackprojectorBase:
208
220
  end_x = convert_index(end_x, self.n_x, self.n_x)
209
221
  end_y = convert_index(end_y, self.n_y, self.n_y)
210
222
  self.slice_shape = (end_y - start_y, end_x - start_x)
223
+ if self.extra_options["centered_axis"] and not (
224
+ rot_center_is_in_middle_of_roi(self.rot_center, (start_x, end_x, start_y, end_y))
225
+ ):
226
+ warnings.warn(
227
+ "Using 'centered_axis' when doing a non-centered ROI reconstruction might have side effects: 'start_xy' and 'end_xy' have a different meaning",
228
+ RuntimeWarning,
229
+ )
230
+ # self.extra_options["centered_axis"] = False
231
+ if self.extra_options.get("clip_outer_circle", False) and (
232
+ start_x > 2 or start_y > 2 or abs(end_y - self.n_y) > 2 or abs(end_y - self.n_y) > 2
233
+ ):
234
+ warnings.warn("clip_outer_circle is not supported when doing RoI reconstruction", RuntimeWarning)
235
+ self.extra_options["clip_outer_circle"] = False
211
236
  self.n_x = self.slice_shape[-1]
212
237
  self.n_y = self.slice_shape[-2]
213
238
  self.offsets = {"x": start_x, "y": start_y}
@@ -245,6 +270,11 @@ class BackprojectorBase:
245
270
  if filter_name in ["None", "none"]:
246
271
  self.sino_filter = None
247
272
  return
273
+
274
+ # TODO
275
+ if not (self.extra_options.get("crop_filtered_data", True)):
276
+ warnings.warn("crop_filtered_data = False is not supported for FBP yet", RuntimeWarning)
277
+ #
248
278
  sinofilter_other_kwargs = self._get_filter_init_extra_options()
249
279
  self.sino_filter = self.SinoFilterClass(
250
280
  self.sino_shape,
@@ -355,7 +385,7 @@ class BackprojectorBase:
355
385
 
356
386
  def backproj(self, sino, output=None, do_checks=True):
357
387
  if self.halftomo and self.rot_center < self.dwidth:
358
- self.sino_mult.prepare_sino(sino)
388
+ sino = self.sino_mult.prepare_sino(sino)
359
389
  self._transfer_to_texture(sino)
360
390
  d_slice = self._set_output(output, check=do_checks)
361
391
  self._set_kernel_slice_arg(d_slice)
@@ -377,6 +407,7 @@ class BackprojectorBase:
377
407
  # if a new device array was allocated for sinogram, then the filtering can overwrite it,
378
408
  # since it won't affect user argument
379
409
  if id(d_sino) != id(sino):
410
+ # if id(d_sino) != id(sino) and self.extra_options.get("crop_filtered_data", True):
380
411
  filt_kwargs = {"output": d_sino}
381
412
  #
382
413
  sino_to_backproject = self.sino_filter(d_sino, **filt_kwargs)
@@ -5,15 +5,6 @@ from silx.image.tomography import compute_fourier_filter, get_next_power
5
5
  from ..processing.padding_base import PaddingBase
6
6
  from ..utils import check_supported, get_num_threads
7
7
 
8
- # # COMPAT.
9
- # from .filtering_cuda import CudaSinoFilter
10
-
11
- # SinoFilter = deprecated_class(
12
- # "From version 2023.1, 'filtering_cuda.CudaSinoFilter' should be used instead of 'filtering.SinoFilter'. In the future, 'filtering.SinoFilter' will be a numpy-only class.",
13
- # do_print=True,
14
- # )(CudaSinoFilter)
15
- # #
16
-
17
8
 
18
9
  class SinoFilter:
19
10
  available_filters = [
@@ -44,11 +35,44 @@ class SinoFilter:
44
35
  sino_shape,
45
36
  filter_name=None,
46
37
  padding_mode="zeros",
38
+ crop_filtered_data=True,
47
39
  extra_options=None,
48
40
  ):
41
+ """
42
+ Initialize a SinoFilter instance.
43
+
44
+ Parameters
45
+ ----------
46
+ sino_shape: tuple
47
+ Shape of sinogram, in the form (n_angles, detector_width) or (n_sinos, n_angles, detector_width)
48
+ filter_name: str, optional
49
+ Name of the filter. Default is ram-lak.
50
+ padding_mode: str, optional
51
+ How to pad the data prior to filtering. Default is zero-padding, corresponding to linear convolution with the filter kernel.
52
+ In practice this value is often set to "edges" for interior tomography.
53
+ crop_filtered_data: bool, optional
54
+ Whether to crop the final, filtered sinogram. Default is True. See notes below.
55
+ extra_options: dict, optional
56
+ Dictionary of advanced extra options.
57
+
58
+ Notes
59
+ -----
60
+ Sinogram filtering done in the Filtered Back-Projection (FBP) method consists, in theory, in applying a high-pass filter
61
+ to the sinogram prior to backprojection. This high-pass filter is normally the Ramachandran-Lakshminarayanan (Ram-Lak) filter
62
+ yielding a close-to-ideal reconstruction (see Natterer's "Mathematical methods in image reconstruction").
63
+ As the filter kernel has a large extent in spatial domain, it's best performed in Fourier domain via the Fourier-convolution theorem.
64
+ Filtering in Fourier domain should be done with a data padded to at least twice its size.
65
+ Zero-padding should be used for mathematical correctness (so that multiplication in Fourier domain corresponds to an actual linear convolution).
66
+ However if the sinogram does not decay to "zero" near the edges (i.e in interior tomography), padding with zeros usually gives artefacts after filtering.
67
+ In this case, padding with edges is preferred (corresponding to a convolution with the "edges" extension mode).
68
+
69
+ After inverse Fourier transform, the (padded and filtered) data is cropped back to its original size.
70
+ In some cases, it's preferable to keep the data un-cropped for further processing.
71
+ """
72
+
49
73
  self._init_extra_options(extra_options)
50
74
  self._set_padding_mode(padding_mode)
51
- self._calculate_shapes(sino_shape)
75
+ self._calculate_shapes(sino_shape, crop_filtered_data)
52
76
  self._init_fft()
53
77
  self._allocate_memory()
54
78
  self._compute_filter(filter_name)
@@ -67,7 +91,7 @@ class SinoFilter:
67
91
  check_supported(padding_mode, self.available_padding_modes, "padding mode")
68
92
  self.padding_mode = padding_mode
69
93
 
70
- def _calculate_shapes(self, sino_shape):
94
+ def _calculate_shapes(self, sino_shape, crop_filtered_data):
71
95
  self.ndim = len(sino_shape)
72
96
  if self.ndim == 2:
73
97
  n_angles, dwidth = sino_shape
@@ -90,6 +114,11 @@ class SinoFilter:
90
114
  self.pad_left = (self.dwidth_padded - self.dwidth) // 2
91
115
  self.pad_right = self.dwidth_padded - self.dwidth - self.pad_left
92
116
 
117
+ self.crop_filtered_data = crop_filtered_data
118
+ self.output_shape = self.sino_shape
119
+ if not (self.crop_filtered_data):
120
+ self.output_shape = self.sino_padded_shape
121
+
93
122
  def _init_fft(self):
94
123
  pass
95
124
 
@@ -147,19 +176,16 @@ class SinoFilter:
147
176
  if arr.shape != self.sino_shape:
148
177
  raise ValueError("Expected sinogram shape %s, got %s" % (self.sino_shape, arr.shape))
149
178
 
150
- def filter_sino(self, sino, output=None, no_output=False):
179
+ def filter_sino(self, sino, output=None):
151
180
  """
152
181
  Perform the sinogram siltering.
153
182
 
154
183
  Parameters
155
184
  ----------
156
- sino: numpy.ndarray or pycuda.gpuarray.GPUArray
185
+ sino: array
157
186
  Input sinogram (2D or 3D)
158
- output: numpy.ndarray or pycuda.gpuarray.GPUArray, optional
187
+ output: array, optional
159
188
  Output array.
160
- no_output: bool, optional
161
- If set to True, no copy is be done. The resulting data lies
162
- in self.d_sino_padded.
163
189
  """
164
190
  self._check_array(sino)
165
191
  # sino_padded = np.pad(
@@ -169,16 +195,21 @@ class SinoFilter:
169
195
  sino_padded_f = rfft(sino_padded, axis=1, workers=get_num_threads(self.extra_options["fft_threads"]))
170
196
  sino_padded_f *= self.filter_f
171
197
  sino_filtered = irfft(sino_padded_f, axis=1, workers=get_num_threads(self.extra_options["fft_threads"]))
198
+
172
199
  if output is None:
173
- res = np.zeros(self.sino_shape, dtype=np.float32)
200
+ if not (self.crop_filtered_data):
201
+ # No need to allocate extra memory here
202
+ return sino_filtered
203
+ res = np.zeros(self.output_shape, dtype=np.float32)
174
204
  else:
175
205
  res = output
176
- if self.ndim == 2:
177
- # res[:] = sino_filtered[:, : self.dwidth] # pylint: disable=E1126 # ?!
178
- res[:] = sino_filtered[:, self.pad_left : -self.pad_right] # pylint: disable=E1126 # ?!
206
+
207
+ if self.crop_filtered_data:
208
+ # res[:] = sino_filtered[..., : self.dwidth] # pylint: disable=E1126 # ?!
209
+ res[:] = sino_filtered[..., self.pad_left : -self.pad_right] # pylint: disable=E1126 # ?!
179
210
  else:
180
- # res[:] = sino_filtered[:, :, : self.dwidth] # pylint: disable=E1126 # ?!
181
- res[:] = sino_filtered[:, :, self.pad_left : -self.pad_right] # pylint: disable=E1126 # ?!
211
+ res[:] = sino_filtered[:]
212
+
182
213
  return res
183
214
 
184
215
  __call__ = filter_sino
@@ -191,6 +222,7 @@ def filter_sinogram(
191
222
  padding_mode="constant",
192
223
  normalize=True,
193
224
  filter_cutoff=1.0,
225
+ crop_filtered_data=True,
194
226
  **padding_kwargs,
195
227
  ):
196
228
  """
@@ -228,6 +260,8 @@ def filter_sinogram(
228
260
  fourier_filter = fourier_filter[: padded_width // 2 + 1] # R2C
229
261
  sino_f = rfft(sinogram_padded, axis=1)
230
262
  sino_f *= fourier_filter
231
- # sino_filtered = irfft(sino_f, axis=1)[:, :width] # pylint: disable=E1126 # ?!
232
- sino_filtered = irfft(sino_f, axis=1)[:, pad_left:-pad_right] # pylint: disable=E1126 # ?!
263
+ sino_filtered = irfft(sino_f, axis=1)
264
+ if crop_filtered_data:
265
+ # sino_filtered = sino_filtered[:, :width] # pylint: disable=E1126 # ?!
266
+ sino_filtered = sino_filtered[:, pad_left:-pad_right] # pylint: disable=E1126 # ?!
233
267
  return sino_filtered
@@ -1,25 +1,33 @@
1
1
  import numpy as np
2
2
  from ..cuda.processing import CudaProcessing
3
- from ..utils import get_cuda_srcfile
3
+ from ..utils import docstring, get_cuda_srcfile
4
4
  from ..processing.padding_cuda import CudaPadding
5
5
  from ..processing.fft_cuda import get_fft_class
6
6
  from .filtering import SinoFilter
7
7
 
8
8
 
9
9
  class CudaSinoFilter(SinoFilter):
10
- default_extra_options = {**SinoFilter.default_extra_options, **{"fft_backend": "vkfft"}}
10
+ default_extra_options = {**SinoFilter.default_extra_options, "fft_backend": "vkfft"}
11
11
 
12
+ @docstring(SinoFilter)
12
13
  def __init__(
13
14
  self,
14
15
  sino_shape,
15
16
  filter_name=None,
16
17
  padding_mode="zeros",
18
+ crop_filtered_data=True,
17
19
  extra_options=None,
18
20
  cuda_options=None,
19
21
  ):
20
22
  self._cuda_options = cuda_options or {}
21
23
  self.cuda = CudaProcessing(**self._cuda_options)
22
- super().__init__(sino_shape, filter_name=filter_name, padding_mode=padding_mode, extra_options=extra_options)
24
+ super().__init__(
25
+ sino_shape,
26
+ filter_name=filter_name,
27
+ padding_mode=padding_mode,
28
+ crop_filtered_data=crop_filtered_data,
29
+ extra_options=extra_options,
30
+ )
23
31
  self._init_kernels()
24
32
 
25
33
  def _init_fft(self):
@@ -60,25 +68,12 @@ class CudaSinoFilter(SinoFilter):
60
68
  )
61
69
 
62
70
  def filter_sino(self, sino, output=None):
63
- """
64
- Perform the sinogram siltering.
65
-
66
- Parameters
67
- ----------
68
- sino: numpy.ndarray or pycuda.gpuarray.GPUArray
69
- Input sinogram (2D or 3D)
70
- output: pycuda.gpuarray.GPUArray, optional
71
- Output array.
72
- no_output: bool, optional
73
- If set to True, no copy is be done. The resulting data lies
74
- in self.d_sino_padded.
75
- """
76
71
  self._check_array(sino)
77
72
  if not (isinstance(sino, self.cuda.array_class)):
78
73
  sino = self.cuda.set_array("sino", sino)
79
74
  elif not (sino.flags.c_contiguous):
80
75
  # Transfer the device array into another, c-contiguous, device array
81
- # We can throw an error as well in this case, but often we so something like fbp(radios[:, i, :])
76
+ # We can throw an error as well in this case, but often we do something like fbp(radios[:, i, :])
82
77
  sino_tmp = self.cuda.allocate_array("sino_contig", sino.shape)
83
78
  sino_tmp.set(sino)
84
79
  sino = sino_tmp
@@ -97,13 +92,19 @@ class CudaSinoFilter(SinoFilter):
97
92
 
98
93
  # return
99
94
  if output is None:
100
- res = self.cuda.allocate_array("output", self.sino_shape)
95
+ if not (self.crop_filtered_data):
96
+ # No need to allocate extra memory here
97
+ return self.d_sino_padded
98
+ res = self.cuda.allocate_array("output", self.output_shape)
101
99
  else:
102
100
  res = output
103
- if self.ndim == 2:
104
- res[:] = self.d_sino_padded[:, self.pad_left : self.pad_left + self.dwidth]
101
+
102
+ if self.crop_filtered_data:
103
+ # res[:] = sino_filtered[..., : self.dwidth] # pylint: disable=E1126 # ?!
104
+ res[:] = self.d_sino_padded[..., self.pad_left : -self.pad_right] # pylint: disable=E1126 # ?!
105
105
  else:
106
- res[:] = self.d_sino_padded[:, :, self.pad_left : self.pad_left + self.dwidth]
106
+ res[:] = self.d_sino_padded[:]
107
+
107
108
  return res
108
109
 
109
110
  __call__ = filter_sino
@@ -19,13 +19,22 @@ class OpenCLSinoFilter(SinoFilter):
19
19
  sino_shape,
20
20
  filter_name=None,
21
21
  padding_mode="zeros",
22
+ crop_filtered_data=True,
22
23
  extra_options=None,
23
24
  opencl_options=None,
24
25
  ):
25
26
  self._opencl_options = opencl_options or {}
26
27
  self.opencl = OpenCLProcessing(**self._opencl_options)
27
28
  self.queue = self.opencl.queue
28
- super().__init__(sino_shape, filter_name=filter_name, padding_mode=padding_mode, extra_options=extra_options)
29
+ super().__init__(
30
+ sino_shape,
31
+ filter_name=filter_name,
32
+ padding_mode=padding_mode,
33
+ crop_filtered_data=crop_filtered_data,
34
+ extra_options=extra_options,
35
+ )
36
+ if not (crop_filtered_data):
37
+ raise NotImplementedError # TODO
29
38
  self._init_kernels()
30
39
 
31
40
  def _init_fft(self):
@@ -61,19 +70,6 @@ class OpenCLSinoFilter(SinoFilter):
61
70
  self.memcpy2D = OpenCLMemcpy2D(queue=self.queue)
62
71
 
63
72
  def filter_sino(self, sino, output=None):
64
- """
65
- Perform the sinogram siltering.
66
-
67
- Parameters
68
- ----------
69
- sino: numpy.ndarray or pyopencl.array
70
- Input sinogram (2D or 3D)
71
- output: pyopencl.array, optional
72
- Output array.
73
- no_output: bool, optional
74
- If set to True, no copy is be done. The resulting data lies
75
- in self.d_sino_padded.
76
- """
77
73
  self._check_array(sino)
78
74
  sino = self.opencl.set_array("sino", sino)
79
75
 
@@ -1,3 +1,15 @@
1
+ # Generalized Hierarchical Backprojection (GHBP)
2
+ # for fast tomographic reconstruction from ultra high resolution images at non-negligible fan angles.
3
+ #
4
+ # Authors/Contributions:
5
+ # - Jonas Graetz, Fraunhofer IIS / Universität Würzburg: Algorithm Design and original OpenCL/Python implementation.
6
+ # - Alessandro Mirone, ESRF: CUDA translation, ESRF / BM18 integration, testing <mirone@esrf.fr>
7
+ # - Pierre Paleo, ESRF: ESRF / BM18 integration, testing <pierre.paleo@esrf.fr>
8
+ #
9
+ # JG was funded by the German Federal Ministry of Education and Research (BMBF), grant 05E2019,
10
+ # funding the development of BM18 at ESRF in collaboration with the Fraunhofer Gesellschaft,
11
+ # the Julius-Maximilians-Universität Würzburg, and the University of Passau
12
+
1
13
  import math
2
14
  import numpy as np
3
15
 
@@ -12,6 +24,7 @@ from .fbp import CudaBackprojector
12
24
 
13
25
 
14
26
  try:
27
+ # ruff: noqa: F401
15
28
  import pycuda.driver as cuda
16
29
  from pycuda import gpuarray as garray
17
30
 
@@ -60,7 +73,7 @@ class HierarchicalBackprojector(CudaBackprojector):
60
73
 
61
74
  # to do the reconstruction in reduction_steps steps
62
75
  self.reduction_steps = self.extra_options.get("hbp_reduction_steps", 2)
63
- reduction_factor = int(math.ceil((sino_shape[-2]) ** (1 / self.reduction_steps)))
76
+ reduction_factor = math.ceil((sino_shape[-2]) ** (1 / self.reduction_steps))
64
77
 
65
78
  # TODO customize
66
79
  axis_source_meters = 1.0e9
@@ -138,15 +151,15 @@ class HierarchicalBackprojector(CudaBackprojector):
138
151
 
139
152
  N = self.slice_shape[1] * fac
140
153
 
141
- angularRange = abs(self.angles.ptp()) / self.sino_shape[0] * reductionFactor
154
+ angularRange = abs(np.ptp(self.angles)) / self.sino_shape[0] * reductionFactor
142
155
 
143
- ngrids = int(math.ceil(self.sino_shape[0] / reductionFactor))
156
+ ngrids = math.ceil(self.sino_shape[0] / reductionFactor)
144
157
 
145
158
  grid_width = int(
146
159
  np.rint(2 * N * self.whf[0])
147
160
  ) # double sampling to account/compensate for diamond shaped grid of ray-intersections
148
- grid_height = int(
149
- math.ceil(angularRange * N * self.whf[1])
161
+ grid_height = math.ceil(
162
+ angularRange * N * self.whf[1]
150
163
  ) # small-angle approximation, generates as much "lines" as needed to account for all intersection levels
151
164
 
152
165
  m = (len(self.angles) // reductionFactor) * reductionFactor
@@ -166,7 +179,7 @@ class HierarchicalBackprojector(CudaBackprojector):
166
179
  )
167
180
  ]
168
181
  self.gridInvTransforms += [np.array([np.linalg.inv(t) for t in self.gridTransforms[-1]], dtype=np.float32)]
169
- self.grids += [(grid_height, grid_width, int(math.ceil(ngrids / legs)))]
182
+ self.grids += [(grid_height, grid_width, math.ceil(ngrids / legs))]
170
183
  self.reductionFactors += [reductionFactor]
171
184
 
172
185
  ### intermediate level grids: accumulation grids ###
@@ -177,13 +190,13 @@ class HierarchicalBackprojector(CudaBackprojector):
177
190
  # for a reasonable (with regard to memory requirement) grid-aspect ratio in the intermediate levels,
178
191
  # the covered angular range per grid should not exceed 28.6°, i.e.,
179
192
  # fewer than 7 (6.3) or 13 (12.6) grids for a 180° / 360° scan is not reasonable
180
- if int(math.ceil(ngrids / reductionFactor)) < 20:
193
+ if math.ceil(ngrids / reductionFactor) < 20:
181
194
  break
182
195
  angularRange *= reductionFactor
183
- ngrids = int(math.ceil(ngrids / reductionFactor))
196
+ ngrids = math.ceil(ngrids / reductionFactor)
184
197
 
185
- grid_height = int(
186
- math.ceil(angularRange * N * self.whf[1])
198
+ grid_height = math.ceil(
199
+ angularRange * N * self.whf[1]
187
200
  ) # implicit small angle approximation, whose validity is
188
201
  # asserted by the preceding "break"
189
202
  gridAinvT = self._getAinvT(N, grid_height, grid_width)
@@ -205,7 +218,7 @@ class HierarchicalBackprojector(CudaBackprojector):
205
218
  )
206
219
  ]
207
220
  self.gridInvTransforms += [np.array([np.linalg.inv(t) for t in self.gridTransforms[-1]], dtype=np.float32)]
208
- self.grids += [(grid_height, grid_width, int(math.ceil(ngrids / legs)))]
221
+ self.grids += [(grid_height, grid_width, math.ceil(ngrids / legs))]
209
222
  self.reductionFactors += [reductionFactor]
210
223
 
211
224
  ##### final accumulation grid #################
@@ -331,7 +344,7 @@ class HierarchicalBackprojector(CudaBackprojector):
331
344
  )
332
345
 
333
346
  else:
334
- for leg in list(range(0, self.legs)):
347
+ for leg in list(range(self.legs)):
335
348
  gridOffset = leg * self.grids[0][2]
336
349
  projOffset = gridOffset * self.reductionFactors[0]
337
350
  gws = getGridSize(self.grids[0], lws)
@@ -418,7 +431,7 @@ def get_max_grid_size(grids):
418
431
 
419
432
 
420
433
  def getGridSize(minimum, local):
421
- m, l = np.array(minimum), np.array(local)
434
+ m, l = np.array(minimum), np.array(local) # noqa: E741
422
435
  new = (m // l) * l
423
436
  new[new < m] += l[new < m]
424
437
  return tuple(map(int, new // l))
@@ -12,6 +12,28 @@ except ImportError:
12
12
  class MLEMReconstructor:
13
13
  """
14
14
  A reconstructor for MLEM reconstruction using the CorrCT toolbox.
15
+
16
+ Parameters
17
+ ----------
18
+ data_vwu_shape : tuple
19
+ Shape of the input data, expected to be (n_slices, n_angles, n_dets). Raises an error if the shape is not 3D.
20
+ angles_rad : numpy.ndarray
21
+ Angles in radians for the projections. Must match the second dimension of `data_vwu_shape`.
22
+ shifts_vu : numpy.ndarray, optional.
23
+ Shifts in the v and u directions for each angle. If provided, must have the same number of cols as `angles_rad`. Each col is (tv,tu)
24
+ cor : float, optional
25
+ Center of rotation, which will be adjusted based on the sinogram width.
26
+ n_iterations : int, optional
27
+ Number of iterations for the MLEM algorithm. Default is 50.
28
+ extra_options : dict, optional
29
+ Additional options for the reconstruction process. Default options include:
30
+ - scale_factor (float, default is 1.0): Scale factor for the reconstruction.
31
+ - compute_shifts (boolean, default is False): Whether to compute shifts.
32
+ - tomo_consistency (boolean, default is False): Whether to enforce tomographic consistency.
33
+ - v_min_for_v_shifts (number, default is 0): Minimum value for vertical shifts.
34
+ - v_max_for_v_shifts (number, default is None): Maximum value for vertical shifts.
35
+ - v_min_for_u_shifts (number, default is 0): Minimum value for horizontal shifts.
36
+ - v_max_for_u_shifts (number, default is None): Maximum value for horizontal shifts.
15
37
  """
16
38
 
17
39
  default_extra_options = {
@@ -21,25 +43,32 @@ class MLEMReconstructor:
21
43
  "v_max_for_v_shifts": None,
22
44
  "v_min_for_u_shifts": 0,
23
45
  "v_max_for_u_shifts": None,
46
+ "scale_factor": 1.0,
47
+ "centered_axis": False,
48
+ "clip_outer_circle": False,
49
+ "outer_circle_value": 0.0,
50
+ "filter_cutoff": 1.0,
51
+ "padding_mode": None,
52
+ "crop_filtered_data": True,
24
53
  }
25
54
 
26
55
  def __init__(
27
56
  self,
28
- sinos_shape,
57
+ data_vwu_shape,
29
58
  angles_rad,
30
59
  shifts_uv=None,
31
- cor=None,
60
+ cor=None, # absolute
32
61
  n_iterations=50,
33
62
  extra_options=None,
34
63
  ):
35
- """ """
36
64
  if not (__have_corrct__):
37
65
  raise ImportError("Need corrct package")
38
66
  self.angles_rad = angles_rad
39
67
  self.n_iterations = n_iterations
68
+ self.scale_factor = extra_options.get("scale_factor", 1.0)
40
69
 
41
70
  self._configure_extra_options(extra_options)
42
- self._set_sino_shape(sinos_shape)
71
+ self._set_sino_shape(data_vwu_shape)
43
72
  self._set_shifts(shifts_uv, cor)
44
73
 
45
74
  def _configure_extra_options(self, extra_options):
@@ -58,16 +87,24 @@ class MLEMReconstructor:
58
87
 
59
88
  def _set_shifts(self, shifts_uv, cor):
60
89
  if shifts_uv is None:
61
- self.shifts_uv = np.zeros([self.n_angles, 2])
90
+ self.shifts_vu = None
62
91
  else:
63
92
  if shifts_uv.shape[0] != self.n_angles:
64
93
  raise ValueError(
65
94
  f"Number of shifts given ({shifts_uv.shape[0]}) does not mathc the number of projections ({self.n_angles})."
66
95
  )
67
- self.shifts_uv = shifts_uv.copy()
68
- self.cor = cor
96
+ self.shifts_vu = -shifts_uv.copy().T[::-1]
97
+ if cor is None:
98
+ self.cor = 0.0
99
+ else:
100
+ self.cor = (
101
+ -cor + (self.sinos_shape[-1] - 1) / 2.0
102
+ ) # convert absolute to relative in the ASTRA convention, which is opposite to Nabu relative convention.
103
+
104
+ def reset_rot_center(self, cor):
105
+ self.cor = -cor + (self.sinos_shape[-1] - 1) / 2.0
69
106
 
70
- def reconstruct(self, data_vwu):
107
+ def reconstruct(self, data_vwu, x0=None):
71
108
  """
72
109
  data_align_vwu: numpy.ndarray or pycuda.gpuarray
73
110
  Raw data, with shape (n_sinograms, n_angles, width)
@@ -76,14 +113,17 @@ class MLEMReconstructor:
76
113
  """
77
114
  if not isinstance(data_vwu, np.ndarray):
78
115
  data_vwu = data_vwu.get()
79
- data_vwu /= data_vwu.mean()
116
+ # data_vwu /= data_vwu.mean()
80
117
 
81
118
  # MLEM recons
82
119
  self.vol_geom_align = cct.models.VolumeGeometry.get_default_from_data(data_vwu)
83
- self.prj_geom_align = cct.models.ProjectionGeometry.get_default_parallel()
84
- # Vertical shifts were handled in pipeline. Set them to ZERO
85
- self.shifts_uv[:, 1] = 0.0
86
- self.prj_geom_align.set_detector_shifts_vu(self.shifts_uv.T[::-1])
120
+ if self.shifts_vu is not None:
121
+ self.prj_geom_align = cct.models.ProjectionGeometry.get_default_parallel()
122
+ # Vertical shifts were handled in pipeline. Set them to ZERO
123
+ self.shifts_vu[:, 0] = 0.0
124
+ self.prj_geom_align.set_detector_shifts_vu(self.shifts_vu, self.cor)
125
+ else:
126
+ self.prj_geom_align = None
87
127
 
88
128
  variances_align = cct.processing.compute_variance_poisson(data_vwu)
89
129
  self.weights_align = cct.processing.compute_variance_weight(variances_align, normalized=True) # , use_std=True
@@ -94,6 +134,5 @@ class MLEMReconstructor:
94
134
  with cct.projectors.ProjectorUncorrected(
95
135
  self.vol_geom_align, self.angles_rad, rot_axis_shift_pix=self.cor, prj_geom=self.prj_geom_align
96
136
  ) as A:
97
- rec, _ = solver(A, data_vwu, iterations=self.n_iterations, **self.solver_opts)
98
-
99
- return rec
137
+ rec, _ = solver(A, data_vwu, iterations=self.n_iterations, x0=x0, **self.solver_opts)
138
+ return rec * self.scale_factor
@@ -32,7 +32,6 @@ class Projector:
32
32
 
33
33
  Parameters
34
34
  -----------
35
-
36
35
  slice_shape: tuple
37
36
  Shape of the slice: (num_rows, num_columns).
38
37
  angles: int or sequence
@@ -198,10 +197,9 @@ class Projector:
198
197
  if image.dtype != np.dtype("f"):
199
198
  raise ValueError("Expected float32 data type, got %s" % str(image.dtype))
200
199
  if not isinstance(image, (np.ndarray, garray.GPUArray)):
201
- raise ValueError("Expected either numpy.ndarray or pyopencl.array.Array")
202
- if isinstance(image, np.ndarray):
203
- if not image.flags["C_CONTIGUOUS"]:
204
- raise ValueError("Please use C-contiguous arrays")
200
+ raise TypeError("Expected either numpy.ndarray or pyopencl.array.Array")
201
+ if isinstance(image, np.ndarray) and not image.flags["C_CONTIGUOUS"]:
202
+ raise ValueError("Please use C-contiguous arrays")
205
203
 
206
204
  def set_image(self, image, check=True):
207
205
  if check:
@@ -290,7 +290,7 @@ def match_half_sinos_parts(sino, angles, output=None):
290
290
  """
291
291
  n_a = angles.size
292
292
  n_a_2 = n_a // 2
293
- sino_part1 = sino[:n_a_2, :]
293
+ # sino_part1 = sino[:n_a_2, :]
294
294
  sino_part2 = sino[n_a_2:, :]
295
295
  angles = np.rad2deg(angles) # more numerically stable ?
296
296
  angles_1 = angles[:n_a_2]
@@ -212,7 +212,6 @@ class CudaSinoNormalization(SinoNormalization):
212
212
  else:
213
213
  # This kernel seems to have an issue on arrays that are not C-contiguous.
214
214
  # We have to process image per image.
215
- nz = np.int32(1)
216
215
  nthreadsperblock = (1, 32, 1) # TODO tune
217
216
  nblocks = (1, int(updiv(self.n_angles, nthreadsperblock[1])), 1)
218
217
  for i in range(sinos.shape[0]):