nabu 2024.2.13__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 (198) 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 +14 -0
  136. nabu/reconstruction/fbp_base.py +40 -8
  137. nabu/reconstruction/fbp_opencl.py +8 -0
  138. nabu/reconstruction/filtering.py +59 -25
  139. nabu/reconstruction/filtering_cuda.py +22 -21
  140. nabu/reconstruction/filtering_opencl.py +10 -14
  141. nabu/reconstruction/hbp.py +26 -13
  142. nabu/reconstruction/mlem.py +55 -16
  143. nabu/reconstruction/projection.py +3 -5
  144. nabu/reconstruction/sinogram.py +1 -1
  145. nabu/reconstruction/sinogram_cuda.py +0 -1
  146. nabu/reconstruction/tests/test_cone.py +37 -2
  147. nabu/reconstruction/tests/test_deringer.py +4 -4
  148. nabu/reconstruction/tests/test_fbp.py +36 -15
  149. nabu/reconstruction/tests/test_filtering.py +27 -7
  150. nabu/reconstruction/tests/test_halftomo.py +28 -2
  151. nabu/reconstruction/tests/test_mlem.py +94 -64
  152. nabu/reconstruction/tests/test_projector.py +7 -2
  153. nabu/reconstruction/tests/test_reconstructor.py +1 -1
  154. nabu/reconstruction/tests/test_sino_normalization.py +0 -1
  155. nabu/resources/dataset_analyzer.py +210 -24
  156. nabu/resources/gpu.py +4 -4
  157. nabu/resources/logger.py +4 -4
  158. nabu/resources/nxflatfield.py +103 -37
  159. nabu/resources/tests/test_dataset_analyzer.py +37 -0
  160. nabu/resources/tests/test_extract.py +11 -0
  161. nabu/resources/tests/test_nxflatfield.py +5 -5
  162. nabu/resources/utils.py +16 -10
  163. nabu/stitching/alignment.py +8 -11
  164. nabu/stitching/config.py +44 -35
  165. nabu/stitching/definitions.py +2 -2
  166. nabu/stitching/frame_composition.py +8 -10
  167. nabu/stitching/overlap.py +4 -4
  168. nabu/stitching/sample_normalization.py +5 -5
  169. nabu/stitching/slurm_utils.py +2 -2
  170. nabu/stitching/stitcher/base.py +2 -0
  171. nabu/stitching/stitcher/dumper/base.py +0 -1
  172. nabu/stitching/stitcher/dumper/postprocessing.py +1 -1
  173. nabu/stitching/stitcher/post_processing.py +11 -9
  174. nabu/stitching/stitcher/pre_processing.py +37 -31
  175. nabu/stitching/stitcher/single_axis.py +2 -3
  176. nabu/stitching/stitcher_2D.py +2 -1
  177. nabu/stitching/tests/test_config.py +10 -11
  178. nabu/stitching/tests/test_sample_normalization.py +1 -1
  179. nabu/stitching/tests/test_slurm_utils.py +1 -2
  180. nabu/stitching/tests/test_y_preprocessing_stitching.py +11 -8
  181. nabu/stitching/tests/test_z_postprocessing_stitching.py +3 -3
  182. nabu/stitching/tests/test_z_preprocessing_stitching.py +27 -24
  183. nabu/stitching/utils/tests/__init__.py +0 -0
  184. nabu/stitching/utils/tests/test_post-processing.py +1 -0
  185. nabu/stitching/utils/utils.py +16 -18
  186. nabu/tests.py +0 -3
  187. nabu/testutils.py +62 -9
  188. nabu/utils.py +50 -20
  189. {nabu-2024.2.13.dist-info → nabu-2025.1.0.dist-info}/METADATA +7 -7
  190. nabu-2025.1.0.dist-info/RECORD +328 -0
  191. {nabu-2024.2.13.dist-info → nabu-2025.1.0.dist-info}/WHEEL +1 -1
  192. {nabu-2024.2.13.dist-info → nabu-2025.1.0.dist-info}/entry_points.txt +2 -1
  193. nabu/app/correct_rot.py +0 -70
  194. nabu/io/tests/test_detector_distortion.py +0 -178
  195. nabu-2024.2.13.dist-info/RECORD +0 -317
  196. /nabu/{stitching → app}/tests/__init__.py +0 -0
  197. {nabu-2024.2.13.dist-info → nabu-2025.1.0.dist-info}/licenses/LICENSE +0 -0
  198. {nabu-2024.2.13.dist-info → nabu-2025.1.0.dist-info}/top_level.txt +0 -0
