nabu 2024.2.4__py3-none-any.whl → 2025.1.0.dev4__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/bootstrap_stitching.py +4 -2
- nabu/app/cast_volume.py +7 -13
- nabu/app/cli_configs.py +0 -5
- nabu/app/compare_volumes.py +1 -1
- nabu/app/composite_cor.py +2 -4
- nabu/app/correct_rot.py +0 -8
- nabu/app/diag_to_pix.py +5 -6
- nabu/app/diag_to_rot.py +10 -11
- nabu/app/multicor.py +1 -1
- nabu/app/parse_reconstruction_log.py +1 -0
- nabu/app/prepare_weights_double.py +1 -2
- nabu/app/reconstruct_helical.py +1 -5
- nabu/app/reduce_dark_flat.py +0 -2
- nabu/app/rotate.py +3 -1
- nabu/app/tests/test_reduce_dark_flat.py +2 -2
- nabu/app/validator.py +1 -4
- nabu/cuda/convolution.py +1 -1
- nabu/cuda/fft.py +1 -1
- nabu/cuda/medfilt.py +1 -1
- nabu/cuda/padding.py +1 -1
- nabu/cuda/src/cone.cu +19 -9
- nabu/cuda/src/hierarchical_backproj.cu +16 -0
- nabu/cuda/utils.py +2 -2
- nabu/estimation/alignment.py +17 -31
- nabu/estimation/cor.py +23 -29
- nabu/estimation/cor_sino.py +2 -8
- nabu/estimation/focus.py +4 -8
- nabu/estimation/tests/test_alignment.py +2 -0
- nabu/estimation/tests/test_tilt.py +1 -1
- nabu/estimation/tilt.py +5 -4
- nabu/io/cast_volume.py +5 -5
- nabu/io/detector_distortion.py +5 -6
- nabu/io/reader.py +3 -3
- nabu/io/reader_helical.py +5 -4
- nabu/io/tests/test_cast_volume.py +2 -2
- nabu/io/tests/test_readers.py +4 -4
- nabu/io/tests/test_writers.py +2 -2
- nabu/io/utils.py +8 -4
- nabu/io/writer.py +1 -2
- nabu/misc/fftshift.py +1 -1
- nabu/misc/fourier_filters.py +1 -1
- nabu/misc/histogram.py +1 -1
- nabu/misc/histogram_cuda.py +1 -1
- nabu/misc/padding_base.py +1 -1
- nabu/misc/rotation.py +1 -1
- nabu/misc/rotation_cuda.py +1 -1
- nabu/misc/tests/test_binning.py +1 -1
- nabu/misc/transpose.py +1 -1
- nabu/misc/unsharp.py +1 -1
- nabu/misc/unsharp_cuda.py +1 -1
- nabu/misc/unsharp_opencl.py +1 -1
- nabu/misc/utils.py +1 -1
- nabu/opencl/fft.py +1 -1
- nabu/opencl/padding.py +1 -1
- nabu/opencl/utils.py +8 -8
- nabu/pipeline/config.py +2 -2
- nabu/pipeline/config_validators.py +4 -3
- nabu/pipeline/datadump.py +3 -3
- nabu/pipeline/estimators.py +6 -6
- nabu/pipeline/fullfield/chunked.py +4 -5
- nabu/pipeline/fullfield/dataset_validator.py +0 -1
- nabu/pipeline/fullfield/nabu_config.py +2 -1
- nabu/pipeline/fullfield/reconstruction.py +9 -8
- nabu/pipeline/helical/dataset_validator.py +3 -4
- nabu/pipeline/helical/fbp.py +4 -4
- nabu/pipeline/helical/filtering.py +5 -4
- nabu/pipeline/helical/gridded_accumulator.py +9 -10
- nabu/pipeline/helical/helical_chunked_regridded.py +1 -0
- nabu/pipeline/helical/helical_reconstruction.py +10 -7
- nabu/pipeline/helical/helical_utils.py +1 -2
- nabu/pipeline/helical/nabu_config.py +1 -0
- nabu/pipeline/helical/span_strategy.py +1 -0
- nabu/pipeline/helical/weight_balancer.py +1 -2
- nabu/pipeline/tests/__init__.py +0 -0
- nabu/pipeline/utils.py +1 -1
- nabu/pipeline/writer.py +1 -1
- nabu/preproc/alignment.py +0 -10
- nabu/preproc/ctf.py +8 -8
- nabu/preproc/ctf_cuda.py +1 -1
- nabu/preproc/double_flatfield_cuda.py +2 -2
- nabu/preproc/double_flatfield_variable_region.py +0 -1
- nabu/preproc/flatfield.py +1 -1
- nabu/preproc/flatfield_cuda.py +1 -2
- nabu/preproc/flatfield_variable_region.py +3 -3
- nabu/preproc/phase.py +2 -4
- nabu/preproc/phase_cuda.py +2 -2
- nabu/preproc/shift_cuda.py +0 -1
- nabu/preproc/tests/test_ctf.py +3 -3
- nabu/preproc/tests/test_double_flatfield.py +1 -1
- nabu/preproc/tests/test_flatfield.py +1 -1
- nabu/preproc/tests/test_vshift.py +4 -1
- nabu/processing/azim.py +2 -2
- nabu/processing/convolution_cuda.py +6 -4
- nabu/processing/fft_base.py +1 -1
- nabu/processing/fft_cuda.py +19 -8
- nabu/processing/fft_opencl.py +9 -4
- nabu/processing/fftshift.py +1 -1
- nabu/processing/histogram.py +1 -1
- nabu/processing/muladd.py +0 -1
- nabu/processing/padding_base.py +1 -1
- nabu/processing/padding_cuda.py +0 -1
- nabu/processing/processing_base.py +1 -1
- nabu/processing/tests/test_fft.py +1 -1
- nabu/processing/tests/test_fftshift.py +1 -1
- nabu/processing/tests/test_medfilt.py +1 -3
- nabu/processing/tests/test_padding.py +1 -1
- nabu/processing/tests/test_roll.py +1 -1
- nabu/processing/unsharp_opencl.py +1 -1
- nabu/reconstruction/astra.py +245 -0
- nabu/reconstruction/cone.py +9 -4
- nabu/reconstruction/fbp_base.py +2 -2
- nabu/reconstruction/filtering_cuda.py +1 -1
- nabu/reconstruction/hbp.py +16 -3
- nabu/reconstruction/mlem.py +0 -1
- nabu/reconstruction/projection.py +3 -5
- nabu/reconstruction/sinogram.py +1 -1
- nabu/reconstruction/sinogram_cuda.py +0 -1
- nabu/reconstruction/tests/test_cone.py +76 -3
- nabu/reconstruction/tests/test_deringer.py +2 -2
- nabu/reconstruction/tests/test_fbp.py +1 -1
- nabu/reconstruction/tests/test_halftomo.py +27 -1
- nabu/reconstruction/tests/test_mlem.py +3 -2
- nabu/reconstruction/tests/test_projector.py +7 -2
- nabu/reconstruction/tests/test_sino_normalization.py +0 -1
- nabu/resources/dataset_analyzer.py +4 -4
- nabu/resources/gpu.py +4 -4
- nabu/resources/logger.py +4 -4
- nabu/resources/nxflatfield.py +2 -2
- nabu/resources/tests/test_nxflatfield.py +4 -4
- nabu/stitching/alignment.py +1 -4
- nabu/stitching/config.py +19 -16
- nabu/stitching/frame_composition.py +8 -10
- nabu/stitching/overlap.py +2 -2
- nabu/stitching/slurm_utils.py +2 -2
- nabu/stitching/stitcher/base.py +2 -0
- nabu/stitching/stitcher/dumper/base.py +0 -1
- nabu/stitching/stitcher/dumper/postprocessing.py +1 -1
- nabu/stitching/stitcher/post_processing.py +6 -6
- nabu/stitching/stitcher/pre_processing.py +13 -11
- nabu/stitching/stitcher/single_axis.py +3 -4
- nabu/stitching/stitcher_2D.py +2 -1
- nabu/stitching/tests/test_config.py +7 -8
- nabu/stitching/tests/test_sample_normalization.py +1 -1
- nabu/stitching/tests/test_slurm_utils.py +1 -2
- nabu/stitching/tests/test_z_postprocessing_stitching.py +1 -1
- nabu/stitching/tests/test_z_preprocessing_stitching.py +4 -4
- nabu/stitching/utils/tests/__init__.py +0 -0
- nabu/stitching/utils/tests/test_post-processing.py +1 -0
- nabu/stitching/utils/utils.py +10 -12
- nabu/tests.py +0 -3
- nabu/testutils.py +30 -8
- nabu/utils.py +28 -18
- {nabu-2024.2.4.dist-info → nabu-2025.1.0.dev4.dist-info}/METADATA +25 -25
- nabu-2025.1.0.dev4.dist-info/RECORD +320 -0
- {nabu-2024.2.4.dist-info → nabu-2025.1.0.dev4.dist-info}/WHEEL +1 -1
- nabu/io/tests/test_detector_distortion.py +0 -178
- nabu/resources/tests/test_extract.py +0 -9
- nabu-2024.2.4.dist-info/RECORD +0 -318
- /nabu/{stitching → app}/tests/__init__.py +0 -0
- {nabu-2024.2.4.dist-info → nabu-2025.1.0.dev4.dist-info}/LICENSE +0 -0
- {nabu-2024.2.4.dist-info → nabu-2025.1.0.dev4.dist-info}/entry_points.txt +0 -0
- {nabu-2024.2.4.dist-info → nabu-2025.1.0.dev4.dist-info}/top_level.txt +0 -0
@@ -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)
|
nabu/reconstruction/cone.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
+
import logging
|
1
2
|
from math import sqrt
|
2
3
|
import numpy as np
|
3
4
|
|
4
5
|
from ..cuda.kernel import CudaKernel
|
5
6
|
from ..cuda.processing import CudaProcessing
|
6
7
|
from ..reconstruction.filtering_cuda import CudaSinoFilter
|
7
|
-
from ..utils import get_cuda_srcfile
|
8
|
+
from ..utils import get_cuda_srcfile, updiv
|
8
9
|
|
9
10
|
try:
|
10
11
|
import astra
|
@@ -14,6 +15,9 @@ except ImportError:
|
|
14
15
|
__have_astra__ = False
|
15
16
|
|
16
17
|
|
18
|
+
_logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
17
21
|
class ConebeamReconstructor:
|
18
22
|
"""
|
19
23
|
A reconstructor for cone-beam geometry using the astra toolbox.
|
@@ -159,7 +163,9 @@ class ConebeamReconstructor:
|
|
159
163
|
def _init_fdk(self, padding_mode, filter_name):
|
160
164
|
self.padding_mode = padding_mode
|
161
165
|
self._use_astra_fdk = bool(self.extra_options.get("use_astra_fdk", True))
|
162
|
-
self._use_astra_fdk
|
166
|
+
if self._use_astra_fdk and padding_mode not in ["zeros", "constant", None, "none"]:
|
167
|
+
self._use_astra_fdk = False
|
168
|
+
_logger.warning("padding_mode was set to %s, cannot use native astra FDK" % padding_mode)
|
163
169
|
if self._use_astra_fdk:
|
164
170
|
return
|
165
171
|
self.sino_filter = CudaSinoFilter(
|
@@ -386,12 +392,11 @@ def fdk_preweighting(d_sinos, proj_geom, relative_z_position=0.0, cor_shift=0.0)
|
|
386
392
|
signature="Piiifffffiii",
|
387
393
|
)
|
388
394
|
|
389
|
-
# n_angles, n_z, n_x = d_sinos.shape
|
390
395
|
n_z, n_angles, n_x = d_sinos.shape
|
391
396
|
det_origin = sqrt(proj_geom["DistanceOriginDetector"] ** 2 + cor_shift**2)
|
392
397
|
|
393
398
|
block = (32, 16, 1)
|
394
|
-
grid = ((
|
399
|
+
grid = (updiv(n_x, block[0]), updiv(n_angles, block[1]), 1)
|
395
400
|
|
396
401
|
preweight_kernel(
|
397
402
|
d_sinos,
|
nabu/reconstruction/fbp_base.py
CHANGED
@@ -82,7 +82,7 @@ class BackprojectorBase:
|
|
82
82
|
backend_options: dict, optional
|
83
83
|
OpenCL/Cuda options passed to the OpenCLProcessing or CudaProcessing class.
|
84
84
|
|
85
|
-
Other
|
85
|
+
Other Parameters
|
86
86
|
-----------------
|
87
87
|
extra_options: dict, optional
|
88
88
|
Dictionary with a set of advanced options. The default are the following:
|
@@ -354,7 +354,7 @@ class BackprojectorBase:
|
|
354
354
|
|
355
355
|
def backproj(self, sino, output=None, do_checks=True):
|
356
356
|
if self.halftomo and self.rot_center < self.dwidth:
|
357
|
-
self.sino_mult.prepare_sino(sino)
|
357
|
+
sino = self.sino_mult.prepare_sino(sino)
|
358
358
|
self._transfer_to_texture(sino)
|
359
359
|
d_slice = self._set_output(output, check=do_checks)
|
360
360
|
self._set_kernel_slice_arg(d_slice)
|
@@ -7,7 +7,7 @@ from .filtering import SinoFilter
|
|
7
7
|
|
8
8
|
|
9
9
|
class CudaSinoFilter(SinoFilter):
|
10
|
-
default_extra_options = {**SinoFilter.default_extra_options,
|
10
|
+
default_extra_options = {**SinoFilter.default_extra_options, "fft_backend": "vkfft"}
|
11
11
|
|
12
12
|
def __init__(
|
13
13
|
self,
|
nabu/reconstruction/hbp.py
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
# Generalized Hierarchical Backprojection (GHBP)
|
2
|
+
# for fast tomographic reconstruction from ultra high resolution images at non-negligible fan angles.
|
3
|
+
#
|
4
|
+
# Authors/Contributions:
|
5
|
+
# - Jonas Graetz, Fraunhofer IIS / Universität Würzburg: Algorithm Design and original OpenCL/Python implementation.
|
6
|
+
# - Alessandro Mirone, ESRF: CUDA translation, ESRF / BM18 integration, testing <mirone@esrf.fr>
|
7
|
+
# - Pierre Paleo, ESRF: ESRF / BM18 integration, testing <pierre.paleo@esrf.fr>
|
8
|
+
#
|
9
|
+
# JG was funded by the German Federal Ministry of Education and Research (BMBF), grant 05E2019,
|
10
|
+
# funding the development of BM18 at ESRF in collaboration with the Fraunhofer Gesellschaft,
|
11
|
+
# the Julius-Maximilians-Universität Würzburg, and the University of Passau
|
12
|
+
|
1
13
|
import math
|
2
14
|
import numpy as np
|
3
15
|
|
@@ -12,6 +24,7 @@ from .fbp import CudaBackprojector
|
|
12
24
|
|
13
25
|
|
14
26
|
try:
|
27
|
+
# ruff: noqa: F401
|
15
28
|
import pycuda.driver as cuda
|
16
29
|
from pycuda import gpuarray as garray
|
17
30
|
|
@@ -138,7 +151,7 @@ class HierarchicalBackprojector(CudaBackprojector):
|
|
138
151
|
|
139
152
|
N = self.slice_shape[1] * fac
|
140
153
|
|
141
|
-
angularRange = abs(self.angles
|
154
|
+
angularRange = abs(np.ptp(self.angles)) / self.sino_shape[0] * reductionFactor
|
142
155
|
|
143
156
|
ngrids = int(math.ceil(self.sino_shape[0] / reductionFactor))
|
144
157
|
|
@@ -331,7 +344,7 @@ class HierarchicalBackprojector(CudaBackprojector):
|
|
331
344
|
)
|
332
345
|
|
333
346
|
else:
|
334
|
-
for leg in list(range(
|
347
|
+
for leg in list(range(self.legs)):
|
335
348
|
gridOffset = leg * self.grids[0][2]
|
336
349
|
projOffset = gridOffset * self.reductionFactors[0]
|
337
350
|
gws = getGridSize(self.grids[0], lws)
|
@@ -418,7 +431,7 @@ def get_max_grid_size(grids):
|
|
418
431
|
|
419
432
|
|
420
433
|
def getGridSize(minimum, local):
|
421
|
-
m, l = np.array(minimum), np.array(local)
|
434
|
+
m, l = np.array(minimum), np.array(local) # noqa: E741
|
422
435
|
new = (m // l) * l
|
423
436
|
new[new < m] += l[new < m]
|
424
437
|
return tuple(map(int, new // l))
|
nabu/reconstruction/mlem.py
CHANGED
@@ -32,7 +32,6 @@ class Projector:
|
|
32
32
|
|
33
33
|
Parameters
|
34
34
|
-----------
|
35
|
-
|
36
35
|
slice_shape: tuple
|
37
36
|
Shape of the slice: (num_rows, num_columns).
|
38
37
|
angles: int or sequence
|
@@ -198,10 +197,9 @@ class Projector:
|
|
198
197
|
if image.dtype != np.dtype("f"):
|
199
198
|
raise ValueError("Expected float32 data type, got %s" % str(image.dtype))
|
200
199
|
if not isinstance(image, (np.ndarray, garray.GPUArray)):
|
201
|
-
raise
|
202
|
-
if isinstance(image, np.ndarray):
|
203
|
-
|
204
|
-
raise ValueError("Please use C-contiguous arrays")
|
200
|
+
raise TypeError("Expected either numpy.ndarray or pyopencl.array.Array")
|
201
|
+
if isinstance(image, np.ndarray) and not image.flags["C_CONTIGUOUS"]:
|
202
|
+
raise ValueError("Please use C-contiguous arrays")
|
205
203
|
|
206
204
|
def set_image(self, image, check=True):
|
207
205
|
if check:
|
nabu/reconstruction/sinogram.py
CHANGED
@@ -290,7 +290,7 @@ def match_half_sinos_parts(sino, angles, output=None):
|
|
290
290
|
"""
|
291
291
|
n_a = angles.size
|
292
292
|
n_a_2 = n_a // 2
|
293
|
-
sino_part1 = sino[:n_a_2, :]
|
293
|
+
# sino_part1 = sino[:n_a_2, :]
|
294
294
|
sino_part2 = sino[n_a_2:, :]
|
295
295
|
angles = np.rad2deg(angles) # more numerically stable ?
|
296
296
|
angles_1 = angles[:n_a_2]
|
@@ -212,7 +212,6 @@ class CudaSinoNormalization(SinoNormalization):
|
|
212
212
|
else:
|
213
213
|
# This kernel seems to have an issue on arrays that are not C-contiguous.
|
214
214
|
# We have to process image per image.
|
215
|
-
nz = np.int32(1)
|
216
215
|
nthreadsperblock = (1, 32, 1) # TODO tune
|
217
216
|
nblocks = (1, int(updiv(self.n_angles, nthreadsperblock[1])), 1)
|
218
217
|
for i in range(sinos.shape[0]):
|
@@ -1,7 +1,9 @@
|
|
1
|
+
import logging
|
1
2
|
import pytest
|
2
3
|
import numpy as np
|
3
4
|
from scipy.ndimage import gaussian_filter, shift
|
4
5
|
from nabu.utils import subdivide_into_overlapping_segment, clip_circle
|
6
|
+
from nabu.testutils import __do_long_tests__, generate_tests_scenarios
|
5
7
|
|
6
8
|
try:
|
7
9
|
import astra
|
@@ -387,6 +389,77 @@ class TestCone:
|
|
387
389
|
str(roi)
|
388
390
|
)
|
389
391
|
|
392
|
+
def test_fdk_preweight(self, caplog):
|
393
|
+
"""
|
394
|
+
Check that nabu's FDK pre-weighting give the same results as astra
|
395
|
+
"""
|
396
|
+
shapes = [
|
397
|
+
{"n_z": 256, "n_x": 256, "n_a": 500},
|
398
|
+
# {"n_z": 250, "n_x": 340, "n_a": 250}, # Astra reconstruction is incorrect in this case!
|
399
|
+
]
|
400
|
+
src_orig_dist = 1000
|
401
|
+
orig_det_dist = 50
|
402
|
+
|
403
|
+
rot_centers_from_middle = [0]
|
404
|
+
if __do_long_tests__:
|
405
|
+
rot_centers_from_middle.extend([10, -15])
|
406
|
+
|
407
|
+
params_list = generate_tests_scenarios({"shape": shapes, "rot_center": rot_centers_from_middle})
|
408
|
+
|
409
|
+
for params in params_list:
|
410
|
+
n_z = params["shape"]["n_z"]
|
411
|
+
n_x = n_y = params["shape"]["n_x"]
|
412
|
+
n_a = params["shape"]["n_a"]
|
413
|
+
rc = params["rot_center"]
|
414
|
+
volume, cone_data = generate_hollow_cube_cone_sinograms(
|
415
|
+
vol_shape=(n_z, n_y, n_x),
|
416
|
+
n_angles=n_a,
|
417
|
+
src_orig_dist=src_orig_dist,
|
418
|
+
orig_det_dist=orig_det_dist,
|
419
|
+
apply_filter=False,
|
420
|
+
rot_center_shift=rc,
|
421
|
+
)
|
422
|
+
|
423
|
+
reconstructor_args = [(n_z, n_a, n_x), src_orig_dist, orig_det_dist]
|
424
|
+
reconstructor_kwargs_base = {
|
425
|
+
"volume_shape": volume.shape,
|
426
|
+
"rot_center": (n_x - 1) / 2 + rc,
|
427
|
+
"cuda_options": {"ctx": self.ctx},
|
428
|
+
}
|
429
|
+
reconstructor_kwargs_astra = {"padding_mode": "zeros", "extra_options": {"use_astra_fdk": True}}
|
430
|
+
reconstructor_kwargs_nabu = {"padding_mode": "zeros", "extra_options": {"use_astra_fdk": False}}
|
431
|
+
reconstructor_astra = ConebeamReconstructor(
|
432
|
+
*reconstructor_args, **{**reconstructor_kwargs_base, **reconstructor_kwargs_astra}
|
433
|
+
)
|
434
|
+
assert reconstructor_astra._use_astra_fdk is True, "reconstructor_astra should use native astra FDK"
|
435
|
+
reconstructor_nabu = ConebeamReconstructor(
|
436
|
+
*reconstructor_args, **{**reconstructor_kwargs_base, **reconstructor_kwargs_nabu}
|
437
|
+
)
|
438
|
+
ref = reconstructor_astra.reconstruct(cone_data)
|
439
|
+
res = reconstructor_nabu.reconstruct(cone_data)
|
440
|
+
|
441
|
+
reconstructor_kwargs_nabu = {"padding_mode": "edges", "extra_options": {"use_astra_fdk": False}}
|
442
|
+
cb_ep = ConebeamReconstructor(
|
443
|
+
*reconstructor_args, **{**reconstructor_kwargs_base, **reconstructor_kwargs_nabu}
|
444
|
+
)
|
445
|
+
res_ep = cb_ep.reconstruct(cone_data) # noqa: F841
|
446
|
+
|
447
|
+
assert np.max(np.abs(res - ref)) < 2e-3, "Wrong FDK results for parameters: %s" % (str(params))
|
448
|
+
|
449
|
+
# Test with edges padding - only nabu can do that
|
450
|
+
reconstructor_kwargs_nabu["padding_mode"] = "edges"
|
451
|
+
reconstructor_nabu = ConebeamReconstructor(
|
452
|
+
*reconstructor_args, **{**reconstructor_kwargs_base, **reconstructor_kwargs_nabu}
|
453
|
+
)
|
454
|
+
reconstructor_nabu.reconstruct(cone_data)
|
455
|
+
# result is slightly different than "res" in the borders, which is expected
|
456
|
+
# it would be good to test it as well, but it's outside of the scope of this test
|
457
|
+
|
458
|
+
with caplog.at_level(logging.WARNING):
|
459
|
+
reconstructor_kwargs_nabu = {"padding_mode": "edges", "extra_options": {"use_astra_fdk": True}}
|
460
|
+
ConebeamReconstructor(*reconstructor_args, **{**reconstructor_kwargs_base, **reconstructor_kwargs_nabu})
|
461
|
+
assert "cannot use native astra FDK" in caplog.text
|
462
|
+
|
390
463
|
|
391
464
|
def generate_hollow_cube_cone_sinograms(
|
392
465
|
vol_shape,
|
@@ -403,13 +476,13 @@ def generate_hollow_cube_cone_sinograms(
|
|
403
476
|
vol_geom = astra.create_vol_geom(n_y, n_x, n_z)
|
404
477
|
|
405
478
|
prj_width = prj_width or n_x
|
406
|
-
prj_height = n_z
|
479
|
+
# prj_height = n_z
|
407
480
|
angles = np.linspace(0, 2 * np.pi, n_angles, True)
|
408
481
|
|
409
|
-
proj_geom = astra.create_proj_geom("cone", 1.0, 1.0,
|
482
|
+
proj_geom = astra.create_proj_geom("cone", 1.0, 1.0, n_z, prj_width, angles, src_orig_dist, orig_det_dist)
|
410
483
|
if rot_center_shift is not None:
|
411
484
|
proj_geom = astra.geom_postalignment(proj_geom, (-rot_center_shift, 0))
|
412
|
-
magnification = 1 + orig_det_dist / src_orig_dist
|
485
|
+
# magnification = 1 + orig_det_dist / src_orig_dist
|
413
486
|
|
414
487
|
# hollow cube
|
415
488
|
cube = np.zeros(astra.geom_size(vol_geom), dtype="f")
|
@@ -139,9 +139,9 @@ class TestDeringer:
|
|
139
139
|
)
|
140
140
|
def test_vo_deringer(self):
|
141
141
|
deringer = VoDeringer(self.sino.shape)
|
142
|
-
sino_deringed = deringer.remove_rings_sinogram(self.sino)
|
142
|
+
sino_deringed = deringer.remove_rings_sinogram(self.sino) # noqa: F841
|
143
143
|
sinos = np.tile(self.sino, (10, 1, 1))
|
144
|
-
sinos_deringed = deringer.remove_rings_sinograms(sinos)
|
144
|
+
sinos_deringed = deringer.remove_rings_sinograms(sinos) # noqa: F841
|
145
145
|
# TODO check result. The generated test sinogram is "too synthetic" for this kind of deringer
|
146
146
|
|
147
147
|
@pytest.mark.skipif(
|
@@ -262,7 +262,7 @@ class TestFBP:
|
|
262
262
|
# Need to translate the axis a little bit, because of non-centered differentiation.
|
263
263
|
# prepend -> +0.5 ; append -> -0.5
|
264
264
|
B = self._get_backprojector(config, sino_diff.shape, filter_name="hilbert", rot_center=255.5 + 0.5)
|
265
|
-
rec = self.apply_fbp(config, B, sino_diff)
|
265
|
+
rec = self.apply_fbp(config, B, sino_diff) # noqa: F841
|
266
266
|
# Looks good, but all frequencies are not recovered. Use a metric like SSIM or FRC ?
|
267
267
|
|
268
268
|
|
@@ -99,10 +99,36 @@ class TestHalftomo:
|
|
99
99
|
rot_center = sino.shape[-1] - 1 - self.rot_center
|
100
100
|
return self.test_halftomo_right_side(config, sino=sino, rot_center=rot_center)
|
101
101
|
|
102
|
+
def test_halftomo_plain_backprojection(self, config):
|
103
|
+
backprojector = self._get_backprojector(
|
104
|
+
config,
|
105
|
+
self.sino.shape,
|
106
|
+
rot_center=self.rot_center,
|
107
|
+
halftomo=True,
|
108
|
+
padding_mode="edges",
|
109
|
+
extra_options={"centered_axis": True},
|
110
|
+
)
|
111
|
+
d_sino_filtered = backprojector.sino_filter.filter_sino(self.sino) # device array
|
112
|
+
h_sino_filtered = d_sino_filtered.get()
|
113
|
+
reference_fbp = backprojector.fbp(self.sino)
|
114
|
+
|
115
|
+
def _check(rec, array_type):
|
116
|
+
assert (
|
117
|
+
np.max(np.abs(rec - reference_fbp)) < 1e-7
|
118
|
+
), "Something wrong with halftomo backproj using %s array and configuration %s" % (array_type, str(config))
|
119
|
+
|
120
|
+
# Test with device array
|
121
|
+
rec_from_already_filtered_sino = backprojector.backproj(d_sino_filtered)
|
122
|
+
_check(rec_from_already_filtered_sino, "device")
|
123
|
+
|
124
|
+
# Test with numpy array
|
125
|
+
rec_from_already_filtered_sino = backprojector.backproj(h_sino_filtered)
|
126
|
+
_check(rec_from_already_filtered_sino, "numpy")
|
127
|
+
|
102
128
|
def test_halftomo_cor_outside_fov(self, config):
|
103
129
|
sino = np.ascontiguousarray(self.sino[:, : self.sino.shape[-1] // 2])
|
104
130
|
backprojector = self._get_backprojector(config, sino.shape, rot_center=self.rot_center, halftomo=True)
|
105
|
-
res = backprojector.fbp(sino)
|
131
|
+
res = backprojector.fbp(sino) # noqa: F841
|
106
132
|
# Just check that it runs, but no reference results. Who does this anyway ?!
|
107
133
|
|
108
134
|
@pytest.mark.skipif(not (__has_pycuda__), reason="Need pycuda")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import pytest
|
2
2
|
import numpy as np
|
3
|
-
from nabu.testutils import get_data
|
3
|
+
from nabu.testutils import get_data
|
4
4
|
|
5
5
|
from nabu.cuda.utils import __has_pycuda__
|
6
6
|
from nabu.reconstruction.mlem import MLEMReconstructor, __have_corrct__
|
@@ -27,7 +27,8 @@ class TestMLEM:
|
|
27
27
|
"""These tests test the general MLEM reconstruction algorithm
|
28
28
|
and the behavior of the reconstruction with respect to horizontal shifts.
|
29
29
|
Only horizontal shifts are tested here because vertical shifts are handled outside
|
30
|
-
the reconstruction object, but in the embedding reconstruction pipeline. See FullFieldReconstructor
|
30
|
+
the reconstruction object, but in the embedding reconstruction pipeline. See FullFieldReconstructor
|
31
|
+
"""
|
31
32
|
|
32
33
|
def _create_MLEM_reconstructor(self, shifts_uv=None):
|
33
34
|
return MLEMReconstructor(
|
@@ -3,14 +3,17 @@ import pytest
|
|
3
3
|
from nabu.testutils import get_data
|
4
4
|
from nabu.cuda.utils import __has_pycuda__
|
5
5
|
|
6
|
+
textures_available = False
|
6
7
|
if __has_pycuda__:
|
7
8
|
import pycuda.gpuarray as garray
|
8
9
|
|
9
10
|
# from pycuda.cumath import fabs
|
10
11
|
from pycuda.elementwise import ElementwiseKernel
|
11
|
-
from nabu.cuda.utils import get_cuda_context
|
12
|
+
from nabu.cuda.utils import get_cuda_context, check_textures_availability
|
12
13
|
from nabu.reconstruction.projection import Projector
|
13
14
|
from nabu.reconstruction.fbp import Backprojector
|
15
|
+
|
16
|
+
textures_available = check_textures_availability()
|
14
17
|
try:
|
15
18
|
import astra
|
16
19
|
|
@@ -30,6 +33,7 @@ def bootstrap(request):
|
|
30
33
|
cls.ctx = get_cuda_context()
|
31
34
|
|
32
35
|
|
36
|
+
@pytest.mark.skipif(not (textures_available), reason="Textures not supported")
|
33
37
|
@pytest.mark.skipif(not (__has_pycuda__), reason="Need pycuda for this test")
|
34
38
|
@pytest.mark.usefixtures("bootstrap")
|
35
39
|
class TestProjection:
|
@@ -63,7 +67,8 @@ class TestProjection:
|
|
63
67
|
def test_odd_size(self):
|
64
68
|
image = self.image[:511, :]
|
65
69
|
P = Projector(image.shape, self.n_angles - 1)
|
66
|
-
res = P(image)
|
70
|
+
res = P(image) # noqa: F841
|
71
|
+
# TODO check
|
67
72
|
|
68
73
|
@pytest.mark.skipif(not (__has_astra__), reason="Need astra-toolbox for this test")
|
69
74
|
def test_against_astra(self):
|
@@ -311,15 +311,15 @@ class EDFDatasetAnalyzer(DatasetAnalyzer):
|
|
311
311
|
# (eg. subsampling, binning, distortion correction...)
|
312
312
|
# (3) The following spawns one reader instance per file, which is not elegant,
|
313
313
|
# but in principle there are typically 1-2 reduced flats in a scan
|
314
|
-
readers = {k: EDFStackReader([self.raw_flats[k].file_path()], **reader_kwargs) for k in self.raw_flats
|
315
|
-
return {k: readers[k].load_data()[0] for k in self.raw_flats
|
314
|
+
readers = {k: EDFStackReader([self.raw_flats[k].file_path()], **reader_kwargs) for k in self.raw_flats}
|
315
|
+
return {k: readers[k].load_data()[0] for k in self.raw_flats}
|
316
316
|
|
317
317
|
def get_reduced_darks(self, **reader_kwargs):
|
318
318
|
# See notes in get_reduced_flats() above
|
319
319
|
if self.raw_darks in [None, {}]:
|
320
320
|
raise FileNotFoundError("No reduced dark ('darkend.edf' or 'dark.edf') found in %s" % self.location)
|
321
|
-
readers = {k: EDFStackReader([self.raw_darks[k].file_path()], **reader_kwargs) for k in self.raw_darks
|
322
|
-
return {k: readers[k].load_data()[0] for k in self.raw_darks
|
321
|
+
readers = {k: EDFStackReader([self.raw_darks[k].file_path()], **reader_kwargs) for k in self.raw_darks}
|
322
|
+
return {k: readers[k].load_data()[0] for k in self.raw_darks}
|
323
323
|
|
324
324
|
@property
|
325
325
|
def files(self):
|