nabu 2024.1.9__py3-none-any.whl → 2024.2.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 (151) hide show
  1. nabu/__init__.py +1 -1
  2. nabu/app/bootstrap.py +2 -3
  3. nabu/app/cast_volume.py +4 -2
  4. nabu/app/cli_configs.py +5 -0
  5. nabu/app/composite_cor.py +1 -1
  6. nabu/app/create_distortion_map_from_poly.py +5 -6
  7. nabu/app/diag_to_pix.py +7 -19
  8. nabu/app/diag_to_rot.py +14 -29
  9. nabu/app/double_flatfield.py +32 -44
  10. nabu/app/parse_reconstruction_log.py +3 -0
  11. nabu/app/reconstruct.py +53 -15
  12. nabu/app/reconstruct_helical.py +2 -2
  13. nabu/app/stitching.py +27 -13
  14. nabu/app/tests/test_reduce_dark_flat.py +4 -1
  15. nabu/cuda/kernel.py +11 -2
  16. nabu/cuda/processing.py +2 -2
  17. nabu/cuda/src/cone.cu +77 -0
  18. nabu/cuda/src/hierarchical_backproj.cu +271 -0
  19. nabu/cuda/utils.py +0 -6
  20. nabu/estimation/alignment.py +5 -19
  21. nabu/estimation/cor.py +173 -599
  22. nabu/estimation/cor_sino.py +356 -26
  23. nabu/estimation/focus.py +63 -11
  24. nabu/estimation/tests/test_cor.py +124 -58
  25. nabu/estimation/tests/test_focus.py +6 -6
  26. nabu/estimation/tilt.py +2 -1
  27. nabu/estimation/utils.py +5 -33
  28. nabu/io/__init__.py +1 -1
  29. nabu/io/cast_volume.py +1 -1
  30. nabu/io/reader.py +416 -21
  31. nabu/io/tests/test_readers.py +422 -0
  32. nabu/io/tests/test_writers.py +1 -102
  33. nabu/io/writer.py +4 -433
  34. nabu/opencl/kernel.py +14 -3
  35. nabu/opencl/processing.py +8 -0
  36. nabu/pipeline/config_validators.py +5 -2
  37. nabu/pipeline/datadump.py +12 -5
  38. nabu/pipeline/estimators.py +162 -188
  39. nabu/pipeline/fullfield/chunked.py +168 -92
  40. nabu/pipeline/fullfield/chunked_cuda.py +7 -3
  41. nabu/pipeline/fullfield/computations.py +2 -7
  42. nabu/pipeline/fullfield/dataset_validator.py +0 -4
  43. nabu/pipeline/fullfield/nabu_config.py +37 -13
  44. nabu/pipeline/fullfield/processconfig.py +22 -13
  45. nabu/pipeline/fullfield/reconstruction.py +13 -9
  46. nabu/pipeline/helical/helical_chunked_regridded.py +1 -1
  47. nabu/pipeline/helical/helical_chunked_regridded_cuda.py +1 -0
  48. nabu/pipeline/helical/helical_reconstruction.py +1 -1
  49. nabu/pipeline/params.py +21 -1
  50. nabu/pipeline/processconfig.py +1 -12
  51. nabu/pipeline/reader.py +146 -0
  52. nabu/pipeline/tests/test_estimators.py +44 -72
  53. nabu/pipeline/utils.py +4 -2
  54. nabu/pipeline/writer.py +10 -2
  55. nabu/preproc/ccd_cuda.py +1 -1
  56. nabu/preproc/ctf.py +14 -7
  57. nabu/preproc/ctf_cuda.py +2 -3
  58. nabu/preproc/double_flatfield.py +5 -12
  59. nabu/preproc/double_flatfield_cuda.py +2 -2
  60. nabu/preproc/flatfield.py +5 -1
  61. nabu/preproc/flatfield_cuda.py +5 -1
  62. nabu/preproc/phase.py +24 -73
  63. nabu/preproc/phase_cuda.py +5 -8
  64. nabu/preproc/tests/test_ctf.py +11 -7
  65. nabu/preproc/tests/test_flatfield.py +67 -122
  66. nabu/preproc/tests/test_paganin.py +54 -30
  67. nabu/processing/azim.py +206 -0
  68. nabu/processing/convolution_cuda.py +1 -1
  69. nabu/processing/fft_cuda.py +15 -17
  70. nabu/processing/histogram.py +2 -0
  71. nabu/processing/histogram_cuda.py +2 -1
  72. nabu/processing/kernel_base.py +3 -0
  73. nabu/processing/muladd_cuda.py +1 -0
  74. nabu/processing/padding_opencl.py +1 -1
  75. nabu/processing/roll_opencl.py +1 -0
  76. nabu/processing/rotation_cuda.py +2 -2
  77. nabu/processing/tests/test_fft.py +17 -10
  78. nabu/processing/unsharp_cuda.py +1 -1
  79. nabu/reconstruction/cone.py +104 -40
  80. nabu/reconstruction/fbp.py +3 -0
  81. nabu/reconstruction/fbp_base.py +7 -2
  82. nabu/reconstruction/filtering.py +20 -7
  83. nabu/reconstruction/filtering_cuda.py +7 -1
  84. nabu/reconstruction/hbp.py +424 -0
  85. nabu/reconstruction/mlem.py +99 -0
  86. nabu/reconstruction/reconstructor.py +2 -0
  87. nabu/reconstruction/rings_cuda.py +19 -19
  88. nabu/reconstruction/sinogram_cuda.py +1 -0
  89. nabu/reconstruction/sinogram_opencl.py +3 -1
  90. nabu/reconstruction/tests/test_cone.py +10 -5
  91. nabu/reconstruction/tests/test_deringer.py +7 -6
  92. nabu/reconstruction/tests/test_fbp.py +124 -10
  93. nabu/reconstruction/tests/test_filtering.py +13 -11
  94. nabu/reconstruction/tests/test_halftomo.py +30 -4
  95. nabu/reconstruction/tests/test_mlem.py +91 -0
  96. nabu/reconstruction/tests/test_reconstructor.py +8 -3
  97. nabu/resources/dataset_analyzer.py +142 -92
  98. nabu/resources/gpu.py +1 -0
  99. nabu/resources/nxflatfield.py +134 -125
  100. nabu/resources/templates/id16a_fluo.conf +42 -0
  101. nabu/resources/tests/test_extract.py +10 -0
  102. nabu/resources/tests/test_nxflatfield.py +2 -2
  103. nabu/stitching/alignment.py +80 -24
  104. nabu/stitching/config.py +105 -68
  105. nabu/stitching/definitions.py +1 -0
  106. nabu/stitching/frame_composition.py +68 -60
  107. nabu/stitching/overlap.py +91 -51
  108. nabu/stitching/single_axis_stitching.py +32 -0
  109. nabu/stitching/slurm_utils.py +6 -6
  110. nabu/stitching/stitcher/__init__.py +0 -0
  111. nabu/stitching/stitcher/base.py +124 -0
  112. nabu/stitching/stitcher/dumper/__init__.py +3 -0
  113. nabu/stitching/stitcher/dumper/base.py +94 -0
  114. nabu/stitching/stitcher/dumper/postprocessing.py +356 -0
  115. nabu/stitching/stitcher/dumper/preprocessing.py +60 -0
  116. nabu/stitching/stitcher/post_processing.py +555 -0
  117. nabu/stitching/stitcher/pre_processing.py +1068 -0
  118. nabu/stitching/stitcher/single_axis.py +484 -0
  119. nabu/stitching/stitcher/stitcher.py +0 -0
  120. nabu/stitching/stitcher/y_stitcher.py +13 -0
  121. nabu/stitching/stitcher/z_stitcher.py +45 -0
  122. nabu/stitching/stitcher_2D.py +278 -0
  123. nabu/stitching/tests/test_config.py +12 -37
  124. nabu/stitching/tests/test_frame_composition.py +33 -59
  125. nabu/stitching/tests/test_overlap.py +149 -7
  126. nabu/stitching/tests/test_utils.py +1 -1
  127. nabu/stitching/tests/test_y_preprocessing_stitching.py +132 -0
  128. nabu/stitching/tests/{test_z_stitching.py → test_z_postprocessing_stitching.py} +167 -561
  129. nabu/stitching/tests/test_z_preprocessing_stitching.py +431 -0
  130. nabu/stitching/utils/__init__.py +1 -0
  131. nabu/stitching/utils/post_processing.py +281 -0
  132. nabu/stitching/utils/tests/test_post-processing.py +21 -0
  133. nabu/stitching/{utils.py → utils/utils.py} +79 -52
  134. nabu/stitching/y_stitching.py +27 -0
  135. nabu/stitching/z_stitching.py +32 -2263
  136. nabu/testutils.py +1 -152
  137. nabu/thirdparty/tomocupy_remove_stripe.py +43 -9
  138. nabu/utils.py +158 -61
  139. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/METADATA +10 -3
  140. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/RECORD +144 -121
  141. nabu/io/tiffwriter_zmm.py +0 -99
  142. nabu/pipeline/fallback_utils.py +0 -149
  143. nabu/pipeline/helical/tests/test_accumulator.py +0 -158
  144. nabu/pipeline/helical/tests/test_pipeline_elements_full.py +0 -355
  145. nabu/pipeline/helical/tests/test_strategy.py +0 -61
  146. nabu/pipeline/helical/utils.py +0 -51
  147. nabu/pipeline/tests/test_chunk_reader.py +0 -74
  148. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/LICENSE +0 -0
  149. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/WHEEL +0 -0
  150. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/entry_points.txt +0 -0
  151. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/top_level.txt +0 -0
