Skip to content

Commit 910d911

Browse files
author
Holger Kohr
committed
ENH: add vector geometries, closes odlgroup#1023
1 parent e47579a commit 910d911

File tree

6 files changed

+899
-30
lines changed

6 files changed

+899
-30
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

+108-17
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@
5252

5353
from odl.discr import DiscreteLp, DiscreteLpElement
5454
from odl.tomo.geometry import (
55-
Geometry, DivergentBeamGeometry, ParallelBeamGeometry, FlatDetector)
55+
Geometry, DivergentBeamGeometry, ParallelBeamGeometry,
56+
ParallelVecGeometry, ConeVecGeometry,
57+
FlatDetector)
5658
from odl.tomo.util.utility import euler_matrix
5759
from odl.util.utility import pkg_supports
5860

@@ -211,6 +213,75 @@ def astra_volume_geometry(discr_reco):
211213
return vol_geom
212214

213215

216+
def vecs_odl_axes_to_astra_axes(vecs):
217+
"""Convert geometry vectors from ODL axis convention to ASTRA.
218+
219+
Parameters
220+
----------
221+
vecs : array-like, shape ``(N, 6)`` or ``(N, 12)``
222+
Vectors defining the geometric configuration in each
223+
projection. The number ``N`` of rows determines the number
224+
of projections, and the number of columns the spatial
225+
dimension (6 for 2D, 12 for 3D).
226+
227+
Returns
228+
-------
229+
astra_vecs : `numpy.ndarray`, same shape as ``vecs``
230+
The converted geometry vectors.
231+
"""
232+
vecs = np.asarray(vecs, dtype=float)
233+
234+
if vecs.shape[1] == 6:
235+
# 2D geometry, nothing to do since the axes are the same
236+
return vecs
237+
elif vecs.shape[1] == 12:
238+
# 3D geometry
239+
# ASTRA has (z, y, x) axis convention, in contrast to (x, y, z) in ODL,
240+
# so we need to adapt to this by changing the order.
241+
newind = []
242+
for i in range(4):
243+
newind.extend([2 + 3 * i, 1 + 3 * i, 0 + 3 * i])
244+
return vecs[:, newind]
245+
else:
246+
raise ValueError('`vecs` must have shape (N, 6) or (N, 12), got '
247+
'array with shape {}'.format(vecs.shape))
248+
249+
250+
def vecs_astra_axes_to_odl_axes(vecs):
251+
"""Convert geometry vectors from ASTRA axis convention to ODL.
252+
253+
Parameters
254+
----------
255+
vecs : array-like, shape ``(N, 6)`` or ``(N, 12)``
256+
Vectors defining the geometric configuration in each
257+
projection. The number ``N`` of rows determines the number
258+
of projections, and the number of columns the spatial
259+
dimension (6 for 2D, 12 for 3D).
260+
261+
Returns
262+
-------
263+
odl_vecs : `numpy.ndarray`, same shape as ``vecs``
264+
The converted geometry vectors.
265+
"""
266+
vecs = np.asarray(vecs, dtype=float)
267+
268+
if vecs.shape[1] == 6:
269+
# 2D geometry, nothing to do since the axes are the same
270+
return vecs
271+
elif vecs.shape[1] == 12:
272+
# 3D geometry
273+
# ASTRA has (z, y, x) axis convention, in contrast to (x, y, z) in ODL,
274+
# so we need to adapt to this by changing the order.
275+
newind = []
276+
for i in range(4):
277+
newind.extend([2 + 3 * i, 1 + 3 * i, 0 + 3 * i])
278+
newind = np.argsort(newind).tolist()
279+
return vecs[:, newind]
280+
else:
281+
raise ValueError('`vecs` must have shape (N, 6) or (N, 12), got '
282+
'array with shape {}'.format(vecs.shape))
283+
284+
214285
def astra_conebeam_3d_geom_to_vec(geometry):
215286
"""Create vectors for ASTRA projection geometries from ODL geometry.
216287
@@ -261,14 +332,7 @@ def astra_conebeam_3d_geom_to_vec(geometry):
261332
vectors[:, 6:9] = det_axes[0] * px_sizes[0]
262333
vectors[:, 9:12] = det_axes[1] * px_sizes[1]
263334

264-
# ASTRA has (z, y, x) axis convention, in contrast to (x, y, z) in ODL,
265-
# so we need to adapt to this by changing the order.
266-
newind = []
267-
for i in range(4):
268-
newind += [2 + 3 * i, 1 + 3 * i, 0 + 3 * i]
269-
vectors = vectors[:, newind]
270-
271-
return vectors
335+
return vecs_odl_axes_to_astra_axes(vectors)
272336

273337

274338
def astra_conebeam_2d_geom_to_vec(geometry):
@@ -325,7 +389,7 @@ def astra_conebeam_2d_geom_to_vec(geometry):
325389
px_size = geometry.det_partition.cell_sides[0]
326390
vectors[:, 4:6] = det_axis * px_size
327391

328-
return vectors
392+
return vecs_odl_axes_to_astra_axes(vectors)
329393

330394

331395
def astra_parallel_3d_geom_to_vec(geometry):
@@ -379,13 +443,7 @@ def astra_parallel_3d_geom_to_vec(geometry):
379443
vectors[:, 6:9] = det_axes[0] * px_sizes[0]
380444
vectors[:, 9:12] = det_axes[1] * px_sizes[1]
381445

382-
# ASTRA has (z, y, x) axis convention, in contrast to (x, y, z) in ODL,
383-
# so we need to adapt to this by changing the order.
384-
new_ind = []
385-
for i in range(4):
386-
new_ind += [2 + 3 * i, 1 + 3 * i, 0 + 3 * i]
387-
vectors = vectors[:, new_ind]
388-
return vectors
446+
return vecs_odl_axes_to_astra_axes(vectors)
389447

390448

391449
def astra_projection_geometry(geometry):
@@ -435,6 +493,22 @@ def astra_projection_geometry(geometry):
435493
vec = astra_conebeam_2d_geom_to_vec(geometry)
436494
proj_geom = astra.create_proj_geom('fanflat_vec', det_count, vec)
437495

496+
elif isinstance(geometry, ParallelVecGeometry) and geometry.ndim == 2:
497+
det_count = geometry.detector.size
498+
vec = geometry.vectors
499+
# TODO: flip axes?
500+
if not astra_supports('parallel2d_vec_geometry'):
501+
raise NotImplementedError(
502+
"'parallel_vec' geometry not supported by ASTRA "
503+
'v{}'.format(ASTRA_VERSION))
504+
proj_geom = astra.create_proj_geom('parallel_vec', det_count, vec)
505+
506+
elif isinstance(geometry, ConeVecGeometry) and geometry.ndim == 2:
507+
det_count = geometry.detector.size
508+
vec = geometry.vectors
509+
# TODO: flip axes?
510+
proj_geom = astra.create_proj_geom('fanflat_vec', det_count, vec)
511+
438512
elif (isinstance(geometry, ParallelBeamGeometry) and
439513
isinstance(geometry.detector, FlatDetector) and
440514
geometry.ndim == 3):
@@ -452,6 +526,23 @@ def astra_projection_geometry(geometry):
452526
vec = astra_conebeam_3d_geom_to_vec(geometry)
453527
proj_geom = astra.create_proj_geom('cone_vec', det_row_count,
454528
det_col_count, vec)
529+
530+
elif isinstance(geometry, ParallelVecGeometry) and geometry.ndim == 3:
531+
det_row_count = geometry.det_partition.shape[1]
532+
det_col_count = geometry.det_partition.shape[0]
533+
vec = geometry.vectors
534+
# TODO: flip axes?
535+
proj_geom = astra.create_proj_geom('parallel3d_vec', det_row_count,
536+
det_col_count, vec)
537+
538+
elif isinstance(geometry, ConeVecGeometry) and geometry.ndim == 3:
539+
det_row_count = geometry.det_partition.shape[1]
540+
det_col_count = geometry.det_partition.shape[0]
541+
vec = geometry.vectors
542+
# TODO: flip axes?
543+
proj_geom = astra.create_proj_geom('cone_vec', det_row_count,
544+
det_col_count, vec)
545+
455546
else:
456547
raise NotImplementedError('unknown ASTRA geometry type {!r}'
457548
''.format(geometry))

0 commit comments

Comments
 (0)