Source code for cil.plugins.tigre.ProjectionOperator

#  Copyright 2021 United Kingdom Research and Innovation
#  Copyright 2021 The University of Manchester
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
#
# Authors:
# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt
import logging

import numpy as np

from cil.framework import ImageData, AcquisitionData, BlockGeometry
from cil.framework.labels import AcquisitionDimension, ImageDimension
from cil.optimisation.operators import BlockOperator, LinearOperator
from cil.plugins.tigre import CIL2TIGREGeometry

log = logging.getLogger(__name__)

try:
    from _Atb import _Atb_ext as Atb
    from _Ax import _Ax_ext as Ax

except ModuleNotFoundError:
    raise ModuleNotFoundError(
        "This plugin requires the additional package TIGRE\n" +
        "Please install it via conda as tigre from the ccpi channel")

try:
    from tigre.utilities.gpu import GpuIds
    has_gpu_sel = True
except ModuleNotFoundError:
    has_gpu_sel = False


[docs] class ProjectionOperator(LinearOperator): """ ProjectionOperator configures and calls TIGRE Projectors for your dataset. Please refer to the TIGRE documentation for further descriptions https://github.com/CERN/TIGRE https://iopscience.iop.org/article/10.1088/2057-1976/2/5/055010 Parameters ---------- image_geometry : `ImageGeometry`, default used if None A description of the area/volume to reconstruct acquisition_geometry :`AcquisitionGeometry`, `BlockGeometry` A description of the acquisition data. If passed a BlockGeometry it will return a BlockOperator. direct_method : str, default 'interpolated' The method used by the forward projector, 'Siddon' for ray-voxel intersection, 'interpolated' for interpolated projection adjoint_weights : str, default 'matched' The weighting method used by the cone-beam backward projector, 'matched' for weights to approximately match the 'interpolated' forward projector, 'FDK' for FDK weights Example ------- >>> from cil.plugins.tigre import ProjectionOperator >>> PO = ProjectionOperator(image.geometry, data.geometry) >>> forward_projection = PO.direct(image) >>> backward_projection = PO.adjoint(data) """ def __new__(cls, image_geometry=None, acquisition_geometry=None, \ direct_method='interpolated',adjoint_weights='matched', **kwargs): if isinstance(acquisition_geometry, BlockGeometry): log.info("BlockOperator is returned.") K = [] for ag in acquisition_geometry: K.append( ProjectionOperator_ag(image_geometry=image_geometry, acquisition_geometry=ag, \ direct_method=direct_method, adjoint_weights=adjoint_weights, **kwargs) ) return BlockOperator(*K) else: log.info("Standard Operator is returned.") return super(ProjectionOperator, cls).__new__(ProjectionOperator_ag)
class ProjectionOperator_ag(ProjectionOperator): '''TIGRE Projection Operator''' def __init__(self, image_geometry=None, acquisition_geometry=None, direct_method='interpolated', adjoint_weights='matched', **kwargs): """ ProjectionOperator configures and calls TIGRE Projectors for your dataset. Please refer to the TIGRE documentation for further descriptions https://github.com/CERN/TIGRE https://iopscience.iop.org/article/10.1088/2057-1976/2/5/055010 Parameters ---------- image_geometry : ImageGeometry, default used if None A description of the area/volume to reconstruct acquisition_geometry : AcquisitionGeometry A description of the acquisition data direct_method : str, default 'interpolated' The method used by the forward projector, 'Siddon' for ray-voxel intersection, 'interpolated' for interpolated projection adjoint_weights : str, default 'matched' The weighting method used by the cone-beam backward projector, 'matched' for weights to approximately match the 'interpolated' forward projector, 'FDK' for FDK weights Example ------- >>> from cil.plugins.tigre import ProjectionOperator >>> PO = ProjectionOperator(image.geometry, data.geometry) >>> forward_projection = PO.direct(image) >>> backward_projection = PO.adjoint(data) """ if acquisition_geometry is None: raise TypeError("Please specify an acquisition_geometry to configure this operator") if image_geometry == None: image_geometry = acquisition_geometry.get_ImageGeometry() device = kwargs.get('device', 'gpu') if device != 'gpu': raise ValueError( "TIGRE projectors are GPU only. Got device = {}".format( device)) ImageDimension.check_order_for_engine('tigre', image_geometry) AcquisitionDimension.check_order_for_engine('tigre', acquisition_geometry) super(ProjectionOperator,self).__init__(domain_geometry=image_geometry,\ range_geometry=acquisition_geometry) if direct_method not in ['interpolated', 'Siddon']: raise ValueError( "direct_method expected 'interpolated' or 'Siddon' got {}". format(direct_method)) if adjoint_weights not in ['matched', 'FDK']: raise ValueError( "adjoint_weights expected 'matched' or 'FDK' got {}".format( adjoint_weights)) self.method = {'direct': direct_method, 'adjoint': adjoint_weights} #set up TIGRE geometry tigre_geom, tigre_angles = CIL2TIGREGeometry.getTIGREGeometry( image_geometry, acquisition_geometry) tigre_geom.check_geo(tigre_angles) tigre_geom.cast_to_single() self.tigre_geom = tigre_geom #set up TIGRE GPU targets (from 2.2) if has_gpu_sel: self.gpuids = GpuIds() def __call_Ax(self, data): if has_gpu_sel: return Ax(data, self.tigre_geom, self.tigre_geom.angles, self.method['direct'], self.tigre_geom.mode, self.gpuids) else: return Ax(data, self.tigre_geom, self.tigre_geom.angles, self.method['direct'], self.tigre_geom.mode) def direct(self, x, out=None): data = x.as_array() if self.tigre_geom.is2D: data_temp = np.expand_dims(data, axis=0) arr_out = self.__call_Ax(data_temp) arr_out = np.squeeze(arr_out, axis=1) else: arr_out = self.__call_Ax(data) #if single angle projection remove the dimension for CIL if arr_out.shape[0] == 1: arr_out = np.squeeze(arr_out, axis=0) if out is None: out = AcquisitionData(arr_out, deep_copy=False, geometry=self._range_geometry.copy(), suppress_warning=True) return out else: out.fill(arr_out) def __call_Atb(self, data): if has_gpu_sel: return Atb(data, self.tigre_geom, self.tigre_geom.angles, self.method['adjoint'], self.tigre_geom.mode, self.gpuids) else: return Atb(data, self.tigre_geom, self.tigre_geom.angles, self.method['adjoint'], self.tigre_geom.mode) def adjoint(self, x, out=None): data = x.as_array() #if single angle projection add the dimension in for TIGRE if x.dimension_labels[0] != AcquisitionDimension.ANGLE: data = np.expand_dims(data, axis=0) if self.tigre_geom.is2D: data = np.expand_dims(data, axis=1) arr_out = self.__call_Atb(data) arr_out = np.squeeze(arr_out, axis=0) else: arr_out = self.__call_Atb(data) if out is None: out = ImageData(arr_out, deep_copy=False, geometry=self._domain_geometry.copy(), suppress_warning=True) return out else: out.fill(arr_out) def domain_geometry(self): return self._domain_geometry def range_geometry(self): return self._range_geometry