@@ -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]):
@@ -460,6 +460,41 @@ class TestCone:
460
460
  ConebeamReconstructor(*reconstructor_args, **{**reconstructor_kwargs_base, **reconstructor_kwargs_nabu})
461
461
  assert "cannot use native astra FDK" in caplog.text
462
462
 
463
+ def test_reconstruct_noncontiguous_data(self):
464
+ n_z = 206
465
+ n_y = n_x = 256
466
+ n_a = 500
467
+ src_orig_dist = 1000
468
+ orig_det_dist = 50
469
+
470
+ volume, cone_data = generate_hollow_cube_cone_sinograms(
471
+ vol_shape=(n_z, n_y, n_x),
472
+ n_angles=n_a,
473
+ src_orig_dist=src_orig_dist,
474
+ orig_det_dist=orig_det_dist,
475
+ apply_filter=False,
476
+ rot_center_shift=10,
477
+ )
478
+ cone_reconstructor = ConebeamReconstructor(
479
+ cone_data.shape,
480
+ src_orig_dist,
481
+ orig_det_dist,
482
+ volume_shape=volume.shape,
483
+ rot_center=(n_x - 1) / 2 + 10,
484
+ cuda_options={"ctx": self.ctx},
485
+ extra_options={"use_astra_fdk": False},
486
+ )
487
+ ref = cone_reconstructor.reconstruct(cone_data)
488
+
489
+ radios = cone_reconstructor.cuda.allocate_array("_radios", (n_a, n_z, n_x))
490
+ for i in range(n_a):
491
+ radios[i] = cone_data[:, i, :]
492
+
493
+ sinos_discontig = radios.transpose(axes=(1, 0, 2))
494
+ assert cone_reconstructor.cuda.is_contiguous(sinos_discontig) is False
495
+ res = cone_reconstructor.reconstruct(sinos_discontig)
496
+ assert np.allclose(res, ref), "Reconstructing non-contiguous data failed"
497
+
463
498
 
464
499
  def generate_hollow_cube_cone_sinograms(
465
500
  vol_shape,
@@ -476,13 +511,13 @@ def generate_hollow_cube_cone_sinograms(
476
511
  vol_geom = astra.create_vol_geom(n_y, n_x, n_z)
477
512
 
478
513
  prj_width = prj_width or n_x
479
- prj_height = n_z
514
+ # prj_height = n_z
480
515
  angles = np.linspace(0, 2 * np.pi, n_angles, True)
481
516
 
482
517
  proj_geom = astra.create_proj_geom("cone", 1.0, 1.0, n_z, prj_width, angles, src_orig_dist, orig_det_dist)
483
518
  if rot_center_shift is not None:
484
519
  proj_geom = astra.geom_postalignment(proj_geom, (-rot_center_shift, 0))
485
- magnification = 1 + orig_det_dist / src_orig_dist
520
+ # magnification = 1 + orig_det_dist / src_orig_dist
486
521
 
487
522
  # hollow cube
488
523
  cube = np.zeros(astra.geom_size(vol_geom), dtype="f")
@@ -38,7 +38,7 @@ if __do_long_tests__:
38
38
  "sigma": [1.0, 2.0],
39
39
  "wname": ["db15", "haar", "rbio4.4"],
40
40
  "padding": [None, (100, 100), (50, 71)],
41
- "fft_implem": ["skcuda", "vkfft"],
41
+ "fft_implem": ["vkfft"],
42
42
  }
43
43
  )
44
44
 
@@ -107,7 +107,7 @@ class TestDeringer:
107
107
 
