# -----------------------------------------------------------------------
# Copyright: 2010-2022, imec Vision Lab, University of Antwerp
# 2013-2022, CWI, Amsterdam
#
# Contact: astra@astra-toolbox.com
# Website: http://www.astra-toolbox.com/
#
# This file is part of the ASTRA Toolbox.
#
#
# The ASTRA Toolbox is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# The ASTRA Toolbox is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with the ASTRA Toolbox. If not, see <http://www.gnu.org/licenses/>.
#
# -----------------------------------------------------------------------
"""Additional functions for PyAstraToolbox.
.. moduleauthor:: Daniel M. Pelt <D.M.Pelt@cwi.nl>
"""
from copy import deepcopy
from . import creators as ac
import numpy as np
try:
from six.moves import range
except ImportError:
# six 1.3.0
from six.moves import xrange as range
from . import data2d
from . import data3d
from . import projector
from . import algorithm
from . import pythonutils
from .log import AstraError
[docs]
def clear():
"""Clears all used memory of the ASTRA Toolbox.
.. note::
This is irreversible.
"""
data2d.clear()
data3d.clear()
projector.clear()
algorithm.clear()
[docs]
def data_op(op, data, scalar, gpu_core, mask=None):
"""Perform data operation on data.
:param op: Operation to perform.
:param data: Data to perform operation on.
:param scalar: Scalar argument to data operation.
:param gpu_core: GPU core to perform operation on.
:param mask: Optional mask.
"""
cfg = ac.astra_dict('DataOperation_CUDA')
cfg['Operation'] = op
cfg['Scalar'] = scalar
cfg['DataId'] = data
if not mask == None:
cfg['MaskId'] = mask
cfg['option']['GPUindex'] = gpu_core
alg_id = algorithm.create(cfg)
algorithm.run(alg_id)
algorithm.delete(alg_id)
[docs]
def add_noise_to_sino(sinogram_in, I0, seed=None):
"""Adds Poisson noise to a sinogram.
:param sinogram_in: Sinogram to add noise to.
:type sinogram_in: :class:`numpy.ndarray`
:param I0: Background intensity. Lower values lead to higher noise.
:type I0: :class:`float`
:returns: :class:`numpy.ndarray` -- the sinogram with added noise.
"""
if not seed==None:
curstate = np.random.get_state()
np.random.seed(seed)
if isinstance(sinogram_in, np.ndarray):
sinogramRaw = sinogram_in
else:
sinogramRaw = data2d.get(sinogram_in)
max_sinogramRaw = sinogramRaw.max()
sinogramRawScaled = sinogramRaw / max_sinogramRaw
# to detector count
sinogramCT = I0 * np.exp(-sinogramRawScaled)
# add poison noise
sinogramCT_C = np.zeros_like(sinogramCT)
for i in range(sinogramCT_C.shape[0]):
for j in range(sinogramCT_C.shape[1]):
sinogramCT_C[i, j] = np.random.poisson(sinogramCT[i, j])
# to density
sinogramCT_D = sinogramCT_C / I0
sinogram_out = -max_sinogramRaw * np.log(sinogramCT_D)
if not isinstance(sinogram_in, np.ndarray):
data2d.store(sinogram_in, sinogram_out)
if not seed==None:
np.random.set_state(curstate)
return sinogram_out
[docs]
def move_vol_geom(geom, pos, is_relative=False):
"""Moves center of volume geometry to new position.
:param geom: Input volume geometry
:type geom: :class:`dict`
:param pos: Tuple (x,y[,z]) for new position, with the center of the image at (0,0[,0])
:type pos: :class:`tuple`
:param is_relative: Whether new position is relative to the old position
:type is_relative: :class:`bool`
:returns: :class:`dict` -- Volume geometry with the new center
"""
ret_geom = geom.copy()
ret_geom['option'] = geom['option'].copy()
if not is_relative:
ret_geom['option']['WindowMinX'] = -geom['GridColCount']/2.
ret_geom['option']['WindowMaxX'] = geom['GridColCount']/2.
ret_geom['option']['WindowMinY'] = -geom['GridRowCount']/2.
ret_geom['option']['WindowMaxY'] = geom['GridRowCount']/2.
if len(pos)>2:
ret_geom['option']['WindowMinZ'] = -geom['GridSliceCount']/2.
ret_geom['option']['WindowMaxZ'] = geom['GridSliceCount']/2.
ret_geom['option']['WindowMinX'] += pos[0]
ret_geom['option']['WindowMaxX'] += pos[0]
ret_geom['option']['WindowMinY'] += pos[1]
ret_geom['option']['WindowMaxY'] += pos[1]
if len(pos)>2:
ret_geom['option']['WindowMinZ'] += pos[2]
ret_geom['option']['WindowMaxZ'] += pos[2]
return ret_geom
[docs]
def geom_size(geom, dim=None):
"""Returns the size of a volume or sinogram, based on the projection or volume geometry.
:param geom: Geometry to calculate size from
:type geometry: :class:`dict`
:param dim: Optional axis index to return
:type dim: :class:`int`
"""
return pythonutils.geom_size(geom,dim)
[docs]
def geom_2vec(proj_geom):
"""Returns a vector-based projection geometry from a basic projection geometry.
:param proj_geom: Projection geometry to convert
:type proj_geom: :class:`dict`
"""
if proj_geom['type'] == 'parallel':
angles = proj_geom['ProjectionAngles']
vectors = np.zeros((len(angles), 6))
for i in range(len(angles)):
# source
vectors[i, 0] = np.sin(angles[i])
vectors[i, 1] = -np.cos(angles[i])
# center of detector
vectors[i, 2] = 0
vectors[i, 3] = 0
# vector from detector pixel 0 to 1
vectors[i, 4] = np.cos(angles[i]) * proj_geom['DetectorWidth']
vectors[i, 5] = np.sin(angles[i]) * proj_geom['DetectorWidth']
proj_geom_out = ac.create_proj_geom(
'parallel_vec', proj_geom['DetectorCount'], vectors)
elif proj_geom['type'] == 'fanflat':
angles = proj_geom['ProjectionAngles']
vectors = np.zeros((len(angles), 6))
for i in range(len(angles)):
# source
vectors[i, 0] = np.sin(angles[i]) * proj_geom['DistanceOriginSource']
vectors[i, 1] = -np.cos(angles[i]) * proj_geom['DistanceOriginSource']
# center of detector
vectors[i, 2] = -np.sin(angles[i]) * proj_geom['DistanceOriginDetector']
vectors[i, 3] = np.cos(angles[i]) * proj_geom['DistanceOriginDetector']
# vector from detector pixel 0 to 1
vectors[i, 4] = np.cos(angles[i]) * proj_geom['DetectorWidth']
vectors[i, 5] = np.sin(angles[i]) * proj_geom['DetectorWidth']
proj_geom_out = ac.create_proj_geom(
'fanflat_vec', proj_geom['DetectorCount'], vectors)
elif proj_geom['type'] == 'cone':
angles = proj_geom['ProjectionAngles']
vectors = np.zeros((len(angles), 12))
for i in range(len(angles)):
# source
vectors[i, 0] = np.sin(angles[i]) * proj_geom['DistanceOriginSource']
vectors[i, 1] = -np.cos(angles[i]) * proj_geom['DistanceOriginSource']
vectors[i, 2] = 0
# center of detector
vectors[i, 3] = -np.sin(angles[i]) * proj_geom['DistanceOriginDetector']
vectors[i, 4] = np.cos(angles[i]) * proj_geom['DistanceOriginDetector']
vectors[i, 5] = 0
# vector from detector pixel (0,0) to (0,1)
vectors[i, 6] = np.cos(angles[i]) * proj_geom['DetectorSpacingX']
vectors[i, 7] = np.sin(angles[i]) * proj_geom['DetectorSpacingX']
vectors[i, 8] = 0
# vector from detector pixel (0,0) to (1,0)
vectors[i, 9] = 0
vectors[i, 10] = 0
vectors[i, 11] = proj_geom['DetectorSpacingY']
proj_geom_out = ac.create_proj_geom(
'cone_vec', proj_geom['DetectorRowCount'], proj_geom['DetectorColCount'], vectors)
# PARALLEL
elif proj_geom['type'] == 'parallel3d':
angles = proj_geom['ProjectionAngles']
vectors = np.zeros((len(angles), 12))
for i in range(len(angles)):
# ray direction
vectors[i, 0] = np.sin(angles[i])
vectors[i, 1] = -np.cos(angles[i])
vectors[i, 2] = 0
# center of detector
vectors[i, 3] = 0
vectors[i, 4] = 0
vectors[i, 5] = 0
# vector from detector pixel (0,0) to (0,1)
vectors[i, 6] = np.cos(angles[i]) * proj_geom['DetectorSpacingX']
vectors[i, 7] = np.sin(angles[i]) * proj_geom['DetectorSpacingX']
vectors[i, 8] = 0
# vector from detector pixel (0,0) to (1,0)
vectors[i, 9] = 0
vectors[i, 10] = 0
vectors[i, 11] = proj_geom['DetectorSpacingY']
proj_geom_out = ac.create_proj_geom(
'parallel3d_vec', proj_geom['DetectorRowCount'], proj_geom['DetectorColCount'], vectors)
elif proj_geom['type'] in ['parallel_vec', 'fanflat_vec', 'parallel3d_vec', 'cone_vec']:
return deepcopy(proj_geom)
else:
raise AstraError('No suitable vector geometry found for type: ' + proj_geom['type'])
return proj_geom_out
[docs]
def geom_postalignment(proj_geom, factor):
"""Apply a postalignment to a vector-based projection geometry.
Can be used to model the rotation axis offset.
For 2D geometries, the argument factor is a single float specifying the
distance to shift the detector (measured in detector pixels).
For 3D geometries, factor can be a pair of floats specifying the horizontal
resp. vertical distances to shift the detector. If only a single float
is specified, this is treated as a horizontal shift.
:param proj_geom: input projection geometry
:type proj_geom: :class:`dict`
:param factor: number of pixels to shift the detector
:type factor: :class:`float`
"""
proj_geom = geom_2vec(proj_geom)
if proj_geom['type'] == 'parallel_vec' or proj_geom['type'] == 'fanflat_vec':
V = proj_geom['Vectors']
V[:,2:4] = V[:,2:4] + factor * V[:,4:6]
elif proj_geom['type'] == 'parallel3d_vec' or proj_geom['type'] == 'cone_vec':
if isinstance(factor, (float, int)):
factor = factor, 0
V = proj_geom['Vectors']
V[:,3:6] = V[:,3:6] + factor[0] * V[:,6:9]
if len(factor) > 1: # Accommodate factor = (number,) semantics
V[:,3:6] = V[:,3:6] + factor[1] * V[:,9:12]
else:
raise AstraError(proj_geom['type'] + 'geometry is not suitable for postalignment')
return proj_geom