# 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.txtfromcil.frameworkimport(DataProcessor,AcquisitionData,ImageData,DataContainer,ImageGeometry,VectorGeometry,AcquisitionGeometry)importnumpyasnpimportweakrefimportlogginglog=logging.getLogger(__name__)# Note to developers: Binner and Slicer share a lot of common code# so Binner has been implemented as a child of Slicer. This makes use# of commonality and redefines only the methods that differ. These methods# dictate the style of slicer
[docs]classSlicer(DataProcessor):"""This creates a Slicer processor. The processor will crop the data, and then return every n input pixels along a dimension from the starting index. The output will be a data container with the data, and geometry updated to reflect the operation. Parameters ---------- roi : dict The region-of-interest to slice {'axis_name1':(start,stop,step), 'axis_name2':(start,stop,step)} The `key` being the axis name to apply the processor to, the `value` holding a tuple containing the ROI description Start: Starting index of input data. Must be an integer, or `None` defaults to index 0. Stop: Stopping index of input data. Must be an integer, or `None` defaults to index N. Step: Number of pixels to average together. Must be an integer or `None` defaults to 1. Example ------- >>> from cil.processors import Slicer >>> roi = {'horizontal':(10,-10,2),'vertical':(10,-10,2)} >>> processor = Slicer(roi) >>> processor.set_input(data) >>> data_sliced= processor.get_output() Example ------- >>> from cil.processors import Slicer >>> roi = {'horizontal':(None,None,2),'vertical':(None,None,2)} >>> processor = Slicer(roi) >>> processor.set_input(data.geometry) >>> geometry_sliced = processor.get_output() Note ---- The indices provided are start inclusive, stop exclusive. All elements along a dimension will be included if the axis does not appear in the roi dictionary, or if passed as {'axis_name',-1} If only one number is provided, then it is interpreted as Stop. i.e. {'axis_name1':(stop)} If two numbers are provided, then they are interpreted as Start and Stop i.e. {'axis_name1':(start, stop)} Negative indexing can be used to specify the index. i.e. {'axis_name1':(10, -10)} will crop the dimension symmetrically If Stop - Start is not multiple of Step, then the resulted dimension will have (Stop - Start) // Step elements, i.e. (Stop - Start) % Step elements will be ignored """def__init__(self,roi=None):kwargs={'_roi_input':roi,'_roi_ordered':None,'_data_array':False,'_geometry':None,'_processed_dims':None,'_shape_in':None,'_shape_out_full':None,'_shape_out':None,'_labels_out':None,'_labels_in':None,'_pixel_indices':None,'_accelerated':True}super(Slicer,self).__init__(**kwargs)
[docs]defset_input(self,dataset):""" Set the input data or geometry to the processor Parameters ---------- dataset : DataContainer, Geometry The input DataContainer or Geometry """ifissubclass(type(dataset),DataContainer)orisinstance(dataset,(AcquisitionGeometry,ImageGeometry)):ifself.check_input(dataset):self.__dict__['input']=weakref.ref(dataset)self.__dict__['shouldRun']=Trueelse:raiseValueError('Input data not compatible')else:raiseTypeError("Input type mismatch: got {0} expecting {1}"\
.format(type(dataset),DataContainer))self._set_up()
defcheck_input(self,data):ifisinstance(data,(ImageData,AcquisitionData)):self._data_array=Trueself._geometry=data.geometryelifisinstance(data,DataContainer):self._data_array=Trueself._geometry=Noneelifisinstance(data,(ImageGeometry,AcquisitionGeometry)):self._data_array=Falseself._geometry=dataelse:raiseTypeError('Processor supports following data types:\n'+' - ImageData\n - AcquisitionData\n - DataContainer\n - ImageGeometry\n - AcquisitionGeometry')ifself._data_array:ifdata.dtype!=np.float32:raiseTypeError("Expected float32")if(self._roi_input==None):raiseValueError('Please, specify roi')forkeyinself._roi_input.keys():ifkeynotindata.dimension_labels:raiseValueError('Wrong label is specified for roi, expected one of {}.'.format(data.dimension_labels))returnTruedef_set_up(self):""" This parses the input roi generically and then configures the processor according to its class. """#read inputdata=self.get_input()self._parse_roi(data.ndim,data.shape,data.dimension_labels)#processor specific configurationsself._configure()# set boolean of dimensions to processself._processed_dims=[0ifself._shape_out_full[i]==self._shape_in[i]else1foriinrange(4)]self._shape_out=tuple([iforiinself._shape_out_fullifi>1])self._labels_out=[self._labels_in[i]fori,xinenumerate(self._shape_out_full)ifx>1]def_parse_roi(self,ndim,shape,dimension_labels):''' Process the input roi '''offset=4-ndimlabels_in=[None]*4labels_in[offset::]=dimension_labelsshape_in=[1]*4shape_in[offset::]=shape# defaultsrange_list=[range(0,x,1)forxinshape_in]foriinrange(ndim):roi=self._roi_input.get(dimension_labels[i],None)ifroi==Noneorroi==-1:continuestart=range_list[offset+i].startstop=range_list[offset+i].stopstep=range_list[offset+i].step# accepts a tuple, range or slicetry:roi=[roi.start,roi.stop,roi.step]exceptAttributeError:roi=list(roi)length=len(roi)iflength==1:ifroi[0]isnotNone:stop=roi[0]eliflength>1:ifroi[0]isnotNone:start=roi[0]ifroi[1]isnotNone:stop=roi[1]iflength>2:ifroi[2]isnotNone:step=roi[2]# deal with negative indexingifstart<0:start+=shape_in[offset+i]ifstop<=0:stop+=shape_in[offset+i]ifstop>shape_in[offset+i]:log.warning(f"ROI for axis {dimension_labels[i]} has 'stop' out of bounds. Using axis length as stop value."f" Got stop index: {stop}, using {shape_in[offset+i]}")stop=shape_in[offset+i]ifstart>shape_in[offset+i]:raiseValueError(f"ROI for axis {dimension_labels[i]} has 'start' out of bounds."f" Got start index: {start} for axis length {shape_in[offset+i]}")ifstart>=stop:raiseValueError(f"ROI for axis {dimension_labels[i]} has 'start' out of bounds."f" Got start index: {start}, stop index {stop}")# set valuesrange_list[offset+i]=range(int(start),int(stop),int(step))# set valuesself._shape_in=shape_inself._labels_in=labels_inself._roi_ordered=range_listdef_configure(self):""" Once the ROI has been parsed this configure the input specifically for use with Slicer """self._shape_out_full=[len(x)forxinself._roi_ordered]self._pixel_indices=[(x[0],x[-1])forxinself._roi_ordered]def_get_slice_position(self,roi):""" Return the vertical position to extract a single slice for sliced geometry """returnroi.startdef_get_angles(self,roi):""" Returns the sliced angles according to the roi """returnself._geometry.angles[roi.start:roi.stop:roi.step]def_process_acquisition_geometry(self):""" Creates the new acquisition geometry """geometry_new=self._geometry.copy()processed_dims=self._processed_dims.copy()# deal with vertical first as it may change the geometry typeif'vertical'inself._geometry.dimension_labels:vert_ind=self._labels_in.index('vertical')ifprocessed_dims[vert_ind]:roi=self._roi_ordered[vert_ind]n_elements=len(roi)ifn_elements>1:# difference in end indices, minus differences in start indices, divided by 2pixel_offset=((self._shape_in[vert_ind]-1-self._pixel_indices[vert_ind][1])-self._pixel_indices[vert_ind][0])*0.5geometry_new.config.shift_detector_in_plane(pixel_offset,'vertical')geometry_new.config.panel.num_pixels[1]=n_elementselse:try:position=self._get_slice_position(roi)geometry_new=geometry_new.get_slice(vertical=position)exceptValueError:log.warning("Unable to calculate the requested 2D geometry. Returning geometry=`None`")returnNonegeometry_new.config.panel.pixel_size[1]*=roi.stepprocessed_dims[vert_ind]=Falsefori,axisinenumerate(self._labels_in):ifnotprocessed_dims[i]:continueroi=self._roi_ordered[i]n_elements=len(roi)ifaxis=='channel':geometry_new.set_channels(num_channels=n_elements)elifaxis=='angle':geometry_new.config.angles.angle_data=self._get_angles(roi)elifaxis=='horizontal':pixel_offset=((self._shape_in[i]-1-self._pixel_indices[i][1])-self._pixel_indices[i][0])*0.5geometry_new.config.shift_detector_in_plane(pixel_offset,axis)geometry_new.config.panel.num_pixels[0]=n_elementsgeometry_new.config.panel.pixel_size[0]*=roi.stepreturngeometry_newdef_process_image_geometry(self):""" Creates the new image geometry """iflen(self._shape_out)==0:returnNoneeliflen(self._shape_out)==1:returnVectorGeometry(self._shape_out[0],dimension_labels=self._labels_out[0])geometry_new=self._geometry.copy()fori,axisinenumerate(self._labels_in):ifnotself._processed_dims[i]:continueroi=self._roi_ordered[i]n_elements=len(roi)voxel_offset=(self._shape_in[i]-1-self._pixel_indices[i][1]-self._pixel_indices[i][0])*0.5ifaxis=='channel':geometry_new.channels=n_elementsgeometry_new.channel_spacing*=roi.stepelifaxis=='vertical':geometry_new.center_z-=voxel_offset*geometry_new.voxel_size_zgeometry_new.voxel_num_z=n_elementsgeometry_new.voxel_size_z*=roi.stepelifaxis=='horizontal_x':geometry_new.center_x-=voxel_offset*geometry_new.voxel_size_xgeometry_new.voxel_num_x=n_elementsgeometry_new.voxel_size_x*=roi.stepelifaxis=='horizontal_y':geometry_new.center_y-=voxel_offset*geometry_new.voxel_size_ygeometry_new.voxel_num_y=n_elementsgeometry_new.voxel_size_y*=roi.stepreturngeometry_newdef_process_data(self,dc_in,dc_out):""" Slice the data array """slice_obj=tuple([slice(x.start,x.stop,x.step)forxinself._roi_ordered])arr_in=dc_in.array.reshape(self._shape_in)dc_out.fill(np.squeeze(arr_in[slice_obj]))
[docs]defprocess(self,out=None):""" Processes the input data Parameters ---------- out : ImageData, AcquisitionData, DataContainer, optional Fills the referenced DataContainer with the processed output and suppresses the return Returns ------- DataContainer The downsampled output is returned. Depending on the input type this may be: ImageData, AcquisitionData, DataContainer, ImageGeometry, AcquisitionGeometry """data=self.get_input()ifisinstance(self._geometry,ImageGeometry):new_geometry=self._process_image_geometry()elifisinstance(self._geometry,AcquisitionGeometry):new_geometry=self._process_acquisition_geometry()else:new_geometry=None# return if just acting on geometryifnotself._data_array:returnnew_geometry# create output array or check size and shape of passed outifoutisNone:ifnew_geometryisnotNone:data_out=new_geometry.allocate(None)else:processed_array=np.empty(self._shape_out,dtype=np.float32)data_out=DataContainer(processed_array,False,self._labels_out)else:try:out.array=np.asarray(out.array,dtype=np.float32,order='C').reshape(self._shape_out)except:raiseValueError("Array of `out` not compatible. Expected shape: {0}, data type: {1} Got shape: {2}, data type: {3}".format(self._shape_out,np.float32,out.array.shape,out.array.dtype))ifnew_geometryisnotNone:ifout.geometry!=new_geometry:raiseValueError("Geometry of `out` not as expected. Got {0}, expected {1}".format(out.geometry,new_geometry))data_out=outself._process_data(data,data_out)returndata_out