Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add vector geometries #1418

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d24bad2
ENH: add vector geometries, closes #1023
Jun 8, 2017
452a9bf
WIP: fix weighting for vec backprojection
Jun 9, 2017
771b38d
WIP: vectorize VecGeometry methods
Sep 5, 2017
2d99692
MAINT: fix old-style repr of vec geometry
kohr-h Aug 20, 2018
195f2dc
ENH: add parallel 2d version of geom->vectors converter
kohr-h Aug 20, 2018
6e8b102
ENH: allow different discr in irrelevant axes in resizing op
kohr-h Aug 21, 2018
36f0911
WIP: fix adjoint weighting in par2d vec geometry
kohr-h Aug 21, 2018
f891d9d
ENH: add support for ASTRA parallel_vec
kohr-h Aug 21, 2018
567bef6
WIP: add axis check examples for vec geometries
kohr-h Aug 21, 2018
87ae3f1
ENH: fix coord sys handling in vec geometries
kohr-h Aug 22, 2018
a05ebd3
ENH: ditch pkg_resources for `pkg_supports` function
kohr-h Aug 23, 2018
bd0a2a5
WIP: add `coords` arg to geom->vec converters
kohr-h Aug 23, 2018
d487ce3
ENH: add VecGeometry.__getitem__
kohr-h Oct 1, 2018
d3a75a8
MAINT: update 3d vec geom example
kohr-h Feb 13, 2019
e084e95
Fix imports and names in tomo subpackage
kohr-h Nov 5, 2019
f21af6b
Fix astra_setup tests
kohr-h Nov 5, 2019
6b8eb8b
WIP: simplify index interpolation to rounding
kohr-h Jan 19, 2020
252b7d0
WIP: Fix broadcasting in vec geometry methods
kohr-h Jan 20, 2020
047a79d
Check dimension in parallel_beam_geometry
kohr-h Sep 6, 2020
1445d13
Minor fixes in docs and comments of astra_setup.py
kohr-h Sep 6, 2020
5562656
Replace `is` by `==` in pspace ops comparison
kohr-h Nov 8, 2020
6ca631d
Improve formatting in `pkg_supports`
kohr-h Nov 8, 2020
accaf92
Temporary weight changes and minor fixes
kohr-h Nov 8, 2020
4bf2d7a
Fix broadcasting in base VecGeometry methods
kohr-h Nov 8, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions examples/tomo/checks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ They are primarily intended for developers who change back-end code or introduce