108
108
  @pytest.mark.skipif(
109
109
  not (__has_cuda_deringer__) or munchetal_filter is None,
110
- reason="Need pycuda, pycudwt and (scikit-cuda or pyvkfft) for this test",
110
+ reason="Need pycuda, pycudwt and (cupy? or pyvkfft) for this test",
111
111
  )
112
112
  @pytest.mark.parametrize("config", fw_scenarios)
113
113
  def test_cuda_munch_deringer(self, config):
@@ -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(
@@ -7,10 +7,10 @@ from nabu.testutils import get_data, generate_tests_scenarios, __do_long_tests__
7
7
  from nabu.cuda.utils import get_cuda_context, __has_pycuda__
8
8
  from nabu.opencl.utils import get_opencl_context, __has_pyopencl__
9
9
 
10
- from nabu.processing.fft_cuda import has_skcuda, has_vkfft as has_vkfft_cu
10
+ from nabu.processing.fft_cuda import has_vkfft as has_vkfft_cu
11
11
  from nabu.processing.fft_opencl import has_vkfft as has_vkfft_cl
12
12
 
13
- __has_pycuda__ = __has_pycuda__ and (has_skcuda() or has_vkfft_cu())
13
+ __has_pycuda__ = __has_pycuda__ and has_vkfft_cu()
14
14
  __has_pyopencl__ = __has_pyopencl__ and has_vkfft_cl()
15
15
 
16
16
  if __has_pycuda__:
@@ -40,7 +40,7 @@ def bootstrap(request):
40
40
  # always use contiguous arrays
41
41
  cls.sino_511 = np.ascontiguousarray(cls.sino_512[:, :-1])
42
42
  # Could be set to 5.0e-2 when using textures. When not using textures, interpolation slightly differs
43
- cls.tol = 5.1e-2
43
+ cls.tol = 2e-2 # 5.1e-2
44
44
 
45
45
  if __has_pycuda__:
46
46
  cls.cuda_ctx = get_cuda_context(cleanup_at_exit=False)
@@ -62,7 +62,7 @@ class TestFBP:
62
62
  def _get_backprojector(self, config, *bp_args, **bp_kwargs):
63
63
  if config["backend"] == "cuda":
64
64
  if not (__has_pycuda__):
65
- pytest.skip("Need pycuda + (scikit-cuda or pyvkfft)")
65
+ pytest.skip("Need pycuda + (cupy? or pyvkfft)")
66
66
  Backprojector = CudaBackprojector
67
67
  ctx = self.cuda_ctx
68
68
  else:
@@ -98,10 +98,14 @@ class TestFBP:
98
98
  B = self._get_backprojector(config, (500, 512))
99
99
  res = self.apply_fbp(config, B, self.sino_512)
100
100
 
101
- delta_clipped = clip_to_inner_circle(res - self.ref_512)
102
- err_max = np.max(np.abs(delta_clipped))
101
+ diff = res - self.ref_512
102
+ tol = self.tol
103
+ if not (B._use_textures):
104
+ diff = clip_to_inner_circle(diff)
105
+ tol = 5.1e-2
106
+ err_max = np.max(np.abs(diff))
103
107
 
104
- assert err_max < self.tol, "Something wrong with config=%s" % (str(config))
108
+ assert err_max < tol, "Something wrong with config=%s" % (str(config))
105
109
 
106
110
  @pytest.mark.parametrize("config", scenarios)
107
111
  def test_fbp_511(self, config):
@@ -112,10 +116,29 @@ class TestFBP:
112
116
  res = self.apply_fbp(config, B, self.sino_511)
113
117
  ref = self.ref_512[:-1, :-1]
114
118
 
115
- delta_clipped = clip_to_inner_circle(res - ref)
116
- err_max = np.max(np.abs(delta_clipped))
117
-
118
- assert err_max < self.tol, "Something wrong with config=%s" % (str(config))
119
+ diff = clip_to_inner_circle(res - ref)
120
+ err_max = np.max(np.abs(diff))
121
+ tol = self.tol
122
+ if not (B._use_textures):
123
+ tol = 5.1e-2
124
+
125
+ assert err_max < tol, "Something wrong with config=%s" % (str(config))
126
+
127
+ # Cropping the singoram to sino[:, :-1] gives a reconstruction
128
+ # that is not fully equivalent to rec512[:-1, :-1] in the upper half of the image, outside FoV.
129
+ # However, nabu Backprojector gives the same results as astra
130
+ # Probably we should check this instead:
131
+
132
+ # B = self._get_backprojector(config, (500, 511), rot_center=255.5, extra_options={"centered_axis": True})
133
+ # res = self.apply_fbp(config, B, self.sino_511)
134
+ # import astra
135
+ # proj_geom = astra.create_proj_geom('parallel', 1, 511, B.angles)
136
+ # proj_geom = astra.geom_postalignment(proj_geom, - 0.5)
137
+ # vol_geom = astra.create_vol_geom(511, 511)
138
+ # proj_id = astra.create_projector("cuda", proj_geom, vol_geom)
139
+ # ref = astra.create_reconstruction("FBP_CUDA", proj_id, self.sino_511, proj_id)[1]
140
+ # err_max = np.max(np.abs(res - ref))
141
+ # assert err_max < self.tol, "Something wrong with config=%s" % (str(config))
119
142
 
120
143
  @pytest.mark.parametrize("config", scenarios)
121
144
  def test_fbp_roi(self, config):
@@ -194,8 +217,7 @@ class TestFBP:
194
217
  )
