nabu 2024.2.14__py3-none-any.whl → 2025.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- doc/doc_config.py +32 -0
- nabu/__init__.py +1 -1
- nabu/app/bootstrap_stitching.py +4 -2
- nabu/app/cast_volume.py +16 -14
- nabu/app/cli_configs.py +102 -9
- nabu/app/compare_volumes.py +1 -1
- nabu/app/composite_cor.py +2 -4
- nabu/app/diag_to_pix.py +5 -6
- nabu/app/diag_to_rot.py +10 -11
- nabu/app/double_flatfield.py +18 -5
- nabu/app/estimate_motion.py +75 -0
- nabu/app/multicor.py +28 -15
- nabu/app/parse_reconstruction_log.py +1 -0
- nabu/app/pcaflats.py +122 -0
- nabu/app/prepare_weights_double.py +1 -2
- nabu/app/reconstruct.py +1 -7
- nabu/app/reconstruct_helical.py +5 -9
- nabu/app/reduce_dark_flat.py +5 -4
- nabu/app/rotate.py +3 -1
- nabu/app/stitching.py +7 -2
- 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/backproj.cu +6 -6
- nabu/cuda/src/cone.cu +4 -0
- nabu/cuda/src/hierarchical_backproj.cu +14 -0
- nabu/cuda/utils.py +2 -2
- nabu/estimation/alignment.py +17 -31
- nabu/estimation/cor.py +27 -33
- nabu/estimation/cor_sino.py +2 -8
- nabu/estimation/focus.py +4 -8
- nabu/estimation/motion.py +557 -0
- nabu/estimation/tests/test_alignment.py +2 -0
- nabu/estimation/tests/test_motion_estimation.py +471 -0
- nabu/estimation/tests/test_tilt.py +1 -1
- nabu/estimation/tilt.py +6 -5
- nabu/estimation/translation.py +47 -1
- nabu/io/cast_volume.py +108 -18
- nabu/io/detector_distortion.py +5 -6
- nabu/io/reader.py +45 -6
- nabu/io/reader_helical.py +5 -4
- nabu/io/tests/test_cast_volume.py +2 -2
- nabu/io/tests/test_readers.py +41 -38
- nabu/io/tests/test_remove_volume.py +152 -0
- 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/src/backproj.cl +6 -6
- nabu/opencl/utils.py +8 -8
- nabu/pipeline/config.py +2 -2
- nabu/pipeline/config_validators.py +46 -46
- nabu/pipeline/datadump.py +3 -3
- nabu/pipeline/estimators.py +271 -11
- nabu/pipeline/fullfield/chunked.py +103 -67
- nabu/pipeline/fullfield/chunked_cuda.py +5 -2
- nabu/pipeline/fullfield/computations.py +4 -1
- nabu/pipeline/fullfield/dataset_validator.py +0 -1
- nabu/pipeline/fullfield/get_double_flatfield.py +147 -0
- nabu/pipeline/fullfield/nabu_config.py +36 -17
- nabu/pipeline/fullfield/processconfig.py +41 -7
- nabu/pipeline/fullfield/reconstruction.py +14 -10
- 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 +10 -11
- nabu/pipeline/helical/helical_chunked_regridded.py +1 -0
- nabu/pipeline/helical/helical_reconstruction.py +12 -9
- nabu/pipeline/helical/helical_utils.py +1 -2
- nabu/pipeline/helical/nabu_config.py +2 -1
- nabu/pipeline/helical/span_strategy.py +1 -0
- nabu/pipeline/helical/weight_balancer.py +2 -3
- nabu/pipeline/params.py +20 -3
- nabu/pipeline/tests/__init__.py +0 -0
- nabu/pipeline/tests/test_estimators.py +240 -3
- nabu/pipeline/utils.py +1 -1
- nabu/pipeline/writer.py +1 -1
- nabu/preproc/alignment.py +0 -10
- nabu/preproc/ccd.py +53 -3
- 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 +307 -2
- 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.py +4 -2
- nabu/preproc/shift_cuda.py +0 -1
- nabu/preproc/tests/test_ctf.py +4 -4
- nabu/preproc/tests/test_double_flatfield.py +1 -1
- nabu/preproc/tests/test_flatfield.py +1 -1
- nabu/preproc/tests/test_paganin.py +1 -3
- nabu/preproc/tests/test_pcaflats.py +154 -0
- nabu/preproc/tests/test_vshift.py +4 -1
- nabu/processing/azim.py +9 -5
- nabu/processing/convolution_cuda.py +6 -4
- nabu/processing/fft_base.py +7 -3
- nabu/processing/fft_cuda.py +25 -164
- nabu/processing/fft_opencl.py +28 -6
- 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 -2
- nabu/processing/processing_base.py +12 -6
- nabu/processing/rotation_cuda.py +3 -1
- nabu/processing/tests/test_fft.py +2 -64
- 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/tests/test_rotation.py +4 -2
- nabu/processing/unsharp_opencl.py +1 -1
- nabu/reconstruction/astra.py +245 -0
- nabu/reconstruction/cone.py +39 -9
- nabu/reconstruction/fbp.py +7 -0
- nabu/reconstruction/fbp_base.py +36 -5
- nabu/reconstruction/filtering.py +59 -25
- nabu/reconstruction/filtering_cuda.py +22 -21
- nabu/reconstruction/filtering_opencl.py +10 -14
- nabu/reconstruction/hbp.py +26 -13
- nabu/reconstruction/mlem.py +55 -16
- 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 +37 -2
- nabu/reconstruction/tests/test_deringer.py +4 -4
- nabu/reconstruction/tests/test_fbp.py +36 -15
- nabu/reconstruction/tests/test_filtering.py +27 -7
- nabu/reconstruction/tests/test_halftomo.py +28 -2
- nabu/reconstruction/tests/test_mlem.py +94 -64
- nabu/reconstruction/tests/test_projector.py +7 -2
- nabu/reconstruction/tests/test_reconstructor.py +1 -1
- nabu/reconstruction/tests/test_sino_normalization.py +0 -1
- nabu/resources/dataset_analyzer.py +210 -24
- nabu/resources/gpu.py +4 -4
- nabu/resources/logger.py +4 -4
- nabu/resources/nxflatfield.py +103 -37
- nabu/resources/tests/test_dataset_analyzer.py +37 -0
- nabu/resources/tests/test_extract.py +11 -0
- nabu/resources/tests/test_nxflatfield.py +5 -5
- nabu/resources/utils.py +16 -10
- nabu/stitching/alignment.py +8 -11
- nabu/stitching/config.py +44 -35
- nabu/stitching/definitions.py +2 -2
- nabu/stitching/frame_composition.py +8 -10
- nabu/stitching/overlap.py +4 -4
- nabu/stitching/sample_normalization.py +5 -5
- 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 +11 -9
- nabu/stitching/stitcher/pre_processing.py +37 -31
- nabu/stitching/stitcher/single_axis.py +2 -3
- nabu/stitching/stitcher_2D.py +2 -1
- nabu/stitching/tests/test_config.py +10 -11
- nabu/stitching/tests/test_sample_normalization.py +1 -1
- nabu/stitching/tests/test_slurm_utils.py +1 -2
- nabu/stitching/tests/test_y_preprocessing_stitching.py +11 -8
- nabu/stitching/tests/test_z_postprocessing_stitching.py +3 -3
- nabu/stitching/tests/test_z_preprocessing_stitching.py +27 -24
- nabu/stitching/utils/tests/__init__.py +0 -0
- nabu/stitching/utils/tests/test_post-processing.py +1 -0
- nabu/stitching/utils/utils.py +16 -18
- nabu/tests.py +0 -3
- nabu/testutils.py +62 -9
- nabu/utils.py +50 -20
- {nabu-2024.2.14.dist-info → nabu-2025.1.0.dist-info}/METADATA +7 -7
- nabu-2025.1.0.dist-info/RECORD +328 -0
- {nabu-2024.2.14.dist-info → nabu-2025.1.0.dist-info}/WHEEL +1 -1
- {nabu-2024.2.14.dist-info → nabu-2025.1.0.dist-info}/entry_points.txt +2 -1
- nabu/app/correct_rot.py +0 -70
- nabu/io/tests/test_detector_distortion.py +0 -178
- nabu-2024.2.14.dist-info/RECORD +0 -317
- /nabu/{stitching → app}/tests/__init__.py +0 -0
- {nabu-2024.2.14.dist-info → nabu-2025.1.0.dist-info}/licenses/LICENSE +0 -0
- {nabu-2024.2.14.dist-info → nabu-2025.1.0.dist-info}/top_level.txt +0 -0
|
@@ -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,
|
|
@@ -476,13 +511,13 @@ def generate_hollow_cube_cone_sinograms(
|
|
|
476
511
|
vol_geom = astra.create_vol_geom(n_y, n_x, n_z)
|
|
477
512
|
|
|
478
513
|
prj_width = prj_width or n_x
|
|
479
|
-
prj_height = n_z
|
|
514
|
+
# prj_height = n_z
|
|
480
515
|
angles = np.linspace(0, 2 * np.pi, n_angles, True)
|
|
481
516
|
|
|
482
517
|
proj_geom = astra.create_proj_geom("cone", 1.0, 1.0, n_z, prj_width, angles, src_orig_dist, orig_det_dist)
|
|
483
518
|
if rot_center_shift is not None:
|
|
484
519
|
proj_geom = astra.geom_postalignment(proj_geom, (-rot_center_shift, 0))
|
|
485
|
-
magnification = 1 + orig_det_dist / src_orig_dist
|
|
520
|
+
# magnification = 1 + orig_det_dist / src_orig_dist
|
|
486
521
|
|
|
487
522
|
# hollow cube
|
|
488
523
|
cube = np.zeros(astra.geom_size(vol_geom), dtype="f")
|
|
@@ -38,7 +38,7 @@ if __do_long_tests__:
|
|
|
38
38
|
"sigma": [1.0, 2.0],
|
|
39
39
|
"wname": ["db15", "haar", "rbio4.4"],
|
|
40
40
|
"padding": [None, (100, 100), (50, 71)],
|
|
41
|
-
"fft_implem": ["
|
|
41
|
+
"fft_implem": ["vkfft"],
|
|
42
42
|
}
|
|
43
43
|
)
|
|
44
44
|
|
|
@@ -107,7 +107,7 @@ class TestDeringer:
|
|
|
107
107
|
|
|
108
108
|
@pytest.mark.skipif(
|
|
109
109
|
not (__has_cuda_deringer__) or munchetal_filter is None,
|
|
110
|
-
reason="Need pycuda, pycudwt and (
|
|
110
|
+
reason="Need pycuda, pycudwt and (cupy? or pyvkfft) for this test",
|
|
111
111
|
)
|
|
112
112
|
@pytest.mark.parametrize("config", fw_scenarios)
|
|
113
113
|
def test_cuda_munch_deringer(self, config):
|
|
@@ -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(
|
|
@@ -7,10 +7,10 @@ from nabu.testutils import get_data, generate_tests_scenarios, __do_long_tests__
|
|
|
7
7
|
from nabu.cuda.utils import get_cuda_context, __has_pycuda__
|
|
8
8
|
from nabu.opencl.utils import get_opencl_context, __has_pyopencl__
|
|
9
9
|
|
|
10
|
-
from nabu.processing.fft_cuda import
|
|
10
|
+
from nabu.processing.fft_cuda import has_vkfft as has_vkfft_cu
|
|
11
11
|
from nabu.processing.fft_opencl import has_vkfft as has_vkfft_cl
|
|
12
12
|
|
|
13
|
-
__has_pycuda__ = __has_pycuda__ and
|
|
13
|
+
__has_pycuda__ = __has_pycuda__ and has_vkfft_cu()
|
|
14
14
|
__has_pyopencl__ = __has_pyopencl__ and has_vkfft_cl()
|
|
15
15
|
|
|
16
16
|
if __has_pycuda__:
|
|
@@ -40,7 +40,7 @@ def bootstrap(request):
|
|
|
40
40
|
# always use contiguous arrays
|
|
41
41
|
cls.sino_511 = np.ascontiguousarray(cls.sino_512[:, :-1])
|
|
42
42
|
# Could be set to 5.0e-2 when using textures. When not using textures, interpolation slightly differs
|
|
43
|
-
cls.tol = 5.1e-2
|
|
43
|
+
cls.tol = 2e-2 # 5.1e-2
|
|
44
44
|
|
|
45
45
|
if __has_pycuda__:
|
|
46
46
|
cls.cuda_ctx = get_cuda_context(cleanup_at_exit=False)
|
|
@@ -62,7 +62,7 @@ class TestFBP:
|
|
|
62
62
|
def _get_backprojector(self, config, *bp_args, **bp_kwargs):
|
|
63
63
|
if config["backend"] == "cuda":
|
|
64
64
|
if not (__has_pycuda__):
|
|
65
|
-
pytest.skip("Need pycuda + (
|
|
65
|
+
pytest.skip("Need pycuda + (cupy? or pyvkfft)")
|
|
66
66
|
Backprojector = CudaBackprojector
|
|
67
67
|
ctx = self.cuda_ctx
|
|
68
68
|
else:
|
|
@@ -98,10 +98,14 @@ class TestFBP:
|
|
|
98
98
|
B = self._get_backprojector(config, (500, 512))
|
|
99
99
|
res = self.apply_fbp(config, B, self.sino_512)
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
diff = res - self.ref_512
|
|
102
|
+
tol = self.tol
|
|
103
|
+
if not (B._use_textures):
|
|
104
|
+
diff = clip_to_inner_circle(diff)
|
|
105
|
+
tol = 5.1e-2
|
|
106
|
+
err_max = np.max(np.abs(diff))
|
|
103
107
|
|
|
104
|
-
assert err_max <
|
|
108
|
+
assert err_max < tol, "Something wrong with config=%s" % (str(config))
|
|
105
109
|
|
|
106
110
|
@pytest.mark.parametrize("config", scenarios)
|
|
107
111
|
def test_fbp_511(self, config):
|
|
@@ -112,10 +116,29 @@ class TestFBP:
|
|
|
112
116
|
res = self.apply_fbp(config, B, self.sino_511)
|
|
113
117
|
ref = self.ref_512[:-1, :-1]
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
err_max = np.max(np.abs(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
diff = clip_to_inner_circle(res - ref)
|
|
120
|
+
err_max = np.max(np.abs(diff))
|
|
121
|
+
tol = self.tol
|
|
122
|
+
if not (B._use_textures):
|
|
123
|
+
tol = 5.1e-2
|
|
124
|
+
|
|
125
|
+
assert err_max < tol, "Something wrong with config=%s" % (str(config))
|
|
126
|
+
|
|
127
|
+
# Cropping the singoram to sino[:, :-1] gives a reconstruction
|
|
128
|
+
# that is not fully equivalent to rec512[:-1, :-1] in the upper half of the image, outside FoV.
|
|
129
|
+
# However, nabu Backprojector gives the same results as astra
|
|
130
|
+
# Probably we should check this instead:
|
|
131
|
+
|
|
132
|
+
# B = self._get_backprojector(config, (500, 511), rot_center=255.5, extra_options={"centered_axis": True})
|
|
133
|
+
# res = self.apply_fbp(config, B, self.sino_511)
|
|
134
|
+
# import astra
|
|
135
|
+
# proj_geom = astra.create_proj_geom('parallel', 1, 511, B.angles)
|
|
136
|
+
# proj_geom = astra.geom_postalignment(proj_geom, - 0.5)
|
|
137
|
+
# vol_geom = astra.create_vol_geom(511, 511)
|
|
138
|
+
# proj_id = astra.create_projector("cuda", proj_geom, vol_geom)
|
|
139
|
+
# ref = astra.create_reconstruction("FBP_CUDA", proj_id, self.sino_511, proj_id)[1]
|
|
140
|
+
# err_max = np.max(np.abs(res - ref))
|
|
141
|
+
# assert err_max < self.tol, "Something wrong with config=%s" % (str(config))
|
|
119
142
|
|
|
120
143
|
@pytest.mark.parametrize("config", scenarios)
|
|
121
144
|
def test_fbp_roi(self, config):
|
|
@@ -194,8 +217,7 @@ class TestFBP:
|
|
|
194
217
|
)
|
|
195
218
|
res_noclip = B0.fbp(sino)
|
|
196
219
|
ref = clip_to_inner_circle(res_noclip, radius_factor=1)
|
|
197
|
-
|
|
198
|
-
err_max = np.max(abs_diff)
|
|
220
|
+
err_max = np.max(np.abs(res - ref))
|
|
199
221
|
assert err_max < tol, "Max error is too high for rot_center=%s ; %s" % (str(rot_center), str(config))
|
|
200
222
|
|
|
201
223
|
# Test with custom outer circle value
|
|
@@ -223,7 +245,6 @@ class TestFBP:
|
|
|
223
245
|
ref = B0.fbp(self.sino_512)
|
|
224
246
|
|
|
225
247
|
# Check that "centered_axis" worked
|
|
226
|
-
|
|
227
248
|
B = self._get_backprojector(config, sino.shape, rot_center=rot_center, extra_options={"centered_axis": True})
|
|
228
249
|
res = self.apply_fbp(config, B, sino)
|
|
229
250
|
# The outside region (outer circle) is different as "res" is a wider slice
|
|
@@ -262,7 +283,7 @@ class TestFBP:
|
|
|
262
283
|
# Need to translate the axis a little bit, because of non-centered differentiation.
|
|
263
284
|
# prepend -> +0.5 ; append -> -0.5
|
|
264
285
|
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)
|
|
286
|
+
rec = self.apply_fbp(config, B, sino_diff) # noqa: F841
|
|
266
287
|
# Looks good, but all frequencies are not recovered. Use a metric like SSIM or FRC ?
|
|
267
288
|
|
|
268
289
|
|
|
@@ -14,11 +14,13 @@ if __has_pyopencl__:
|
|
|
14
14
|
from nabu.opencl.processing import OpenCLProcessing
|
|
15
15
|
from nabu.reconstruction.filtering_opencl import OpenCLSinoFilter, __has_vkfft__
|
|
16
16
|
|
|
17
|
-
filters_to_test = ["ramlak", "shepp-logan"
|
|
17
|
+
filters_to_test = ["ramlak", "shepp-logan"]
|
|
18
18
|
padding_modes_to_test = ["constant", "edge"]
|
|
19
|
+
crop_filtered_data = [True]
|
|
19
20
|
if __do_long_tests__:
|
|
20
|
-
filters_to_test
|
|
21
|
+
filters_to_test.extend(["cosine", "hamming", "hann", "lanczos"])
|
|
21
22
|
padding_modes_to_test = SinoFilter.available_padding_modes
|
|
23
|
+
crop_filtered_data = [True, False]
|
|
22
24
|
|
|
23
25
|
tests_scenarios = generate_tests_scenarios(
|
|
24
26
|
{
|
|
@@ -26,6 +28,7 @@ tests_scenarios = generate_tests_scenarios(
|
|
|
26
28
|
"padding_mode": padding_modes_to_test,
|
|
27
29
|
"output_provided": [True, False],
|
|
28
30
|
"truncated_sino": [True, False],
|
|
31
|
+
"crop_filtered_data": crop_filtered_data,
|
|
29
32
|
}
|
|
30
33
|
)
|
|
31
34
|
|
|
@@ -61,9 +64,10 @@ class TestSinoFilter:
|
|
|
61
64
|
sino.shape,
|
|
62
65
|
filter_name=config["filter_name"],
|
|
63
66
|
padding_mode=config["padding_mode"],
|
|
67
|
+
crop_filtered_data=config["crop_filtered_data"],
|
|
64
68
|
)
|
|
65
69
|
if config["output_provided"]:
|
|
66
|
-
output = np.
|
|
70
|
+
output = np.zeros(sino_filter.output_shape, "f")
|
|
67
71
|
else:
|
|
68
72
|
output = None
|
|
69
73
|
res = sino_filter.filter_sino(sino, output=output)
|
|
@@ -71,7 +75,11 @@ class TestSinoFilter:
|
|
|
71
75
|
assert id(res) == id(output), "when providing output, return value must not change"
|
|
72
76
|
|
|
73
77
|
ref = filter_sinogram(
|
|
74
|
-
sino,
|
|
78
|
+
sino,
|
|
79
|
+
sino_filter.dwidth_padded,
|
|
80
|
+
filter_name=config["filter_name"],
|
|
81
|
+
padding_mode=config["padding_mode"],
|
|
82
|
+
crop_filtered_data=config["crop_filtered_data"],
|
|
75
83
|
)
|
|
76
84
|
|
|
77
85
|
assert np.allclose(res, ref, atol=4e-6)
|
|
@@ -86,10 +94,11 @@ class TestSinoFilter:
|
|
|
86
94
|
sino.shape,
|
|
87
95
|
filter_name=config["filter_name"],
|
|
88
96
|
padding_mode=config["padding_mode"],
|
|
97
|
+
crop_filtered_data=config["crop_filtered_data"],
|
|
89
98
|
cuda_options={"ctx": self.ctx_cuda},
|
|
90
99
|
)
|
|
91
100
|
if config["output_provided"]:
|
|
92
|
-
output = garray.zeros(
|
|
101
|
+
output = garray.zeros(sino_filter.output_shape, "f")
|
|
93
102
|
else:
|
|
94
103
|
output = None
|
|
95
104
|
res = sino_filter.filter_sino(sino, output=output)
|
|
@@ -97,7 +106,11 @@ class TestSinoFilter:
|
|
|
97
106
|
assert id(res) == id(output), "when providing output, return value must not change"
|
|
98
107
|
|
|
99
108
|
ref = filter_sinogram(
|
|
100
|
-
h_sino,
|
|
109
|
+
h_sino,
|
|
110
|
+
sino_filter.dwidth_padded,
|
|
111
|
+
filter_name=config["filter_name"],
|
|
112
|
+
padding_mode=config["padding_mode"],
|
|
113
|
+
crop_filtered_data=config["crop_filtered_data"],
|
|
101
114
|
)
|
|
102
115
|
|
|
103
116
|
assert np.allclose(res.get(), ref, atol=6e-5), "test_cuda_filter: something wrong with config=%s" % (
|
|
@@ -109,6 +122,8 @@ class TestSinoFilter:
|
|
|
109
122
|
)
|
|
110
123
|
@pytest.mark.parametrize("config", tests_scenarios)
|
|
111
124
|
def test_opencl_filter(self, config):
|
|
125
|
+
if not (config["crop_filtered_data"]):
|
|
126
|
+
pytest.skip("crop_filtered_data=False is not supported for OpenCL backend yet")
|
|
112
127
|
sino = self.sino_cl if not (config["truncated_sino"]) else self.sino_truncated_cl
|
|
113
128
|
h_sino = self.sino if not (config["truncated_sino"]) else self.sino_truncated
|
|
114
129
|
|
|
@@ -117,6 +132,7 @@ class TestSinoFilter:
|
|
|
117
132
|
filter_name=config["filter_name"],
|
|
118
133
|
padding_mode=config["padding_mode"],
|
|
119
134
|
opencl_options={"ctx": self.cl.ctx},
|
|
135
|
+
crop_filtered_data=config["crop_filtered_data"],
|
|
120
136
|
)
|
|
121
137
|
if config["output_provided"]:
|
|
122
138
|
output = parray.zeros(self.cl.queue, sino.shape, "f")
|
|
@@ -127,7 +143,11 @@ class TestSinoFilter:
|
|
|
127
143
|
assert id(res) == id(output), "when providing output, return value must not change"
|
|
128
144
|
|
|
129
145
|
ref = filter_sinogram(
|
|
130
|
-
h_sino,
|
|
146
|
+
h_sino,
|
|
147
|
+
sino_filter.dwidth_padded,
|
|
148
|
+
filter_name=config["filter_name"],
|
|
149
|
+
padding_mode=config["padding_mode"],
|
|
150
|
+
crop_filtered_data=config["crop_filtered_data"],
|
|
131
151
|
)
|
|
132
152
|
|
|
133
153
|
assert np.allclose(res.get(), ref, atol=6e-5), "test_opencl_filter: something wrong with config=%s" % (
|
|
@@ -42,7 +42,7 @@ class TestHalftomo:
|
|
|
42
42
|
def _get_backprojector(self, config, *bp_args, **bp_kwargs):
|
|
43
43
|
if config["backend"] == "cuda":
|
|
44
44
|
if not (__has_pycuda__):
|
|
45
|
-
pytest.skip("Need pycuda +
|
|
45
|
+
pytest.skip("Need pycuda + cupy? or vkfft")
|
|
46
46
|
Backprojector = CudaBackprojector
|
|
47
47
|
ctx = self.cuda_ctx
|
|
48
48
|
else:
|
|
@@ -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__
|
|
@@ -9,83 +9,113 @@ from nabu.reconstruction.mlem import MLEMReconstructor, __have_corrct__
|
|
|
9
9
|
@pytest.fixture(scope="class")
|
|
10
10
|
def bootstrap(request):
|
|
11
11
|
cls = request.cls
|
|
12
|
-
datafile = get_data("
|
|
13
|
-
cls.
|
|
12
|
+
datafile = get_data("test_mlem.npz")
|
|
13
|
+
cls.data_wvu = datafile["data_wvu"]
|
|
14
14
|
cls.angles_rad = datafile["angles_rad"]
|
|
15
|
-
cls.
|
|
16
|
-
cls.
|
|
17
|
-
cls.
|
|
18
|
-
cls.
|
|
19
|
-
cls.
|
|
20
|
-
|
|
21
|
-
cls.
|
|
15
|
+
cls.pixel_size_cm = datafile["pixel_size"] * 1e4 # pixel_size originally in um
|
|
16
|
+
cls.true_cor = datafile["true_cor"]
|
|
17
|
+
cls.mlem_cor_None_nosh = datafile["mlem_cor_None_nosh"]
|
|
18
|
+
cls.mlem_cor_truecor_nosh = datafile["mlem_cor_truecor_nosh"]
|
|
19
|
+
cls.mlem_cor_truecor_shifts_v0 = datafile["mlem_cor_truecor_shifts_v0"]
|
|
20
|
+
cls.shifts_uv_v0 = datafile["shifts_uv_v0"]
|
|
21
|
+
cls.shifts_uv = datafile["shifts_uv"]
|
|
22
|
+
|
|
23
|
+
cls.tol = 1.3e-4
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
@pytest.mark.skipif(not (__has_pycuda__ and __have_corrct__), reason="Need pycuda and corrct for this test")
|
|
25
27
|
@pytest.mark.usefixtures("bootstrap")
|
|
26
|
-
class
|
|
28
|
+
class TestMLEMReconstructor:
|
|
27
29
|
"""These tests test the general MLEM reconstruction algorithm
|
|
28
30
|
and the behavior of the reconstruction with respect to horizontal shifts.
|
|
29
31
|
Only horizontal shifts are tested here because vertical shifts are handled outside
|
|
30
|
-
the reconstruction object, but in the embedding reconstruction pipeline. See FullFieldReconstructor
|
|
32
|
+
the reconstruction object, but in the embedding reconstruction pipeline. See FullFieldReconstructor
|
|
33
|
+
It is compared against a reference reconstruction generated with the `rec_mlem` function
|
|
34
|
+
defined in the `generate_test_data.py` script.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def _rec_mlem(self, cor, shifts_uv, data_wvu, angles_rad):
|
|
38
|
+
n_angles, n_z, n_x = data_wvu.shape
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
mlem = MLEMReconstructor(
|
|
41
|
+
(n_z, n_angles, n_x),
|
|
42
|
+
angles_rad,
|
|
43
|
+
shifts_uv=shifts_uv,
|
|
44
|
+
cor=cor,
|
|
45
|
+
n_iterations=50,
|
|
46
|
+
extra_options={"centered_axis": True, "clip_outer_circle": True, "scale_factor": 1 / self.pixel_size_cm},
|
|
35
47
|
)
|
|
48
|
+
rec_mlem = mlem.reconstruct(data_wvu.swapaxes(0, 1))
|
|
49
|
+
return rec_mlem
|
|
36
50
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
rec =
|
|
40
|
-
delta = np.abs(rec
|
|
51
|
+
def test_simple_mlem_recons_cor_None_nosh(self):
|
|
52
|
+
slice_index = 25
|
|
53
|
+
rec = self._rec_mlem(None, None, self.data_wvu, self.angles_rad)[slice_index]
|
|
54
|
+
delta = np.abs(rec - self.mlem_cor_None_nosh)
|
|
41
55
|
assert np.max(delta) < self.tol
|
|
42
56
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
delta = np.abs(rec[:, ::-1] - self.ref_rec_shiftsu)
|
|
49
|
-
assert np.max(delta) < self.tol
|
|
57
|
+
def test_simple_mlem_recons_cor_truecor_nosh(self):
|
|
58
|
+
slice_index = 25
|
|
59
|
+
rec = self._rec_mlem(self.true_cor, None, self.data_wvu, self.angles_rad)[slice_index]
|
|
60
|
+
delta = np.abs(rec - self.mlem_cor_truecor_nosh)
|
|
61
|
+
assert np.max(delta) < 2.6e-4
|
|
50
62
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
def test_compare_with_fbp(self):
|
|
64
|
+
from nabu.reconstruction.fbp import Backprojector
|
|
65
|
+
|
|
66
|
+
def _rec_fbp(cor, shifts_uv, data_wvu, angles_rad):
|
|
67
|
+
n_angles, n_z, n_x = data_wvu.shape
|
|
56
68
|
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
if shifts_uv is None:
|
|
70
|
+
fbp = Backprojector(
|
|
71
|
+
(n_angles, n_x),
|
|
72
|
+
angles=angles_rad,
|
|
73
|
+
rot_center=cor,
|
|
74
|
+
halftomo=False,
|
|
75
|
+
padding_mode="edges",
|
|
76
|
+
extra_options={
|
|
77
|
+
"centered_axis": True,
|
|
78
|
+
"clip_outer_circle": True,
|
|
79
|
+
"scale_factor": 1 / self.pixel_size_cm,
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
fbp = Backprojector(
|
|
84
|
+
(n_angles, n_x),
|
|
85
|
+
angles=angles_rad,
|
|
86
|
+
rot_center=cor,
|
|
87
|
+
halftomo=False,
|
|
88
|
+
padding_mode="edges",
|
|
89
|
+
extra_options={
|
|
90
|
+
"centered_axis": True,
|
|
91
|
+
"clip_outer_circle": True,
|
|
92
|
+
"scale_factor": 1 / self.pixel_size_cm, # convert um to cm
|
|
93
|
+
"axis_correction": shifts_uv[:, 0],
|
|
94
|
+
},
|
|
95
|
+
)
|
|
59
96
|
|
|
97
|
+
rec_fbp = np.zeros((n_z, n_x, n_x), "f")
|
|
98
|
+
for i in range(n_z):
|
|
99
|
+
rec_fbp[i] = fbp.fbp(data_wvu[:, i])
|
|
100
|
+
|
|
101
|
+
return rec_fbp
|
|
102
|
+
|
|
103
|
+
fbp = _rec_fbp(self.true_cor, None, self.data_wvu, self.angles_rad)[25]
|
|
104
|
+
mlem = self._rec_mlem(self.true_cor, None, self.data_wvu, self.angles_rad)[25]
|
|
105
|
+
delta = np.abs(fbp - mlem)
|
|
106
|
+
assert (
|
|
107
|
+
np.max(delta) < 400
|
|
108
|
+
) # These two should not be really equal. But the test should test that both algo FBP and MLEM behave similarly.
|
|
109
|
+
|
|
110
|
+
def test_mlem_zeroshifts_equal_noshifts(self):
|
|
60
111
|
shifts = np.zeros((len(self.angles_rad), 2))
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
"""
|
|
112
|
+
rec_nosh = self._rec_mlem(self.true_cor, None, self.data_wvu, self.angles_rad)
|
|
113
|
+
rec_zerosh = self._rec_mlem(self.true_cor, shifts, self.data_wvu, self.angles_rad)
|
|
114
|
+
delta = np.abs(rec_nosh - rec_zerosh)
|
|
115
|
+
assert np.max(delta) < self.tol
|
|
116
|
+
|
|
117
|
+
def test_mlem_recons_with_u_shifts(self):
|
|
118
|
+
slice_index = 25
|
|
119
|
+
rec = self._rec_mlem(self.true_cor, self.shifts_uv_v0, self.data_wvu, self.angles_rad)[slice_index]
|
|
120
|
+
delta = np.abs(rec - self.mlem_cor_truecor_shifts_v0)
|
|
121
|
+
assert np.max(delta) < self.tol
|
|
@@ -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):
|
|
@@ -48,7 +48,7 @@ def bootstrap(request):
|
|
|
48
48
|
)
|
|
49
49
|
@pytest.mark.usefixtures("bootstrap")
|
|
50
50
|
class TestReconstructor:
|
|
51
|
-
@pytest.mark.skipif(not (__has_cuda_fbp__), reason="need pycuda and (
|
|
51
|
+
@pytest.mark.skipif(not (__has_cuda_fbp__), reason="need pycuda and (cupy? or vkfft)")
|
|
52
52
|
@pytest.mark.parametrize("config", scenarios)
|
|
53
53
|
def test_cuda_reconstructor(self, config):
|
|
54
54
|
data = self.projs
|