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

ENH: Add cuboid_phantom #1446

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
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
235 changes: 234 additions & 1 deletion odl/phantom/geometric.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
from odl.discr.lp_discr import uniform_discr_fromdiscr
from odl.util.numerics import resize_array

__all__ = ('cuboid', 'defrise', 'ellipsoid_phantom', 'indicate_proj_axis',
__all__ = ('cuboid', 'defrise',
'cuboid_phantom', 'ellipsoid_phantom', 'indicate_proj_axis',
'smooth_cuboid', 'tgv_phantom')


Expand Down Expand Up @@ -703,6 +704,232 @@ def ellipsoid_phantom(space, ellipsoids, min_pt=None, max_pt=None):
resize_array(tmp_phantom, space.shape, offset))


def _cuboid_phantom_2d(space, cuboids):
"""Create a phantom of cuboids in 2d space.

Parameters
----------
space : `DiscreteLp`
Uniformly discretized space in which the phantom should be generated.
If ``space.shape`` is 1 in an axis, a corresponding slice of the
phantom is created (instead of squashing the whole phantom into the
slice).
cuboids : list of lists
Each row should contain the entries ::

'value',
'axis_1', 'axis_2',
'center_x', 'center_y',
'rotation'

The provided cuboids need to be specified relative to the
reference rectangle ``[-1, -1] x [1, 1]``. Angles are to be given
in radians.

Returns
-------
phantom : ``space`` element
2D cuboids phantom in ``space``.
"""
# Blank image
p = np.zeros(space.shape, dtype=space.dtype)

minp = space.grid.min_pt
maxp = space.grid.max_pt

# Create the pixel grid
grid_in = space.grid.meshgrid

# move points to [-1, 1]
grid = []
for i in range(2):
mean_i = (minp[i] + maxp[i]) / 2.0
# Where space.shape = 1, we have minp = maxp, so we set diff_i = 1
# to avoid division by zero. Effectively, this allows constructing
# a slice of a 2D phantom.
diff_i = (maxp[i] - minp[i]) / 2.0 or 1.0
grid.append((grid_in[i] - mean_i) / diff_i)

for cuboid in cuboids:
assert len(cuboid) == 6

intensity = cuboid[0]
a_squared = cuboid[1] ** 2
b_squared = cuboid[2] ** 2
x0 = cuboid[3]
y0 = cuboid[4]
theta = cuboid[5]

scales = [1 / a_squared, 1 / b_squared]
center = (np.array([x0, y0]) + 1.0) / 2.0

# Create the offset x,y and z values for the grid
if theta != 0:
# Rotate the points to the expected coordinate system.
ctheta = np.cos(theta)
stheta = np.sin(theta)

mat = np.array([[ctheta, stheta],
[-stheta, ctheta]])

# Calculate the points that could possibly be inside the volume
# Since the points are rotated, we cannot do anything directional
# without more logic
max_radius = np.abs(mat).dot(np.sqrt([a_squared, b_squared]))
idx, shapes = _getshapes_2d(center, max_radius, space.shape)

subgrid = [g[idi] for g, idi in zip(grid, shapes)]
offset_points = [vec * (xi - x0i)[..., None]
for xi, vec, x0i in zip(subgrid, mat.T, [x0, y0])]
rotated = offset_points[0] + offset_points[1]
np.square(rotated, out=rotated)
max_distance = np.maximum(scales[0] * rotated[..., 0],
scales[1] * rotated[..., 1])
else:
# Calculate the points that could possibly be inside the volume
max_radius = np.sqrt([a_squared, b_squared])
idx, shapes = _getshapes_2d(center, max_radius, space.shape)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you have to do this for non-rotated rectangles? Can't you just get the min and max per axis and use those directly as bounding boxes? In other words, can't you directly construct slice objects from the min and max and use those for indexing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTOH, I can see why you want to use the same kind of logic. Maybe it's okay.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll just leave this as is since the other optimization is relatively minor for most cases. This would only give a notable improvement in the case of slightly rotated and very elongated rectangles, which I'll just leave for now.


subgrid = [g[idi] for g, idi in zip(grid, shapes)]
squared_dist = [ai * (xi - x0i) ** 2
for xi, ai, x0i in zip(subgrid,
scales,
[x0, y0])]

# Parentheses to get best order for broadcasting
max_distance = np.maximum(squared_dist[0], squared_dist[1])

# Find the points within the ellipse
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rectangle?

inside = max_distance <= 1

# Add the ellipse intensity to those points
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

p[idx][inside] += intensity

return space.element(p)