@@ -236,7 +236,7 @@ class TestCone:
236
236
  err_median_profile = np.median(diff, axis=(-1, -2))
237
237
 
238
238
  assert np.max(err_max_profile) < 2e-3
239
- assert np.max(err_median_profile) < 5e-6
239
+ assert np.max(err_median_profile) < 5.1e-6
240
240
 
241
241
  def test_reconstruction_horizontal_translations(self):
242
242
  n_z = n_y = n_x = 256
@@ -273,7 +273,7 @@ class TestCone:
273
273
  # Error tolerance has to be higher for these shifts.
274
274
  for shift_type, shifts, err_tol in [
275
275
  ("integer shifts", shifts_int, 5e-3),
276
- ("float shifts", shifts_float, 1.4e-1),
276
+ ("float shifts", shifts_float, 1.5e-1),
277
277
  ]:
278
278
  cone_data_shifted = np.zeros_like(cone_data)
279
279
  [shift(cone_data[:, i, :], (0, shifts[i]), output=cone_data_shifted[:, i, :]) for i in range(n_a)]
@@ -281,8 +281,8 @@ class TestCone:
281
281
  # Reconstruct with horizontal shifts
282
282
  cone_reconstructor_with_correction = ConebeamReconstructor(
283
283
  *reconstructor_args,
284
- axis_correction=shifts,
285
284
  **reconstructor_kwargs,
285
+ extra_options={"axis_correction": -shifts},
286
286
  )