195
218
  res_noclip = B0.fbp(sino)
196
219
  ref = clip_to_inner_circle(res_noclip, radius_factor=1)
197
- abs_diff = np.abs(res - ref)
198
- err_max = np.max(abs_diff)
220
+ err_max = np.max(np.abs(res - ref))
199
221
  assert err_max < tol, "Max error is too high for rot_center=%s ; %s" % (str(rot_center), str(config))
200
222
 
201
223
  # Test with custom outer circle value
@@ -223,7 +245,6 @@ class TestFBP:
223
245
  ref = B0.fbp(self.sino_512)
224
246
 
225
247
  # Check that "centered_axis" worked
226
-
227
248
  B = self._get_backprojector(config, sino.shape, rot_center=rot_center, extra_options={"centered_axis": True})
228
249
  res = self.apply_fbp(config, B, sino)
229
250
  # The outside region (outer circle) is different as "res" is a wider slice
@@ -262,7 +283,7 @@ class TestFBP:
262
283
  # Need to translate the axis a little bit, because of non-centered differentiation.
263
284
  # prepend -> +0.5 ; append -> -0.5
264
285
  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)
286
+ rec = self.apply_fbp(config, B, sino_diff) # noqa: F841
266
287
  # Looks good, but all frequencies are not recovered. Use a metric like SSIM or FRC ?
267
288
 
268
289
 
@@ -14,11 +14,13 @@ if __has_pyopencl__:
14
14
  from nabu.opencl.processing import OpenCLProcessing
15
15
  from nabu.reconstruction.filtering_opencl import OpenCLSinoFilter, __has_vkfft__
16
16
 
17
- filters_to_test = ["ramlak", "shepp-logan", "tukey"]
17
+ filters_to_test = ["ramlak", "shepp-logan"]
18
18
  padding_modes_to_test = ["constant", "edge"]
19
+ crop_filtered_data = [True]
19
20
  if __do_long_tests__:
20
- filters_to_test = ["ramlak", "shepp-logan", "cosine", "hamming", "hann", "tukey", "lanczos"]
21
+ filters_to_test.extend(["cosine", "hamming", "hann", "lanczos"])
21
22
  padding_modes_to_test = SinoFilter.available_padding_modes
23
+ crop_filtered_data = [True, False]
22
24
 
