nabu 2024.2.4__py3-none-any.whl → 2025.1.0.dev4__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 (164) 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 +7 -13
  5. nabu/app/cli_configs.py +0 -5
  6. nabu/app/compare_volumes.py +1 -1
  7. nabu/app/composite_cor.py +2 -4
  8. nabu/app/correct_rot.py +0 -8
  9. nabu/app/diag_to_pix.py +5 -6
  10. nabu/app/diag_to_rot.py +10 -11
  11. nabu/app/multicor.py +1 -1
  12. nabu/app/parse_reconstruction_log.py +1 -0
  13. nabu/app/prepare_weights_double.py +1 -2
  14. nabu/app/reconstruct_helical.py +1 -5
  15. nabu/app/reduce_dark_flat.py +0 -2
  16. nabu/app/rotate.py +3 -1
  17. nabu/app/tests/test_reduce_dark_flat.py +2 -2
  18. nabu/app/validator.py +1 -4
  19. nabu/cuda/convolution.py +1 -1
  20. nabu/cuda/fft.py +1 -1
  21. nabu/cuda/medfilt.py +1 -1
  22. nabu/cuda/padding.py +1 -1
  23. nabu/cuda/src/cone.cu +19 -9
  24. nabu/cuda/src/hierarchical_backproj.cu +16 -0
  25. nabu/cuda/utils.py +2 -2
  26. nabu/estimation/alignment.py +17 -31
  27. nabu/estimation/cor.py +23 -29
  28. nabu/estimation/cor_sino.py +2 -8
  29. nabu/estimation/focus.py +4 -8
  30. nabu/estimation/tests/test_alignment.py +2 -0
  31. nabu/estimation/tests/test_tilt.py +1 -1
  32. nabu/estimation/tilt.py +5 -4
  33. nabu/io/cast_volume.py +5 -5
  34. nabu/io/detector_distortion.py +5 -6
  35. nabu/io/reader.py +3 -3
  36. nabu/io/reader_helical.py +5 -4
  37. nabu/io/tests/test_cast_volume.py +2 -2
  38. nabu/io/tests/test_readers.py +4 -4
  39. nabu/io/tests/test_writers.py +2 -2
  40. nabu/io/utils.py +8 -4
  41. nabu/io/writer.py +1 -2
  42. nabu/misc/fftshift.py +1 -1
  43. nabu/misc/fourier_filters.py +1 -1
  44. nabu/misc/histogram.py +1 -1
  45. nabu/misc/histogram_cuda.py +1 -1
  46. nabu/misc/padding_base.py +1 -1
  47. nabu/misc/rotation.py +1 -1
  48. nabu/misc/rotation_cuda.py +1 -1
  49. nabu/misc/tests/test_binning.py +1 -1
  50. nabu/misc/transpose.py +1 -1
  51. nabu/misc/unsharp.py +1 -1
  52. nabu/misc/unsharp_cuda.py +1 -1
  53. nabu/misc/unsharp_opencl.py +1 -1
  54. nabu/misc/utils.py +1 -1
  55. nabu/opencl/fft.py +1 -1
  56. nabu/opencl/padding.py +1 -1
  57. nabu/opencl/utils.py +8 -8
  58. nabu/pipeline/config.py +2 -2
  59. nabu/pipeline/config_validators.py +4 -3
  60. nabu/pipeline/datadump.py +3 -3
  61. nabu/pipeline/estimators.py +6 -6
  62. nabu/pipeline/fullfield/chunked.py +4 -5
  63. nabu/pipeline/fullfield/dataset_validator.py +0 -1
  64. nabu/pipeline/fullfield/nabu_config.py +2 -1
  65. nabu/pipeline/fullfield/reconstruction.py +9 -8
  66. nabu/pipeline/helical/dataset_validator.py +3 -4
  67. nabu/pipeline/helical/fbp.py +4 -4
  68. nabu/pipeline/helical/filtering.py +5 -4
  69. nabu/pipeline/helical/gridded_accumulator.py +9 -10
  70. nabu/pipeline/helical/helical_chunked_regridded.py +1 -0
  71. nabu/pipeline/helical/helical_reconstruction.py +10 -7
  72. nabu/pipeline/helical/helical_utils.py +1 -2
  73. nabu/pipeline/helical/nabu_config.py +1 -0
  74. nabu/pipeline/helical/span_strategy.py +1 -0
  75. nabu/pipeline/helical/weight_balancer.py +1 -2
  76. nabu/pipeline/tests/__init__.py +0 -0
  77. nabu/pipeline/utils.py +1 -1
  78. nabu/pipeline/writer.py +1 -1
  79. nabu/preproc/alignment.py +0 -10
  80. nabu/preproc/ctf.py +8 -8
  81. nabu/preproc/ctf_cuda.py +1 -1
  82. nabu/preproc/double_flatfield_cuda.py +2 -2
  83. nabu/preproc/double_flatfield_variable_region.py +0 -1
  84. nabu/preproc/flatfield.py +1 -1
  85. nabu/preproc/flatfield_cuda.py +1 -2
  86. nabu/preproc/flatfield_variable_region.py +3 -3
  87. nabu/preproc/phase.py +2 -4
  88. nabu/preproc/phase_cuda.py +2 -2
  89. nabu/preproc/shift_cuda.py +0 -1
  90. nabu/preproc/tests/test_ctf.py +3 -3
  91. nabu/preproc/tests/test_double_flatfield.py +1 -1
  92. nabu/preproc/tests/test_flatfield.py +1 -1
  93. nabu/preproc/tests/test_vshift.py +4 -1
  94. nabu/processing/azim.py +2 -2
  95. nabu/processing/convolution_cuda.py +6 -4
  96. nabu/processing/fft_base.py +1 -1
  97. nabu/processing/fft_cuda.py +19 -8
  98. nabu/processing/fft_opencl.py +9 -4
  99. nabu/processing/fftshift.py +1 -1
  100. nabu/processing/histogram.py +1 -1
  101. nabu/processing/muladd.py +0 -1
  102. nabu/processing/padding_base.py +1 -1
  103. nabu/processing/padding_cuda.py +0 -1
  104. nabu/processing/processing_base.py +1 -1
  105. nabu/processing/tests/test_fft.py +1 -1
  106. nabu/processing/tests/test_fftshift.py +1 -1
  107. nabu/processing/tests/test_medfilt.py +1 -3
  108. nabu/processing/tests/test_padding.py +1 -1
  109. nabu/processing/tests/test_roll.py +1 -1
  110. nabu/processing/unsharp_opencl.py +1 -1
  111. nabu/reconstruction/astra.py +245 -0
  112. nabu/reconstruction/cone.py +9 -4
  113. nabu/reconstruction/fbp_base.py +2 -2
  114. nabu/reconstruction/filtering_cuda.py +1 -1
  115. nabu/reconstruction/hbp.py +16 -3
  116. nabu/reconstruction/mlem.py +0 -1
  117. nabu/reconstruction/projection.py +3 -5
  118. nabu/reconstruction/sinogram.py +1 -1
  119. nabu/reconstruction/sinogram_cuda.py +0 -1
  120. nabu/reconstruction/tests/test_cone.py +76 -3
  121. nabu/reconstruction/tests/test_deringer.py +2 -2
  122. nabu/reconstruction/tests/test_fbp.py +1 -1
  123. nabu/reconstruction/tests/test_halftomo.py +27 -1
  124. nabu/reconstruction/tests/test_mlem.py +3 -2
  125. nabu/reconstruction/tests/test_projector.py +7 -2
  126. nabu/reconstruction/tests/test_sino_normalization.py +0 -1
  127. nabu/resources/dataset_analyzer.py +4 -4
  128. nabu/resources/gpu.py +4 -4
  129. nabu/resources/logger.py +4 -4
  130. nabu/resources/nxflatfield.py +2 -2
  131. nabu/resources/tests/test_nxflatfield.py +4 -4
  132. nabu/stitching/alignment.py +1 -4
  133. nabu/stitching/config.py +19 -16
  134. nabu/stitching/frame_composition.py +8 -10
  135. nabu/stitching/overlap.py +2 -2
  136. nabu/stitching/slurm_utils.py +2 -2
  137. nabu/stitching/stitcher/base.py +2 -0
  138. nabu/stitching/stitcher/dumper/base.py +0 -1
  139. nabu/stitching/stitcher/dumper/postprocessing.py +1 -1
  140. nabu/stitching/stitcher/post_processing.py +6 -6
  141. nabu/stitching/stitcher/pre_processing.py +13 -11
  142. nabu/stitching/stitcher/single_axis.py +3 -4
  143. nabu/stitching/stitcher_2D.py +2 -1
  144. nabu/stitching/tests/test_config.py +7 -8
  145. nabu/stitching/tests/test_sample_normalization.py +1 -1
  146. nabu/stitching/tests/test_slurm_utils.py +1 -2
  147. nabu/stitching/tests/test_z_postprocessing_stitching.py +1 -1
  148. nabu/stitching/tests/test_z_preprocessing_stitching.py +4 -4
  149. nabu/stitching/utils/tests/__init__.py +0 -0
  150. nabu/stitching/utils/tests/test_post-processing.py +1 -0
  151. nabu/stitching/utils/utils.py +10 -12
  152. nabu/tests.py +0 -3
  153. nabu/testutils.py +30 -8
  154. nabu/utils.py +28 -18
  155. {nabu-2024.2.4.dist-info → nabu-2025.1.0.dev4.dist-info}/METADATA +25 -25
  156. nabu-2025.1.0.dev4.dist-info/RECORD +320 -0
  157. {nabu-2024.2.4.dist-info → nabu-2025.1.0.dev4.dist-info}/WHEEL +1 -1
  158. nabu/io/tests/test_detector_distortion.py +0 -178
  159. nabu/resources/tests/test_extract.py +0 -9
  160. nabu-2024.2.4.dist-info/RECORD +0 -318
  161. /nabu/{stitching → app}/tests/__init__.py +0 -0
  162. {nabu-2024.2.4.dist-info → nabu-2025.1.0.dev4.dist-info}/LICENSE +0 -0
  163. {nabu-2024.2.4.dist-info → nabu-2025.1.0.dev4.dist-info}/entry_points.txt +0 -0
  164. {nabu-2024.2.4.dist-info → nabu-2025.1.0.dev4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,245 @@
1
+ # ruff: noqa
2
+ try:
3
+ import astra
4
+
5
+ __have_astra__ = True
6
+ except ImportError:
7
+ __have_astra__ = False
8
+ astra = None
9
+
10
+
11
+ class AstraReconstructor:
12
+ """
13
+ Base class for reconstructors based on the Astra toolbox
14
+ """
15
+
16
+ default_extra_options = {
17
+ "axis_correction": None,
18
+ "clip_outer_circle": False,
19
+ "scale_factor": None,
20
+ "filter_cutoff": 1.0,
21
+ "outer_circle_value": 0.0,
22
+ }
23
+
24
+ def __init__(
25
+ self,
26
+ sinos_shape,
27
+ angles=None,
28
+ volume_shape=None,
29
+ rot_center=None,
30
+ pixel_size=None,
31
+ padding_mode="zeros",
32
+ filter_name=None,
33
+ slice_roi=None,
34
+ cuda_options=None,
35
+ extra_options=None,
36
+ ):
37
+ self._configure_extra_options(extra_options)
38
+ self._init_cuda(cuda_options)
39
+ self._set_sino_shape(sinos_shape)
40
+ self._orig_prog_geom = None
41
+ self._init_geometry(
42
+ source_origin_dist,
43
+ origin_detector_dist,
44
+ pixel_size,
45
+ angles,
46
+ volume_shape,
47
+ rot_center,
48
+ relative_z_position,
49
+ slice_roi,
50
+ )
51
+ self._init_fdk(padding_mode, filter_name)
52
+ self._alg_id = None
53
+ self._vol_id = None
54
+ self._proj_id = None
55
+
56
+ def _configure_extra_options(self, extra_options):
57
+ self.extra_options = self.default_extra_options.copy()
58
+ self.extra_options.update(extra_options or {})
59
+
60
+ def _init_cuda(self, cuda_options):
61
+ cuda_options = cuda_options or {}
62
+ self.cuda = CudaProcessing(**cuda_options)
63
+
64
+ def _set_sino_shape(self, sinos_shape):
65
+ if len(sinos_shape) != 3:
66
+ raise ValueError("Expected a 3D shape")
67
+ self.sinos_shape = sinos_shape
68
+ self.n_sinos, self.n_angles, self.prj_width = sinos_shape
69
+
70
+ def _set_pixel_size(self, pixel_size):
71
+ if pixel_size is None:
72
+ det_spacing_y = det_spacing_x = 1
73
+ elif np.iterable(pixel_size):
74
+ det_spacing_y, det_spacing_x = pixel_size
75
+ else:
76
+ # assuming scalar
77
+ det_spacing_y = det_spacing_x = pixel_size
78
+ self._det_spacing_y = det_spacing_y
79
+ self._det_spacing_x = det_spacing_x
80
+
81
+ def _set_slice_roi(self, slice_roi):
82
+ self.slice_roi = slice_roi
83
+ self._vol_geom_n_x = self.n_x
84
+ self._vol_geom_n_y = self.n_y
85
+ self._crop_data = True
86
+ if slice_roi is None:
87
+ return
88
+ start_x, end_x, start_y, end_y = slice_roi
89
+ if roi_is_centered(self.volume_shape[1:], (slice(start_y, end_y), slice(start_x, end_x))):
90
+ # Astra can only reconstruct subregion centered around the origin
91
+ self._vol_geom_n_x = self.n_x - start_x * 2
92
+ self._vol_geom_n_y = self.n_y - start_y * 2
93
+ else:
94
+ raise NotImplementedError(
95
+ "Astra supports only slice_roi centered around origin (got slice_roi=%s with n_x=%d, n_y=%d)"
96
+ % (str(slice_roi), self.n_x, self.n_y)
97
+ )
98
+
99
+ def _init_geometry(
100
+ self,
101
+ source_origin_dist,
102
+ origin_detector_dist,
103
+ pixel_size,
104
+ angles,
105
+ volume_shape,
106
+ rot_center,
107
+ relative_z_position,
108
+ slice_roi,
109
+ ):
110
+ if angles is None:
111
+ self.angles = np.linspace(0, 2 * np.pi, self.n_angles, endpoint=True)
112
+ else:
113
+ self.angles = angles
114
+ if volume_shape is None:
115
+ volume_shape = (self.sinos_shape[0], self.sinos_shape[2], self.sinos_shape[2])
116
+ self.volume_shape = volume_shape
117
+ self.n_z, self.n_y, self.n_x = self.volume_shape
118
+ self.source_origin_dist = source_origin_dist
119
+ self.origin_detector_dist = origin_detector_dist
120
+ self.magnification = 1 + origin_detector_dist / source_origin_dist
121
+ self._set_slice_roi(slice_roi)
122
+ self.vol_geom = astra.create_vol_geom(self._vol_geom_n_y, self._vol_geom_n_x, self.n_z)
123
+ self.vol_shape = astra.geom_size(self.vol_geom)
124
+ self._cor_shift = 0.0
125
+ self.rot_center = rot_center
126
+ if rot_center is not None:
127
+ self._cor_shift = (self.sinos_shape[-1] - 1) / 2.0 - rot_center
128
+ self._set_pixel_size(pixel_size)
129
+ self._axis_corrections = self.extra_options.get("axis_correction", None)
130
+ self._create_astra_proj_geometry(relative_z_position)
131
+
132
+ def _create_astra_proj_geometry(self, relative_z_position):
133
+ # This object has to be re-created each time, because once the modifications below are done,
134
+ # it is no more a "cone" geometry but a "cone_vec" geometry, and cannot be updated subsequently
135
+ # (see astra/functions.py:271)
136
+ self.proj_geom = astra.create_proj_geom(
137
+ "cone",
138
+ self._det_spacing_x,
139
+ self._det_spacing_y,
140
+ self.n_sinos,
141
+ self.prj_width,
142
+ self.angles,
143
+ self.source_origin_dist,
144
+ self.origin_detector_dist,
145
+ )
146
+ self.relative_z_position = relative_z_position or 0.0
147
+ # This will turn the geometry of type "cone" into a geometry of type "cone_vec"
148
+ if self._orig_prog_geom is None:
149
+ self._orig_prog_geom = self.proj_geom
150
+ self.proj_geom = astra.geom_postalignment(self.proj_geom, (self._cor_shift, 0))
151
+ # (src, detector_center, u, v) = (srcX, srcY, srcZ, dX, dY, dZ, uX, uY, uZ, vX, vY, vZ)
152
+ vecs = self.proj_geom["Vectors"]
153
+
154
+ # To adapt the center of rotation:
155
+ # dX = cor_shift * cos(theta) - origin_detector_dist * sin(theta)
156
+ # dY = origin_detector_dist * cos(theta) + cor_shift * sin(theta)
157
+ if self._axis_corrections is not None:
158
+ # should we check that dX and dY match the above formulas ?
159
+ cor_shifts = self._cor_shift + self._axis_corrections
160
+ vecs[:, 3] = cor_shifts * np.cos(self.angles) - self.origin_detector_dist * np.sin(self.angles)
161
+ vecs[:, 4] = self.origin_detector_dist * np.cos(self.angles) + cor_shifts * np.sin(self.angles)
162
+
163
+ # To adapt the z position:
164
+ # Component 2 of vecs is the z coordinate of the source, component 5 is the z component of the detector position
165
+ # We need to re-create the same inclination of the cone beam, thus we need to keep the inclination of the two z positions.
166
+ # The detector is centered on the rotation axis, thus moving it up or down, just moves it out of the reconstruction volume.
167
+ # We can bring back the detector in the correct volume position, by applying a rigid translation of both the detector and the source.
168
+ # The translation is exactly the amount that brought the detector up or down, but in the opposite direction.
169
+ vecs[:, 2] = -self.relative_z_position
170
+
171
+ def _set_output(self, volume):
172
+ if volume is not None:
173
+ expected_shape = self.vol_shape # if not (self._crop_data) else self._output_cropped_shape
174
+ self.cuda.check_array(volume, expected_shape)
175
+ self.cuda.set_array("output", volume)
176
+ if volume is None:
177
+ self.cuda.allocate_array("output", self.vol_shape)
178
+ d_volume = self.cuda.get_array("output")
179
+ z, y, x = d_volume.shape
180
+ self._vol_link = astra.data3d.GPULink(d_volume.ptr, x, y, z, d_volume.strides[-2])
181
+ self._vol_id = astra.data3d.link("-vol", self.vol_geom, self._vol_link)
182
+
183
+ def _set_input(self, sinos):
184
+ self.cuda.check_array(sinos, self.sinos_shape)
185
+ self.cuda.set_array("sinos", sinos) # self.cuda.sinos is now a GPU array
186
+ # TODO don't create new link/proj_id if ptr is the same ?
187
+ # But it seems Astra modifies the input sinogram while doing FDK, so this might be not relevant
188
+ d_sinos = self.cuda.get_array("sinos")
189
+
190
+ # self._proj_data_link = astra.data3d.GPULink(d_sinos.ptr, self.prj_width, self.n_angles, self.n_z, sinos.strides[-2])
191
+ self._proj_data_link = astra.data3d.GPULink(
192
+ d_sinos.ptr, self.prj_width, self.n_angles, self.n_sinos, d_sinos.strides[-2]
193
+ )
194
+ self._proj_id = astra.data3d.link("-sino", self.proj_geom, self._proj_data_link)
195
+
196
+ def _preprocess_data(self):
197
+ d_sinos = self.cuda.sinos
198
+ for i in range(d_sinos.shape[0]):
199
+ self.sino_filter.filter_sino(d_sinos[i], output=d_sinos[i])
200
+
201
+ def _update_reconstruction(self):
202
+ cfg = astra.astra_dict("BP3D_CUDA")
203
+ cfg["ReconstructionDataId"] = self._vol_id
204
+ cfg["ProjectionDataId"] = self._proj_id
205
+ if self._alg_id is not None:
206
+ astra.algorithm.delete(self._alg_id)
207
+ self._alg_id = astra.algorithm.create(cfg)
208
+
209
+ def reconstruct(self, sinos, output=None, relative_z_position=None):
210
+ """
211
+ sinos: numpy.ndarray or pycuda.gpuarray
212
+ Sinograms, with shape (n_sinograms, n_angles, width)
213
+ output: pycuda.gpuarray, optional
214
+ Output array. If not provided, a new numpy array is returned
215
+ relative_z_position: int, optional
216
+ Position of the central slice of the slab, with respect to the full stack of slices.
217
+ By default it is set to zero, meaning that the current slab is assumed in the middle of the stack
218
+ """
219
+ self._create_astra_proj_geometry(relative_z_position)
220
+ self._set_input(sinos)
221
+ self._set_output(output)
222
+ self._preprocess_data()
223
+ self._update_reconstruction()
224
+ astra.algorithm.run(self._alg_id)
225
+ #
226
+ # NB: Could also be done with
227
+ # from astra.experimental import direct_BP3D
228
+ # projector_id = astra.create_projector("cuda3d", self.proj_geom, self.vol_geom, options=None)
229
+ # direct_BP3D(projector_id, self._vol_link, self._proj_data_link)
230
+ #
231
+ result = self.cuda.get_array("output")
232
+ if output is None:
233
+ result = result.get()
234
+ if self.extra_options.get("scale_factor", None) is not None:
235
+ result *= np.float32(self.extra_options["scale_factor"]) # in-place for pycuda
236
+ self.cuda.recover_arrays_references(["sinos", "output"])
237
+ return result
238
+
239
+ def __del__(self):
240
+ if getattr(self, "_alg_id", None) is not None:
241
+ astra.algorithm.delete(self._alg_id)
242
+ if getattr(self, "_vol_id", None) is not None:
243
+ astra.data3d.delete(self._vol_id)
244
+ if getattr(self, "_proj_id", None) is not None:
245
+ astra.data3d.delete(self._proj_id)
@@ -1,10 +1,11 @@
1
+ import logging
1
2
  from math import sqrt
2
3
  import numpy as np
3
4
 
4
5
  from ..cuda.kernel import CudaKernel
5
6
  from ..cuda.processing import CudaProcessing
6
7
  from ..reconstruction.filtering_cuda import CudaSinoFilter
7
- from ..utils import get_cuda_srcfile
8
+ from ..utils import get_cuda_srcfile, updiv
8
9
 
9
10
  try:
10
11
  import astra
@@ -14,6 +15,9 @@ except ImportError:
14
15
  __have_astra__ = False
15
16
 
16
17
 
18
+ _logger = logging.getLogger(__name__)
19
+
20
+
17
21
  class ConebeamReconstructor:
18
22
  """
19
23
  A reconstructor for cone-beam geometry using the astra toolbox.
@@ -159,7 +163,9 @@ class ConebeamReconstructor:
159
163
  def _init_fdk(self, padding_mode, filter_name):
160
164
  self.padding_mode = padding_mode
161
165
  self._use_astra_fdk = bool(self.extra_options.get("use_astra_fdk", True))
162
- self._use_astra_fdk &= padding_mode in ["zeros", "constant", None, "none"]
166
+ if self._use_astra_fdk and padding_mode not in ["zeros", "constant", None, "none"]:
167
+ self._use_astra_fdk = False
168
+ _logger.warning("padding_mode was set to %s, cannot use native astra FDK" % padding_mode)
163
169
  if self._use_astra_fdk:
164
170
  return
165
171
  self.sino_filter = CudaSinoFilter(
@@ -386,12 +392,11 @@ def fdk_preweighting(d_sinos, proj_geom, relative_z_position=0.0, cor_shift=0.0)
386
392
  signature="Piiifffffiii",
387
393
  )
388
394
 
389
- # n_angles, n_z, n_x = d_sinos.shape
390
395
  n_z, n_angles, n_x = d_sinos.shape
391
396
  det_origin = sqrt(proj_geom["DistanceOriginDetector"] ** 2 + cor_shift**2)
392
397
 
393
398
  block = (32, 16, 1)
394
- grid = (((n_x + 32 - 1) // 32) * ((n_z + 32 - 1) // 32), (n_angles + 16 - 1) // 16, 1)
399
+ grid = (updiv(n_x, block[0]), updiv(n_angles, block[1]), 1)
395
400
 
396
401
  preweight_kernel(
397
402
  d_sinos,
@@ -82,7 +82,7 @@ class BackprojectorBase:
82
82
  backend_options: dict, optional
83
83
  OpenCL/Cuda options passed to the OpenCLProcessing or CudaProcessing class.
84
84
 
85
- Other parameters
85
+ Other Parameters
86
86
  -----------------
87
87
  extra_options: dict, optional
88
88
  Dictionary with a set of advanced options. The default are the following:
@@ -354,7 +354,7 @@ class BackprojectorBase:
354
354
 
355
355
  def backproj(self, sino, output=None, do_checks=True):
356
356
  if self.halftomo and self.rot_center < self.dwidth:
357
- self.sino_mult.prepare_sino(sino)
357
+ sino = self.sino_mult.prepare_sino(sino)
358
358
  self._transfer_to_texture(sino)
359
359
  d_slice = self._set_output(output, check=do_checks)
360
360
  self._set_kernel_slice_arg(d_slice)
@@ -7,7 +7,7 @@ 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
12
  def __init__(
13
13
  self,
@@ -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
 
@@ -138,7 +151,7 @@ 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
156
  ngrids = int(math.ceil(self.sino_shape[0] / reductionFactor))
144
157
 
@@ -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))
@@ -32,7 +32,6 @@ class MLEMReconstructor:
32
32
  n_iterations=50,
33
33
  extra_options=None,
34
34
  ):
35
- """ """
36
35
  if not (__have_corrct__):
37
36
  raise ImportError("Need corrct package")
38
37
  self.angles_rad = angles_rad
@@ -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]):
@@ -1,7 +1,9 @@
1
+ import logging
1
2
  import pytest
2
3
  import numpy as np
3
4
  from scipy.ndimage import gaussian_filter, shift
4
5
  from nabu.utils import subdivide_into_overlapping_segment, clip_circle
6
+ from nabu.testutils import __do_long_tests__, generate_tests_scenarios
5
7
 
6
8
  try:
7
9
  import astra
@@ -387,6 +389,77 @@ class TestCone:
387
389
  str(roi)
388
390
  )
389
391
 
392
+ def test_fdk_preweight(self, caplog):
393
+ """
394
+ Check that nabu's FDK pre-weighting give the same results as astra
395
+ """
396
+ shapes = [
397
+ {"n_z": 256, "n_x": 256, "n_a": 500},
398
+ # {"n_z": 250, "n_x": 340, "n_a": 250}, # Astra reconstruction is incorrect in this case!
399
+ ]
400
+ src_orig_dist = 1000
401
+ orig_det_dist = 50
402
+
403
+ rot_centers_from_middle = [0]
404
+ if __do_long_tests__:
405
+ rot_centers_from_middle.extend([10, -15])
406
+
407
+ params_list = generate_tests_scenarios({"shape": shapes, "rot_center": rot_centers_from_middle})
408
+
409
+ for params in params_list:
410
+ n_z = params["shape"]["n_z"]
411
+ n_x = n_y = params["shape"]["n_x"]
412
+ n_a = params["shape"]["n_a"]
413
+ rc = params["rot_center"]
414
+ volume, cone_data = generate_hollow_cube_cone_sinograms(
415
+ vol_shape=(n_z, n_y, n_x),
416
+ n_angles=n_a,
417
+ src_orig_dist=src_orig_dist,
418
+ orig_det_dist=orig_det_dist,
419
+ apply_filter=False,
420
+ rot_center_shift=rc,
421
+ )
422
+
423
+ reconstructor_args = [(n_z, n_a, n_x), src_orig_dist, orig_det_dist]
424
+ reconstructor_kwargs_base = {
425
+ "volume_shape": volume.shape,
426
+ "rot_center": (n_x - 1) / 2 + rc,
427
+ "cuda_options": {"ctx": self.ctx},
428
+ }
429
+ reconstructor_kwargs_astra = {"padding_mode": "zeros", "extra_options": {"use_astra_fdk": True}}
430
+ reconstructor_kwargs_nabu = {"padding_mode": "zeros", "extra_options": {"use_astra_fdk": False}}
431
+ reconstructor_astra = ConebeamReconstructor(
432
+ *reconstructor_args, **{**reconstructor_kwargs_base, **reconstructor_kwargs_astra}
433
+ )
434
+ assert reconstructor_astra._use_astra_fdk is True, "reconstructor_astra should use native astra FDK"
435
+ reconstructor_nabu = ConebeamReconstructor(
436
+ *reconstructor_args, **{**reconstructor_kwargs_base, **reconstructor_kwargs_nabu}
437
+ )
438
+ ref = reconstructor_astra.reconstruct(cone_data)
439
+ res = reconstructor_nabu.reconstruct(cone_data)
440
+
441
+ reconstructor_kwargs_nabu = {"padding_mode": "edges", "extra_options": {"use_astra_fdk": False}}
442
+ cb_ep = ConebeamReconstructor(
443
+ *reconstructor_args, **{**reconstructor_kwargs_base, **reconstructor_kwargs_nabu}
444
+ )
445
+ res_ep = cb_ep.reconstruct(cone_data) # noqa: F841
446
+
447
+ assert np.max(np.abs(res - ref)) < 2e-3, "Wrong FDK results for parameters: %s" % (str(params))
448
+
449
+ # Test with edges padding - only nabu can do that
450
+ reconstructor_kwargs_nabu["padding_mode"] = "edges"
451
+ reconstructor_nabu = ConebeamReconstructor(
452
+ *reconstructor_args, **{**reconstructor_kwargs_base, **reconstructor_kwargs_nabu}
453
+ )
454
+ reconstructor_nabu.reconstruct(cone_data)
455
+ # result is slightly different than "res" in the borders, which is expected
456
+ # it would be good to test it as well, but it's outside of the scope of this test
457
+
458
+ with caplog.at_level(logging.WARNING):
459
+ reconstructor_kwargs_nabu = {"padding_mode": "edges", "extra_options": {"use_astra_fdk": True}}
460
+ ConebeamReconstructor(*reconstructor_args, **{**reconstructor_kwargs_base, **reconstructor_kwargs_nabu})
461
+ assert "cannot use native astra FDK" in caplog.text
462
+
390
463
 
391
464
  def generate_hollow_cube_cone_sinograms(
392
465
  vol_shape,
@@ -403,13 +476,13 @@ def generate_hollow_cube_cone_sinograms(
403
476
  vol_geom = astra.create_vol_geom(n_y, n_x, n_z)
404
477
 
405
478
  prj_width = prj_width or n_x
406
- prj_height = n_z
479
+ # prj_height = n_z
407
480
  angles = np.linspace(0, 2 * np.pi, n_angles, True)
408
481
 
409
- proj_geom = astra.create_proj_geom("cone", 1.0, 1.0, prj_width, prj_width, angles, src_orig_dist, orig_det_dist)
482
+ proj_geom = astra.create_proj_geom("cone", 1.0, 1.0, n_z, prj_width, angles, src_orig_dist, orig_det_dist)
410
483
  if rot_center_shift is not None:
411
484
  proj_geom = astra.geom_postalignment(proj_geom, (-rot_center_shift, 0))
412
- magnification = 1 + orig_det_dist / src_orig_dist
485
+ # magnification = 1 + orig_det_dist / src_orig_dist
413
486
 
414
487
  # hollow cube
415
488
  cube = np.zeros(astra.geom_size(vol_geom), dtype="f")
@@ -139,9 +139,9 @@ class TestDeringer:
139
139
  )
140
140
  def test_vo_deringer(self):
141
141
  deringer = VoDeringer(self.sino.shape)
142
- sino_deringed = deringer.remove_rings_sinogram(self.sino)
142
+ sino_deringed = deringer.remove_rings_sinogram(self.sino) # noqa: F841
143
143
  sinos = np.tile(self.sino, (10, 1, 1))
144
- sinos_deringed = deringer.remove_rings_sinograms(sinos)
144
+ sinos_deringed = deringer.remove_rings_sinograms(sinos) # noqa: F841
145
145
  # TODO check result. The generated test sinogram is "too synthetic" for this kind of deringer
146
146
 
147
147
  @pytest.mark.skipif(
@@ -262,7 +262,7 @@ class TestFBP:
262
262
  # Need to translate the axis a little bit, because of non-centered differentiation.
263
263
  # prepend -> +0.5 ; append -> -0.5
264
264
  B = self._get_backprojector(config, sino_diff.shape, filter_name="hilbert", rot_center=255.5 + 0.5)
265
- rec = self.apply_fbp(config, B, sino_diff)
265
+ rec = self.apply_fbp(config, B, sino_diff) # noqa: F841
266
266
  # Looks good, but all frequencies are not recovered. Use a metric like SSIM or FRC ?
267
267
 
268
268
 
@@ -99,10 +99,36 @@ class TestHalftomo:
99
99
  rot_center = sino.shape[-1] - 1 - self.rot_center
100
100
  return self.test_halftomo_right_side(config, sino=sino, rot_center=rot_center)
101
101
 
102
+ def test_halftomo_plain_backprojection(self, config):
103
+ backprojector = self._get_backprojector(
104
+ config,
105
+ self.sino.shape,
106
+ rot_center=self.rot_center,
107
+ halftomo=True,
108
+ padding_mode="edges",
109
+ extra_options={"centered_axis": True},
110
+ )
111
+ d_sino_filtered = backprojector.sino_filter.filter_sino(self.sino) # device array
112
+ h_sino_filtered = d_sino_filtered.get()
113
+ reference_fbp = backprojector.fbp(self.sino)
114
+
115
+ def _check(rec, array_type):
116
+ assert (
117
+ np.max(np.abs(rec - reference_fbp)) < 1e-7
118
+ ), "Something wrong with halftomo backproj using %s array and configuration %s" % (array_type, str(config))
119
+
120
+ # Test with device array
121
+ rec_from_already_filtered_sino = backprojector.backproj(d_sino_filtered)
122
+ _check(rec_from_already_filtered_sino, "device")
123
+
124
+ # Test with numpy array
125
+ rec_from_already_filtered_sino = backprojector.backproj(h_sino_filtered)
126
+ _check(rec_from_already_filtered_sino, "numpy")
127
+
102
128
  def test_halftomo_cor_outside_fov(self, config):
103
129
  sino = np.ascontiguousarray(self.sino[:, : self.sino.shape[-1] // 2])
104
130
  backprojector = self._get_backprojector(config, sino.shape, rot_center=self.rot_center, halftomo=True)
105
- res = backprojector.fbp(sino)
131
+ res = backprojector.fbp(sino) # noqa: F841
106
132
  # Just check that it runs, but no reference results. Who does this anyway ?!
107
133
 
108
134
  @pytest.mark.skipif(not (__has_pycuda__), reason="Need pycuda")
@@ -1,6 +1,6 @@
1
1
  import pytest
2
2
  import numpy as np
3
- from nabu.testutils import get_data, __do_long_tests__
3
+ from nabu.testutils import get_data
4
4
 
5
5
  from nabu.cuda.utils import __has_pycuda__
6
6
  from nabu.reconstruction.mlem import MLEMReconstructor, __have_corrct__
@@ -27,7 +27,8 @@ class TestMLEM:
27
27
  """These tests test the general MLEM reconstruction algorithm
28
28
  and the behavior of the reconstruction with respect to horizontal shifts.
29
29
  Only horizontal shifts are tested here because vertical shifts are handled outside
30
- the reconstruction object, but in the embedding reconstruction pipeline. See FullFieldReconstructor"""
30
+ the reconstruction object, but in the embedding reconstruction pipeline. See FullFieldReconstructor
31
+ """
31
32
 
32
33
  def _create_MLEM_reconstructor(self, shifts_uv=None):
33
34
  return MLEMReconstructor(
@@ -3,14 +3,17 @@ import pytest
3
3
  from nabu.testutils import get_data
4
4
  from nabu.cuda.utils import __has_pycuda__
5
5
 
6
+ textures_available = False
6
7
  if __has_pycuda__:
7
8
  import pycuda.gpuarray as garray
8
9
 
9
10
  # from pycuda.cumath import fabs
10
11
  from pycuda.elementwise import ElementwiseKernel
11
- from nabu.cuda.utils import get_cuda_context
12
+ from nabu.cuda.utils import get_cuda_context, check_textures_availability
12
13
  from nabu.reconstruction.projection import Projector
13
14
  from nabu.reconstruction.fbp import Backprojector
15
+
16
+ textures_available = check_textures_availability()
14
17
  try:
15
18
  import astra
16
19
 
@@ -30,6 +33,7 @@ def bootstrap(request):
30
33
  cls.ctx = get_cuda_context()
31
34
 
32
35
 
36
+ @pytest.mark.skipif(not (textures_available), reason="Textures not supported")
33
37
  @pytest.mark.skipif(not (__has_pycuda__), reason="Need pycuda for this test")
34
38
  @pytest.mark.usefixtures("bootstrap")
35
39
  class TestProjection:
@@ -63,7 +67,8 @@ class TestProjection:
63
67
  def test_odd_size(self):
64
68
  image = self.image[:511, :]
65
69
  P = Projector(image.shape, self.n_angles - 1)
66
- res = P(image)
70
+ res = P(image) # noqa: F841
71
+ # TODO check
67
72
 
68
73
  @pytest.mark.skipif(not (__has_astra__), reason="Need astra-toolbox for this test")
69
74
  def test_against_astra(self):
@@ -1,4 +1,3 @@
1
- import os.path as path
2
1
  import numpy as np
3
2
  import pytest
4
3
  from nabu.testutils import get_data
@@ -311,15 +311,15 @@ class EDFDatasetAnalyzer(DatasetAnalyzer):
311
311
  # (eg. subsampling, binning, distortion correction...)
312
312
  # (3) The following spawns one reader instance per file, which is not elegant,
313
313
  # but in principle there are typically 1-2 reduced flats in a scan
314
- readers = {k: EDFStackReader([self.raw_flats[k].file_path()], **reader_kwargs) for k in self.raw_flats.keys()}
315
- return {k: readers[k].load_data()[0] for k in self.raw_flats.keys()}
314
+ readers = {k: EDFStackReader([self.raw_flats[k].file_path()], **reader_kwargs) for k in self.raw_flats}
315
+ return {k: readers[k].load_data()[0] for k in self.raw_flats}
316
316
 
317
317
  def get_reduced_darks(self, **reader_kwargs):
318
318
  # See notes in get_reduced_flats() above
319
319
  if self.raw_darks in [None, {}]:
320
320
  raise FileNotFoundError("No reduced dark ('darkend.edf' or 'dark.edf') found in %s" % self.location)
321
- readers = {k: EDFStackReader([self.raw_darks[k].file_path()], **reader_kwargs) for k in self.raw_darks.keys()}
322
- return {k: readers[k].load_data()[0] for k in self.raw_darks.keys()}
321
+ readers = {k: EDFStackReader([self.raw_darks[k].file_path()], **reader_kwargs) for k in self.raw_darks}
322
+ return {k: readers[k].load_data()[0] for k in self.raw_darks}
323
323
 
324
324
  @property
325
325
  def files(self):