2
2
import torch
3
3
import tomosipo as ts
4
4
from ts_algorithms import fdk
5
+ from ts_algorithms .fdk import fdk_weigh_projections
5
6
import numpy as np
6
7
7
8
@@ -12,30 +13,100 @@ def make_box_phantom():
12
13
return x
13
14
14
15
15
- def test_fdk_reconstruction ():
16
- vg = ts .volume (shape = 64 , size = 1 )
17
- pg = ts .cone (angles = 32 , shape = (64 , 64 ), size = (2 , 2 ), src_det_dist = 2 , src_orig_dist = 2 )
16
+ def astra_fdk (A , y ):
17
+ vg , pg = A .astra_compat_vg , A .astra_compat_pg
18
+
19
+ vd = ts .data (vg )
20
+ pd = ts .data (pg , y .cpu ().numpy ())
21
+ ts .fdk (vd , pd )
22
+ # XXX: disgregard clean up of vd and pd (tests are short-lived and
23
+ # small)
24
+ return torch .from_numpy (vd .data .copy ()).to (y .device )
25
+
26
+
27
+ # Standard parameters
28
+ vg64 = [
29
+ ts .volume (shape = 64 ), # voxel size == 1
30
+ ts .volume (shape = 64 , size = 1 ), # volume size == 1
31
+ ts .volume (shape = 64 , size = 1 ), # idem
32
+ ]
33
+ pg64 = [
34
+ ts .cone (angles = 96 , shape = 96 , src_det_dist = 192 ), # pixel size == 1
35
+ ts .cone (angles = 96 , shape = 96 , size = 1.5 , src_det_dist = 3 ), # detector size == 1 / 64
36
+ ts .cone (angles = 96 , shape = 96 , size = 3 , src_det_dist = 3 , src_orig_dist = 3 ), # magnification 2
37
+ ]
38
+ phantom64 = [
39
+ make_box_phantom (),
40
+ make_box_phantom (),
41
+ make_box_phantom (),
42
+ ]
43
+
44
+
45
+ @pytest .mark .parametrize ("vg, pg, x" , zip (vg64 , pg64 , phantom64 ))
46
+ def test_astra_compatibility (vg , pg , x ):
47
+ A = ts .operator (vg , pg )
48
+ y = A (x )
49
+ rec_ts = fdk (A , y )
50
+ rec_astra = astra_fdk (A , y )
51
+
52
+ print (abs (rec_ts - rec_astra ).max ())
53
+ assert torch .allclose (rec_ts , rec_astra , atol = 5e-4 )
54
+
55
+
56
+ def test_fdk_flipped_cone_geometry ():
57
+ vg = ts .volume (shape = 64 )
58
+ angles = np .linspace (0 , 2 * np .pi , 96 )
59
+ R = ts .rotate (pos = 0 , axis = (1 , 0 , 0 ), rad = angles )
60
+ pg = ts .cone_vec (
61
+ shape = (96 , 96 ),
62
+ src_pos = [[0 , 130 , 0 ]], # usually -130
63
+ det_pos = [[0 , 0 , 0 ]],
64
+ det_v = [[1 , 0 , 0 ]],
65
+ det_u = [[0 , 0 , 1 ]],
66
+ )
67
+ A = ts .operator (vg , R * pg )
18
68
69
+ fdk (A , torch .ones (A .range_shape ))
70
+
71
+
72
+ @pytest .mark .parametrize ("vg, pg, x" , zip (vg64 , pg64 , phantom64 ))
73
+ def test_fdk_inverse (vg , pg , x ):
74
+ """Rough test if reconstruction is close to original volume.
75
+
76
+ The mean error must be less than 10%. The sharp edges of the box
77
+ phantom make this a difficult test case.
78
+ """
19
79
A = ts .operator (vg , pg )
20
- x = make_box_phantom ()
21
80
y = A (x )
22
81
23
- # rough test if reconstruction is close to original volume
24
82
rec = fdk (A , y )
25
- assert torch .mean (torch .abs (rec - x )) < 0.15
26
- rec_nonPadded = fdk (A , y , padded = False )
27
- assert torch .mean (torch .abs (rec_nonPadded - x )) < 0.15
83
+ assert torch .mean (torch .abs (rec - x )) < 0.1
84
+
28
85
29
- # test whether cone and cone_vec geometries yield the same result
86
+ @pytest .mark .parametrize ("vg, pg, x" , zip (vg64 , pg64 , phantom64 ))
87
+ def test_fdk_cone_vec (vg , pg , x ):
88
+ """ Test that cone and cone_vec yield same result."""
89
+ A = ts .operator (vg , pg )
30
90
A_vec = ts .operator (vg , pg .to_vec ())
91
+ y = A (x )
92
+
93
+ rec = fdk (A , y )
31
94
rec_vec = fdk (A_vec , y )
32
- assert torch .allclose (rec_vec , rec , atol = 1e-3 , rtol = 1e-2 )
33
- assert torch .mean (torch .abs (rec_vec - rec )) < 1e-6
95
+ assert torch .allclose (rec , rec_vec , atol = 5e-4 )
96
+
97
+
98
+ @pytest .mark .parametrize ("vg, pg, x" , zip (vg64 , pg64 , phantom64 ))
99
+ def test_fdk_gpu (vg , pg , x ):
100
+ """ Test that cuda and cpu tensors yield same result."""
101
+ A = ts .operator (vg , pg )
102
+ y = A (x )
34
103
35
- # test whether GPU and CPU calculations yield the same result
104
+ rec_cpu = fdk ( A , y )
36
105
rec_cuda = fdk (A , y .cuda ()).cpu ()
37
- assert torch .allclose (rec_cuda , rec , atol = 1e-3 , rtol = 1e-2 )
38
- assert torch .mean (torch .abs (rec_cuda - rec )) < 1e-6
106
+
107
+ # The atol is necessary because the ASTRA backprojection appears
108
+ # to differ slightly when given cpu and gpu arguments...
109
+ assert torch .allclose (rec_cpu , rec_cuda , atol = 5e-4 )
39
110
40
111
41
112
def test_fdk_off_center_cor ():
@@ -150,6 +221,45 @@ def test_fdk_off_center_cor_subsets():
150
221
assert torch .allclose (r [sub_slice ], r_sub , atol = 1e-1 , rtol = 1e-6 )
151
222
152
223
224
+
225
+ @pytest .mark .parametrize ("vg, pg, x" , zip (vg64 , pg64 , phantom64 ))
226
+ def test_fdk_split_detector (vg , pg , x ):
227
+ """Split detector in four quarters
228
+
229
+ Test that pre-weighting each quarter individually is the same as
230
+ pre-weighting the full detector at once.
231
+ """
232
+
233
+ pg = pg .to_vec ()
234
+
235
+ # determine the half-length of the detector shape:
236
+ n , m = np .array (pg .det_shape ) // 2
237
+
238
+ # Generate slices to split the detector of a projection geometry
239
+ # into four slices.
240
+ pg_slices = [
241
+ np .s_ [:, :n , :m ],
242
+ np .s_ [:, :n , m :],
243
+ np .s_ [:, n :, :m ],
244
+ np .s_ [:, n :, m :],
245
+ ]
246
+ # Change slices to be in 'sinogram' form with angles in the middle.
247
+ sino_slices = [(slice_v , slice_angles , slice_u ) for (slice_angles , slice_v , slice_u ) in pg_slices ]
248
+
249
+ A = ts .operator (vg , pg )
250
+ y = A (x )
251
+
252
+ As = [ts .operator (vg , pg [pg_slice ]) for pg_slice in pg_slices ]
253
+
254
+ w = fdk_weigh_projections (A , y )
255
+ sub_ws = [fdk_weigh_projections (A_sub , y [sino_slice ].contiguous ()) for A_sub , sino_slice in zip (As , sino_slices )]
256
+
257
+ for sub_w , sino_slice in zip (sub_ws , sino_slices ):
258
+ abs_diff = abs (w [sino_slice ] - sub_w )
259
+ print (sub_w .max (), abs_diff .max ().item (), abs_diff .mean ().item ())
260
+ assert torch .allclose (w [sino_slice ], sub_w , rtol = 1e-2 )
261
+
262
+
153
263
def test_fdk_rotating_volume ():
154
264
"""Test that fdk handles volume_vec geometries correctly
155
265
@@ -341,11 +451,11 @@ def test_fdk_errors():
341
451
342
452
# 4. Rotation center behind source position
343
453
vg = ts .volume (pos = (0 , - 64 , 0 ), shape = 64 ).to_vec ()
344
- pg = ts .cone (shape = 96 , angles = 1 , src_det_dist = 128 )
454
+ pg = ts .cone (shape = 96 , angles = 1 , src_det_dist = 128 ). to_vec ()
345
455
angles = np .linspace (0 , 2 * np .pi , 90 )
346
456
R = ts .rotate (pos = (0 , - 129 , 0 ), axis = (1 , 0 , 0 ), rad = angles )
347
457
348
458
A = ts .operator (R * vg , pg )
349
459
350
- with pytest .raises ( ValueError ):
460
+ with pytest .warns ( UserWarning ):
351
461
fdk (A , torch .ones (A .range_shape ))
0 commit comments