Source code for tomophantom.TomoP3D

"""Modules for generating 3D synthetic phantoms and their analytical projection data. 
The dynamic phantoms (resulting in 4D arrays) can also be generated.

API Summary:

* :func:`Model` - generates a 3D phantom using a :ref:`ref_glossary_model` from :ref:`howto_3d_libs`.
* :func:`ModelSub` - generates a vertical subset (a cutoff) for a :ref:`ref_glossary_model` from :ref:`howto_3d_libs`.
* :func:`ModelTemporal` - generates a :ref:`ref_glossary_dynamic_model` (3D + t) from the from :ref:`howto_3d_libs`.
* :func:`ModelTemporalSub` - generates a vertical subset (cutoff) of a 4D :ref:`ref_glossary_dynamic_model` from the :ref:`howto_3d_libs`.
* :func:`ModelSino` - generates 3D projection data for a :ref:`ref_glossary_model` from the :ref:`howto_3d_libs`.
* :func:`ModelSinoSub` - generates a vertical subset (cutoff) for 3D projection data for a :ref:`ref_glossary_model` from the :ref:`howto_3d_libs`.
* :func:`ModelSinoTemporal` - generates (3D + t) 4D projection data for a temporal model from the :ref:`howto_3d_libs`.
* :func:`ModelSinoTemporalSub` - generates (3D + t) 4D projection data (subset) for a temporal model from the :ref:`howto_3d_libs`.
* :func:`Object` - generates one elementary 3D :ref:`ref_glossary_object` from the provided parameters, see :ref:`ref_object_api`.
* :func:`ObjectSino` - generates a 3D projection data for one :ref:`ref_glossary_object` from the provided parameters, see :ref:`ref_object_api`.
"""

import ctypes
import numpy as np
from numbers import Number
from enum import Enum
from typing import Union, Tuple
from pathlib import Path

import tomophantom.ctypes.external as external

__all__ = [
    "Model",
    "ModelSub",
    "ModelTemporal",
    "ModelTemporalSub",
    "ModelSino",
    "ModelSinoSub",
    "ModelSinoTemporal",
    "ModelSinoTemporalSub",
    "Object",
    "ObjectSino",
]


class Objects3D(Enum):
    """Enumeration with the available objects for 3D phantoms"""

    GAUSSIAN = "gaussian"
    PARABOLOID = "paraboloid"
    ELLIPSOID = "ellipsoid"
    CONE = "cone"
    CUBOID = "cuboid"
    ELLIPCYLINDER = "elliptical_cylinder"


def _check_params3d(model_no: int, models_library_path: Path) -> np.ndarray:
    """Check parameters before executing the generation script.

    Args:
        model_no (int): Model number from the Phantom3DLibrary.dat library file.
        models_library_path (Path): A path to the library file.

    Returns:
        list: a list of integers
    """
    params = np.ascontiguousarray(np.zeros([12], dtype=ctypes.c_int))
    external.c_checkParams3D(
        params,
        model_no,
        models_library_path,
    )
    return params


