Skip to content

Commit 9bee03d

Browse files
Holger Kohrkohr-h
Holger Kohr
authored andcommitted
ENH: add vector geometries, closes odlgroup#1023
1 parent dd01b85 commit 9bee03d

File tree

6 files changed

+901
-34
lines changed

6 files changed

+901
-34
lines changed
+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Example using the ray transform a custom vector geometry.
2+
3+
We manually build a "circle plus line trajectory" (CLT) geometry by
4+
extracting the vectors from a circular geometry and extending it by
5+
vertical shifts, starting at the initial position.
6+
"""
7+
8+
import numpy as np
9+
import odl
10+
11+
# Reconstruction space: discretized functions on the cube [-20, 20]^3
12+
# with 300 samples per dimension.
13+
reco_space = odl.uniform_discr(
14+
min_pt=[-20, -20, -20], max_pt=[20, 20, 20], shape=[300, 300, 300],
15+
dtype='float32')
16+
17+
# First part of the geometry: a 3D single-axis parallel beam geometry with
18+
# flat detector
19+
# Angles: uniformly spaced, n = 180, min = 0, max = 2 * pi
20+
angle_partition = odl.uniform_partition(0, 2 * np.pi, 180)
21+
# Detector: uniformly sampled, n = (512, 512), min = (-30, -30), max = (30, 30)
22+
detector_partition = odl.uniform_partition([-30, -30], [30, 30], [512, 512])
23+
circle_geometry = odl.tomo.CircularConeFlatGeometry(
24+
angle_partition, detector_partition, src_radius=1000, det_radius=100,
25+
axis=[1, 0, 0])
26+
27+
circle_vecs = odl.tomo.astra_conebeam_3d_geom_to_vec(circle_geometry)
28+
29+
# Cover the whole volume vertically, somewhat undersampled though
30+
vert_shift_min = -22
31+
vert_shift_max = 22
32+
num_shifts = 180
33+
vert_shifts = np.linspace(vert_shift_min, vert_shift_max, num=num_shifts)
34+
inital_vecs = circle_vecs[0]
35+
36+
# Start from the initial position of the circle vectors and add the vertical
37+
# shifts to the columns 2 and 5 (source and detector z positions)
38+
line_vecs = np.repeat(circle_vecs[0][None, :], num_shifts, axis=0)
39+
line_vecs[:, 2] += vert_shifts
40+
line_vecs[:, 5] += vert_shifts
41+
42+
# Build the composed geometry and the corresponding ray transform
43+
# (= forward projection)
44+
composed_vecs = np.vstack([circle_vecs, line_vecs])
45+
composed_geom = odl.tomo.ConeVecGeometry(detector_partition.shape,
46+
composed_vecs)
47+
48+
ray_trafo = odl.tomo.RayTransform(reco_space, composed_geom)
49+
50+
# Create a Shepp-Logan phantom (modified version) and projection data
51+
phantom = odl.phantom.shepp_logan(reco_space, True)
52+
proj_data = ray_trafo(phantom)
53+
54+
# Back-projection can be done by simply calling the adjoint operator on the
55+
# projection data (or any element in the projection space).
56+
backproj = ray_trafo.adjoint(proj_data)
57+
58+
# Show the slice z=0 of phantom and backprojection, as well as a projection
59+
# image at theta=0 and a sinogram at v=0 (middle detector row)
60+
phantom.show(coords=[None, None, 0], title='Phantom, middle z slice')
61+
backproj.show(coords=[None, None, 0], title='Back-projection, middle z slice')
62+
proj_data.show(indices=[0, slice(None), slice(None)],
63+
title='Projection 0 (circle start)')
64+
proj_data.show(indices=[45, slice(None), slice(None)],
65+
title='Projection 45 (circle 1/4)')
66+
proj_data.show(indices=[90, slice(None), slice(None)],
67+
title='Projection 90 (circle 1/2)')
68+
proj_data.show(indices=[135, slice(None), slice(None)],
69+
title='Projection 135 (circle 3/4)')
70+
proj_data.show(indices=[179, slice(None), slice(None)],
71+
title='Projection 179 (circle end)')
72+
proj_data.show(indices=[180, slice(None), slice(None)],
73+
title='Projection 180 (line start)')
74+
proj_data.show(indices=[270, slice(None), slice(None)],
75+
title='Projection 270 (line middle)')
76+
proj_data.show(indices=[359, slice(None), slice(None)],
77+
title='Projection 359 (line end)')
78+
proj_data.show(coords=[None, None, 0], title='Sinogram, middle slice')

odl/tomo/backends/astra_setup.py

+107-17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2014-2017 The ODL contributors
1+
# Copyright 2014-2018 The ODL contributors
22
#
33
# This file is part of ODL.
44
#
@@ -37,6 +37,7 @@
3737
from odl.discr import DiscreteLp, DiscreteLpElement
3838
from odl.tomo.geometry import (
3939
Geometry, DivergentBeamGeometry, ParallelBeamGeometry,
40+
ParallelVecGeometry, ConeVecGeometry,
4041
Flat1dDetector, Flat2dDetector)
4142
from odl.tomo.util.utility import euler_matrix
4243
from odl.util.npy_compat import moveaxis
@@ -230,6 +231,75 @@ def astra_volume_geometry(reco_space):
230231
return vol_geom
231232

232233

234+
def vecs_odl_axes_to_astra_axes(vecs):
235+
"""Convert geometry vectors from ODL axis convention to ASTRA.
236+
237+
Parameters
238+
----------
239+
vecs : array-like, shape ``(N, 6)`` or ``(N, 12)``
240+
Vectors defining the geometric configuration in each
241+
projection. The number ``N`` of rows determines the number
242+
of projections, and the number of columns the spatial
243+
dimension (6 for 2D, 12 for 3D).
244+
245+
Returns
246+
-------
247+
astra_vecs : `numpy.ndarray`, same shape as ``vecs``
248+
The converted geometry vectors.
249+
"""
250+
vecs = np.asarray(vecs, dtype=float)
251+
252+
if vecs.shape[1] == 6:
253+
# 2D geometry, nothing to do since the axes are the same
254+
return vecs
255+
elif vecs.shape[1] == 12:
256+
# 3D geometry
257+
# ASTRA has (z, y, x) axis convention, in contrast to (x, y, z) in ODL,
258+
# so we need to adapt to this by changing the order.
259+
newind = []
260+
for i in range(4):
261+
newind.extend([2 + 3 * i, 1 + 3 * i, 0 + 3 * i])
262+
return vecs[:, newind]
263+
else:
264+
raise ValueError('`vecs` must have shape (N, 6) or (N, 12), got '
265+
'array with shape {}'.format(vecs.shape))
266+
267+
268+
def vecs_astra_axes_to_odl_axes(vecs):
269+
"""Convert geometry vectors from ASTRA axis convention to ODL.
270+
271+
Parameters
272+
----------
273+
vecs : array-like, shape ``(N, 6)`` or ``(N, 12)``
274+
Vectors defining the geometric configuration in each
275+
projection. The number ``N`` of rows determines the number
276+
of projections, and the number of columns the spatial
277+
dimension (6 for 2D, 12 for 3D).
278+
279+
Returns
280+
-------
281+
odl_vecs : `numpy.ndarray`, same shape as ``vecs``
282+
The converted geometry vectors.
283+
"""
284+
vecs = np.asarray(vecs, dtype=float)
285+
286+
if vecs.shape[1] == 6:
287+
# 2D geometry, nothing to do since the axes are the same
288+
return vecs
289+
elif vecs.shape[1] == 12:
290+
# 3D geometry
291+
# ASTRA has (z, y, x) axis convention, in contrast to (x, y, z) in ODL,
292+
# so we need to adapt to this by changing the order.
293+
newind = []
294+
for i in range(4):
295+
newind.extend([2 + 3 * i, 1 + 3 * i, 0 + 3 * i])
296+
newind = np.argsort(newind).tolist()
297+
return vecs[:, newind]
298+
else:
299+
raise ValueError('`vecs` must have shape (N, 6) or (N, 12), got '
300+
'array with shape {}'.format(vecs.shape))
301+
302+
233303
def astra_conebeam_3d_geom_to_vec(geometry):
234304
"""Create vectors for ASTRA projection geometries from ODL geometry.
235305
@@ -288,14 +358,7 @@ def astra_conebeam_3d_geom_to_vec(geometry):
288358
vectors[:, 9:12] = det_axes[0] * px_sizes[0]
289359
vectors[:, 6:9] = det_axes[1] * px_sizes[1]
290360

