Source code for cil.processors.Normaliser
# Copyright 2019 United Kingdom Research and Innovation
# Copyright 2019 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.framework import Processor, DataContainer, AcquisitionData,\
AcquisitionGeometry, ImageGeometry, ImageData
import numpy
import warnings
[docs]
class Normaliser(Processor):
r""" This processor can be used to normalise data with flat-field and dark-field images.
The dark-field image is used to remove the offset from the input data, and the flat-field image is used to scale the data.
The normalisation is done as follows:
.. math::
\text{output} = \frac{\text{input} - \text{offset}}{\text{scale} - \text{offset}}
where :math:`\text{input}`, :math:`\text{offset}` and :math:`\text{scale}` are the input, dark-field and flat-field images respectively.
The processor can be used in-place to reduce memory usage.
Parameters
----------
flat_field : numpy.ndarray, optional
The flat field image. It must have the same shape as a single projection.
dark_field : numpy.ndarray, optional
The dark field image. It must have the same shape as a single projection.
tolerance : float, optional
The value to substitute for output values where division by zero occurs.
Examples
--------
Basic usage with flat and dark fields:
>>> from cil.processors import Normaliser
>>> normaliser = Normaliser(flat_field, dark_field)
>>> normaliser.set_input(data)
>>> normalised_data = normaliser.get_output()
In-place usage:
>>> from cil.processors import Normaliser
>>> normaliser = Normaliser(flat_field, dark_field)
>>> normaliser.set_input(data)
>>> normaliser.get_output(out=data)
"""
def __init__(self, flat_field = None, dark_field = None, tolerance = 1e-5):
kwargs = {
'flat_field' : flat_field,
'dark_field' : dark_field,
# very small number. Used when there is a division by zero
'tolerance' : tolerance
}
#DataProcessor.__init__(self, **kwargs)
super(Normaliser, self).__init__(**kwargs)
if not flat_field is None:
self.set_flat_field(flat_field)
if not dark_field is None:
self.set_dark_field(dark_field)
def check_input(self, dataset):
if dataset.number_of_dimensions == 3 or\
dataset.number_of_dimensions == 2:
return True
else:
raise ValueError("Expected input dimensions is 2 or 3, got {0}"\
.format(dataset.number_of_dimensions))
def set_dark_field(self, df):
if type(df) is numpy.ndarray:
if len(numpy.shape(df)) == 3:
raise ValueError('Dark Field should be 2D')
elif len(numpy.shape(df)) == 2:
self.dark_field = df
elif issubclass(type(df), DataContainer):
self.dark_field = df.as_array()
def set_flat_field(self, df):
if type(df) is numpy.ndarray:
if len(numpy.shape(df)) == 3:
raise ValueError('Flat Field should be 2D')
elif len(numpy.shape(df)) == 2:
self.flat_field = df
elif issubclass(type(df), DataContainer):
self.flat_field = df.as_array()
[docs]
@staticmethod
def estimate_normalised_error(projection, flat, dark, delta_flat, delta_dark):
'''returns the estimated relative error of the normalised projection
n = (projection - dark) / (flat - dark)
Dn/n = (flat-dark + projection-dark)/((flat-dark)*(projection-dark))*(Df/f + Dd/d)
'''
a = (projection - dark)
b = (flat-dark)
df = delta_flat / flat
dd = delta_dark / dark
rel_norm_error = (b + a) / (b * a) * (df + dd)
return rel_norm_error
def process(self, out=None):
input_arr = self.get_input().array
if out is None:
out = self.get_input().geometry.allocate(None)
out_array = out.array
if self.flat_field is None and self.dark_field is None:
raise ValueError('No flat or dark field provided. Please provide at least one.')
if self.dark_field is None:
offset = 0
else:
if input_arr.shape[-len(self.dark_field.shape)::]!=self.dark_field.shape:
raise ValueError('Dark field and projections size do not match.')
offset = self.dark_field
zero_err = False
if self.flat_field is not None:
if input_arr.shape[-len(self.flat_field.shape)::]!=self.flat_field.shape:
raise ValueError('Flat field and projections size do not match.')
scale = self.flat_field - offset
numpy.seterr(divide='warn')
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
denom_inv = 1.0 / scale
# Check if any warnings were raised and handle these later
if len(w) > 0 and issubclass(w[-1].category, RuntimeWarning):
zero_err = True
if self.dark_field is not None:
numpy.subtract(input_arr, offset, out=out_array)
input_arr = out_array
if self.flat_field is not None:
numpy.multiply(input_arr, denom_inv, out=out_array)
if zero_err:
warnings.warn("Divide by zero detected. These values will be substituted by the set tolerance. Please check your data for bad pixels.", UserWarning, stacklevel=2)
# create map of zeros in scale array
zero_map = numpy.where(scale == 0)
# set these to the tolerance value in all projections
for i in range(out_array.shape[0]):
out_array[i][zero_map] = self.tolerance
return out