def cuboid_phantom(space, cuboids, min_pt=None, max_pt=None):
"""Return a phantom given by cuboids.

Parameters
----------
space : `DiscreteLp`
Space in which the phantom should be created, must be 2- or
3-dimensional. If ``space.shape`` is 1 in an axis, a corresponding
slice of the phantom is created (instead of squashing the whole
phantom into the slice).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3D is not supported, so this needs an update

cuboids : sequence of sequences
If ``space`` is 2-dimensional, each row should contain the entries ::

'value',
'axis_1', 'axis_2',
'center_x', 'center_y',
'rotation'

The provided cuboids need to be specified relative to the
reference rectangle ``[-1, -1] x [1, 1]``.
The angles are to be given in radians.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about 3D?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3D is not currently supported, I'll update the rest to reflect this


min_pt, max_pt : array-like, optional
If provided, use these vectors to determine the bounding box of the
phantom instead of ``space.min_pt`` and ``space.max_pt``.
It is currently required that ``min_pt >= space.min_pt`` and
``max_pt <= space.max_pt``, i.e., shifting or scaling outside the
original space is not allowed.

Providing one of them results in a shift, e.g., for ``min_pt``::

new_min_pt = min_pt
new_max_pt = space.max_pt + (min_pt - space.min_pt)

Providing both results in a scaled version of the phantom.

Notes
-----
The phantom is created by adding the values of each cuboid. The
cuboid are defined by a center point
``(center_x, center_y, [center_z])``, the lengths of its principial
axes ``(axis_1, axis_2, [axis_2])``, and a rotation angle ``rotation``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

axis_3

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But since 3D isn't supported, remove 3rd axis stuff.

in 2D or Euler angles ``(rotation_phi, rotation_theta, rotation_psi)``
in 3D.

This function is heavily optimized, achieving runtimes about 20 times
faster than "trivial" implementations. It is therefore recommended to use
it in all phantoms where applicable.

The main optimization is that it only considers a subset of all the
points when updating for each cuboids. It does this by first finding
a subset of points that could possibly be inside the cuboids. This
optimization is very good for "square" cuboids, but not so
much for elongated or rotated ones.

It also does calculations wherever possible on the meshgrid instead of
individual points.

Examples
--------
Create a circle with a smaller circle inside:

>>> space = odl.uniform_discr([-1, -1], [1, 1], [5, 5])
>>> cuboids = [[1.0, 1.0, 1.0, 0.0, 0.0, 0.0],
... [1.0, 0.6, 0.6, 0.0, 0.0, 0.0]]
>>> print(cuboid_phantom(space, cuboids))
[[ 0., 0., 1., 0., 0.],
[ 0., 1., 2., 1., 0.],
[ 1., 2., 2., 2., 1.],
[ 0., 1., 2., 1., 0.],
[ 0., 0., 1., 0., 0.]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something is not right with this example. It doesn't look like a cuboid to me.


See Also
--------
odl.phantom.transmission.shepp_logan : Classical Shepp-Logan phantom,
typically used for transmission imaging
odl.phantom.geometric.defrise_ellipses : Ellipses for the
Defrise phantom
"""
if space.ndim == 2:
_phantom = _cuboid_phantom_2d
else:
raise ValueError('dimension not 2, no phantom available')

if min_pt is None and max_pt is None:
return _phantom(space, cuboids)

else:
# Generate a temporary space with given `min_pt` and `max_pt`
# (snapped to the cell grid), create the phantom in that space and
# resize to the target size for `space`.
# The snapped points are constructed by finding the index of
# `min/max_pt` in the space partition, indexing the partition with
# that index, yielding a single-cell partition, and then taking
# the lower-left/upper-right corner of that cell.
if min_pt is None:
snapped_min_pt = space.min_pt
else:
min_pt_cell = space.partition[space.partition.index(min_pt)]
snapped_min_pt = min_pt_cell.min_pt

if max_pt is None:
snapped_max_pt = space.max_pt
else:
max_pt_cell = space.partition[space.partition.index(max_pt)]
snapped_max_pt = max_pt_cell.max_pt
# Avoid snapping to the next cell where max_pt falls exactly on
# a boundary
for i in range(space.ndim):
if max_pt[i] in space.partition.cell_boundary_vecs[i]:
snapped_max_pt[i] = max_pt[i]

tmp_space = uniform_discr_fromdiscr(
space, min_pt=snapped_min_pt, max_pt=snapped_max_pt,
cell_sides=space.cell_sides)

tmp_phantom = _phantom(tmp_space, ellipsoids)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch has obviously never been run, since it cannot work.

offset = space.partition.index(tmp_space.min_pt)
return space.element(
resize_array(tmp_phantom, space.shape, offset))


def smooth_cuboid(space, min_pt=None, max_pt=None, axis=0):
"""Cuboid with smooth variations.

Expand Down Expand Up @@ -876,6 +1103,12 @@ def sigmoid(val):
# Indicate proj axis 3D
indicate_proj_axis(space).show('indicate_proj_axis 3d')

# cuboid phantom 2D
space = odl.uniform_discr([-1, -1], [1, 1], [300, 300])
ellipses = [[1.0, 0.6, 0.6, 0.0, 0.0, 1.5],
[1.0, 0.1, 0.3, 0.0, 0.0, 0.3]]
cuboid_phantom(space, ellipses).show('cuboid phantom 2d')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use cuboids or cubes instead of ellipses.


# ellipsoid phantom 2D
space = odl.uniform_discr([-1, -1], [1, 1], [300, 300])
ellipses = [[1.0, 1.0, 1.0, 0.0, 0.0, 0.0],
Expand Down