23
25
  tests_scenarios = generate_tests_scenarios(
24
26
  {
@@ -26,6 +28,7 @@ tests_scenarios = generate_tests_scenarios(
26
28
  "padding_mode": padding_modes_to_test,
27
29
  "output_provided": [True, False],
28
30
  "truncated_sino": [True, False],
31
+ "crop_filtered_data": crop_filtered_data,
29
32
  }
30
33
  )
31
34
 
@@ -61,9 +64,10 @@ class TestSinoFilter:
61
64
  sino.shape,
62
65
  filter_name=config["filter_name"],
63
66
  padding_mode=config["padding_mode"],
67
+ crop_filtered_data=config["crop_filtered_data"],
64
68
  )
65
69
  if config["output_provided"]:
66
- output = np.zeros_like(sino)
70
+ output = np.zeros(sino_filter.output_shape, "f")
67
71
  else:
68
72
  output = None
69
73
  res = sino_filter.filter_sino(sino, output=output)
@@ -71,7 +75,11 @@ class TestSinoFilter:
71
75
  assert id(res) == id(output), "when providing output, return value must not change"
72
76
 
73
77
  ref = filter_sinogram(
74
- sino, sino_filter.dwidth_padded, filter_name=config["filter_name"], padding_mode=config["padding_mode"]
78
+ sino,
79
+ sino_filter.dwidth_padded,
80
+ filter_name=config["filter_name"],
81
+ padding_mode=config["padding_mode"],
82
+ crop_filtered_data=config["crop_filtered_data"],
75
83
  )
76
84
 
77
85
  assert np.allclose(res, ref, atol=4e-6)
@@ -86,10 +94,11 @@ class TestSinoFilter:
86
94
  sino.shape,
87
95
  filter_name=config["filter_name"],
88
96
  padding_mode=config["padding_mode"],
97
+ crop_filtered_data=config["crop_filtered_data"],
89
98
  cuda_options={"ctx": self.ctx_cuda},
90
99
  )
91
100
  if config["output_provided"]:
92
- output = garray.zeros(sino.shape, "f")
101
+ output = garray.zeros(sino_filter.output_shape, "f")
93
102
  else:
94
103
  output = None
95
104
  res = sino_filter.filter_sino(sino, output=output)
@@ -97,7 +106,11 @@ class TestSinoFilter:
97
106
  assert id(res) == id(output), "when providing output, return value must not change"
98
107
 
99
108
  ref = filter_sinogram(
100
- h_sino, sino_filter.dwidth_padded, filter_name=config["filter_name"], padding_mode=config["padding_mode"]
109
+ h_sino,
110
+ sino_filter.dwidth_padded,
111
+ filter_name=config["filter_name"],
112
+ padding_mode=config["padding_mode"],
113
+ crop_filtered_data=config["crop_filtered_data"],
101
114
  )
102
115
 
103
116
  assert np.allclose(res.get(), ref, atol=6e-5), "test_cuda_filter: something wrong with config=%s" % (
@@ -109,6 +122,8 @@ class TestSinoFilter:
109
122
  )
110
123
  @pytest.mark.parametrize("config", tests_scenarios)
111
124
  def test_opencl_filter(self, config):
125
+ if not (config["crop_filtered_data"]):
126
+ pytest.skip("crop_filtered_data=False is not supported for OpenCL backend yet")
112
127
  sino = self.sino_cl if not (config["truncated_sino"]) else self.sino_truncated_cl
113
128
  h_sino = self.sino if not (config["truncated_sino"]) else self.sino_truncated
114
129
 
@@ -117,6 +132,7 @@ class TestSinoFilter:
117
132
  filter_name=config["filter_name"],
118
133
  padding_mode=config["padding_mode"],
119
134
  opencl_options={"ctx": self.cl.ctx},
135
+ crop_filtered_data=config["crop_filtered_data"],
120
136
  )
121
137
  if config["output_provided"]:
122
138
  output = parray.zeros(self.cl.queue, sino.shape, "f")
@@ -127,7 +143,11 @@ class TestSinoFilter:
127
143
  assert id(res) == id(output), "when providing output, return value must not change"
128
144
 