291-
# ASTRA has (z, y, x) axis convention, in contrast to (x, y, z) in ODL,
292-
# so we need to adapt to this by changing the order.
293-
newind = []
294-
for i in range(4):
295-
newind += [2 + 3 * i, 1 + 3 * i, 0 + 3 * i]
296-
vectors = vectors[:, newind]
297-
298-
return vectors
361+
return vecs_odl_axes_to_astra_axes(vectors)
299362

300363

301364
def astra_conebeam_2d_geom_to_vec(geometry):
@@ -354,7 +417,7 @@ def astra_conebeam_2d_geom_to_vec(geometry):
354417
px_size = geometry.det_partition.cell_sides[0]
355418
vectors[:, 4:6] = det_axis * px_size
356419

357-
return vectors
420+
return vecs_odl_axes_to_astra_axes(vectors)
358421

359422

360423
def astra_parallel_3d_geom_to_vec(geometry):
@@ -416,13 +479,7 @@ def astra_parallel_3d_geom_to_vec(geometry):
416479
vectors[:, 9:12] = det_axes[0] * px_sizes[0]
417480
vectors[:, 6:9] = det_axes[1] * px_sizes[1]
418481

419-
# ASTRA has (z, y, x) axis convention, in contrast to (x, y, z) in ODL,
420-
# so we need to adapt to this by changing the order.
421-
new_ind = []
422-
for i in range(4):
423-
new_ind += [2 + 3 * i, 1 + 3 * i, 0 + 3 * i]
424-
vectors = vectors[:, new_ind]
425-
return vectors
482+
return vecs_odl_axes_to_astra_axes(vectors)
426483