287
287
 
288
288
  rec_with_correction = cone_reconstructor_with_correction.reconstruct(cone_data_shifted)
@@ -330,7 +330,6 @@ class TestCone:
330
330
 
331
331
  metric = lambda img: np.max(np.abs(clip_circle(img, radius=int(0.85 * 128))))
332
332
  error_profile = np.array([metric(rec[i] - rec_z[i]) for i in range(n_z)])
333
- assert error_profile.max() < 3e-2, "Max error for padding=%s is too high" % padding_mode
334
333
 
335
334
  # import matplotlib.pyplot as plt
336
335
  # plt.figure()
@@ -338,6 +337,12 @@ class TestCone:
338
337
  # plt.legend([padding_mode])
339
338
  # plt.show()
340
339
 
340
+ assert error_profile.max() < 3.1e-2, "Max error for padding=%s is too high" % padding_mode
341
+ if padding_mode != "zeros":
342
+ assert not (np.allclose(rec[n_z // 2], rec_z[n_z // 2])), (
343
+ "Reconstruction should be different when padding_mode=%s" % padding_mode
344
+ )
345
+
341
346
  def test_roi(self):
342
347
  n_z = n_y = n_x = 256
343
348
  n_a = 500
@@ -367,7 +372,7 @@ class TestCone:
367
372
  ref = cone_reconstructor_full.reconstruct(cone_data)
368
373
 
369
374
  # roi is in the form (start_x, end_x, start_y, end_y)
370
- for roi in ((20, -20, 10, -10), (0, n_x, 0, n_y), (15, -15, 15, -15)):
375
+ for roi in ((20, -20, 10, -10), (0, n_x, 0, n_y), (50, -50, 15, -15)):
371
376
  # convert negative indices
372
377
  start_x, end_x, start_y, end_y = roi
373
378
  if start_y < 0:
@@ -1,7 +1,7 @@
1
1
  import numpy as np
2
2
  import pytest
3
3
  from nabu.reconstruction.rings_cuda import CudaSinoMeanDeringer
4
- from nabu.testutils import compare_arrays, get_data, generate_tests_scenarios, __do_long_tests__, __do_large_mem_tests__
4
+ from nabu.testutils import compare_arrays, get_data, generate_tests_scenarios, __do_long_tests__
5
5
  from nabu.reconstruction.rings import MunchDeringer, SinoMeanDeringer, VoDeringer, __has_algotom__
6
6
  from nabu.thirdparty.pore3d_deringer_munch import munchetal_filter
7
7
  from nabu.cuda.utils import __has_pycuda__, get_cuda_context
@@ -9,11 +9,12 @@ from nabu.cuda.utils import __has_pycuda__, get_cuda_context
9
9
  if __has_pycuda__:
10
10
  import pycuda.gpuarray as garray
11
11
  from nabu.processing.fft_cuda import get_available_fft_implems
12
+ from nabu.thirdparty.tomocupy_remove_stripe import __have_tomocupy_deringer__
13
+
12
14
  from nabu.reconstruction.rings_cuda import (
13
15
  CudaMunchDeringer,
14
16
  can_use_cuda_deringer,
15
17
  CudaVoDeringer,
16
- __have_tomocupy_deringer__,
17
18
  )
18
19
 
19
20
  __has_cuda_deringer__ = can_use_cuda_deringer()
@@ -27,7 +28,7 @@ fw_scenarios = generate_tests_scenarios(
27
28
  "sigma": [1.0],
28
29
  "wname": ["db15"],
29
30
  "padding": [(100, 100)],
30
- "fft_implem": ["skcuda"],
31
+ "fft_implem": ["vkfft"],
31
32
  }
32
33
  )
33
34
  if __do_long_tests__:
@@ -58,7 +59,7 @@ def bootstrap(request):
58
59
 
59
60
 
60
61
  @pytest.mark.usefixtures("bootstrap")
61
- class TestMunchDeringer:
62
+ class TestDeringer:
62
63
  @staticmethod
63
64
  def add_stripes_to_sino(sino, rings_desc):
64
65
  """
@@ -144,8 +145,8 @@ class TestMunchDeringer:
144
145
  # TODO check result. The generated test sinogram is "too synthetic" for this kind of deringer
145
146
 
146
147
  @pytest.mark.skipif(
147
- not (__have_tomocupy_deringer__ and __do_large_mem_tests__),
148
- reason="Need cupy for this test, and use NABU_LARGE_MEM_TESTS",
148
+ not (__have_tomocupy_deringer__),
149
+ reason="Need cupy for this test",
149
150
  )
150
151
  def test_cuda_vo_deringer(self):
151
152
  # Beware, this deringer seems to be buggy for "too-small" sinograms
@@ -15,6 +15,7 @@ __has_pyopencl__ = __has_pyopencl__ and has_vkfft_cl()
15
15
 
16
16
  if __has_pycuda__:
17
17
  from nabu.reconstruction.fbp import CudaBackprojector
18
+ from nabu.reconstruction.hbp import HierarchicalBackprojector
18
19
  if __has_pyopencl__:
19
20
  from nabu.reconstruction.fbp_opencl import OpenCLBackprojector
20
21
 
@@ -50,12 +51,13 @@ def bootstrap(request):
50
51
  cls.cuda_ctx.pop()
51
52
 
52
53
 
54
+ def clip_to_inner_circle(img, radius_factor=0.99, out_value=0):
55
+ radius = int(radius_factor * max(img.shape) / 2)
56
+ return clip_circle(img, radius=radius, out_value=out_value)
57
+
58
+
53
59
  @pytest.mark.usefixtures("bootstrap")
54
60
  class TestFBP:
55
- @staticmethod
56
- def clip_to_inner_circle(img, radius_factor=0.99):
57
- radius = int(radius_factor * max(img.shape) / 2)
58
- return clip_circle(img, radius=radius)
59
61
 
60
62
  def _get_backprojector(self, config, *bp_args, **bp_kwargs):
61
63
  if config["backend"] == "cuda":
@@ -96,7 +98,7 @@ class TestFBP:
96
98
  B = self._get_backprojector(config, (500, 512))
97
99
  res = self.apply_fbp(config, B, self.sino_512)
98
100
 
99
- delta_clipped = self.clip_to_inner_circle(res - self.ref_512)
101
+ delta_clipped = clip_to_inner_circle(res - self.ref_512)
100
102
  err_max = np.max(np.abs(delta_clipped))
101
103
 
102
104
  assert err_max < self.tol, "Something wrong with config=%s" % (str(config))
@@ -110,7 +112,7 @@ class TestFBP:
110
112
  res = self.apply_fbp(config, B, self.sino_511)
111
113
  ref = self.ref_512[:-1, :-1]
112
114
 
113
- delta_clipped = self.clip_to_inner_circle(res - ref)
115
+ delta_clipped = clip_to_inner_circle(res - ref)
114
116
  err_max = np.max(np.abs(delta_clipped))
115
117
 
116
118
  assert err_max < self.tol, "Something wrong with config=%s" % (str(config))
@@ -191,12 +193,24 @@ class TestFBP:
191
193
  config, sino.shape, rot_center=rot_center, extra_options={"clip_outer_circle": False}
192
194
  )
193
195
  res_noclip = B0.fbp(sino)
194
- ref = self.clip_to_inner_circle(res_noclip, radius_factor=1)
195
-
196
+ ref = clip_to_inner_circle(res_noclip, radius_factor=1)
196
197
  abs_diff = np.abs(res - ref)
197
198
  err_max = np.max(abs_diff)
198
199
  assert err_max < tol, "Max error is too high for rot_center=%s ; %s" % (str(rot_center), str(config))
199
200
 
201
+ # Test with custom outer circle value
202
+ B1 = self._get_backprojector(
203
+ config,
204
+ sino.shape,
205
+ rot_center=rot_center,
206
+ extra_options={"clip_outer_circle": True, "outer_circle_value": np.nan},
207
+ )
208
+ res1 = self.apply_fbp(config, B1, sino)
209
+ ref1 = clip_to_inner_circle(res_noclip, radius_factor=1, out_value=np.nan)
210
+ abs_diff1 = np.abs(res1 - ref1)
211
+ err_max1 = np.nanmax(abs_diff1)
212
+ assert err_max1 < tol, "Max error is too high for rot_center=%s ; %s" % (str(rot_center), str(config))
213
+
200
214
  @pytest.mark.parametrize("config", scenarios)
201
215
  def test_fbp_centered_axis(self, config):
202
216
  """
@@ -213,7 +227,7 @@ class TestFBP:
213
227
  B = self._get_backprojector(config, sino.shape, rot_center=rot_center, extra_options={"centered_axis": True})
214
228
  res = self.apply_fbp(config, B, sino)
215
229
  # The outside region (outer circle) is different as "res" is a wider slice
216
- diff = self.clip_to_inner_circle(res[50:-50, 50:-50] - ref)
230
+ diff = clip_to_inner_circle(res[50:-50, 50:-50] - ref)
217
231
  err_max = np.max(np.abs(diff))
218
232
  assert err_max < 5e-2, "centered_axis without clip_circle: something wrong"
219
233
 
@@ -228,7 +242,7 @@ class TestFBP:
228
242
  },
