Source code for xdesign.phantom.custom

"""Defines an object for simulating X-ray phantoms.

.. moduleauthor:: Daniel J Ching <carterbox@users.noreply.github.com>
"""

__author__ = "Daniel Ching, Doga Gursoy"
__copyright__ = "Copyright (c) 2016, UChicago Argonne, LLC."
__docformat__ = 'restructuredtext en'
__all__ = [
    'Soil',
    'WetCircles',
    'Foam',
    'Softwood',
]

from copy import deepcopy
import itertools
import logging
import pickle
import warnings

import numpy as np
from scipy.spatial import Delaunay

from xdesign.constants import PI
from xdesign.geometry import *
from xdesign.material import *
from xdesign.phantom.phantom import *
from xdesign.phantom.standards import *

logger = logging.getLogger(__name__)


[docs]class Foam(UnitCircle): """Generates a phantom with structure similar to foam.""" def __init__(self, size_range=[0.05, 0.01], gap=0, porosity=1): super(Foam, self).__init__(radius=0.5, material=SimpleMaterial(1.0)) if porosity < 0 or porosity > 1: raise ValueError('Porosity must be in the range [0,1).') self.sprinkle( 300, size_range, gap, material=SimpleMaterial(-1.0), max_density=porosity )
[docs]class Softwood(Phantom): """Generate a Phantom with structure similar to wood. Parameters ---------- ringsize : float [cm] The thickness of the annual rings in cm. latewood_fraction : float The volume ratio of latewood cells to earlywood cells ray_fraction : float The ratio of rows of ray cells to rows of tracheids ray_height : float [cm] The height of the ray cells cell_width, cell_height : float [cm] The shape of the earlywood cells cell_thickness : float [cm] The thickness of the earlywood cell walls frame : arraylike [cm] A bounding box for the cells """ def __init__(self): super(Softwood, self).__init__() ring_size = 0.5 ring_offset = np.random.rand() latewood_fraction = 0.35 ray_fraction = 1 / 8 ray_height = 0.01 ray_width = 0.09 ray_thickness = 0.002 cell_width, cell_height = 0.03, 0.03 cell_thickness = 0.004 frame = np.array([[-0.2, -0.2], [0.2, 0.2]]) # ------------------- def five_p(): return 1 + np.random.normal(scale=0.05) atol = 1e-16 # for rounding errors cellulose = SimpleMaterial(1) x0, y0 = frame[0, 0], frame[0, 1] x1, y1 = frame[1, 0], frame[1, 1] # Place the cells one by one at (x, y) y = y0 for r in itertools.count(): # Check that the row is in the frame if y + cell_height > y1 and abs(y + cell_height - y1) > atol: # Stop if cell reaches out of frame break # Add random jitter to each row x = x0 + cell_width * np.random.normal(scale=0.1) if r % 2 == 1: # Offset odd number rows by 1/2 cell width x += cell_width / 2 # Decide whether to make a ray cell if np.random.rand() < ray_fraction: is_ray = True else: is_ray = False for c in itertools.count(): ring_progress = ((x + ring_offset) / ring_size) % 1 if x < x0 and abs(x - x0) > atol: # skip first cell if jittered outside the frame x += cell_width if is_ray: cell = WoodCell( corner=Point([x, y]), material=cellulose, width=ray_width * five_p(), height=ray_height, wall_thickness=ray_thickness * five_p() ) else: # not ray cells if ring_progress < 1 - latewood_fraction: # earlywood dw, dt = 1, 1 else: # transition to latewood dw = 0.6 dt = 1.5 cell = WoodCell( corner=Point([x, y]), material=cellulose, width=cell_width * dw * five_p(), height=cell_height, wall_thickness=cell_thickness * dt * five_p() ) self.append(cell) x += cell.width if x + cell.width > x1 and abs(x + cell.width - x1) > atol: # Stop if cell reaches out of frame break y += cell.height
class WoodCell(Phantom): """Generate a Phantom with structure similar to a single wood cell. A wood cell has two parts: the lumen which is the empty center area of the cell and the cell wall substance which is generally hexagonal. """ def __init__( self, corner=Point([0.5, 0.5]), width=0.003, height=0.003, wall_thickness=0.0008, material=None ): super(WoodCell, self).__init__() p1 = deepcopy(corner) + Point([width / 2, height / 2]) cell_wall = Rectangle(p1, [width, height]) wt = wall_thickness p1 = deepcopy(p1) lumen = -Rectangle(p1, [width - 2 * wt, height - 2 * wt]) self._geometry = Mesh(faces=[cell_wall, lumen]) self.material = material self.height = height self.width = width # self.append(center)
[docs]class Soil(UnitCircle): """Generates a phantom with structure similar to soil. References ----------- Schlüter, S., Sheppard, A., Brown, K., & Wildenschild, D. (2014). Image processing of multiphase images obtained via X‐ray microtomography: a review. Water Resources Research, 50(4), 3615-3639. """ def __init__(self, porosity=0.412): super(Soil, self).__init__(radius=0.5, material=SimpleMaterial(0.5)) self.sprinkle( 30, [0.1, 0.03], 0, material=SimpleMaterial(0.5), max_density=1 - porosity ) # use overlap to approximate area opening transform because opening is # not discrete self.sprinkle(100, 0.02, 0.01, material=SimpleMaterial(-.25))
[docs]class WetCircles(UnitCircle): def __init__(self): super(WetCircles, self).__init__( radius=0.5, material=SimpleMaterial(0.5) ) porosity = 0.412 np.random.seed(0) self.sprinkle( 30, [0.1, 0.03], 0.005, material=SimpleMaterial(0.5), max_density=1 - porosity ) pairs = [(23, 12), (12, 19), (29, 11), (22, 5), (1, 3), (21, 9), (8, 2), (2, 27)] for p in pairs: A = self.children[p[0] - 1].geometry B = self.children[p[1] - 1].geometry thetaA = [PI / 2, 10] thetaB = [PI / 2, 10] mesh = wet_circles(A, B, thetaA, thetaB) self.append(Phantom(geometry=mesh, material=SimpleMaterial(-.25)))
def wet_circles(A, B, thetaA, thetaB): """Generates a mesh that wets the surface of circles A and B. Parameters ------------- A,B : Circle theta : list the number of radians that the wet covers and number of the points on the surface range """ vector = B.center - A.center if vector.x > 0: angleA = np.arctan(vector.y / vector.x) angleB = PI + angleA else: angleB = np.arctan(vector.y / vector.x) angleA = PI + angleB # print(vector) rA = A.radius rB = B.radius points = [] for t in ((np.arange(0, thetaA[1]) / (thetaA[1] - 1) - 0.5) * thetaA[0] + angleA): x = rA * np.cos(t) + A.center.x y = rA * np.sin(t) + A.center.y points.append([x, y]) mid = len(points) for t in ((np.arange(0, thetaB[1]) / (thetaB[1] - 1) - 0.5) * thetaB[0] + angleB): x = rB * np.cos(t) + B.center.x y = rB * np.sin(t) + B.center.y points.append([x, y]) points = np.array(points) # Triangulate the polygon tri = Delaunay(points) # Remove extra triangles # print(tri.simplices) mask = np.sum(tri.simplices < mid, 1) mask = np.logical_and(mask < 3, mask > 0) tri.simplices = tri.simplices[mask, :] # print(tri.simplices) m = Mesh() for t in tri.simplices: m.append( Triangle( Point([points[t[0], 0], points[t[0], 1]]), Point([points[t[1], 0], points[t[1], 1]]), Point([points[t[2], 0], points[t[2], 1]]) ) ) return m