Source code for odl.phantom.misc_phantoms

# Copyright 2014-2020 The ODL contributors
#
# This file is part of ODL.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.

"""Miscellaneous phantoms that do not fit in other categories."""

from __future__ import absolute_import, division, print_function

import sys

import numpy as np

__all__ = ('submarine', 'text')


[docs]def submarine(space, smooth=True, taper=20.0): """Return a 'submarine' phantom consisting in an ellipsoid and a box. Parameters ---------- space : `DiscretizedSpace` Discretized space in which the phantom is supposed to be created. smooth : bool, optional If ``True``, the boundaries are smoothed out. Otherwise, the function steps from 0 to 1 at the boundaries. taper : float, optional Tapering parameter for the boundary smoothing. Larger values mean faster taper, i.e. sharper boundaries. Returns ------- phantom : ``space`` element The submarine phantom in ``space``. """ if space.ndim == 2: if smooth: return _submarine_2d_smooth(space, taper) else: return _submarine_2d_nonsmooth(space) else: raise ValueError('phantom only defined in 2 dimensions, got {}' ''.format(space.ndim))
def _submarine_2d_smooth(space, taper): """Return a 2d smooth 'submarine' phantom.""" def logistic(x, c): """Smoothed step function from 0 to 1, centered at 0.""" return 1. / (1 + np.exp(-c * x)) def blurred_ellipse(x): """Blurred characteristic function of an ellipse. If ``space.domain`` is a rectangle ``[0, 1] x [0, 1]``, the ellipse is centered at ``(0.6, 0.3)`` and has half-axes ``(0.4, 0.14)``. For other domains, the values are scaled accordingly. """ halfaxes = np.array([0.4, 0.14]) * space.domain.extent center = np.array([0.6, 0.3]) * space.domain.extent center += space.domain.min() # Efficiently calculate |z|^2, z = (x - center) / radii sq_ndist = np.zeros_like(x[0]) for xi, rad, cen in zip(x, halfaxes, center): sq_ndist = sq_ndist + ((xi - cen) / rad) ** 2 out = np.sqrt(sq_ndist) out -= 1 # Return logistic(taper * (1 - |z|)) return logistic(out, -taper) def blurred_rect(x): """Blurred characteristic function of a rectangle. If ``space.domain`` is a rectangle ``[0, 1] x [0, 1]``, the rect has lower left ``(0.56, 0.4)`` and upper right ``(0.76, 0.6)``. For other domains, the values are scaled accordingly. """ xlower = np.array([0.56, 0.4]) * space.domain.extent xlower += space.domain.min() xupper = np.array([0.76, 0.6]) * space.domain.extent xupper += space.domain.min() out = np.ones_like(x[0]) for xi, low, upp in zip(x, xlower, xupper): length = upp - low out = out * (logistic((xi - low) / length, taper) * logistic((upp - xi) / length, taper)) return out out = space.element(blurred_ellipse) out += space.element(blurred_rect) return out.ufuncs.minimum(1, out=out) def _submarine_2d_nonsmooth(space): """Return a 2d nonsmooth 'submarine' phantom.""" def ellipse(x): """Characteristic function of an ellipse. If ``space.domain`` is a rectangle ``[0, 1] x [0, 1]``, the ellipse is centered at ``(0.6, 0.3)`` and has half-axes ``(0.4, 0.14)``. For other domains, the values are scaled accordingly. """ halfaxes = np.array([0.4, 0.14]) * space.domain.extent center = np.array([0.6, 0.3]) * space.domain.extent center += space.domain.min() sq_ndist = np.zeros_like(x[0]) for xi, rad, cen in zip(x, halfaxes, center): sq_ndist = sq_ndist + ((xi - cen) / rad) ** 2 return np.where(sq_ndist <= 1, 1, 0) def rect(x): """Characteristic function of a rectangle. If ``space.domain`` is a rectangle ``[0, 1] x [0, 1]``, the rect has lower left ``(0.56, 0.4)`` and upper right ``(0.76, 0.6)``. For other domains, the values are scaled accordingly. """ xlower = np.array([0.56, 0.4]) * space.domain.extent xlower += space.domain.min() xupper = np.array([0.76, 0.6]) * space.domain.extent xupper += space.domain.min() out = np.ones_like(x[0]) for xi, low, upp in zip(x, xlower, xupper): out = out * ((xi >= low) & (xi <= upp)) return out out = space.element(ellipse) out += space.element(rect) return out.ufuncs.minimum(1, out=out)
[docs]def text(space, text, font=None, border=0.2, inverted=True): """Create phantom from text. The text is represented by a scalar image taking values in [0, 1]. Depending on the choice of font, the text may or may not be anti-aliased. anti-aliased text can take any value between 0 and 1, while non-anti-aliased text produces a binary image. This method requires the ``pillow`` package. Parameters ---------- space : `DiscretizedSpace` Discretized space in which the phantom is supposed to be created. Must be two-dimensional. text : str The text that should be written onto the background. font : str, optional The font that should be used to write the text. Available options are platform dependent. Default: Platform dependent. 'arial' for windows, 'LiberationSans-Regular' for linux and 'Helvetica' for OSX border : float, optional Padding added around the text. 0.0 indicates that the phantom should occupy all of the space along its largest dimension while 1.0 gives a maximally padded image (text not visible). inverted : bool, optional If the phantom should be given in inverted style, i.e. white on black. Returns ------- phantom : ``space`` element The text phantom in ``space``. Notes ----- The set of available fonts is highly platform dependent, and there is no obvious way (except from trial and error) to find what fonts are supported on an arbitrary platform. In general, the fonts ``'arial'``, ``'calibri'`` and ``'impact'`` tend to be available on windows. Platform dependent tricks: **Linux**:: $ find /usr/share/fonts -name "*.[to]tf" """ from PIL import Image, ImageDraw, ImageFont if space.ndim != 2: raise ValueError('`space` must be two-dimensional') if font is None: platform = sys.platform if platform == 'win32': # Windows font = 'arial' elif platform == 'darwin': # Mac OSX font = 'Helvetica' else: # Assume platform is linux font = 'LiberationSans-Regular' text = str(text) # Figure out what font size we should use by creating a very high # resolution font and calculating the size of the text in this font init_size = 1000 init_pil_font = ImageFont.truetype(font + ".ttf", size=init_size, encoding="unic") init_text_width, init_text_height = init_pil_font.getsize(text) # True size is given by how much too large (or small) the example was scaled_init_size = (1.0 - border) * init_size size = scaled_init_size * min([space.shape[0] / init_text_width, space.shape[1] / init_text_height]) size = int(size) # Create font pil_font = ImageFont.truetype(font + ".ttf", size=size, encoding="unic") text_width, text_height = pil_font.getsize(text) # create a blank canvas with extra space between lines canvas = Image.new('RGB', space.shape, (255, 255, 255)) # draw the text onto the canvas draw = ImageDraw.Draw(canvas) offset = ((space.shape[0] - text_width) // 2, (space.shape[1] - text_height) // 2) white = "#000000" draw.text(offset, text, font=pil_font, fill=white) # Convert the canvas into an array with values in [0, 1] arr = np.asarray(canvas) arr = np.sum(arr, -1) arr = arr / np.max(arr) arr = np.rot90(arr, -1) if inverted: arr = 1 - arr return space.element(arr)
if __name__ == '__main__': # Show the phantoms import odl from odl.util.testutils import run_doctests space = odl.uniform_discr([-1, -1], [1, 1], [300, 300]) submarine(space, smooth=False).show('submarine smooth=False') submarine(space, smooth=True).show('submarine smooth=True') submarine(space, smooth=True, taper=50).show('submarine taper=50') text(space, text='phantom').show('phantom') # Run also the doctests run_doctests()