229
243
  )
230
244
  res2 = self.apply_fbp(config, B, sino)
231
- diff = res2 - self.clip_to_inner_circle(res, radius_factor=1)
245
+ diff = res2 - clip_to_inner_circle(res, radius_factor=1)
232
246
  err_max = np.max(np.abs(diff))
233
247
  assert err_max < 1e-5, "centered_axis with clip_circle: something wrong"
234
248
 
@@ -250,3 +264,103 @@ class TestFBP:
250
264
  B = self._get_backprojector(config, sino_diff.shape, filter_name="hilbert", rot_center=255.5 + 0.5)
251
265
  rec = self.apply_fbp(config, B, sino_diff)
252
266
  # Looks good, but all frequencies are not recovered. Use a metric like SSIM or FRC ?
267
+
268
+
269
+ @pytest.mark.skipif(not (__has_pycuda__), reason="Need pycuda for using HBP")
270
+ @pytest.mark.usefixtures("bootstrap")
271
+ class TestHBP:
272
+
273
+ def _compare_to_reference(self, res, ref, err_msg="", radius_factor=0.9, rel_tol=0.02):
274
+ delta_clipped = clip_to_inner_circle(res - ref, radius_factor=radius_factor)
275
+ err_max = np.max(np.abs(delta_clipped))
276
+ err_max_rel = err_max / ref.max()
277
+ assert err_max_rel < rel_tol, err_msg
278
+
279
+ def test_hbp_simple(self):
280
+ B = HierarchicalBackprojector(self.sino_512.shape)
281
+ res = B.fbp(self.sino_512)
282
+ self._compare_to_reference(res, self.ref_512)
283
+
284
+ def test_hbp_input_output(self):
285
+ B = HierarchicalBackprojector(self.sino_512.shape)
286
+
287
+ d_sino = B._processing.to_device("d_sino2", self.sino_512)
288
+ d_slice = B._processing.allocate_array("d_slice2", self.ref_512.shape)
289
+ h_slice = np.zeros_like(self.ref_512)
290
+
291
+ # in: host, out: host (not provided)
292
+ # see test above
293
+
294
+ # in: host, out: host (provided)
295
+ res = B.fbp(self.sino_512, output=h_slice)
296
+ self._compare_to_reference(h_slice, self.ref_512, err_msg="in: host, out: host (provided)")
297
+ h_slice.fill(0)
298
+
299
+ # in: host, out: device
300
+ res = B.fbp(self.sino_512, output=d_slice)
301
+ self._compare_to_reference(d_slice.get(), self.ref_512, err_msg="in: host, out: device")
302
+ d_slice.fill(0)
303
+
304
+ # in: device, out: host (not provided)
305
+ res = B.fbp(d_sino)
306
+ self._compare_to_reference(res, self.ref_512, err_msg="in: device, out: host (not provided)")
307
+
308
+ # in: device, out: host (provided)
309
+ res = B.fbp(d_sino, output=h_slice)
310
+ self._compare_to_reference(h_slice, self.ref_512, err_msg="in: device, out: host (provided)")
311
+ h_slice.fill(0)
312
+
313
+ # in: device, out: device
314
+ res = B.fbp(d_sino, output=d_slice)
315
+ self._compare_to_reference(d_slice.get(), self.ref_512, err_msg="in: device, out: device")
316
+ d_slice.fill(0)
317
+
318
+ def test_hbp_cor(self):
319
+ """
320
+ Test HBP with various sinogram shapes, obtained by truncating horizontally the original sinogram.
321
+ The Center of rotation is always 255.5 (the one of original sinogram), so it also tests reconstruction with a shifted CoR.
322
+ """
323
+ for crop in [1, 2, 5, 10]:
324
+ sino = np.ascontiguousarray(self.sino_512[:, :-crop])
325
+ B = HierarchicalBackprojector(sino.shape, rot_center=255.5)
326
+ res = B.fbp(sino)
327
+
328
+ # HBP always uses "centered_axis=1", so we cannot compare non-integer shifts
329
+ if crop % 2 == 0:
330
+ ref = self.ref_512[crop // 2 : -crop // 2, crop // 2 : -crop // 2]
331
+ self._compare_to_reference(res, ref, radius_factor=0.95, rel_tol=0.02)
332
+
333
+ def test_hbp_clip_circle(self):
334
+ B_clip = HierarchicalBackprojector(self.sino_512.shape, extra_options={"clip_outer_circle": True})
335
+ B_noclip = HierarchicalBackprojector(self.sino_512.shape, extra_options={"clip_outer_circle": False})
336
+ res_clip = B_clip.fbp(self.sino_512)
337
+ res_noclip = B_noclip.fbp(self.sino_512)
338
+ self._compare_to_reference(res_clip, clip_to_inner_circle(res_noclip, radius_factor=1), "clip_circle")
339
+
340
+ def test_hbp_axis_corr(self):
341
+ sino = self.sino_512
342
+
343
+ # Create a sinogram with a drift in the rotation axis
344
+ def create_drifted_sino(sino, drifts):
345
+ out = np.zeros_like(sino)
346
+ for i in range(sino.shape[0]):
347
+ out[i] = shift(sino[i], drifts[i])
348
+ return out
349
+
350
+ drifts = np.linspace(0, 20, sino.shape[0])
351
+ sino = create_drifted_sino(sino, drifts)
352
+
353
+ B = HierarchicalBackprojector(sino.shape, extra_options={"axis_correction": drifts})
354
+ res = B.fbp(sino)
355
+
356
+ # Max error is relatively high, migh be due to interpolation of scipy shift in sinogram
357
+ self._compare_to_reference(res, self.ref_512, radius_factor=0.95, rel_tol=0.04, err_msg="axis_corr")
358
+
359
+ @pytest.mark.skipif(not (__do_long_tests__), reason="need NABU_LONG_TESTS=1 for this test")
360
+ def test_hbp_scale_factor(self):
361
+ scale_factor = 0.03125
362
+ B_scaled = HierarchicalBackprojector(self.sino_512.shape, extra_options={"scale_factor": scale_factor})
363
+ B_unscaled = HierarchicalBackprojector(self.sino_512.shape)
364
+ res_scaled = B_scaled.fbp(self.sino_512)
365
+ res_unscaled = B_unscaled.fbp(self.sino_512)
366
+ self._compare_to_reference(res_scaled, res_unscaled * scale_factor, rel_tol=1e-7, err_msg="scale_factor")
@@ -25,6 +25,7 @@ tests_scenarios = generate_tests_scenarios(
25
25
  "filter_name": filters_to_test,
26
26
  "padding_mode": padding_modes_to_test,
27
27
  "output_provided": [True, False],
28
+ "truncated_sino": [True, False],
28
29
  }
29
30
  )