[docs] def Model( model_no: int, phantom_size: Union[int, Tuple[int, int, int]], models_library_path: Path, ) -> np.ndarray: """Generates a 3D phantom based on the model no. in the library file Phantom3DLibrary.dat. Args: model_no (int): Model number from the Phantom3DLibrary.dat library file. phantom_size (int, Tuple(int)): A scalar or a tuple with phantom dimensions. models_library_path (Path): A path to the library file. Returns: np.ndarray: The generated 3D phantom. """ if type(phantom_size) == tuple: N1, N2, N3 = [int(i) for i in phantom_size] else: N1 = N2 = N3 = phantom_size # check the validity of model's parameters params = np.ascontiguousarray(np.zeros([12], dtype=ctypes.c_int)) params = _check_params3d(model_no, models_library_path) __testParams3D(params) # check parameters and terminate if incorrect if params[3] == 1: phantom = np.zeros([N1, N2, N3], dtype="float32", order="C") else: raise ValueError( "The selected model is temporal (3D+time), use 'ModelTemporal' function instead" ) external.c_model3d( np.ascontiguousarray(phantom), model_no, N1, N2, N3, 0, N1, models_library_path, ) return phantom
[docs] def ModelSub( model_no: int, phantom_size: Union[int, Tuple[int, int, int]], sub_index: Tuple[int, int], models_library_path: Path, ) -> np.ndarray: """Generates a subset of 3D phantom (vertical cutoff) based on the model no. in the library file Phantom3DLibrary.dat. Args: model_no (int): Model number from the Phantom3DLibrary.dat library file. phantom_size (int, Tuple(int)): A scalar or a tuple with phantom dimensions. sub_index (Tuple[int, int]): a tuple containing 2 indeces (lower, upper) to select a vertical subset. models_library_path (Path): A path to the library file. Returns: np.ndarray: The generated 3D phantom. """ if type(phantom_size) == tuple: N1, N2, N3 = [int(i) for i in phantom_size] else: N1 = N2 = N3 = phantom_size Z1, Z2 = [int(i) for i in sub_index] rangecheck = Z2 > Z1 if not rangecheck: raise ValueError("Upper index must be larger than the lower one") rangecheck = Z1 >= 0 and Z1 < N3 if not rangecheck: raise ValueError("Range of the lower index is incorrect") rangecheck = Z2 >= 0 and Z2 <= N3 if not rangecheck: raise ValueError("Range of the higher index is incorrect") sub_size = Z2 - Z1 # the size of the vertical slab # check the validity of model's parameters params = np.ascontiguousarray(np.zeros([12], dtype=ctypes.c_int)) params = _check_params3d(model_no, models_library_path) __testParams3D(params) # check parameters and terminate if incorrect if params[3] == 1: phantom = np.zeros([sub_size, N2, N3], dtype="float32", order="C") else: raise ValueError( "The selected model is temporal (3D+time), use 'ModelTemporal' function instead" ) external.c_model3d( np.ascontiguousarray(phantom), model_no, N1, N2, N3, Z1, Z2, models_library_path, ) return phantom
[docs] def ModelTemporal( model_no: int, phantom_size: Union[int, Tuple[int, int, int]], models_library_path: Path, ) -> np.ndarray: """Generates 4D (3D+time) phantom based on the model no. in the library file Phantom3DLibrary.dat. Args: model_no (int): Model number from the Phantom3DLibrary.dat library file. phantom_size (int, Tuple(int)): A scalar or a tuple with phantom dimensions. models_library_path (Path): A path to the library file. Returns: np.ndarray: The generated 4D phantom. """ if type(phantom_size) == tuple: N1, N2, N3 = [int(i) for i in phantom_size] else: N1 = N2 = N3 = phantom_size # check the validity of model's parameters params = np.ascontiguousarray(np.zeros([12], dtype=ctypes.c_int)) params = _check_params3d(model_no, models_library_path) __testParams3D(params) # check parameters and terminate if incorrect if params[3] == 1: raise ValueError( "The selected model is stationary (3D), use 'Model' function instead" ) else: phantom = np.zeros([params[3], N1, N2, N3], dtype="float32", order="C") external.c_model3d( np.ascontiguousarray(phantom), model_no, N1, N2, N3, 0, N1, models_library_path, ) return phantom
[docs] def ModelTemporalSub( model_no: int, phantom_size: Union[int, Tuple[int, int, int]], sub_index: Tuple[int, int], models_library_path: Path, ) -> np.ndarray: """Generates a subset of 4D phantom (vertical cutoff) based on the model no. in the library file Phantom3DLibrary.dat. Args: model_no (int): Model number from the Phantom3DLibrary.dat library file. phantom_size (int, Tuple(int)): A scalar or a tuple with phantom dimensions. sub_index (Tuple[int, int]): a tuple containing 2 indeces (lower, upper) to select a vertical subset. models_library_path (Path): A path to the library file. Returns: np.ndarray: The generated 4D phantom. """ if type(phantom_size) == tuple: N1, N2, N3 = [int(i) for i in phantom_size] else: N1 = N2 = N3 = phantom_size Z1, Z2 = [int(i) for i in sub_index] rangecheck = Z2 > Z1 if not rangecheck: raise ValueError("Upper index must be larger than the lower one") rangecheck = Z1 >= 0 and Z1 < N3 if not rangecheck: raise ValueError("Range of the lower index is incorrect") rangecheck = Z2 >= 0 and Z2 <= N3 if not rangecheck: raise ValueError("Range of the higher index is incorrect") sub_size = Z2 - Z1 # the size of the vertical slab # check the validity of model's parameters params = np.ascontiguousarray(np.zeros([12], dtype=ctypes.c_int)) params = _check_params3d(model_no, models_library_path) __testParams3D(params) # check parameters and terminate if incorrect if params[3] == 1: raise ValueError( "The selected model is temporal (3D+time), use 'ModelTemporal' function instead" ) else: phantom = np.zeros([params[3], sub_size, N2, N3], dtype="float32", order="C") external.c_model3d( np.ascontiguousarray(phantom), model_no, N1, N2, N3, Z1, Z2, models_library_path, ) return phantom
[docs] def ModelSino( model_no: int, phantom_size: int, detector_horiz: int, detector_vert: int, angles: np.ndarray, models_library_path: Path, ) -> np.ndarray: """Generates a 3D analytical sinogram for corresponding models in the library file Phantom3DLibrary.dat. Args: model_no (int): Model number from the Phantom3DLibrary.dat library file. phantom_size (int): A scalar for squared phantom dimensions (2D slice). detector_horiz (int): Size of the horizontal detector in pixels. detector_vert (int): Size of the vertical detector in pixels. angles (np.ndarray): Angles vector in degrees. models_library_path (Path): A path to the library file. Returns: np.ndarray: The generated 3D projection data of the following shape [detector_vert, AngTot, detector_horiz]. """ if type(phantom_size) == tuple: raise ValueError( "Please give a scalar for the phantom size (2d slice), if one needs non-cubic data generator please see ModelSinoSub function" ) # check the validity of model's parameters params = np.ascontiguousarray(np.zeros([12], dtype=ctypes.c_int)) params = _check_params3d(model_no, models_library_path) __testParams3D(params) # check parameters and terminate if incorrect angles_total = len(angles) if params[3] == 1: # execute the model building function sino3d = np.zeros( [angles_total, detector_vert, detector_horiz], dtype="float32", order="C" ) else: raise ValueError( "The selected model is temporal (4D), use 'ModelTemporalSino' function instead" ) external.c_model_sino3d( np.ascontiguousarray(sino3d), model_no, detector_horiz, detector_vert, 0, detector_vert, phantom_size, np.ascontiguousarray(angles), angles_total, models_library_path, ) return np.swapaxes(sino3d, 0, 1)
[docs] def ModelSinoSub( model_no: int, phantom_size: int, detector_horiz: int, detector_vert: int, sub_index: tuple, angles: np.ndarray, models_library_path: Path, ) -> np.ndarray: """Generates a 3D analytical sinogram with vertical cutoff using sub_index tuple for corresponding models in the library file Phantom3DLibrary.dat. Args: model_no (int): Model number from the Phantom3DLibrary.dat library file. phantom_size (int): A scalar for squared phantom dimensions (2D slice). detector_horiz (int): Size of the horizontal detector in pixels. detector_vert (int): Size of the vertical detector in pixels. sub_index (Tuple[int, int]): a tuple containing 2 indeces (lower, upper) to select a vertical subset. angles (np.ndarray): Angles vector in degrees. models_library_path (Path): A path to the library file. Returns: np.ndarray: The generated 3D projection data of the following shape [detector_vert, AngTot, detector_horiz]. """ if type(phantom_size) == tuple: raise ValueError( "Please give a scalar for phantom size, projection data cannot be obtained for non-cubic phantom" ) Z1, Z2 = [int(i) for i in sub_index] rangecheck = Z2 > Z1 if not rangecheck: raise ValueError("Upper index must be larger than the lower one") rangecheck = Z1 >= 0 and Z1 < detector_vert if not rangecheck: raise ValueError("Range of the lower index is incorrect") rangecheck = Z2 >= 0 and Z2 <= detector_vert if not rangecheck: raise ValueError("Range of the higher index is incorrect") sub_size = Z2 - Z1 # the size of the vertical slab # check the validity of model's parameters params = np.ascontiguousarray(np.zeros([12], dtype=ctypes.c_int)) params = _check_params3d(model_no, models_library_path) __testParams3D(params) # check parameters and terminate if incorrect angles_total = len(angles) if params[3] == 1: # execute the model building function sino3d = np.zeros( [angles_total, sub_size, detector_horiz], dtype="float32", order="C" ) else: raise ValueError( "The selected model is temporal (4D), use 'ModelTemporalSino' function instead" ) external.c_model_sino3d( np.ascontiguousarray(sino3d), model_no, detector_horiz, detector_vert, Z1, Z2, phantom_size, np.ascontiguousarray(angles), angles_total, models_library_path, ) return np.swapaxes(sino3d, 0, 1)
[docs] def ModelSinoTemporal( model_no: int, phantom_size: int, detector_horiz: int, detector_vert: int, angles: np.ndarray, models_library_path: Path, ) -> np.ndarray: """Generates a 4D (3D+time) analytical sinogram for corresponding models in the library file Phantom3DLibrary.dat. Args: model_no (int): Temporal model number from the Phantom3DLibrary.dat library file. phantom_size (int): A scalar for squared phantom dimensions (2D slice). detector_horiz (int): Size of the horizontal detector in pixels. detector_vert (int): Size of the vertical detector in pixels. angles (np.ndarray): Angles vector in degrees. models_library_path (Path): A path to the library file. Returns: np.ndarray: The generated 4D projection data of the following shape [time_frames, detector_vert, AngTot, detector_horiz]. """ if type(phantom_size) == tuple: raise ValueError( "Please give a scalar for phantom size, projection data cannot be obtained for non-cubic phantom" ) # check the validity of model's parameters params = np.ascontiguousarray(np.zeros([12], dtype=ctypes.c_int)) params = _check_params3d(model_no, models_library_path) __testParams3D(params) # check parameters and terminate if incorrect angles_total = len(angles) if params[3] == 1: raise ValueError( "The selected model is stationary (3D), use 'ModelSino' function instead" ) else: sino4d = np.zeros( [params[3], angles_total, detector_vert, detector_horiz], dtype="float32", order="C", ) external.c_model_sino3d( np.ascontiguousarray(sino4d), model_no, detector_horiz, detector_vert, 0, detector_vert, phantom_size, np.ascontiguousarray(angles), angles_total, models_library_path, ) return np.swapaxes(sino4d, 1, 2)
[docs] def ModelSinoTemporalSub( model_no: int, phantom_size: int, detector_horiz: int, detector_vert: int, sub_index: tuple, angles: np.ndarray, models_library_path: Path, ) -> np.ndarray: """Generates a 4D analytical sinogram with vertical cutoff using sub_index tuple for corresponding models in the library file Phantom3DLibrary.dat. Args: model_no (int): Temporal model number from the Phantom3DLibrary.dat library file. phantom_size (int): A scalar for squared phantom dimensions (2D slice). detector_horiz (int): Size of the horizontal detector in pixels. detector_vert (int): Size of the vertical detector in pixels. sub_index (Tuple[int, int]): a tuple containing 2 indeces (lower, upper) to select a vertical subset. angles (np.ndarray): Angles vector in degrees. models_library_path (Path): A path to the library file. Returns: np.ndarray: The generated 3D projection data of the following shape [time, detector_vert, AngTot, detector_horiz]. """ if type(phantom_size) == tuple: raise ValueError( "Please give a scalar for phantom size, projection data cannot be obtained for non-cubic phantom" ) Z1, Z2 = [int(i) for i in sub_index] rangecheck = Z2 > Z1 if not rangecheck: raise ValueError("Upper index must be larger than the lower one") rangecheck = Z1 >= 0 and Z1 < detector_vert if not rangecheck: raise ValueError("Range of the lower index is incorrect") rangecheck = Z2 >= 0 and Z2 <= detector_vert if not rangecheck: raise ValueError("Range of the higher index is incorrect") sub_size = Z2 - Z1 # the size of the vertical slab # check the validity of model's parameters params = np.ascontiguousarray(np.zeros([12], dtype=ctypes.c_int)) params = _check_params3d(model_no, models_library_path) __testParams3D(params) # check parameters and terminate if incorrect angles_total = len(angles) if params[3] == 1: raise ValueError( "The selected model is stationary (3D), use 'ModelSino' function instead" ) else: sino4d = np.zeros( [params[3], angles_total, sub_size, detector_horiz], dtype="float32", order="C", ) external.c_model_sino3d( np.ascontiguousarray(sino4d), model_no, detector_horiz, detector_vert, Z1, Z2, phantom_size, np.ascontiguousarray(angles), angles_total, models_library_path, ) return np.swapaxes(sino4d, 1, 2)
[docs] def Object( phantom_size: Union[int, Tuple[int, int, int]], obj_params: Union[list, dict] ) -> np.ndarray: """Generates a 3D object for the standalone geometrical figure that is parametrised in the "obj_params" dictionary, see :ref:`ref_object_api`. Args: phantom_size (int, Tuple(int)): A scalar or a tuple with phantom dimensions. obj_params (a list of dicts or dict): A dictionary with parameters of an object, see :ref:`ref_object_api`. Returns: np.ndarray: The generated 3D object. """ if type(phantom_size) == tuple: N1, N2, N3 = [int(i) for i in phantom_size] else: N1 = N2 = N3 = phantom_size if type(obj_params) is dict: obj_params = [obj_params] object3d = np.zeros([N1, N2, N3], dtype="float32", order="C") # unpacking obj_params dictionary for obj in obj_params: if __testParams3d(obj): objectName = obj["Obj"].value C0 = obj["C0"] x0 = obj["x0"] y0 = obj["y0"] z0 = obj["z0"] a = obj["a"] b = obj["b"] c = obj["c"] phi1 = obj["phi1"] phi2 = 0.0 phi3 = 0.0 tt = 0 external.c_object3d( np.ascontiguousarray(object3d), N1, N2, N3, 0, N1, objectName, C0, x0, y0, z0, a, b, c, phi1, phi2, phi3, tt, ) return object3d
[docs] def ObjectSino( phantom_size: int, detector_horiz: int, detector_vert: int, angles: np.ndarray, obj_params: Union[list, dict], ) -> np.ndarray: """Generates a 3D analytical projection data for the standalone geometrical figure that is parametrised in the "obj_params" dictionary, see :ref:`ref_object_api`. Args: phantom_size (int): A scalar for phantom dimensions. detector_horiz (int): Size of the horizontal detector in pixels. detector_vert (int): Size of the vertical detector in pixels. angles (np.ndarray): Angles vector in degrees. obj_params (a list of dicts or dict): A dictionary with parameters of an object, see :ref:`ref_object_api`. Returns: np.ndarray: The generated 3D projection data for an object. """ if type(phantom_size) == tuple: raise ValueError( "Please give a scalar for phantom size, projection data cannot be obtained for non-cubic phantom" ) if type(obj_params) is dict: obj_params = [obj_params] angles_total = len(angles) tt = 0 sino3d = np.zeros( [angles_total, detector_vert, detector_horiz], dtype="float32", order="C" ) # unpacking obj_params dictionary for obj in obj_params: if __testParams3d(obj): objectName = obj["Obj"].value C0 = obj["C0"] x0 = obj["x0"] y0 = obj["y0"] z0 = obj["z0"] a = obj["a"] b = obj["b"] c = obj["c"] phi1 = obj["phi1"] phi2 = 0.0 phi3 = 0.0 tt = 0 external.c_object_sino3d( np.ascontiguousarray(sino3d), detector_horiz, detector_vert, 0, detector_vert, phantom_size, angles, angles_total, objectName, C0, x0, y0, z0, a, b, c, phi1, phi2, phi3, tt, ) return np.swapaxes(sino3d, 0, 1)
def __testParams3d(obj): """Performs a simple type check of the input parameters and a range check""" if not type(obj) is dict: raise TypeError("obj is not a dict {0}".format(type(obj))) # type check for k, v in obj.items(): if not isinstance(v, Number): if not k == "Obj": raise TypeError(k, "is not a Number") typecheck = True # range check rangecheck = obj["x0"] >= -1 and obj["x0"] <= 1 if not rangecheck: raise ValueError("x0 is out of range. Must be between -1 and 1") rangecheck = rangecheck and obj["y0"] >= -1 and obj["y0"] <= 1 if not rangecheck: raise ValueError("y0 is out of range. Must be between -1 and 1") rangecheck = rangecheck and obj["z0"] >= -1 and obj["z0"] <= 1 if not rangecheck: raise ValueError("z0 is out of range. Must be between -1 and 1") rangecheck = rangecheck and obj["a"] > 0 and obj["a"] <= 2 if not rangecheck: raise ValueError("a (object size) must be positive in [0,2] range") rangecheck = rangecheck and obj["b"] > 0 and obj["b"] <= 2 if not rangecheck: raise ValueError("b (object size) must be positive in [0,2] range") rangecheck = rangecheck and obj["c"] > 0 and obj["c"] <= 2 if not rangecheck: raise ValueError("c (object size) must be positive in [0,2] range") return rangecheck and typecheck def __testParams3D(obj): if obj[0] == 0: raise TypeError( "Check if the library file <Phantom3DLibrary.dat> exists, the given path is correct and the syntax is valid" ) if obj[1] == 0: raise TypeError( "The given model is not found, check available models in <Phantom3DLibrary.dat> file" ) if obj[2] == 0: raise TypeError( "Components number cannot be negative, check <Phantom3DLibrary.dat> file" ) if obj[3] == 0: raise TypeError( "TimeSteps cannot be negative, check <Phantom3DLibrary.dat> file" ) if obj[4] == 0: raise TypeError("Unknown name of the object, check <Phantom3DLibrary.dat> file") if obj[5] == 0: raise TypeError( "C0 should not be equal to zero, check <Phantom3DLibrary.dat> file" ) if obj[6] == 0: raise TypeError( "x0 (object position) must be in [-1,1] range, check <Phantom3DLibrary.dat> file" ) if obj[7] == 0: raise TypeError( "y0 (object position) must be in [-1,1] range, check <Phantom3DLibrary.dat> file" ) if obj[8] == 0: raise TypeError( "z0 (object position) must be in [-1,1] range, check <Phantom3DLibrary.dat> file" ) if obj[9] == 0: raise TypeError( "a (object size) must be positive in [0,2] range, check <Phantom3DLibrary.dat> file" ) if obj[10] == 0: raise TypeError( "b (object size) must be positive in [0,2] range, check <Phantom3DLibrary.dat> file" ) if obj[11] == 0: raise TypeError( "c (object size) must be positive in [0,2] range, check <Phantom3DLibrary.dat> file" ) return 0