Source code for flexdata.geometry

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
This module contains acquisition geometry classes: circular, linear, helical.
The circular class corresponds to the simplest case of circular orbit cone-beam CT with minimal number of parameters.
Additional parameters can be use to define a non-conventional geometry.
For instance: offsets and rotations ('det_roll', 'det_tan', 'det_ort', etc.), axis tilts ('axs_roll', 'axs_pitch'),
volume transformations ('vol_tra', 'vol_rot'), recostruction resolution and anisotropic sampling ('img_pixel', 'det_sample', 'vol_sample').
"""
# >>>>>>>>>>>>>>>>>>>>>>>>>>>> Imports >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

import numpy
import scipy.spatial
from datetime import datetime

FLEXTOML_VERSION = '0.1.0'


# >>>>>>>>>>>>>>>>>>>>>>>>>>>> Classes >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
[docs] class basic(): ''' Base geometry class. Needs only SDD, ODD and pixel size to initialize. Use 'parameters' to store parameters and 'description' to store relevant metadata. ''' def __init__(self, src2obj = None, det2obj = None, det_pixel = None, img_pixel = None, unit = 'mm'): ''' Constructor for the base geometry class. Args: src2obj : source to detector distance det2obj : object to detector distance det_pixel: detector pixel size img_pixel: reconstruction volume voxel size (optional) unit : unit length (default = 'mm') ''' # Default voxel size: if (not img_pixel) and (det_pixel): img_pixel = det_pixel / (src2obj + det2obj) * src2obj # Parameters: self.parameters = {'src2obj':src2obj, 'det2obj':det2obj, 'det_pixel':det_pixel, 'img_pixel':img_pixel, 'unit':unit, 'axs_tan':0, # rotation axis translation 'vol_rot':[0.0, 0.0, 0.0], # volume rotations and translation vectors 'vol_tra':[0.0, 0.0, 0.0], 'det_sample':[1,1], # detector and volume sampling (use to indicate that pixels were binned) 'vol_sample':[1,1,1] } self.description = { 'flextoml_version': FLEXTOML_VERSION, } def __str__(self): ''' Show my description. ''' return 'Geometry class: ' + str(type(self)) + '\n Parameters: \n' + str(self.parameters) def __repr__(self): ''' Show my description. ''' return 'Geometry class: ' + str(type(self)) + '\n Parameters: \n' + str(self.parameters) def __getitem__(self, key): ''' Retrieve one of the geometry parameters. ''' val = self.parameters.get(key) if val is None: print('WARNING! A property with an unknown key is requested: ' + key) return val def __setitem__(self, key, val): ''' Set one of the geometry parameters. ''' if not key in self.parameters.keys(): print('WARNING! A property with an unknown key is added to geometry: ' + key) self.parameters[key] = val
[docs] def copy(self): ''' Copy me. ''' geom = type(self)() geom.parameters = self.parameters.copy() geom.description = self.description.copy() return geom
[docs] def to_dictionary(self): ''' Return a dictionary describing this geometry. ''' records = self.parameters.copy() records.update(self.description) return records
[docs] def from_matrix(self, R, T): """ Rotates and translates the reconstruction volume. Args: R (3x3 array): rotation matrix T (1x3 array): translation vector """ # Translate to flex geometry: self.parameters['vol_rot'] = _mat2euler(R.T, axes='sxyz') self.parameters['vol_tra'] = numpy.array(self.parameters['vol_tra']) - numpy.dot(T, R.T)[[0,2,1]] * self.voxel
[docs] def from_dictionary(self, dictionary): ''' Use dictionary records to initialize this geometry. ''' # Fill gaps if needed: if not dictionary.get('src2obj'): if (not dictionary.get('src2det')): raise Exception('Filed missing in geometry record: src2det') elif (not dictionary.get('det2obj')): raise Exception('Filed missing in geometry record: det2obj') dictionary['src2obj'] = dictionary['src2det'] - dictionary['det2obj'] if (not dictionary.get('det2obj')): if (not dictionary.get('src2det')): raise Exception('Filed missing in geometry record: src2det') else: dictionary['det2obj'] = dictionary['src2det'] - dictionary['src2obj'] # Copy records: for key in dictionary.keys(): value = dictionary[key] if key in self.parameters.keys(): self.parameters[key] = value else: self.description[key] = value # After-check: if not self.parameters.get('img_pixel'): self.parameters['img_pixel'] = self.parameters['det_pixel'] / self.magnification if not self.parameters.get('det_pixel'): self.parameters['det_pixel'] = self.parameters['img_pixel'] * self.magnification # Check if all necessary keys are there: min_set = ['src2obj', 'det2obj', 'det_pixel'] for key in min_set: if not key in self.parameters: raise Exception('Geometry parameter missing after parcing: ' + key)
[docs] def log(self, message): """Add a message to the geometry changelog. This changelog will be saved with the geometry in a toml file. :param message: A message to log (single line). :returns: None :rtype: NoneType """ changelog = self.description.get('changelog', '') changelog += f"{datetime.now():%Y-%m-%d}: {message}\n" self.description['changelog'] = changelog
@property def vol_sample(self): ''' Voxel shape. ''' return self.parameters.get('vol_sample') @vol_sample.setter def vol_sample(self, sample): ''' Voxel shape. ''' self.parameters['vol_sample'] = sample @property def det_sample(self): ''' Pixel shape. ''' return self.parameters.get('det_sample') @det_sample.setter def det_sample(self, sample): ''' Pixel shape. ''' self.parameters['det_sample'] = sample @property def pixel(self): ''' Pixel size (mm). ''' return self.parameters.get('det_pixel') * numpy.array(self.parameters.get('det_sample')) @property def voxel(self): ''' Voxel size (mm). ''' return self.parameters.get('img_pixel') * numpy.array(self.parameters.get('vol_sample')) @property def src2obj(self): ''' Source-to-object distance. ''' return self.parameters.get('src2obj') @property def det2obj(self): ''' Detector-to-object distance. ''' return self.parameters.get('det2obj') @property def src2det(self): ''' Source-to-detector distance. ''' return self.parameters.get('src2obj') + self.parameters.get('det2obj') @property def magnification(self): ''' Magnification. ''' return self.src2det / self.src2obj
[docs] def volume_xyz(self, shape, offset = [0.,0.,0.]): """ Coordinate grid in units of the geometry. Args: shape : volume shape offset: offset in length units """ xx = (numpy.arange(0, shape[0]) - shape[0] / 2) * self.voxel[0] - offset[0] yy = (numpy.arange(0, shape[1]) - shape[1] / 2) * self.voxel[1] - offset[1] zz = (numpy.arange(0, shape[2]) - shape[2] / 2) * self.voxel[2] - offset[2] return xx, yy, zz
[docs] def from_astra_cone_vec(self, vectors): ''' Take vectors from ASTRA 'cone_vec' geometry. This will override any other parameters. ''' self._vectors_ = vectors
[docs] def astra_projection_geom(self, data_shape, index = None): ''' Get ASTRA projection geometry. Args: data_shape: [detector_count_z, theta_count, detector_count_x] index : if provided - sequence of the rotation angles Returns: geometry : ASTRA cone-beam geometry. ''' import astra # Get vectors: if hasattr(self, '_vectors_'): if index is None: vectors = self._vectors_ else: vectors = self._vectors_[index] else: vectors = self.get_vectors(data_shape[1], index) # Get ASTRA geometry: det_count_x = data_shape[2] det_count_z = data_shape[0] return astra.create_proj_geom('cone_vec', det_count_z, det_count_x, vectors)
[docs] def astra_volume_geom(self, vol_shape, slice_first = None, slice_last = None): ''' Initialize ASTRA volume geometry. Args: vol_shape : volume array shape slice_first: first slice of an ROI to update slice_last : last slice of an ROI to update ''' import astra # Shape and size (mm) of the volume vol_shape = numpy.array(vol_shape) vol_size = vol_shape * self.voxel if (slice_first is not None) & (slice_last is not None): # Generate volume geometry for one chunk of data: length = vol_shape[0] # Compute offset from the centre: centre = (length - 1) / 2 offset = (slice_first + slice_last) / 2 - centre offset = offset * self.voxel[0] shape = [slice_last - slice_first + 1, vol_shape[1], vol_shape[2]] vol_size = numpy.array(shape) * self.voxel[0] else: shape = vol_shape offset = 0 #vol_geom = astra.creators.create_vol_geom(shape[1], shape[2], shape[0], vol_geom = astra.create_vol_geom(shape[1], shape[2], shape[0], -vol_size[2]/2, vol_size[2]/2, -vol_size[1]/2, vol_size[1]/2, -vol_size[0]/2 + offset, vol_size[0]/2 + offset) return vol_geom
[docs] def get_vectors(self, proj_count, index = None): ''' Get source, detector and detector orientation vectors. Args: angle_count : number of rotation angles index : index of angles that should be used ''' # Create source orbit: src_vect = self.get_source_orbit(proj_count, index) # Create detector orbit (same as source but 180 degrees offset): det_vect, det_tan, det_rad, det_orth = self.get_detector_orbit(proj_count, index) # Apply global rotations and translations: self._transform_vectors_(src_vect, det_vect, det_tan, det_rad, det_orth) # Append all vectors together: vectors = numpy.concatenate([src_vect, det_vect, det_tan * self.pixel[1], det_orth * self.pixel[0]], axis = 1) return vectors
[docs] def get_source_orbit(self, proj_count = None, index = None): ''' Get the source orbit. In the base class it is a circular orbit. Args: proj_count: number of projections index : index of the projection subset Returns: src_pos : array of the source positions. Can be generated by circular_orbit(...), for instance. ''' raise Exception('Override this method in a geometry class derived from the base class!')
[docs] def get_detector_orbit(self, proj_count = None, index = None): ''' Get the detector orbit. In the base class it is a circular orbit. Args: proj_count: number of projections index : index of the projection subset Returns: src_pos : array of the source positions. Can be generated by circular_orbit(...), for instance. ''' raise Exception('Override this method in a geometry class derived from the base class!')
def _transform_vectors_(self, src_vect, det_vect, det_tan, det_rad, det_orth): ''' Rotate and translate vectors depending on the volume orientation. ''' vol_rot = self.parameters['vol_rot'] vol_tra = self.parameters['vol_tra'] # Rotate everything relative to the reconstruction volume: R = _euler2mat_(vol_rot[0], vol_rot[1], vol_rot[2], 'rzyx') det_tan[:] = numpy.dot(det_tan, R) det_rad[:] = numpy.dot(det_rad, R) det_orth[:] = numpy.dot(det_orth, R) src_vect[:] = numpy.dot(src_vect,R) det_vect[:] = numpy.dot(det_vect,R) # Add translation: T = numpy.array([vol_tra[1], vol_tra[2], vol_tra[0]]) src_vect -= numpy.dot(T, R) det_vect -= numpy.dot(T, R)
[docs] def detector_size(self, proj_shape): ''' Get the size of detector in length units. ''' if len(proj_shape) == 3: return numpy.array(proj_shape[::2]) * self.pixel else: return numpy.array(proj_shape) * self.pixel
[docs] def detector_centre(self): ''' Get the centre coordinate of the first position of the detector. ''' det_pos, det_tan, det_rad, det_orth = self.get_detector_orbit(proj_count = 3) return [det_pos[0][2], det_pos[0][0]]
[docs] def detector_bounds(self, proj_shape): ''' Get the boundaries of the detector at the start of the scan in length units. ''' det_pos, det_tan, det_rad, det_orth = self.get_detector_orbit(proj_count = 3) sz = self.detector_size(proj_shape) / 2 cntr = self.detector_centre() vrt = [-sz[0], sz[0]] hrz = [-sz[1], sz[1]] return numpy.array([vrt, hrz]) + numpy.array(cntr)[:, None]
[docs] def volume_size(self, vol_shape): ''' Return volume size in length units. ''' return numpy.array(vol_shape) * self.voxel
[docs] def volume_bounds(self, vol_shape): ''' Return volume bounds: ''' sz = self.volume_size(vol_shape) / 2 vrt = [-sz[0], sz[0]] mag = [-sz[1], sz[1]] hrz = [-sz[2], sz[2]] return numpy.array([vrt, mag, hrz]) + numpy.array(self.parameters['vol_tra'])[:, None]
[docs] class circular(basic): ''' Circular orbit geometry class. Includes additional parameters such as detector and source shifts and rotations. ''' def __init__(self, src2obj = None, det2obj = None, det_pixel = None, img_pixel = None, ang_range = (0, 360), unit = 'mm'): ''' Constructor for the circular geometry class. Args: src2obj : source to detector distance det2obj : object to detector distance det_pixel: detector pixel size img_pixel: reconstruction volume voxel size (optional) ang_range: range of rotation (default = 0..360) unit : unit length (default = 'mm') ''' # Parent init: basic.__init__(self, src2obj, det2obj, det_pixel, img_pixel, unit) # Additional parameters: self.parameters.update({ 'ang_range': ang_range, 'src_ort':0, # source vertical, tangential shifts 'src_tan':0, 'det_ort':0, # detector shifts and rotations (in degrees) 'det_tan':0, 'det_roll':0, 'det_pitch':0, 'det_yaw':0, 'axs_roll':0, # rotation axis roll and pitch (in degrees) 'axs_pitch':0, })
[docs] def get_thetas(self, proj_count = None, index = None): ''' Get rotation angles. Either returns an equidistant array or self.thetas if that is defined. Args: proj_count : number of angles in equidistant case. Not used if 'thetas' are defined explicitly in parameters. Returns: thetas : angle array in degrees ''' thetas = self.parameters.get('thetas') # Initialize thetas: if thetas is None: thetas = numpy.linspace(self['ang_range'][0], self['ang_range'][1], proj_count) if not index is None: thetas = thetas[index] return thetas
[docs] def get_source_orbit(self, proj_count = None, index = None): ''' Get the source orbit. Args: proj_count: number of projections index : index of the projection subset Returns: src_pos : array of the source positions. ''' src2obj = self.src2obj src_ort = self.parameters['src_ort'] src_tan = self.parameters['src_tan'] axs_tan = self.parameters['axs_tan'] axs_roll = self.parameters['axs_roll'] axs_pitch = self.parameters['axs_pitch'] # Create source orbit: thetas = self.get_thetas(proj_count) # src_ort may be a vector or a scalar: org = numpy.outer(src_ort, [0,0,1]) src_vect, src_tan, src_rad, serc_orth = circular_orbit(src2obj, thetas, roll = axs_roll, pitch = axs_pitch, yaw = 0, origin = org, tan_shift = src_tan - axs_tan, index = index) return src_vect
[docs] def get_detector_orbit(self, proj_count = None, index = None): ''' Get the detector orbit. Args: proj_count: number of projections index : index of the projection subset Returns: det_pos : array of the detector positions. det_tan : array of detector tangential directional vector det_rad : array of detector radial directional vector det_orth: array of detector orthogonal directional vector ''' det2obj = self.det2obj # Detector translations: det_ort = self.parameters['det_ort'] det_tan = self.parameters['det_tan'] # Rotation axis translations and rotations: axs_tan = self.parameters['axs_tan'] axs_roll = self.parameters['axs_roll'] axs_pitch = self.parameters['axs_pitch'] # Detector rotations: det_roll = self.parameters['det_roll'] det_yaw = self.parameters['det_yaw'] det_pitch = self.parameters['det_pitch'] # Create detector orbit: thetas = self.get_thetas(proj_count) # det_ort may be a vector or a scalar: org = numpy.outer(det_ort, [0,0,1]) det_pos, det_tan, det_rad, det_orth = circular_orbit(det2obj, thetas, roll = axs_roll, pitch = axs_pitch, yaw = 180, origin = org, tan_shift = -det_tan + axs_tan, index = index) # Invert vectors to keep them alligned with the source vectors: det_tan, det_rad, det_orth = -det_tan, -det_rad, -det_orth # Apply detector rotations: for ii in range(det_pos.shape[0]): T = _axangle2mat_(det_rad[ii, :], det_roll) det_tan[ii, :] = T.dot(det_tan[ii, :]) det_orth[ii, :] = T.dot(det_orth[ii, :]) T = _axangle2mat_(det_orth[ii, :], det_yaw) det_tan[ii, :] = T.dot(det_tan[ii, :]) det_rad[ii, :] = T.dot(det_rad[ii, :]) T = _axangle2mat_(det_tan[ii, :], det_pitch) det_rad[ii, :] = T.dot(det_rad[ii, :]) det_orth[ii, :] = T.dot(det_orth[ii, :]) return det_pos, det_tan, det_rad, det_orth
[docs] class helical(circular): ''' Helical orbit geometry class. Similar to the 'circular' class with additional parameter of helix ''' def __init__(self, src2obj = None, det2obj = None, det_pixel = None, img_pixel = None, axis_range = (0, 100), ang_range = (0, 720), unit = 'mm'): ''' Constructor for the helical geometry class. Args: src2obj : source to detector distance det2obj : object to detector distance det_pixel: detector pixel size img_pixel: reconstruction volume voxel size (optional) axis_range: range of the movement along the axis of rotation ang_range: range of angles (default = 0..360) unit : unit length (default = 'mm') ''' # Parent init: circular.__init__(self, src2obj, det2obj, det_pixel, img_pixel, ang_range, unit) # Additional parameters: self.parameters['axs_rng'] = axis_range
[docs] def get_source_orbit(self, proj_count = None, index = None): ''' Get the source orbit. In the base class it is a circular orbit. ''' src2obj = self.src2obj src_ort = self.parameters['src_ort'] src_tan = self.parameters['src_tan'] axs_tan = self.parameters['axs_tan'] axs_roll = self.parameters['axs_roll'] axs_pitch = self.parameters['axs_pitch'] # Create source orbit: thetas = self.get_thetas(proj_count) # src_ort may be a vector or a scalar: org = numpy.outer(src_ort, [0,0,1]) src_vect, src_tan, src_rad, src_orth = circular_orbit(src2obj, thetas, roll = axs_roll, pitch = axs_pitch, yaw = 0, origin = org, tan_shift = src_tan - axs_tan, index = index) # Add axial motion: vrt = numpy.linspace(self.parameters['axs_rng'][0], self.parameters['axs_rng'][1], proj_count) if index: vrt = vrt[index] src_vect = src_vect + src_orth * vrt[:, None] return src_vect
[docs] def get_detector_orbit(self, proj_count = None, index = None): ''' Get the detector orbit. In the base class it is a circular orbit. ''' det2obj = self.det2obj # Detector translations: det_ort = self.parameters['det_ort'] det_tan = self.parameters['det_tan'] # Rotation axis translations and rotations: axs_tan = self.parameters['axs_tan'] axs_roll = self.parameters['axs_roll'] axs_pitch = self.parameters['axs_pitch'] # Detector rotations: det_roll = self.parameters['det_roll'] det_yaw = self.parameters['det_yaw'] det_pitch = self.parameters['det_pitch'] # Create detector orbit: thetas = self.get_thetas(proj_count) # det_ort may be a vector or a scalar: org = numpy.outer(det_ort, [0,0,1]) det_pos, det_tan, det_rad, det_orth = circular_orbit(det2obj, thetas, roll = axs_roll, pitch = axs_pitch, yaw = 180, origin = org, tan_shift = -det_tan + axs_tan, index = index) # Add axial motion: vrt = numpy.linspace(self.parameters['axs_rng'][0], self.parameters['axs_rng'][1], proj_count) if index: vrt = vrt[index] det_pos = det_pos + det_orth * vrt[:, None] # Invert vectors to keep them alligned with the source vectors: det_tan, det_rad, det_orth = -det_tan, -det_rad, -det_orth # Apply detector rotations: for ii in range(det_pos.shape[0]): T = _axangle2mat_(det_rad[ii, :], det_roll) det_tan[ii, :] = T.dot(det_tan[ii, :]) det_orth[ii, :] = T.dot(det_orth[ii, :]) T = _axangle2mat_(det_orth[ii, :], det_yaw) det_tan[ii, :] = T.dot(det_tan[ii, :]) det_rad[ii, :] = T.dot(det_rad[ii, :]) T = _axangle2mat_(det_tan[ii, :], det_pitch) det_rad[ii, :] = T.dot(det_rad[ii, :]) det_orth[ii, :] = T.dot(det_orth[ii, :]) return det_pos, det_tan, det_rad, det_orth
[docs] class linear(basic): ''' A simple linear orbit geometry class. ''' def __init__(self, src2obj = None, det2obj = None, det_pixel = None, img_pixel = None, src_hrz_rng = (0, 1), src_vrt_rng = (0, 1), det_hrz_rng = (1, 0), det_vrt_rng = (1, 0), unit = 'mm'): ''' Constructor for the linear geometry class. Args: src2obj : source to detector distance det2obj : object to detector distance det_pixel: detector pixel size img_pixel: reconstruction volume voxel size (optional) src_hrz_rng: source horizlontal movement range src_vrt_rng: source vertical movement range det_hrz_rng: detector horizlontal movement range det_vrt_rng: detector vertical movement range unit : unit length (default = 'mm') ''' # Parent init: basic.__init__(self, src2obj, det2obj, det_pixel, img_pixel, unit) # Additional parameters: self.parameters.update({ 'src_hrz_rng':src_hrz_rng, # source vertical, horizlontal motion range 'src_vrt_rng':src_vrt_rng, 'det_hrz_rng':det_hrz_rng, # source vertical, horizlontal motion range 'det_vrt_rng':det_vrt_rng, 'det_roll':0, # detector rotations (in degrees) 'det_pitch':0, 'det_yaw':0, })
[docs] def get_source_orbit(self, proj_count = None, index = None): ''' Get the source orbit. In the base class it is a circular orbit. ''' src2obj = self.src2obj src_hrz_rng = self.parameters['src_hrz_rng'] src_vrt_rng = self.parameters['src_vrt_rng'] # Create source orbit: src_vect, src_tan, src_rad, serc_ort = linear_orbit(src_hrz_rng, (-src2obj, -src2obj), src_vrt_rng, proj_count, index) return src_vect
[docs] def get_detector_orbit(self, proj_count = None, index = None): ''' Get the detector orbit. In the base class it is a circular orbit. ''' det2obj = self.det2obj det_hrz_rng = self.parameters['det_hrz_rng'] det_vrt_rng = self.parameters['det_vrt_rng'] # Detector rotations: det_roll = self.parameters['det_roll'] det_yaw = self.parameters['det_yaw'] det_pitch = self.parameters['det_pitch'] # Create detector orbit: det_pos, det_tan, det_rad, det_orth = linear_orbit(det_hrz_rng, (det2obj, det2obj), det_vrt_rng, proj_count, index) # Apply detector rotations: for ii in range(det_pos.shape[0]): T = _axangle2mat_(det_rad[ii, :], det_roll) det_tan[ii, :] = T.dot(det_tan[ii, :]) det_orth[ii, :] = T.dot(det_orth[ii, :]) T = _axangle2mat_(det_orth[ii, :], det_yaw) det_tan[ii, :] = T.dot(det_tan[ii, :]) det_rad[ii, :] = T.dot(det_rad[ii, :]) T = _axangle2mat_(det_tan[ii, :], det_pitch) det_rad[ii, :] = T.dot(det_rad[ii, :]) det_orth[ii, :] = T.dot(det_orth[ii, :]) return det_pos, det_tan, det_rad, det_orth
# >>>>>>>>>>>>>>>>>>>>>>>>>>>> Static methods >>>>>>>>>>>>>>>>>>>>>>>
[docs] def tiles_shape(shape, geometry_list): """ Compute the size of the stiched dataset. Args: shape: shape of a single projection stack. geometry_list: list of geometries. """ # Phisical detector size: min_x, min_y = numpy.inf, numpy.inf max_x, max_y = -numpy.inf, -numpy.inf det_pixel = geometry_list[0].pixel axs_hrz = 0 # Find out the size required for the final dataset for geo in geometry_list: bounds = geo.detector_bounds(shape) min_x = min([min_x, bounds[1][0]]) min_y = min([min_y, bounds[0][0]]) max_x = max([max_x, bounds[1][1]]) max_y = max([max_y, bounds[0][1]]) axs_hrz += geo['axs_tan'] / len(geometry_list) # Big slice: new_shape = numpy.array([(max_y - min_y) / det_pixel[0], shape[1], (max_x - min_x) / det_pixel[1]]) new_shape = numpy.ceil(new_shape).astype('int') # safety margin.. # Copy one of the geometry records and sett the correct translation: geometry = geometry_list[0].copy() geometry['axs_tan'] = axs_hrz geometry['det_tan'] = (max_x + min_x) / 2 + axs_hrz geometry['det_ort'] = (max_y + min_y) / 2 # Update volume center: geometry['vol_tra'][0] = (geometry['det_ort'] * geometry.src2obj + geometry['src_ort'] * geometry.det2obj) / geometry.src2det geometry['vol_tra'][2] = axs_hrz return new_shape, geometry
[docs] def astra_projection_geom(geom, data_shape, index = None): ''' Initialize ASTRA projection geometry. Args: geom : geometry class data_shape: [detector_count_z, theta_count, detector_count_x] index : if provided - sequence of the rotation angles ''' return geom.astra_projection_geom(data_shape, index)
[docs] def astra_volume_geom(geom, vol_shape, slice_first = None, slice_last = None): ''' Initialize ASTRA volume geometry. ''' return geom.astra_volume_geom(vol_shape, slice_first = None, slice_last = None)
[docs] def get_vectors(geom, angle_count, index = None): ''' Get source, detector and detector orientation vectors. Args: geom : geometry class angle_count : number of rotation angles index : index of angles that should be used ''' return geom.get_vectors(angle_count, index)
[docs] def detector_size(geom, proj_shape): ''' Get the size of detector in length units. ''' return geom.detector_size(proj_shape)
[docs] def detector_bounds(geom, proj_shape): ''' Get the boundaries of the detector in length units. ''' return geom.detector_bounds(proj_shape)
[docs] def volume_bounds(geom, proj_shape): ''' A very simplified version of volume bounds... ''' return geom.volume_bounds(proj_shape)
[docs] def volume_shape(geom, proj_shape): ''' Based on physical volume bnounds compute shape in pixels: ''' return geom.volume_shape(proj_shape)
[docs] def linear_orbit(hrz_rng, rad_rng, vrt_rng, proj_count, index = None): ''' Generate a linear orbit vector. Args: hrz_rng: horizontal range of motion rad_rng: radial range of motion vrt_rng: vertical range of motion Returns: position : position vector tangent : tangent direction radius : radal direction orthogonal : orthogonal direction ''' h = numpy.linspace(hrz_rng[0], hrz_rng[1], proj_count) r = numpy.linspace(rad_rng[0], rad_rng[1], proj_count) v = numpy.linspace(vrt_rng[0], vrt_rng[1], proj_count) position = numpy.stack((h, r, v)).transpose((1,0)) radius = numpy.zeros([proj_count, 3]) radius[:, 1] = 1 tangent = numpy.zeros([proj_count, 3]) tangent[:, 0] = 1 # Orthogonal to tangent and radius: orthogonal = numpy.cross(radius, tangent) if index is not None: return position[index], tangent[index], radius[index], orthogonal[index] else: return position, tangent, radius, orthogonal
[docs] def circular_orbit(radius, thetas, roll = 0, pitch = 0, yaw = 0, origin = [0, 0, 0], tan_shift = 0, index = None): ''' Generate a circular orbit vector. Args: radius : orbit radius angle_count: number of rotation angles roll, pitch: define orientation of the rotation axis yaw : initial angular position origin : xyz vector of the orbit centre tan_shift : tangential shift from the default position (scalar or array) index : index of the subset of total rotation angles Returns: position : position vector tangent : tangent direction radius : radal direction orthogonal : orthogonal direction ''' # Generate axis and orthogonals: M = _euler2mat_(pitch, roll, 0) axis = M.dot([0, 0, 1]) tan0 = M.dot([1, 0, 0]) rad0 = M.dot([0, 1, 0]) # Genertate initial circular orbit: M = _axangle2mat_(axis, yaw) v0 = M.dot([0, -radius, 0]) tan0 = M.dot(tan0) rad0 = M.dot(rad0) position = numpy.zeros([len(thetas), 3]) tangent = numpy.zeros([len(thetas), 3]) radius = numpy.zeros([len(thetas), 3]) for ii, theta in enumerate(thetas): Rt = _axangle2mat_(axis, theta) position[ii, :] = Rt.dot(v0) tangent[ii, :] = Rt.dot(tan0) radius[ii, :] = Rt.dot(rad0) # Apply origin shift: if numpy.ndim(origin) == 1: position += numpy.array(origin)[None, :] else: position += numpy.array(origin) # Apply other shifts: if numpy.size(tan_shift) == 1: position += tangent * tan_shift else: position += tangent * tan_shift[:, None] # Orthogonal to tangent and radius: orthogonal = numpy.cross(radius, tangent) if index is not None: return position[index], tangent[index], radius[index], orthogonal[index] else: return position, tangent, radius, orthogonal
def _convertAxesToScipy(axes): # convert from transforms3d convention to scipy convention if axes[0] == 'r': # 'rxyz', rotating/intrinsic axes -> XYZ return axes[1:].upper() elif axes[0] == 's': # 'sxyz', static/extrinsic axes -> xyz return axes[1:].lower() else: # assumed to be already in scipy format return axes def _mat2euler(M, axes='sxyz'): seq = _convertAxesToScipy(axes) return scipy.spatial.transform.Rotation.from_matrix(M).as_euler(seq=seq, degrees=True) def _euler2mat_(a, b, c, axes='sxyz'): seq = _convertAxesToScipy(axes) return scipy.spatial.transform.Rotation.from_euler(seq=seq, angles=[a,b,c], degrees=True).as_matrix() def _axangle2mat_(ax, a): a = numpy.deg2rad(a) x,y,z = ax n = numpy.sqrt(x*x + y*y + z*z) x = a*x/n y = a*y/n z = a*z/n return scipy.spatial.transform.Rotation.from_rotvec([x,y,z]).as_matrix()