129
145
  ref = filter_sinogram(
130
- h_sino, sino_filter.dwidth_padded, filter_name=config["filter_name"], padding_mode=config["padding_mode"]
146
+ h_sino,
147
+ sino_filter.dwidth_padded,
148
+ filter_name=config["filter_name"],
149
+ padding_mode=config["padding_mode"],
150
+ crop_filtered_data=config["crop_filtered_data"],
131
151
  )
132
152
 
133
153
  assert np.allclose(res.get(), ref, atol=6e-5), "test_opencl_filter: something wrong with config=%s" % (
@@ -42,7 +42,7 @@ class TestHalftomo:
42
42
  def _get_backprojector(self, config, *bp_args, **bp_kwargs):
43
43
  if config["backend"] == "cuda":
44
44
  if not (__has_pycuda__):
45
- pytest.skip("Need pycuda + scikit-cuda or vkfft")
45
+ pytest.skip("Need pycuda + cupy? or vkfft")
46
46
  Backprojector = CudaBackprojector
47
47
  ctx = self.cuda_ctx
48
48
  else:
@@ -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__
@@ -9,83 +9,113 @@ from nabu.reconstruction.mlem import MLEMReconstructor, __have_corrct__
9
9
  @pytest.fixture(scope="class")
10
10
  def bootstrap(request):
11
11
  cls = request.cls
12
- datafile = get_data("sl_mlem.npz")
13
- cls.data = datafile["data"]
12
+ datafile = get_data("test_mlem.npz")
13
+ cls.data_wvu = datafile["data_wvu"]
14
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
15
+ cls.pixel_size_cm = datafile["pixel_size"] * 1e4 # pixel_size originally in um
16
+ cls.true_cor = datafile["true_cor"]
17
+ cls.mlem_cor_None_nosh = datafile["mlem_cor_None_nosh"]
18
+ cls.mlem_cor_truecor_nosh = datafile["mlem_cor_truecor_nosh"]
19
+ cls.mlem_cor_truecor_shifts_v0 = datafile["mlem_cor_truecor_shifts_v0"]
20
+ cls.shifts_uv_v0 = datafile["shifts_uv_v0"]
21
+ cls.shifts_uv = datafile["shifts_uv"]
22
+
23
+ cls.tol = 1.3e-4
22
24
 
23
25
 
24
26
  @pytest.mark.skipif(not (__has_pycuda__ and __have_corrct__), reason="Need pycuda and corrct for this test")
25
27
  @pytest.mark.usefixtures("bootstrap")
26
- class TestMLEM:
28
+ class TestMLEMReconstructor:
27
29
  """These tests test the general MLEM reconstruction algorithm
28
30
  and the behavior of the reconstruction with respect to horizontal shifts.
29
31
  Only horizontal shifts are tested here because vertical shifts are handled outside
30
- the reconstruction object, but in the embedding reconstruction pipeline. See FullFieldReconstructor"""
32
+ the reconstruction object, but in the embedding reconstruction pipeline. See FullFieldReconstructor
33
+ It is compared against a reference reconstruction generated with the `rec_mlem` function
34
+ defined in the `generate_test_data.py` script.
35
+ """
36
+
37
+ def _rec_mlem(self, cor, shifts_uv, data_wvu, angles_rad):
38
+ n_angles, n_z, n_x = data_wvu.shape
31
39
 
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
40
+ mlem = MLEMReconstructor(
41
+ (n_z, n_angles, n_x),
42
+ angles_rad,
43
+ shifts_uv=shifts_uv,
44
+ cor=cor,
45
+ n_iterations=50,
46
+ extra_options={"centered_axis": True, "clip_outer_circle": True, "scale_factor": 1 / self.pixel_size_cm},
35
47
  )
48
+ rec_mlem = mlem.reconstruct(data_wvu.swapaxes(0, 1))
49
+ return rec_mlem
36
50
 
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)
51
+ def test_simple_mlem_recons_cor_None_nosh(self):
52
+ slice_index = 25
53
+ rec = self._rec_mlem(None, None, self.data_wvu, self.angles_rad)[slice_index]
54
+ delta = np.abs(rec - self.mlem_cor_None_nosh)
41
55
  assert np.max(delta) < self.tol
42
56
 
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
57
+ def test_simple_mlem_recons_cor_truecor_nosh(self):
58
+ slice_index = 25
59
+ rec = self._rec_mlem(self.true_cor, None, self.data_wvu, self.angles_rad)[slice_index]
60
+ delta = np.abs(rec - self.mlem_cor_truecor_nosh)
61
+ assert np.max(delta) < 2.6e-4
50
62
 
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
63
+ def test_compare_with_fbp(self):
64
+ from nabu.reconstruction.fbp import Backprojector
65
+
66
+ def _rec_fbp(cor, shifts_uv, data_wvu, angles_rad):
67
+ n_angles, n_z, n_x = data_wvu.shape
56
68
 
57
- def test_mlem_recons_with_constant_v_shifts(self):
58
- from nabu.preproc.shift import VerticalShift
69
+ if shifts_uv is None:
70
+ fbp = Backprojector(
71
+ (n_angles, n_x),
72
+ angles=angles_rad,
73
+ rot_center=cor,
74
+ halftomo=False,
75
+ padding_mode="edges",
76
+ extra_options={
77
+ "centered_axis": True,
78
+ "clip_outer_circle": True,
79
+ "scale_factor": 1 / self.pixel_size_cm,
80
+ },
81
+ )
82
+ else:
83
+ fbp = Backprojector(
84
+ (n_angles, n_x),
85
+ angles=angles_rad,
86
+ rot_center=cor,
87
+ halftomo=False,
88
+ padding_mode="edges",
89
+ extra_options={
90
+ "centered_axis": True,
91
+ "clip_outer_circle": True,
92
+ "scale_factor": 1 / self.pixel_size_cm, # convert um to cm
93
+ "axis_correction": shifts_uv[:, 0],
94
+ },
95
+ )
59
96
 
97
+ rec_fbp = np.zeros((n_z, n_x, n_x), "f")
98
+ for i in range(n_z):
99
+ rec_fbp[i] = fbp.fbp(data_wvu[:, i])
100
+
101
+ return rec_fbp
102
+
103
+ fbp = _rec_fbp(self.true_cor, None, self.data_wvu, self.angles_rad)[25]
104
+ mlem = self._rec_mlem(self.true_cor, None, self.data_wvu, self.angles_rad)[25]
105
+ delta = np.abs(fbp - mlem)
106
+ assert (
107
+ np.max(delta) < 400
108
+ ) # These two should not be really equal. But the test should test that both algo FBP and MLEM behave similarly.
109
+
110
+ def test_mlem_zeroshifts_equal_noshifts(self):
60
111
  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
- """
112
+ rec_nosh = self._rec_mlem(self.true_cor, None, self.data_wvu, self.angles_rad)
113
+ rec_zerosh = self._rec_mlem(self.true_cor, shifts, self.data_wvu, self.angles_rad)
114
+ delta = np.abs(rec_nosh - rec_zerosh)
115
+ assert np.max(delta) < self.tol
116
+
117
+ def test_mlem_recons_with_u_shifts(self):
118
+ slice_index = 25
119
+ rec = self._rec_mlem(self.true_cor, self.shifts_uv_v0, self.data_wvu, self.angles_rad)[slice_index]
120
+ delta = np.abs(rec - self.mlem_cor_truecor_shifts_v0)
121
+ assert np.max(delta) < self.tol
@@ -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):
@@ -48,7 +48,7 @@ def bootstrap(request):
48
48
  )
49
49
  @pytest.mark.usefixtures("bootstrap")
50
50
  class TestReconstructor:
51
- @pytest.mark.skipif(not (__has_cuda_fbp__), reason="need pycuda and (scikit-cuda or vkfft)")
51
+ @pytest.mark.skipif(not (__has_cuda_fbp__), reason="need pycuda and (cupy? or vkfft)")
52
52
  @pytest.mark.parametrize("config", scenarios)
53
53
  def test_cuda_reconstructor(self, config):
54
54
  data = self.projs
@@ -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