nabu 2025.1.0.dev5__py3-none-any.whl → 2025.1.0.dev12__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 (60) hide show
  1. doc/doc_config.py +32 -0
  2. nabu/__init__.py +1 -1
  3. nabu/app/double_flatfield.py +18 -5
  4. nabu/app/reconstruct_helical.py +4 -4
  5. nabu/app/stitching.py +7 -2
  6. nabu/cuda/src/backproj.cu +10 -10
  7. nabu/cuda/src/cone.cu +4 -0
  8. nabu/cuda/utils.py +1 -1
  9. nabu/estimation/cor.py +3 -3
  10. nabu/io/cast_volume.py +13 -0
  11. nabu/io/reader.py +3 -2
  12. nabu/opencl/src/backproj.cl +10 -10
  13. nabu/pipeline/estimators.py +6 -6
  14. nabu/pipeline/fullfield/chunked.py +13 -13
  15. nabu/pipeline/fullfield/computations.py +4 -1
  16. nabu/pipeline/fullfield/get_double_flatfield.py +147 -0
  17. nabu/pipeline/fullfield/nabu_config.py +16 -4
  18. nabu/pipeline/fullfield/processconfig.py +22 -2
  19. nabu/pipeline/fullfield/reconstruction.py +9 -4
  20. nabu/pipeline/helical/gridded_accumulator.py +1 -1
  21. nabu/pipeline/helical/helical_reconstruction.py +2 -2
  22. nabu/pipeline/helical/nabu_config.py +1 -1
  23. nabu/pipeline/helical/weight_balancer.py +1 -1
  24. nabu/pipeline/params.py +8 -3
  25. nabu/preproc/shift.py +1 -1
  26. nabu/processing/fft_base.py +6 -2
  27. nabu/processing/fft_cuda.py +23 -4
  28. nabu/processing/fft_opencl.py +19 -2
  29. nabu/processing/padding_cuda.py +0 -1
  30. nabu/processing/processing_base.py +11 -5
  31. nabu/reconstruction/astra.py +245 -0
  32. nabu/reconstruction/cone.py +34 -9
  33. nabu/reconstruction/fbp.py +7 -0
  34. nabu/reconstruction/fbp_base.py +8 -0
  35. nabu/reconstruction/filtering.py +59 -25
  36. nabu/reconstruction/filtering_cuda.py +21 -20
  37. nabu/reconstruction/filtering_opencl.py +8 -14
  38. nabu/reconstruction/hbp.py +10 -10
  39. nabu/reconstruction/rings_cuda.py +41 -13
  40. nabu/reconstruction/tests/test_cone.py +35 -0
  41. nabu/reconstruction/tests/test_fbp.py +32 -11
  42. nabu/reconstruction/tests/test_filtering.py +14 -5
  43. nabu/resources/dataset_analyzer.py +34 -2
  44. nabu/resources/tests/test_extract.py +4 -2
  45. nabu/stitching/config.py +6 -1
  46. nabu/stitching/stitcher/dumper/__init__.py +1 -0
  47. nabu/stitching/stitcher/dumper/postprocessing.py +105 -1
  48. nabu/stitching/stitcher/post_processing.py +14 -4
  49. nabu/stitching/stitcher/pre_processing.py +1 -1
  50. nabu/stitching/stitcher/single_axis.py +8 -7
  51. nabu/stitching/stitcher/z_stitcher.py +8 -4
  52. nabu/stitching/utils/utils.py +2 -2
  53. nabu/testutils.py +2 -2
  54. nabu/utils.py +9 -2
  55. {nabu-2025.1.0.dev5.dist-info → nabu-2025.1.0.dev12.dist-info}/METADATA +9 -28
  56. {nabu-2025.1.0.dev5.dist-info → nabu-2025.1.0.dev12.dist-info}/RECORD +60 -57
  57. {nabu-2025.1.0.dev5.dist-info → nabu-2025.1.0.dev12.dist-info}/WHEEL +1 -1
  58. {nabu-2025.1.0.dev5.dist-info → nabu-2025.1.0.dev12.dist-info}/entry_points.txt +0 -0
  59. {nabu-2025.1.0.dev5.dist-info → nabu-2025.1.0.dev12.dist-info/licenses}/LICENSE +0 -0
  60. {nabu-2025.1.0.dev5.dist-info → nabu-2025.1.0.dev12.dist-info}/top_level.txt +0 -0
