# 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
from cil.processors import Slicer
import numpy as np
try:
from cil.processors.cilacc_binner import Binner_IPP
has_ipp = True
except:
has_ipp = False
# 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]
class Binner(Slicer):
"""This creates a Binner processor.
The processor will crop the data, and then average together 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 bin {'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.
accelerated : boolean, default=True
Uses the CIL accelerated backend if `True`, numpy if `False`.
Example
-------
>>> from cil.processors import Binner
>>> roi = {'horizontal':(10,-10,2),'vertical':(10,-10,2)}
>>> processor = Binner(roi)
>>> processor.set_input(data)
>>> data_binned = processor.get_output()
Example
-------
>>> from cil.processors import Binner
>>> roi = {'horizontal':(None,None,2),'vertical':(None,None,2)}
>>> processor = Binner(roi)
>>> processor.set_input(data.geometry)
>>> geometry_binned = 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, accelerated=True):
if accelerated and not has_ipp:
raise RuntimeError("Cannot run accelerated Binner without the IPP libraries.")
super(Binner,self).__init__(roi = roi)
self._accelerated = accelerated
def _configure(self):
"""
Once the ROI has been parsed this configures the input specifically for use with Binner
"""
#as binning we only include bins that are inside boundaries
self._shape_out_full = [int((x.stop - x.start)//x.step) for x in self._roi_ordered]
self._pixel_indices = []
# fix roi_ordered for binner based on shape out
for i in range(4):
start = self._roi_ordered[i].start
stop = self._roi_ordered[i].start + self._shape_out_full[i] * self._roi_ordered[i].step
self._roi_ordered[i] = range(
start,
stop,
self._roi_ordered[i].step
)
self._pixel_indices.append((start, stop-1))
def _get_slice_position(self, roi):
"""
Return the vertical position to extract a single slice for binned geometry
"""
return roi.start + roi.step/2
def _get_angles(self, roi):
"""
Returns the binned angles according to the roi
"""
n_elements = len(roi)
shape = (n_elements, roi.step)
return self._geometry.angles[roi.start:roi.start+n_elements*roi.step].reshape(shape).mean(1)
def _bin_array_numpy(self, array_in, array_binned):
"""
Bins the array using numpy. This method is slower and less memory efficient than self._bin_array_acc
"""
shape_object = []
slice_object = []
for i in range(4):
# reshape the data to add each 'bin' dimensions
shape_object.append(self._shape_out_full[i])
shape_object.append(self._roi_ordered[i].step)
shape_object = tuple(shape_object)
slice_object = tuple([slice(x.start, x.stop) for x in self._roi_ordered])
data_resized = array_in.reshape(self._shape_in)[slice_object].reshape(shape_object)
mean_order = (-1, 1, 2, 3)
for i in range(4):
data_resized = data_resized.mean(mean_order[i])
np.copyto(array_binned, data_resized)
def _bin_array_acc(self, array_in, array_binned):
"""
Bins the array using the accelerated CIL backend
"""
indices_start = [x.start for x in self._roi_ordered]
bins = [x.step for x in self._roi_ordered]
binner_ipp = Binner_IPP(self._shape_in, self._shape_out_full, indices_start, bins)
res = binner_ipp.bin(array_in, array_binned)
if res != 0:
raise RuntimeError("Call failed")
def _process_data(self, dc_in, dc_out):
"""
Bin the data array
"""
if self._accelerated:
self._bin_array_acc(dc_in.array, dc_out.array)
else:
self._bin_array_numpy(dc_in.array, dc_out.array)