427484

428485
def astra_projection_geometry(geometry):
@@ -472,6 +529,22 @@ def astra_projection_geometry(geometry):
472529
vec = astra_conebeam_2d_geom_to_vec(geometry)
473530
proj_geom = astra.create_proj_geom('fanflat_vec', det_count, vec)
474531

532+
elif isinstance(geometry, ParallelVecGeometry) and geometry.ndim == 2:
533+
det_count = geometry.detector.size
534+
vec = geometry.vectors
535+
# TODO: flip axes?
536+
if not astra_supports('parallel2d_vec_geometry'):
537+
raise NotImplementedError(
538+
"'parallel_vec' geometry not supported by ASTRA "
539+
'v{}'.format(ASTRA_VERSION))
540+
proj_geom = astra.create_proj_geom('parallel_vec', det_count, vec)
541+
542+
elif isinstance(geometry, ConeVecGeometry) and geometry.ndim == 2:
543+
det_count = geometry.detector.size
544+
vec = geometry.vectors
545+
# TODO: flip axes?
546+
proj_geom = astra.create_proj_geom('fanflat_vec', det_count, vec)
547+
475548
elif (isinstance(geometry, ParallelBeamGeometry) and
476549
isinstance(geometry.detector, (Flat1dDetector, Flat2dDetector)) and
477550
geometry.ndim == 3):
@@ -491,6 +564,23 @@ def astra_projection_geometry(geometry):
491564
vec = astra_conebeam_3d_geom_to_vec(geometry)
492565
proj_geom = astra.create_proj_geom('cone_vec', det_row_count,
493566
det_col_count, vec)
567+
568+
elif isinstance(geometry, ParallelVecGeometry) and geometry.ndim == 3:
569+
det_row_count = geometry.det_partition.shape[1]
570+
det_col_count = geometry.det_partition.shape[0]
571+
vec = geometry.vectors
572+
# TODO: flip axes?
573+
proj_geom = astra.create_proj_geom('parallel3d_vec', det_row_count,
574+
det_col_count, vec)
575+
576+
elif isinstance(geometry, ConeVecGeometry) and geometry.ndim == 3:
577+
det_row_count = geometry.det_partition.shape[1]
578+
det_col_count = geometry.det_partition.shape[0]
579+
vec = geometry.vectors
580+
# TODO: flip axes?
581+
proj_geom = astra.create_proj_geom('cone_vec', det_row_count,
582+
det_col_count, vec)
583+
494584
else:
495585
raise NotImplementedError('unknown ASTRA geometry type {!r}'
496586
''.format(geometry))

0 commit comments

Comments
 (0)