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
@@ -88,7 +88,7 @@ class ProcessingBase:
88
88
  self.allocate_array(array_name, array_ref.shape, dtype=dtype)
89
89
  getattr(self, array_name).set(array_ref)
90
90
  else:
91
- raise ValueError("Expected numpy array or pycuda array")
91
+ raise TypeError("Expected numpy array or pycuda array")
92
92
  return getattr(self, array_name)
93
93
 
94
94
  def get_array(self, array_name):
@@ -99,6 +99,15 @@ class ProcessingBase:
99
99
  _recover_arrays_references = recover_arrays_references
100
100
  _allocate_array = allocate_array
101
101
  _set_array = set_array
102
+ # --
103
+
104
+ def is_contiguous(self, arr):
105
+ if isinstance(arr, self.array_class):
106
+ return arr.flags.c_contiguous
107
+ elif isinstance(arr, np.ndarray):
108
+ return arr.flags["C_CONTIGUOUS"]
109
+ else:
110
+ raise TypeError
102
111
 
103
112
  def check_array(self, arr, expected_shape, expected_dtype="f", check_contiguous=True):
104
113
  """
@@ -108,11 +117,8 @@ class ProcessingBase:
108
117
  raise ValueError("Expected shape %s but got %s" % (str(expected_shape), str(arr.shape)))
109
118
  if arr.dtype != np.dtype(expected_dtype):
110
119
  raise ValueError("Expected data type %s but got %s" % (str(expected_dtype), str(arr.dtype)))
111
- if check_contiguous:
112
- if isinstance(arr, np.ndarray) and not (arr.flags["C_CONTIGUOUS"]):
113
- raise ValueError("Expected C-contiguous array")
114
- if isinstance(arr, self.array_class) and not arr.flags.c_contiguous:
115
- raise ValueError("Expected C-contiguous array")
120
+ if check_contiguous and not (self.is_contiguous(arr)):
121
+ raise ValueError("Expected C-contiguous array")
116
122
 
117
123
  def kernel(self, *args, **kwargs):
118
124
  raise ValueError("Base class")
@@ -1,7 +1,7 @@
1
1
  import numpy as np
2
2
  from .rotation import Rotation
3
3
  from ..utils import get_cuda_srcfile, updiv
4
- from ..cuda.utils import __has_pycuda__, copy_array
4
+ from ..cuda.utils import __has_pycuda__, copy_array, check_textures_availability
5
5
  from ..cuda.processing import CudaProcessing
6
6
 
7
7
  if __has_pycuda__:
@@ -11,6 +11,8 @@ if __has_pycuda__:
11
11
 
12
12
  class CudaRotation(Rotation):
13
13
  def __init__(self, shape, angle, center=None, mode="edge", reshape=False, cuda_options=None, **sk_kwargs):
14
+ if not (check_textures_availability()):
15
+ raise RuntimeError("Need cuda textures for this class")
14
16
  if center is None:
15
17
  center = ((shape[1] - 1) / 2.0, (shape[0] - 1) / 2.0)
16
18
  super().__init__(shape, angle, center=center, mode=mode, reshape=reshape, **sk_kwargs)
@@ -4,14 +4,13 @@ import numpy as np
4
4
  from scipy.fft import fftn, ifftn, rfftn, irfftn
5
5
  from nabu.testutils import generate_tests_scenarios, get_data, get_array_of_given_shape, __do_long_tests__
6
6
  from nabu.cuda.utils import get_cuda_context, __has_pycuda__
7
- from nabu.processing.fft_cuda import SKCUFFT, VKCUFFT, get_available_fft_implems
7
+ from nabu.processing.fft_cuda import VKCUFFT, get_available_fft_implems
8
8
  from nabu.opencl.utils import __has_pyopencl__, get_opencl_context
9
9
  from nabu.processing.fft_opencl import VKCLFFT, has_vkfft as has_cl_vkfft
10
10
  from nabu.processing.fft_base import is_fast_axes
11
11
 
12
12
  available_cuda_fft = get_available_fft_implems()
13
13
  __has_vkfft__ = "vkfft" in available_cuda_fft
14
- __has_skcuda__ = "skcuda" in available_cuda_fft
15
14
 
16
15
 
17
16
  scenarios = {
@@ -113,67 +112,6 @@ class TestFFT:
113
112
  ref = ref_ifft_func(data, axes=axes)
114
113
  return ref
115
114
 
116
- @pytest.mark.skipif(
117
- not (__has_skcuda__ and __has_pycuda__), reason="Need pycuda and (scikit-cuda or vkfft) for this test"
118
- )
119
- @pytest.mark.parametrize("config", scenarios)
120
- def test_sckcuda(self, config):
121
- r2c = config["r2c"]
122
- shape = config["shape"]
123
- precision = config["precision"]
124
- ndim = len(shape)
125
- if ndim == 3 and not (__do_long_tests__):
126
- pytest.skip("3D FFTs are done only for long tests - use NABU_LONG_TESTS=1")
127
-
128
- data = self._get_data_array(config)
129
-
130
- res, cufft = self._do_fft(data, r2c, return_fft_obj=True, backend_cls=SKCUFFT)
131
- ref = self._do_reference_fft(data, r2c)
132
-
133
- tol = self.abs_tol[precision][ndim]
134
- self.check_result(res, ref, config, tol, name="skcuda")
135
-
136
- # Complex-to-complex can also be performed on real data (as in numpy.fft.fft(real_data))
137
- if not (r2c):
138
- res = self._do_fft(data, False, backend_cls=SKCUFFT)
139
- ref = self._do_reference_fft(data, False)
140
- self.check_result(res, ref, config, tol, name="skcuda")
141
-
142
- # IFFT
143
- res = cufft.ifft(cufft.output_fft).get()
144
- self.check_result(res, data, config, tol, name="skcuda")
145
- # Perhaps we should also check against numpy/scipy ifft,
146
- # but it does not yield the good shape for R2C on odd-sized data
147
-
148
- @pytest.mark.skipif(
149
- not (__has_skcuda__ and __has_pycuda__), reason="Need pycuda and (scikit-cuda or vkfft) for this test"
150
- )
151
- @pytest.mark.parametrize("config", scenarios)
152
- def test_skcuda_batched(self, config):
153
- shape = config["shape"]
154
- if len(shape) == 1:
155
- return
156
- elif len(shape) == 3 and not (__do_long_tests__):
157
- pytest.skip("3D FFTs are done only for long tests - use NABU_LONG_TESTS=1")
158
- r2c = config["r2c"]
159
- tol = self.abs_tol[config["precision"]][len(shape)]
160
-
161
- data = self._get_data_array(config)
162
-
163
- if data.ndim == 2:
164
- axes_to_test = [(0,), (1,)]
165
- elif data.ndim == 3:
166
- # axes_to_test = [(1, 2), (2, 1), (2,)] # See fft.py: works for C2C but not R2C ?
167
- axes_to_test = [(2,)]
168
-
169
- for axes in axes_to_test:
170
- res, cufft = self._do_fft(data, r2c, axes=axes, return_fft_obj=True, backend_cls=SKCUFFT)
171
- ref = self._do_reference_fft(data, r2c, axes=axes)
172
- self.check_result(res, ref, config, tol, name="skcuda batched axes=%s" % (str(axes)))
173
- # IFFT
174
- res = cufft.ifft(cufft.output_fft).get()
175
- self.check_result(res, data, config, tol, name="skcuda")
176
-
177
115
  @pytest.mark.parametrize("config", scenarios)
178
116
  def test_vkfft(self, config):
179
117
  backend = config["backend"]
@@ -190,7 +128,7 @@ class TestFFT:
190
128
  pytest.skip("R2C with odd-sized fast dimension is not supported in VKFFT")
191
129
 
192
130
  # FIXME - vkfft + POCL fail for R2C in one dimension
193
- if config["backend"] == "opencl" and r2c and ndim == 1:
131
+ if config["backend"] == "opencl" and r2c and ndim == 1: # noqa: SIM102
194
132
  if self.cl_ctx.devices[0].platform.name.strip().lower() == "portable computing language":
195
133
  pytest.skip("Something wrong with vkfft + pocl for R2C 1D")
196
134
  # ---
@@ -2,7 +2,7 @@ import numpy as np
2
2
  import pytest
3
3
  from nabu.cuda.utils import get_cuda_context, __has_pycuda__
4
4
  from nabu.opencl.utils import __has_pyopencl__, get_opencl_context
5
- from nabu.testutils import get_data, generate_tests_scenarios, __do_long_tests__
5
+ from nabu.testutils import get_data, generate_tests_scenarios
6
6
 
7
7
  if __has_pyopencl__:
8
8
  from nabu.processing.fftshift import OpenCLFFTshift
@@ -34,16 +34,14 @@ def bootstrap(request):
34
34
 
35
35
  @pytest.mark.skipif(not (__has_pycuda__), reason="Need Cuda/pycuda for this test")
36
36
  @pytest.mark.usefixtures("bootstrap")
37
- class TestMedianFilter(object):
37
+ class TestMedianFilter:
38
38
  @classmethod
39
39
  def allocate_numpy_arrays(cls):
40
- shape = cls.data.shape
41
40
  cls.input = cls.data
42
41
  cls.input3d = np.tile(cls.input, (2, 1, 1))
43
42
 
44
43
  @classmethod
45
44
  def allocate_cuda_arrays(cls):
46
- shape = cls.data.shape
47
45
  cls.d_input = garray.to_gpu(cls.input)
48
46
  cls.d_output = garray.zeros_like(cls.d_input)
49
47
  cls.d_input3d = garray.to_gpu(cls.input3d)
@@ -108,7 +108,7 @@ class TestPadding:
108
108
  d_img.set(data)
109
109
  d_out = padding.processing.allocate_array("d_out", padding.padded_shape, dtype="f")
110
110
 
111
- res = padding.pad(d_img, output=d_out)
111
+ padding.pad(d_img, output=d_out)
112
112
 
113
113
  ref = np.roll(np.pad(data, pad_width, mode=mode), (-pad_width[0][0], -pad_width[1][0]), axis=(0, 1))
114
114
 
@@ -2,7 +2,7 @@ import numpy as np
2
2
  import pytest
3
3
  from nabu.cuda.utils import get_cuda_context, __has_pycuda__
4
4
  from nabu.opencl.utils import __has_pyopencl__, get_opencl_context
5
- from nabu.testutils import get_data, generate_tests_scenarios, __do_long_tests__
5
+ from nabu.testutils import get_data, generate_tests_scenarios
6
6
  from nabu.processing.roll_opencl import OpenCLRoll
7
7
 
8
8
  configs_roll = {
@@ -3,7 +3,7 @@ import pytest
3
3
  from nabu.testutils import generate_tests_scenarios
4
4
  from nabu.processing.rotation_cuda import Rotation
5
5
  from nabu.processing.rotation import __have__skimage__
6
- from nabu.cuda.utils import __has_pycuda__, get_cuda_context
6
+ from nabu.cuda.utils import __has_pycuda__, get_cuda_context, check_textures_availability
7
7
 
8
8
  if __have__skimage__:
9
9
  from skimage.transform import rotate
@@ -68,7 +68,9 @@ class TestRotation:
68
68
  res = R(self.image)
69
69
  self._check_result(res, config, 1e-6)
70
70
 
71
- @pytest.mark.skipif(not (__has_pycuda__), reason="Need cuda rotation")
71
+ @pytest.mark.skipif(
72
+ not (__has_pycuda__) or not (check_textures_availability()), reason="Need cuda rotation (and textures)"
73
+ )
72
74
  @pytest.mark.parametrize("config", scenarios)
73
75
  def test_cuda_rotation(self, config):
74
76
  R = CudaRotation(
@@ -1,5 +1,5 @@
1
1
  try:
2
- import pyopencl.array as parray
2
+ import pyopencl.array as parray # noqa: F401
3
3
  from pyopencl.elementwise import ElementwiseKernel
4
4
  from ..opencl.processing import OpenCLProcessing
5
5
 
@@ -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)
@@ -31,6 +31,7 @@ class ConebeamReconstructor:
31
31
  "outer_circle_value": 0.0,
32
32
  # "use_astra_fdk": True,
33
33
  "use_astra_fdk": False,
34
+ "crop_filtered_data": True,
34
35
  }
35
36
 
36
37
  def __init__(
@@ -131,6 +132,7 @@ class ConebeamReconstructor:
131
132
  self._init_cuda(cuda_options)
132
133
  self._set_sino_shape(sinos_shape)
133
134
  self._orig_prog_geom = None
135
+ self._use_astra_fdk = bool(self.extra_options.get("use_astra_fdk", True))
134
136
  self._init_geometry(
135
137
  source_origin_dist,
136
138
  origin_detector_dist,
@@ -149,6 +151,7 @@ class ConebeamReconstructor:
149
151
  def _configure_extra_options(self, extra_options):
150
152
  self.extra_options = self.default_extra_options.copy()
151
153
  self.extra_options.update(extra_options or {})
154
+ self._crop_filtered_data = self.extra_options.get("crop_filtered_data", True)
152
155
 
153
156
  def _init_cuda(self, cuda_options):
154
157
  cuda_options = cuda_options or {}
@@ -162,7 +165,6 @@ class ConebeamReconstructor:
162
165
 
163
166
  def _init_fdk(self, padding_mode, filter_name):
164
167
  self.padding_mode = padding_mode
165
- self._use_astra_fdk = bool(self.extra_options.get("use_astra_fdk", True))
166
168
  if self._use_astra_fdk and padding_mode not in ["zeros", "constant", None, "none"]:
167
169
  self._use_astra_fdk = False
168
170
  _logger.warning("padding_mode was set to %s, cannot use native astra FDK" % padding_mode)
@@ -172,6 +174,7 @@ class ConebeamReconstructor:
172
174
  self.sinos_shape[1:],
173
175
  filter_name=filter_name,
174
176
  padding_mode=self.padding_mode,
177
+ crop_filtered_data=self.extra_options.get("crop_filtered_data", True),
175
178
  # TODO (?) configure FFT backend
176
179
  extra_options={"cutoff": self.extra_options.get("filter_cutoff", 1.0)},
177
180
  cuda_options={"ctx": self.cuda.ctx},
@@ -248,12 +251,18 @@ class ConebeamReconstructor:
248
251
  # This object has to be re-created each time, because once the modifications below are done,
249
252
  # it is no more a "cone" geometry but a "cone_vec" geometry, and cannot be updated subsequently
250
253
  # (see astra/functions.py:271)
254
+
255
+ if not (self._crop_filtered_data) and hasattr(self, "sino_filter"):
256
+ prj_width = self.sino_filter.sino_padded_shape[-1]
257
+ else:
258
+ prj_width = self.prj_width
259
+
251
260
  self.proj_geom = astra.create_proj_geom(
252
261
  "cone",
253
262
  self._det_spacing_x,
254
263
  self._det_spacing_y,
255
264
  self.n_sinos,
256
- self.prj_width,
265
+ prj_width,
257
266
  self.angles,
258
267
  self.source_origin_dist,
259
268
  self.origin_detector_dist,
@@ -283,6 +292,11 @@ class ConebeamReconstructor:
283
292
  # The translation is exactly the amount that brought the detector up or down, but in the opposite direction.
284
293
  vecs[:, 2] = -self.relative_z_position
285
294
 
295
+ def reset_rot_center(self, rot_center):
296
+ self.rot_center = rot_center
297
+ self._cor_shift = (self.sinos_shape[-1] - 1) / 2.0 - rot_center
298
+ self._create_astra_proj_geometry(self.relative_z_position)
299
+
286
300
  def _set_output(self, volume):
287
301
  if volume is not None:
288
302
  expected_shape = self.vol_shape # if not (self._crop_data) else self._output_cropped_shape
@@ -296,15 +310,25 @@ class ConebeamReconstructor:
296
310
  self._vol_id = astra.data3d.link("-vol", self.vol_geom, self._vol_link)
297
311
 
298
312
  def _set_input(self, sinos):
299
- self.cuda.check_array(sinos, self.sinos_shape)
300
- self.cuda.set_array("sinos", sinos) # self.cuda.sinos is now a GPU array
313
+ self.cuda.check_array(sinos, self.sinos_shape, check_contiguous=False)
301
314
  # TODO don't create new link/proj_id if ptr is the same ?
302
315
  # But it seems Astra modifies the input sinogram while doing FDK, so this might be not relevant
303
- d_sinos = self.cuda.get_array("sinos")
304
-
305
- # self._proj_data_link = astra.data3d.GPULink(d_sinos.ptr, self.prj_width, self.n_angles, self.n_z, sinos.strides[-2])
316
+ d_sinos = self.cuda.set_array("sinos", sinos) # self.cuda.sinos is now a GPU array
317
+
318
+ self._reallocate_sinos = False
319
+ if not (self.cuda.is_contiguous(d_sinos)) or not (self._crop_filtered_data):
320
+ self._reallocate_sinos = True
321
+ if self._crop_filtered_data:
322
+ sinos_shape = self.sinos_shape
323
+ # Sometimes, the user does not want to crop data after filtering
324
+ # In this case, the backprojector input should be directly the filtered-but-uncropped data.
325
+ # For cone-beam reconstruction, the FDK pre-weighting takes place on input sinogram (not filtered yet),
326
+ # then filter, then 3D backprojection the un-cropped data.
327
+ else:
328
+ sinos_shape = (self.n_z,) + self.sino_filter.sino_padded_shape
329
+ d_sinos = self.cuda.allocate_array("sinos_contig", sinos_shape)
306
330
  self._proj_data_link = astra.data3d.GPULink(
307
- d_sinos.ptr, self.prj_width, self.n_angles, self.n_sinos, d_sinos.strides[-2]
331
+ d_sinos.ptr, d_sinos.shape[-1], self.n_angles, self.n_sinos, d_sinos.strides[-2]
308
332
  )
309
333
  self._proj_id = astra.data3d.link("-sino", self.proj_geom, self._proj_data_link)
310
334
 
@@ -315,8 +339,12 @@ class ConebeamReconstructor:
315
339
  fdk_preweighting(
316
340
  d_sinos, self._orig_prog_geom, relative_z_position=self.relative_z_position, cor_shift=self._cor_shift
317
341
  )
342
+ d_sinos_filtered = d_sinos
343
+ if self._reallocate_sinos:
344
+ d_sinos_filtered = self.cuda.sinos_contig
345
+
318
346
  for i in range(d_sinos.shape[0]):
319
- self.sino_filter.filter_sino(d_sinos[i], output=d_sinos[i])
347
+ self.sino_filter.filter_sino(d_sinos[i], output=d_sinos_filtered[i])
320
348
 
321
349
  def _update_reconstruction(self):
322
350
  if self._use_astra_fdk:
@@ -385,11 +413,13 @@ def roi_is_centered(shape, slice_):
385
413
 
386
414
 
387
415
  def fdk_preweighting(d_sinos, proj_geom, relative_z_position=0.0, cor_shift=0.0):
416
+ discontiguous_sinograms = not (d_sinos.flags.c_contiguous)
388
417
 
389
418
  preweight_kernel = CudaKernel(
390
419
  "devFDK_preweight",
391
420
  filename=get_cuda_srcfile("cone.cu"),
392
421
  signature="Piiifffffiii",
422
+ options=["-DRADIOS_LAYOUT"] if discontiguous_sinograms else None,
393
423
  )
394
424
 
395
425
  n_z, n_angles, n_x = d_sinos.shape
@@ -56,6 +56,13 @@ class CudaBackprojector(BackprojectorBase):
56
56
  if self._use_textures:
57
57
  self.texref_proj = self.gpu_projector.module.get_texref(self._kernel_options["texture_name"])
58
58
  self.texref_proj.set_filter_mode(cuda.filter_mode.LINEAR)
59
+ # Set boundary extension to "zero", i.e array[n] = 0 for n < 0 and n >= array.size
60
+ # address_mode.BORDER : extension with zeros
61
+ # address_mode.CLAMP : extension with edges
62
+ # pycuda does not tell if first argument "dim" is 0-based ?
63
+ self.texref_proj.set_address_mode(0, cuda.address_mode.BORDER)
64
+ self.texref_proj.set_address_mode(1, cuda.address_mode.BORDER)
65
+ self.texref_proj.set_address_mode(2, cuda.address_mode.BORDER)
59
66
  self.gpu_projector.prepare(self._kernel_options["kernel_signature"], [self.texref_proj])
60
67
  # Bind texture
61
68
  self._d_sino_cua = cuda.np_to_array(np.zeros(self.sino_shape, "f"), "C")
@@ -79,6 +86,13 @@ class CudaBackprojector(BackprojectorBase):
79
86
  self.sino_mult = CudaSinoMult(self.sino_shape, self.rot_center, ctx=self._processing.ctx)
80
87
  self._prepare_textures() # has to be done after compilation for Cuda (to bind texture to built kernel)
81
88
 
89
+ def _get_filter_init_extra_options(self):
90
+ return {
91
+ "cuda_options": {
92
+ "ctx": self._processing.ctx,
93
+ },
94
+ }
95
+
82
96
  def _transfer_to_texture(self, sino, do_checks=True):
83
97
  if do_checks and not (sino.flags.c_contiguous):
84
98
  raise ValueError("Expected C-Contiguous array")