Example | Purpose | Complexity
------- | ------- | ----------
[`check_axes_cone2d_bp.py`](https://github.com/odlgroup/odl/blob/master/examples/tomo/checks/check_axes_cone2d_bp.py) | Check axis conventions and shifts for back-projection in 2D cone beam (= fan beam) geometry | middle
[`check_axes_cone2d_fp.py`](https://github.com/odlgroup/odl/blob/master/examples/tomo/checks/check_axes_cone2d_fp.py) | Check axis conventions and shifts for forward projection in 2D cone beam (= fan beam) geometry | middle
[`check_axes_cone3d_bp.py`](https://github.com/odlgroup/odl/blob/master/examples/tomo/checks/check_axes_cone3d_bp.py) | Check axis conventions and shifts for back-projection in 3D circular cone beam geometry | high
[`check_axes_cone3d_fp.py`](https://github.com/odlgroup/odl/blob/master/examples/tomo/checks/check_axes_cone3d_fp.py) | Check axis conventions and shifts for forward projection in 3D circular cone beam geometry | high
[`check_axes_parallel2d_bp.py`](https://github.com/odlgroup/odl/blob/master/examples/tomo/checks/check_axes_parallel2d_bp.py) | Check axis conventions and shifts for back-projection in 2D parallel beam geometry | middle
[`check_axes_parallel2d_fp.py`](https://github.com/odlgroup/odl/blob/master/examples/tomo/checks/check_axes_parallel2d_fp.py) | Check axis conventions and shifts for forward projection in 2D parallel beam geometry | middle
[`check_axes_parallel3d_bp.py`](https://github.com/odlgroup/odl/blob/master/examples/tomo/checks/check_axes_parallel3d_bp.py) | Check axis conventions and shifts for back-projection in 3D parallel beam geometry | high
[`check_axes_parallel3d_fp.py`](https://github.com/odlgroup/odl/blob/master/examples/tomo/checks/check_axes_parallel3d_fp.py) | Check axis conventions and shifts for forward projection in 3D parallel beam geometry | high
[`check_axes_cone2d_bp.py`](check_axes_cone2d_bp.py) | Check axis conventions and shifts for back-projection in 2D cone beam (= fan beam) geometry | middle
[`check_axes_cone2d_fp.py`](check_axes_cone2d_fp.py) | Check axis conventions and shifts for forward projection in 2D cone beam (= fan beam) geometry | middle
[`check_axes_cone2d_vec_fp.py`](check_axes_cone2d_vec_fp.py) | Check axis conventions and shifts for forward projection in 2D cone beam "vec" geometry | middle
[`check_axes_cone3d_bp.py`](check_axes_cone3d_bp.py) | Check axis conventions and shifts for back-projection in 3D circular cone beam geometry | high
[`check_axes_cone3d_fp.py`](check_axes_cone3d_fp.py) | Check axis conventions and shifts for forward projection in 3D circular cone beam geometry | high
[`check_axes_parallel2d_bp.py`](check_axes_parallel2d_bp.py) | Check axis conventions and shifts for back-projection in 2D parallel beam geometry | middle
[`check_axes_parallel2d_fp.py`](check_axes_parallel2d_fp.py) | Check axis conventions and shifts for forward projection in 2D parallel beam geometry | middle
[`check_axes_parallel2d_vec_bp.py`](check_axes_parallel2d_vec_bp.py) | Check axis conventions and shifts for back-projection in 2D parallel beam "vec" geometry | middle
[`check_axes_parallel2d_vec_fp.py`](check_axes_parallel2d_vec_fp.py) | Check axis conventions and shifts for forward projection in 2D parallel beam "vec" geometry | middle
[`check_axes_parallel3d_bp.py`](check_axes_parallel3d_bp.py) | Check axis conventions and shifts for back-projection in 3D parallel beam geometry | high
[`check_axes_parallel3d_fp.py`](check_axes_parallel3d_fp.py) | Check axis conventions and shifts for forward projection in 3D parallel beam geometry | high
99 changes: 99 additions & 0 deletions examples/tomo/checks/check_axes_cone2d_vec_fp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Cone2D_vec example for checking that orientations are handled correctly.

Due to differing axis conventions between ODL and the ray transform
back-ends, a check is needed to confirm that the translation steps are
done correctly.

Both pairs of plots of ODL projections and NumPy axis sums should look
the same in the sense that they should show the same features in the
right arrangement (not flipped, rotated, etc.).

This example is best run in Spyder section-by-section (CTRL-Enter).
"""

# %% Set up the things that never change

import matplotlib.pyplot as plt
import numpy as np
import odl

# Set back-end here (for `None` the fastest available is chosen)
impl = None
# Set a volume shift. This should move the projections in the same direction.
shift = np.array([0.0, 25.0])

img_shape = (100, 150)
img_max_pt = np.array(img_shape, dtype=float) / 2
img_min_pt = -img_max_pt
reco_space = odl.uniform_discr(img_min_pt + shift, img_max_pt + shift,
img_shape, dtype='float32')
phantom = odl.phantom.indicate_proj_axis(reco_space)

assert np.allclose(reco_space.cell_sides, 1)

# Generate ASTRA vectors, but in the ODL geometry convention.
# We use 0, 90, 180 and 270 degrees as angles, with the detector starting
# with reference point (0, 1000) and axis (1, 0), rotating counter-clockwise.
# The source points are chosen opposite to the detector at radius 500.
# The detector `u` vector from pixel 0 to pixel 1 is equal to the detector
# axis, since we choose the pixel size to be equal to 1.
src_radius = 500
det_radius = 1000
det_refpoints = np.array([(0, 1), (-1, 0), (0, -1), (1, 0)]) * det_radius
src_points = -det_refpoints / det_radius * src_radius
det_axes = np.array([(1, 0), (0, 1), (-1, 0), (0, -1)])
vectors = np.empty((4, 6))
vectors[:, 0:2] = src_points
vectors[:, 2:4] = det_refpoints
vectors[:, 4:6] = det_axes

# Choose enough pixels to cover the object projections
fan_angle = np.arctan(img_max_pt[1] / src_radius)
det_size = np.floor(2 * (src_radius + det_radius) * np.sin(fan_angle))
det_shape = int(det_size)

# Sum manually using Numpy
sum_along_x = np.sum(phantom, axis=0)
sum_along_y = np.sum(phantom, axis=1)


# %% Test forward projection along y axis


geometry = odl.tomo.ConeVecGeometry(det_shape, vectors)
# Check initial configuration
assert np.allclose(geometry.det_axes(0), [[1, 0]])
assert np.allclose(geometry.det_refpoint(0), [[0, det_radius]])

# Create projections
ray_trafo = odl.tomo.RayTransform(reco_space, geometry, impl=impl)
proj_data = ray_trafo(phantom)

# Axis in this image is x. This corresponds to 0 degrees or index 0.
proj_data.show(
indices=[0, None],
title='Projection at 0 degrees ~ Sum along y axis'
)
fig, ax = plt.subplots()
ax.plot(sum_along_y)
ax.set(xlabel="x", title='Sum along y axis')
plt.show()
# Check axes in geometry
axis_sum_y = geometry.det_axis(0)
assert np.allclose(axis_sum_y, [1, 0])


# %% Test forward projection along x axis


# Axis in this image is y. This corresponds to 90 degrees or index 1.
proj_data.show(indices=[1, None],
title='Projection at 90 degrees ~ Sum along x axis')
fig, ax = plt.subplots()
ax.plot(sum_along_x)
ax.set_xlabel('y')
plt.title('Sum along x axis')
plt.show()
# Check axes in geometry
axis_sum_x = geometry.det_axis(1)
assert np.allclose(axis_sum_x, [0, 1])
42 changes: 42 additions & 0 deletions examples/tomo/checks/check_axes_parallel2d_vec_bp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Parallel2D_vec example for checking that orientations are handled correctly.

Due to differing axis conventions between ODL and the ray transform
back-ends, a check is needed to confirm that the translation steps are
done correctly.

The back-projected data should be a blurry version of the phantom, with
all features in the correct positions, not flipped or rotated.
"""

import numpy as np
import odl

# Set back-end here (for `None` the fastest available is chosen)
impl = None
# Set a volume shift. This should not have any influence on the back-projected
# data.
shift = np.array([0.0, 25.0])

img_shape = (100, 150)
img_max_pt = np.array(img_shape, dtype=float) / 2
img_min_pt = -img_max_pt
reco_space = odl.uniform_discr(img_min_pt + shift, img_max_pt + shift,
img_shape, dtype='float32')
phantom = odl.phantom.indicate_proj_axis(reco_space)

assert np.allclose(reco_space.cell_sides, 1)

# Make standard parallel beam geometry with 360 angles and cast it to a
# vec geometry
geometry = odl.tomo.parallel_beam_geometry(reco_space, num_angles=360)
geometry = odl.tomo.ParallelVecGeometry(
det_shape=geometry.det_partition.shape,
vectors=odl.tomo.parallel_2d_geom_to_astra_vecs(geometry)
)

# Test back-projection
ray_trafo = odl.tomo.RayTransform(reco_space, geometry, impl=impl)
proj_data = ray_trafo(phantom)
backproj = ray_trafo.adjoint(proj_data)
backproj.show('Back-projection')
phantom.show('Phantom')
95 changes: 95 additions & 0 deletions examples/tomo/checks/check_axes_parallel2d_vec_fp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Parallel2D_vec example for checking that orientations are handled correctly.

Due to differing axis conventions between ODL and the ray transform
back-ends, a check is needed to confirm that the translation steps are
done correctly.

Both pairs of plots of ODL projections and NumPy axis sums should look
the same in the sense that they should show the same features in the
right arrangement (not flipped, rotated, etc.).

This example is best run in Spyder section-by-section (CTRL-Enter).
"""

# %% Set up the things that never change

import matplotlib.pyplot as plt
import numpy as np
import odl

# Set back-end here (for `None` the fastest available is chosen)
impl = None
# Set a volume shift. This should move the projections in the same direction.
shift = np.array([0.0, 25.0])

img_shape = (100, 150)
img_max_pt = np.array(img_shape, dtype=float) / 2
img_min_pt = -img_max_pt
reco_space = odl.uniform_discr(img_min_pt + shift, img_max_pt + shift,
img_shape, dtype='float32')
phantom = odl.phantom.indicate_proj_axis(reco_space)

assert np.allclose(reco_space.cell_sides, 1)

# Generate ASTRA vectors, but in the ODL geometry convention.
# We use 0, 90, 180 and 270 degrees as angles, with the detector starting
# with reference point (0, 1) and axis (1, 0), rotating counter-clockwise.
# The vector to the detector reference point coincides with the ray direction.
# The detector `u` vector from pixel 0 to pixel 1 is equal to the detector
# axis, since we choose the pixel size to be equal to 1.
det_refpoints = np.array([(0, 1), (-1, 0), (0, -1), (1, 0)])
ray_dirs = det_refpoints
det_axes = np.array([(1, 0), (0, 1), (-1, 0), (0, -1)])
vectors = np.empty((4, 6))
vectors[:, 0:2] = ray_dirs
vectors[:, 2:4] = det_refpoints
vectors[:, 4:6] = det_axes

# Choose enough pixels to cover the object projections
det_size = np.floor(1.1 * np.sqrt(np.sum(np.square(img_shape))))
det_shape = int(det_size)

# Sum manually using Numpy
sum_along_x = np.sum(phantom, axis=0)
sum_along_y = np.sum(phantom, axis=1)


# %% Test forward projection along y axis


geometry = odl.tomo.ParallelVecGeometry(det_shape, vectors)
# Check initial configuration
assert np.allclose(geometry.det_axes(0), [[1, 0]])
assert np.allclose(geometry.det_refpoint(0), [[0, 1]])

# Create projections
ray_trafo = odl.tomo.RayTransform(reco_space, geometry, impl=impl)
proj_data = ray_trafo(phantom)

# Axis in this image is x. This corresponds to 0 degrees or index 0.
proj_data.show(indices=[0, None],
title='Projection at 0 degrees ~ Sum along y axis')
fig, ax = plt.subplots()
ax.plot(sum_along_y)
ax.set_xlabel('x')
plt.title('Sum along y axis')
plt.show()
# Check axes in geometry
axis_sum_y = geometry.det_axis(0)
assert np.allclose(axis_sum_y, [1, 0])


# %% Test forward projection along x axis


# Axis in this image is y. This corresponds to 90 degrees or index 1.
proj_data.show(indices=[1, None],
title='Projection at 90 degrees ~ Sum along x axis')
fig, ax = plt.subplots()
ax.plot(sum_along_x)
ax.set_xlabel('y')
plt.title('Sum along x axis')
plt.show()
# Check axes in geometry
axis_sum_x = geometry.det_axis(1)
assert np.allclose(axis_sum_x, [0, 1])
78 changes: 78 additions & 0 deletions examples/tomo/ray_trafo_vec_geom_3d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Example using the ray transform with a custom vector geometry.

We manually build a "circle plus line trajectory" (CLT) geometry by
extracting the vectors from a circular geometry and extending it by
vertical shifts, starting at the initial position.
"""

import numpy as np
import odl

# Reconstruction space: discretized functions on the cube [-20, 20]^3
# with 300 samples per dimension.
reco_space = odl.uniform_discr(
min_pt=[-20, -20, -20], max_pt=[20, 20, 20], shape=[300, 300, 300],
dtype='float32')

# First part of the geometry: a 3D single-axis parallel beam geometry with
# flat detector
# Angles: uniformly spaced, n = 180, min = 0, max = 2 * pi
angle_partition = odl.uniform_partition(0, 2 * np.pi, 180)
# Detector: uniformly sampled, n = (512, 512), min = (-30, -30), max = (30, 30)
detector_partition = odl.uniform_partition([-30, -30], [30, 30], [512, 512])
circle_geometry = odl.tomo.ConeFlatGeometry(
angle_partition, detector_partition, src_radius=1000, det_radius=100,
axis=[1, 0, 0])

circle_vecs = odl.tomo.cone_3d_geom_to_astra_vecs(circle_geometry)

# Cover the whole volume vertically, somewhat undersampled though
vert_shift_min = -22
vert_shift_max = 22
num_shifts = 180
vert_shifts = np.linspace(vert_shift_min, vert_shift_max, num=num_shifts)
inital_vecs = circle_vecs[0]

# Start from the initial position of the circle vectors and add the vertical
# shifts to the columns 2 and 5 (source and detector z positions)
line_vecs = np.repeat(circle_vecs[0][None, :], num_shifts, axis=0)
line_vecs[:, 2] += vert_shifts
line_vecs[:, 5] += vert_shifts

# Build the composed geometry and the corresponding ray transform
# (= forward projection)
composed_vecs = np.vstack([circle_vecs, line_vecs])
composed_geom = odl.tomo.ConeVecGeometry(detector_partition.shape,
composed_vecs)

ray_trafo = odl.tomo.RayTransform(reco_space, composed_geom)

# Create a Shepp-Logan phantom (modified version) and projection data
phantom = odl.phantom.shepp_logan(reco_space, True)
proj_data = ray_trafo(phantom)

# Back-projection can be done by simply calling the adjoint operator on the
# projection data (or any element in the projection space).
backproj = ray_trafo.adjoint(proj_data)

# Show the slice z=0 of phantom and backprojection, as well as a projection
# image at theta=0 and a sinogram at v=0 (middle detector row)
phantom.show(coords=[None, None, 0], title='Phantom, middle z slice')
backproj.show(coords=[None, None, 0], title='Back-projection, middle z slice')
proj_data.show(indices=[0, slice(None), slice(None)],
title='Projection 0 (circle start)')
proj_data.show(indices=[45, slice(None), slice(None)],
title='Projection 45 (circle 1/4)')
proj_data.show(indices=[90, slice(None), slice(None)],
title='Projection 90 (circle 1/2)')
proj_data.show(indices=[135, slice(None), slice(None)],
title='Projection 135 (circle 3/4)')
proj_data.show(indices=[179, slice(None), slice(None)],
title='Projection 179 (circle end)')
proj_data.show(indices=[180, slice(None), slice(None)],
title='Projection 180 (line start)')
proj_data.show(indices=[270, slice(None), slice(None)],
title='Projection 270 (line middle)')
proj_data.show(indices=[359, slice(None), slice(None)],
title='Projection 359 (line end)')
proj_data.show(coords=[None, None, 0], title='Sinogram, middle slice')
8 changes: 5 additions & 3 deletions odl/discr/discr_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,12 @@ def __init__(self, domain, range=None, ran_shp=None, **kwargs):
'`ran_shp`')

for i in range(domain.ndim):
if (ran.is_uniform_byaxis[i] and
if (
domain.shape[i] != ran.shape[i] and # axis i relevant
ran.is_uniform_byaxis[i] and
domain.is_uniform_byaxis[i] and
not np.isclose(ran.cell_sides[i],
domain.cell_sides[i])):
not np.isclose(ran.cell_sides[i], domain.cell_sides[i])
):
raise ValueError(
'in axis {}: cell sides of domain and range differ '
'significantly: (difference {})'
Expand Down
2 changes: 1 addition & 1 deletion odl/operator/pspace_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ def _convert_to_spmatrix(operators):
'{}'.format(len(row), i, ncols))

for j, col in enumerate(row):
if col is None or col is 0:
if col is None or col == 0:
pass
elif isinstance(col, Operator):
irow.append(i)
Expand Down
Loading