30
31
 
@@ -33,12 +34,16 @@ tests_scenarios = generate_tests_scenarios(
33
34
  def bootstrap(request):
34
35
  cls = request.cls
35
36
  cls.sino = get_data("mri_sino500.npz")["data"]
37
+ cls.sino_truncated = np.ascontiguousarray(cls.sino[:, 160:-160])
38
+
36
39
  if __has_pycuda__:
37
40
  cls.ctx_cuda = get_cuda_context(cleanup_at_exit=False)
38
41
  cls.sino_cuda = garray.to_gpu(cls.sino)
42
+ cls.sino_truncated_cuda = garray.to_gpu(cls.sino_truncated)
39
43
  if __has_pyopencl__:
40
44
  cls.cl = OpenCLProcessing(device_type="all")
41
45
  cls.sino_cl = parray.to_device(cls.cl.queue, cls.sino)
46
+ cls.sino_truncated_cl = parray.to_device(cls.cl.queue, cls.sino_truncated)
42
47
 
43
48
  yield
44
49
 
@@ -50,7 +55,7 @@ def bootstrap(request):
50
55
  class TestSinoFilter:
51
56
  @pytest.mark.parametrize("config", tests_scenarios)
52
57
  def test_filter(self, config):
53
- sino = self.sino
58
+ sino = self.sino if not (config["truncated_sino"]) else self.sino_truncated
54
59
 
55
60
  sino_filter = SinoFilter(
56
61
  sino.shape,
@@ -69,12 +74,13 @@ class TestSinoFilter:
69
74
  sino, sino_filter.dwidth_padded, filter_name=config["filter_name"], padding_mode=config["padding_mode"]
70
75
  )
71
76
 
72
- assert np.allclose(res, ref, atol=1e-6)
77
+ assert np.allclose(res, ref, atol=4e-6)
73
78
 
74
79
  @pytest.mark.skipif(not (__has_pycuda__), reason="Need Cuda + pycuda to use CudaSinoFilter")
75
80
  @pytest.mark.parametrize("config", tests_scenarios)
76
81
  def test_cuda_filter(self, config):
77
- sino = self.sino_cuda
82
+ sino = self.sino_cuda if not (config["truncated_sino"]) else self.sino_truncated_cuda
83
+ h_sino = self.sino if not (config["truncated_sino"]) else self.sino_truncated
78
84
 
79
85
  sino_filter = CudaSinoFilter(
80
86
  sino.shape,
@@ -91,14 +97,9 @@ class TestSinoFilter:
91
97
  assert id(res) == id(output), "when providing output, return value must not change"
92
98
 
93
99
  ref = filter_sinogram(
94
- self.sino, sino_filter.dwidth_padded, filter_name=config["filter_name"], padding_mode=config["padding_mode"]
100
+ h_sino, sino_filter.dwidth_padded, filter_name=config["filter_name"], padding_mode=config["padding_mode"]
95
101
  )
96
102
 
97
- if not np.allclose(res.get(), ref, atol=6e-5):
98
- from spire.utils import ims
99
-
100
- ims([res.get(), ref, res.get() - ref])
101
-
102
103
  assert np.allclose(res.get(), ref, atol=6e-5), "test_cuda_filter: something wrong with config=%s" % (
103
104
  str(config)
104
105
  )
@@ -108,7 +109,8 @@ class TestSinoFilter:
108
109
  )
109
110
  @pytest.mark.parametrize("config", tests_scenarios)
110
111
  def test_opencl_filter(self, config):
111
- sino = self.sino_cl
112
+ sino = self.sino_cl if not (config["truncated_sino"]) else self.sino_truncated_cl
113
+ h_sino = self.sino if not (config["truncated_sino"]) else self.sino_truncated
112
114
 
113
115
  sino_filter = OpenCLSinoFilter(
114
116
  sino.shape,
@@ -125,7 +127,7 @@ class TestSinoFilter:
125
127
  assert id(res) == id(output), "when providing output, return value must not change"
126
128
 
127
129
  ref = filter_sinogram(
128
- self.sino, sino_filter.dwidth_padded, filter_name=config["filter_name"], padding_mode=config["padding_mode"]
130
+ h_sino, sino_filter.dwidth_padded, filter_name=config["filter_name"], padding_mode=config["padding_mode"]
129
131
  )
130
132
 
131
133
  assert np.allclose(res.get(), ref, atol=6e-5), "test_opencl_filter: something wrong with config=%s" % (
@@ -1,16 +1,21 @@
1
1
  import numpy as np
2
2
  import pytest
3
+ from nabu.processing.fft_cuda import get_available_fft_implems
3
4
  from nabu.testutils import get_data, generate_tests_scenarios, compare_shifted_images
4
- from nabu.cuda.utils import get_cuda_context, __has_pycuda__, __has_cufft__
5
+ from nabu.cuda.utils import get_cuda_context, __has_pycuda__
5
6
  from nabu.opencl.utils import get_opencl_context, __has_pyopencl__
6
- from nabu.processing.fft_opencl import has_vkfft as has_vkfft_cl
7
7
  from nabu.thirdparty.algotom_convert_sino import extend_sinogram
8
8
 
9
+ __has_cufft__ = False
10
+ if __has_pycuda__:
11
+ avail_fft = get_available_fft_implems()
12
+ __has_cufft__ = len(avail_fft) > 0
9
13
  __has_pycuda__ = __has_pycuda__ and __has_cufft__ # need both for using Cuda backprojector
10
- __has_pyopencl__ = __has_pyopencl__ and has_vkfft_cl()
14
+
11
15
 
12
16
  if __has_pycuda__:
13
17
  from nabu.reconstruction.fbp import CudaBackprojector
18
+ from nabu.reconstruction.hbp import HierarchicalBackprojector
14
19
  if __has_pyopencl__:
15
20
  from nabu.reconstruction.fbp_opencl import OpenCLBackprojector
16
21
 
@@ -37,7 +42,7 @@ class TestHalftomo:
37
42
  def _get_backprojector(self, config, *bp_args, **bp_kwargs):
38
43
  if config["backend"] == "cuda":
39
44
  if not (__has_pycuda__):
40
- pytest.skip("Need pycuda + scikit-cuda")
45
+ pytest.skip("Need pycuda + scikit-cuda or vkfft")
41
46
  Backprojector = CudaBackprojector
42
47
  ctx = self.cuda_ctx
43
48
  else:
@@ -99,3 +104,24 @@ class TestHalftomo:
99
104
  backprojector = self._get_backprojector(config, sino.shape, rot_center=self.rot_center, halftomo=True)
100
105
  res = backprojector.fbp(sino)
101
106
  # Just check that it runs, but no reference results. Who does this anyway ?!
107
+
108
+ @pytest.mark.skipif(not (__has_pycuda__), reason="Need pycuda")
109
+ def test_hbp_halftomo(self, config):
110
+ if config["backend"] == "opencl":
111
+ pytest.skip("No HBP available in OpenCL")
112
+ B = HierarchicalBackprojector(self.sino.shape, halftomo=True, rot_center=self.rot_center, padding_mode="edge")
113
+ res = B.fbp(self.sino)
114
+
115
+ sino_extended, rot_center_ext = extend_sinogram(self.sino, self.rot_center, apply_log=False)
116
+ sino_extended *= 2 # compat. with nabu normalization
117
+ B_extended = HierarchicalBackprojector(
118
+ sino_extended.shape,
119
+ rot_center=rot_center_ext,
120
+ padding_mode="edge",
121
+ angles=np.linspace(0, 2 * np.pi, self.sino.shape[0], True),
122
+ )
123
+ res_e = B_extended.fbp(sino_extended)
124
+
125
+ # see notes in test_halftomo_right_side()
126
+ metric, upper_bound = compare_shifted_images(res, res_e, return_upper_bound=True)
127
+ assert metric < 5, "Something wrong for halftomo with HBP"
@@ -0,0 +1,91 @@
1
+ import pytest
2
+ import numpy as np
3
+ from nabu.testutils import get_data, __do_long_tests__
4
+
5
+ from nabu.cuda.utils import __has_pycuda__
6
+ from nabu.reconstruction.mlem import MLEMReconstructor, __have_corrct__
7
+
8
+
9
+ @pytest.fixture(scope="class")
10
+ def bootstrap(request):
11
+ cls = request.cls
12
+ datafile = get_data("sl_mlem.npz")
13
+ cls.data = datafile["data"]
14
+ cls.angles_rad = datafile["angles_rad"]
15
+ cls.random_u_shifts = datafile["random_u_shifts"]
16
+ cls.ref_rec_noshifts = datafile["ref_rec_noshifts"]
17
+ cls.ref_rec_shiftsu = datafile["ref_rec_shiftsu"]
18
+ cls.ref_rec_u_rand = datafile["ref_rec_u_rand"]
19
+ cls.ref_rec_shiftsv = datafile["ref_rec_shiftsv"]
20
+ # cls.ref_rec_v_rand = datafile["ref_rec_v_rand"]
21
+ cls.tol = 2e-4
22
+
23
+
24
+ @pytest.mark.skipif(not (__has_pycuda__ and __have_corrct__), reason="Need pycuda and corrct for this test")
25
+ @pytest.mark.usefixtures("bootstrap")
26
+ class TestMLEM:
27
+ """These tests test the general MLEM reconstruction algorithm
28
+ and the behavior of the reconstruction with respect to horizontal shifts.
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"""
31
+
32
+ def _create_MLEM_reconstructor(self, shifts_uv=None):
33
+ return MLEMReconstructor(
34
+ self.data.shape, -self.angles_rad, shifts_uv, cor=0.0, n_iterations=10 # mind the sign
35
+ )
36
+
37
+ def test_simple_mlem_recons(self):
38
+ R = self._create_MLEM_reconstructor()
39
+ rec = R.reconstruct(self.data)
40
+ delta = np.abs(rec[:, ::-1, :] - self.ref_rec_noshifts)
41
+ assert np.max(delta) < self.tol
42
+
43
+ def test_mlem_recons_with_u_shifts(self):
44
+ shifts = np.zeros((len(self.angles_rad), 2))
45
+ shifts[:, 0] = -5
46
+ R = self._create_MLEM_reconstructor(shifts)
47
+ rec = R.reconstruct(self.data)
48
+ delta = np.abs(rec[:, ::-1] - self.ref_rec_shiftsu)
49
+ assert np.max(delta) < self.tol
50
+
51
+ def test_mlem_recons_with_random_u_shifts(self):
52
+ R = self._create_MLEM_reconstructor(self.random_u_shifts)
53
+ rec = R.reconstruct(self.data)
54
+ delta = np.abs(rec[:, ::-1] - self.ref_rec_u_rand)
55
+ assert np.max(delta) < self.tol
56
+
57
+ def test_mlem_recons_with_constant_v_shifts(self):
58
+ from nabu.preproc.shift import VerticalShift
59
+
60
+ shifts = np.zeros((len(self.angles_rad), 2))
61
+ shifts[:, 1] = -20
62
+
63
+ nv, n_angles, nu = self.data.shape
64
+ radios_movements = VerticalShift(
65
+ (n_angles, nv, nu), -shifts[:, 1]
66
+ ) # Minus sign here mimics what is done in the pipeline.
67
+ tmp_in = np.swapaxes(self.data, 0, 1).copy()
68
+ tmp_out = np.zeros_like(tmp_in)
69
+ radios_movements.apply_vertical_shifts(tmp_in, list(range(n_angles)), output=tmp_out)
70
+ data = np.swapaxes(tmp_out, 0, 1).copy()
71
+
72
+ R = self._create_MLEM_reconstructor(shifts)
73
+ rec = R.reconstruct(data)
74
+
75
+ axslice = 120
76
+ trslice = 84
77
+ axslice1 = self.ref_rec_shiftsv[axslice]
78
+ axslice2 = rec[axslice, ::-1]
79
+ trslice1 = self.ref_rec_shiftsv[trslice]
80
+ trslice2 = rec[trslice, ::-1]
81
+ # delta = np.abs(rec[:, ::-1] - self.ref_rec_shiftsv)
82
+ delta_ax = np.abs(axslice1 - axslice2)
83
+ delta_tr = np.abs(trslice1 - trslice2)
84
+ assert max(np.max(delta_ax), np.max(delta_tr)) < self.tol
85
+
86
+ @pytest.mark.skip(reason="No valid reference reconstruction for this test.")
87
+ def test_mlem_recons_with_random_v_shifts(self):
88
+ """NOT YET IMPLEMENTED.
89
+ This is a temporary version due to unpexcted behavior of CorrCT/Astra to
90
+ compute a reference implementation. See [question on Astra's github](https://github.com/astra-toolbox/astra-toolbox/discussions/520).
91
+ """
@@ -1,13 +1,18 @@
1
1
  import numpy as np
2
2
  import pytest
3
+ from nabu.processing.fft_cuda import get_available_fft_implems
3
4
  from nabu.testutils import (
4
5
  get_big_data,
5
6
  __big_testdata_dir__,
6
- compare_arrays,
7
7
  generate_tests_scenarios,
8
8
  __do_long_tests__,
9
9
  )
10
- from nabu.cuda.utils import __has_pycuda__, __has_cufft__, get_cuda_context
10
+ from nabu.cuda.utils import __has_pycuda__, get_cuda_context
11
+
12
+ __has_cufft__ = False
13
+ if __has_pycuda__:
14
+ avail_fft = get_available_fft_implems()
15
+ __has_cufft__ = len(avail_fft) > 0
11
16
 
12
17
  __has_cuda_fbp__ = __has_cufft__ and __has_pycuda__
13
18
  if __has_cuda_fbp__:
@@ -43,7 +48,7 @@ def bootstrap(request):
43
48
  )
44
49
  @pytest.mark.usefixtures("bootstrap")
45
50
  class TestReconstructor:
46
- @pytest.mark.skipif(not (__has_cuda_fbp__), reason="need pycuda and scikit-cuda")
51
+ @pytest.mark.skipif(not (__has_cuda_fbp__), reason="need pycuda and (scikit-cuda or vkfft)")
47
52
  @pytest.mark.parametrize("config", scenarios)
48
53
  def test_cuda_reconstructor(self, config):
49
54
  data = self.projs