@@ -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,
@@ -296,15 +305,25 @@ class ConebeamReconstructor:
296
305
  self._vol_id = astra.data3d.link("-vol", self.vol_geom, self._vol_link)
297
306
 
298
307
  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
308
+ self.cuda.check_array(sinos, self.sinos_shape, check_contiguous=False)
301
309
  # TODO don't create new link/proj_id if ptr is the same ?
302
310
  # 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])
311
+ d_sinos = self.cuda.set_array("sinos", sinos) # self.cuda.sinos is now a GPU array
312
+
313
+ self._reallocate_sinos = False
314
+ if not (self.cuda.is_contiguous(d_sinos)) or not (self._crop_filtered_data):
315
+ self._reallocate_sinos = True
316
+ if self._crop_filtered_data:
317
+ sinos_shape = self.sinos_shape
318
+ # Sometimes, the user does not want to crop data after filtering
319
+ # In this case, the backprojector input should be directly the filtered-but-uncropped data.
320
+ # For cone-beam reconstruction, the FDK pre-weighting takes place on input sinogram (not filtered yet),
321
+ # then filter, then 3D backprojection the un-cropped data.
322
+ else:
323
+ sinos_shape = (self.n_z,) + self.sino_filter.sino_padded_shape
324
+ d_sinos = self.cuda.allocate_array("sinos_contig", sinos_shape)
306
325
  self._proj_data_link = astra.data3d.GPULink(
307
- d_sinos.ptr, self.prj_width, self.n_angles, self.n_sinos, d_sinos.strides[-2]
326
+ d_sinos.ptr, d_sinos.shape[-1], self.n_angles, self.n_sinos, d_sinos.strides[-2]
308
327
  )
309
328
  self._proj_id = astra.data3d.link("-sino", self.proj_geom, self._proj_data_link)
310
329
 
@@ -315,8 +334,12 @@ class ConebeamReconstructor:
315
334
  fdk_preweighting(
316
335
  d_sinos, self._orig_prog_geom, relative_z_position=self.relative_z_position, cor_shift=self._cor_shift
317
336
  )
337
+ d_sinos_filtered = d_sinos
338
+ if self._reallocate_sinos:
339
+ d_sinos_filtered = self.cuda.sinos_contig
340
+
318
341
  for i in range(d_sinos.shape[0]):
319
- self.sino_filter.filter_sino(d_sinos[i], output=d_sinos[i])
342
+ self.sino_filter.filter_sino(d_sinos[i], output=d_sinos_filtered[i])
320
343
 
321
344
  def _update_reconstruction(self):
322
345
  if self._use_astra_fdk:
@@ -385,11 +408,13 @@ def roi_is_centered(shape, slice_):
385
408
 
386
409
 
387
410
  def fdk_preweighting(d_sinos, proj_geom, relative_z_position=0.0, cor_shift=0.0):
411
+ discontiguous_sinograms = not (d_sinos.flags.c_contiguous)
388
412
 
389
413
  preweight_kernel = CudaKernel(
390
414
  "devFDK_preweight",
391
415
  filename=get_cuda_srcfile("cone.cu"),
392
416
  signature="Piiifffffiii",
417
+ options=["-DRADIOS_LAYOUT"] if discontiguous_sinograms else None,
393
418
  )
394
419
 
395
420
  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")
@@ -1,3 +1,4 @@
1
+ import warnings
1
2
  import numpy as np
2
3
  from ..utils import updiv, nextpow2, convert_index, deprecation_warning
3
4
  from ..processing.processing_base import ProcessingBase
@@ -22,6 +23,7 @@ class BackprojectorBase:
22
23
  "scale_factor": None,
23
24
  "filter_cutoff": 1.0,
24
25
  "outer_circle_value": 0.0,
26
+ "crop_filtered_data": True,
25
27
  }
26
28
 
27
29
  kernel_filename = None
