nabu 2024.1.9__py3-none-any.whl → 2024.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nabu/__init__.py +1 -1
- nabu/app/bootstrap.py +2 -3
- nabu/app/cast_volume.py +4 -2
- nabu/app/cli_configs.py +5 -0
- nabu/app/composite_cor.py +1 -1
- nabu/app/create_distortion_map_from_poly.py +5 -6
- nabu/app/diag_to_pix.py +7 -19
- nabu/app/diag_to_rot.py +14 -29
- nabu/app/double_flatfield.py +32 -44
- nabu/app/parse_reconstruction_log.py +3 -0
- nabu/app/reconstruct.py +53 -15
- nabu/app/reconstruct_helical.py +2 -2
- nabu/app/stitching.py +27 -13
- nabu/app/tests/test_reduce_dark_flat.py +4 -1
- nabu/cuda/kernel.py +11 -2
- nabu/cuda/processing.py +2 -2
- nabu/cuda/src/cone.cu +77 -0
- nabu/cuda/src/hierarchical_backproj.cu +271 -0
- nabu/cuda/utils.py +0 -6
- nabu/estimation/alignment.py +5 -19
- nabu/estimation/cor.py +173 -599
- nabu/estimation/cor_sino.py +356 -26
- nabu/estimation/focus.py +63 -11
- nabu/estimation/tests/test_cor.py +124 -58
- nabu/estimation/tests/test_focus.py +6 -6
- nabu/estimation/tilt.py +2 -1
- nabu/estimation/utils.py +5 -33
- nabu/io/__init__.py +1 -1
- nabu/io/cast_volume.py +1 -1
- nabu/io/reader.py +416 -21
- nabu/io/tests/test_readers.py +422 -0
- nabu/io/tests/test_writers.py +1 -102
- nabu/io/writer.py +4 -433
- nabu/opencl/kernel.py +14 -3
- nabu/opencl/processing.py +8 -0
- nabu/pipeline/config_validators.py +5 -2
- nabu/pipeline/datadump.py +12 -5
- nabu/pipeline/estimators.py +162 -188
- nabu/pipeline/fullfield/chunked.py +168 -92
- nabu/pipeline/fullfield/chunked_cuda.py +7 -3
- nabu/pipeline/fullfield/computations.py +2 -7
- nabu/pipeline/fullfield/dataset_validator.py +0 -4
- nabu/pipeline/fullfield/nabu_config.py +37 -13
- nabu/pipeline/fullfield/processconfig.py +22 -13
- nabu/pipeline/fullfield/reconstruction.py +13 -9
- nabu/pipeline/helical/helical_chunked_regridded.py +1 -1
- nabu/pipeline/helical/helical_chunked_regridded_cuda.py +1 -0
- nabu/pipeline/helical/helical_reconstruction.py +1 -1
- nabu/pipeline/params.py +21 -1
- nabu/pipeline/processconfig.py +1 -12
- nabu/pipeline/reader.py +146 -0
- nabu/pipeline/tests/test_estimators.py +44 -72
- nabu/pipeline/utils.py +4 -2
- nabu/pipeline/writer.py +10 -2
- nabu/preproc/ccd_cuda.py +1 -1
- nabu/preproc/ctf.py +14 -7
- nabu/preproc/ctf_cuda.py +2 -3
- nabu/preproc/double_flatfield.py +5 -12
- nabu/preproc/double_flatfield_cuda.py +2 -2
- nabu/preproc/flatfield.py +5 -1
- nabu/preproc/flatfield_cuda.py +5 -1
- nabu/preproc/phase.py +24 -73
- nabu/preproc/phase_cuda.py +5 -8
- nabu/preproc/tests/test_ctf.py +11 -7
- nabu/preproc/tests/test_flatfield.py +67 -122
- nabu/preproc/tests/test_paganin.py +54 -30
- nabu/processing/azim.py +206 -0
- nabu/processing/convolution_cuda.py +1 -1
- nabu/processing/fft_cuda.py +15 -17
- nabu/processing/histogram.py +2 -0
- nabu/processing/histogram_cuda.py +2 -1
- nabu/processing/kernel_base.py +3 -0
- nabu/processing/muladd_cuda.py +1 -0
- nabu/processing/padding_opencl.py +1 -1
- nabu/processing/roll_opencl.py +1 -0
- nabu/processing/rotation_cuda.py +2 -2
- nabu/processing/tests/test_fft.py +17 -10
- nabu/processing/unsharp_cuda.py +1 -1
- nabu/reconstruction/cone.py +104 -40
- nabu/reconstruction/fbp.py +3 -0
- nabu/reconstruction/fbp_base.py +7 -2
- nabu/reconstruction/filtering.py +20 -7
- nabu/reconstruction/filtering_cuda.py +7 -1
- nabu/reconstruction/hbp.py +424 -0
- nabu/reconstruction/mlem.py +99 -0
- nabu/reconstruction/reconstructor.py +2 -0
- nabu/reconstruction/rings_cuda.py +19 -19
- nabu/reconstruction/sinogram_cuda.py +1 -0
- nabu/reconstruction/sinogram_opencl.py +3 -1
- nabu/reconstruction/tests/test_cone.py +10 -5
- nabu/reconstruction/tests/test_deringer.py +7 -6
- nabu/reconstruction/tests/test_fbp.py +124 -10
- nabu/reconstruction/tests/test_filtering.py +13 -11
- nabu/reconstruction/tests/test_halftomo.py +30 -4
- nabu/reconstruction/tests/test_mlem.py +91 -0
- nabu/reconstruction/tests/test_reconstructor.py +8 -3
- nabu/resources/dataset_analyzer.py +142 -92
- nabu/resources/gpu.py +1 -0
- nabu/resources/nxflatfield.py +134 -125
- nabu/resources/templates/id16a_fluo.conf +42 -0
- nabu/resources/tests/test_extract.py +10 -0
- nabu/resources/tests/test_nxflatfield.py +2 -2
- nabu/stitching/alignment.py +80 -24
- nabu/stitching/config.py +105 -68
- nabu/stitching/definitions.py +1 -0
- nabu/stitching/frame_composition.py +68 -60
- nabu/stitching/overlap.py +91 -51
- nabu/stitching/single_axis_stitching.py +32 -0
- nabu/stitching/slurm_utils.py +6 -6
- nabu/stitching/stitcher/__init__.py +0 -0
- nabu/stitching/stitcher/base.py +124 -0
- nabu/stitching/stitcher/dumper/__init__.py +3 -0
- nabu/stitching/stitcher/dumper/base.py +94 -0
- nabu/stitching/stitcher/dumper/postprocessing.py +356 -0
- nabu/stitching/stitcher/dumper/preprocessing.py +60 -0
- nabu/stitching/stitcher/post_processing.py +555 -0
- nabu/stitching/stitcher/pre_processing.py +1068 -0
- nabu/stitching/stitcher/single_axis.py +484 -0
- nabu/stitching/stitcher/stitcher.py +0 -0
- nabu/stitching/stitcher/y_stitcher.py +13 -0
- nabu/stitching/stitcher/z_stitcher.py +45 -0
- nabu/stitching/stitcher_2D.py +278 -0
- nabu/stitching/tests/test_config.py +12 -37
- nabu/stitching/tests/test_frame_composition.py +33 -59
- nabu/stitching/tests/test_overlap.py +149 -7
- nabu/stitching/tests/test_utils.py +1 -1
- nabu/stitching/tests/test_y_preprocessing_stitching.py +132 -0
- nabu/stitching/tests/{test_z_stitching.py → test_z_postprocessing_stitching.py} +167 -561
- nabu/stitching/tests/test_z_preprocessing_stitching.py +431 -0
- nabu/stitching/utils/__init__.py +1 -0
- nabu/stitching/utils/post_processing.py +281 -0
- nabu/stitching/utils/tests/test_post-processing.py +21 -0
- nabu/stitching/{utils.py → utils/utils.py} +79 -52
- nabu/stitching/y_stitching.py +27 -0
- nabu/stitching/z_stitching.py +32 -2263
- nabu/testutils.py +1 -152
- nabu/thirdparty/tomocupy_remove_stripe.py +43 -9
- nabu/utils.py +158 -61
- {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/METADATA +10 -3
- {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/RECORD +144 -121
- nabu/io/tiffwriter_zmm.py +0 -99
- nabu/pipeline/fallback_utils.py +0 -149
- nabu/pipeline/helical/tests/test_accumulator.py +0 -158
- nabu/pipeline/helical/tests/test_pipeline_elements_full.py +0 -355
- nabu/pipeline/helical/tests/test_strategy.py +0 -61
- nabu/pipeline/helical/utils.py +0 -51
- nabu/pipeline/tests/test_chunk_reader.py +0 -74
- {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/LICENSE +0 -0
- {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/WHEEL +0 -0
- {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/entry_points.txt +0 -0
- {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/top_level.txt +0 -0
@@ -236,7 +236,7 @@ class TestCone:
|
|
236
236
|
err_median_profile = np.median(diff, axis=(-1, -2))
|
237
237
|
|
238
238
|
assert np.max(err_max_profile) < 2e-3
|
239
|
-
assert np.max(err_median_profile) <
|
239
|
+
assert np.max(err_median_profile) < 5.1e-6
|
240
240
|
|
241
241
|
def test_reconstruction_horizontal_translations(self):
|
242
242
|
n_z = n_y = n_x = 256
|
@@ -273,7 +273,7 @@ class TestCone:
|
|
273
273
|
# Error tolerance has to be higher for these shifts.
|
274
274
|
for shift_type, shifts, err_tol in [
|
275
275
|
("integer shifts", shifts_int, 5e-3),
|
276
|
-
("float shifts", shifts_float, 1.
|
276
|
+
("float shifts", shifts_float, 1.5e-1),
|
277
277
|
]:
|
278
278
|
cone_data_shifted = np.zeros_like(cone_data)
|
279
279
|
[shift(cone_data[:, i, :], (0, shifts[i]), output=cone_data_shifted[:, i, :]) for i in range(n_a)]
|
@@ -281,8 +281,8 @@ class TestCone:
|
|
281
281
|
# Reconstruct with horizontal shifts
|
282
282
|
cone_reconstructor_with_correction = ConebeamReconstructor(
|
283
283
|
*reconstructor_args,
|
284
|
-
axis_correction=shifts,
|
285
284
|
**reconstructor_kwargs,
|
285
|
+
extra_options={"axis_correction": -shifts},
|
286
286
|
)
|
287
287
|
|
288
288
|
rec_with_correction = cone_reconstructor_with_correction.reconstruct(cone_data_shifted)
|
@@ -330,7 +330,6 @@ class TestCone:
|
|
330
330
|
|
331
331
|
metric = lambda img: np.max(np.abs(clip_circle(img, radius=int(0.85 * 128))))
|
332
332
|
error_profile = np.array([metric(rec[i] - rec_z[i]) for i in range(n_z)])
|
333
|
-
assert error_profile.max() < 3e-2, "Max error for padding=%s is too high" % padding_mode
|
334
333
|
|
335
334
|
# import matplotlib.pyplot as plt
|
336
335
|
# plt.figure()
|
@@ -338,6 +337,12 @@ class TestCone:
|
|
338
337
|
# plt.legend([padding_mode])
|
339
338
|
# plt.show()
|
340
339
|
|
340
|
+
assert error_profile.max() < 3.1e-2, "Max error for padding=%s is too high" % padding_mode
|
341
|
+
if padding_mode != "zeros":
|
342
|
+
assert not (np.allclose(rec[n_z // 2], rec_z[n_z // 2])), (
|
343
|
+
"Reconstruction should be different when padding_mode=%s" % padding_mode
|
344
|
+
)
|
345
|
+
|
341
346
|
def test_roi(self):
|
342
347
|
n_z = n_y = n_x = 256
|
343
348
|
n_a = 500
|
@@ -367,7 +372,7 @@ class TestCone:
|
|
367
372
|
ref = cone_reconstructor_full.reconstruct(cone_data)
|
368
373
|
|
369
374
|
# roi is in the form (start_x, end_x, start_y, end_y)
|
370
|
-
for roi in ((20, -20, 10, -10), (0, n_x, 0, n_y), (
|
375
|
+
for roi in ((20, -20, 10, -10), (0, n_x, 0, n_y), (50, -50, 15, -15)):
|
371
376
|
# convert negative indices
|
372
377
|
start_x, end_x, start_y, end_y = roi
|
373
378
|
if start_y < 0:
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import numpy as np
|
2
2
|
import pytest
|
3
3
|
from nabu.reconstruction.rings_cuda import CudaSinoMeanDeringer
|
4
|
-
from nabu.testutils import compare_arrays, get_data, generate_tests_scenarios, __do_long_tests__
|
4
|
+
from nabu.testutils import compare_arrays, get_data, generate_tests_scenarios, __do_long_tests__
|
5
5
|
from nabu.reconstruction.rings import MunchDeringer, SinoMeanDeringer, VoDeringer, __has_algotom__
|
6
6
|
from nabu.thirdparty.pore3d_deringer_munch import munchetal_filter
|
7
7
|
from nabu.cuda.utils import __has_pycuda__, get_cuda_context
|
@@ -9,11 +9,12 @@ from nabu.cuda.utils import __has_pycuda__, get_cuda_context
|
|
9
9
|
if __has_pycuda__:
|
10
10
|
import pycuda.gpuarray as garray
|
11
11
|
from nabu.processing.fft_cuda import get_available_fft_implems
|
12
|
+
from nabu.thirdparty.tomocupy_remove_stripe import __have_tomocupy_deringer__
|
13
|
+
|
12
14
|
from nabu.reconstruction.rings_cuda import (
|
13
15
|
CudaMunchDeringer,
|
14
16
|
can_use_cuda_deringer,
|
15
17
|
CudaVoDeringer,
|
16
|
-
__have_tomocupy_deringer__,
|
17
18
|
)
|
18
19
|
|
19
20
|
__has_cuda_deringer__ = can_use_cuda_deringer()
|
@@ -27,7 +28,7 @@ fw_scenarios = generate_tests_scenarios(
|
|
27
28
|
"sigma": [1.0],
|
28
29
|
"wname": ["db15"],
|
29
30
|
"padding": [(100, 100)],
|
30
|
-
"fft_implem": ["
|
31
|
+
"fft_implem": ["vkfft"],
|
31
32
|
}
|
32
33
|
)
|
33
34
|
if __do_long_tests__:
|
@@ -58,7 +59,7 @@ def bootstrap(request):
|
|
58
59
|
|
59
60
|
|
60
61
|
@pytest.mark.usefixtures("bootstrap")
|
61
|
-
class
|
62
|
+
class TestDeringer:
|
62
63
|
@staticmethod
|
63
64
|
def add_stripes_to_sino(sino, rings_desc):
|
64
65
|
"""
|
@@ -144,8 +145,8 @@ class TestMunchDeringer:
|
|
144
145
|
# TODO check result. The generated test sinogram is "too synthetic" for this kind of deringer
|
145
146
|
|
146
147
|
@pytest.mark.skipif(
|
147
|
-
not (__have_tomocupy_deringer__
|
148
|
-
reason="Need cupy for this test
|
148
|
+
not (__have_tomocupy_deringer__),
|
149
|
+
reason="Need cupy for this test",
|
149
150
|
)
|
150
151
|
def test_cuda_vo_deringer(self):
|
151
152
|
# Beware, this deringer seems to be buggy for "too-small" sinograms
|
@@ -15,6 +15,7 @@ __has_pyopencl__ = __has_pyopencl__ and has_vkfft_cl()
|
|
15
15
|
|
16
16
|
if __has_pycuda__:
|
17
17
|
from nabu.reconstruction.fbp import CudaBackprojector
|
18
|
+
from nabu.reconstruction.hbp import HierarchicalBackprojector
|
18
19
|
if __has_pyopencl__:
|
19
20
|
from nabu.reconstruction.fbp_opencl import OpenCLBackprojector
|
20
21
|
|
@@ -50,12 +51,13 @@ def bootstrap(request):
|
|
50
51
|
cls.cuda_ctx.pop()
|
51
52
|
|
52
53
|
|
54
|
+
def clip_to_inner_circle(img, radius_factor=0.99, out_value=0):
|
55
|
+
radius = int(radius_factor * max(img.shape) / 2)
|
56
|
+
return clip_circle(img, radius=radius, out_value=out_value)
|
57
|
+
|
58
|
+
|
53
59
|
@pytest.mark.usefixtures("bootstrap")
|
54
60
|
class TestFBP:
|
55
|
-
@staticmethod
|
56
|
-
def clip_to_inner_circle(img, radius_factor=0.99):
|
57
|
-
radius = int(radius_factor * max(img.shape) / 2)
|
58
|
-
return clip_circle(img, radius=radius)
|
59
61
|
|
60
62
|
def _get_backprojector(self, config, *bp_args, **bp_kwargs):
|
61
63
|
if config["backend"] == "cuda":
|
@@ -96,7 +98,7 @@ class TestFBP:
|
|
96
98
|
B = self._get_backprojector(config, (500, 512))
|
97
99
|
res = self.apply_fbp(config, B, self.sino_512)
|
98
100
|
|
99
|
-
delta_clipped =
|
101
|
+
delta_clipped = clip_to_inner_circle(res - self.ref_512)
|
100
102
|
err_max = np.max(np.abs(delta_clipped))
|
101
103
|
|
102
104
|
assert err_max < self.tol, "Something wrong with config=%s" % (str(config))
|
@@ -110,7 +112,7 @@ class TestFBP:
|
|
110
112
|
res = self.apply_fbp(config, B, self.sino_511)
|
111
113
|
ref = self.ref_512[:-1, :-1]
|
112
114
|
|
113
|
-
delta_clipped =
|
115
|
+
delta_clipped = clip_to_inner_circle(res - ref)
|
114
116
|
err_max = np.max(np.abs(delta_clipped))
|
115
117
|
|
116
118
|
assert err_max < self.tol, "Something wrong with config=%s" % (str(config))
|
@@ -191,12 +193,24 @@ class TestFBP:
|
|
191
193
|
config, sino.shape, rot_center=rot_center, extra_options={"clip_outer_circle": False}
|
192
194
|
)
|
193
195
|
res_noclip = B0.fbp(sino)
|
194
|
-
ref =
|
195
|
-
|
196
|
+
ref = clip_to_inner_circle(res_noclip, radius_factor=1)
|
196
197
|
abs_diff = np.abs(res - ref)
|
197
198
|
err_max = np.max(abs_diff)
|
198
199
|
assert err_max < tol, "Max error is too high for rot_center=%s ; %s" % (str(rot_center), str(config))
|
199
200
|
|
201
|
+
# Test with custom outer circle value
|
202
|
+
B1 = self._get_backprojector(
|
203
|
+
config,
|
204
|
+
sino.shape,
|
205
|
+
rot_center=rot_center,
|
206
|
+
extra_options={"clip_outer_circle": True, "outer_circle_value": np.nan},
|
207
|
+
)
|
208
|
+
res1 = self.apply_fbp(config, B1, sino)
|
209
|
+
ref1 = clip_to_inner_circle(res_noclip, radius_factor=1, out_value=np.nan)
|
210
|
+
abs_diff1 = np.abs(res1 - ref1)
|
211
|
+
err_max1 = np.nanmax(abs_diff1)
|
212
|
+
assert err_max1 < tol, "Max error is too high for rot_center=%s ; %s" % (str(rot_center), str(config))
|
213
|
+
|
200
214
|
@pytest.mark.parametrize("config", scenarios)
|
201
215
|
def test_fbp_centered_axis(self, config):
|
202
216
|
"""
|
@@ -213,7 +227,7 @@ class TestFBP:
|
|
213
227
|
B = self._get_backprojector(config, sino.shape, rot_center=rot_center, extra_options={"centered_axis": True})
|
214
228
|
res = self.apply_fbp(config, B, sino)
|
215
229
|
# The outside region (outer circle) is different as "res" is a wider slice
|
216
|
-
diff =
|
230
|
+
diff = clip_to_inner_circle(res[50:-50, 50:-50] - ref)
|
217
231
|
err_max = np.max(np.abs(diff))
|
218
232
|
assert err_max < 5e-2, "centered_axis without clip_circle: something wrong"
|
219
233
|
|
@@ -228,7 +242,7 @@ class TestFBP:
|
|
228
242
|
},
|
229
243
|
)
|
230
244
|
res2 = self.apply_fbp(config, B, sino)
|
231
|
-
diff = res2 -
|
245
|
+
diff = res2 - clip_to_inner_circle(res, radius_factor=1)
|
232
246
|
err_max = np.max(np.abs(diff))
|
233
247
|
assert err_max < 1e-5, "centered_axis with clip_circle: something wrong"
|
234
248
|
|
@@ -250,3 +264,103 @@ class TestFBP:
|
|
250
264
|
B = self._get_backprojector(config, sino_diff.shape, filter_name="hilbert", rot_center=255.5 + 0.5)
|
251
265
|
rec = self.apply_fbp(config, B, sino_diff)
|
252
266
|
# Looks good, but all frequencies are not recovered. Use a metric like SSIM or FRC ?
|
267
|
+
|
268
|
+
|
269
|
+
@pytest.mark.skipif(not (__has_pycuda__), reason="Need pycuda for using HBP")
|
270
|
+
@pytest.mark.usefixtures("bootstrap")
|
271
|
+
class TestHBP:
|
272
|
+
|
273
|
+
def _compare_to_reference(self, res, ref, err_msg="", radius_factor=0.9, rel_tol=0.02):
|
274
|
+
delta_clipped = clip_to_inner_circle(res - ref, radius_factor=radius_factor)
|
275
|
+
err_max = np.max(np.abs(delta_clipped))
|
276
|
+
err_max_rel = err_max / ref.max()
|
277
|
+
assert err_max_rel < rel_tol, err_msg
|
278
|
+
|
279
|
+
def test_hbp_simple(self):
|
280
|
+
B = HierarchicalBackprojector(self.sino_512.shape)
|
281
|
+
res = B.fbp(self.sino_512)
|
282
|
+
self._compare_to_reference(res, self.ref_512)
|
283
|
+
|
284
|
+
def test_hbp_input_output(self):
|
285
|
+
B = HierarchicalBackprojector(self.sino_512.shape)
|
286
|
+
|
287
|
+
d_sino = B._processing.to_device("d_sino2", self.sino_512)
|
288
|
+
d_slice = B._processing.allocate_array("d_slice2", self.ref_512.shape)
|
289
|
+
h_slice = np.zeros_like(self.ref_512)
|
290
|
+
|
291
|
+
# in: host, out: host (not provided)
|
292
|
+
# see test above
|
293
|
+
|
294
|
+
# in: host, out: host (provided)
|
295
|
+
res = B.fbp(self.sino_512, output=h_slice)
|
296
|
+
self._compare_to_reference(h_slice, self.ref_512, err_msg="in: host, out: host (provided)")
|
297
|
+
h_slice.fill(0)
|
298
|
+
|
299
|
+
# in: host, out: device
|
300
|
+
res = B.fbp(self.sino_512, output=d_slice)
|
301
|
+
self._compare_to_reference(d_slice.get(), self.ref_512, err_msg="in: host, out: device")
|
302
|
+
d_slice.fill(0)
|
303
|
+
|
304
|
+
# in: device, out: host (not provided)
|
305
|
+
res = B.fbp(d_sino)
|
306
|
+
self._compare_to_reference(res, self.ref_512, err_msg="in: device, out: host (not provided)")
|
307
|
+
|
308
|
+
# in: device, out: host (provided)
|
309
|
+
res = B.fbp(d_sino, output=h_slice)
|
310
|
+
self._compare_to_reference(h_slice, self.ref_512, err_msg="in: device, out: host (provided)")
|
311
|
+
h_slice.fill(0)
|
312
|
+
|
313
|
+
# in: device, out: device
|
314
|
+
res = B.fbp(d_sino, output=d_slice)
|
315
|
+
self._compare_to_reference(d_slice.get(), self.ref_512, err_msg="in: device, out: device")
|
316
|
+
d_slice.fill(0)
|
317
|
+
|
318
|
+
def test_hbp_cor(self):
|
319
|
+
"""
|
320
|
+
Test HBP with various sinogram shapes, obtained by truncating horizontally the original sinogram.
|
321
|
+
The Center of rotation is always 255.5 (the one of original sinogram), so it also tests reconstruction with a shifted CoR.
|
322
|
+
"""
|
323
|
+
for crop in [1, 2, 5, 10]:
|
324
|
+
sino = np.ascontiguousarray(self.sino_512[:, :-crop])
|
325
|
+
B = HierarchicalBackprojector(sino.shape, rot_center=255.5)
|
326
|
+
res = B.fbp(sino)
|
327
|
+
|
328
|
+
# HBP always uses "centered_axis=1", so we cannot compare non-integer shifts
|
329
|
+
if crop % 2 == 0:
|
330
|
+
ref = self.ref_512[crop // 2 : -crop // 2, crop // 2 : -crop // 2]
|
331
|
+
self._compare_to_reference(res, ref, radius_factor=0.95, rel_tol=0.02)
|
332
|
+
|
333
|
+
def test_hbp_clip_circle(self):
|
334
|
+
B_clip = HierarchicalBackprojector(self.sino_512.shape, extra_options={"clip_outer_circle": True})
|
335
|
+
B_noclip = HierarchicalBackprojector(self.sino_512.shape, extra_options={"clip_outer_circle": False})
|
336
|
+
res_clip = B_clip.fbp(self.sino_512)
|
337
|
+
res_noclip = B_noclip.fbp(self.sino_512)
|
338
|
+
self._compare_to_reference(res_clip, clip_to_inner_circle(res_noclip, radius_factor=1), "clip_circle")
|
339
|
+
|
340
|
+
def test_hbp_axis_corr(self):
|
341
|
+
sino = self.sino_512
|
342
|
+
|
343
|
+
# Create a sinogram with a drift in the rotation axis
|
344
|
+
def create_drifted_sino(sino, drifts):
|
345
|
+
out = np.zeros_like(sino)
|
346
|
+
for i in range(sino.shape[0]):
|
347
|
+
out[i] = shift(sino[i], drifts[i])
|
348
|
+
return out
|
349
|
+
|
350
|
+
drifts = np.linspace(0, 20, sino.shape[0])
|
351
|
+
sino = create_drifted_sino(sino, drifts)
|
352
|
+
|
353
|
+
B = HierarchicalBackprojector(sino.shape, extra_options={"axis_correction": drifts})
|
354
|
+
res = B.fbp(sino)
|
355
|
+
|
356
|
+
# Max error is relatively high, migh be due to interpolation of scipy shift in sinogram
|
357
|
+
self._compare_to_reference(res, self.ref_512, radius_factor=0.95, rel_tol=0.04, err_msg="axis_corr")
|
358
|
+
|
359
|
+
@pytest.mark.skipif(not (__do_long_tests__), reason="need NABU_LONG_TESTS=1 for this test")
|
360
|
+
def test_hbp_scale_factor(self):
|
361
|
+
scale_factor = 0.03125
|
362
|
+
B_scaled = HierarchicalBackprojector(self.sino_512.shape, extra_options={"scale_factor": scale_factor})
|
363
|
+
B_unscaled = HierarchicalBackprojector(self.sino_512.shape)
|
364
|
+
res_scaled = B_scaled.fbp(self.sino_512)
|
365
|
+
res_unscaled = B_unscaled.fbp(self.sino_512)
|
366
|
+
self._compare_to_reference(res_scaled, res_unscaled * scale_factor, rel_tol=1e-7, err_msg="scale_factor")
|
@@ -25,6 +25,7 @@ tests_scenarios = generate_tests_scenarios(
|
|
25
25
|
"filter_name": filters_to_test,
|
26
26
|
"padding_mode": padding_modes_to_test,
|
27
27
|
"output_provided": [True, False],
|
28
|
+
"truncated_sino": [True, False],
|
28
29
|
}
|
29
30
|
)
|
30
31
|
|
@@ -33,12 +34,16 @@ tests_scenarios = generate_tests_scenarios(
|
|
33
34
|
def bootstrap(request):
|
34
35
|
cls = request.cls
|
35
36
|
cls.sino = get_data("mri_sino500.npz")["data"]
|
37
|
+
cls.sino_truncated = np.ascontiguousarray(cls.sino[:, 160:-160])
|
38
|
+
|
36
39
|
if __has_pycuda__:
|
37
40
|
cls.ctx_cuda = get_cuda_context(cleanup_at_exit=False)
|
38
41
|
cls.sino_cuda = garray.to_gpu(cls.sino)
|
42
|
+
cls.sino_truncated_cuda = garray.to_gpu(cls.sino_truncated)
|
39
43
|
if __has_pyopencl__:
|
40
44
|
cls.cl = OpenCLProcessing(device_type="all")
|
41
45
|
cls.sino_cl = parray.to_device(cls.cl.queue, cls.sino)
|
46
|
+
cls.sino_truncated_cl = parray.to_device(cls.cl.queue, cls.sino_truncated)
|
42
47
|
|
43
48
|
yield
|
44
49
|
|
@@ -50,7 +55,7 @@ def bootstrap(request):
|
|
50
55
|
class TestSinoFilter:
|
51
56
|
@pytest.mark.parametrize("config", tests_scenarios)
|
52
57
|
def test_filter(self, config):
|
53
|
-
sino = self.sino
|
58
|
+
sino = self.sino if not (config["truncated_sino"]) else self.sino_truncated
|
54
59
|
|
55
60
|
sino_filter = SinoFilter(
|
56
61
|
sino.shape,
|
@@ -69,12 +74,13 @@ class TestSinoFilter:
|
|
69
74
|
sino, sino_filter.dwidth_padded, filter_name=config["filter_name"], padding_mode=config["padding_mode"]
|
70
75
|
)
|
71
76
|
|
72
|
-
assert np.allclose(res, ref, atol=
|
77
|
+
assert np.allclose(res, ref, atol=4e-6)
|
73
78
|
|
74
79
|
@pytest.mark.skipif(not (__has_pycuda__), reason="Need Cuda + pycuda to use CudaSinoFilter")
|
75
80
|
@pytest.mark.parametrize("config", tests_scenarios)
|
76
81
|
def test_cuda_filter(self, config):
|
77
|
-
sino = self.sino_cuda
|
82
|
+
sino = self.sino_cuda if not (config["truncated_sino"]) else self.sino_truncated_cuda
|
83
|
+
h_sino = self.sino if not (config["truncated_sino"]) else self.sino_truncated
|
78
84
|
|
79
85
|
sino_filter = CudaSinoFilter(
|
80
86
|
sino.shape,
|
@@ -91,14 +97,9 @@ class TestSinoFilter:
|
|
91
97
|
assert id(res) == id(output), "when providing output, return value must not change"
|
92
98
|
|
93
99
|
ref = filter_sinogram(
|
94
|
-
|
100
|
+
h_sino, sino_filter.dwidth_padded, filter_name=config["filter_name"], padding_mode=config["padding_mode"]
|
95
101
|
)
|
96
102
|
|
97
|
-
if not np.allclose(res.get(), ref, atol=6e-5):
|
98
|
-
from spire.utils import ims
|
99
|
-
|
100
|
-
ims([res.get(), ref, res.get() - ref])
|
101
|
-
|
102
103
|
assert np.allclose(res.get(), ref, atol=6e-5), "test_cuda_filter: something wrong with config=%s" % (
|
103
104
|
str(config)
|
104
105
|
)
|
@@ -108,7 +109,8 @@ class TestSinoFilter:
|
|
108
109
|
)
|
109
110
|
@pytest.mark.parametrize("config", tests_scenarios)
|
110
111
|
def test_opencl_filter(self, config):
|
111
|
-
sino = self.sino_cl
|
112
|
+
sino = self.sino_cl if not (config["truncated_sino"]) else self.sino_truncated_cl
|
113
|
+
h_sino = self.sino if not (config["truncated_sino"]) else self.sino_truncated
|
112
114
|
|
113
115
|
sino_filter = OpenCLSinoFilter(
|
114
116
|
sino.shape,
|
@@ -125,7 +127,7 @@ class TestSinoFilter:
|
|
125
127
|
assert id(res) == id(output), "when providing output, return value must not change"
|
126
128
|
|
127
129
|
ref = filter_sinogram(
|
128
|
-
|
130
|
+
h_sino, sino_filter.dwidth_padded, filter_name=config["filter_name"], padding_mode=config["padding_mode"]
|
129
131
|
)
|
130
132
|
|
131
133
|
assert np.allclose(res.get(), ref, atol=6e-5), "test_opencl_filter: something wrong with config=%s" % (
|
@@ -1,16 +1,21 @@
|
|
1
1
|
import numpy as np
|
2
2
|
import pytest
|
3
|
+
from nabu.processing.fft_cuda import get_available_fft_implems
|
3
4
|
from nabu.testutils import get_data, generate_tests_scenarios, compare_shifted_images
|
4
|
-
from nabu.cuda.utils import get_cuda_context, __has_pycuda__
|
5
|
+
from nabu.cuda.utils import get_cuda_context, __has_pycuda__
|
5
6
|
from nabu.opencl.utils import get_opencl_context, __has_pyopencl__
|
6
|
-
from nabu.processing.fft_opencl import has_vkfft as has_vkfft_cl
|
7
7
|
from nabu.thirdparty.algotom_convert_sino import extend_sinogram
|
8
8
|
|
9
|
+
__has_cufft__ = False
|
10
|
+
if __has_pycuda__:
|
11
|
+
avail_fft = get_available_fft_implems()
|
12
|
+
__has_cufft__ = len(avail_fft) > 0
|
9
13
|
__has_pycuda__ = __has_pycuda__ and __has_cufft__ # need both for using Cuda backprojector
|
10
|
-
|
14
|
+
|
11
15
|
|
12
16
|
if __has_pycuda__:
|
13
17
|
from nabu.reconstruction.fbp import CudaBackprojector
|
18
|
+
from nabu.reconstruction.hbp import HierarchicalBackprojector
|
14
19
|
if __has_pyopencl__:
|
15
20
|
from nabu.reconstruction.fbp_opencl import OpenCLBackprojector
|
16
21
|
|
@@ -37,7 +42,7 @@ class TestHalftomo:
|
|
37
42
|
def _get_backprojector(self, config, *bp_args, **bp_kwargs):
|
38
43
|
if config["backend"] == "cuda":
|
39
44
|
if not (__has_pycuda__):
|
40
|
-
pytest.skip("Need pycuda + scikit-cuda")
|
45
|
+
pytest.skip("Need pycuda + scikit-cuda or vkfft")
|
41
46
|
Backprojector = CudaBackprojector
|
42
47
|
ctx = self.cuda_ctx
|
43
48
|
else:
|
@@ -99,3 +104,24 @@ class TestHalftomo:
|
|
99
104
|
backprojector = self._get_backprojector(config, sino.shape, rot_center=self.rot_center, halftomo=True)
|
100
105
|
res = backprojector.fbp(sino)
|
101
106
|
# Just check that it runs, but no reference results. Who does this anyway ?!
|
107
|
+
|
108
|
+
@pytest.mark.skipif(not (__has_pycuda__), reason="Need pycuda")
|
109
|
+
def test_hbp_halftomo(self, config):
|
110
|
+
if config["backend"] == "opencl":
|
111
|
+
pytest.skip("No HBP available in OpenCL")
|
112
|
+
B = HierarchicalBackprojector(self.sino.shape, halftomo=True, rot_center=self.rot_center, padding_mode="edge")
|
113
|
+
res = B.fbp(self.sino)
|
114
|
+
|
115
|
+
sino_extended, rot_center_ext = extend_sinogram(self.sino, self.rot_center, apply_log=False)
|
116
|
+
sino_extended *= 2 # compat. with nabu normalization
|
117
|
+
B_extended = HierarchicalBackprojector(
|
118
|
+
sino_extended.shape,
|
119
|
+
rot_center=rot_center_ext,
|
120
|
+
padding_mode="edge",
|
121
|
+
angles=np.linspace(0, 2 * np.pi, self.sino.shape[0], True),
|
122
|
+
)
|
123
|
+
res_e = B_extended.fbp(sino_extended)
|
124
|
+
|
125
|
+
# see notes in test_halftomo_right_side()
|
126
|
+
metric, upper_bound = compare_shifted_images(res, res_e, return_upper_bound=True)
|
127
|
+
assert metric < 5, "Something wrong for halftomo with HBP"
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import pytest
|
2
|
+
import numpy as np
|
3
|
+
from nabu.testutils import get_data, __do_long_tests__
|
4
|
+
|
5
|
+
from nabu.cuda.utils import __has_pycuda__
|
6
|
+
from nabu.reconstruction.mlem import MLEMReconstructor, __have_corrct__
|
7
|
+
|
8
|
+
|
9
|
+
@pytest.fixture(scope="class")
|
10
|
+
def bootstrap(request):
|
11
|
+
cls = request.cls
|
12
|
+
datafile = get_data("sl_mlem.npz")
|
13
|
+
cls.data = datafile["data"]
|
14
|
+
cls.angles_rad = datafile["angles_rad"]
|
15
|
+
cls.random_u_shifts = datafile["random_u_shifts"]
|
16
|
+
cls.ref_rec_noshifts = datafile["ref_rec_noshifts"]
|
17
|
+
cls.ref_rec_shiftsu = datafile["ref_rec_shiftsu"]
|
18
|
+
cls.ref_rec_u_rand = datafile["ref_rec_u_rand"]
|
19
|
+
cls.ref_rec_shiftsv = datafile["ref_rec_shiftsv"]
|
20
|
+
# cls.ref_rec_v_rand = datafile["ref_rec_v_rand"]
|
21
|
+
cls.tol = 2e-4
|
22
|
+
|
23
|
+
|
24
|
+
@pytest.mark.skipif(not (__has_pycuda__ and __have_corrct__), reason="Need pycuda and corrct for this test")
|
25
|
+
@pytest.mark.usefixtures("bootstrap")
|
26
|
+
class TestMLEM:
|
27
|
+
"""These tests test the general MLEM reconstruction algorithm
|
28
|
+
and the behavior of the reconstruction with respect to horizontal shifts.
|
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"""
|
31
|
+
|
32
|
+
def _create_MLEM_reconstructor(self, shifts_uv=None):
|
33
|
+
return MLEMReconstructor(
|
34
|
+
self.data.shape, -self.angles_rad, shifts_uv, cor=0.0, n_iterations=10 # mind the sign
|
35
|
+
)
|
36
|
+
|
37
|
+
def test_simple_mlem_recons(self):
|
38
|
+
R = self._create_MLEM_reconstructor()
|
39
|
+
rec = R.reconstruct(self.data)
|
40
|
+
delta = np.abs(rec[:, ::-1, :] - self.ref_rec_noshifts)
|
41
|
+
assert np.max(delta) < self.tol
|
42
|
+
|
43
|
+
def test_mlem_recons_with_u_shifts(self):
|
44
|
+
shifts = np.zeros((len(self.angles_rad), 2))
|
45
|
+
shifts[:, 0] = -5
|
46
|
+
R = self._create_MLEM_reconstructor(shifts)
|
47
|
+
rec = R.reconstruct(self.data)
|
48
|
+
delta = np.abs(rec[:, ::-1] - self.ref_rec_shiftsu)
|
49
|
+
assert np.max(delta) < self.tol
|
50
|
+
|
51
|
+
def test_mlem_recons_with_random_u_shifts(self):
|
52
|
+
R = self._create_MLEM_reconstructor(self.random_u_shifts)
|
53
|
+
rec = R.reconstruct(self.data)
|
54
|
+
delta = np.abs(rec[:, ::-1] - self.ref_rec_u_rand)
|
55
|
+
assert np.max(delta) < self.tol
|
56
|
+
|
57
|
+
def test_mlem_recons_with_constant_v_shifts(self):
|
58
|
+
from nabu.preproc.shift import VerticalShift
|
59
|
+
|
60
|
+
shifts = np.zeros((len(self.angles_rad), 2))
|
61
|
+
shifts[:, 1] = -20
|
62
|
+
|
63
|
+
nv, n_angles, nu = self.data.shape
|
64
|
+
radios_movements = VerticalShift(
|
65
|
+
(n_angles, nv, nu), -shifts[:, 1]
|
66
|
+
) # Minus sign here mimics what is done in the pipeline.
|
67
|
+
tmp_in = np.swapaxes(self.data, 0, 1).copy()
|
68
|
+
tmp_out = np.zeros_like(tmp_in)
|
69
|
+
radios_movements.apply_vertical_shifts(tmp_in, list(range(n_angles)), output=tmp_out)
|
70
|
+
data = np.swapaxes(tmp_out, 0, 1).copy()
|
71
|
+
|
72
|
+
R = self._create_MLEM_reconstructor(shifts)
|
73
|
+
rec = R.reconstruct(data)
|
74
|
+
|
75
|
+
axslice = 120
|
76
|
+
trslice = 84
|
77
|
+
axslice1 = self.ref_rec_shiftsv[axslice]
|
78
|
+
axslice2 = rec[axslice, ::-1]
|
79
|
+
trslice1 = self.ref_rec_shiftsv[trslice]
|
80
|
+
trslice2 = rec[trslice, ::-1]
|
81
|
+
# delta = np.abs(rec[:, ::-1] - self.ref_rec_shiftsv)
|
82
|
+
delta_ax = np.abs(axslice1 - axslice2)
|
83
|
+
delta_tr = np.abs(trslice1 - trslice2)
|
84
|
+
assert max(np.max(delta_ax), np.max(delta_tr)) < self.tol
|
85
|
+
|
86
|
+
@pytest.mark.skip(reason="No valid reference reconstruction for this test.")
|
87
|
+
def test_mlem_recons_with_random_v_shifts(self):
|
88
|
+
"""NOT YET IMPLEMENTED.
|
89
|
+
This is a temporary version due to unpexcted behavior of CorrCT/Astra to
|
90
|
+
compute a reference implementation. See [question on Astra's github](https://github.com/astra-toolbox/astra-toolbox/discussions/520).
|
91
|
+
"""
|
@@ -1,13 +1,18 @@
|
|
1
1
|
import numpy as np
|
2
2
|
import pytest
|
3
|
+
from nabu.processing.fft_cuda import get_available_fft_implems
|
3
4
|
from nabu.testutils import (
|
4
5
|
get_big_data,
|
5
6
|
__big_testdata_dir__,
|
6
|
-
compare_arrays,
|
7
7
|
generate_tests_scenarios,
|
8
8
|
__do_long_tests__,
|
9
9
|
)
|
10
|
-
from nabu.cuda.utils import __has_pycuda__,
|
10
|
+
from nabu.cuda.utils import __has_pycuda__, get_cuda_context
|
11
|
+
|
12
|
+
__has_cufft__ = False
|
13
|
+
if __has_pycuda__:
|
14
|
+
avail_fft = get_available_fft_implems()
|
15
|
+
__has_cufft__ = len(avail_fft) > 0
|
11
16
|
|
12
17
|
__has_cuda_fbp__ = __has_cufft__ and __has_pycuda__
|
13
18
|
if __has_cuda_fbp__:
|
@@ -43,7 +48,7 @@ def bootstrap(request):
|
|
43
48
|
)
|
44
49
|
@pytest.mark.usefixtures("bootstrap")
|
45
50
|
class TestReconstructor:
|
46
|
-
@pytest.mark.skipif(not (__has_cuda_fbp__), reason="need pycuda and scikit-cuda")
|
51
|
+
@pytest.mark.skipif(not (__has_cuda_fbp__), reason="need pycuda and (scikit-cuda or vkfft)")
|
47
52
|
@pytest.mark.parametrize("config", scenarios)
|
48
53
|
def test_cuda_reconstructor(self, config):
|
49
54
|
data = self.projs
|