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.
- doc/doc_config.py +32 -0
- nabu/__init__.py +1 -1
- nabu/app/double_flatfield.py +18 -5
- nabu/app/reconstruct_helical.py +4 -4
- nabu/app/stitching.py +7 -2
- nabu/cuda/src/backproj.cu +10 -10
- nabu/cuda/src/cone.cu +4 -0
- nabu/cuda/utils.py +1 -1
- nabu/estimation/cor.py +3 -3
- nabu/io/cast_volume.py +13 -0
- nabu/io/reader.py +3 -2
- nabu/opencl/src/backproj.cl +10 -10
- nabu/pipeline/estimators.py +6 -6
- nabu/pipeline/fullfield/chunked.py +13 -13
- nabu/pipeline/fullfield/computations.py +4 -1
- nabu/pipeline/fullfield/get_double_flatfield.py +147 -0
- nabu/pipeline/fullfield/nabu_config.py +16 -4
- nabu/pipeline/fullfield/processconfig.py +22 -2
- nabu/pipeline/fullfield/reconstruction.py +9 -4
- nabu/pipeline/helical/gridded_accumulator.py +1 -1
- nabu/pipeline/helical/helical_reconstruction.py +2 -2
- nabu/pipeline/helical/nabu_config.py +1 -1
- nabu/pipeline/helical/weight_balancer.py +1 -1
- nabu/pipeline/params.py +8 -3
- nabu/preproc/shift.py +1 -1
- nabu/processing/fft_base.py +6 -2
- nabu/processing/fft_cuda.py +23 -4
- nabu/processing/fft_opencl.py +19 -2
- nabu/processing/padding_cuda.py +0 -1
- nabu/processing/processing_base.py +11 -5
- nabu/reconstruction/astra.py +245 -0
- nabu/reconstruction/cone.py +34 -9
- nabu/reconstruction/fbp.py +7 -0
- nabu/reconstruction/fbp_base.py +8 -0
- nabu/reconstruction/filtering.py +59 -25
- nabu/reconstruction/filtering_cuda.py +21 -20
- nabu/reconstruction/filtering_opencl.py +8 -14
- nabu/reconstruction/hbp.py +10 -10
- nabu/reconstruction/rings_cuda.py +41 -13
- nabu/reconstruction/tests/test_cone.py +35 -0
- nabu/reconstruction/tests/test_fbp.py +32 -11
- nabu/reconstruction/tests/test_filtering.py +14 -5
- nabu/resources/dataset_analyzer.py +34 -2
- nabu/resources/tests/test_extract.py +4 -2
- nabu/stitching/config.py +6 -1
- nabu/stitching/stitcher/dumper/__init__.py +1 -0
- nabu/stitching/stitcher/dumper/postprocessing.py +105 -1
- nabu/stitching/stitcher/post_processing.py +14 -4
- nabu/stitching/stitcher/pre_processing.py +1 -1
- nabu/stitching/stitcher/single_axis.py +8 -7
- nabu/stitching/stitcher/z_stitcher.py +8 -4
- nabu/stitching/utils/utils.py +2 -2
- nabu/testutils.py +2 -2
- nabu/utils.py +9 -2
- {nabu-2025.1.0.dev5.dist-info → nabu-2025.1.0.dev12.dist-info}/METADATA +9 -28
- {nabu-2025.1.0.dev5.dist-info → nabu-2025.1.0.dev12.dist-info}/RECORD +60 -57
- {nabu-2025.1.0.dev5.dist-info → nabu-2025.1.0.dev12.dist-info}/WHEEL +1 -1
- {nabu-2025.1.0.dev5.dist-info → nabu-2025.1.0.dev12.dist-info}/entry_points.txt +0 -0
- {nabu-2025.1.0.dev5.dist-info → nabu-2025.1.0.dev12.dist-info/licenses}/LICENSE +0 -0
- {nabu-2025.1.0.dev5.dist-info → nabu-2025.1.0.dev12.dist-info}/top_level.txt +0 -0
nabu/reconstruction/cone.py
CHANGED
@@ -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
|
-
|
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.
|
304
|
-
|
305
|
-
|
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,
|
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=
|
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
|
nabu/reconstruction/fbp.py
CHANGED
@@ -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")
|
nabu/reconstruction/fbp_base.py
CHANGED
@@ -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)
|
nabu/reconstruction/filtering.py
CHANGED
@@ -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
|
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:
|
185
|
+
sino: array
|
157
186
|
Input sinogram (2D or 3D)
|
158
|
-
output:
|
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
|
-
|
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
|
-
|
177
|
-
|
178
|
-
res[:] = sino_filtered[
|
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
|
-
|
181
|
-
|
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
|
-
|
232
|
-
|
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__(
|
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
|
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
|
-
|
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
|
-
|
104
|
-
|
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[
|
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__(
|
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
|
|
nabu/reconstruction/hbp.py
CHANGED
@@ -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 =
|
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 =
|
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 =
|
162
|
-
|
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,
|
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
|
193
|
+
if math.ceil(ngrids / reductionFactor) < 20:
|
194
194
|
break
|
195
195
|
angularRange *= reductionFactor
|
196
|
-
ngrids =
|
196
|
+
ngrids = math.ceil(ngrids / reductionFactor)
|
197
197
|
|
198
|
-
grid_height =
|
199
|
-
|
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,
|
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.
|
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.
|
282
|
-
|
283
|
-
|
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.
|
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,
|