@@ -245,6 +247,11 @@ class BackprojectorBase:
245
247
  sinofilter_other_kwargs = {}
246
248
  if self.backend != "numpy":
247
249
  sinofilter_other_kwargs["%s_options" % self.backend] = {"ctx": self._processing.ctx}
250
+ sinofilter_other_kwargs["crop_filtered_data"] = self.extra_options.get("crop_filtered_data", True)
251
+ # TODO
252
+ if not (self.extra_options.get("crop_filtered_data", True)):
253
+ warnings.warn("crop_filtered_data = False is not supported for FBP yet", RuntimeWarning)
254
+ #
248
255
  self.sino_filter = self.SinoFilterClass(
249
256
  self.sino_shape,
250
257
  filter_name=self.filter_name,
@@ -376,6 +383,7 @@ class BackprojectorBase:
376
383
  # if a new device array was allocated for sinogram, then the filtering can overwrite it,
377
384
  # since it won't affect user argument
378
385
  if id(d_sino) != id(sino):
386
+ # if id(d_sino) != id(sino) and self.extra_options.get("crop_filtered_data", True):
379
387
  filt_kwargs = {"output": d_sino}
380
388
  #
381
389
  sino_to_backproject = self.sino_filter(d_sino, **filt_kwargs)
@@ -5,15 +5,6 @@ from silx.image.tomography import compute_fourier_filter, get_next_power
5
5
  from ..processing.padding_base import PaddingBase
6
6
  from ..utils import check_supported, get_num_threads
7
7
 
8
- # # COMPAT.
9
- # from .filtering_cuda import CudaSinoFilter
10
-
11
- # SinoFilter = deprecated_class(
12
- # "From version 2023.1, 'filtering_cuda.CudaSinoFilter' should be used instead of 'filtering.SinoFilter'. In the future, 'filtering.SinoFilter' will be a numpy-only class.",
13
- # do_print=True,
14
- # )(CudaSinoFilter)
15
- # #
16
-
17
8
 
18
9
  class SinoFilter:
19
10
  available_filters = [
@@ -44,11 +35,44 @@ class SinoFilter:
44
35
  sino_shape,
45
36
  filter_name=None,
46
37
  padding_mode="zeros",
38
+ crop_filtered_data=True,
47
39
  extra_options=None,
48
40
  ):
41
+ """
42
+ Initialize a SinoFilter instance.
43
+
44
+ Parameters
45
+ ----------
46
+ sino_shape: tuple
47
+ Shape of sinogram, in the form (n_angles, detector_width) or (n_sinos, n_angles, detector_width)
48
+ filter_name: str, optional
49
+ Name of the filter. Default is ram-lak.
50
+ padding_mode: str, optional
51
+ How to pad the data prior to filtering. Default is zero-padding, corresponding to linear convolution with the filter kernel.
52
+ In practice this value is often set to "edges" for interior tomography.
53
+ crop_filtered_data: bool, optional
54
+ Whether to crop the final, filtered sinogram. Default is True. See notes below.
55
+ extra_options: dict, optional
56
+ Dictionary of advanced extra options.
57
+
58
+ Notes
59
+ -----
60
+ Sinogram filtering done in the Filtered Back-Projection (FBP) method consists, in theory, in applying a high-pass filter
61
+ to the sinogram prior to backprojection. This high-pass filter is normally the Ramachandran-Lakshminarayanan (Ram-Lak) filter
62
+ yielding a close-to-ideal reconstruction (see Natterer's "Mathematical methods in image reconstruction").
63
+ As the filter kernel has a large extent in spatial domain, it's best performed in Fourier domain via the Fourier-convolution theorem.
64
+ Filtering in Fourier domain should be done with a data padded to at least twice its size.
65
+ Zero-padding should be used for mathematical correctness (so that multiplication in Fourier domain corresponds to an actual linear convolution).
66
+ However if the sinogram does not decay to "zero" near the edges (i.e in interior tomography), padding with zeros usually gives artefacts after filtering.
67
+ In this case, padding with edges is preferred (corresponding to a convolution with the "edges" extension mode).
68
+
69
+ After inverse Fourier transform, the (padded and filtered) data is cropped back to its original size.
70
+ In some cases, it's preferable to keep the data un-cropped for further processing.
71
+ """
72
+
49
73
  self._init_extra_options(extra_options)
50
74
  self._set_padding_mode(padding_mode)
51
- self._calculate_shapes(sino_shape)
75
+ self._calculate_shapes(sino_shape, crop_filtered_data)
52
76
  self._init_fft()
53
77
  self._allocate_memory()
54
78
  self._compute_filter(filter_name)
@@ -67,7 +91,7 @@ class SinoFilter:
67
91
  check_supported(padding_mode, self.available_padding_modes, "padding mode")
68
92
  self.padding_mode = padding_mode
69
93
 
70
- def _calculate_shapes(self, sino_shape):
94
+ def _calculate_shapes(self, sino_shape, crop_filtered_data):
71
95
  self.ndim = len(sino_shape)
72
96
  if self.ndim == 2:
73
97
  n_angles, dwidth = sino_shape
@@ -90,6 +114,11 @@ class SinoFilter:
90
114
  self.pad_left = (self.dwidth_padded - self.dwidth) // 2
91
115
  self.pad_right = self.dwidth_padded - self.dwidth - self.pad_left
92
116
 
117
+ self.crop_filtered_data = crop_filtered_data
118
+ self.output_shape = self.sino_shape
119
+ if not (self.crop_filtered_data):
120
+ self.output_shape = self.sino_padded_shape
121
+
93
122
  def _init_fft(self):
94
123
  pass
95
124
 
@@ -147,19 +176,16 @@ class SinoFilter:
147
176
  if arr.shape != self.sino_shape:
148
177
  raise ValueError("Expected sinogram shape %s, got %s" % (self.sino_shape, arr.shape))
149
178
 
150
- def filter_sino(self, sino, output=None, no_output=False):
179
+ def filter_sino(self, sino, output=None):
151
180
  """
152
181
  Perform the sinogram siltering.
153
182
 
154
183
  Parameters
155
184
  ----------
156
- sino: numpy.ndarray or pycuda.gpuarray.GPUArray
185
+ sino: array
157
186
  Input sinogram (2D or 3D)
158
- output: numpy.ndarray or pycuda.gpuarray.GPUArray, optional
187
+ output: array, optional
159
188
  Output array.
160
- no_output: bool, optional
161
- If set to True, no copy is be done. The resulting data lies
162
- in self.d_sino_padded.
163
189
  """
164
190
  self._check_array(sino)
165
191
  # sino_padded = np.pad(
@@ -169,16 +195,21 @@ class SinoFilter:
169
195
  sino_padded_f = rfft(sino_padded, axis=1, workers=get_num_threads(self.extra_options["fft_threads"]))
170
196
  sino_padded_f *= self.filter_f
171
197
  sino_filtered = irfft(sino_padded_f, axis=1, workers=get_num_threads(self.extra_options["fft_threads"]))
198
+
172
199
  if output is None:
173
- res = np.zeros(self.sino_shape, dtype=np.float32)
200
+ if not (self.crop_filtered_data):
201
+ # No need to allocate extra memory here
202
+ return sino_filtered
203
+ res = np.zeros(self.output_shape, dtype=np.float32)
174
204
  else:
175
205
  res = output
176
- if self.ndim == 2:
177
- # res[:] = sino_filtered[:, : self.dwidth] # pylint: disable=E1126 # ?!
178
- res[:] = sino_filtered[:, self.pad_left : -self.pad_right] # pylint: disable=E1126 # ?!
206
+
207
+ if self.crop_filtered_data:
208
+ # res[:] = sino_filtered[..., : self.dwidth] # pylint: disable=E1126 # ?!
209
+ res[:] = sino_filtered[..., self.pad_left : -self.pad_right] # pylint: disable=E1126 # ?!
179
210
  else:
180
- # res[:] = sino_filtered[:, :, : self.dwidth] # pylint: disable=E1126 # ?!
181
- res[:] = sino_filtered[:, :, self.pad_left : -self.pad_right] # pylint: disable=E1126 # ?!
211
+ res[:] = sino_filtered[:]
212
+
182
213
  return res
183
214
 
184
215
  __call__ = filter_sino
@@ -191,6 +222,7 @@ def filter_sinogram(
191
222
  padding_mode="constant",
192
223
  normalize=True,
193
224
  filter_cutoff=1.0,
225
+ crop_filtered_data=True,
194
226
  **padding_kwargs,
195
227
  ):
196
228
  """
@@ -228,6 +260,8 @@ def filter_sinogram(
228
260
  fourier_filter = fourier_filter[: padded_width // 2 + 1] # R2C
229
261
  sino_f = rfft(sinogram_padded, axis=1)
230
262
  sino_f *= fourier_filter
231
- # sino_filtered = irfft(sino_f, axis=1)[:, :width] # pylint: disable=E1126 # ?!
232
- sino_filtered = irfft(sino_f, axis=1)[:, pad_left:-pad_right] # pylint: disable=E1126 # ?!
263
+ sino_filtered = irfft(sino_f, axis=1)
264
+ if crop_filtered_data:
265
+ # sino_filtered = sino_filtered[:, :width] # pylint: disable=E1126 # ?!
266
+ sino_filtered = sino_filtered[:, pad_left:-pad_right] # pylint: disable=E1126 # ?!
233
267
  return sino_filtered
@@ -1,6 +1,6 @@
1
1
  import numpy as np
2
2
  from ..cuda.processing import CudaProcessing
3
- from ..utils import get_cuda_srcfile
3
+ from ..utils import docstring, get_cuda_srcfile
4
4
  from ..processing.padding_cuda import CudaPadding
5
5
  from ..processing.fft_cuda import get_fft_class
6
6
  from .filtering import SinoFilter
@@ -9,17 +9,25 @@ from .filtering import SinoFilter
9
9
  class CudaSinoFilter(SinoFilter):
10
10
  default_extra_options = {**SinoFilter.default_extra_options, "fft_backend": "vkfft"}
11
11
 
12
+ @docstring(SinoFilter)
12
13
  def __init__(
13
14
  self,
14
15
  sino_shape,
15
16
  filter_name=None,
16
17
  padding_mode="zeros",
18
+ crop_filtered_data=True,
17
19
  extra_options=None,
18
20
  cuda_options=None,
19
21
  ):
20
22
  self._cuda_options = cuda_options or {}
21
23
  self.cuda = CudaProcessing(**self._cuda_options)
22
- super().__init__(sino_shape, filter_name=filter_name, padding_mode=padding_mode, extra_options=extra_options)
24
+ super().__init__(
25
+ sino_shape,
26
+ filter_name=filter_name,
27
+ padding_mode=padding_mode,
28
+ crop_filtered_data=crop_filtered_data,
29
+ extra_options=extra_options,
30
+ )
23
31
  self._init_kernels()
24
32
 
25
33
  def _init_fft(self):
@@ -60,25 +68,12 @@ class CudaSinoFilter(SinoFilter):
60
68
  )
61
69
 
62
70
  def filter_sino(self, sino, output=None):
63
- """
64
- Perform the sinogram siltering.
65
-
66
- Parameters
67
- ----------
68
- sino: numpy.ndarray or pycuda.gpuarray.GPUArray
69
- Input sinogram (2D or 3D)
70
- output: pycuda.gpuarray.GPUArray, optional
71
- Output array.
72
- no_output: bool, optional
73
- If set to True, no copy is be done. The resulting data lies
74
- in self.d_sino_padded.
75
- """
76
71
  self._check_array(sino)
77
72
  if not (isinstance(sino, self.cuda.array_class)):
78
73
  sino = self.cuda.set_array("sino", sino)
79
74
  elif not (sino.flags.c_contiguous):
80
75
  # Transfer the device array into another, c-contiguous, device array
81
- # We can throw an error as well in this case, but often we so something like fbp(radios[:, i, :])
76
+ # We can throw an error as well in this case, but often we do something like fbp(radios[:, i, :])
82
77
  sino_tmp = self.cuda.allocate_array("sino_contig", sino.shape)
83
78
  sino_tmp.set(sino)
84
79
  sino = sino_tmp
@@ -97,13 +92,19 @@ class CudaSinoFilter(SinoFilter):
97
92
 
98
93
  # return
99
94
  if output is None:
100
- res = self.cuda.allocate_array("output", self.sino_shape)
95
+ if not (self.crop_filtered_data):
96
+ # No need to allocate extra memory here
97
+ return self.d_sino_padded
98
+ res = self.cuda.allocate_array("output", self.output_shape)
101
99
  else:
102
100
  res = output
103
- if self.ndim == 2:
104
- res[:] = self.d_sino_padded[:, self.pad_left : self.pad_left + self.dwidth]
101
+
102
+ if self.crop_filtered_data:
103
+ # res[:] = sino_filtered[..., : self.dwidth] # pylint: disable=E1126 # ?!
104
+ res[:] = self.d_sino_padded[..., self.pad_left : -self.pad_right] # pylint: disable=E1126 # ?!
105
105
  else:
106
- res[:] = self.d_sino_padded[:, :, self.pad_left : self.pad_left + self.dwidth]
106
+ res[:] = self.d_sino_padded[:]
107
+
107
108
  return res
108
109
 
109
110
  __call__ = filter_sino
@@ -19,13 +19,20 @@ class OpenCLSinoFilter(SinoFilter):
19
19
  sino_shape,
20
20
  filter_name=None,
21
21
  padding_mode="zeros",
22
+ crop_filtered_data=True,
22
23
  extra_options=None,
23
24
  opencl_options=None,
24
25
  ):
25
26
  self._opencl_options = opencl_options or {}
26
27
  self.opencl = OpenCLProcessing(**self._opencl_options)
27
28
  self.queue = self.opencl.queue
28
- super().__init__(sino_shape, filter_name=filter_name, padding_mode=padding_mode, extra_options=extra_options)
29
+ super().__init__(
30
+ sino_shape,
31
+ filter_name=filter_name,
32
+ padding_mode=padding_mode,
33
+ crop_filtered_data=crop_filtered_data,
34
+ extra_options=extra_options,
35
+ )
29
36
  self._init_kernels()
30
37
 
31
38
  def _init_fft(self):
@@ -61,19 +68,6 @@ class OpenCLSinoFilter(SinoFilter):
61
68
  self.memcpy2D = OpenCLMemcpy2D(queue=self.queue)
62
69
 
63
70
  def filter_sino(self, sino, output=None):
64
- """
65
- Perform the sinogram siltering.
66
-
67
- Parameters
68
- ----------
69
- sino: numpy.ndarray or pyopencl.array
70
- Input sinogram (2D or 3D)
71
- output: pyopencl.array, optional
72
- Output array.
73
- no_output: bool, optional
74
- If set to True, no copy is be done. The resulting data lies
75
- in self.d_sino_padded.
76
- """
77
71
  self._check_array(sino)
78
72
  sino = self.opencl.set_array("sino", sino)
79
73
 
@@ -73,7 +73,7 @@ class HierarchicalBackprojector(CudaBackprojector):
73
73
 
74
74
  # to do the reconstruction in reduction_steps steps
75
75
  self.reduction_steps = self.extra_options.get("hbp_reduction_steps", 2)
76
- reduction_factor = int(math.ceil((sino_shape[-2]) ** (1 / self.reduction_steps)))
76
+ reduction_factor = math.ceil((sino_shape[-2]) ** (1 / self.reduction_steps))
77
77
 
78
78
  # TODO customize
79
79
  axis_source_meters = 1.0e9
@@ -153,13 +153,13 @@ class HierarchicalBackprojector(CudaBackprojector):
153
153
 
154
154
  angularRange = abs(np.ptp(self.angles)) / self.sino_shape[0] * reductionFactor
155
155
 
156
- ngrids = int(math.ceil(self.sino_shape[0] / reductionFactor))
156
+ ngrids = math.ceil(self.sino_shape[0] / reductionFactor)
157
157
 
158
158
  grid_width = int(
159
159
  np.rint(2 * N * self.whf[0])
160
160
  ) # double sampling to account/compensate for diamond shaped grid of ray-intersections
161
- grid_height = int(
162
- math.ceil(angularRange * N * self.whf[1])
161
+ grid_height = math.ceil(
162
+ angularRange * N * self.whf[1]
163
163
  ) # small-angle approximation, generates as much "lines" as needed to account for all intersection levels
164
164
 
165
165
  m = (len(self.angles) // reductionFactor) * reductionFactor
@@ -179,7 +179,7 @@ class HierarchicalBackprojector(CudaBackprojector):
179
179
  )
180
180
  ]
181
181
  self.gridInvTransforms += [np.array([np.linalg.inv(t) for t in self.gridTransforms[-1]], dtype=np.float32)]
182
- self.grids += [(grid_height, grid_width, int(math.ceil(ngrids / legs)))]
182
+ self.grids += [(grid_height, grid_width, math.ceil(ngrids / legs))]
183
183
  self.reductionFactors += [reductionFactor]
184
184
 
185
185
  ### intermediate level grids: accumulation grids ###
@@ -190,13 +190,13 @@ class HierarchicalBackprojector(CudaBackprojector):
190
190
  # for a reasonable (with regard to memory requirement) grid-aspect ratio in the intermediate levels,
191
191
  # the covered angular range per grid should not exceed 28.6°, i.e.,
192
192
  # fewer than 7 (6.3) or 13 (12.6) grids for a 180° / 360° scan is not reasonable
193
- if int(math.ceil(ngrids / reductionFactor)) < 20:
193
+ if math.ceil(ngrids / reductionFactor) < 20:
194
194
  break
195
195
  angularRange *= reductionFactor
196
- ngrids = int(math.ceil(ngrids / reductionFactor))
196
+ ngrids = math.ceil(ngrids / reductionFactor)
197
197
 
198
- grid_height = int(
199
- math.ceil(angularRange * N * self.whf[1])
198
+ grid_height = math.ceil(
199
+ angularRange * N * self.whf[1]
200
200
  ) # implicit small angle approximation, whose validity is
201
201
  # asserted by the preceding "break"
202
202
  gridAinvT = self._getAinvT(N, grid_height, grid_width)
@@ -218,7 +218,7 @@ class HierarchicalBackprojector(CudaBackprojector):
218
218
  )
219
219
  ]
220
220
  self.gridInvTransforms += [np.array([np.linalg.inv(t) for t in self.gridTransforms[-1]], dtype=np.float32)]
221
- self.grids += [(grid_height, grid_width, int(math.ceil(ngrids / legs)))]
221
+ self.grids += [(grid_height, grid_width, math.ceil(ngrids / legs))]
222
222
  self.reductionFactors += [reductionFactor]
223
223
 
224
224
  ##### final accumulation grid #################
@@ -264,13 +264,7 @@ class CudaSinoMeanDeringer(SinoMeanDeringer):
264
264
  filename=get_cuda_srcfile("normalization.cu"),
265
265
  signature="PPiii",
266
266
  )
267
- self._mean_kernel_block = (32, 1, 32)
268
- self._mean_kernel_grid = [updiv(a, b) for a, b in zip(self.sinos_shape[::-1], self._mean_kernel_block)]
269
- self._mean_kernel_args = [self.d_sino_profile, np.int32(self.n_x), np.int32(self.n_angles), np.int32(self.n_z)]
270
- self._mean_kernel_kwargs = {
271
- "grid": self._mean_kernel_grid,
272
- "block": self._mean_kernel_block,
273
- }
267
+ self._set_kernel_args_normalization()
274
268
 
275
269
  self._op_kernel = self.processing.kernel(
276
270
  "inplace_generic_op_3Dby1D",
@@ -278,9 +272,25 @@ class CudaSinoMeanDeringer(SinoMeanDeringer):
278
272
  signature="PPiii",
279
273
  options=["-DGENERIC_OP=%d" % (3 if self.mode == "divide" else 1)],
280
274
  )
281
- self._op_kernel_block = (16, 16, 4)
282
- self._op_kernel_grid = [updiv(a, b) for a, b in zip(self.sinos_shape[::-1], self._op_kernel_block)]
283
- self._op_kernel_args = [self.d_sino_profile, np.int32(self.n_x), np.int32(self.n_angles), np.int32(self.n_z)]
275
+ self._set_kernel_args_mult()
276
+
277
+ def _set_kernel_args_normalization(self, blk_z=32, n_z=None):
278
+ n_z = n_z or self.n_z
279
+ self._mean_kernel_block = (32, 1, blk_z)
280
+ sinos_shape_xyz = self.sinos_shape[1:][::-1] + (n_z,)
281
+ self._mean_kernel_grid = [updiv(a, b) for a, b in zip(sinos_shape_xyz, self._mean_kernel_block)]
282
+ self._mean_kernel_args = [self.d_sino_profile, np.int32(self.n_x), np.int32(self.n_angles), np.int32(n_z)]
283
+ self._mean_kernel_kwargs = {
284
+ "grid": self._mean_kernel_grid,
285
+ "block": self._mean_kernel_block,
286
+ }
287
+
288
+ def _set_kernel_args_mult(self, blk_z=4, n_z=None):
289
+ n_z = n_z or self.n_z
290
+ self._op_kernel_block = (16, 16, blk_z)
291
+ sinos_shape_xyz = self.sinos_shape[1:][::-1] + (n_z,)
292
+ self._op_kernel_grid = [updiv(a, b) for a, b in zip(sinos_shape_xyz, self._op_kernel_block)]
293
+ self._op_kernel_args = [self.d_sino_profile, np.int32(self.n_x), np.int32(self.n_angles), np.int32(n_z)]
284
294
  self._op_kernel_kwargs = {
285
295
  "grid": self._op_kernel_grid,
286
296
  "block": self._op_kernel_block,
@@ -315,23 +325,41 @@ class CudaSinoMeanDeringer(SinoMeanDeringer):
315
325
  self.d_sino_profile[:] = sino_profile_p[self._pad_left : -self._pad_right]
316
326
  return self.d_sino_profile
317
327
 
328
+ def _remove_rings_sino(self, d_sino):
329
+ self._mean_kernel(d_sino, *self._mean_kernel_args, **self._mean_kernel_kwargs)
330
+ self._apply_filter(self.d_sino_profile)
331
+ self._op_kernel(d_sino, *self._op_kernel_args, **self._op_kernel_kwargs)
332
+
318
333
  def remove_rings_sinogram(self, sino, output=None):
319
334
  #
320
335
  if output is not None:
321
336
  raise NotImplementedError
322
337
  #
323
338
  if not (sino.flags.c_contiguous):
339
+ # If the sinogram (or stack of sinogram) is not C-Contiguous, we'll proceed by looping over each
340
+ # C-Contiguous sinogram
324
341
  d_sino = self.processing.allocate_array("d_sino", sino.shape, np.float32)
325
342
  d_sino[:] = sino[:]
326
343
  else:
327
344
  d_sino = sino
328
- self._mean_kernel(d_sino, *self._mean_kernel_args, **self._mean_kernel_kwargs)
329
- self._apply_filter(self.d_sino_profile)
330
- self._op_kernel(d_sino, *self._op_kernel_args, **self._op_kernel_kwargs)
345
+ self._remove_rings_sino(d_sino)
331
346
  if not (sino.flags.c_contiguous):
332
347
  sino[:] = self.processing.d_sino[:]
333
348
  return sino
334
349
 
335
350
  def remove_rings_sinograms(self, sinograms):
351
+ if sinograms.flags.c_contiguous:
352
+ self._remove_rings_sino(sinograms)
353
+ return sinograms
354
+
355
+ # If the stack of sinograms is not C-Contiguous, we have to proceed by looping over each C-Contiguous sinogram
356
+ # (i.e don't copy the entire stack, just one sinogram at a time)
357
+ self._set_kernel_args_normalization(blk_z=1, n_z=1)
358
+ self._set_kernel_args_mult(blk_z=1, n_z=1)
336
359
  for i in range(sinograms.shape[0]):
337
360
  self.remove_rings_sinogram(sinograms[i])
361
+ self._set_kernel_args_normalization()
362
+ self._set_kernel_args_mult()
363
+ return sinograms
364
+
365
+ remove_rings = remove_rings_sinograms
@@ -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,