Skip to content

Commit b0beeff

Browse files
authored
Merge pull request #1157 from adler-j/issue-1054__helical_helper
Add helical_geometry
2 parents 5b55b01 + fcda57c commit b0beeff

File tree

4 files changed

+230
-49
lines changed

4 files changed

+230
-49
lines changed

examples/tomo/filtered_backprojection_helical_3d.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,21 @@
1717

1818

1919
# Reconstruction space: discretized functions on the cube
20-
# [-20, 20]^2 x [0, 40] with 300 samples per dimension.
21-
reco_space = odl.uniform_discr(
22-
min_pt=[-20, -20, 0], max_pt=[20, 20, 40], shape=[300, 300, 300],
20+
# [-20, 20]^3 with 200 samples per dimension.
21+
space = odl.uniform_discr(
22+
min_pt=[-20, -20, -20], max_pt=[20, 20, 20], shape=[200, 200, 200],
2323
dtype='float32')
2424

25-
# Make a helical cone beam geometry with flat detector
26-
# Angles: uniformly spaced, n = 2000, min = 0, max = 8 * 2 * pi
27-
# This gives 8 full turns of the helix.
28-
angle_partition = odl.uniform_partition(0, 8 * 2 * np.pi, 2000)
29-
# Detector: uniformly sampled with a small height,
30-
# n = (512, 64), min = (-30, -4), max = (30, 4)
31-
detector_partition = odl.uniform_partition([-40, -4], [40, 4], [512, 64])
32-
# Create geometry
33-
geometry = odl.tomo.ConeFlatGeometry(
34-
angle_partition, detector_partition, src_radius=100, det_radius=100,
35-
pitch=5.0)
36-
25+
# Create helical geometry
26+
geometry = odl.tomo.helical_geometry(space,
27+
src_radius=100, det_radius=100,
28+
num_turns=7.5, num_angles=1000)
3729

3830
# --- Create Filtered Back-Projection (FBP) operator --- #
3931

4032

4133
# Ray transform (= forward projection).
42-
ray_trafo = odl.tomo.RayTransform(reco_space, geometry)
34+
ray_trafo = odl.tomo.RayTransform(space, geometry)
4335

4436
# Unwindowed fbp
4537
# We select a Hamming filter, and only use the lowest 80% of frequencies to
@@ -54,7 +46,7 @@
5446

5547

5648
# Create a discrete Shepp-Logan phantom (modified version)
57-
phantom = odl.phantom.shepp_logan(reco_space, modified=True)
49+
phantom = odl.phantom.shepp_logan(space, modified=True)
5850

5951
# Create projection data by calling the ray transform on the phantom
6052
proj_data = ray_trafo(phantom)

odl/test/solvers/functional/default_functionals_test.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ def test_L1_norm(space, sigma):
5656
x = noise_element(space)
5757

5858
# Test functional evaluation
59-
expected_result = func.domain.element(np.abs(x)).inner(space.one())
60-
assert pytest.approx(func(x), expected_result)
59+
expected_result = np.abs(x).inner(space.one())
60+
assert func(x) == pytest.approx(expected_result)
6161

6262
# Test gradient - expecting sign function
6363
expected_result = func.domain.element(np.sign(x))
@@ -125,7 +125,7 @@ def test_L2_norm(space, sigma):
125125

126126
# Test functional evaluation
127127
expected_result = np.sqrt((x ** 2).inner(space.one()))
128-
assert pytest.approx(func(x), expected_result)
128+
assert func(x) == pytest.approx(expected_result)
129129

130130
# Test gradient
131131
if x_norm > 0:
@@ -172,7 +172,7 @@ def test_L2_norm(space, sigma):
172172

173173
# Verify that the biconjugate is the functional itself
174174
func_cc_cc = func_cc.convex_conj
175-
assert pytest.approx(func_cc_cc(x), func(x))
175+
assert func_cc_cc(x) == pytest.approx(func(x))
176176

177177

178178
def test_L2_norm_squared(space, sigma):
@@ -183,7 +183,7 @@ def test_L2_norm_squared(space, sigma):
183183

184184
# Test functional evaluation
185185
expected_result = x_norm ** 2
186-
assert pytest.approx(func(x), expected_result)
186+
assert func(x) == pytest.approx(expected_result)
187187

188188
# Test gradient
189189
expected_result = 2.0 * x
@@ -198,7 +198,7 @@ def test_L2_norm_squared(space, sigma):
198198

199199
# Test evaluation of the convex conjugate
200200
expected_result = x_norm ** 2 / 4.0
201-
assert pytest.approx(func_cc(x), expected_result)
201+
assert func_cc(x) == pytest.approx(expected_result)
202202

203203
# Test gradient of the convex conjugate
204204
expected_result = x / 2.0
@@ -212,7 +212,7 @@ def test_L2_norm_squared(space, sigma):
212212
func_cc_cc = func_cc.convex_conj
213213

214214
# Check that they evaluate to the same value
215-
assert pytest.approx(func_cc_cc(x), func(x))
215+
assert func_cc_cc(x) == pytest.approx(func(x))
216216

217217
# Check that their gradients evaluate to the same value
218218
assert all_almost_equal(func_cc_cc.gradient(x), func.gradient(x))
@@ -281,7 +281,7 @@ def test_kullback_leibler(space):
281281
# Evaluation of the functional
282282
expected_result = ((x - prior + prior * np.log(prior / x))
283283
.inner(one_elem))
284-
assert pytest.approx(func(x), expected_result)
284+
assert func(x) == pytest.approx(expected_result)
285285

286286
# Check property for prior
287287
assert all_almost_equal(func.prior, prior)
@@ -317,7 +317,7 @@ def test_kullback_leibler(space):
317317
# Evaluation of convex conjugate
318318
with np.errstate(all='ignore'):
319319
expected_result = - (prior * np.log(1 - x)).inner(one_elem)
320-
assert pytest.approx(cc_func(x), expected_result)
320+
assert cc_func(x) == pytest.approx(expected_result)
321321

322322
x_wrong = noise_element(space)
323323
x_wrong = x_wrong - x_wrong.ufuncs.max() + 1.01
@@ -340,7 +340,7 @@ def test_kullback_leibler(space):
340340

341341
# Check that they evaluate the same
342342
with np.errstate(all='ignore'):
343-
assert pytest.approx(cc_cc_func(x), func(x))
343+
assert cc_cc_func(x) == pytest.approx(func(x))
344344

345345

346346
def test_kullback_leibler_cross_entorpy(space):
@@ -359,7 +359,7 @@ def test_kullback_leibler_cross_entorpy(space):
359359
# Evaluation of the functional
360360
expected_result = ((prior - x + x * np.log(x / prior))
361361
.inner(one_elem))
362-
assert pytest.approx(func(x), expected_result)
362+
assert func(x) == pytest.approx(expected_result)
363363

364364
# Check property for prior
365365
assert all_almost_equal(func.prior, prior)
@@ -390,7 +390,7 @@ def test_kullback_leibler_cross_entorpy(space):
390390

391391
# Evaluation of convex conjugate
392392
expected_result = (prior * (np.exp(x) - 1)).inner(one_elem)
393-
assert pytest.approx(cc_func(x), expected_result)
393+
assert cc_func(x) == pytest.approx(expected_result)
394394

395395
# The gradient of the convex conjugate
396396
expected_result = prior * np.exp(x)
@@ -406,7 +406,7 @@ def test_kullback_leibler_cross_entorpy(space):
406406
cc_cc_func = cc_func.convex_conj
407407

408408
# Check that they evaluate the same
409-
assert pytest.approx(cc_cc_func(x), func(x))
409+
assert cc_cc_func(x) == pytest.approx(func(x))
410410

411411

412412
def test_quadratic_form(space):
@@ -425,7 +425,7 @@ def test_quadratic_form(space):
425425

426426
# Evaluation of the functional
427427
expected_result = x.inner(operator(x)) + vector.inner(x) + constant
428-
assert pytest.approx(func(x), expected_result)
428+
assert func(x) == pytest.approx(expected_result)
429429

430430
# The gradient
431431
expected_gradient = 2 * operator(x) + vector
@@ -438,7 +438,7 @@ def test_quadratic_form(space):
438438
func_no_operator = odl.solvers.QuadraticForm(vector=vector,
439439
constant=constant)
440440
expected_result = vector.inner(x) + constant
441-
assert pytest.approx(func_no_operator(x), expected_result)
441+
assert func_no_operator(x) == pytest.approx(expected_result)
442442

443443
expected_gradient = vector
444444
assert all_almost_equal(func_no_operator.gradient(x), expected_gradient)
@@ -455,7 +455,7 @@ def test_quadratic_form(space):
455455
# Test with no offset
456456
func_no_offset = odl.solvers.QuadraticForm(operator, constant=constant)
457457
expected_result = x.inner(operator(x)) + constant
458-
assert pytest.approx(func_no_offset(x), expected_result)
458+
assert func_no_offset(x) == pytest.approx(expected_result)
459459

460460

461461
def test_separable_sum(space):

odl/test/tomo/geometry/geometry_test.py

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -376,12 +376,12 @@ def test_parallel_beam_geometry_helper():
376376

377377
# Validate angles
378378
assert geometry.motion_partition.is_uniform
379-
assert pytest.approx(geometry.motion_partition.extent, np.pi)
379+
assert geometry.motion_partition.extent == pytest.approx(np.pi)
380380
assert geometry.motion_partition.cell_sides <= np.pi / (rho * omega)
381381

382382
# Validate detector
383383
assert geometry.det_partition.cell_sides <= np.pi / omega
384-
assert pytest.approx(geometry.det_partition.extent, 2 * rho)
384+
assert geometry.det_partition.extent == pytest.approx(2 * rho)
385385

386386
# --- 3d case ---
387387

@@ -390,18 +390,18 @@ def test_parallel_beam_geometry_helper():
390390

391391
# Validate angles
392392
assert geometry.motion_partition.is_uniform
393-
assert pytest.approx(geometry.motion_partition.extent, np.pi)
393+
assert geometry.motion_partition.extent == pytest.approx(np.pi)
394394
assert geometry.motion_partition.cell_sides <= np.pi / (rho * omega)
395395

396396
# Validate detector
397397
assert geometry.det_partition.cell_sides[0] <= np.pi / omega
398-
assert pytest.approx(geometry.det_partition.cell_sides[0], 0.05)
399-
assert pytest.approx(geometry.det_partition.extent[0], 2 * rho)
398+
assert geometry.det_partition.cell_sides[1] == pytest.approx(0.05)
399+
assert geometry.det_partition.extent[0] == pytest.approx(2 * rho)
400400

401401
# Validate that new detector axis is correctly aligned with the rotation
402402
# axis and that there is one detector row per slice
403-
assert pytest.approx(geometry.det_partition.min_pt[1], 0.0)
404-
assert pytest.approx(geometry.det_partition.max_pt[1], 2.0)
403+
assert geometry.det_partition.min_pt[1] == pytest.approx(0.0)
404+
assert geometry.det_partition.max_pt[1] == pytest.approx(2.0)
405405
assert geometry.det_partition.shape[1] == space.shape[2]
406406
assert all_equal(geometry.det_axes_init[1], geometry.axis)
407407

@@ -414,12 +414,12 @@ def test_parallel_beam_geometry_helper():
414414

415415
# Validate angles
416416
assert geometry.motion_partition.is_uniform
417-
assert pytest.approx(geometry.motion_partition.extent, np.pi)
417+
assert geometry.motion_partition.extent == pytest.approx(np.pi)
418418
assert geometry.motion_partition.cell_sides <= np.pi / (rho * omega)
419419

420420
# Validate detector
421421
assert geometry.det_partition.cell_sides <= np.pi / omega
422-
assert pytest.approx(geometry.det_partition.extent, 2 * rho)
422+
assert geometry.det_partition.extent == pytest.approx(2 * rho)
423423

424424

425425
def test_fanflat_props(shift):
@@ -653,21 +653,21 @@ def test_cone_beam_geometry_helper():
653653

654654
# Validate angles
655655
assert geometry.motion_partition.is_uniform
656-
assert pytest.approx(geometry.motion_partition.extent, 2 * np.pi)
656+
assert geometry.motion_partition.extent == pytest.approx(2 * np.pi)
657657
assert (geometry.motion_partition.cell_sides <=
658658
(r + rho) / r * np.pi / (rho * omega))
659659

660660
# Validate detector
661661
det_width = 2 * magnification * rho
662662
R = np.hypot(r, det_width / 2)
663663
assert geometry.det_partition.cell_sides <= np.pi * R / (r * omega)
664-
assert pytest.approx(geometry.det_partition.extent, det_width)
664+
assert geometry.det_partition.extent == pytest.approx(det_width)
665665

666666
# Short scan option
667667
fan_angle = 2 * np.arctan(det_width / (2 * r))
668668
geometry = odl.tomo.cone_beam_geometry(space, src_radius, det_radius,
669669
short_scan=True)
670-
assert pytest.approx(geometry.motion_params.extent, np.pi + fan_angle)
670+
assert geometry.motion_params.extent == pytest.approx(np.pi + fan_angle)
671671

672672
# --- 3d case ---
673673

@@ -676,7 +676,7 @@ def test_cone_beam_geometry_helper():
676676

677677
# Validate angles
678678
assert geometry.motion_partition.is_uniform
679-
assert pytest.approx(geometry.motion_partition.extent, 2 * np.pi)
679+
assert geometry.motion_partition.extent == pytest.approx(2 * np.pi)
680680
assert (geometry.motion_partition.cell_sides <=
681681
(r + rho) / r * np.pi / (rho * omega))
682682

@@ -699,15 +699,51 @@ def test_cone_beam_geometry_helper():
699699

700700
# Validate angles
701701
assert geometry.motion_partition.is_uniform
702-
assert pytest.approx(geometry.motion_partition.extent, 2 * np.pi)
702+
assert geometry.motion_partition.extent == pytest.approx(2 * np.pi)
703703
assert (geometry.motion_partition.cell_sides <=
704704
(r + rho) / r * np.pi / (rho * omega))
705705

706706
# Validate detector
707707
det_width = 2 * magnification * rho
708708
R = np.hypot(r, det_width / 2)
709709
assert geometry.det_partition.cell_sides <= np.pi * R / (r * omega)
710-
assert pytest.approx(geometry.det_partition.extent, det_width)
710+
assert geometry.det_partition.extent == pytest.approx(det_width)
711+
712+
713+
def test_helical_geometry_helper():
714+
"""Test that helical_geometry satisfies the sampling conditions.
715+
716+
See the `helical_geometry` documentation for the exact conditions.
717+
"""
718+
# Parameters
719+
src_radius = 3
720+
det_radius = 9
721+
num_turns = 4
722+
723+
magnification = (src_radius + det_radius) / src_radius
724+
rho = np.sqrt(2)
725+
omega = np.pi * 10.0
726+
r = src_radius + det_radius
727+
728+
# Create object
729+
space = odl.uniform_discr([-1, -1, -2], [1, 1, 2], [20, 20, 40])
730+
geometry = odl.tomo.helical_geometry(space, src_radius, det_radius,
731+
num_turns=num_turns)
732+
733+
# Validate angles
734+
assert geometry.motion_partition.is_uniform
735+
assert (geometry.motion_partition.extent ==
736+
pytest.approx(num_turns * 2 * np.pi))
737+
assert (geometry.motion_partition.cell_sides <=
738+
(r + rho) / r * np.pi / (rho * omega))
739+
740+
# Validate detector
741+
det_width = 2 * magnification * rho
742+
R = np.hypot(r, det_width / 2)
743+
assert geometry.det_partition.cell_sides[0] <= np.pi * R / (r * omega)
744+
mag = (3 + 9) / (3 + rho)
745+
delta_h = space.cell_sides[2] * mag
746+
assert geometry.det_partition.cell_sides[1] <= delta_h
711747

712748

713749
if __name__ == '__main__':

0 commit comments

Comments
 (0)