diff options
author | Edoardo Pasca <edo.paskino@gmail.com> | 2019-06-14 13:45:59 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-06-14 13:45:59 +0100 |
commit | 8ef0e4372624b4a19a7ad69626949760b938eb99 (patch) | |
tree | 2cf6e46c707e05ee1280eea092dd03e1f8ffe8e6 /Wrappers | |
parent | d108cd7e9652992001a116347941eda7e75b3301 (diff) | |
parent | 33140f96f2482701f044f1de65f9dfcc48a1a7f5 (diff) | |
download | framework-8ef0e4372624b4a19a7ad69626949760b938eb99.tar.gz framework-8ef0e4372624b4a19a7ad69626949760b938eb99.tar.bz2 framework-8ef0e4372624b4a19a7ad69626949760b938eb99.tar.xz framework-8ef0e4372624b4a19a7ad69626949760b938eb99.zip |
Merge pull request #207 from vais-ral/composite_operator_datacontainer
Block operator and datacontainer
Diffstat (limited to 'Wrappers')
129 files changed, 17873 insertions, 2064 deletions
diff --git a/Wrappers/Python/ccpi/contrib/__init__.py b/Wrappers/Python/ccpi/contrib/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/Wrappers/Python/ccpi/contrib/__init__.py diff --git a/Wrappers/Python/ccpi/contrib/optimisation/__init__.py b/Wrappers/Python/ccpi/contrib/optimisation/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/Wrappers/Python/ccpi/contrib/optimisation/__init__.py diff --git a/Wrappers/Python/ccpi/contrib/optimisation/algorithms/__init__.py b/Wrappers/Python/ccpi/contrib/optimisation/algorithms/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/Wrappers/Python/ccpi/contrib/optimisation/algorithms/__init__.py diff --git a/Wrappers/Python/ccpi/optimisation/spdhg.py b/Wrappers/Python/ccpi/contrib/optimisation/algorithms/spdhg.py index 263a7cd..263a7cd 100755 --- a/Wrappers/Python/ccpi/optimisation/spdhg.py +++ b/Wrappers/Python/ccpi/contrib/optimisation/algorithms/spdhg.py diff --git a/Wrappers/Python/ccpi/framework/BlockDataContainer.py b/Wrappers/Python/ccpi/framework/BlockDataContainer.py new file mode 100755 index 0000000..670e214 --- /dev/null +++ b/Wrappers/Python/ccpi/framework/BlockDataContainer.py @@ -0,0 +1,496 @@ + # -*- coding: utf-8 -*- +""" +Created on Tue Mar 5 16:04:45 2019 + +@author: ofn77899 +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import numpy +from numbers import Number +import functools +from ccpi.framework import DataContainer +#from ccpi.framework import AcquisitionData, ImageData +#from ccpi.optimisation.operators import Operator, LinearOperator + +class BlockDataContainer(object): + '''Class to hold DataContainers as column vector + + Provides basic algebra between BlockDataContainer's, DataContainer's and + subclasses and Numbers + + 1) algebra between `BlockDataContainer`s will be element-wise, only if + the shape of the 2 `BlockDataContainer`s is the same, otherwise it + will fail + 2) algebra between `BlockDataContainer`s and `list` or `numpy array` will + work as long as the number of `rows` and element of the arrays match, + indipendently on the fact that the `BlockDataContainer` could be nested + 3) algebra between `BlockDataContainer` and one `DataContainer` is possible. + It will require that all the `DataContainers` in the block to be + compatible with the `DataContainer` we want to algebra with. Should we + require that the `DataContainer` is the same type? Like `ImageData` or `AcquisitionData`? + 4) algebra between `BlockDataContainer` and a `Number` is possible and it + will be done with each element of the `BlockDataContainer` even if nested + + A = [ [B,C] , D] + A * 3 = [ 3 * [B,C] , 3* D] = [ [ 3*B, 3*C] , 3*D ] + + ''' + ADD = 'add' + SUBTRACT = 'subtract' + MULTIPLY = 'multiply' + DIVIDE = 'divide' + POWER = 'power' + __array_priority__ = 1 + __container_priority__ = 2 + def __init__(self, *args, **kwargs): + '''''' + self.containers = args + self.index = 0 + shape = kwargs.get('shape', None) + if shape is None: + shape = (len(args),1) +# shape = (len(args),1) + self.shape = shape + + n_elements = functools.reduce(lambda x,y: x*y, shape, 1) + if len(args) != n_elements: + raise ValueError( + 'Dimension and size do not match: expected {} got {}' + .format(n_elements, len(args))) + + + def __iter__(self): + '''BlockDataContainer is Iterable''' + return self + def next(self): + '''python2 backwards compatibility''' + return self.__next__() + def __next__(self): + try: + out = self[self.index] + except IndexError as ie: + raise StopIteration() + self.index+=1 + return out + + def is_compatible(self, other): + '''basic check if the size of the 2 objects fit''' + + if isinstance(other, Number): + return True + elif isinstance(other, (list, numpy.ndarray)) : + for ot in other: + if not isinstance(ot, (Number,\ + numpy.int, numpy.int8, numpy.int16, numpy.int32, numpy.int64,\ + numpy.float, numpy.float16, numpy.float32, numpy.float64, \ + numpy.complex)): + raise ValueError('List/ numpy array can only contain numbers {}'\ + .format(type(ot))) + return len(self.containers) == len(other) + elif issubclass(other.__class__, DataContainer): + ret = True + for i, el in enumerate(self.containers): + if isinstance(el, BlockDataContainer): + a = el.is_compatible(other) + else: + a = el.shape == other.shape + ret = ret and a + return ret + #return self.get_item(0).shape == other.shape + return len(self.containers) == len(other.containers) + + def get_item(self, row): + if row > self.shape[0]: + raise ValueError('Requested row {} > max {}'.format(row, self.shape[0])) + return self.containers[row] + + def __getitem__(self, row): + return self.get_item(row) + + def add(self, other, *args, **kwargs): + '''Algebra: add method of BlockDataContainer with number/DataContainer or BlockDataContainer + + :param: other (number, DataContainer or subclasses or BlockDataContainer + :param: out (optional): provides a placehold for the resul. + ''' + out = kwargs.get('out', None) + if out is not None: + self.binary_operations(BlockDataContainer.ADD, other, *args, **kwargs) + else: + return self.binary_operations(BlockDataContainer.ADD, other, *args, **kwargs) + def subtract(self, other, *args, **kwargs): + '''Algebra: subtract method of BlockDataContainer with number/DataContainer or BlockDataContainer + + :param: other (number, DataContainer or subclasses or BlockDataContainer + :param: out (optional): provides a placehold for the resul. + ''' + out = kwargs.get('out', None) + if out is not None: + self.binary_operations(BlockDataContainer.SUBTRACT, other, *args, **kwargs) + else: + return self.binary_operations(BlockDataContainer.SUBTRACT, other, *args, **kwargs) + def multiply(self, other, *args, **kwargs): + '''Algebra: multiply method of BlockDataContainer with number/DataContainer or BlockDataContainer + + :param: other (number, DataContainer or subclasses or BlockDataContainer + :param: out (optional): provides a placehold for the resul. + ''' + out = kwargs.get('out', None) + if out is not None: + self.binary_operations(BlockDataContainer.MULTIPLY, other, *args, **kwargs) + else: + return self.binary_operations(BlockDataContainer.MULTIPLY, other, *args, **kwargs) + def divide(self, other, *args, **kwargs): + '''Algebra: divide method of BlockDataContainer with number/DataContainer or BlockDataContainer + + :param: other (number, DataContainer or subclasses or BlockDataContainer + :param: out (optional): provides a placehold for the resul. + ''' + out = kwargs.get('out', None) + if out is not None: + self.binary_operations(BlockDataContainer.DIVIDE, other, *args, **kwargs) + else: + return self.binary_operations(BlockDataContainer.DIVIDE, other, *args, **kwargs) + + + def binary_operations(self, operation, other, *args, **kwargs): + '''Algebra: generic method of algebric operation with BlockDataContainer with number/DataContainer or BlockDataContainer + + Provides commutativity with DataContainer and subclasses, i.e. this + class's reverse algebric methods take precedence w.r.t. direct algebric + methods of DataContainer and subclasses. + + This method is not to be used directly + ''' + if not self.is_compatible(other): + raise ValueError('Incompatible for divide') + out = kwargs.get('out', None) + if isinstance(other, Number) or issubclass(other.__class__, DataContainer): + # try to do algebra with one DataContainer. Will raise error if not compatible + kw = kwargs.copy() + res = [] + for i,el in enumerate(self.containers): + if operation == BlockDataContainer.ADD: + op = el.add + elif operation == BlockDataContainer.SUBTRACT: + op = el.subtract + elif operation == BlockDataContainer.MULTIPLY: + op = el.multiply + elif operation == BlockDataContainer.DIVIDE: + op = el.divide + elif operation == BlockDataContainer.POWER: + op = el.power + else: + raise ValueError('Unsupported operation', operation) + if out is not None: + kw['out'] = out.get_item(i) + op(other, *args, **kw) + else: + res.append(op(other, *args, **kw)) + if out is not None: + return + else: + return type(self)(*res, shape=self.shape) + elif isinstance(other, (list, numpy.ndarray, BlockDataContainer)): + # try to do algebra with one DataContainer. Will raise error if not compatible + kw = kwargs.copy() + res = [] + if isinstance(other, BlockDataContainer): + the_other = other.containers + else: + the_other = other + for i,zel in enumerate(zip ( self.containers, the_other) ): + el = zel[0] + ot = zel[1] + if operation == BlockDataContainer.ADD: + op = el.add + elif operation == BlockDataContainer.SUBTRACT: + op = el.subtract + elif operation == BlockDataContainer.MULTIPLY: + op = el.multiply + elif operation == BlockDataContainer.DIVIDE: + op = el.divide + elif operation == BlockDataContainer.POWER: + op = el.power + else: + raise ValueError('Unsupported operation', operation) + if out is not None: + kw['out'] = out.get_item(i) + op(ot, *args, **kw) + else: + res.append(op(ot, *args, **kw)) + if out is not None: + return + else: + return type(self)(*res, shape=self.shape) + return type(self)(*[ operation(ot, *args, **kwargs) for el,ot in zip(self.containers,other)], shape=self.shape) + else: + raise ValueError('Incompatible type {}'.format(type(other))) + + + def power(self, other, *args, **kwargs): + if not self.is_compatible(other): + raise ValueError('Incompatible for power') + out = kwargs.get('out', None) + if isinstance(other, Number): + return type(self)(*[ el.power(other, *args, **kwargs) for el in self.containers], shape=self.shape) + elif isinstance(other, list) or isinstance(other, numpy.ndarray): + return type(self)(*[ el.power(ot, *args, **kwargs) for el,ot in zip(self.containers,other)], shape=self.shape) + return type(self)(*[ el.power(ot, *args, **kwargs) for el,ot in zip(self.containers,other.containers)], shape=self.shape) + + def maximum(self,other, *args, **kwargs): + if not self.is_compatible(other): + raise ValueError('Incompatible for maximum') + out = kwargs.get('out', None) + if isinstance(other, Number): + return type(self)(*[ el.maximum(other, *args, **kwargs) for el in self.containers], shape=self.shape) + elif isinstance(other, list) or isinstance(other, numpy.ndarray): + return type(self)(*[ el.maximum(ot, *args, **kwargs) for el,ot in zip(self.containers,other)], shape=self.shape) + return type(self)(*[ el.maximum(ot, *args, **kwargs) for el,ot in zip(self.containers,other.containers)], shape=self.shape) + + + def minimum(self,other, *args, **kwargs): + if not self.is_compatible(other): + raise ValueError('Incompatible for maximum') + out = kwargs.get('out', None) + if isinstance(other, Number): + return type(self)(*[ el.minimum(other, *args, **kwargs) for el in self.containers], shape=self.shape) + elif isinstance(other, list) or isinstance(other, numpy.ndarray): + return type(self)(*[ el.minimum(ot, *args, **kwargs) for el,ot in zip(self.containers,other)], shape=self.shape) + return type(self)(*[ el.minimum(ot, *args, **kwargs) for el,ot in zip(self.containers,other.containers)], shape=self.shape) + + + ## unary operations + def abs(self, *args, **kwargs): + return type(self)(*[ el.abs(*args, **kwargs) for el in self.containers], shape=self.shape) + def sign(self, *args, **kwargs): + return type(self)(*[ el.sign(*args, **kwargs) for el in self.containers], shape=self.shape) + def sqrt(self, *args, **kwargs): + return type(self)(*[ el.sqrt(*args, **kwargs) for el in self.containers], shape=self.shape) + def conjugate(self, out=None): + return type(self)(*[el.conjugate() for el in self.containers], shape=self.shape) + + ## reductions + + def sum(self, *args, **kwargs): + return numpy.sum([ el.sum(*args, **kwargs) for el in self.containers]) + + def squared_norm(self): + y = numpy.asarray([el.squared_norm() for el in self.containers]) + return y.sum() + + + def norm(self): + return numpy.sqrt(self.squared_norm()) + + def pnorm(self, p=2): + + if p==1: + return sum(self.abs()) + elif p==2: + return sum([el*el for el in self.containers]).sqrt() + else: + return ValueError('Not implemented') + + def copy(self): + '''alias of clone''' + return self.clone() + def clone(self): + return type(self)(*[el.copy() for el in self.containers], shape=self.shape) + def fill(self, other): + if isinstance (other, BlockDataContainer): + if not self.is_compatible(other): + raise ValueError('Incompatible containers') + for el,ot in zip(self.containers, other.containers): + el.fill(ot) + else: + return ValueError('Cannot fill with object provided {}'.format(type(other))) + + def __add__(self, other): + return self.add( other ) + # __radd__ + + def __sub__(self, other): + return self.subtract( other ) + # __rsub__ + + def __mul__(self, other): + return self.multiply(other) + # __rmul__ + + def __div__(self, other): + return self.divide(other) + # __rdiv__ + def __truediv__(self, other): + return self.divide(other) + + def __pow__(self, other): + return self.power(other) + # reverse operand + def __radd__(self, other): + '''Reverse addition + + to make sure that this method is called rather than the __mul__ of a numpy array + the class constant __array_priority__ must be set > 0 + https://docs.scipy.org/doc/numpy-1.15.1/reference/arrays.classes.html#numpy.class.__array_priority__ + ''' + return self + other + # __radd__ + + def __rsub__(self, other): + '''Reverse subtraction + + to make sure that this method is called rather than the __mul__ of a numpy array + the class constant __array_priority__ must be set > 0 + https://docs.scipy.org/doc/numpy-1.15.1/reference/arrays.classes.html#numpy.class.__array_priority__ + ''' + return (-1 * self) + other + # __rsub__ + + def __rmul__(self, other): + '''Reverse multiplication + + to make sure that this method is called rather than the __mul__ of a numpy array + the class constant __array_priority__ must be set > 0 + https://docs.scipy.org/doc/numpy-1.15.1/reference/arrays.classes.html#numpy.class.__array_priority__ + ''' + return self * other + # __rmul__ + + def __rdiv__(self, other): + '''Reverse division + + to make sure that this method is called rather than the __mul__ of a numpy array + the class constant __array_priority__ must be set > 0 + https://docs.scipy.org/doc/numpy-1.15.1/reference/arrays.classes.html#numpy.class.__array_priority__ + ''' + return pow(self / other, -1) + # __rdiv__ + def __rtruediv__(self, other): + '''Reverse truedivision + + to make sure that this method is called rather than the __mul__ of a numpy array + the class constant __array_priority__ must be set > 0 + https://docs.scipy.org/doc/numpy-1.15.1/reference/arrays.classes.html#numpy.class.__array_priority__ + ''' + return self.__rdiv__(other) + + def __rpow__(self, other): + '''Reverse power + + to make sure that this method is called rather than the __mul__ of a numpy array + the class constant __array_priority__ must be set > 0 + https://docs.scipy.org/doc/numpy-1.15.1/reference/arrays.classes.html#numpy.class.__array_priority__ + ''' + return other.power(self) + + def __iadd__(self, other): + '''Inline addition''' + if isinstance (other, BlockDataContainer): + for el,ot in zip(self.containers, other.containers): + el += ot + elif isinstance(other, Number): + for el in self.containers: + el += other + elif isinstance(other, list) or isinstance(other, numpy.ndarray): + if not self.is_compatible(other): + raise ValueError('Incompatible for __iadd__') + for el,ot in zip(self.containers, other): + el += ot + return self + # __iadd__ + + def __isub__(self, other): + '''Inline subtraction''' + if isinstance (other, BlockDataContainer): + for el,ot in zip(self.containers, other.containers): + el -= ot + elif isinstance(other, Number): + for el in self.containers: + el -= other + elif isinstance(other, list) or isinstance(other, numpy.ndarray): + if not self.is_compatible(other): + raise ValueError('Incompatible for __isub__') + for el,ot in zip(self.containers, other): + el -= ot + return self + # __isub__ + + def __imul__(self, other): + '''Inline multiplication''' + if isinstance (other, BlockDataContainer): + for el,ot in zip(self.containers, other.containers): + el *= ot + elif isinstance(other, Number): + for el in self.containers: + el *= other + elif isinstance(other, list) or isinstance(other, numpy.ndarray): + if not self.is_compatible(other): + raise ValueError('Incompatible for __imul__') + for el,ot in zip(self.containers, other): + el *= ot + return self + # __imul__ + + def __idiv__(self, other): + '''Inline division''' + if isinstance (other, BlockDataContainer): + for el,ot in zip(self.containers, other.containers): + el /= ot + elif isinstance(other, Number): + for el in self.containers: + el /= other + elif isinstance(other, list) or isinstance(other, numpy.ndarray): + if not self.is_compatible(other): + raise ValueError('Incompatible for __idiv__') + for el,ot in zip(self.containers, other): + el /= ot + return self + # __rdiv__ + def __itruediv__(self, other): + '''Inline truedivision''' + return self.__idiv__(other) + + def dot(self, other): +# + tmp = [ self.containers[i].dot(other.containers[i]) for i in range(self.shape[0])] + return sum(tmp) + + + +if __name__ == '__main__': + + from ccpi.framework import ImageGeometry, BlockGeometry + import numpy + + N, M = 2, 3 + ig = ImageGeometry(N, M) + BG = BlockGeometry(ig, ig) + + U = BG.allocate('random_int') + V = BG.allocate('random_int') + + + print ("test sum BDC " ) + w = U[0].as_array() + U[1].as_array() + w1 = sum(U).as_array() + numpy.testing.assert_array_equal(w, w1) + + print ("test sum BDC " ) + z = numpy.sqrt(U[0].as_array()**2 + U[1].as_array()**2) + z1 = sum(U**2).sqrt().as_array() + numpy.testing.assert_array_equal(z, z1) + + z2 = U.pnorm(2) + + zzz = U.dot(V) + + + + + + diff --git a/Wrappers/Python/ccpi/framework/BlockGeometry.py b/Wrappers/Python/ccpi/framework/BlockGeometry.py new file mode 100755 index 0000000..e58035b --- /dev/null +++ b/Wrappers/Python/ccpi/framework/BlockGeometry.py @@ -0,0 +1,86 @@ +from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import numpy
+from numbers import Number
+import functools
+from ccpi.framework import BlockDataContainer
+#from ccpi.optimisation.operators import Operator, LinearOperator
+
+class BlockGeometry(object):
+
+ RANDOM = 'random'
+ RANDOM_INT = 'random_int'
+
+
+
+ '''Class to hold Geometry as column vector'''
+ #__array_priority__ = 1
+ def __init__(self, *args, **kwargs):
+ ''''''
+ self.geometries = args
+ self.index = 0
+
+ shape = (len(args),1)
+ self.shape = shape
+
+ n_elements = functools.reduce(lambda x,y: x*y, shape, 1)
+ if len(args) != n_elements:
+ raise ValueError(
+ 'Dimension and size do not match: expected {} got {}'
+ .format(n_elements, len(args)))
+
+
+ def get_item(self, index):
+ '''returns the Geometry in the BlockGeometry located at position index'''
+ return self.geometries[index]
+
+ def allocate(self, value=0, dimension_labels=None, **kwargs):
+
+ symmetry = kwargs.get('symmetry',False)
+ containers = [geom.allocate(value) for geom in self.geometries]
+
+ if symmetry == True:
+
+ # for 2x2
+ # [ ig11, ig12\
+ # ig21, ig22]
+
+ # Row-wise Order
+
+ if len(containers)==4:
+ containers[1]=containers[2]
+
+ # for 3x3
+ # [ ig11, ig12, ig13\
+ # ig21, ig22, ig23\
+ # ig31, ig32, ig33]
+
+ elif len(containers)==9:
+ containers[1]=containers[3]
+ containers[2]=containers[6]
+ containers[5]=containers[7]
+
+ # for 4x4
+ # [ ig11, ig12, ig13, ig14\
+ # ig21, ig22, ig23, ig24\
+ # ig31, ig32, ig33, ig34
+ # ig41, ig42, ig43, ig44]
+
+ elif len(containers) == 16:
+ containers[1]=containers[4]
+ containers[2]=containers[8]
+ containers[3]=containers[12]
+ containers[6]=containers[9]
+ containers[7]=containers[10]
+ containers[11]=containers[15]
+
+
+
+
+ return BlockDataContainer(*containers)
+
+
+
diff --git a/Wrappers/Python/ccpi/framework/TestData.py b/Wrappers/Python/ccpi/framework/TestData.py new file mode 100755 index 0000000..e7dc908 --- /dev/null +++ b/Wrappers/Python/ccpi/framework/TestData.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*-
+from ccpi.framework import ImageData, ImageGeometry
+import numpy
+from PIL import Image
+import os
+import os.path
+
+data_dir = os.path.abspath(os.path.join(
+ os.path.dirname(__file__),
+ '../data/')
+)
+
+class TestData(object):
+ BOAT = 'boat.tiff'
+ CAMERA = 'camera.png'
+ PEPPERS = 'peppers.tiff'
+ RESOLUTION_CHART = 'resolution_chart.tiff'
+ SIMPLE_PHANTOM_2D = 'hotdog'
+ SHAPES = 'shapes.png'
+
+ def __init__(self, **kwargs):
+ self.data_dir = kwargs.get('data_dir', data_dir)
+
+ def load(self, which, size=(512,512), scale=(0,1), **kwargs):
+ if which not in [TestData.BOAT, TestData.CAMERA,
+ TestData.PEPPERS, TestData.RESOLUTION_CHART,
+ TestData.SIMPLE_PHANTOM_2D, TestData.SHAPES]:
+ raise ValueError('Unknown TestData {}.'.format(which))
+ if which == TestData.SIMPLE_PHANTOM_2D:
+ N = size[0]
+ M = size[1]
+ sdata = numpy.zeros((N,M))
+ sdata[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+ sdata[round(M/8):round(7*M/8),round(3*M/8):round(5*M/8)] = 1
+ ig = ImageGeometry(voxel_num_x = N, voxel_num_y = M, dimension_labels=[ImageGeometry.HORIZONTAL_X, ImageGeometry.HORIZONTAL_Y])
+ data = ig.allocate()
+ data.fill(sdata)
+
+ elif which == TestData.SHAPES:
+
+ tmp = numpy.array(Image.open(os.path.join(self.data_dir, which)).convert('L'))
+ N = 200
+ M = 300
+ ig = ImageGeometry(voxel_num_x = N, voxel_num_y = M, dimension_labels=[ImageGeometry.HORIZONTAL_X, ImageGeometry.HORIZONTAL_Y])
+ data = ig.allocate()
+ data.fill(tmp/numpy.max(tmp))
+
+ else:
+ tmp = Image.open(os.path.join(self.data_dir, which))
+ print (tmp)
+ bands = tmp.getbands()
+ if len(bands) > 1:
+ ig = ImageGeometry(voxel_num_x=size[0], voxel_num_y=size[1], channels=len(bands),
+ dimension_labels=[ImageGeometry.HORIZONTAL_X, ImageGeometry.HORIZONTAL_Y, ImageGeometry.CHANNEL])
+ data = ig.allocate()
+ else:
+ ig = ImageGeometry(voxel_num_x = size[0], voxel_num_y = size[1], dimension_labels=[ImageGeometry.HORIZONTAL_X, ImageGeometry.HORIZONTAL_Y])
+ data = ig.allocate()
+ data.fill(numpy.array(tmp.resize((size[1],size[0]))))
+ if scale is not None:
+ dmax = data.as_array().max()
+ dmin = data.as_array().min()
+ # scale 0,1
+ data = (data -dmin) / (dmax - dmin)
+ if scale != (0,1):
+ #data = (data-dmin)/(dmax-dmin) * (scale[1]-scale[0]) +scale[0])
+ data *= (scale[1]-scale[0])
+ data += scale[0]
+ print ("data.geometry", data.geometry)
+ return data
+
+ def camera(**kwargs):
+
+ tmp = Image.open(os.path.join(data_dir, 'camera.png'))
+
+ size = kwargs.get('size',(512, 512))
+
+ data = numpy.array(tmp.resize(size))
+
+ data = data/data.max()
+
+ return ImageData(data)
+
+
+ def boat(**kwargs):
+
+ tmp = Image.open(os.path.join(data_dir, 'boat.tiff'))
+
+ size = kwargs.get('size',(512, 512))
+
+ data = numpy.array(tmp.resize(size))
+
+ data = data/data.max()
+
+ return ImageData(data)
+
+
+ def peppers(**kwargs):
+
+ tmp = Image.open(os.path.join(data_dir, 'peppers.tiff'))
+
+ size = kwargs.get('size',(512, 512))
+
+ data = numpy.array(tmp.resize(size))
+
+ data = data/data.max()
+
+ return ImageData(data)
+
+ def shapes(**kwargs):
+
+ tmp = Image.open(os.path.join(data_dir, 'shapes.png')).convert('LA')
+
+ size = kwargs.get('size',(300, 200))
+
+ data = numpy.array(tmp.resize(size))
+
+ data = data/data.max()
+
+ return ImageData(data)
+
diff --git a/Wrappers/Python/ccpi/framework/Vector.py b/Wrappers/Python/ccpi/framework/Vector.py new file mode 100755 index 0000000..542cb7a --- /dev/null +++ b/Wrappers/Python/ccpi/framework/Vector.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Edoardo Pasca + +# 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import numpy +import sys +from datetime import timedelta, datetime +import warnings +from functools import reduce +from numbers import Number +from ccpi.framework import DataContainer + +class VectorData(DataContainer): + def __init__(self, array=None, **kwargs): + self.geometry = kwargs.get('geometry', None) + self.dtype = kwargs.get('dtype', numpy.float32) + + if self.geometry is None: + if array is None: + raise ValueError('Please specify either a geometry or an array') + else: + if len(array.shape) > 1: + raise ValueError('Incompatible size: expected 1D got {}'.format(array.shape)) + out = array + self.geometry = VectorGeometry(array.shape[0]) + self.length = self.geometry.length + else: + self.length = self.geometry.length + + if array is None: + out = numpy.zeros((self.length,), dtype=self.dtype) + else: + if self.length == array.shape[0]: + out = array + else: + raise ValueError('Incompatible size: expecting {} got {}'.format((self.length,), array.shape)) + deep_copy = True + super(VectorData, self).__init__(out, deep_copy, None) + +class VectorGeometry(object): + RANDOM = 'random' + RANDOM_INT = 'random_int' + + def __init__(self, + length): + + self.length = length + self.shape = (length, ) + + + def clone(self): + '''returns a copy of VectorGeometry''' + return VectorGeometry(self.length) + + def allocate(self, value=0, **kwargs): + '''allocates an VectorData according to the size expressed in the instance''' + self.dtype = kwargs.get('dtype', numpy.float32) + out = VectorData(geometry=self, dtype=self.dtype) + if isinstance(value, Number): + if value != 0: + out += value + else: + if value == VectorGeometry.RANDOM: + seed = kwargs.get('seed', None) + if seed is not None: + numpy.random.seed(seed) + out.fill(numpy.random.random_sample(self.shape)) + elif value == VectorGeometry.RANDOM_INT: + seed = kwargs.get('seed', None) + if seed is not None: + numpy.random.seed(seed) + max_value = kwargs.get('max_value', 100) + out.fill(numpy.random.randint(max_value,size=self.shape)) + else: + raise ValueError('Value {} unknown'.format(value)) + return out diff --git a/Wrappers/Python/ccpi/framework/__init__.py b/Wrappers/Python/ccpi/framework/__init__.py new file mode 100755 index 0000000..3de27ed --- /dev/null +++ b/Wrappers/Python/ccpi/framework/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Mar 5 16:00:18 2019 + +@author: ofn77899 +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import numpy +import sys +from datetime import timedelta, datetime +import warnings +from functools import reduce + + +from .framework import DataContainer +from .framework import ImageData, AcquisitionData +from .framework import ImageGeometry, AcquisitionGeometry +from .framework import find_key, message +from .framework import DataProcessor +from .framework import AX, PixelByPixelDataProcessor, CastDataContainer +from .BlockDataContainer import BlockDataContainer +from .BlockGeometry import BlockGeometry +from .TestData import TestData +from .Vector import VectorGeometry, VectorData diff --git a/Wrappers/Python/ccpi/framework.py b/Wrappers/Python/ccpi/framework/framework.py index 24f4ca6..caea1e1 100644..100755 --- a/Wrappers/Python/ccpi/framework.py +++ b/Wrappers/Python/ccpi/framework/framework.py @@ -27,6 +27,7 @@ import sys from datetime import timedelta, datetime import warnings from functools import reduce +from numbers import Number def find_key(dic, val): """return the key of dictionary dic given the value""" @@ -43,6 +44,13 @@ def message(cls, msg, *args): class ImageGeometry(object): + RANDOM = 'random' + RANDOM_INT = 'random_int' + CHANNEL = 'channel' + ANGLE = 'angle' + VERTICAL = 'vertical' + HORIZONTAL_X = 'horizontal_x' + HORIZONTAL_Y = 'horizontal_y' def __init__(self, voxel_num_x=0, @@ -54,7 +62,8 @@ class ImageGeometry(object): center_x=0, center_y=0, center_z=0, - channels=1): + channels=1, + **kwargs): self.voxel_num_x = voxel_num_x self.voxel_num_y = voxel_num_y @@ -67,6 +76,49 @@ class ImageGeometry(object): self.center_z = center_z self.channels = channels + # this is some code repetition + if self.channels > 1: + if self.voxel_num_z>1: + self.length = 4 + shape = (self.channels, self.voxel_num_z, self.voxel_num_y, self.voxel_num_x) + dim_labels = [ImageGeometry.CHANNEL, ImageGeometry.VERTICAL, + ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] + else: + self.length = 3 + shape = (self.channels, self.voxel_num_y, self.voxel_num_x) + dim_labels = [ImageGeometry.CHANNEL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] + else: + if self.voxel_num_z>1: + self.length = 3 + shape = (self.voxel_num_z, self.voxel_num_y, self.voxel_num_x) + dim_labels = [ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_Y, + ImageGeometry.HORIZONTAL_X] + else: + self.length = 2 + shape = (self.voxel_num_y, self.voxel_num_x) + dim_labels = [ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] + + labels = kwargs.get('dimension_labels', None) + if labels is None: + self.shape = shape + self.dimension_labels = dim_labels + else: + order = self.get_order_by_label(labels, dim_labels) + if order != [0,1,2]: + # resort + self.shape = tuple([shape[i] for i in order]) + self.dimension_labels = labels + + def get_order_by_label(self, dimension_labels, default_dimension_labels): + order = [] + for i, el in enumerate(dimension_labels): + for j, ek in enumerate(default_dimension_labels): + if el == ek: + order.append(j) + break + return order + + def get_min_x(self): return self.center_x - 0.5*self.voxel_num_x*self.voxel_size_x @@ -111,14 +163,52 @@ class ImageGeometry(object): repres += "voxel_size : x{0},y{1},z{2}\n".format(self.voxel_size_x, self.voxel_size_y, self.voxel_size_z) repres += "center : x{0},y{1},z{2}\n".format(self.center_x, self.center_y, self.center_z) return repres - def allocate(self, value=0, dimension_labels=None): + def allocate(self, value=0, dimension_labels=None, **kwargs): '''allocates an ImageData according to the size expressed in the instance''' - out = ImageData(geometry=self, dimension_labels=dimension_labels) - if value != 0: - out += value + if dimension_labels is None: + out = ImageData(geometry=self, dimension_labels=self.dimension_labels) + else: + out = ImageData(geometry=self, dimension_labels=dimension_labels) + if isinstance(value, Number): + if value != 0: + out += value + else: + if value == ImageGeometry.RANDOM: + seed = kwargs.get('seed', None) + if seed is not None: + numpy.random.seed(seed) + out.fill(numpy.random.random_sample(self.shape)) + elif value == ImageGeometry.RANDOM_INT: + seed = kwargs.get('seed', None) + if seed is not None: + numpy.random.seed(seed) + max_value = kwargs.get('max_value', 100) + out.fill(numpy.random.randint(max_value,size=self.shape)) + else: + raise ValueError('Value {} unknown'.format(value)) + return out + # The following methods return 2 members of the class, therefore I + # don't think we need to implement them. + # Additionally using __len__ is confusing as one would think this is + # an iterable. + #def __len__(self): + # '''returns the length of the geometry''' + # return self.length + #def shape(self): + # '''Returns the shape of the array of the ImageData it describes''' + # return self.shape + class AcquisitionGeometry(object): - + RANDOM = 'random' + RANDOM_INT = 'random_int' + ANGLE_UNIT = 'angle_unit' + DEGREE = 'degree' + RADIAN = 'radian' + CHANNEL = 'channel' + ANGLE = 'angle' + VERTICAL = 'vertical' + HORIZONTAL = 'horizontal' def __init__(self, geom_type, dimension, @@ -130,7 +220,7 @@ class AcquisitionGeometry(object): dist_source_center=None, dist_center_detector=None, channels=1, - angle_unit='degree' + **kwargs ): """ General inputs for standard type projection geometries @@ -155,12 +245,16 @@ class AcquisitionGeometry(object): source_to_center_dist (if parallel: NaN) center_to_detector_dist (if parallel: NaN) standard or nonstandard (vec) geometry - angles + angles is expected numpy array, dtype - float32 angles_format radians or degrees """ self.geom_type = geom_type # 'parallel' or 'cone' self.dimension = dimension # 2D or 3D - self.angles = angles + if isinstance(angles, numpy.ndarray): + self.angles = angles + else: + raise ValueError('numpy array is expected') + num_of_angles = len (angles) self.dist_source_center = dist_source_center self.dist_center_detector = dist_center_detector @@ -171,6 +265,53 @@ class AcquisitionGeometry(object): self.pixel_size_v = pixel_size_v self.channels = channels + self.angle_unit=kwargs.get(AcquisitionGeometry.ANGLE_UNIT, + AcquisitionGeometry.DEGREE) + + # default labels + if channels > 1: + if pixel_num_v > 1: + shape = (channels, num_of_angles , pixel_num_v, pixel_num_h) + dim_labels = [AcquisitionGeometry.CHANNEL , + AcquisitionGeometry.ANGLE , AcquisitionGeometry.VERTICAL , + AcquisitionGeometry.HORIZONTAL] + else: + shape = (channels , num_of_angles, pixel_num_h) + dim_labels = [AcquisitionGeometry.CHANNEL , + AcquisitionGeometry.ANGLE, AcquisitionGeometry.HORIZONTAL] + else: + if pixel_num_v > 1: + shape = (num_of_angles, pixel_num_v, pixel_num_h) + dim_labels = [AcquisitionGeometry.ANGLE , AcquisitionGeometry.VERTICAL , + AcquisitionGeometry.HORIZONTAL] + else: + shape = (num_of_angles, pixel_num_h) + dim_labels = [AcquisitionGeometry.ANGLE, AcquisitionGeometry.HORIZONTAL] + + labels = kwargs.get('dimension_labels', None) + if labels is None: + self.shape = shape + self.dimension_labels = dim_labels + else: + if len(labels) != len(dim_labels): + raise ValueError('Wrong number of labels. Expected {} got {}'.format(len(dim_labels), len(labels))) + order = self.get_order_by_label(labels, dim_labels) + if order != [0,1,2]: + # resort + self.shape = tuple([shape[i] for i in order]) + self.dimension_labels = labels + + def get_order_by_label(self, dimension_labels, default_dimension_labels): + order = [] + for i, el in enumerate(dimension_labels): + for j, ek in enumerate(default_dimension_labels): + if el == ek: + order.append(j) + break + return order + + + def clone(self): '''returns a copy of the AcquisitionGeometry''' @@ -198,15 +339,36 @@ class AcquisitionGeometry(object): return repres def allocate(self, value=0, dimension_labels=None): '''allocates an AcquisitionData according to the size expressed in the instance''' - out = AcquisitionData(geometry=self, dimension_labels=dimension_labels) - if value != 0: - out += value + if dimension_labels is None: + out = AcquisitionData(geometry=self, dimension_labels=self.dimension_labels) + else: + out = AcquisitionData(geometry=self, dimension_labels=dimension_labels) + if isinstance(value, Number): + if value != 0: + out += value + else: + if value == AcquisitionData.RANDOM: + seed = kwargs.get('seed', None) + if seed is not None: + numpy.random.seed(seed) + out.fill(numpy.random.random_sample(self.shape)) + elif value == AcquisitionData.RANDOM_INT: + seed = kwargs.get('seed', None) + if seed is not None: + numpy.random.seed(seed) + max_value = kwargs.get('max_value', 100) + out.fill(numpy.random.randint(max_value,size=self.shape)) + else: + raise ValueError('Value {} unknown'.format(value)) + return out + class DataContainer(object): '''Generic class to hold data Data is currently held in a numpy arrays''' + __container_priority__ = 1 def __init__ (self, array, deep_copy=True, dimension_labels=None, **kwargs): '''Holds the data''' @@ -382,116 +544,21 @@ class DataContainer(object): return self.shape == other.shape ## algebra - def __add__(self, other , out=None, *args, **kwargs): - if issubclass(type(other), DataContainer): - if self.check_dimensions(other): - out = self.as_array() + other.as_array() - return type(self)(out, - deep_copy=True, - dimension_labels=self.dimension_labels, - geometry=self.geometry) - else: - raise ValueError('Wrong shape: {0} and {1}'.format(self.shape, - other.shape)) - elif isinstance(other, (int, float, complex)): - return type(self)( - self.as_array() + other, - deep_copy=True, - dimension_labels=self.dimension_labels, - geometry=self.geometry) - else: - raise TypeError('Cannot {0} DataContainer with {1}'.format("add" , - type(other))) - # __add__ + def __add__(self, other): + return self.add(other) + def __mul__(self, other): + return self.multiply(other) def __sub__(self, other): - if issubclass(type(other), DataContainer): - if self.check_dimensions(other): - out = self.as_array() - other.as_array() - return type(self)(out, - deep_copy=True, - dimension_labels=self.dimension_labels, - geometry=self.geometry) - else: - raise ValueError('__sub__ Wrong shape: {0} and {1}'.format(self.shape, - other.shape)) - elif isinstance(other, (int, float, complex)): - return type(self)(self.as_array() - other, - deep_copy=True, - dimension_labels=self.dimension_labels, - geometry=self.geometry) - else: - raise TypeError('Cannot {0} DataContainer with {1}'.format("subtract" , - type(other))) - # __sub__ - def __truediv__(self,other): - return self.__div__(other) - + return self.subtract(other) def __div__(self, other): - if issubclass(type(other), DataContainer): - if self.check_dimensions(other): - out = self.as_array() / other.as_array() - return type(self)(out, - deep_copy=True, - dimension_labels=self.dimension_labels, - geometry=self.geometry) - else: - raise ValueError('__div__ Wrong shape: {0} and {1}'.format(self.shape, - other.shape)) - elif isinstance(other, (int, float, complex)): - return type(self)(self.as_array() / other, - deep_copy=True, - dimension_labels=self.dimension_labels, - geometry=self.geometry) - else: - raise TypeError('Cannot {0} DataContainer with {1}'.format("divide" , - type(other))) - # __div__ - + return self.divide(other) + def __truediv__(self, other): + return self.divide(other) def __pow__(self, other): - if issubclass(type(other), DataContainer): - if self.check_dimensions(other): - out = self.as_array() ** other.as_array() - return type(self)(out, - deep_copy=True, - dimension_labels=self.dimension_labels, - geometry=self.geometry) - else: - raise ValueError('__pow__ Wrong shape: {0} and {1}'.format(self.shape, - other.shape)) - elif isinstance(other, (int, float, complex)): - return type(self)(self.as_array() ** other, - deep_copy=True, - dimension_labels=self.dimension_labels, - geometry=self.geometry) - else: - raise TypeError('pow: Cannot {0} DataContainer with {1}'.format("power" , - type(other))) - # __pow__ + return self.power(other) + - def __mul__(self, other): - if issubclass(type(other), DataContainer): - if self.check_dimensions(other): - out = self.as_array() * other.as_array() - return type(self)(out, - deep_copy=True, - dimension_labels=self.dimension_labels, - geometry=self.geometry) - else: - raise ValueError('*:Wrong shape: {0} and {1}'.format(self.shape, - other.shape)) - elif isinstance(other, (int, float, complex,\ - numpy.int, numpy.int8, numpy.int16, numpy.int32, numpy.int64,\ - numpy.float, numpy.float16, numpy.float32, numpy.float64, \ - numpy.complex)): - return type(self)(self.as_array() * other, - deep_copy=True, - dimension_labels=self.dimension_labels, - geometry=self.geometry) - else: - raise TypeError('Cannot {0} DataContainer with {1}'.format("multiply" , - type(other))) - # __mul__ # reverse operand def __radd__(self, other): @@ -532,54 +599,27 @@ class DataContainer(object): # (+=, -=, *=, /= , //=, # must return self - - def __iadd__(self, other): - if isinstance(other, (int, float)) : - numpy.add(self.array, other, out=self.array) - elif issubclass(type(other), DataContainer): - if self.check_dimensions(other): - numpy.add(self.array, other.array, out=self.array) - else: - raise ValueError('Dimensions do not match') - return self - # __iadd__ + kw = {'out':self} + return self.add(other, **kw) def __imul__(self, other): - if isinstance(other, (int, float)) : - arr = self.as_array() - numpy.multiply(arr, other, out=arr) - elif issubclass(type(other), DataContainer): - if self.check_dimensions(other): - numpy.multiply(self.array, other.array, out=self.array) - else: - raise ValueError('Dimensions do not match') - return self - # __imul__ + kw = {'out':self} + return self.multiply(other, **kw) def __isub__(self, other): - if isinstance(other, (int, float)) : - numpy.subtract(self.array, other, out=self.array) - elif issubclass(type(other), DataContainer): - if self.check_dimensions(other): - numpy.subtract(self.array, other.array, out=self.array) - else: - raise ValueError('Dimensions do not match') - return self - # __isub__ + kw = {'out':self} + return self.subtract(other, **kw) def __idiv__(self, other): - return self.__itruediv__(other) + kw = {'out':self} + return self.divide(other, **kw) + def __itruediv__(self, other): - if isinstance(other, (int, float)) : - numpy.divide(self.array, other, out=self.array) - elif issubclass(type(other), DataContainer): - if self.check_dimensions(other): - numpy.divide(self.array, other.array, out=self.array) - else: - raise ValueError('Dimensions do not match') - return self - # __idiv__ + kw = {'out':self} + return self.divide(other, **kw) + + def __str__ (self, representation=False): repres = "" @@ -639,8 +679,9 @@ class DataContainer(object): ## binary operations - def pixel_wise_binary(self,pwop, x2 , out=None, *args, **kwargs): - + def pixel_wise_binary(self, pwop, x2, *args, **kwargs): + out = kwargs.get('out', None) + if out is None: if isinstance(x2, (int, float, complex)): out = pwop(self.as_array() , x2 , *args, **kwargs ) @@ -658,24 +699,30 @@ class DataContainer(object): elif issubclass(type(out), DataContainer) and issubclass(type(x2), DataContainer): if self.check_dimensions(out) and self.check_dimensions(x2): - pwop(self.as_array(), x2.as_array(), out=out.as_array(), *args, **kwargs ) + kwargs['out'] = out.as_array() + pwop(self.as_array(), x2.as_array(), *args, **kwargs ) #return type(self)(out.as_array(), # deep_copy=False, # dimension_labels=self.dimension_labels, # geometry=self.geometry) return out else: - raise ValueError(message(type(self),"Wrong size for data memory: ", out.shape,self.shape)) - elif issubclass(type(out), DataContainer) and isinstance(x2, (int,float,complex)): + raise ValueError(message(type(self),"Wrong size for data memory: out {} x2 {} expected {}".format( out.shape,x2.shape ,self.shape))) + elif issubclass(type(out), DataContainer) and \ + isinstance(x2, (int,float,complex, numpy.int, numpy.int8, \ + numpy.int16, numpy.int32, numpy.int64,\ + numpy.float, numpy.float16, numpy.float32,\ + numpy.float64, numpy.complex)): if self.check_dimensions(out): - - pwop(self.as_array(), x2, out=out.as_array(), *args, **kwargs ) + kwargs['out']=out.as_array() + pwop(self.as_array(), x2, *args, **kwargs ) return out else: raise ValueError(message(type(self),"Wrong size for data memory: ", out.shape,self.shape)) elif issubclass(type(out), numpy.ndarray): if self.array.shape == out.shape and self.array.dtype == out.dtype: - pwop(self.as_array(), x2 , out=out, *args, **kwargs) + kwargs['out'] = out + pwop(self.as_array(), x2, *args, **kwargs) #return type(self)(out, # deep_copy=False, # dimension_labels=self.dimension_labels, @@ -683,26 +730,43 @@ class DataContainer(object): else: raise ValueError (message(type(self), "incompatible class:" , pwop.__name__, type(out))) - def add(self, other , out=None, *args, **kwargs): - return self.pixel_wise_binary(numpy.add, other, out=out, *args, **kwargs) - - def subtract(self, other, out=None , *args, **kwargs): - return self.pixel_wise_binary(numpy.subtract, other, out=out, *args, **kwargs) + def add(self, other, *args, **kwargs): + if hasattr(other, '__container_priority__') and \ + self.__class__.__container_priority__ < other.__class__.__container_priority__: + return other.add(self, *args, **kwargs) + return self.pixel_wise_binary(numpy.add, other, *args, **kwargs) + + def subtract(self, other, *args, **kwargs): + if hasattr(other, '__container_priority__') and \ + self.__class__.__container_priority__ < other.__class__.__container_priority__: + return other.subtract(self, *args, **kwargs) + return self.pixel_wise_binary(numpy.subtract, other, *args, **kwargs) + + def multiply(self, other, *args, **kwargs): + if hasattr(other, '__container_priority__') and \ + self.__class__.__container_priority__ < other.__class__.__container_priority__: + return other.multiply(self, *args, **kwargs) + return self.pixel_wise_binary(numpy.multiply, other, *args, **kwargs) + + def divide(self, other, *args, **kwargs): + if hasattr(other, '__container_priority__') and \ + self.__class__.__container_priority__ < other.__class__.__container_priority__: + return other.divide(self, *args, **kwargs) + return self.pixel_wise_binary(numpy.divide, other, *args, **kwargs) + + def power(self, other, *args, **kwargs): + return self.pixel_wise_binary(numpy.power, other, *args, **kwargs) + + def maximum(self, x2, *args, **kwargs): + return self.pixel_wise_binary(numpy.maximum, x2, *args, **kwargs) + + def minimum(self,x2, out=None, *args, **kwargs): + return self.pixel_wise_binary(numpy.minimum, x2=x2, out=out, *args, **kwargs) - def multiply(self, other , out=None, *args, **kwargs): - return self.pixel_wise_binary(numpy.multiply, other, out=out, *args, **kwargs) - - def divide(self, other , out=None ,*args, **kwargs): - return self.pixel_wise_binary(numpy.divide, other, out=out, *args, **kwargs) - - def power(self, other , out=None, *args, **kwargs): - return self.pixel_wise_binary(numpy.power, other, out=out, *args, **kwargs) - - def maximum(self,x2, out=None, *args, **kwargs): - return self.pixel_wise_binary(numpy.maximum, x2=x2, out=out, *args, **kwargs) ## unary operations - def pixel_wise_unary(self,pwop, out=None, *args, **kwargs): + def pixel_wise_unary(self, pwop, *args, **kwargs): + out = kwargs.get('out', None) if out is None: out = pwop(self.as_array() , *args, **kwargs ) return type(self)(out, @@ -711,104 +775,90 @@ class DataContainer(object): geometry=self.geometry) elif issubclass(type(out), DataContainer): if self.check_dimensions(out): - pwop(self.as_array(), out=out.as_array(), *args, **kwargs ) + kwargs['out'] = out.as_array() + pwop(self.as_array(), *args, **kwargs ) else: raise ValueError(message(type(self),"Wrong size for data memory: ", out.shape,self.shape)) elif issubclass(type(out), numpy.ndarray): if self.array.shape == out.shape and self.array.dtype == out.dtype: - pwop(self.as_array(), out=out, *args, **kwargs) + kwargs['out'] = out + pwop(self.as_array(), *args, **kwargs) else: raise ValueError (message(type(self), "incompatible class:" , pwop.__name__, type(out))) - def abs(self, out=None, *args, **kwargs): - return self.pixel_wise_unary(numpy.abs, out=out, *args, **kwargs) - - def sign(self, out=None, *args, **kwargs): - return self.pixel_wise_unary(numpy.sign , out=out, *args, **kwargs) + def abs(self, *args, **kwargs): + return self.pixel_wise_unary(numpy.abs, *args, **kwargs) - def sqrt(self, out=None, *args, **kwargs): - return self.pixel_wise_unary(numpy.sqrt, out=out, *args, **kwargs) + def sign(self, *args, **kwargs): + return self.pixel_wise_unary(numpy.sign, *args, **kwargs) + def sqrt(self, *args, **kwargs): + return self.pixel_wise_unary(numpy.sqrt, *args, **kwargs) + + def conjugate(self, *args, **kwargs): + return self.pixel_wise_unary(numpy.conjugate, *args, **kwargs) #def __abs__(self): # operation = FM.OPERATION.ABS # return self.callFieldMath(operation, None, self.mask, self.maskOnValue) # __abs__ ## reductions - def sum(self, out=None, *args, **kwargs): + def sum(self, *args, **kwargs): return self.as_array().sum(*args, **kwargs) def squared_norm(self): '''return the squared euclidean norm of the DataContainer viewed as a vector''' - shape = self.shape - size = reduce(lambda x,y:x*y, shape, 1) - y = numpy.reshape(self.as_array(), (size, )) - return numpy.dot(y, y.conjugate()) + #shape = self.shape + #size = reduce(lambda x,y:x*y, shape, 1) + #y = numpy.reshape(self.as_array(), (size, )) + return self.dot(self.conjugate()) + #return self.dot(self) def norm(self): '''return the euclidean norm of the DataContainer viewed as a vector''' return numpy.sqrt(self.squared_norm()) + + def dot(self, other, *args, **kwargs): '''return the inner product of 2 DataContainers viewed as vectors''' + method = kwargs.get('method', 'numpy') + if method not in ['numpy','reduce']: + raise ValueError('dot: specified method not valid. Expecting numpy or reduce got {} '.format( + method)) + if self.shape == other.shape: - return numpy.dot(self.as_array().ravel(), other.as_array().ravel()) + # return (self*other).sum() + if method == 'numpy': + return numpy.dot(self.as_array().ravel(), other.as_array().ravel()) + elif method == 'reduce': + # see https://github.com/vais-ral/CCPi-Framework/pull/273 + # notice that Python seems to be smart enough to use + # the appropriate type to hold the result of the reduction + sf = reduce(lambda x,y: x + y[0]*y[1], + zip(self.as_array().ravel(), + other.as_array().ravel()), + 0) + return sf else: raise ValueError('Shapes are not aligned: {} != {}'.format(self.shape, other.shape)) - + + class ImageData(DataContainer): '''DataContainer for holding 2D or 3D DataContainer''' + __container_priority__ = 1 + + def __init__(self, array = None, deep_copy=False, dimension_labels=None, **kwargs): - self.geometry = None + self.geometry = kwargs.get('geometry', None) if array is None: - if 'geometry' in kwargs.keys(): - geometry = kwargs['geometry'] - self.geometry = geometry - channels = geometry.channels - horiz_x = geometry.voxel_num_x - horiz_y = geometry.voxel_num_y - vert = 1 if geometry.voxel_num_z is None\ - else geometry.voxel_num_z # this should be 1 for 2D - if dimension_labels is None: - if channels > 1: - if vert > 1: - shape = (channels, vert, horiz_y, horiz_x) - dim_labels = ['channel' ,'vertical' , 'horizontal_y' , - 'horizontal_x'] - else: - shape = (channels , horiz_y, horiz_x) - dim_labels = ['channel' , 'horizontal_y' , - 'horizontal_x'] - else: - if vert > 1: - shape = (vert, horiz_y, horiz_x) - dim_labels = ['vertical' , 'horizontal_y' , - 'horizontal_x'] - else: - shape = (horiz_y, horiz_x) - dim_labels = ['horizontal_y' , - 'horizontal_x'] - dimension_labels = dim_labels - else: - shape = [] - for dim in dimension_labels: - if dim == 'channel': - shape.append(channels) - elif dim == 'horizontal_y': - shape.append(horiz_y) - elif dim == 'vertical': - shape.append(vert) - elif dim == 'horizontal_x': - shape.append(horiz_x) - if len(shape) != len(dimension_labels): - raise ValueError('Missing {0} axes'.format( - len(dimension_labels) - len(shape))) - shape = tuple(shape) + if self.geometry is not None: + shape, dimension_labels = self.get_shape_labels(self.geometry, dimension_labels) array = numpy.zeros( shape , dtype=numpy.float32) super(ImageData, self).__init__(array, deep_copy, @@ -818,6 +868,11 @@ class ImageData(DataContainer): raise ValueError('Please pass either a DataContainer, ' +\ 'a numpy array or a geometry') else: + if self.geometry is not None: + shape, labels = self.get_shape_labels(self.geometry, dimension_labels) + if array.shape != shape: + raise ValueError('Shape mismatch {} {}'.format(shape, array.shape)) + if issubclass(type(array) , DataContainer): # if the array is a DataContainer get the info from there if not ( array.number_of_dimensions == 2 or \ @@ -838,14 +893,17 @@ class ImageData(DataContainer): if dimension_labels is None: if array.ndim == 4: - dimension_labels = ['channel' ,'vertical' , 'horizontal_y' , - 'horizontal_x'] + dimension_labels = [ImageGeometry.CHANNEL, + ImageGeometry.VERTICAL, + ImageGeometry.HORIZONTAL_Y, + ImageGeometry.HORIZONTAL_X] elif array.ndim == 3: - dimension_labels = ['vertical' , 'horizontal_y' , - 'horizontal_x'] + dimension_labels = [ImageGeometry.VERTICAL, + ImageGeometry.HORIZONTAL_Y, + ImageGeometry.HORIZONTAL_X] else: - dimension_labels = ['horizontal_y' , - 'horizontal_x'] + dimension_labels = [ ImageGeometry.HORIZONTAL_Y, + ImageGeometry.HORIZONTAL_X] #DataContainer.__init__(self, array, deep_copy, dimension_labels, **kwargs) super(ImageData, self).__init__(array, deep_copy, @@ -861,75 +919,93 @@ class ImageData(DataContainer): self.spacing = value def subset(self, dimensions=None, **kw): + # FIXME: this is clearly not rigth + # it should be something like + # out = DataContainer.subset(self, dimensions, **kw) + # followed by regeneration of the proper geometry. out = super(ImageData, self).subset(dimensions, **kw) #out.geometry = self.recalculate_geometry(dimensions , **kw) out.geometry = self.geometry return out - + + def get_shape_labels(self, geometry, dimension_labels=None): + channels = geometry.channels + horiz_x = geometry.voxel_num_x + horiz_y = geometry.voxel_num_y + vert = 1 if geometry.voxel_num_z is None\ + else geometry.voxel_num_z # this should be 1 for 2D + if dimension_labels is None: + if channels > 1: + if vert > 1: + shape = (channels, vert, horiz_y, horiz_x) + dim_labels = [ImageGeometry.CHANNEL, + ImageGeometry.VERTICAL, + ImageGeometry.HORIZONTAL_Y, + ImageGeometry.HORIZONTAL_X] + else: + shape = (channels , horiz_y, horiz_x) + dim_labels = [ImageGeometry.CHANNEL, + ImageGeometry.HORIZONTAL_Y, + ImageGeometry.HORIZONTAL_X] + else: + if vert > 1: + shape = (vert, horiz_y, horiz_x) + dim_labels = [ImageGeometry.VERTICAL, + ImageGeometry.HORIZONTAL_Y, + ImageGeometry.HORIZONTAL_X] + else: + shape = (horiz_y, horiz_x) + dim_labels = [ImageGeometry.HORIZONTAL_Y, + ImageGeometry.HORIZONTAL_X] + dimension_labels = dim_labels + else: + shape = [] + for i in range(len(dimension_labels)): + dim = dimension_labels[i] + if dim == ImageGeometry.CHANNEL: + shape.append(channels) + elif dim == ImageGeometry.HORIZONTAL_Y: + shape.append(horiz_y) + elif dim == ImageGeometry.VERTICAL: + shape.append(vert) + elif dim == ImageGeometry.HORIZONTAL_X: + shape.append(horiz_x) + if len(shape) != len(dimension_labels): + raise ValueError('Missing {0} axes {1} shape {2}'.format( + len(dimension_labels) - len(shape), dimension_labels, shape)) + shape = tuple(shape) + + return (shape, dimension_labels) + class AcquisitionData(DataContainer): '''DataContainer for holding 2D or 3D sinogram''' + __container_priority__ = 1 + + def __init__(self, array = None, deep_copy=True, dimension_labels=None, **kwargs): - self.geometry = None + self.geometry = kwargs.get('geometry', None) if array is None: if 'geometry' in kwargs.keys(): geometry = kwargs['geometry'] self.geometry = geometry - channels = geometry.channels - horiz = geometry.pixel_num_h - vert = geometry.pixel_num_v - angles = geometry.angles - num_of_angles = numpy.shape(angles)[0] - if dimension_labels is None: - if channels > 1: - if vert > 1: - shape = (channels, num_of_angles , vert, horiz) - dim_labels = ['channel' , ' angle' , - 'vertical' , 'horizontal'] - else: - shape = (channels , num_of_angles, horiz) - dim_labels = ['channel' , 'angle' , - 'horizontal'] - else: - if vert > 1: - shape = (num_of_angles, vert, horiz) - dim_labels = ['angle' , 'vertical' , - 'horizontal'] - else: - shape = (num_of_angles, horiz) - dim_labels = ['angle' , - 'horizontal'] - - dimension_labels = dim_labels - else: - shape = [] - for dim in dimension_labels: - if dim == 'channel': - shape.append(channels) - elif dim == 'angle': - shape.append(num_of_angles) - elif dim == 'vertical': - shape.append(vert) - elif dim == 'horizontal': - shape.append(horiz) - if len(shape) != len(dimension_labels): - raise ValueError('Missing {0} axes.\nExpected{1} got {2}}'\ - .format( - len(dimension_labels) - len(shape), - dimension_labels, shape) - ) - shape = tuple(shape) + shape, dimension_labels = self.get_shape_labels(geometry, dimension_labels) + array = numpy.zeros( shape , dtype=numpy.float32) super(AcquisitionData, self).__init__(array, deep_copy, dimension_labels, **kwargs) else: - + if self.geometry is not None: + shape, labels = self.get_shape_labels(self.geometry, dimension_labels) + if array.shape != shape: + raise ValueError('Shape mismatch {} {}'.format(shape, array.shape)) + if issubclass(type(array) ,DataContainer): # if the array is a DataContainer get the info from there if not ( array.number_of_dimensions == 2 or \ @@ -950,21 +1026,81 @@ class AcquisitionData(DataContainer): if dimension_labels is None: if array.ndim == 4: - dimension_labels = ['channel' ,'angle' , 'vertical' , - 'horizontal'] + dimension_labels = [AcquisitionGeometry.CHANNEL, + AcquisitionGeometry.ANGLE, + AcquisitionGeometry.VERTICAL, + AcquisitionGeometry.HORIZONTAL] elif array.ndim == 3: - dimension_labels = ['angle' , 'vertical' , - 'horizontal'] + dimension_labels = [AcquisitionGeometry.ANGLE, + AcquisitionGeometry.VERTICAL, + AcquisitionGeometry.HORIZONTAL] else: - dimension_labels = ['angle' , - 'horizontal'] - - #DataContainer.__init__(self, array, deep_copy, dimension_labels, **kwargs) + dimension_labels = [AcquisitionGeometry.ANGLE, + AcquisitionGeometry.HORIZONTAL] + super(AcquisitionData, self).__init__(array, deep_copy, dimension_labels, **kwargs) + def get_shape_labels(self, geometry, dimension_labels=None): + channels = geometry.channels + horiz = geometry.pixel_num_h + vert = geometry.pixel_num_v + angles = geometry.angles + num_of_angles = numpy.shape(angles)[0] + + if dimension_labels is None: + if channels > 1: + if vert > 1: + shape = (channels, num_of_angles , vert, horiz) + dim_labels = [AcquisitionGeometry.CHANNEL, + AcquisitionGeometry.ANGLE, + AcquisitionGeometry.VERTICAL, + AcquisitionGeometry.HORIZONTAL] + else: + shape = (channels , num_of_angles, horiz) + dim_labels = [AcquisitionGeometry.CHANNEL, + AcquisitionGeometry.ANGLE, + AcquisitionGeometry.HORIZONTAL] + else: + if vert > 1: + shape = (num_of_angles, vert, horiz) + dim_labels = [AcquisitionGeometry.ANGLE, + AcquisitionGeometry.VERTICAL, + AcquisitionGeometry.HORIZONTAL + ] + else: + shape = (num_of_angles, horiz) + dim_labels = [AcquisitionGeometry.ANGLE, + AcquisitionGeometry.HORIZONTAL + ] + + dimension_labels = dim_labels + else: + shape = [] + for i in range(len(dimension_labels)): + dim = dimension_labels[i] + + if dim == AcquisitionGeometry.CHANNEL: + shape.append(channels) + elif dim == AcquisitionGeometry.ANGLE: + shape.append(num_of_angles) + elif dim == AcquisitionGeometry.VERTICAL: + shape.append(vert) + elif dim == AcquisitionGeometry.HORIZONTAL: + shape.append(horiz) + if len(shape) != len(dimension_labels): + raise ValueError('Missing {0} axes.\nExpected{1} got {2}'\ + .format( + len(dimension_labels) - len(shape), + dimension_labels, shape) + ) + shape = tuple(shape) + return (shape, dimension_labels) + + class DataProcessor(object): + '''Defines a generic DataContainer processor accepts DataContainer as inputs and @@ -1010,6 +1146,7 @@ class DataProcessor(object): raise NotImplementedError('Implement basic checks for input DataContainer') def get_output(self, out=None): + for k,v in self.__dict__.items(): if v is None and k != 'output': raise ValueError('Key {0} is None'.format(k)) diff --git a/Wrappers/Python/ccpi/io/NEXUSDataReader.py b/Wrappers/Python/ccpi/io/NEXUSDataReader.py new file mode 100644 index 0000000..e6d4d3b --- /dev/null +++ b/Wrappers/Python/ccpi/io/NEXUSDataReader.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Apr 3 10:30:25 2019 + +@author: evelina +""" + + +import numpy +import os +from ccpi.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry + + +h5pyAvailable = True +try: + import h5py +except: + h5pyAvailable = False + + +class NEXUSDataReader(object): + + def __init__(self, + **kwargs): + + ''' + Constructor + + Input: + + nexus_file full path to NEXUS file + ''' + + self.nexus_file = kwargs.get('nexus_file', None) + + if self.nexus_file is not None: + self.set_up(nexus_file = self.nexus_file) + + def set_up(self, + nexus_file = None): + + self.nexus_file = nexus_file + + # check that h5py library is installed + if (h5pyAvailable == False): + raise Exception('h5py is not available, cannot load NEXUS files.') + + if self.nexus_file == None: + raise Exception('Path to nexus file is required.') + + # check if nexus file exists + if not(os.path.isfile(self.nexus_file)): + raise Exception('File\n {}\n does not exist.'.format(self.nexus_file)) + + def load_data(self): + + ''' + Parse NEXUS file and returns either ImageData or Acquisition Data + depending on file content + ''' + + try: + with h5py.File(self.nexus_file,'r') as file: + + if (file.attrs['creator'] != 'NEXUSDataWriter.py'): + raise Exception('We can parse only files created by NEXUSDataWriter.py') + + ds_data = file['entry1/tomo_entry/data/data'] + data = numpy.array(ds_data, dtype = 'float32') + + dimension_labels = [] + + for i in range(data.ndim): + dimension_labels.append(ds_data.attrs['dim{}'.format(i)]) + + if ds_data.attrs['data_type'] == 'ImageData': + self._geometry = ImageGeometry(voxel_num_x = ds_data.attrs['voxel_num_x'], + voxel_num_y = ds_data.attrs['voxel_num_y'], + voxel_num_z = ds_data.attrs['voxel_num_z'], + voxel_size_x = ds_data.attrs['voxel_size_x'], + voxel_size_y = ds_data.attrs['voxel_size_y'], + voxel_size_z = ds_data.attrs['voxel_size_z'], + center_x = ds_data.attrs['center_x'], + center_y = ds_data.attrs['center_y'], + center_z = ds_data.attrs['center_z'], + channels = ds_data.attrs['channels']) + + return ImageData(array = data, + deep_copy = False, + geometry = self._geometry, + dimension_labels = dimension_labels) + + else: # AcquisitionData + if ds_data.attrs.__contains__('dist_source_center'): + dist_source_center = ds_data.attrs['dist_source_center'] + else: + dist_source_center = None + + if ds_data.attrs.__contains__('dist_center_detector'): + dist_center_detector = ds_data.attrs['dist_center_detector'] + else: + dist_center_detector = None + + self._geometry = AcquisitionGeometry(geom_type = ds_data.attrs['geom_type'], + dimension = ds_data.attrs['dimension'], + dist_source_center = dist_source_center, + dist_center_detector = dist_center_detector, + pixel_num_h = ds_data.attrs['pixel_num_h'], + pixel_size_h = ds_data.attrs['pixel_size_h'], + pixel_num_v = ds_data.attrs['pixel_num_v'], + pixel_size_v = ds_data.attrs['pixel_size_v'], + channels = ds_data.attrs['channels'], + angles = numpy.array(file['entry1/tomo_entry/data/rotation_angle'], dtype = 'float32')) + #angle_unit = file['entry1/tomo_entry/data/rotation_angle'].attrs['units']) + + return AcquisitionData(array = data, + deep_copy = False, + geometry = self._geometry, + dimension_labels = dimension_labels) + + except: + print("Error reading nexus file") + raise + + def get_geometry(self): + + ''' + Return either ImageGeometry or AcquisitionGeometry + depepnding on file content + ''' + + return self._geometry + + +''' +# usage example +reader = NEXUSDataReader() +reader.set_up(nexus_file = '/home/evelina/test_nexus.nxs') +acquisition_data = reader.load_data() +print(acquisition_data) +ag = reader.get_geometry() +print(ag) + +reader = NEXUSDataReader() +reader.set_up(nexus_file = '/home/evelina/test_nexus_im.nxs') +image_data = reader.load_data() +print(image_data) +ig = reader.get_geometry() +print(ig) + +reader = NEXUSDataReader() +reader.set_up(nexus_file = '/home/evelina/test_nexus_ag.nxs') +ad = reader.load_data() +print(ad) +ad = reader.get_geometry() +print(ad) +''' diff --git a/Wrappers/Python/ccpi/io/NEXUSDataWriter.py b/Wrappers/Python/ccpi/io/NEXUSDataWriter.py new file mode 100644 index 0000000..6f5c0b2 --- /dev/null +++ b/Wrappers/Python/ccpi/io/NEXUSDataWriter.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu May 2 10:11:20 2019 + +@author: evelina +""" + + +import numpy +import os +from ccpi.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry +import datetime + + +h5pyAvailable = True +try: + import h5py +except: + h5pyAvailable = False + + +class NEXUSDataWriter(object): + + def __init__(self, + **kwargs): + + self.data_container = kwargs.get('data_container', None) + self.file_name = kwargs.get('file_name', None) + + if ((self.data_container is not None) and (self.file_name is not None)): + self.set_up(data_container = self.data_container, + file_name = self.file_name) + + def set_up(self, + data_container = None, + file_name = None): + + self.data_container = data_container + self.file_name = file_name + + if not ((isinstance(self.data_container, ImageData)) or + (isinstance(self.data_container, AcquisitionData))): + raise Exception('Writer supports only following data types:\n' + + ' - ImageData\n - AcquisitionData') + + # check that h5py library is installed + if (h5pyAvailable == False): + raise Exception('h5py is not available, cannot load NEXUS files.') + + def write_file(self): + + # if the folder does not exist, create the folder + if not os.path.isdir(os.path.dirname(self.file_name)): + os.mkdir(os.path.dirname(self.file_name)) + + # create the file + with h5py.File(self.file_name, 'w') as f: + + # give the file some important attributes + f.attrs['file_name'] = self.file_name + f.attrs['file_time'] = str(datetime.datetime.utcnow()) + f.attrs['creator'] = 'NEXUSDataWriter.py' + f.attrs['NeXus_version'] = '4.3.0' + f.attrs['HDF5_Version'] = h5py.version.hdf5_version + f.attrs['h5py_version'] = h5py.version.version + + # create the NXentry group + nxentry = f.create_group('entry1/tomo_entry') + nxentry.attrs['NX_class'] = 'NXentry' + + # create dataset to store data + ds_data = f.create_dataset('entry1/tomo_entry/data/data', + (self.data_container.as_array().shape), + dtype = 'float32', + data = self.data_container.as_array()) + + # set up dataset attributes + if (isinstance(self.data_container, ImageData)): + ds_data.attrs['data_type'] = 'ImageData' + else: + ds_data.attrs['data_type'] = 'AcquisitionData' + + for i in range(self.data_container.as_array().ndim): + ds_data.attrs['dim{}'.format(i)] = self.data_container.dimension_labels[i] + + if (isinstance(self.data_container, AcquisitionData)): + ds_data.attrs['geom_type'] = self.data_container.geometry.geom_type + ds_data.attrs['dimension'] = self.data_container.geometry.dimension + if self.data_container.geometry.dist_source_center is not None: + ds_data.attrs['dist_source_center'] = self.data_container.geometry.dist_source_center + if self.data_container.geometry.dist_center_detector is not None: + ds_data.attrs['dist_center_detector'] = self.data_container.geometry.dist_center_detector + ds_data.attrs['pixel_num_h'] = self.data_container.geometry.pixel_num_h + ds_data.attrs['pixel_size_h'] = self.data_container.geometry.pixel_size_h + ds_data.attrs['pixel_num_v'] = self.data_container.geometry.pixel_num_v + ds_data.attrs['pixel_size_v'] = self.data_container.geometry.pixel_size_v + ds_data.attrs['channels'] = self.data_container.geometry.channels + ds_data.attrs['n_angles'] = self.data_container.geometry.angles.shape[0] + + # create the dataset to store rotation angles + ds_angles = f.create_dataset('entry1/tomo_entry/data/rotation_angle', + (self.data_container.geometry.angles.shape), + dtype = 'float32', + data = self.data_container.geometry.angles) + + #ds_angles.attrs['units'] = self.data_container.geometry.angle_unit + + else: # ImageData + + ds_data.attrs['voxel_num_x'] = self.data_container.geometry.voxel_num_x + ds_data.attrs['voxel_num_y'] = self.data_container.geometry.voxel_num_y + ds_data.attrs['voxel_num_z'] = self.data_container.geometry.voxel_num_z + ds_data.attrs['voxel_size_x'] = self.data_container.geometry.voxel_size_x + ds_data.attrs['voxel_size_y'] = self.data_container.geometry.voxel_size_y + ds_data.attrs['voxel_size_z'] = self.data_container.geometry.voxel_size_z + ds_data.attrs['center_x'] = self.data_container.geometry.center_x + ds_data.attrs['center_y'] = self.data_container.geometry.center_y + ds_data.attrs['center_z'] = self.data_container.geometry.center_z + ds_data.attrs['channels'] = self.data_container.geometry.channels + + +''' +# usage example +xtek_file = '/home/evelina/nikon_data/SophiaBeads_256_averaged.xtekct' +reader = NikonDataReader() +reader.set_up(xtek_file = xtek_file, + binning = [3, 1], + roi = [200, 500, 1500, 2000], + normalize = True) + +data = reader.load_projections() +ag = reader.get_geometry() + +writer = NEXUSDataWriter() +writer.set_up(file_name = '/home/evelina/test_nexus.nxs', + data_container = data) + +writer.write_file() + +ig = ImageGeometry(voxel_num_x = 100, + voxel_num_y = 100) +im = ImageData(array = numpy.zeros((100, 100), dtype = 'float'), + geometry = ig) +im_writer = NEXUSDataWriter() + +im_writer.set_up(file_name = '/home/evelina/test_nexus_im.nxs', + data_container = im) +im_writer.write_file() + +ag = AcquisitionGeometry(geom_type = 'parallel', + dimension = '2D', + angles = numpy.array([0, 1]), + pixel_num_h = 200, + pixel_size_h = 1, + pixel_num_v = 100, + pixel_size_v = 1) + +ad = ag.allocate() +ag_writer = NEXUSDataWriter() +ag_writer.set_up(file_name = '/home/evelina/test_nexus_ag.nxs', + data_container = ad) +ag_writer.write_file() +'''
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/io/NikonDataReader.py b/Wrappers/Python/ccpi/io/NikonDataReader.py new file mode 100644 index 0000000..703b65b --- /dev/null +++ b/Wrappers/Python/ccpi/io/NikonDataReader.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Apr 3 10:30:25 2019 + +@author: evelina +""" + +from ccpi.framework import AcquisitionData, AcquisitionGeometry +import numpy +import os + + +pilAvailable = True +try: + from PIL import Image +except: + pilAvailable = False + + +class NikonDataReader(object): + + def __init__(self, + **kwargs): + ''' + Constructor + + Input: + + xtek_file full path to .xtexct file + + roi region-of-interest to load. If roi = -1 (default), + full projections will be loaded. Otherwise roi is + given by [(row0, row1), (column0, column1)], where + row0, column0 are coordinates of top left corner and + row1, column1 are coordinates of bottom right corner. + + binning number of pixels to bin (combine) along 0 (column) + and 1 (row) dimension. If binning = [1, 1] (default), + projections in original resolution are loaded. Note, + if binning[0] != binning[1], then loaded projections + will have anisotropic pixels, which are currently not + supported by the Framework + + normalize normalize loaded projections by detector + white level (I_0). Default value is False, + i.e. no normalization. + + flip default = False, flip projections in the left-right direction + + ''' + + self.xtek_file = kwargs.get('xtek_file', None) + self.roi = kwargs.get('roi', -1) + self.binning = kwargs.get('binning', [1, 1]) + self.normalize = kwargs.get('normalize', False) + self.flip = kwargs.get('flip', False) + + if self.xtek_file is not None: + self.set_up(xtek_file = self.xtek_file, + roi = self.roi, + binning = self.binning, + normalize = self.normalize, + flip = self.flip) + + def set_up(self, + xtek_file = None, + roi = -1, + binning = [1, 1], + normalize = False, + flip = False): + + self.xtek_file = xtek_file + self.roi = roi + self.binning = binning + self.normalize = normalize + self.flip = flip + + if self.xtek_file == None: + raise Exception('Path to xtek file is required.') + + # check if xtek file exists + if not(os.path.isfile(self.xtek_file)): + raise Exception('File\n {}\n does not exist.'.format(self.xtek_file)) + + # check that PIL library is installed + if (pilAvailable == False): + raise Exception("PIL (pillow) is not available, cannot load TIFF files.") + + # parse xtek file + with open(self.xtek_file, 'r') as f: + content = f.readlines() + + content = [x.strip() for x in content] + + for line in content: + # filename of TIFF files + if line.startswith("Name"): + self._experiment_name = line.split('=')[1] + # number of projections + elif line.startswith("Projections"): + num_projections = int(line.split('=')[1]) + # white level - used for normalization + elif line.startswith("WhiteLevel"): + self._white_level = float(line.split('=')[1]) + # number of pixels along Y axis + elif line.startswith("DetectorPixelsY"): + pixel_num_v_0 = int(line.split('=')[1]) + # number of pixels along X axis + elif line.startswith("DetectorPixelsX"): + pixel_num_h_0 = int(line.split('=')[1]) + # pixel size along X axis + elif line.startswith("DetectorPixelSizeX"): + pixel_size_h_0 = float(line.split('=')[1]) + # pixel size along Y axis + elif line.startswith("DetectorPixelSizeY"): + pixel_size_v_0 = float(line.split('=')[1]) + # source to center of rotation distance + elif line.startswith("SrcToObject"): + source_x = float(line.split('=')[1]) + # source to detector distance + elif line.startswith("SrcToDetector"): + detector_x = float(line.split('=')[1]) + # initial angular position of a rotation stage + elif line.startswith("InitialAngle"): + initial_angle = float(line.split('=')[1]) + # angular increment (in degrees) + elif line.startswith("AngularStep"): + angular_step = float(line.split('=')[1]) + + if self.roi == -1: + self._roi_par = [(0, pixel_num_v_0), \ + (0, pixel_num_h_0)] + else: + self._roi_par = self.roi.copy() + if self._roi_par[0] == -1: + self._roi_par[0] = (0, pixel_num_v_0) + if self._roi_par[1] == -1: + self._roi_par[1] = (0, pixel_num_h_0) + + # calculate number of pixels and pixel size + if (self.binning == [1, 1]): + pixel_num_v = self._roi_par[0][1] - self._roi_par[0][0] + pixel_num_h = self._roi_par[1][1] - self._roi_par[1][0] + pixel_size_v = pixel_size_v_0 + pixel_size_h = pixel_size_h_0 + else: + pixel_num_v = (self._roi_par[0][1] - self._roi_par[0][0]) // self.binning[0] + pixel_num_h = (self._roi_par[1][1] - self._roi_par[1][0]) // self.binning[1] + pixel_size_v = pixel_size_v_0 * self.binning[0] + pixel_size_h = pixel_size_h_0 * self.binning[1] + + ''' + Parse the angles file .ang or _ctdata.txt file and returns the angles + as an numpy array. + ''' + input_path = os.path.dirname(self.xtek_file) + angles_ctdata_file = os.path.join(input_path, '_ctdata.txt') + angles_named_file = os.path.join(input_path, self._experiment_name+'.ang') + angles = numpy.zeros(num_projections, dtype = 'float') + + # look for _ctdata.txt + if os.path.exists(angles_ctdata_file): + # read txt file with angles + with open(angles_ctdata_file) as f: + content = f.readlines() + # skip firt three lines + # read the middle value of 3 values in each line as angles in degrees + index = 0 + for line in content[3:]: + angles[index] = float(line.split(' ')[1]) + index += 1 + angles = angles + initial_angle + + # look for ang file + elif os.path.exists(angles_named_file): + # read the angles file which is text with first line as header + with open(angles_named_file) as f: + content = f.readlines() + # skip first line + index = 0 + for line in content[1:]: + angles[index] = float(line.split(':')[1]) + index += 1 + angles = numpy.flipud(angles + initial_angle) # angles are in the reverse order + + else: # calculate angles based on xtek file + angles = initial_angle + angular_step * range(num_projections) + + # fill in metadata + self._ag = AcquisitionGeometry(geom_type = 'cone', + dimension = '3D', + angles = angles, + pixel_num_h = pixel_num_h, + pixel_size_h = pixel_size_h, + pixel_num_v = pixel_num_v, + pixel_size_v = pixel_size_v, + dist_source_center = source_x, + dist_center_detector = detector_x - source_x, + channels = 1, + angle_unit = 'degree') + + def get_geometry(self): + + ''' + Return AcquisitionGeometry object + ''' + + return self._ag + + def load_projections(self): + + ''' + Load projections and return AcquisitionData container + ''' + + # get path to projections + path_projection = os.path.dirname(self.xtek_file) + + # get number of projections + num_projections = numpy.shape(self._ag.angles)[0] + + # allocate array to store projections + data = numpy.zeros((num_projections, self._ag.pixel_num_v, self._ag.pixel_num_h), dtype = float) + + for i in range(num_projections): + + filename = (path_projection + '/' + self._experiment_name + '_{:04d}.tif').format(i + 1) + + try: + tmp = numpy.asarray(Image.open(filename), dtype = float) + except: + print('Error reading\n {}\n file.'.format(filename)) + raise + + if (self.binning == [1, 1]): + data[i, :, :] = tmp[self._roi_par[0][0]:self._roi_par[0][1], self._roi_par[1][0]:self._roi_par[1][1]] + else: + shape = (self._ag.pixel_num_v, self.binning[0], + self._ag.pixel_num_h, self.binning[1]) + data[i, :, :] = tmp[self._roi_par[0][0]:(self._roi_par[0][0] + (((self._roi_par[0][1] - self._roi_par[0][0]) // self.binning[0]) * self.binning[0])), \ + self._roi_par[1][0]:(self._roi_par[1][0] + (((self._roi_par[1][1] - self._roi_par[1][0]) // self.binning[1]) * self.binning[1]))].reshape(shape).mean(-1).mean(1) + + if (self.normalize): + data /= self._white_level + data[data > 1] = 1 + + if self.flip: + return AcquisitionData(array = data[:, :, ::-1], + deep_copy = False, + geometry = self._ag, + dimension_labels = ['angle', \ + 'vertical', \ + 'horizontal']) + else: + return AcquisitionData(array = data, + deep_copy = False, + geometry = self._ag, + dimension_labels = ['angle', \ + 'vertical', \ + 'horizontal']) + + +''' +# usage example +xtek_file = '/home/evelina/nikon_data/SophiaBeads_256_averaged.xtekct' +reader = NikonDataReader() +reader.set_up(xtek_file = xtek_file, + binning = [1, 1], + roi = -1, + normalize = True, + flip = True) + +data = reader.load_projections() +print(data) +ag = reader.get_geometry() +print(ag) + +plt.imshow(data.as_array()[1, :, :]) +plt.show() +''' diff --git a/Wrappers/Python/ccpi/io/__init__.py b/Wrappers/Python/ccpi/io/__init__.py index 9233d7a..455faba 100644 --- a/Wrappers/Python/ccpi/io/__init__.py +++ b/Wrappers/Python/ccpi/io/__init__.py @@ -15,4 +15,8 @@ # 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.
\ No newline at end of file +# limitations under the License.
+
+from .NEXUSDataReader import NEXUSDataReader
+from .NEXUSDataWriter import NEXUSDataWriter
+from .NikonDataReader import NikonDataReader
diff --git a/Wrappers/Python/ccpi/io/reader.py b/Wrappers/Python/ccpi/io/reader.py index 856f5e0..07e3bf9 100644 --- a/Wrappers/Python/ccpi/io/reader.py +++ b/Wrappers/Python/ccpi/io/reader.py @@ -241,26 +241,37 @@ class NexusReader(object): pass
dims = file[self.data_path].shape
if ymin is None and ymax is None:
- data = np.array(file[self.data_path])
+
+ try:
+ image_keys = self.get_image_keys()
+ print ("image_keys", image_keys)
+ projections = np.array(file[self.data_path])
+ data = projections[image_keys==0]
+ except KeyError as ke:
+ print (ke)
+ data = np.array(file[self.data_path])
+
else:
+ image_keys = self.get_image_keys()
+ print ("image_keys", image_keys)
+ projections = np.array(file[self.data_path])[image_keys==0]
if ymin is None:
ymin = 0
if ymax > dims[1]:
raise ValueError('ymax out of range')
- data = np.array(file[self.data_path][:,:ymax,:])
+ data = projections[:,:ymax,:]
elif ymax is None:
ymax = dims[1]
if ymin < 0:
raise ValueError('ymin out of range')
- data = np.array(file[self.data_path][:,ymin:,:])
+ data = projections[:,ymin:,:]
else:
if ymax > dims[1]:
raise ValueError('ymax out of range')
if ymin < 0:
raise ValueError('ymin out of range')
- data = np.array(file[self.data_path]
- [: , ymin:ymax , :] )
+ data = projections[: , ymin:ymax , :]
except:
print("Error reading nexus file")
diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/Algorithm.py b/Wrappers/Python/ccpi/optimisation/algorithms/Algorithm.py index 680b268..c62d0ea 100755 --- a/Wrappers/Python/ccpi/optimisation/algorithms/Algorithm.py +++ b/Wrappers/Python/ccpi/optimisation/algorithms/Algorithm.py @@ -16,7 +16,7 @@ # 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. -import time +import time, functools from numbers import Integral class Algorithm(object): @@ -34,7 +34,7 @@ class Algorithm(object): method will stop when the stopping cryterion is met. ''' - def __init__(self): + def __init__(self, **kwargs): '''Constructor Set the minimal number of parameters: @@ -48,11 +48,12 @@ class Algorithm(object): when evaluating the objective is computationally expensive. ''' self.iteration = 0 - self.__max_iteration = 0 + self.__max_iteration = kwargs.get('max_iteration', 0) self.__loss = [] self.memopt = False + self.configured = False self.timing = [] - self.update_objective_interval = 1 + self.update_objective_interval = kwargs.get('update_objective_interval', 1) def set_up(self, *args, **kwargs): '''Set up the algorithm''' raise NotImplementedError() @@ -86,14 +87,18 @@ class Algorithm(object): raise StopIteration() else: time0 = time.time() + if not self.configured: + raise ValueError('Algorithm not configured correctly. Please run set_up.') self.update() self.timing.append( time.time() - time0 ) if self.iteration % self.update_objective_interval == 0: self.update_objective() self.iteration += 1 + def get_output(self): '''Returns the solution found''' return self.x + def get_last_loss(self): '''Returns the last stored value of the loss function @@ -140,18 +145,64 @@ class Algorithm(object): raise ValueError('Update objective interval must be an integer >= 1') else: raise ValueError('Update objective interval must be an integer >= 1') - def run(self, iterations, verbose=False, callback=None): + def run(self, iterations, verbose=True, callback=None): '''run n iterations and update the user with the callback if specified''' if self.should_stop(): print ("Stop cryterion has been reached.") i = 0 for _ in self: - if verbose: - print ("Iteration {}/{}, objective {}".format(self.iteration, - self.max_iteration, self.get_last_objective()) ) - if callback is not None: - callback(self.iteration, self.get_last_objective()) + if i == 0 and verbose: + print (self.verbose_header()) + if (self.iteration -1) % self.update_objective_interval == 0: + if verbose: + print (self.verbose_output()) + if callback is not None: + callback(self.iteration -1, self.get_last_objective(), self.x) i += 1 if i == iterations: break - + + def verbose_output(self): + '''Creates a nice tabulated output''' + timing = self.timing[-self.update_objective_interval-1:-1] + if len (timing) == 0: + t = 0 + else: + t = sum(timing)/len(timing) + out = "{:>9} {:>10} {:>13} {}".format( + self.iteration-1, + self.max_iteration, + "{:.3f}".format(t), + self.objective_to_string() + ) + return out + + def objective_to_string(self): + el = self.get_last_objective() + if type(el) == list: + string = functools.reduce(lambda x,y: x+' {:>13.5e}'.format(y), el[:-1],'') + string += '{:>15.5e}'.format(el[-1]) + else: + string = "{:>20.5e}".format(el) + return string + def verbose_header(self): + el = self.get_last_objective() + if type(el) == list: + out = "{:>9} {:>10} {:>13} {:>13} {:>13} {:>15}\n".format('Iter', + 'Max Iter', + 'Time/Iter', + 'Primal' , 'Dual', 'Primal-Dual') + out += "{:>9} {:>10} {:>13} {:>13} {:>13} {:>15}".format('', + '', + '[s]', + 'Objective' , 'Objective', 'Gap') + else: + out = "{:>9} {:>10} {:>13} {:>20}\n".format('Iter', + 'Max Iter', + 'Time/Iter', + 'Objective') + out += "{:>9} {:>10} {:>13} {:>20}".format('', + '', + '[s]', + '') + return out diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/CGLS.py b/Wrappers/Python/ccpi/optimisation/algorithms/CGLS.py index 7194eb8..15acc31 100755 --- a/Wrappers/Python/ccpi/optimisation/algorithms/CGLS.py +++ b/Wrappers/Python/ccpi/optimisation/algorithms/CGLS.py @@ -23,7 +23,9 @@ Created on Thu Feb 21 11:11:23 2019 """ from ccpi.optimisation.algorithms import Algorithm -#from collections.abc import Iterable +from ccpi.optimisation.functions import Norm2Sq +import numpy + class CGLS(Algorithm): '''Conjugate Gradient Least Squares algorithm @@ -48,20 +50,26 @@ class CGLS(Algorithm): def set_up(self, x_init, operator , data ): self.r = data.copy() - self.x = x_init.copy() + self.x = x_init * 0 self.operator = operator self.d = operator.adjoint(self.r) self.normr2 = self.d.squared_norm() + + self.s = self.operator.domain_geometry().allocate() #if isinstance(self.normr2, Iterable): # self.normr2 = sum(self.normr2) #self.normr2 = numpy.sqrt(self.normr2) #print ("set_up" , self.normr2) + n = Norm2Sq(operator, self.data) + self.loss.append(n(x_init)) + self.configured = True def update(self): - + self.update_new() + def update_old(self): Ad = self.operator.direct(self.d) #norm = (Ad*Ad).sum() #if isinstance(norm, Iterable): @@ -83,5 +91,44 @@ class CGLS(Algorithm): self.normr2 = normr2_new self.d = s + beta*self.d + def update_new(self): + + Ad = self.operator.direct(self.d) + norm = Ad.squared_norm() + if norm == 0.: + print ('norm = 0, cannot update solution') + print ("self.d norm", self.d.squared_norm(), self.d.as_array()) + raise StopIteration() + alpha = self.normr2/norm + if alpha == 0.: + print ('alpha = 0, cannot update solution') + raise StopIteration() + self.d *= alpha + Ad *= alpha + self.r -= Ad + + self.x += self.d + + self.operator.adjoint(self.r, out=self.s) + s = self.s + + normr2_new = s.squared_norm() + + beta = normr2_new/self.normr2 + self.normr2 = normr2_new + self.d *= (beta/alpha) + self.d += s + def update_objective(self): - self.loss.append(self.r.squared_norm())
\ No newline at end of file + a = self.r.squared_norm() + if a is numpy.nan: + raise StopIteration() + self.loss.append(a) + +# def should_stop(self): +# if self.iteration > 0: +# x = self.get_last_objective() +# a = x > 0 +# return self.max_iteration_stop_cryterion() or (not a) +# else: +# return False diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/FBPD.py b/Wrappers/Python/ccpi/optimisation/algorithms/FBPD.py deleted file mode 100644 index 798fb61..0000000 --- a/Wrappers/Python/ccpi/optimisation/algorithms/FBPD.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- -# This work is part of the Core Imaging Library developed by -# Visual Analytics and Imaging System Group of the Science Technology -# Facilities Council, STFC - -# Copyright 2019 Edoardo Pasca - -# 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. -""" -Created on Thu Feb 21 11:09:03 2019 - -@author: ofn77899 -""" - -from ccpi.optimisation.algorithms import Algorithm -from ccpi.optimisation.funcs import ZeroFun - -class FBPD(Algorithm): - '''FBPD Algorithm - - Parameters: - x_init: initial guess - f: constraint - g: data fidelity - h: regularizer - opt: additional algorithm - ''' - constraint = None - data_fidelity = None - regulariser = None - def __init__(self, **kwargs): - pass - def set_up(self, x_init, operator=None, constraint=None, data_fidelity=None,\ - regulariser=None, opt=None): - - # default inputs - if constraint is None: - self.constraint = ZeroFun() - else: - self.constraint = constraint - if data_fidelity is None: - data_fidelity = ZeroFun() - else: - self.data_fidelity = data_fidelity - if regulariser is None: - self.regulariser = ZeroFun() - else: - self.regulariser = regulariser - - # algorithmic parameters - - - # step-sizes - self.tau = 2 / (self.data_fidelity.L + 2) - self.sigma = (1/self.tau - self.data_fidelity.L/2) / self.regulariser.L - - self.inv_sigma = 1/self.sigma - - # initialization - self.x = x_init - self.y = operator.direct(self.x) - - - def update(self): - - # primal forward-backward step - x_old = self.x - self.x = self.x - self.tau * ( self.data_fidelity.grad(self.x) + self.operator.adjoint(self.y) ) - self.x = self.constraint.prox(self.x, self.tau); - - # dual forward-backward step - self.y = self.y + self.sigma * self.operator.direct(2*self.x - x_old); - self.y = self.y - self.sigma * self.regulariser.prox(self.inv_sigma*self.y, self.inv_sigma); - - # time and criterion - self.loss = self.constraint(self.x) + self.data_fidelity(self.x) + self.regulariser(self.operator.direct(self.x)) diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/FISTA.py b/Wrappers/Python/ccpi/optimisation/algorithms/FISTA.py index bc4489e..c8fd0d4 100755 --- a/Wrappers/Python/ccpi/optimisation/algorithms/FISTA.py +++ b/Wrappers/Python/ccpi/optimisation/algorithms/FISTA.py @@ -6,7 +6,7 @@ Created on Thu Feb 21 11:07:30 2019 """ from ccpi.optimisation.algorithms import Algorithm -from ccpi.optimisation.funcs import ZeroFun +from ccpi.optimisation.functions import ZeroFunction import numpy class FISTA(Algorithm): @@ -20,102 +20,65 @@ class FISTA(Algorithm): x_init: initial guess f: data fidelity g: regularizer - h: - opt: additional algorithm + opt: additional options ''' - + + def __init__(self, **kwargs): '''initialisation can be done at creation time if all proper variables are passed or later with set_up''' super(FISTA, self).__init__() - self.f = None - self.g = None + self.f = kwargs.get('f', None) + self.g = kwargs.get('g', ZeroFunction()) + self.x_init = kwargs.get('x_init',None) self.invL = None self.t_old = 1 - args = ['x_init', 'f', 'g', 'opt'] - for k,v in kwargs.items(): - if k in args: - args.pop(args.index(k)) - if len(args) == 0: - return self.set_up(kwargs['x_init'], - f=kwargs['f'], - g=kwargs['g'], - opt=kwargs['opt']) + if self.x_init is not None and \ + self.f is not None and self.g is not None: + print ("FISTA set_up called from creator") + self.set_up(self.x_init, self.f, self.g) + - def set_up(self, x_init, f=None, g=None, opt=None): + def set_up(self, x_init, f, g, opt=None, **kwargs): - # default inputs - if f is None: - self.f = ZeroFun() - else: - self.f = f - if g is None: - g = ZeroFun() - self.g = g - else: - self.g = g + self.f = f + self.g = g # algorithmic parameters if opt is None: - opt = {'tol': 1e-4, 'memopt':False} - - self.tol = opt['tol'] if 'tol' in opt.keys() else 1e-4 - memopt = opt['memopt'] if 'memopt' in opt.keys() else False - self.memopt = memopt - - # initialization - if memopt: - self.y = x_init.clone() - self.x_old = x_init.clone() - self.x = x_init.clone() - self.u = x_init.clone() - else: - self.x_old = x_init.copy() - self.y = x_init.copy() - - #timing = numpy.zeros(max_iter) - #criter = numpy.zeros(max_iter) + opt = {'tol': 1e-4} - + self.y = x_init.copy() + self.x_old = x_init.copy() + self.x = x_init.copy() + self.u = x_init.copy() + + self.invL = 1/f.L self.t_old = 1 + self.update_objective() + self.configured = True def update(self): - # algorithm loop - #for it in range(0, max_iter): - - if self.memopt: - # u = y - invL*f.grad(y) - # store the result in x_old - self.f.gradient(self.y, out=self.u) - self.u.__imul__( -self.invL ) - self.u.__iadd__( self.y ) - # x = g.prox(u,invL) - self.g.proximal(self.u, self.invL, out=self.x) - - self.t = 0.5*(1 + numpy.sqrt(1 + 4*(self.t_old**2))) - - # y = x + (t_old-1)/t*(x-x_old) - self.x.subtract(self.x_old, out=self.y) - self.y.__imul__ ((self.t_old-1)/self.t) - self.y.__iadd__( self.x ) - - self.x_old.fill(self.x) - self.t_old = self.t - - - else: - u = self.y - self.invL*self.f.grad(self.y) - - self.x = self.g.prox(u,self.invL) - - self.t = 0.5*(1 + numpy.sqrt(1 + 4*(self.t_old**2))) - - self.y = self.x + (self.t_old-1)/self.t*(self.x-self.x_old) - - self.x_old = self.x.copy() - self.t_old = self.t + + self.f.gradient(self.y, out=self.u) + self.u.__imul__( -self.invL ) + self.u.__iadd__( self.y ) + # x = g.prox(u,invL) + self.g.proximal(self.u, self.invL, out=self.x) + + self.t = 0.5*(1 + numpy.sqrt(1 + 4*(self.t_old**2))) + +# self.x.subtract(self.x_old, out=self.y) + self.y = self.x - self.x_old + self.y.__imul__ ((self.t_old-1)/self.t) + self.y.__iadd__( self.x ) + + self.x_old.fill(self.x) + self.t_old = self.t def update_objective(self): - self.loss.append( self.f(self.x) + self.g(self.x) )
\ No newline at end of file + self.loss.append( self.f(self.x) + self.g(self.x) ) + + diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/GradientDescent.py b/Wrappers/Python/ccpi/optimisation/algorithms/GradientDescent.py index 7794b4d..34bf954 100755 --- a/Wrappers/Python/ccpi/optimisation/algorithms/GradientDescent.py +++ b/Wrappers/Python/ccpi/optimisation/algorithms/GradientDescent.py @@ -40,7 +40,7 @@ class GradientDescent(Algorithm): if k in args: args.pop(args.index(k)) if len(args) == 0: - return self.set_up(x_init=kwargs['x_init'], + self.set_up(x_init=kwargs['x_init'], objective_function=kwargs['objective_function'], rate=kwargs['rate']) @@ -51,13 +51,18 @@ class GradientDescent(Algorithm): def set_up(self, x_init, objective_function, rate): '''initialisation of the algorithm''' self.x = x_init.copy() - if self.memopt: - self.x_update = x_init.copy() self.objective_function = objective_function self.rate = rate self.loss.append(objective_function(x_init)) self.iteration = 0 - + try: + self.memopt = self.objective_function.memopt + except AttributeError as ae: + self.memopt = False + if self.memopt: + self.x_update = x_init.copy() + self.configured = True + def update(self): '''Single iteration''' if self.memopt: @@ -65,8 +70,8 @@ class GradientDescent(Algorithm): self.x_update *= -self.rate self.x += self.x_update else: - self.x += -self.rate * self.objective_function.grad(self.x) + self.x += -self.rate * self.objective_function.gradient(self.x) def update_objective(self): self.loss.append(self.objective_function(self.x)) -
\ No newline at end of file + diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/PDHG.py b/Wrappers/Python/ccpi/optimisation/algorithms/PDHG.py new file mode 100644 index 0000000..3afd8b0 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/algorithms/PDHG.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Mon Feb 4 16:18:06 2019 + +@author: evangelos +""" +from ccpi.optimisation.algorithms import Algorithm +from ccpi.framework import ImageData, DataContainer +import numpy as np +import numpy +import time +from ccpi.optimisation.operators import BlockOperator +from ccpi.framework import BlockDataContainer +from ccpi.optimisation.functions import FunctionOperatorComposition + +class PDHG(Algorithm): + '''Primal Dual Hybrid Gradient''' + + def __init__(self, **kwargs): + super(PDHG, self).__init__(max_iteration=kwargs.get('max_iteration',0)) + self.f = kwargs.get('f', None) + self.operator = kwargs.get('operator', None) + self.g = kwargs.get('g', None) + self.tau = kwargs.get('tau', None) + self.sigma = kwargs.get('sigma', 1.) + + + if self.f is not None and self.operator is not None and \ + self.g is not None: + if self.tau is None: + # Compute operator Norm + normK = self.operator.norm() + # Primal & dual stepsizes + self.tau = 1/(self.sigma*normK**2) + print ("Calling from creator") + self.set_up(self.f, + self.g, + self.operator, + self.tau, + self.sigma) + + def set_up(self, f, g, operator, tau = None, sigma = None, opt = None, **kwargs): + # algorithmic parameters + self.operator = operator + self.f = f + self.g = g + self.tau = tau + self.sigma = sigma + self.opt = opt + if sigma is None and tau is None: + raise ValueError('Need sigma*tau||K||^2<1') + + self.x_old = self.operator.domain_geometry().allocate() + self.x_tmp = self.x_old.copy() + self.x = self.x_old.copy() + + self.y_old = self.operator.range_geometry().allocate() + self.y_tmp = self.y_old.copy() + self.y = self.y_old.copy() + + self.xbar = self.x_old.copy() + + # relaxation parameter + self.theta = 1 + self.update_objective() + self.configured = True + + def update(self): + + # Gradient descent, Dual problem solution + self.operator.direct(self.xbar, out=self.y_tmp) + self.y_tmp *= self.sigma + self.y_tmp += self.y_old + + #self.y = self.f.proximal_conjugate(self.y_old, self.sigma) + self.f.proximal_conjugate(self.y_tmp, self.sigma, out=self.y) + + # Gradient ascent, Primal problem solution + self.operator.adjoint(self.y, out=self.x_tmp) + self.x_tmp *= -1*self.tau + self.x_tmp += self.x_old + + + self.g.proximal(self.x_tmp, self.tau, out=self.x) + + #Update + self.x.subtract(self.x_old, out=self.xbar) + self.xbar *= self.theta + self.xbar += self.x + + self.x_old.fill(self.x) + self.y_old.fill(self.y) + + def update_objective(self): + + p1 = self.f(self.operator.direct(self.x)) + self.g(self.x) + d1 = -(self.f.convex_conjugate(self.y) + self.g.convex_conjugate(-1*self.operator.adjoint(self.y))) + + self.loss.append([p1,d1,p1-d1]) + + + +def PDHG_old(f, g, operator, tau = None, sigma = None, opt = None, **kwargs): + + # algorithmic parameters + if opt is None: + opt = {'tol': 1e-6, 'niter': 500, 'show_iter': 100, \ + 'memopt': False} + + if sigma is None and tau is None: + raise ValueError('Need sigma*tau||K||^2<1') + + niter = opt['niter'] if 'niter' in opt.keys() else 1000 + tol = opt['tol'] if 'tol' in opt.keys() else 1e-4 + memopt = opt['memopt'] if 'memopt' in opt.keys() else False + show_iter = opt['show_iter'] if 'show_iter' in opt.keys() else False + stop_crit = opt['stop_crit'] if 'stop_crit' in opt.keys() else False + + x_old = operator.domain_geometry().allocate() + y_old = operator.range_geometry().allocate() + + xbar = x_old.copy() + x_tmp = x_old.copy() + x = x_old.copy() + + y_tmp = y_old.copy() + y = y_tmp.copy() + + + # relaxation parameter + theta = 1 + + t = time.time() + + primal = [] + dual = [] + pdgap = [] + + + for i in range(niter): + + + + if memopt: + operator.direct(xbar, out = y_tmp) + y_tmp *= sigma + y_tmp += y_old + else: + y_tmp = y_old + sigma * operator.direct(xbar) + + f.proximal_conjugate(y_tmp, sigma, out=y) + + if memopt: + operator.adjoint(y, out = x_tmp) + x_tmp *= -1*tau + x_tmp += x_old + else: + x_tmp = x_old - tau*operator.adjoint(y) + + g.proximal(x_tmp, tau, out=x) + + x.subtract(x_old, out=xbar) + xbar *= theta + xbar += x + + x_old.fill(x) + y_old.fill(y) + + if i%10==0: + + p1 = f(operator.direct(x)) + g(x) + d1 = - ( f.convex_conjugate(y) + g.convex_conjugate(-1*operator.adjoint(y)) ) + primal.append(p1) + dual.append(d1) + pdgap.append(p1-d1) + print(p1, d1, p1-d1) + + + + t_end = time.time() + + return x, t_end - t, primal, dual, pdgap + + + diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/SIRT.py b/Wrappers/Python/ccpi/optimisation/algorithms/SIRT.py new file mode 100644 index 0000000..c73d323 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/algorithms/SIRT.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018 Edoardo Pasca + +# 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. + +from ccpi.optimisation.algorithms import Algorithm + +class SIRT(Algorithm): + + '''Simultaneous Iterative Reconstruction Technique + + Parameters: + x_init: initial guess + operator: operator for forward/backward projections + data: data to operate on + constraint: Function with prox-method, for example IndicatorBox to + enforce box constraints, default is None). + ''' + def __init__(self, **kwargs): + super(SIRT, self).__init__() + self.x = kwargs.get('x_init', None) + self.operator = kwargs.get('operator', None) + self.data = kwargs.get('data', None) + self.constraint = kwargs.get('constraint', None) + if self.x is not None and self.operator is not None and \ + self.data is not None: + print ("Calling from creator") + self.set_up(x_init=kwargs['x_init'], + operator=kwargs['operator'], + data=kwargs['data'], + constraint=kwargs['constraint']) + + def set_up(self, x_init, operator , data, constraint=None ): + + self.x = x_init.copy() + self.operator = operator + self.data = data + self.constraint = constraint + + self.r = data.copy() + + self.relax_par = 1.0 + + # Set up scaling matrices D and M. + self.M = 1/self.operator.direct(self.operator.domain_geometry().allocate(value=1.0)) + self.D = 1/self.operator.adjoint(self.operator.range_geometry().allocate(value=1.0)) + self.configured = True + + + def update(self): + + self.r = self.data - self.operator.direct(self.x) + + self.x += self.relax_par * (self.D*self.operator.adjoint(self.M*self.r)) + + if self.constraint is not None: + self.x = self.constraint.proximal(self.x,None) + # self.constraint.proximal(self.x,None, out=self.x) + + def update_objective(self): + self.loss.append(self.r.squared_norm()) diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/__init__.py b/Wrappers/Python/ccpi/optimisation/algorithms/__init__.py index 903bc30..8f255f3 100644 --- a/Wrappers/Python/ccpi/optimisation/algorithms/__init__.py +++ b/Wrappers/Python/ccpi/optimisation/algorithms/__init__.py @@ -24,6 +24,8 @@ Created on Thu Feb 21 11:03:13 2019 from .Algorithm import Algorithm from .CGLS import CGLS +from .SIRT import SIRT from .GradientDescent import GradientDescent from .FISTA import FISTA -from .FBPD import FBPD +from .PDHG import PDHG + diff --git a/Wrappers/Python/ccpi/optimisation/algs.py b/Wrappers/Python/ccpi/optimisation/algs.py deleted file mode 100755 index 15638a9..0000000 --- a/Wrappers/Python/ccpi/optimisation/algs.py +++ /dev/null @@ -1,319 +0,0 @@ -# -*- coding: utf-8 -*- -# This work is part of the Core Imaging Library developed by -# Visual Analytics and Imaging System Group of the Science Technology -# Facilities Council, STFC - -# Copyright 2018 Jakob Jorgensen, Daniil Kazantsev and Edoardo Pasca - -# 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. - -import numpy -import time - -from ccpi.optimisation.funcs import Function -from ccpi.optimisation.funcs import ZeroFun -from ccpi.framework import ImageData -from ccpi.framework import AcquisitionData -from ccpi.optimisation.spdhg import spdhg -from ccpi.optimisation.spdhg import KullbackLeibler -from ccpi.optimisation.spdhg import KullbackLeiblerConvexConjugate - -def FISTA(x_init, f=None, g=None, opt=None): - '''Fast Iterative Shrinkage-Thresholding Algorithm - - Beck, A. and Teboulle, M., 2009. A fast iterative shrinkage-thresholding - algorithm for linear inverse problems. - SIAM journal on imaging sciences,2(1), pp.183-202. - - Parameters: - x_init: initial guess - f: data fidelity - g: regularizer - h: - opt: additional algorithm - ''' - # default inputs - if f is None: f = ZeroFun() - if g is None: g = ZeroFun() - - # algorithmic parameters - if opt is None: - opt = {'tol': 1e-4, 'iter': 1000, 'memopt':False} - - max_iter = opt['iter'] if 'iter' in opt.keys() else 1000 - tol = opt['tol'] if 'tol' in opt.keys() else 1e-4 - memopt = opt['memopt'] if 'memopt' in opt.keys() else False - - - # initialization - if memopt: - y = x_init.clone() - x_old = x_init.clone() - x = x_init.clone() - u = x_init.clone() - else: - x_old = x_init - y = x_init; - - timing = numpy.zeros(max_iter) - criter = numpy.zeros(max_iter) - - invL = 1/f.L - - t_old = 1 - - c = f(x_init) + g(x_init) - - # algorithm loop - for it in range(0, max_iter): - - time0 = time.time() - if memopt: - # u = y - invL*f.grad(y) - # store the result in x_old - f.gradient(y, out=u) - u.__imul__( -invL ) - u.__iadd__( y ) - # x = g.prox(u,invL) - g.proximal(u, invL, out=x) - - t = 0.5*(1 + numpy.sqrt(1 + 4*(t_old**2))) - - # y = x + (t_old-1)/t*(x-x_old) - x.subtract(x_old, out=y) - y.__imul__ ((t_old-1)/t) - y.__iadd__( x ) - - x_old.fill(x) - t_old = t - - - else: - u = y - invL*f.grad(y) - - x = g.prox(u,invL) - - t = 0.5*(1 + numpy.sqrt(1 + 4*(t_old**2))) - - y = x + (t_old-1)/t*(x-x_old) - - x_old = x.copy() - t_old = t - - # time and criterion - timing[it] = time.time() - time0 - criter[it] = f(x) + g(x); - - # stopping rule - #if np.linalg.norm(x - x_old) < tol * np.linalg.norm(x_old) and it > 10: - # break - - #print(it, 'out of', 10, 'iterations', end='\r'); - - #criter = criter[0:it+1]; - timing = numpy.cumsum(timing[0:it+1]); - - return x, it, timing, criter - -def FBPD(x_init, operator=None, constraint=None, data_fidelity=None,\ - regulariser=None, opt=None): - '''FBPD Algorithm - - Parameters: - x_init: initial guess - f: constraint - g: data fidelity - h: regularizer - opt: additional algorithm - ''' - # default inputs - if constraint is None: constraint = ZeroFun() - if data_fidelity is None: data_fidelity = ZeroFun() - if regulariser is None: regulariser = ZeroFun() - - # algorithmic parameters - if opt is None: - opt = {'tol': 1e-4, 'iter': 1000} - else: - try: - max_iter = opt['iter'] - except KeyError as ke: - opt[ke] = 1000 - try: - opt['tol'] = 1000 - except KeyError as ke: - opt[ke] = 1e-4 - tol = opt['tol'] - max_iter = opt['iter'] - memopt = opt['memopts'] if 'memopts' in opt.keys() else False - - # step-sizes - tau = 2 / (data_fidelity.L + 2) - sigma = (1/tau - data_fidelity.L/2) / regulariser.L - inv_sigma = 1/sigma - - # initialization - x = x_init - y = operator.direct(x); - - timing = numpy.zeros(max_iter) - criter = numpy.zeros(max_iter) - - - - - # algorithm loop - for it in range(0, max_iter): - - t = time.time() - - # primal forward-backward step - x_old = x; - x = x - tau * ( data_fidelity.grad(x) + operator.adjoint(y) ); - x = constraint.prox(x, tau); - - # dual forward-backward step - y = y + sigma * operator.direct(2*x - x_old); - y = y - sigma * regulariser.prox(inv_sigma*y, inv_sigma); - - # time and criterion - timing[it] = time.time() - t - criter[it] = constraint(x) + data_fidelity(x) + regulariser(operator.direct(x)) - - # stopping rule - #if np.linalg.norm(x - x_old) < tol * np.linalg.norm(x_old) and it > 10: - # break - - criter = criter[0:it+1] - timing = numpy.cumsum(timing[0:it+1]) - - return x, it, timing, criter - -def CGLS(x_init, operator , data , opt=None): - '''Conjugate Gradient Least Squares algorithm - - Parameters: - x_init: initial guess - operator: operator for forward/backward projections - data: data to operate on - opt: additional algorithm - ''' - - if opt is None: - opt = {'tol': 1e-4, 'iter': 1000} - else: - try: - max_iter = opt['iter'] - except KeyError as ke: - opt[ke] = 1000 - try: - opt['tol'] = 1000 - except KeyError as ke: - opt[ke] = 1e-4 - tol = opt['tol'] - max_iter = opt['iter'] - - r = data.copy() - x = x_init.copy() - - d = operator.adjoint(r) - - normr2 = (d**2).sum() - - timing = numpy.zeros(max_iter) - criter = numpy.zeros(max_iter) - - # algorithm loop - for it in range(0, max_iter): - - t = time.time() - - Ad = operator.direct(d) - alpha = normr2/( (Ad**2).sum() ) - x = x + alpha*d - r = r - alpha*Ad - s = operator.adjoint(r) - - normr2_new = (s**2).sum() - beta = normr2_new/normr2 - normr2 = normr2_new - d = s + beta*d - - # time and criterion - timing[it] = time.time() - t - criter[it] = (r**2).sum() - - return x, it, timing, criter - -def SIRT(x_init, operator , data , opt=None, constraint=None): - '''Simultaneous Iterative Reconstruction Technique - - Parameters: - x_init: initial guess - operator: operator for forward/backward projections - data: data to operate on - opt: additional algorithm - constraint: func of Indicator type specifying convex constraint. - ''' - - if opt is None: - opt = {'tol': 1e-4, 'iter': 1000} - else: - try: - max_iter = opt['iter'] - except KeyError as ke: - opt[ke] = 1000 - try: - opt['tol'] = 1000 - except KeyError as ke: - opt[ke] = 1e-4 - tol = opt['tol'] - max_iter = opt['iter'] - - # Set default constraint to unconstrained - if constraint==None: - constraint = Function() - - x = x_init.clone() - - timing = numpy.zeros(max_iter) - criter = numpy.zeros(max_iter) - - # Relaxation parameter must be strictly between 0 and 2. For now fix at 1.0 - relax_par = 1.0 - - # Set up scaling matrices D and M. - im1 = ImageData(geometry=x_init.geometry) - im1.array[:] = 1.0 - M = 1/operator.direct(im1) - del im1 - aq1 = AcquisitionData(geometry=M.geometry) - aq1.array[:] = 1.0 - D = 1/operator.adjoint(aq1) - del aq1 - - # algorithm loop - for it in range(0, max_iter): - t = time.time() - r = data - operator.direct(x) - - x = constraint.prox(x + relax_par * (D*operator.adjoint(M*r)),None) - - timing[it] = time.time() - t - if it > 0: - criter[it-1] = (r**2).sum() - - r = data - operator.direct(x) - criter[it] = (r**2).sum() - return x, it, timing, criter - diff --git a/Wrappers/Python/ccpi/optimisation/funcs.py b/Wrappers/Python/ccpi/optimisation/funcs.py deleted file mode 100755 index 47ee810..0000000 --- a/Wrappers/Python/ccpi/optimisation/funcs.py +++ /dev/null @@ -1,302 +0,0 @@ -# -*- coding: utf-8 -*- -# This work is part of the Core Imaging Library developed by -# Visual Analytics and Imaging System Group of the Science Technology -# Facilities Council, STFC - -# Copyright 2018 Jakob Jorgensen, Daniil Kazantsev and Edoardo Pasca - -# 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. - -from ccpi.optimisation.ops import Identity, FiniteDiff2D -import numpy -from ccpi.framework import DataContainer - - -def isSizeCorrect(data1 ,data2): - if issubclass(type(data1), DataContainer) and \ - issubclass(type(data2), DataContainer): - # check dimensionality - if data1.check_dimensions(data2): - return True - elif issubclass(type(data1) , numpy.ndarray) and \ - issubclass(type(data2) , numpy.ndarray): - return data1.shape == data2.shape - else: - raise ValueError("{0}: getting two incompatible types: {1} {2}"\ - .format('Function', type(data1), type(data2))) - return False - -class Function(object): - def __init__(self): - self.L = None - def __call__(self,x, out=None): raise NotImplementedError - def grad(self, x): raise NotImplementedError - def prox(self, x, tau): raise NotImplementedError - def gradient(self, x, out=None): raise NotImplementedError - def proximal(self, x, tau, out=None): raise NotImplementedError - - -class Norm2(Function): - - def __init__(self, - gamma=1.0, - direction=None): - super(Norm2, self).__init__() - self.gamma = gamma; - self.direction = direction; - - def __call__(self, x, out=None): - - if out is None: - xx = numpy.sqrt(numpy.sum(numpy.square(x.as_array()), self.direction, - keepdims=True)) - else: - if isSizeCorrect(out, x): - # check dimensionality - if issubclass(type(out), DataContainer): - arr = out.as_array() - numpy.square(x.as_array(), out=arr) - xx = numpy.sqrt(numpy.sum(arr, self.direction, keepdims=True)) - - elif issubclass(type(out) , numpy.ndarray): - numpy.square(x.as_array(), out=out) - xx = numpy.sqrt(numpy.sum(out, self.direction, keepdims=True)) - else: - raise ValueError ('Wrong size: x{0} out{1}'.format(x.shape,out.shape) ) - - p = numpy.sum(self.gamma*xx) - - return p - - def prox(self, x, tau): - - xx = numpy.sqrt(numpy.sum( numpy.square(x.as_array()), self.direction, - keepdims=True )) - xx = numpy.maximum(0, 1 - tau*self.gamma / xx) - p = x.as_array() * xx - - return type(x)(p,geometry=x.geometry) - def proximal(self, x, tau, out=None): - if out is None: - return self.prox(x,tau) - else: - if isSizeCorrect(out, x): - # check dimensionality - if issubclass(type(out), DataContainer): - numpy.square(x.as_array(), out = out.as_array()) - xx = numpy.sqrt(numpy.sum( out.as_array() , self.direction, - keepdims=True )) - xx = numpy.maximum(0, 1 - tau*self.gamma / xx) - x.multiply(xx, out= out.as_array()) - - - elif issubclass(type(out) , numpy.ndarray): - numpy.square(x.as_array(), out=out) - xx = numpy.sqrt(numpy.sum(out, self.direction, keepdims=True)) - - xx = numpy.maximum(0, 1 - tau*self.gamma / xx) - x.multiply(xx, out= out) - else: - raise ValueError ('Wrong size: x{0} out{1}'.format(x.shape,out.shape) ) - - -class TV2D(Norm2): - - def __init__(self, gamma): - super(TV2D,self).__init__(gamma, 0) - self.op = FiniteDiff2D() - self.L = self.op.get_max_sing_val() - - -# Define a class for squared 2-norm -class Norm2sq(Function): - ''' - f(x) = c*||A*x-b||_2^2 - - which has - - grad[f](x) = 2*c*A^T*(A*x-b) - - and Lipschitz constant - - L = 2*c*||A||_2^2 = 2*s1(A)^2 - - where s1(A) is the largest singular value of A. - - ''' - - def __init__(self,A,b,c=1.0,memopt=False): - super(Norm2sq, self).__init__() - - self.A = A # Should be an operator, default identity - self.b = b # Default zero DataSet? - self.c = c # Default 1. - self.memopt = memopt - if memopt: - #self.direct_placehold = A.adjoint(b) - self.direct_placehold = A.allocate_direct() - self.adjoint_placehold = A.allocate_adjoint() - - - # Compute the Lipschitz parameter from the operator if possible - # Leave it initialised to None otherwise - try: - self.L = 2.0*self.c*(self.A.get_max_sing_val()**2) - except AttributeError as ae: - pass - - def grad(self,x): - #return 2*self.c*self.A.adjoint( self.A.direct(x) - self.b ) - return (2.0*self.c)*self.A.adjoint( self.A.direct(x) - self.b ) - - def __call__(self,x): - #return self.c* np.sum(np.square((self.A.direct(x) - self.b).ravel())) - #if out is None: - # return self.c*( ( (self.A.direct(x)-self.b)**2).sum() ) - #else: - y = self.A.direct(x) - y.__isub__(self.b) - #y.__imul__(y) - #return y.sum() * self.c - try: - return y.squared_norm() * self.c - except AttributeError as ae: - # added for compatibility with SIRF - return (y.norm()**2) * self.c - - def gradient(self, x, out = None): - if self.memopt: - #return 2.0*self.c*self.A.adjoint( self.A.direct(x) - self.b ) - - self.A.direct(x, out=self.adjoint_placehold) - self.adjoint_placehold.__isub__( self.b ) - self.A.adjoint(self.adjoint_placehold, out=self.direct_placehold) - self.direct_placehold.__imul__(2.0 * self.c) - # can this be avoided? - out.fill(self.direct_placehold) - else: - return self.grad(x) - - - -class ZeroFun(Function): - - def __init__(self,gamma=0,L=1): - self.gamma = gamma - self.L = L - super(ZeroFun, self).__init__() - - def __call__(self,x): - return 0 - - def prox(self,x,tau): - return x.copy() - - def proximal(self, x, tau, out=None): - if out is None: - return self.prox(x, tau) - else: - if isSizeCorrect(out, x): - # check dimensionality - if issubclass(type(out), DataContainer): - out.fill(x) - - elif issubclass(type(out) , numpy.ndarray): - out[:] = x - else: - raise ValueError ('Wrong size: x{0} out{1}' - .format(x.shape,out.shape) ) - -# A more interesting example, least squares plus 1-norm minimization. -# Define class to represent 1-norm including prox function -class Norm1(Function): - - def __init__(self,gamma): - super(Norm1, self).__init__() - self.gamma = gamma - self.L = 1 - self.sign_x = None - - def __call__(self,x,out=None): - if out is None: - return self.gamma*(x.abs().sum()) - else: - if not x.shape == out.shape: - raise ValueError('Norm1 Incompatible size:', - x.shape, out.shape) - x.abs(out=out) - return out.sum() * self.gamma - - def prox(self,x,tau): - return (x.abs() - tau*self.gamma).maximum(0) * x.sign() - - def proximal(self, x, tau, out=None): - if out is None: - return self.prox(x, tau) - else: - if isSizeCorrect(x,out): - # check dimensionality - if issubclass(type(out), DataContainer): - v = (x.abs() - tau*self.gamma).maximum(0) - x.sign(out=out) - out *= v - #out.fill(self.prox(x,tau)) - elif issubclass(type(out) , numpy.ndarray): - v = (x.abs() - tau*self.gamma).maximum(0) - out[:] = x.sign() - out *= v - #out[:] = self.prox(x,tau) - else: - raise ValueError ('Wrong size: x{0} out{1}'.format(x.shape,out.shape) ) - -# Box constraints indicator function. Calling returns 0 if argument is within -# the box. The prox operator is projection onto the box. Only implements one -# scalar lower and one upper as constraint on all elements. Should generalise -# to vectors to allow different constraints one elements. -class IndicatorBox(Function): - - def __init__(self,lower=-numpy.inf,upper=numpy.inf): - # Do nothing - self.lower = lower - self.upper = upper - super(IndicatorBox, self).__init__() - - def __call__(self,x): - - if (numpy.all(x.array>=self.lower) and - numpy.all(x.array <= self.upper) ): - val = 0 - else: - val = numpy.inf - return val - - def prox(self,x,tau=None): - return (x.maximum(self.lower)).minimum(self.upper) - - def proximal(self, x, tau, out=None): - if out is None: - return self.prox(x, tau) - else: - if not x.shape == out.shape: - raise ValueError('Norm1 Incompatible size:', - x.shape, out.shape) - #(x.abs() - tau*self.gamma).maximum(0) * x.sign() - x.abs(out = out) - out.__isub__(tau*self.gamma) - out.maximum(0, out=out) - if self.sign_x is None or not x.shape == self.sign_x.shape: - self.sign_x = x.sign() - else: - x.sign(out=self.sign_x) - - out.__imul__( self.sign_x ) diff --git a/Wrappers/Python/ccpi/optimisation/functions/BlockFunction.py b/Wrappers/Python/ccpi/optimisation/functions/BlockFunction.py new file mode 100644 index 0000000..3765685 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/functions/BlockFunction.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Mar 8 10:01:31 2019 + +@author: evangelos +""" + +from ccpi.optimisation.functions import Function +from ccpi.framework import BlockDataContainer +from numbers import Number + +class BlockFunction(Function): + + '''BlockFunction acts as a separable sum function, i.e., + + f = [f_1,...,f_n] + + f([x_1,...,x_n]) = f_1(x_1) + .... + f_n(x_n) + + ''' + def __init__(self, *functions): + + super(BlockFunction, self).__init__() + self.functions = functions + self.length = len(self.functions) + + + + + def __call__(self, x): + + '''Evaluates the BlockFunction at a BlockDataContainer x + + :param: x (BlockDataContainer): must have as many rows as self.length + + returns sum(f_i(x_i)) + ''' + + if self.length != x.shape[0]: + raise ValueError('BlockFunction and BlockDataContainer have incompatible size') + t = 0 + for i in range(x.shape[0]): + t += self.functions[i](x.get_item(i)) + return t + + def convex_conjugate(self, x): + + ''' Evaluate convex conjugate of BlockFunction at x + + returns sum(f_i^{*}(x_i)) + + ''' + t = 0 + for i in range(x.shape[0]): + t += self.functions[i].convex_conjugate(x.get_item(i)) + return t + + + def proximal_conjugate(self, x, tau, out = None): + + ''' Evaluate Proximal Operator of tau * f(\cdot) at x + + prox_{tau*f}(x) = sum_{i} prox_{tau*f_{i}}(x_{i}) + + + ''' + + if out is not None: + if isinstance(tau, Number): + for i in range(self.length): + self.functions[i].proximal_conjugate(x.get_item(i), tau, out=out.get_item(i)) + else: + for i in range(self.length): + self.functions[i].proximal_conjugate(x.get_item(i), tau.get_item(i),out=out.get_item(i)) + + else: + + out = [None]*self.length + if isinstance(tau, Number): + for i in range(self.length): + out[i] = self.functions[i].proximal_conjugate(x.get_item(i), tau) + else: + for i in range(self.length): + out[i] = self.functions[i].proximal_conjugate(x.get_item(i), tau.get_item(i)) + + return BlockDataContainer(*out) + + + def proximal(self, x, tau, out = None): + + ''' Evaluate Proximal Operator of tau * f^{*}(\cdot) at x + + prox_{tau*f^{*}}(x) = sum_{i} prox_{tau*f^{*}_{i}}(x_{i}) + + + ''' + + if out is None: + + out = [None]*self.length + if isinstance(tau, Number): + for i in range(self.length): + out[i] = self.functions[i].proximal(x.get_item(i), tau) + else: + for i in range(self.length): + out[i] = self.functions[i].proximal(x.get_item(i), tau.get_item(i)) + + return BlockDataContainer(*out) + + else: + if isinstance(tau, Number): + for i in range(self.length): + self.functions[i].proximal(x.get_item(i), tau, out[i]) + else: + for i in range(self.length): + self.functions[i].proximal(x.get_item(i), tau.get_item(i), out[i]) + + + + def gradient(self,x, out=None): + + ''' Evaluate gradient of f at x: f'(x) + + returns: BlockDataContainer [f_{1}'(x_{1}), ... , f_{n}'(x_{n})] + + ''' + + out = [None]*self.length + for i in range(self.length): + out[i] = self.functions[i].gradient(x.get_item(i)) + + return BlockDataContainer(*out) + + + +if __name__ == '__main__': + + M, N, K = 2,3,5 + + from ccpi.optimisation.functions import L2NormSquared, MixedL21Norm + from ccpi.framework import ImageGeometry, BlockGeometry + from ccpi.optimisation.operators import Gradient, Identity, BlockOperator + import numpy + import numpy as np + + + ig = ImageGeometry(M, N) + BG = BlockGeometry(ig, ig) + + u = ig.allocate('random_int') + B = BlockOperator( Gradient(ig), Identity(ig) ) + + U = B.direct(u) + b = ig.allocate('random_int') + + f1 = 10 * MixedL21Norm() + f2 = 0.5 * L2NormSquared(b=b) + + f = BlockFunction(f1, f2) + tau = 0.3 + + print( " without out " ) + res_no_out = f.proximal_conjugate( U, tau) + res_out = B.range_geometry().allocate() + f.proximal_conjugate( U, tau, out = res_out) + + numpy.testing.assert_array_almost_equal(res_no_out[0][0].as_array(), \ + res_out[0][0].as_array(), decimal=4) + + numpy.testing.assert_array_almost_equal(res_no_out[0][1].as_array(), \ + res_out[0][1].as_array(), decimal=4) + + numpy.testing.assert_array_almost_equal(res_no_out[1].as_array(), \ + res_out[1].as_array(), decimal=4) + + + + ########################################################################## + + + + + + + +# zzz = B.range_geometry().allocate('random_int') +# www = B.range_geometry().allocate() +# www.fill(zzz) + +# res[0].fill(z) + + + + +# f.proximal_conjugate(z, sigma, out = res) + +# print(z1[0][0].as_array()) +# print(res[0][0].as_array()) + + + + +# U = BG.allocate('random_int') +# RES = BG.allocate() +# f = BlockFunction(f1, f2) +# +# z = f.proximal_conjugate(U, 0.2) +# f.proximal_conjugate(U, 0.2, out = RES) +# +# print(z[0].as_array()) +# print(RES[0].as_array()) +# +# print(z[1].as_array()) +# print(RES[1].as_array()) + + + + + + + +
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/optimisation/functions/Function.py b/Wrappers/Python/ccpi/optimisation/functions/Function.py new file mode 100644 index 0000000..ba33666 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/functions/Function.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Jakob Jorgensen, Daniil Kazantsev and Edoardo Pasca + +# 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. + +import warnings +from ccpi.optimisation.functions.ScaledFunction import ScaledFunction + +class Function(object): + '''Abstract class representing a function + + Members: + L is the Lipschitz constant of the gradient of the Function + ''' + def __init__(self): + self.L = None + + def __call__(self,x, out=None): + '''Evaluates the function at x ''' + raise NotImplementedError + + def gradient(self, x, out=None): + '''Returns the gradient of the function at x, if the function is differentiable''' + raise NotImplementedError + + def proximal(self, x, tau, out=None): + '''This returns the proximal operator for the function at x, tau''' + raise NotImplementedError + + def convex_conjugate(self, x, out=None): + '''This evaluates the convex conjugate of the function at x''' + raise NotImplementedError + + def proximal_conjugate(self, x, tau, out = None): + '''This returns the proximal operator for the convex conjugate of the function at x, tau''' + raise NotImplementedError + + def grad(self, x): + '''Alias of gradient(x,None)''' + warnings.warn('''This method will disappear in following + versions of the CIL. Use gradient instead''', DeprecationWarning) + return self.gradient(x, out=None) + + def prox(self, x, tau): + '''Alias of proximal(x, tau, None)''' + warnings.warn('''This method will disappear in following + versions of the CIL. Use proximal instead''', DeprecationWarning) + return self.proximal(x, tau, out=None) + + def __rmul__(self, scalar): + '''Defines the multiplication by a scalar on the left + + returns a ScaledFunction''' + return ScaledFunction(self, scalar) + diff --git a/Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition.py b/Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition.py new file mode 100644 index 0000000..a2445cd --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Mar 8 09:55:36 2019 + +@author: evangelos +""" + +from ccpi.optimisation.functions import Function +from ccpi.optimisation.functions import ScaledFunction + + +class FunctionOperatorComposition(Function): + + ''' Function composition with Operator, i.e., f(Ax) + + A: operator + f: function + + ''' + + def __init__(self, function, operator): + + super(FunctionOperatorComposition, self).__init__() + + self.function = function + self.operator = operator + self.L = function.L * operator.norm()**2 + + + def __call__(self, x): + + ''' Evaluate FunctionOperatorComposition at x + + returns f(Ax) + + ''' + + return self.function(self.operator.direct(x)) + + def gradient(self, x, out=None): +# + ''' Gradient takes into account the Operator''' + if out is None: + return self.operator.adjoint(self.function.gradient(self.operator.direct(x))) + else: + tmp = self.operator.range_geometry().allocate() + self.operator.direct(x, out=tmp) + self.function.gradient(tmp, out=tmp) + self.operator.adjoint(tmp, out=out) + + + + +if __name__ == '__main__': + + from ccpi.framework import ImageGeometry, AcquisitionGeometry + from ccpi.optimisation.operators import Gradient + from ccpi.optimisation.functions import L2NormSquared + from ccpi.astra.ops import AstraProjectorSimple + import numpy as np + + M, N= 50, 50 + ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N) + + detectors = N + angles_num = N + det_w = 1.0 + + angles = np.linspace(0, np.pi, angles_num, endpoint=False) + ag = AcquisitionGeometry('parallel', + '2D', + angles, + detectors,det_w) + + + Aop = AstraProjectorSimple(ig, ag, 'cpu') + + u = ig.allocate('random_int', seed=15) + u1 = ig.allocate('random_int', seed=10) + b = Aop.direct(u1) + + +# G = Gradient(ig) + alpha = 0.5 + + f1 = alpha * L2NormSquared(b=b) + + f_comp = FunctionOperatorComposition(f1, Aop) + + print(f_comp(u)) + + + z1 = Aop.direct(u) + tmp = 0.5 * ((z1 - b)**2).sum() + + + print(tmp) + + + + + + + + + + + +
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition_old.py b/Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition_old.py new file mode 100644 index 0000000..70511bb --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition_old.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Mar 8 09:55:36 2019 + +@author: evangelos +""" + +from ccpi.optimisation.functions import Function +from ccpi.optimisation.functions import ScaledFunction + + +class FunctionOperatorComposition(Function): + + ''' Function composition with Operator, i.e., f(Ax) + + A: operator + f: function + + ''' + + def __init__(self, operator, function): + + super(FunctionOperatorComposition, self).__init__() + self.function = function + self.operator = operator + alpha = 1 + + if isinstance (function, ScaledFunction): + alpha = function.scalar + self.L = 2 * alpha * operator.norm()**2 + + + def __call__(self, x): + + ''' Evaluate FunctionOperatorComposition at x + + returns f(Ax) + + ''' + + return self.function(self.operator.direct(x)) + + #TODO do not know if we need it + def call_adjoint(self, x): + + return self.function(self.operator.adjoint(x)) + + + def convex_conjugate(self, x): + + ''' convex_conjugate does not take into account the Operator''' + return self.function.convex_conjugate(x) + + def proximal(self, x, tau, out=None): + + '''proximal does not take into account the Operator''' + if out is None: + return self.function.proximal(x, tau) + else: + self.function.proximal(x, tau, out=out) + + + def proximal_conjugate(self, x, tau, out=None): + + ''' proximal conjugate does not take into account the Operator''' + if out is None: + return self.function.proximal_conjugate(x, tau) + else: + self.function.proximal_conjugate(x, tau, out=out) + + def gradient(self, x, out=None): + + ''' Gradient takes into account the Operator''' + if out is None: + return self.operator.adjoint( + self.function.gradient(self.operator.direct(x)) + ) + else: + self.operator.adjoint( + self.function.gradient(self.operator.direct(x), + out=out) + ) + +
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/optimisation/functions/IndicatorBox.py b/Wrappers/Python/ccpi/optimisation/functions/IndicatorBox.py new file mode 100755 index 0000000..7fec65e --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/functions/IndicatorBox.py @@ -0,0 +1,134 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + + +from ccpi.optimisation.functions import Function +import numpy +from ccpi.framework import ImageData + +class IndicatorBox(Function): + '''Box constraints indicator function. + + Calling returns 0 if argument is within the box. The prox operator is projection onto the box. + Only implements one scalar lower and one upper as constraint on all elements. Should generalise + to vectors to allow different constraints one elements. +''' + + def __init__(self,lower=-numpy.inf,upper=numpy.inf): + # Do nothing + super(IndicatorBox, self).__init__() + self.lower = lower + self.upper = upper + + + def __call__(self,x): + + if (numpy.all(x.array>=self.lower) and + numpy.all(x.array <= self.upper) ): + val = 0 + else: + val = numpy.inf + return val + + def gradient(self,x): + return ValueError('Not Differentiable') + + def convex_conjugate(self,x): + # support function sup <x, z>, z \in [lower, upper] + # ???? + return x.maximum(0).sum() + + def proximal(self, x, tau, out=None): + + if out is None: + return (x.maximum(self.lower)).minimum(self.upper) + else: + x.maximum(self.lower, out=out) + out.minimum(self.upper, out=out) + + def proximal_conjugate(self, x, tau, out=None): + + if out is None: + + return x - tau * self.proximal(x/tau, tau) + + else: + + self.proximal(x/tau, tau, out=out) + out *= -1*tau + out += x + + + +if __name__ == '__main__': + + from ccpi.framework import ImageGeometry, BlockDataContainer + + N, M = 2,3 + ig = ImageGeometry(voxel_num_x = N, voxel_num_y = M) + + u = ig.allocate('random_int') + tau = 2 + + f = IndicatorBox(2, 3) + + lower = 10 + upper = 30 + + z1 = f.proximal(u, tau) + + z2 = f.proximal_conjugate(u/tau, 1/tau) + + z = z1 + tau * z2 + + numpy.testing.assert_array_equal(z.as_array(), u.as_array()) + + out1 = ig.allocate() + out2 = ig.allocate() + + f.proximal(u, tau, out=out1) + f.proximal_conjugate(u/tau, 1/tau, out = out2) + + p = out1 + tau * out2 + + numpy.testing.assert_array_equal(p.as_array(), u.as_array()) + + d = f.convex_conjugate(u) + print(d) + + + + # what about n-dimensional Block + #uB = BlockDataContainer(u,u,u) + #lowerB = BlockDataContainer(1,2,3) + #upperB = BlockDataContainer(10,21,30) + + #fB = IndicatorBox(lowerB, upperB) + + #z1B = fB.proximal(uB, tau) + + + + + + + +
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/optimisation/functions/KullbackLeibler.py b/Wrappers/Python/ccpi/optimisation/functions/KullbackLeibler.py new file mode 100644 index 0000000..6920829 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/functions/KullbackLeibler.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +import numpy +from ccpi.optimisation.functions import Function +from ccpi.optimisation.functions.ScaledFunction import ScaledFunction +import functools +import scipy.special + +class KullbackLeibler(Function): + + ''' + + KL_div(x, y + back) = int x * log(x/(y+back)) - x + (y+back) + + Assumption: y>=0 + back>=0 + + ''' + + def __init__(self, data, **kwargs): + + super(KullbackLeibler, self).__init__() + + self.b = data + self.bnoise = 0 + + + def __call__(self, x): + + + ''' + + x - y * log( x + bnoise) + y * log(y) - y + bnoise + + + ''' + + ind = x.as_array()>0 + tmp = scipy.special.kl_div(self.b.as_array()[ind], x.as_array()[ind]) + return numpy.sum(tmp) + + + def log(self, datacontainer): + '''calculates the in-place log of the datacontainer''' + if not functools.reduce(lambda x,y: x and y>0, + datacontainer.as_array().ravel(), True): + raise ValueError('KullbackLeibler. Cannot calculate log of negative number') + datacontainer.fill( numpy.log(datacontainer.as_array()) ) + + + def gradient(self, x, out=None): + + if out is None: + return 1 - self.b/(x + self.bnoise) + else: + + x.add(self.bnoise, out=out) + self.b.divide(out, out=out) + out.subtract(1, out=out) + out *= -1 + + def convex_conjugate(self, x): + + xlogy = - scipy.special.xlogy(self.b.as_array(), 1 - x.as_array()) + return numpy.sum(xlogy) + + def proximal(self, x, tau, out=None): + + if out is None: + return 0.5 *( (x - self.bnoise - tau) + ( (x + self.bnoise - tau)**2 + 4*tau*self.b ) .sqrt() ) + else: + + tmp = 0.5 *( (x - self.bnoise - tau) + + ( (x + self.bnoise - tau)**2 + 4*tau*self.b ) .sqrt() + ) + x.add(self.bnoise, out=out) + out -= tau + out *= out + tmp = self.b * (4 * tau) + out.add(tmp, out=out) + out.sqrt(out=out) + + x.subtract(self.bnoise, out=tmp) + tmp -= tau + + out += tmp + + out *= 0.5 + + def proximal_conjugate(self, x, tau, out=None): + + + if out is None: + z = x + tau * self.bnoise + return 0.5*((z + 1) - ((z-1)**2 + 4 * tau * self.b).sqrt()) + else: + + #tmp = x + tau * self.bnoise + tmp = tau * self.bnoise + tmp += x + tmp -= 1 + + self.b.multiply(4*tau, out=out) + + out.add((tmp)**2, out=out) + out.sqrt(out=out) + out *= -1 + tmp += 2 + out += tmp + out *= 0.5 + + def __rmul__(self, scalar): + + ''' Multiplication of L2NormSquared with a scalar + + Returns: ScaledFunction + + ''' + + return ScaledFunction(self, scalar) + + +if __name__ == '__main__': + + from ccpi.framework import ImageGeometry + import numpy + + M, N = 2,3 + ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N) + u = ig.allocate('random_int') + b = ig.allocate('random_int') + u.as_array()[1,1]=0 + u.as_array()[2,0]=0 + b.as_array()[1,1]=0 + b.as_array()[2,0]=0 + + f = KullbackLeibler(b) + + +# longest = reduce(lambda x, y: len(x) if len(x) > len(y) else len(y), strings) + + +# tmp = functools.reduce(lambda x, y: \ +# 0 if x==0 and not numpy.isnan(y) else x * numpy.log(y), \ +# zip(b.as_array().ravel(), u.as_array().ravel()),0) + + +# np.multiply.reduce(X, 0) + + +# sf = reduce(lambda x,y: x + y[0]*y[1], +# zip(self.as_array().ravel(), +# other.as_array().ravel()), +# 0) +#cdef inline number_t xlogy(number_t x, number_t y) nogil: +# if x == 0 and not zisnan(y): +# return 0 +# else: +# return x * zlog(y) + +# if npy_isnan(x): +# return x +# elif x > 0: +# return -x * log(x) +# elif x == 0: +# return 0 +# else: +# return -inf + +# cdef inline double kl_div(double x, double y) nogil: +# if npy_isnan(x) or npy_isnan(y): +# return nan +# elif x > 0 and y > 0: +# return x * log(x / y) - x + y +# elif x == 0 and y >= 0: +# return y +# else: +# return inf + + + + +# def xlogy(self, dc1, dc2): + +# return numpy.sum(numpy.where(dc1.as_array() != 0, dc2.as_array() * numpy.log(dc2.as_array() / dc1.as_array()), 0)) + + + +# f.xlog(u, b) + + + + +# tmp1 = b.as_array() +# tmp2 = u.as_array() +# +# zz = scipy.special.xlogy(tmp1, tmp2) +# +# print(np.sum(zz)) + + +# ww = f.xlogy(b, u) + +# print(ww) + + +#cdef inline double kl_div(double x, double y) nogil: + + + + + + + diff --git a/Wrappers/Python/ccpi/optimisation/functions/L1Norm.py b/Wrappers/Python/ccpi/optimisation/functions/L1Norm.py new file mode 100644 index 0000000..37c2016 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/functions/L1Norm.py @@ -0,0 +1,162 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + + +from ccpi.optimisation.functions import Function +from ccpi.optimisation.functions.ScaledFunction import ScaledFunction +from ccpi.optimisation.operators import ShrinkageOperator + + +class L1Norm(Function): + + ''' + + Class: L1Norm + + Cases: a) f(x) = ||x||_{1} + + b) f(x) = ||x - b||_{1} + + ''' + + def __init__(self, **kwargs): + + super(L1Norm, self).__init__() + self.b = kwargs.get('b',None) + + def __call__(self, x): + + ''' Evaluate L1Norm at x: f(x) ''' + + y = x + if self.b is not None: + y = x - self.b + return y.abs().sum() + + def gradient(self,x): + #TODO implement subgradient??? + return ValueError('Not Differentiable') + + def convex_conjugate(self,x): + #TODO implement Indicator infty??? + + y = 0 + if self.b is not None: + y = 0 + self.b.dot(x) + return y + + def proximal(self, x, tau, out=None): + + # TODO implement shrinkage operator, we will need it later e.g SplitBregman + + if out is None: + if self.b is not None: + return self.b + ShrinkageOperator.__call__(self, x - self.b, tau) + else: + return ShrinkageOperator.__call__(self, x, tau) + else: + if self.b is not None: + out.fill(self.b + ShrinkageOperator.__call__(self, x - self.b, tau)) + else: + out.fill(ShrinkageOperator.__call__(self, x, tau)) + + def proximal_conjugate(self, x, tau, out=None): + + if out is None: + if self.b is not None: + return (x - tau*self.b).divide((x - tau*self.b).abs().maximum(1.0)) + else: + return x.divide(x.abs().maximum(1.0)) + else: + if self.b is not None: + out.fill((x - tau*self.b).divide((x - tau*self.b).abs().maximum(1.0))) + else: + out.fill(x.divide(x.abs().maximum(1.0)) ) + + def __rmul__(self, scalar): + return ScaledFunction(self, scalar) + + +if __name__ == '__main__': + + from ccpi.framework import ImageGeometry + import numpy + N, M = 400,400 + ig = ImageGeometry(N, M) + scalar = 10 + b = ig.allocate('random') + u = ig.allocate('random') + + f = L1Norm() + f_scaled = scalar * L1Norm() + + f_b = L1Norm(b=b) + f_scaled_b = scalar * L1Norm(b=b) + + # call + + a1 = f(u) + a2 = f_scaled(u) + numpy.testing.assert_equal(scalar * a1, a2) + + a3 = f_b(u) + a4 = f_scaled_b(u) + numpy.testing.assert_equal(scalar * a3, a4) + + # proximal + tau = 0.4 + b1 = f.proximal(u, tau*scalar) + b2 = f_scaled.proximal(u, tau) + + numpy.testing.assert_array_almost_equal(b1.as_array(), b2.as_array(), decimal=4) + + b3 = f_b.proximal(u, tau*scalar) + b4 = f_scaled_b.proximal(u, tau) + + z1 = b + (u-b).sign() * ((u-b).abs() - tau * scalar).maximum(0) + + numpy.testing.assert_array_almost_equal(b3.as_array(), b4.as_array(), decimal=4) +# +# #proximal conjugate +# + c1 = f_scaled.proximal_conjugate(u, tau) + c2 = u.divide((u.abs()/scalar).maximum(1.0)) + + numpy.testing.assert_array_almost_equal(c1.as_array(), c2.as_array(), decimal=4) + + c3 = f_scaled_b.proximal_conjugate(u, tau) + c4 = (u - tau*b).divide( ((u-tau*b).abs()/scalar).maximum(1.0) ) + + numpy.testing.assert_array_almost_equal(c3.as_array(), c4.as_array(), decimal=4) + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/optimisation/functions/L2NormSquared.py b/Wrappers/Python/ccpi/optimisation/functions/L2NormSquared.py new file mode 100644 index 0000000..2f05119 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/functions/L2NormSquared.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.optimisation.functions import Function +from ccpi.optimisation.functions.ScaledFunction import ScaledFunction +from ccpi.optimisation.functions import FunctionOperatorComposition + +class L2NormSquared(Function): + + ''' + + Cases: a) f(x) = \|x\|^{2}_{2} + + b) f(x) = ||x - b||^{2}_{2} + + ''' + + def __init__(self, **kwargs): + + super(L2NormSquared, self).__init__() + self.b = kwargs.get('b',None) + self.L = 2 + + def __call__(self, x): + + ''' Evaluate L2NormSquared at x: f(x) ''' + + y = x + if self.b is not None: + y = x - self.b + try: + return y.squared_norm() + except AttributeError as ae: + # added for compatibility with SIRF + return (y.norm()**2) + + def gradient(self, x, out=None): + + ''' Evaluate gradient of L2NormSquared at x: f'(x) ''' + + if out is not None: + + out.fill(x) + if self.b is not None: + out -= self.b + out *= 2 + + else: + + y = x + if self.b is not None: + y = x - self.b + return 2*y + + + def convex_conjugate(self, x): + + ''' Evaluate convex conjugate of L2NormSquared at x: f^{*}(x)''' + + tmp = 0 + + if self.b is not None: + tmp = x.dot(self.b) #(x * self.b).sum() + + return (1./4.) * x.squared_norm() + tmp + + + def proximal(self, x, tau, out = None): + + ''' Evaluate Proximal Operator of tau * f(\cdot) at x: + + prox_{tau*f(\cdot)}(x) = \argmin_{z} \frac{1}{2}|| z - x ||^{2}_{2} + tau * f(z) + + ''' + + if out is None: + + if self.b is None: + return x/(1+2*tau) + else: + tmp = x.subtract(self.b) + tmp /= (1+2*tau) + tmp += self.b + return tmp + + else: + if self.b is not None: + x.subtract(self.b, out=out) + out /= (1+2*tau) + out += self.b + else: + x.divide((1+2*tau), out=out) + + + def proximal_conjugate(self, x, tau, out=None): + + ''' Evaluate Proximal Operator of tau * f^{*}(\cdot) at x (i.e., the convex conjugate of f) : + + prox_{tau*f(\cdot)}(x) = \argmin_{z} \frac{1}{2}|| z - x ||^{2}_{2} + tau * f^{*}(z) + + ''' + + if out is None: + if self.b is not None: + return (x - tau*self.b)/(1 + tau/2) + else: + return x/(1 + tau/2) + else: + if self.b is not None: + x.subtract(tau*self.b, out=out) + out.divide(1+tau/2, out=out) + else: + x.divide(1 + tau/2, out=out) + + def __rmul__(self, scalar): + + ''' Multiplication of L2NormSquared with a scalar + + Returns: ScaledFunction + + ''' + + return ScaledFunction(self, scalar) + + + def composition(self, operator): + + return FunctionOperatorComposition(operator) + + + +if __name__ == '__main__': + + from ccpi.framework import ImageGeometry + import numpy + # TESTS for L2 and scalar * L2 + + M, N, K = 20,30,50 + ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N, voxel_num_z = K) + u = ig.allocate('random_int') + b = ig.allocate('random_int') + + # check grad/call no data + f = L2NormSquared() + a1 = f.gradient(u) + a2 = 2 * u + numpy.testing.assert_array_almost_equal(a1.as_array(), a2.as_array(), decimal=4) + numpy.testing.assert_equal(f(u), u.squared_norm()) + + # check grad/call with data + f1 = L2NormSquared(b=b) + b1 = f1.gradient(u) + b2 = 2 * (u-b) + + numpy.testing.assert_array_almost_equal(b1.as_array(), b2.as_array(), decimal=4) + numpy.testing.assert_equal(f1(u), (u-b).squared_norm()) + + #check convex conjuagate no data + c1 = f.convex_conjugate(u) + c2 = 1/4 * u.squared_norm() + numpy.testing.assert_equal(c1, c2) + + #check convex conjuagate with data + d1 = f1.convex_conjugate(u) + d2 = (1/4) * u.squared_norm() + u.dot(b) + numpy.testing.assert_equal(d1, d2) + + # check proximal no data + tau = 5 + e1 = f.proximal(u, tau) + e2 = u/(1+2*tau) + numpy.testing.assert_array_almost_equal(e1.as_array(), e2.as_array(), decimal=4) + + # check proximal with data + tau = 5 + h1 = f1.proximal(u, tau) + h2 = (u-b)/(1+2*tau) + b + numpy.testing.assert_array_almost_equal(h1.as_array(), h2.as_array(), decimal=4) + + # check proximal conjugate no data + tau = 0.2 + k1 = f.proximal_conjugate(u, tau) + k2 = u/(1 + tau/2 ) + numpy.testing.assert_array_almost_equal(k1.as_array(), k2.as_array(), decimal=4) + + # check proximal conjugate with data + l1 = f1.proximal_conjugate(u, tau) + l2 = (u - tau * b)/(1 + tau/2 ) + numpy.testing.assert_array_almost_equal(l1.as_array(), l2.as_array(), decimal=4) + + + # check scaled function properties + + # scalar + scalar = 100 + f_scaled_no_data = scalar * L2NormSquared() + f_scaled_data = scalar * L2NormSquared(b=b) + + # call + numpy.testing.assert_equal(f_scaled_no_data(u), scalar*f(u)) + numpy.testing.assert_equal(f_scaled_data(u), scalar*f1(u)) + + # grad + numpy.testing.assert_array_almost_equal(f_scaled_no_data.gradient(u).as_array(), scalar*f.gradient(u).as_array(), decimal=4) + numpy.testing.assert_array_almost_equal(f_scaled_data.gradient(u).as_array(), scalar*f1.gradient(u).as_array(), decimal=4) + + # conj + numpy.testing.assert_almost_equal(f_scaled_no_data.convex_conjugate(u), \ + f.convex_conjugate(u/scalar) * scalar, decimal=4) + + numpy.testing.assert_almost_equal(f_scaled_data.convex_conjugate(u), \ + scalar * f1.convex_conjugate(u/scalar), decimal=4) + + # proximal + numpy.testing.assert_array_almost_equal(f_scaled_no_data.proximal(u, tau).as_array(), \ + f.proximal(u, tau*scalar).as_array()) + + + numpy.testing.assert_array_almost_equal(f_scaled_data.proximal(u, tau).as_array(), \ + f1.proximal(u, tau*scalar).as_array()) + + + # proximal conjugate + numpy.testing.assert_array_almost_equal(f_scaled_no_data.proximal_conjugate(u, tau).as_array(), \ + (u/(1 + tau/(2*scalar) )).as_array(), decimal=4) + + numpy.testing.assert_array_almost_equal(f_scaled_data.proximal_conjugate(u, tau).as_array(), \ + ((u - tau * b)/(1 + tau/(2*scalar) )).as_array(), decimal=4) + + + + print( " ####### check without out ######### " ) + + + u_out_no_out = ig.allocate('random_int') + res_no_out = f_scaled_data.proximal_conjugate(u_out_no_out, 0.5) + print(res_no_out.as_array()) + + print( " ####### check with out ######### " ) + + res_out = ig.allocate() + f_scaled_data.proximal_conjugate(u_out_no_out, 0.5, out = res_out) + + print(res_out.as_array()) + + numpy.testing.assert_array_almost_equal(res_no_out.as_array(), \ + res_out.as_array(), decimal=4) + + + + ig1 = ImageGeometry(2,3) + + tau = 0.1 + + u = ig1.allocate('random_int') + b = ig1.allocate('random_int') + + scalar = 0.5 + f_scaled = scalar * L2NormSquared(b=b) + f_noscaled = L2NormSquared(b=b) + + + res1 = f_scaled.proximal(u, tau) + res2 = f_noscaled.proximal(u, tau*scalar) + +# res2 = (u + tau*b)/(1+tau) + + numpy.testing.assert_array_almost_equal(res1.as_array(), \ + res2.as_array(), decimal=4) + diff --git a/Wrappers/Python/ccpi/optimisation/functions/MixedL21Norm.py b/Wrappers/Python/ccpi/optimisation/functions/MixedL21Norm.py new file mode 100755 index 0000000..e8f6da4 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/functions/MixedL21Norm.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.optimisation.functions import Function, ScaledFunction +from ccpi.framework import BlockDataContainer + +import functools + +class MixedL21Norm(Function): + + + ''' + f(x) = ||x||_{2,1} = \sum |x|_{2} + ''' + + def __init__(self, **kwargs): + + super(MixedL21Norm, self).__init__() + self.SymTensor = kwargs.get('SymTensor',False) + + def __call__(self, x): + + ''' Evaluates L2,1Norm at point x + + :param: x is a BlockDataContainer + + ''' + if not isinstance(x, BlockDataContainer): + raise ValueError('__call__ expected BlockDataContainer, got {}'.format(type(x))) + + tmp = [ el**2 for el in x.containers ] + res = sum(tmp).sqrt().sum() + + return res + + def gradient(self, x, out=None): + return ValueError('Not Differentiable') + + def convex_conjugate(self,x): + + ''' This is the Indicator function of ||\cdot||_{2, \infty} + which is either 0 if ||x||_{2, \infty} or \infty + ''' + + return 0.0 + + #tmp = [ el**2 for el in x.containers ] + #print(sum(tmp).sqrt().as_array().max()) + #return sum(tmp).sqrt().as_array().max() + + def proximal(self, x, tau, out=None): + + ''' + For this we need to define a MixedL2,2 norm acting on BDC, + different form L2NormSquared which acts on DC + + ''' + pass + + def proximal_conjugate(self, x, tau, out=None): + + + if out is None: + tmp = [ el*el for el in x.containers] + res = sum(tmp).sqrt().maximum(1.0) + frac = [el/res for el in x.containers] + return BlockDataContainer(*frac) + + + #TODO this is slow, why??? +# return x.divide(x.pnorm().maximum(1.0)) + else: + + res1 = functools.reduce(lambda a,b: a + b*b, x.containers, x.get_item(0) * 0 ) + res = res1.sqrt().maximum(1.0) + x.divide(res, out=out) + +# x.divide(sum([el*el for el in x.containers]).sqrt().maximum(1.0), out=out) + #TODO this is slow, why ??? +# x.divide(x.pnorm().maximum(1.0), out=out) + + + def __rmul__(self, scalar): + + ''' Multiplication of L2NormSquared with a scalar + + Returns: ScaledFunction + + ''' + return ScaledFunction(self, scalar) + + +# +if __name__ == '__main__': + + M, N, K = 2,3,5 + from ccpi.framework import BlockGeometry + import numpy + + ig = ImageGeometry(M, N) + + BG = BlockGeometry(ig, ig) + + U = BG.allocate('random_int') + + # Define no scale and scaled + f_no_scaled = MixedL21Norm() + f_scaled = 0.5 * MixedL21Norm() + + # call + + a1 = f_no_scaled(U) + a2 = f_scaled(U) + print(a1, 2*a2) + + + print( " ####### check without out ######### " ) + + + u_out_no_out = BG.allocate('random_int') + res_no_out = f_scaled.proximal_conjugate(u_out_no_out, 0.5) + print(res_no_out[0].as_array()) + + print( " ####### check with out ######### " ) +# + res_out = BG.allocate() + f_scaled.proximal_conjugate(u_out_no_out, 0.5, out = res_out) +# + print(res_out[0].as_array()) +# + numpy.testing.assert_array_almost_equal(res_no_out[0].as_array(), \ + res_out[0].as_array(), decimal=4) + + numpy.testing.assert_array_almost_equal(res_no_out[1].as_array(), \ + res_out[1].as_array(), decimal=4) +# + + + + + + + diff --git a/Wrappers/Python/ccpi/optimisation/functions/Norm2Sq.py b/Wrappers/Python/ccpi/optimisation/functions/Norm2Sq.py new file mode 100755 index 0000000..8e77f56 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/functions/Norm2Sq.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Jakob Jorgensen, Daniil Kazantsev and Edoardo Pasca + +# 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. +from ccpi.optimisation.functions import Function +import numpy +import warnings + +# Define a class for squared 2-norm +class Norm2Sq(Function): + ''' + f(x) = c*||A*x-b||_2^2 + + which has + + grad[f](x) = 2*c*A^T*(A*x-b) + + and Lipschitz constant + + L = 2*c*||A||_2^2 = 2*s1(A)^2 + + where s1(A) is the largest singular value of A. + + ''' + + def __init__(self,A,b,c=1.0,memopt=False): + super(Norm2Sq, self).__init__() + + self.A = A # Should be an operator, default identity + self.b = b # Default zero DataSet? + self.c = c # Default 1. + if memopt: + try: + self.range_tmp = A.range_geometry().allocate() + self.domain_tmp = A.domain_geometry().allocate() + self.memopt = True + except NameError as ne: + warnings.warn(str(ne)) + self.memopt = False + except NotImplementedError as nie: + print (nie) + warnings.warn(str(nie)) + self.memopt = False + else: + self.memopt = False + + # Compute the Lipschitz parameter from the operator if possible + # Leave it initialised to None otherwise + try: + self.L = 2.0*self.c*(self.A.norm()**2) + except AttributeError as ae: + pass + except NotImplementedError as noe: + pass + + #def grad(self,x): + # return self.gradient(x, out=None) + + def __call__(self,x): + #return self.c* np.sum(np.square((self.A.direct(x) - self.b).ravel())) + #if out is None: + # return self.c*( ( (self.A.direct(x)-self.b)**2).sum() ) + #else: + y = self.A.direct(x) + y.__isub__(self.b) + #y.__imul__(y) + #return y.sum() * self.c + try: + return y.squared_norm() * self.c + except AttributeError as ae: + # added for compatibility with SIRF + return (y.norm()**2) * self.c + + def gradient(self, x, out = None): + if self.memopt: + #return 2.0*self.c*self.A.adjoint( self.A.direct(x) - self.b ) + self.A.direct(x, out=self.range_tmp) + self.range_tmp -= self.b + self.A.adjoint(self.range_tmp, out=out) + #self.direct_placehold.multiply(2.0*self.c, out=out) + out *= (self.c * 2.0) + else: + return (2.0*self.c)*self.A.adjoint( self.A.direct(x) - self.b ) diff --git a/Wrappers/Python/ccpi/optimisation/functions/ScaledFunction.py b/Wrappers/Python/ccpi/optimisation/functions/ScaledFunction.py new file mode 100755 index 0000000..8bf502a --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/functions/ScaledFunction.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# 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.
+from numbers import Number
+import numpy
+import warnings
+
+class ScaledFunction(object):
+
+ '''ScaledFunction
+
+ A class to represent the scalar multiplication of an Function with a scalar.
+ It holds a function and a scalar. Basically it returns the multiplication
+ of the product of the function __call__, convex_conjugate and gradient with the scalar.
+ For the rest it behaves like the function it holds.
+
+ Args:
+ function (Function): a Function or BlockOperator
+ scalar (Number): a scalar multiplier
+ Example:
+ The scaled operator behaves like the following:
+
+ '''
+ def __init__(self, function, scalar):
+ super(ScaledFunction, self).__init__()
+
+ if not isinstance (scalar, Number):
+ raise TypeError('expected scalar: got {}'.format(type(scalar)))
+ self.scalar = scalar
+ self.function = function
+
+ if self.function.L is not None:
+ self.L = self.scalar * self.function.L
+
+ def __call__(self,x, out=None):
+ '''Evaluates the function at x '''
+ return self.scalar * self.function(x)
+
+ def convex_conjugate(self, x):
+ '''returns the convex_conjugate of the scaled function '''
+ return self.scalar * self.function.convex_conjugate(x/self.scalar)
+
+ def gradient(self, x, out=None):
+ '''Returns the gradient of the function at x, if the function is differentiable'''
+ if out is None:
+ return self.scalar * self.function.gradient(x)
+ else:
+ self.function.gradient(x, out=out)
+ out *= self.scalar
+
+ def proximal(self, x, tau, out=None):
+ '''This returns the proximal operator for the function at x, tau
+ '''
+ if out is None:
+ return self.function.proximal(x, tau*self.scalar)
+ else:
+ self.function.proximal(x, tau*self.scalar, out = out)
+
+ def proximal_conjugate(self, x, tau, out = None):
+ '''This returns the proximal operator for the function at x, tau
+ '''
+ if out is None:
+ return self.scalar * self.function.proximal_conjugate(x/self.scalar, tau/self.scalar)
+ else:
+ self.function.proximal_conjugate(x/self.scalar, tau/self.scalar, out=out)
+ out *= self.scalar
+
+ def grad(self, x):
+ '''Alias of gradient(x,None)'''
+ warnings.warn('''This method will disappear in following
+ versions of the CIL. Use gradient instead''', DeprecationWarning)
+ return self.gradient(x, out=None)
+
+ def prox(self, x, tau):
+ '''Alias of proximal(x, tau, None)'''
+ warnings.warn('''This method will disappear in following
+ versions of the CIL. Use proximal instead''', DeprecationWarning)
+ return self.proximal(x, tau, out=None)
+
+
+
+if __name__ == '__main__':
+
+ from ccpi.optimisation.functions import L2NormSquared, MixedL21Norm
+ from ccpi.framework import ImageGeometry, BlockGeometry
+
+ M, N, K = 2,3,5
+ ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N, voxel_num_z = K)
+
+ u = ig.allocate('random_int')
+ b = ig.allocate('random_int')
+
+ BG = BlockGeometry(ig, ig)
+ U = BG.allocate('random_int')
+
+ f2 = 0.5 * L2NormSquared(b=b)
+ f1 = 30 * MixedL21Norm()
+ tau = 0.355
+
+ res_no_out1 = f1.proximal_conjugate(U, tau)
+ res_no_out2 = f2.proximal_conjugate(u, tau)
+
+
+# print( " ######## with out ######## ")
+ res_out1 = BG.allocate()
+ res_out2 = ig.allocate()
+
+ f1.proximal_conjugate(U, tau, out = res_out1)
+ f2.proximal_conjugate(u, tau, out = res_out2)
+
+
+ numpy.testing.assert_array_almost_equal(res_no_out1[0].as_array(), \
+ res_out1[0].as_array(), decimal=4)
+
+ numpy.testing.assert_array_almost_equal(res_no_out2.as_array(), \
+ res_out2.as_array(), decimal=4)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wrappers/Python/ccpi/optimisation/functions/ZeroFunction.py b/Wrappers/Python/ccpi/optimisation/functions/ZeroFunction.py new file mode 100644 index 0000000..a019815 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/functions/ZeroFunction.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.optimisation.functions import Function +from ccpi.framework import BlockDataContainer + +class ZeroFunction(Function): + + ''' ZeroFunction: f(x) = 0 + + + ''' + + def __init__(self): + super(ZeroFunction, self).__init__() + + def __call__(self,x): + return 0 + + def convex_conjugate(self, x): + + ''' This is the support function sup <x, x^{*}> which in fact is the + indicator function for the set = {0} + So 0 if x=0, or inf if x neq 0 + ''' + return x.maximum(0).sum() + + + def proximal(self, x, tau, out=None): + if out is None: + return x.copy() + else: + out.fill(x) + + def proximal_conjugate(self, x, tau, out = None): + if out is None: + return 0 + else: + return 0 diff --git a/Wrappers/Python/ccpi/optimisation/functions/__init__.py b/Wrappers/Python/ccpi/optimisation/functions/__init__.py new file mode 100644 index 0000000..c0eab31 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/functions/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from .Function import Function +from .ZeroFunction import ZeroFunction +from .L1Norm import L1Norm +from .L2NormSquared import L2NormSquared +from .ScaledFunction import ScaledFunction +from .BlockFunction import BlockFunction +from .FunctionOperatorComposition import FunctionOperatorComposition +from .MixedL21Norm import MixedL21Norm +from .IndicatorBox import IndicatorBox +from .KullbackLeibler import KullbackLeibler +from .Norm2Sq import Norm2Sq diff --git a/Wrappers/Python/ccpi/optimisation/operators/BlockOperator.py b/Wrappers/Python/ccpi/optimisation/operators/BlockOperator.py new file mode 100755 index 0000000..cbdc420 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/BlockOperator.py @@ -0,0 +1,417 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Feb 14 12:36:40 2019 + +@author: ofn77899 +""" +#from ccpi.optimisation.ops import Operator +import numpy +from numbers import Number +import functools +from ccpi.framework import AcquisitionData, ImageData, BlockDataContainer, DataContainer +from ccpi.optimisation.operators import Operator, LinearOperator +from ccpi.optimisation.operators.BlockScaledOperator import BlockScaledOperator +from ccpi.framework import BlockGeometry + +class BlockOperator(Operator): + '''A Block matrix containing Operators + + The Block Framework is a generic strategy to treat variational problems in the + following form: + + .. math:: + + min Regulariser + Fidelity + + + BlockOperators have a generic shape M x N, and when applied on an + Nx1 BlockDataContainer, will yield and Mx1 BlockDataContainer. + Notice: BlockDatacontainer are only allowed to have the shape of N x 1, with + N rows and 1 column. + + User may specify the shape of the block, by default is a row vector + + Operators in a Block are required to have the same domain column-wise and the + same range row-wise. + ''' + __array_priority__ = 1 + def __init__(self, *args, **kwargs): + ''' + Class creator + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + :param: vararg (Operator): Operators in the block. + :param: shape (:obj:`tuple`, optional): If shape is passed the Operators in + vararg are considered input in a row-by-row fashion. + Shape and number of Operators must match. + + Example: + BlockOperator(op0,op1) results in a row block + BlockOperator(op0,op1,shape=(1,2)) results in a column block + ''' + self.operators = args + shape = kwargs.get('shape', None) + if shape is None: + shape = (len(args),1) + self.shape = shape + n_elements = functools.reduce(lambda x,y: x*y, shape, 1) + if len(args) != n_elements: + raise ValueError( + 'Dimension and size do not match: expected {} got {}' + .format(n_elements,len(args))) + # test if operators are compatible + if not self.column_wise_compatible(): + raise ValueError('Operators in each column must have the same domain') + if not self.row_wise_compatible(): + raise ValueError('Operators in each row must have the same range') + + def column_wise_compatible(self): + '''Operators in a Block should have the same domain per column''' + rows, cols = self.shape + compatible = True + for col in range(cols): + column_compatible = True + for row in range(1,rows): + dg0 = self.get_item(row-1,col).domain_geometry() + dg1 = self.get_item(row,col).domain_geometry() + column_compatible = dg0.__dict__ == dg1.__dict__ and column_compatible + compatible = compatible and column_compatible + return compatible + + def row_wise_compatible(self): + '''Operators in a Block should have the same range per row''' + rows, cols = self.shape + compatible = True + for row in range(rows): + row_compatible = True + for col in range(1,cols): + dg0 = self.get_item(row,col-1).range_geometry() + dg1 = self.get_item(row,col).range_geometry() + row_compatible = dg0.__dict__ == dg1.__dict__ and row_compatible + compatible = compatible and row_compatible + return compatible + + def get_item(self, row, col): + '''returns the Operator at specified row and col''' + if row > self.shape[0]: + raise ValueError('Requested row {} > max {}'.format(row, self.shape[0])) + if col > self.shape[1]: + raise ValueError('Requested col {} > max {}'.format(col, self.shape[1])) + + index = row*self.shape[1]+col + return self.operators[index] + + def norm(self, **kwargs): + norm = [op.norm(**kwargs)**2 for op in self.operators] + return numpy.sqrt(sum(norm)) + + def direct(self, x, out=None): + '''Direct operation for the BlockOperator + + BlockOperator work on BlockDataContainer, but they will work on DataContainers + and inherited classes by simple wrapping the input in a BlockDataContainer of shape (1,1) + ''' + + if not isinstance (x, BlockDataContainer): + x_b = BlockDataContainer(x) + else: + x_b = x + shape = self.get_output_shape(x_b.shape) + res = [] + + if out is None: + + for row in range(self.shape[0]): + for col in range(self.shape[1]): + if col == 0: + prod = self.get_item(row,col).direct(x_b.get_item(col)) + else: + prod += self.get_item(row,col).direct(x_b.get_item(col)) + res.append(prod) + return BlockDataContainer(*res, shape=shape) + + else: + + tmp = self.range_geometry().allocate() + for row in range(self.shape[0]): + for col in range(self.shape[1]): + if col == 0: + self.get_item(row,col).direct( + x_b.get_item(col), + out=out.get_item(row)) + else: + a = out.get_item(row) + self.get_item(row,col).direct( + x_b.get_item(col), + out=tmp.get_item(row)) + a += tmp.get_item(row) + + def adjoint(self, x, out=None): + '''Adjoint operation for the BlockOperator + + BlockOperator may contain both LinearOperator and Operator + This method exists in BlockOperator as it is not known what type of + Operator it will contain. + + BlockOperator work on BlockDataContainer, but they will work on DataContainers + and inherited classes by simple wrapping the input in a BlockDataContainer of shape (1,1) + + Raises: ValueError if the contained Operators are not linear + ''' + if not self.is_linear(): + raise ValueError('Not all operators in Block are linear.') + if not isinstance (x, BlockDataContainer): + x_b = BlockDataContainer(x) + else: + x_b = x + shape = self.get_output_shape(x_b.shape, adjoint=True) + if out is None: + res = [] + for col in range(self.shape[1]): + for row in range(self.shape[0]): + if row == 0: + prod = self.get_item(row, col).adjoint(x_b.get_item(row)) + else: + prod += self.get_item(row, col).adjoint(x_b.get_item(row)) + res.append(prod) + if self.shape[1]==1: + return ImageData(*res) + else: + return BlockDataContainer(*res, shape=shape) + else: + #tmp = self.domain_geometry().allocate() + + for col in range(self.shape[1]): + for row in range(self.shape[0]): + if row == 0: + if issubclass(out.__class__, DataContainer): + self.get_item(row, col).adjoint( + x_b.get_item(row), + out=out) + else: + op = self.get_item(row,col) + self.get_item(row, col).adjoint( + x_b.get_item(row), + out=out.get_item(col)) + else: + if issubclass(out.__class__, DataContainer): + out += self.get_item(row,col).adjoint( + x_b.get_item(row)) + else: + a = out.get_item(col) + a += self.get_item(row,col).adjoint( + x_b.get_item(row), + ) + def is_linear(self): + '''returns whether all the elements of the BlockOperator are linear''' + return functools.reduce(lambda x, y: x and y.is_linear(), self.operators, True) + + def get_output_shape(self, xshape, adjoint=False): + '''returns the shape of the output BlockDataContainer + + A(N,M) direct u(M,1) -> N,1 + A(N,M)^T adjoint u(N,1) -> M,1 + ''' + rows , cols = self.shape + xrows, xcols = xshape + if xcols != 1: + raise ValueError('BlockDataContainer cannot have more than 1 column') + if adjoint: + if rows != xrows: + raise ValueError('Incompatible shapes {} {}'.format(self.shape, xshape)) + return (cols,xcols) + if cols != xrows: + raise ValueError('Incompatible shapes {} {}'.format((rows,cols), xshape)) + return (rows,xcols) + + def __rmul__(self, scalar): + '''Defines the left multiplication with a scalar + + Args: scalar (number or iterable containing numbers): + + Returns: a block operator with Scaled Operators inside''' + if isinstance (scalar, list) or isinstance(scalar, tuple) or \ + isinstance(scalar, numpy.ndarray): + if len(scalar) != len(self.operators): + raise ValueError('dimensions of scalars and operators do not match') + scalars = scalar + else: + scalars = [scalar for _ in self.operators] + # create a list of ScaledOperator-s + ops = [ v * op for v,op in zip(scalars, self.operators)] + #return BlockScaledOperator(self, scalars ,shape=self.shape) + return type(self)(*ops, shape=self.shape) + @property + def T(self): + '''Return the transposed of self + + input in a row-by-row''' + newshape = (self.shape[1], self.shape[0]) + oplist = [] + for col in range(newshape[1]): + for row in range(newshape[0]): + oplist.append(self.get_item(col,row)) + return type(self)(*oplist, shape=newshape) + + def domain_geometry(self): + '''returns the domain of the BlockOperator + + If the shape of the BlockOperator is (N,1) the domain is a ImageGeometry or AcquisitionGeometry. + Otherwise it is a BlockGeometry. + ''' + if self.shape[1] == 1: + # column BlockOperator + return self.get_item(0,0).domain_geometry() + else: + # get the geometries column wise + # we need only the geometries from the first row + # since it is compatible from __init__ + tmp = [] + for i in range(self.shape[1]): + tmp.append(self.get_item(0,i).domain_geometry()) + return BlockGeometry(*tmp) + + #shape = (self.shape[0], 1) + #return BlockGeometry(*[el.domain_geometry() for el in self.operators], + # shape=self.shape) + + def range_geometry(self): + '''returns the range of the BlockOperator''' + + tmp = [] + for i in range(self.shape[0]): + tmp.append(self.get_item(i,0).range_geometry()) + return BlockGeometry(*tmp) + + + #shape = (self.shape[1], 1) + #return BlockGeometry(*[el.range_geometry() for el in self.operators], + # shape=shape) + + def sum_abs_row(self): + + res = [] + for row in range(self.shape[0]): + for col in range(self.shape[1]): + if col == 0: + prod = self.get_item(row,col).sum_abs_row() + else: + prod += self.get_item(row,col).sum_abs_row() + res.append(prod) + + if self.shape[1]==1: + tmp = sum(res) + return ImageData(tmp) + else: + + return BlockDataContainer(*res) + + def sum_abs_col(self): + + res = [] + for row in range(self.shape[0]): + for col in range(self.shape[1]): + if col == 0: + prod = self.get_item(row, col).sum_abs_col() + else: + prod += self.get_item(row, col).sum_abs_col() + res.append(prod) + + return BlockDataContainer(*res) + + + +if __name__ == '__main__': + + from ccpi.framework import ImageGeometry + from ccpi.optimisation.operators import Gradient, Identity, \ + SparseFiniteDiff, SymmetrizedGradient, ZeroOperator + + + M, N = 4, 3 + ig = ImageGeometry(M, N) + arr = ig.allocate('random_int') + + G = Gradient(ig) + Id = Identity(ig) + + B = BlockOperator(G, Id) + + print(B.sum_abs_row()) +# + Gx = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann') + Gy = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann') + + d1 = abs(Gx.matrix()).toarray().sum(axis=0) + d2 = abs(Gy.matrix()).toarray().sum(axis=0) + d3 = abs(Id.matrix()).toarray().sum(axis=0) + + + d_res = numpy.reshape(d1 + d2 + d3, ig.shape, 'F') + + print(d_res) +# + z1 = abs(Gx.matrix()).toarray().sum(axis=1) + z2 = abs(Gy.matrix()).toarray().sum(axis=1) + z3 = abs(Id.matrix()).toarray().sum(axis=1) +# + z_res = BlockDataContainer(BlockDataContainer(ImageData(numpy.reshape(z2, ig.shape, 'F')),\ + ImageData(numpy.reshape(z1, ig.shape, 'F'))),\ + ImageData(numpy.reshape(z3, ig.shape, 'F'))) +# + ttt = B.sum_abs_col() +# + #TODO this is not working +# numpy.testing.assert_array_almost_equal(z_res[0][0].as_array(), ttt[0][0].as_array(), decimal=4) +# numpy.testing.assert_array_almost_equal(z_res[0][1].as_array(), ttt[0][1].as_array(), decimal=4) +# numpy.testing.assert_array_almost_equal(z_res[1].as_array(), ttt[1].as_array(), decimal=4) + + + u = ig.allocate('random_int') + + z1 = B.direct(u) + res = B.range_geometry().allocate() + + B.direct(u, out = res) + + + + ########################################################################### + # Block Operator for TGV reconstruction + + M, N = 2,3 + ig = ImageGeometry(M, N) + ag = ig + + op11 = Gradient(ig) + op12 = Identity(op11.range_geometry()) + + op22 = SymmetrizedGradient(op11.domain_geometry()) + + op21 = ZeroOperator(ig, op22.range_geometry()) + + + op31 = Identity(ig, ag) + op32 = ZeroOperator(op22.domain_geometry(), ag) + + operator = BlockOperator(op11, -1*op12, op21, op22, op31, op32, shape=(3,2) ) + + z1 = operator.domain_geometry() + z2 = operator.range_geometry() + + print(z1.shape) + print(z2.shape) + + + + + + + + + + +
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/optimisation/operators/BlockScaledOperator.py b/Wrappers/Python/ccpi/optimisation/operators/BlockScaledOperator.py new file mode 100644 index 0000000..74ba9e4 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/BlockScaledOperator.py @@ -0,0 +1,67 @@ +from numbers import Number +import numpy +from ccpi.optimisation.operators import ScaledOperator +import functools + +class BlockScaledOperator(ScaledOperator): + '''ScaledOperator + + A class to represent the scalar multiplication of an Operator with a scalar. + It holds an operator and a scalar. Basically it returns the multiplication + of the result of direct and adjoint of the operator with the scalar. + For the rest it behaves like the operator it holds. + + Args: + operator (Operator): a Operator or LinearOperator + scalar (Number): a scalar multiplier + Example: + The scaled operator behaves like the following: + sop = ScaledOperator(operator, scalar) + sop.direct(x) = scalar * operator.direct(x) + sop.adjoint(x) = scalar * operator.adjoint(x) + sop.norm() = operator.norm() + sop.range_geometry() = operator.range_geometry() + sop.domain_geometry() = operator.domain_geometry() + ''' + def __init__(self, operator, scalar, shape=None): + if shape is None: + shape = operator.shape + + if isinstance(scalar, (list, tuple, numpy.ndarray)): + size = functools.reduce(lambda x,y:x*y, shape, 1) + if len(scalar) != size: + raise ValueError('Scalar and operators size do not match: {}!={}' + .format(len(scalar), len(operator))) + self.scalar = scalar[:] + print ("BlockScaledOperator ", self.scalar) + elif isinstance (scalar, Number): + self.scalar = scalar + else: + raise TypeError('expected scalar to be a number of an iterable: got {}'.format(type(scalar))) + self.operator = operator + self.shape = shape + def direct(self, x, out=None): + print ("BlockScaledOperator self.scalar", self.scalar) + #print ("self.scalar", self.scalar[0]* x.get_item(0).as_array()) + return self.scalar * (self.operator.direct(x, out=out)) + def adjoint(self, x, out=None): + if self.operator.is_linear(): + return self.scalar * self.operator.adjoint(x, out=out) + else: + raise TypeError('Operator is not linear') + def norm(self, **kwargs): + return numpy.abs(self.scalar) * self.operator.norm(**kwargs) + def range_geometry(self): + return self.operator.range_geometry() + def domain_geometry(self): + return self.operator.domain_geometry() + @property + def T(self): + '''Return the transposed of self''' + #print ("transpose before" , self.shape) + #shape = (self.shape[1], self.shape[0]) + ##self.shape = shape + ##self.operator.shape = shape + #print ("transpose" , shape) + #return self + return type(self)(self.operator.T, self.scalar)
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/optimisation/operators/FiniteDifferenceOperator.py b/Wrappers/Python/ccpi/optimisation/operators/FiniteDifferenceOperator.py new file mode 100644 index 0000000..876f45f --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/FiniteDifferenceOperator.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Mar 1 22:51:17 2019 + +@author: evangelos +""" + +from ccpi.optimisation.operators import LinearOperator +from ccpi.framework import ImageData, BlockDataContainer +import numpy as np + +class FiniteDiff(LinearOperator): + + # Works for Neum/Symmetric & periodic boundary conditions + # TODO add central differences??? + # TODO not very well optimised, too many conditions + # TODO add discretisation step, should get that from imageGeometry + + # Grad_order = ['channels', 'direction_z', 'direction_y', 'direction_x'] + # Grad_order = ['channels', 'direction_y', 'direction_x'] + # Grad_order = ['direction_z', 'direction_y', 'direction_x'] + # Grad_order = ['channels', 'direction_z', 'direction_y', 'direction_x'] + + def __init__(self, gm_domain, gm_range=None, direction=0, bnd_cond = 'Neumann'): + '''''' + super(FiniteDiff, self).__init__() + '''FIXME: domain and range should be geometries''' + self.gm_domain = gm_domain + self.gm_range = gm_range + + self.direction = direction + self.bnd_cond = bnd_cond + + # Domain Geometry = Range Geometry if not stated + if self.gm_range is None: + self.gm_range = self.gm_domain + # check direction and "length" of geometry + if self.direction + 1 > len(self.gm_domain.shape): + raise ValueError('Gradient directions more than geometry domain') + + #self.voxel_size = kwargs.get('voxel_size',1) + # this wrongly assumes a homogeneous voxel size +# self.voxel_size = self.gm_domain.voxel_size_x + + + def direct(self, x, out=None): + + x_asarr = x.as_array() + x_sz = len(x.shape) + + if out is None: + out = np.zeros_like(x_asarr) + else: + out = out.as_array() + out[:]=0 + + + + ######################## Direct for 2D ############################### + if x_sz == 2: + + if self.direction == 1: + + np.subtract( x_asarr[:,1:], x_asarr[:,0:-1], out = out[:,0:-1] ) + + if self.bnd_cond == 'Neumann': + pass + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[:,0], x_asarr[:,-1], out = out[:,-1] ) + else: + raise ValueError('No valid boundary conditions') + + if self.direction == 0: + + np.subtract( x_asarr[1:], x_asarr[0:-1], out = out[0:-1,:] ) + + if self.bnd_cond == 'Neumann': + pass + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[0,:], x_asarr[-1,:], out = out[-1,:] ) + else: + raise ValueError('No valid boundary conditions') + + ######################## Direct for 3D ############################### + elif x_sz == 3: + + if self.direction == 0: + + np.subtract( x_asarr[1:,:,:], x_asarr[0:-1,:,:], out = out[0:-1,:,:] ) + + if self.bnd_cond == 'Neumann': + pass + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[0,:,:], x_asarr[-1,:,:], out = out[-1,:,:] ) + else: + raise ValueError('No valid boundary conditions') + + if self.direction == 1: + + np.subtract( x_asarr[:,1:,:], x_asarr[:,0:-1,:], out = out[:,0:-1,:] ) + + if self.bnd_cond == 'Neumann': + pass + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[:,0,:], x_asarr[:,-1,:], out = out[:,-1,:] ) + else: + raise ValueError('No valid boundary conditions') + + + if self.direction == 2: + + np.subtract( x_asarr[:,:,1:], x_asarr[:,:,0:-1], out = out[:,:,0:-1] ) + + if self.bnd_cond == 'Neumann': + pass + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[:,:,0], x_asarr[:,:,-1], out = out[:,:,-1] ) + else: + raise ValueError('No valid boundary conditions') + + ######################## Direct for 4D ############################### + elif x_sz == 4: + + if self.direction == 0: + np.subtract( x_asarr[1:,:,:,:], x_asarr[0:-1,:,:,:], out = out[0:-1,:,:,:] ) + + if self.bnd_cond == 'Neumann': + pass + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[0,:,:,:], x_asarr[-1,:,:,:], out = out[-1,:,:,:] ) + else: + raise ValueError('No valid boundary conditions') + + if self.direction == 1: + np.subtract( x_asarr[:,1:,:,:], x_asarr[:,0:-1,:,:], out = out[:,0:-1,:,:] ) + + if self.bnd_cond == 'Neumann': + pass + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[:,0,:,:], x_asarr[:,-1,:,:], out = out[:,-1,:,:] ) + else: + raise ValueError('No valid boundary conditions') + + if self.direction == 2: + np.subtract( x_asarr[:,:,1:,:], x_asarr[:,:,0:-1,:], out = out[:,:,0:-1,:] ) + + if self.bnd_cond == 'Neumann': + pass + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[:,:,0,:], x_asarr[:,:,-1,:], out = out[:,:,-1,:] ) + else: + raise ValueError('No valid boundary conditions') + + if self.direction == 3: + np.subtract( x_asarr[:,:,:,1:], x_asarr[:,:,:,0:-1], out = out[:,:,:,0:-1] ) + + if self.bnd_cond == 'Neumann': + pass + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[:,:,:,0], x_asarr[:,:,:,-1], out = out[:,:,:,-1] ) + else: + raise ValueError('No valid boundary conditions') + + else: + raise NotImplementedError + +# res = out #/self.voxel_size + return type(x)(out) + + + def adjoint(self, x, out=None): + + x_asarr = x.as_array() + x_sz = len(x.shape) + + if out is None: + out = np.zeros_like(x_asarr) + else: + out = out.as_array() + out[:]=0 + + + ######################## Adjoint for 2D ############################### + if x_sz == 2: + + if self.direction == 1: + + np.subtract( x_asarr[:,1:], x_asarr[:,0:-1], out = out[:,1:] ) + + if self.bnd_cond == 'Neumann': + np.subtract( x_asarr[:,0], 0, out = out[:,0] ) + np.subtract( -x_asarr[:,-2], 0, out = out[:,-1] ) + + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[:,0], x_asarr[:,-1], out = out[:,0] ) + + else: + raise ValueError('No valid boundary conditions') + + if self.direction == 0: + + np.subtract( x_asarr[1:,:], x_asarr[0:-1,:], out = out[1:,:] ) + + if self.bnd_cond == 'Neumann': + np.subtract( x_asarr[0,:], 0, out = out[0,:] ) + np.subtract( -x_asarr[-2,:], 0, out = out[-1,:] ) + + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[0,:], x_asarr[-1,:], out = out[0,:] ) + + else: + raise ValueError('No valid boundary conditions') + + ######################## Adjoint for 3D ############################### + elif x_sz == 3: + + if self.direction == 0: + + np.subtract( x_asarr[1:,:,:], x_asarr[0:-1,:,:], out = out[1:,:,:] ) + + if self.bnd_cond == 'Neumann': + np.subtract( x_asarr[0,:,:], 0, out = out[0,:,:] ) + np.subtract( -x_asarr[-2,:,:], 0, out = out[-1,:,:] ) + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[0,:,:], x_asarr[-1,:,:], out = out[0,:,:] ) + else: + raise ValueError('No valid boundary conditions') + + if self.direction == 1: + np.subtract( x_asarr[:,1:,:], x_asarr[:,0:-1,:], out = out[:,1:,:] ) + + if self.bnd_cond == 'Neumann': + np.subtract( x_asarr[:,0,:], 0, out = out[:,0,:] ) + np.subtract( -x_asarr[:,-2,:], 0, out = out[:,-1,:] ) + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[:,0,:], x_asarr[:,-1,:], out = out[:,0,:] ) + else: + raise ValueError('No valid boundary conditions') + + if self.direction == 2: + np.subtract( x_asarr[:,:,1:], x_asarr[:,:,0:-1], out = out[:,:,1:] ) + + if self.bnd_cond == 'Neumann': + np.subtract( x_asarr[:,:,0], 0, out = out[:,:,0] ) + np.subtract( -x_asarr[:,:,-2], 0, out = out[:,:,-1] ) + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[:,:,0], x_asarr[:,:,-1], out = out[:,:,0] ) + else: + raise ValueError('No valid boundary conditions') + + ######################## Adjoint for 4D ############################### + elif x_sz == 4: + + if self.direction == 0: + np.subtract( x_asarr[1:,:,:,:], x_asarr[0:-1,:,:,:], out = out[1:,:,:,:] ) + + if self.bnd_cond == 'Neumann': + np.subtract( x_asarr[0,:,:,:], 0, out = out[0,:,:,:] ) + np.subtract( -x_asarr[-2,:,:,:], 0, out = out[-1,:,:,:] ) + + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[0,:,:,:], x_asarr[-1,:,:,:], out = out[0,:,:,:] ) + else: + raise ValueError('No valid boundary conditions') + + if self.direction == 1: + np.subtract( x_asarr[:,1:,:,:], x_asarr[:,0:-1,:,:], out = out[:,1:,:,:] ) + + if self.bnd_cond == 'Neumann': + np.subtract( x_asarr[:,0,:,:], 0, out = out[:,0,:,:] ) + np.subtract( -x_asarr[:,-2,:,:], 0, out = out[:,-1,:,:] ) + + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[:,0,:,:], x_asarr[:,-1,:,:], out = out[:,0,:,:] ) + else: + raise ValueError('No valid boundary conditions') + + + if self.direction == 2: + np.subtract( x_asarr[:,:,1:,:], x_asarr[:,:,0:-1,:], out = out[:,:,1:,:] ) + + if self.bnd_cond == 'Neumann': + np.subtract( x_asarr[:,:,0,:], 0, out = out[:,:,0,:] ) + np.subtract( -x_asarr[:,:,-2,:], 0, out = out[:,:,-1,:] ) + + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[:,:,0,:], x_asarr[:,:,-1,:], out = out[:,:,0,:] ) + else: + raise ValueError('No valid boundary conditions') + + if self.direction == 3: + np.subtract( x_asarr[:,:,:,1:], x_asarr[:,:,:,0:-1], out = out[:,:,:,1:] ) + + if self.bnd_cond == 'Neumann': + np.subtract( x_asarr[:,:,:,0], 0, out = out[:,:,:,0] ) + np.subtract( -x_asarr[:,:,:,-2], 0, out = out[:,:,:,-1] ) + + elif self.bnd_cond == 'Periodic': + np.subtract( x_asarr[:,:,:,0], x_asarr[:,:,:,-1], out = out[:,:,:,0] ) + else: + raise ValueError('No valid boundary conditions') + + else: + raise NotImplementedError + + out *= -1 #/self.voxel_size + return type(x)(out) + + def range_geometry(self): + '''Returns the range geometry''' + return self.gm_range + + def domain_geometry(self): + '''Returns the domain geometry''' + return self.gm_domain + + +if __name__ == '__main__': + + from ccpi.framework import ImageGeometry + import numpy + + N, M = 2, 3 + + ig = ImageGeometry(N, M) + + + FD = FiniteDiff(ig, direction = 1, bnd_cond = 'Neumann') + u = FD.domain_geometry().allocate('random_int') + + res = FD.domain_geometry().allocate() + res1 = FD.range_geometry().allocate() + FD.direct(u, out=res) + + z = FD.direct(u) +# print(z.as_array(), res.as_array()) + + for i in range(10): +# + z1 = FD.direct(u) + FD.direct(u, out=res) + + u = ig.allocate('random_int') + res = u + z1 = u + numpy.testing.assert_array_almost_equal(z1.as_array(), \ + res.as_array(), decimal=4) + +# print(z1.as_array(), res.as_array()) + z2 = FD.adjoint(z1) + FD.adjoint(z1, out=res1) + numpy.testing.assert_array_almost_equal(z2.as_array(), \ + res1.as_array(), decimal=4) + + + + + + + + +# w = G.range_geometry().allocate('random_int') + + + +
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/optimisation/operators/GradientOperator.py b/Wrappers/Python/ccpi/optimisation/operators/GradientOperator.py new file mode 100644 index 0000000..cd58b7d --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/GradientOperator.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Mar 1 22:50:04 2019 + +@author: evangelos +""" + +from ccpi.optimisation.operators import Operator, LinearOperator, ScaledOperator +from ccpi.framework import ImageData, ImageGeometry, BlockGeometry, BlockDataContainer +import numpy +from ccpi.optimisation.operators import FiniteDiff, SparseFiniteDiff + +#%% + +class Gradient(LinearOperator): + CORRELATION_SPACE = "Space" + CORRELATION_SPACECHANNEL = "SpaceChannels" + # Grad_order = ['channels', 'direction_z', 'direction_y', 'direction_x'] + # Grad_order = ['channels', 'direction_y', 'direction_x'] + # Grad_order = ['direction_z', 'direction_y', 'direction_x'] + # Grad_order = ['channels', 'direction_z', 'direction_y', 'direction_x'] + def __init__(self, gm_domain, bnd_cond = 'Neumann', **kwargs): + + super(Gradient, self).__init__() + + self.gm_domain = gm_domain # Domain of Grad Operator + + self.correlation = kwargs.get('correlation',Gradient.CORRELATION_SPACE) + + if self.correlation==Gradient.CORRELATION_SPACE: + if self.gm_domain.channels>1: + self.gm_range = BlockGeometry(*[self.gm_domain for _ in range(self.gm_domain.length-1)] ) + if self.gm_domain.length == 4: + # 3D + Channel + # expected Grad_order = ['channels', 'direction_z', 'direction_y', 'direction_x'] + expected_order = [ImageGeometry.CHANNEL, ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] + else: + # 2D + Channel + # expected Grad_order = ['channels', 'direction_y', 'direction_x'] + expected_order = [ImageGeometry.CHANNEL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] + order = self.gm_domain.get_order_by_label(self.gm_domain.dimension_labels, expected_order) + self.ind = order[1:] + #self.ind = numpy.arange(1,self.gm_domain.length) + else: + # no channel info + self.gm_range = BlockGeometry(*[self.gm_domain for _ in range(self.gm_domain.length) ] ) + if self.gm_domain.length == 3: + # 3D + # expected Grad_order = ['direction_z', 'direction_y', 'direction_x'] + expected_order = [ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] + else: + # 2D + expected_order = [ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] + self.ind = self.gm_domain.get_order_by_label(self.gm_domain.dimension_labels, expected_order) + # self.ind = numpy.arange(self.gm_domain.length) + elif self.correlation==Gradient.CORRELATION_SPACECHANNEL: + if self.gm_domain.channels>1: + self.gm_range = BlockGeometry(*[self.gm_domain for _ in range(self.gm_domain.length)]) + self.ind = range(self.gm_domain.length) + else: + raise ValueError('No channels to correlate') + + self.bnd_cond = bnd_cond + + # Call FiniteDiff operator + + self.FD = FiniteDiff(self.gm_domain, direction = 0, bnd_cond = self.bnd_cond) + + + def direct(self, x, out=None): + + + if out is not None: + + for i in range(self.gm_range.shape[0]): + self.FD.direction = self.ind[i] + self.FD.direct(x, out = out[i]) + else: + tmp = self.gm_range.allocate() + for i in range(tmp.shape[0]): + self.FD.direction=self.ind[i] + tmp.get_item(i).fill(self.FD.direct(x)) + return tmp + + def adjoint(self, x, out=None): + + if out is not None: + + tmp = self.gm_domain.allocate() + for i in range(x.shape[0]): + self.FD.direction=self.ind[i] + self.FD.adjoint(x.get_item(i), out = tmp) + if i == 0: + out.fill(tmp) + else: + out += tmp + else: + tmp = self.gm_domain.allocate() + for i in range(x.shape[0]): + self.FD.direction=self.ind[i] + + tmp += self.FD.adjoint(x.get_item(i)) + return tmp + + + def domain_geometry(self): + return self.gm_domain + + def range_geometry(self): + return self.gm_range + + def __rmul__(self, scalar): + return ScaledOperator(self, scalar) + + ########################################################################### + ############### For preconditioning ###################################### + ########################################################################### + def matrix(self): + + tmp = self.gm_range.allocate() + + mat = [] + for i in range(tmp.shape[0]): + + spMat = SparseFiniteDiff(self.gm_domain, direction=self.ind[i], bnd_cond=self.bnd_cond) + mat.append(spMat.matrix()) + + return BlockDataContainer(*mat) + + + def sum_abs_col(self): + + tmp = self.gm_range.allocate() + res = self.gm_domain.allocate() + for i in range(tmp.shape[0]): + spMat = SparseFiniteDiff(self.gm_domain, direction=self.ind[i], bnd_cond=self.bnd_cond) + res += spMat.sum_abs_row() + return res + + def sum_abs_row(self): + + tmp = self.gm_range.allocate() + res = [] + for i in range(tmp.shape[0]): + spMat = SparseFiniteDiff(self.gm_domain, direction=self.ind[i], bnd_cond=self.bnd_cond) + res.append(spMat.sum_abs_col()) + return BlockDataContainer(*res) + + +if __name__ == '__main__': + + + from ccpi.optimisation.operators import Identity, BlockOperator + + + M, N = 20, 30 + ig = ImageGeometry(M, N) + arr = ig.allocate('random_int' ) + + # check direct of Gradient and sparse matrix + G = Gradient(ig) + norm1 = G.norm(iterations=300) + print ("should be sqrt(8) {} {}".format(numpy.sqrt(8), norm1)) + G_sp = G.matrix() + ig4 = ImageGeometry(M,N, channels=3) + G4 = Gradient(ig4, correlation=Gradient.CORRELATION_SPACECHANNEL) + norm4 = G4.norm(iterations=300) + print ("should be sqrt(12) {} {}".format(numpy.sqrt(12), norm4)) + + + res1 = G.direct(arr) + res1y = numpy.reshape(G_sp[0].toarray().dot(arr.as_array().flatten('F')), ig.shape, 'F') + + print(res1[0].as_array()) + print(res1y) + + res1x = numpy.reshape(G_sp[1].toarray().dot(arr.as_array().flatten('F')), ig.shape, 'F') + + print(res1[1].as_array()) + print(res1x) + + #check sum abs row + conc_spmat = numpy.abs(numpy.concatenate( (G_sp[0].toarray(), G_sp[1].toarray() ))) + print(numpy.reshape(conc_spmat.sum(axis=0), ig.shape, 'F')) + print(G.sum_abs_row().as_array()) + + print(numpy.reshape(conc_spmat.sum(axis=1), ((2,) + ig.shape), 'F')) + + print(G.sum_abs_col()[0].as_array()) + print(G.sum_abs_col()[1].as_array()) + + # Check Blockoperator sum abs col and row + + op1 = Gradient(ig) + op2 = Identity(ig) + + B = BlockOperator( op1, op2) + + Brow = B.sum_abs_row() + Bcol = B.sum_abs_col() + + concB = numpy.concatenate( (numpy.abs(numpy.concatenate( (G_sp[0].toarray(), G_sp[1].toarray() ))), op2.matrix().toarray())) + + print(numpy.reshape(concB.sum(axis=0), ig.shape, 'F')) + print(Brow.as_array()) + + print(numpy.reshape(concB.sum(axis=1)[0:12], ((2,) + ig.shape), 'F')) + print(Bcol[1].as_array()) + + +# print(numpy.concatene(G_sp[0].toarray()+ )) +# print(G_sp[1].toarray()) +# +# d1 = G.sum_abs_row() +# print(d1.as_array()) +# +# d2 = G_neum.sum_abs_col() +## print(d2) +# +# +# ########################################################### + a = BlockDataContainer( BlockDataContainer(arr, arr), arr) + b = BlockDataContainer( BlockDataContainer(arr+5, arr+3), arr+2) + c = a/b + + print(c[0][0].as_array(), (arr/(arr+5)).as_array()) + print(c[0][1].as_array(), (arr/(arr+3)).as_array()) + print(c[1].as_array(), (arr/(arr+2)).as_array()) + + + a1 = BlockDataContainer( arr, BlockDataContainer(arr, arr)) +# +# c1 = arr + a +# c2 = arr + a +# c2 = a1 + arr + + from ccpi.framework import ImageGeometry +# from ccpi.optimisation.operators import Gradient +# + N, M = 2, 3 +# + ig = ImageGeometry(N, M) +# + G = Gradient(ig) +# + u = G.domain_geometry().allocate('random_int') + w = G.range_geometry().allocate('random_int') + + + print( "################ without out #############") + + print( (G.direct(u)*w).sum(), (u*G.adjoint(w)).sum() ) + + + print( "################ with out #############") + + res = G.range_geometry().allocate() + res1 = G.domain_geometry().allocate() + G.direct(u, out = res) + G.adjoint(w, out = res1) + + print( (res*w).sum(), (u*res1).sum() ) + + + + diff --git a/Wrappers/Python/ccpi/optimisation/operators/IdentityOperator.py b/Wrappers/Python/ccpi/optimisation/operators/IdentityOperator.py new file mode 100644 index 0000000..8f35373 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/IdentityOperator.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Mar 6 19:30:51 2019 + +@author: evangelos +""" + +from ccpi.optimisation.operators import LinearOperator +import scipy.sparse as sp +import numpy as np +from ccpi.framework import ImageData + + +class Identity(LinearOperator): + + def __init__(self, gm_domain, gm_range=None): + + self.gm_domain = gm_domain + self.gm_range = gm_range + if self.gm_range is None: + self.gm_range = self.gm_domain + + super(Identity, self).__init__() + + def direct(self,x,out=None): + if out is None: + return x.copy() + else: + out.fill(x) + + def adjoint(self,x, out=None): + if out is None: + return x.copy() + else: + out.fill(x) + + def calculate_norm(self, **kwargs): + return 1.0 + + def domain_geometry(self): + return self.gm_domain + + def range_geometry(self): + return self.gm_range + + def matrix(self): + + return sp.eye(np.prod(self.gm_domain.shape)) + + def sum_abs_row(self): + + return self.gm_range.allocate(1)#ImageData(np.array(np.reshape(abs(self.matrix()).sum(axis=0), self.gm_domain.shape, 'F'))) + + def sum_abs_col(self): + + return self.gm_domain.allocate(1)#ImageData(np.array(np.reshape(abs(self.matrix()).sum(axis=1), self.gm_domain.shape, 'F'))) + + +if __name__ == '__main__': + + from ccpi.framework import ImageGeometry + + M, N = 2, 3 + ig = ImageGeometry(M, N) + arr = ig.allocate('random_int') + + Id = Identity(ig) + d = Id.matrix() + print(d.toarray()) + + d1 = Id.sum_abs_col() + print(d1.as_array()) + + + + + +
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/optimisation/operators/LinearOperator.py b/Wrappers/Python/ccpi/optimisation/operators/LinearOperator.py new file mode 100755 index 0000000..55eb692 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/LinearOperator.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*-
+"""
+Created on Tue Mar 5 15:57:52 2019
+
+@author: ofn77899
+"""
+
+from ccpi.optimisation.operators import Operator
+from ccpi.framework import ImageGeometry
+import numpy
+
+
+class LinearOperator(Operator):
+ '''A Linear Operator that maps from a space X <-> Y'''
+ def __init__(self):
+ super(LinearOperator, self).__init__()
+ def is_linear(self):
+ '''Returns if the operator is linear'''
+ return True
+ def adjoint(self,x, out=None):
+ '''returns the adjoint/inverse operation
+
+ only available to linear operators'''
+ raise NotImplementedError
+
+ @staticmethod
+ def PowerMethod(operator, iterations, x_init=None):
+ '''Power method to calculate iteratively the Lipschitz constant'''
+
+ # Initialise random
+ if x_init is None:
+ x0 = operator.domain_geometry().allocate(type(operator.domain_geometry()).RANDOM_INT)
+ else:
+ x0 = x_init.copy()
+
+ x1 = operator.domain_geometry().allocate()
+ y_tmp = operator.range_geometry().allocate()
+ s = numpy.zeros(iterations)
+ # Loop
+ for it in numpy.arange(iterations):
+ operator.direct(x0,out=y_tmp)
+ operator.adjoint(y_tmp,out=x1)
+ x1norm = x1.norm()
+ s[it] = x1.dot(x0) / x0.squared_norm()
+ x1.multiply((1.0/x1norm), out=x0)
+ return numpy.sqrt(s[-1]), numpy.sqrt(s), x0
+
+ def calculate_norm(self, **kwargs):
+ '''Returns the norm of the LinearOperator as calculated by the PowerMethod'''
+ x0 = kwargs.get('x0', None)
+ iterations = kwargs.get('iterations', 25)
+ s1, sall, svec = LinearOperator.PowerMethod(self, iterations, x_init=x0)
+ return s1
+
+
diff --git a/Wrappers/Python/ccpi/optimisation/operators/LinearOperatorMatrix.py b/Wrappers/Python/ccpi/optimisation/operators/LinearOperatorMatrix.py new file mode 100644 index 0000000..90ef938 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/LinearOperatorMatrix.py @@ -0,0 +1,41 @@ +import numpy +from scipy.sparse.linalg import svds +from ccpi.framework import DataContainer +from ccpi.framework import AcquisitionData +from ccpi.framework import VectorData +from ccpi.framework import VectorGeometry +from ccpi.framework import AcquisitionGeometry +from numbers import Number +from ccpi.optimisation.operators import LinearOperator +class LinearOperatorMatrix(LinearOperator): + def __init__(self,A): + self.A = A + M_A, N_A = self.A.shape + self.gm_domain = VectorGeometry(N_A) + self.gm_range = VectorGeometry(M_A) + self.s1 = None # Largest singular value, initially unknown + super(LinearOperatorMatrix, self).__init__() + + def direct(self,x, out=None): + if out is None: + return type(x)(numpy.dot(self.A,x.as_array())) + else: + numpy.dot(self.A, x.as_array(), out=out.as_array()) + + def adjoint(self,x, out=None): + if out is None: + return type(x)(numpy.dot(self.A.transpose(),x.as_array())) + else: + numpy.dot(self.A.transpose(),x.as_array(), out=out.as_array()) + + def size(self): + return self.A.shape + + def calculate_norm(self, **kwargs): + # If unknown, compute and store. If known, simply return it. + return svds(self.A,1,return_singular_vectors=False)[0] + + def domain_geometry(self): + return self.gm_domain + def range_geometry(self): + return self.gm_range diff --git a/Wrappers/Python/ccpi/optimisation/operators/Operator.py b/Wrappers/Python/ccpi/optimisation/operators/Operator.py new file mode 100755 index 0000000..c1adf62 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/Operator.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*-
+"""
+Created on Tue Mar 5 15:55:56 2019
+
+@author: ofn77899
+"""
+from ccpi.optimisation.operators.ScaledOperator import ScaledOperator
+
+class Operator(object):
+ '''Operator that maps from a space X -> Y'''
+ def __init__(self, **kwargs):
+ self.__norm = None
+
+ def is_linear(self):
+ '''Returns if the operator is linear'''
+ return False
+ def direct(self,x, out=None):
+ '''Returns the application of the Operator on x'''
+ raise NotImplementedError
+ def norm(self, **kwargs):
+ '''Returns the norm of the Operator'''
+ if self.__norm is None:
+ self.__norm = self.calculate_norm(**kwargs)
+ return self.__norm
+ def calculate_norm(self, **kwargs):
+ '''Calculates the norm of the Operator'''
+ raise NotImplementedError
+ def range_geometry(self):
+ '''Returns the range of the Operator: Y space'''
+ raise NotImplementedError
+ def domain_geometry(self):
+ '''Returns the domain of the Operator: X space'''
+ raise NotImplementedError
+ def __rmul__(self, scalar):
+ '''Defines the multiplication by a scalar on the left
+
+ returns a ScaledOperator'''
+ return ScaledOperator(self, scalar)
diff --git a/Wrappers/Python/ccpi/optimisation/operators/ScaledOperator.py b/Wrappers/Python/ccpi/optimisation/operators/ScaledOperator.py new file mode 100644 index 0000000..f7be5de --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/ScaledOperator.py @@ -0,0 +1,51 @@ +from numbers import Number +import numpy + +class ScaledOperator(object): + '''ScaledOperator + A class to represent the scalar multiplication of an Operator with a scalar. + It holds an operator and a scalar. Basically it returns the multiplication + of the result of direct and adjoint of the operator with the scalar. + For the rest it behaves like the operator it holds. + Args: + operator (Operator): a Operator or LinearOperator + scalar (Number): a scalar multiplier + Example: + The scaled operator behaves like the following: + sop = ScaledOperator(operator, scalar) + sop.direct(x) = scalar * operator.direct(x) + sop.adjoint(x) = scalar * operator.adjoint(x) + sop.norm() = operator.norm() + sop.range_geometry() = operator.range_geometry() + sop.domain_geometry() = operator.domain_geometry() + ''' + def __init__(self, operator, scalar): + super(ScaledOperator, self).__init__() + if not isinstance (scalar, Number): + raise TypeError('expected scalar: got {}'.format(type(scalar))) + self.scalar = scalar + self.operator = operator + def direct(self, x, out=None): + if out is None: + return self.scalar * self.operator.direct(x, out=out) + else: + self.operator.direct(x, out=out) + out *= self.scalar + def adjoint(self, x, out=None): + if self.operator.is_linear(): + if out is None: + return self.scalar * self.operator.adjoint(x, out=out) + else: + self.operator.adjoint(x, out=out) + out *= self.scalar + else: + raise TypeError('Operator is not linear') + def norm(self, **kwargs): + return numpy.abs(self.scalar) * self.operator.norm(**kwargs) + def range_geometry(self): + return self.operator.range_geometry() + def domain_geometry(self): + return self.operator.domain_geometry() + def is_linear(self): + return self.operator.is_linear() + diff --git a/Wrappers/Python/ccpi/optimisation/operators/ShrinkageOperator.py b/Wrappers/Python/ccpi/optimisation/operators/ShrinkageOperator.py new file mode 100644 index 0000000..f47c655 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/ShrinkageOperator.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Mar 6 19:30:51 2019 + +@author: evangelos +""" + +from ccpi.framework import DataContainer + +class ShrinkageOperator(): + + def __init__(self): + pass + + def __call__(self, x, tau, out=None): + + return x.sign() * (x.abs() - tau).maximum(0) +
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/optimisation/operators/SparseFiniteDiff.py b/Wrappers/Python/ccpi/optimisation/operators/SparseFiniteDiff.py new file mode 100644 index 0000000..cb76dce --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/SparseFiniteDiff.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Tue Apr 2 14:06:15 2019 + +@author: vaggelis +""" + +import scipy.sparse as sp +import numpy as np +from ccpi.framework import ImageData + +class SparseFiniteDiff(object): + + def __init__(self, gm_domain, gm_range=None, direction=0, bnd_cond = 'Neumann'): + + super(SparseFiniteDiff, self).__init__() + self.gm_domain = gm_domain + self.gm_range = gm_range + self.direction = direction + self.bnd_cond = bnd_cond + + if self.gm_range is None: + self.gm_range = self.gm_domain + + self.get_dims = [i for i in gm_domain.shape] + + if self.direction + 1 > len(self.gm_domain.shape): + raise ValueError('Gradient directions more than geometry domain') + + def matrix(self): + + i = self.direction + + mat = sp.spdiags(np.vstack([-np.ones((1,self.get_dims[i])),np.ones((1,self.get_dims[i]))]), [0,1], self.get_dims[i], self.get_dims[i], format = 'lil') + + if self.bnd_cond == 'Neumann': + mat[-1,:] = 0 + elif self.bnd_cond == 'Periodic': + mat[-1,0] = 1 + + tmpGrad = mat if i == 0 else sp.eye(self.get_dims[0]) + + for j in range(1, self.gm_domain.length): + + tmpGrad = sp.kron(mat, tmpGrad ) if j == i else sp.kron(sp.eye(self.get_dims[j]), tmpGrad ) + + return tmpGrad + + def T(self): + return self.matrix().T + + def direct(self, x): + + x_asarr = x.as_array() + res = np.reshape( self.matrix() * x_asarr.flatten('F'), self.gm_domain.shape, 'F') + return type(x)(res) + + def adjoint(self, x): + + x_asarr = x.as_array() + res = np.reshape( self.matrix().T * x_asarr.flatten('F'), self.gm_domain.shape, 'F') + return type(x)(res) + + def sum_abs_row(self): + + res = np.array(np.reshape(abs(self.matrix()).sum(axis=0), self.gm_domain.shape, 'F')) + #res[res==0]=0 + return ImageData(res) + + def sum_abs_col(self): + + res = np.array(np.reshape(abs(self.matrix()).sum(axis=1), self.gm_domain.shape, 'F') ) + #res[res==0]=0 + return ImageData(res) + +if __name__ == '__main__': + + from ccpi.framework import ImageGeometry + from ccpi.optimisation.operators import FiniteDiff + + # 2D + M, N= 2, 3 + ig = ImageGeometry(M, N) + arr = ig.allocate('random_int') + + for i in [0,1]: + + # Neumann + sFD_neum = SparseFiniteDiff(ig, direction=i, bnd_cond='Neumann') + G_neum = FiniteDiff(ig, direction=i, bnd_cond='Neumann') + + # Periodic + sFD_per = SparseFiniteDiff(ig, direction=i, bnd_cond='Periodic') + G_per = FiniteDiff(ig, direction=i, bnd_cond='Periodic') + + u_neum_direct = G_neum.direct(arr) + u_neum_sp_direct = sFD_neum.direct(arr) + np.testing.assert_array_almost_equal(u_neum_direct.as_array(), u_neum_sp_direct.as_array(), decimal=4) + + u_neum_adjoint = G_neum.adjoint(arr) + u_neum_sp_adjoint = sFD_neum.adjoint(arr) + np.testing.assert_array_almost_equal(u_neum_adjoint.as_array(), u_neum_sp_adjoint.as_array(), decimal=4) + + u_per_direct = G_neum.direct(arr) + u_per_sp_direct = sFD_neum.direct(arr) + np.testing.assert_array_almost_equal(u_per_direct.as_array(), u_per_sp_direct.as_array(), decimal=4) + + u_per_adjoint = G_per.adjoint(arr) + u_per_sp_adjoint = sFD_per.adjoint(arr) + np.testing.assert_array_almost_equal(u_per_adjoint.as_array(), u_per_sp_adjoint.as_array(), decimal=4) + + # 3D + M, N, K = 2, 3, 4 + ig3D = ImageGeometry(M, N, K) + arr3D = ig3D.allocate('random_int') + + for i in [0,1,2]: + + # Neumann + sFD_neum3D = SparseFiniteDiff(ig3D, direction=i, bnd_cond='Neumann') + G_neum3D = FiniteDiff(ig3D, direction=i, bnd_cond='Neumann') + + # Periodic + sFD_per3D = SparseFiniteDiff(ig3D, direction=i, bnd_cond='Periodic') + G_per3D = FiniteDiff(ig3D, direction=i, bnd_cond='Periodic') + + u_neum_direct3D = G_neum3D.direct(arr3D) + u_neum_sp_direct3D = sFD_neum3D.direct(arr3D) + np.testing.assert_array_almost_equal(u_neum_direct3D.as_array(), u_neum_sp_direct3D.as_array(), decimal=4) + + u_neum_adjoint3D = G_neum3D.adjoint(arr3D) + u_neum_sp_adjoint3D = sFD_neum3D.adjoint(arr3D) + np.testing.assert_array_almost_equal(u_neum_adjoint3D.as_array(), u_neum_sp_adjoint3D.as_array(), decimal=4) + + u_per_direct3D = G_neum3D.direct(arr3D) + u_per_sp_direct3D = sFD_neum3D.direct(arr3D) + np.testing.assert_array_almost_equal(u_per_direct3D.as_array(), u_per_sp_direct3D.as_array(), decimal=4) + + u_per_adjoint3D = G_per3D.adjoint(arr3D) + u_per_sp_adjoint3D = sFD_per3D.adjoint(arr3D) + np.testing.assert_array_almost_equal(u_per_adjoint3D.as_array(), u_per_sp_adjoint3D.as_array(), decimal=4) + +
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/optimisation/operators/SymmetrizedGradientOperator.py b/Wrappers/Python/ccpi/optimisation/operators/SymmetrizedGradientOperator.py new file mode 100644 index 0000000..205f7e1 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/SymmetrizedGradientOperator.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Mar 1 22:53:55 2019 + +@author: evangelos +""" + +from ccpi.optimisation.operators import Gradient, Operator, LinearOperator, ScaledOperator +from ccpi.framework import ImageData, ImageGeometry, BlockGeometry, BlockDataContainer +import numpy +from ccpi.optimisation.operators import FiniteDiff, SparseFiniteDiff + + +class SymmetrizedGradient(Gradient): + + ''' Symmetrized Gradient, denoted by E: V --> W + where V is the Range of the Gradient Operator + and W is the Range of the Symmetrized Gradient. + ''' + + + def __init__(self, gm_domain, bnd_cond = 'Neumann', **kwargs): + + super(SymmetrizedGradient, self).__init__(gm_domain, bnd_cond, **kwargs) + + ''' + Domain of SymGrad is the Range of Gradient + ''' + + self.gm_domain = self.gm_range + self.bnd_cond = bnd_cond + + self.channels = self.gm_range.get_item(0).channels + + tmp_gm = len(self.gm_domain.geometries)*self.gm_domain.geometries + + self.gm_range = BlockGeometry(*tmp_gm) + + self.FD = FiniteDiff(self.gm_domain, direction = 0, bnd_cond = self.bnd_cond) + + if self.gm_domain.shape[0]==2: + self.order_ind = [0,2,1,3] + else: + self.order_ind = [0,3,6,1,4,7,2,5,8] + + + def direct(self, x, out=None): + + if out is None: + + tmp = [] + for i in range(self.gm_domain.shape[0]): + for j in range(x.shape[0]): + self.FD.direction = i + tmp.append(self.FD.adjoint(x.get_item(j))) + + tmp1 = [tmp[i] for i in self.order_ind] + + res = [0.5 * sum(x) for x in zip(tmp, tmp1)] + + return BlockDataContainer(*res) + + else: + + ind = 0 + for i in range(self.gm_domain.shape[0]): + for j in range(x.shape[0]): + self.FD.direction = i + self.FD.adjoint(x.get_item(j), out=out[ind]) + ind+=1 + out1 = BlockDataContainer(*[out[i] for i in self.order_ind]) + out.fill( 0.5 * (out + out1) ) + + + def adjoint(self, x, out=None): + + if out is None: + + tmp = [None]*self.gm_domain.shape[0] + i = 0 + + for k in range(self.gm_domain.shape[0]): + tmp1 = 0 + for j in range(self.gm_domain.shape[0]): + self.FD.direction = j + tmp1 += self.FD.direct(x[i]) + i+=1 + tmp[k] = tmp1 + return BlockDataContainer(*tmp) + + + else: + + tmp = self.gm_domain.allocate() + i = 0 + for k in range(self.gm_domain.shape[0]): + tmp1 = 0 + for j in range(self.gm_domain.shape[0]): + self.FD.direction = j + self.FD.direct(x[i], out=tmp[j]) + i+=1 + tmp1+=tmp[j] + out[k].fill(tmp1) +# tmp = self.adjoint(x) +# out.fill(tmp) + + + def domain_geometry(self): + return self.gm_domain + + def range_geometry(self): + return self.gm_range + +if __name__ == '__main__': + + ########################################################################### + ## Symmetrized Gradient Tests + from ccpi.framework import DataContainer + from ccpi.optimisation.operators import Gradient, BlockOperator, FiniteDiff + import numpy as np + + N, M = 2, 3 + K = 2 + C = 2 + + ig1 = ImageGeometry(N, M) + ig2 = ImageGeometry(N, M, channels=C) + + E1 = SymmetrizedGradient(ig1, correlation = 'Space', bnd_cond='Neumann') + + try: + E1 = SymmetrizedGradient(ig1, correlation = 'SpaceChannels', bnd_cond='Neumann') + except: + print("No Channels to correlate") + + E2 = SymmetrizedGradient(ig2, correlation = 'SpaceChannels', bnd_cond='Neumann') + + print(E1.domain_geometry().shape, E1.range_geometry().shape) + print(E2.domain_geometry().shape, E2.range_geometry().shape) + + #check Linear operator property + + u1 = E1.domain_geometry().allocate('random_int') + u2 = E2.domain_geometry().allocate('random_int') + + # Need to allocate random_int at the Range of SymGradient + + #a1 = ig1.allocate('random_int') + #a2 = ig1.allocate('random_int') + #a3 = ig1.allocate('random_int') + + #a4 = ig1.allocate('random_int') + #a5 = ig1.allocate('random_int') + #a6 = ig1.allocate('random_int') + + # TODO allocate has to create this symmetry by default!!!!! + #w1 = BlockDataContainer(*[a1, a2, \ + # a2, a3]) + w1 = E1.range_geometry().allocate('random_int',symmetry=True) + + LHS = (E1.direct(u1) * w1).sum() + RHS = (u1 * E1.adjoint(w1)).sum() + + numpy.testing.assert_equal(LHS, RHS) + + u2 = E2.gm_domain.allocate('random_int') + + #aa1 = ig2.allocate('random_int') + #aa2 = ig2.allocate('random_int') + #aa3 = ig2.allocate('random_int') + #aa4 = ig2.allocate('random_int') + #aa5 = ig2.allocate('random_int') + #aa6 = ig2.allocate('random_int') + + #w2 = BlockDataContainer(*[aa1, aa2, aa3, \ + # aa2, aa4, aa5, \ + # aa3, aa5, aa6]) + w2 = E2.range_geometry().allocate('random_int',symmetry=True) + + + LHS1 = (E2.direct(u2) * w2).sum() + RHS1 = (u2 * E2.adjoint(w2)).sum() + + numpy.testing.assert_equal(LHS1, RHS1) + + out = E1.range_geometry().allocate() + E1.direct(u1, out=out) + a1 = E1.direct(u1) + numpy.testing.assert_array_equal(a1[0].as_array(), out[0].as_array()) + numpy.testing.assert_array_equal(a1[1].as_array(), out[1].as_array()) + numpy.testing.assert_array_equal(a1[2].as_array(), out[2].as_array()) + numpy.testing.assert_array_equal(a1[3].as_array(), out[3].as_array()) + + + out1 = E1.domain_geometry().allocate() + E1.adjoint(w1, out=out1) + b1 = E1.adjoint(w1) + + LHS_out = (out * w1).sum() + RHS_out = (u1 * out1).sum() + print(LHS_out, RHS_out) + + + out2 = E2.range_geometry().allocate() + E2.direct(u2, out=out2) + a2 = E2.direct(u2) + + out21 = E2.domain_geometry().allocate() + E2.adjoint(w2, out=out21) + b2 = E2.adjoint(w2) + + LHS_out = (out2 * w2).sum() + RHS_out = (u2 * out21).sum() + print(LHS_out, RHS_out) + + + out = E1.range_geometry().allocate() + E1.direct(u1, out=out) + E1.adjoint(out, out=out1) + + print(E1.norm()) + print(E2.norm()) + + + + + + +# +# +# +# diff --git a/Wrappers/Python/ccpi/optimisation/operators/ZeroOperator.py b/Wrappers/Python/ccpi/optimisation/operators/ZeroOperator.py new file mode 100644 index 0000000..67808de --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/ZeroOperator.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Mar 6 19:25:53 2019 + +@author: evangelos +""" + +import numpy as np +from ccpi.framework import ImageData +from ccpi.optimisation.operators import LinearOperator + +class ZeroOperator(LinearOperator): + + def __init__(self, gm_domain, gm_range=None): + + super(ZeroOperator, self).__init__() + + self.gm_domain = gm_domain + self.gm_range = gm_range + if self.gm_range is None: + self.gm_range = self.gm_domain + + + def direct(self,x,out=None): + if out is None: + return self.gm_range.allocate() + else: + out.fill(self.gm_range.allocate()) + + def adjoint(self,x, out=None): + if out is None: + return self.gm_domain.allocate() + else: + out.fill(self.gm_domain.allocate()) + + def calculate_norm(self, **kwargs): + return 0. + + def domain_geometry(self): + return self.gm_domain + + def range_geometry(self): + return self.gm_range
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/optimisation/operators/__init__.py b/Wrappers/Python/ccpi/optimisation/operators/__init__.py new file mode 100755 index 0000000..23222d4 --- /dev/null +++ b/Wrappers/Python/ccpi/optimisation/operators/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*-
+"""
+Created on Tue Mar 5 15:56:27 2019
+
+@author: ofn77899
+"""
+
+from .Operator import Operator
+from .LinearOperator import LinearOperator
+from .ScaledOperator import ScaledOperator
+from .BlockOperator import BlockOperator
+from .BlockScaledOperator import BlockScaledOperator
+
+from .SparseFiniteDiff import SparseFiniteDiff
+from .ShrinkageOperator import ShrinkageOperator
+
+from .FiniteDifferenceOperator import FiniteDiff
+from .GradientOperator import Gradient
+from .SymmetrizedGradientOperator import SymmetrizedGradient
+from .IdentityOperator import Identity
+from .ZeroOperator import ZeroOperator
+from .LinearOperatorMatrix import LinearOperatorMatrix
+
diff --git a/Wrappers/Python/ccpi/optimisation/ops.py b/Wrappers/Python/ccpi/optimisation/ops.py deleted file mode 100755 index e9e7f44..0000000 --- a/Wrappers/Python/ccpi/optimisation/ops.py +++ /dev/null @@ -1,289 +0,0 @@ -# -*- coding: utf-8 -*- -# This work is part of the Core Imaging Library developed by -# Visual Analytics and Imaging System Group of the Science Technology -# Facilities Council, STFC - -# Copyright 2018 Jakob Jorgensen, Daniil Kazantsev and Edoardo Pasca - -# 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. - -import numpy -from scipy.sparse.linalg import svds -from ccpi.framework import DataContainer -from ccpi.framework import AcquisitionData -from ccpi.framework import ImageData -from ccpi.framework import ImageGeometry -from ccpi.framework import AcquisitionGeometry -from numbers import Number -# Maybe operators need to know what types they take as inputs/outputs -# to not just use generic DataContainer - - -class Operator(object): - '''Operator that maps from a space X -> Y''' - def __init__(self, **kwargs): - self.scalar = 1 - def is_linear(self): - '''Returns if the operator is linear''' - return False - def direct(self,x, out=None): - raise NotImplementedError - def size(self): - # To be defined for specific class - raise NotImplementedError - def norm(self): - raise NotImplementedError - def allocate_direct(self): - '''Allocates memory on the Y space''' - raise NotImplementedError - def allocate_adjoint(self): - '''Allocates memory on the X space''' - raise NotImplementedError - def range_dim(self): - raise NotImplementedError - def domain_dim(self): - raise NotImplementedError - def __rmul__(self, other): - '''reverse multiplication of Operator with number sets the variable scalar in the Operator''' - assert isinstance(other, Number) - self.scalar = other - return self - -class LinearOperator(Operator): - '''Operator that maps from a space X -> Y''' - def is_linear(self): - '''Returns if the operator is linear''' - return True - def adjoint(self,x, out=None): - raise NotImplementedError - -class Identity(Operator): - def __init__(self): - self.s1 = 1.0 - self.L = 1 - super(Identity, self).__init__() - - def direct(self,x,out=None): - if out is None: - return x.copy() - else: - out.fill(x) - - def adjoint(self,x, out=None): - if out is None: - return x.copy() - else: - out.fill(x) - - def size(self): - return NotImplemented - - def get_max_sing_val(self): - return self.s1 - -class TomoIdentity(Operator): - def __init__(self, geometry, **kwargs): - super(TomoIdentity, self).__init__() - self.s1 = 1.0 - self.geometry = geometry - - - def direct(self,x,out=None): - - if out is None: - if self.scalar != 1: - return x * self.scalar - return x.copy() - else: - if self.scalar != 1: - out.fill(x * self.scalar) - return - out.fill(x) - return - - def adjoint(self,x, out=None): - return self.direct(x, out) - - def size(self): - return NotImplemented - - def get_max_sing_val(self): - return self.s1 - def allocate_direct(self): - if issubclass(type(self.geometry), ImageGeometry): - return ImageData(geometry=self.geometry) - elif issubclass(type(self.geometry), AcquisitionGeometry): - return AcquisitionData(geometry=self.geometry) - else: - raise ValueError("Wrong geometry type: expected ImageGeometry of AcquisitionGeometry, got ", type(self.geometry)) - def allocate_adjoint(self): - return self.allocate_direct() - - - -class FiniteDiff2D(Operator): - def __init__(self): - self.s1 = 8.0 - super(FiniteDiff2D, self).__init__() - - def direct(self,x, out=None): - '''Forward differences with Neumann BC.''' - # FIXME this seems to be working only with numpy arrays - - d1 = numpy.zeros_like(x.as_array()) - d1[:,:-1] = x.as_array()[:,1:] - x.as_array()[:,:-1] - d2 = numpy.zeros_like(x.as_array()) - d2[:-1,:] = x.as_array()[1:,:] - x.as_array()[:-1,:] - d = numpy.stack((d1,d2),0) - #x.geometry.voxel_num_z = 2 - return type(x)(d,False,geometry=x.geometry) - - def adjoint(self,x, out=None): - '''Backward differences, Neumann BC.''' - Nrows = x.get_dimension_size('horizontal_x') - Ncols = x.get_dimension_size('horizontal_y') - Nchannels = 1 - if len(x.shape) == 4: - Nchannels = x.get_dimension_size('channel') - zer = numpy.zeros((Nrows,1)) - xxx = x.as_array()[0,:,:-1] - # - h = numpy.concatenate((zer,xxx), 1) - h -= numpy.concatenate((xxx,zer), 1) - - zer = numpy.zeros((1,Ncols)) - xxx = x.as_array()[1,:-1,:] - # - v = numpy.concatenate((zer,xxx), 0) - v -= numpy.concatenate((xxx,zer), 0) - return type(x)(h + v, False, geometry=x.geometry) - - def size(self): - return NotImplemented - - def get_max_sing_val(self): - return self.s1 - -def PowerMethodNonsquareOld(op,numiters): - # Initialise random - # Jakob's - #inputsize = op.size()[1] - #x0 = ImageContainer(numpy.random.randn(*inputsize) - # Edo's - #vg = ImageGeometry(voxel_num_x=inputsize[0], - # voxel_num_y=inputsize[1], - # voxel_num_z=inputsize[2]) - # - #x0 = ImageData(geometry = vg, dimension_labels=['vertical','horizontal_y','horizontal_x']) - #print (x0) - #x0.fill(numpy.random.randn(*x0.shape)) - - x0 = op.create_image_data() - - s = numpy.zeros(numiters) - # Loop - for it in numpy.arange(numiters): - x1 = op.adjoint(op.direct(x0)) - x1norm = numpy.sqrt((x1**2).sum()) - #print ("x0 **********" ,x0) - #print ("x1 **********" ,x1) - s[it] = (x1*x0).sum() / (x0*x0).sum() - x0 = (1.0/x1norm)*x1 - return numpy.sqrt(s[-1]), numpy.sqrt(s), x0 - -#def PowerMethod(op,numiters): -# # Initialise random -# x0 = np.random.randn(400) -# s = np.zeros(numiters) -# # Loop -# for it in np.arange(numiters): -# x1 = np.dot(op.transpose(),np.dot(op,x0)) -# x1norm = np.sqrt(np.sum(np.dot(x1,x1))) -# s[it] = np.dot(x1,x0) / np.dot(x1,x0) -# x0 = (1.0/x1norm)*x1 -# return s, x0 - - -def PowerMethodNonsquare(op,numiters , x0=None): - # Initialise random - # Jakob's - # inputsize , outputsize = op.size() - #x0 = ImageContainer(numpy.random.randn(*inputsize) - # Edo's - #vg = ImageGeometry(voxel_num_x=inputsize[0], - # voxel_num_y=inputsize[1], - # voxel_num_z=inputsize[2]) - # - #x0 = ImageData(geometry = vg, dimension_labels=['vertical','horizontal_y','horizontal_x']) - #print (x0) - #x0.fill(numpy.random.randn(*x0.shape)) - - if x0 is None: - #x0 = op.create_image_data() - x0 = op.allocate_direct() - x0.fill(numpy.random.randn(*x0.shape)) - - s = numpy.zeros(numiters) - # Loop - for it in numpy.arange(numiters): - x1 = op.adjoint(op.direct(x0)) - #x1norm = numpy.sqrt((x1*x1).sum()) - x1norm = x1.norm() - #print ("x0 **********" ,x0) - #print ("x1 **********" ,x1) - s[it] = (x1*x0).sum() / (x0.squared_norm()) - x0 = (1.0/x1norm)*x1 - return numpy.sqrt(s[-1]), numpy.sqrt(s), x0 - -class LinearOperatorMatrix(Operator): - def __init__(self,A): - self.A = A - self.s1 = None # Largest singular value, initially unknown - super(LinearOperatorMatrix, self).__init__() - - def direct(self,x, out=None): - if out is None: - return type(x)(numpy.dot(self.A,x.as_array())) - else: - numpy.dot(self.A, x.as_array(), out=out.as_array()) - - - def adjoint(self,x, out=None): - if out is None: - return type(x)(numpy.dot(self.A.transpose(),x.as_array())) - else: - numpy.dot(self.A.transpose(),x.as_array(), out=out.as_array()) - - - def size(self): - return self.A.shape - - def get_max_sing_val(self): - # If unknown, compute and store. If known, simply return it. - if self.s1 is None: - self.s1 = svds(self.A,1,return_singular_vectors=False)[0] - return self.s1 - else: - return self.s1 - def allocate_direct(self): - '''allocates the memory to hold the result of adjoint''' - #numpy.dot(self.A.transpose(),x.as_array()) - M_A, N_A = self.A.shape - out = numpy.zeros((N_A,1)) - return DataContainer(out) - def allocate_adjoint(self): - '''allocate the memory to hold the result of direct''' - #numpy.dot(self.A.transpose(),x.as_array()) - M_A, N_A = self.A.shape - out = numpy.zeros((M_A,1)) - return DataContainer(out) diff --git a/Wrappers/Python/ccpi/processors.py b/Wrappers/Python/ccpi/processors/CenterOfRotationFinder.py index 3a3671a..936dc05 100755 --- a/Wrappers/Python/ccpi/processors.py +++ b/Wrappers/Python/ccpi/processors/CenterOfRotationFinder.py @@ -1,514 +1,408 @@ -# -*- coding: utf-8 -*-
-# This work is part of the Core Imaging Library developed by
-# Visual Analytics and Imaging System Group of the Science Technology
-# Facilities Council, STFC
-
-# Copyright 2018 Edoardo Pasca
-
-# 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
-
-from ccpi.framework import DataProcessor, DataContainer, AcquisitionData,\
- AcquisitionGeometry, ImageGeometry, ImageData
-from ccpi.reconstruction.parallelbeam import alg as pbalg
-import numpy
-from scipy import ndimage
-
-import matplotlib.pyplot as plt
-
-
-class Normalizer(DataProcessor):
- '''Normalization based on flat and dark
-
- This processor read in a AcquisitionData and normalises it based on
- the instrument reading with and without incident photons or neutrons.
-
- Input: AcquisitionData
- Parameter: 2D projection with flat field (or stack)
- 2D projection with dark field (or stack)
- Output: AcquisitionDataSetn
- '''
-
- 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(Normalizer, 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 = self.set_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 = self.set_flat_field(df.as_array())
-
- @staticmethod
- def normalize_projection(projection, flat, dark, tolerance):
- a = (projection - dark)
- b = (flat-dark)
- with numpy.errstate(divide='ignore', invalid='ignore'):
- c = numpy.true_divide( a, b )
- c[ ~ numpy.isfinite( c )] = tolerance # set to not zero if 0/0
- return c
-
- @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):
-
- projections = self.get_input()
- dark = self.dark_field
- flat = self.flat_field
-
- if projections.number_of_dimensions == 3:
- if not (projections.shape[1:] == dark.shape and \
- projections.shape[1:] == flat.shape):
- raise ValueError('Flats/Dark and projections size do not match.')
-
-
- a = numpy.asarray(
- [ Normalizer.normalize_projection(
- projection, flat, dark, self.tolerance) \
- for projection in projections.as_array() ]
- )
- elif projections.number_of_dimensions == 2:
- a = Normalizer.normalize_projection(projections.as_array(),
- flat, dark, self.tolerance)
- y = type(projections)( a , True,
- dimension_labels=projections.dimension_labels,
- geometry=projections.geometry)
- return y
-
-
-class CenterOfRotationFinder(DataProcessor):
- '''Processor to find the center of rotation in a parallel beam experiment
-
- This processor read in a AcquisitionDataSet and finds the center of rotation
- based on Nghia Vo's method. https://doi.org/10.1364/OE.22.019078
-
- Input: AcquisitionDataSet
-
- Output: float. center of rotation in pixel coordinate
- '''
-
- def __init__(self):
- kwargs = {
-
- }
-
- #DataProcessor.__init__(self, **kwargs)
- super(CenterOfRotationFinder, self).__init__(**kwargs)
-
- def check_input(self, dataset):
- if dataset.number_of_dimensions == 3:
- if dataset.geometry.geom_type == 'parallel':
- return True
- else:
- raise ValueError('{0} is suitable only for parallel beam geometry'\
- .format(self.__class__.__name__))
- else:
- raise ValueError("Expected input dimensions is 3, got {0}"\
- .format(dataset.number_of_dimensions))
-
-
- # #########################################################################
- # Copyright (c) 2015, UChicago Argonne, LLC. All rights reserved. #
- # #
- # Copyright 2015. UChicago Argonne, LLC. This software was produced #
- # under U.S. Government contract DE-AC02-06CH11357 for Argonne National #
- # Laboratory (ANL), which is operated by UChicago Argonne, LLC for the #
- # U.S. Department of Energy. The U.S. Government has rights to use, #
- # reproduce, and distribute this software. NEITHER THE GOVERNMENT NOR #
- # UChicago Argonne, LLC MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR #
- # ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE. If software is #
- # modified to produce derivative works, such modified software should #
- # be clearly marked, so as not to confuse it with the version available #
- # from ANL. #
- # #
- # Additionally, redistribution and use in source and binary forms, with #
- # or without modification, are permitted provided that the following #
- # conditions are met: #
- # #
- # * Redistributions of source code must retain the above copyright #
- # notice, this list of conditions and the following disclaimer. #
- # #
- # * Redistributions in binary form must reproduce the above copyright #
- # notice, this list of conditions and the following disclaimer in #
- # the documentation and/or other materials provided with the #
- # distribution. #
- # #
- # * Neither the name of UChicago Argonne, LLC, Argonne National #
- # Laboratory, ANL, the U.S. Government, nor the names of its #
- # contributors may be used to endorse or promote products derived #
- # from this software without specific prior written permission. #
- # #
- # THIS SOFTWARE IS PROVIDED BY UChicago Argonne, LLC AND CONTRIBUTORS #
- # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT #
- # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS #
- # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL UChicago #
- # Argonne, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, #
- # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, #
- # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; #
- # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER #
- # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT #
- # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN #
- # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE #
- # POSSIBILITY OF SUCH DAMAGE. #
- # #########################################################################
-
- @staticmethod
- def as_ndarray(arr, dtype=None, copy=False):
- if not isinstance(arr, numpy.ndarray):
- arr = numpy.array(arr, dtype=dtype, copy=copy)
- return arr
-
- @staticmethod
- def as_dtype(arr, dtype, copy=False):
- if not arr.dtype == dtype:
- arr = numpy.array(arr, dtype=dtype, copy=copy)
- return arr
-
- @staticmethod
- def as_float32(arr):
- arr = CenterOfRotationFinder.as_ndarray(arr, numpy.float32)
- return CenterOfRotationFinder.as_dtype(arr, numpy.float32)
-
-
-
-
- @staticmethod
- def find_center_vo(tomo, ind=None, smin=-40, smax=40, srad=10, step=0.5,
- ratio=2., drop=20):
- """
- Find rotation axis location using Nghia Vo's method. :cite:`Vo:14`.
-
- Parameters
- ----------
- tomo : ndarray
- 3D tomographic data.
- ind : int, optional
- Index of the slice to be used for reconstruction.
- smin, smax : int, optional
- Reference to the horizontal center of the sinogram.
- srad : float, optional
- Fine search radius.
- step : float, optional
- Step of fine searching.
- ratio : float, optional
- The ratio between the FOV of the camera and the size of object.
- It's used to generate the mask.
- drop : int, optional
- Drop lines around vertical center of the mask.
-
- Returns
- -------
- float
- Rotation axis location.
-
- Notes
- -----
- The function may not yield a correct estimate, if:
-
- - the sample size is bigger than the field of view of the camera.
- In this case the ``ratio`` argument need to be set larger
- than the default of 2.0.
-
- - there is distortion in the imaging hardware. If there's
- no correction applied, the center of the projection image may
- yield a better estimate.
-
- - the sample contrast is weak. Paganin's filter need to be applied
- to overcome this.
-
- - the sample was changed during the scan.
- """
- tomo = CenterOfRotationFinder.as_float32(tomo)
-
- if ind is None:
- ind = tomo.shape[1] // 2
- _tomo = tomo[:, ind, :]
-
-
-
- # Reduce noise by smooth filters. Use different filters for coarse and fine search
- _tomo_cs = ndimage.filters.gaussian_filter(_tomo, (3, 1))
- _tomo_fs = ndimage.filters.median_filter(_tomo, (2, 2))
-
- # Coarse and fine searches for finding the rotation center.
- if _tomo.shape[0] * _tomo.shape[1] > 4e6: # If data is large (>2kx2k)
- #_tomo_coarse = downsample(numpy.expand_dims(_tomo_cs,1), level=2)[:, 0, :]
- #init_cen = _search_coarse(_tomo_coarse, smin, smax, ratio, drop)
- #fine_cen = _search_fine(_tomo_fs, srad, step, init_cen*4, ratio, drop)
- init_cen = CenterOfRotationFinder._search_coarse(_tomo_cs, smin,
- smax, ratio, drop)
- fine_cen = CenterOfRotationFinder._search_fine(_tomo_fs, srad,
- step, init_cen,
- ratio, drop)
- else:
- init_cen = CenterOfRotationFinder._search_coarse(_tomo_cs,
- smin, smax,
- ratio, drop)
- fine_cen = CenterOfRotationFinder._search_fine(_tomo_fs, srad,
- step, init_cen,
- ratio, drop)
-
- #logger.debug('Rotation center search finished: %i', fine_cen)
- return fine_cen
-
-
- @staticmethod
- def _search_coarse(sino, smin, smax, ratio, drop):
- """
- Coarse search for finding the rotation center.
- """
- (Nrow, Ncol) = sino.shape
- centerfliplr = (Ncol - 1.0) / 2.0
-
- # Copy the sinogram and flip left right, the purpose is to
- # make a full [0;2Pi] sinogram
- _copy_sino = numpy.fliplr(sino[1:])
-
- # This image is used for compensating the shift of sinogram 2
- temp_img = numpy.zeros((Nrow - 1, Ncol), dtype='float32')
- temp_img[:] = sino[-1]
-
- # Start coarse search in which the shift step is 1
- listshift = numpy.arange(smin, smax + 1)
- listmetric = numpy.zeros(len(listshift), dtype='float32')
- mask = CenterOfRotationFinder._create_mask(2 * Nrow - 1, Ncol,
- 0.5 * ratio * Ncol, drop)
- for i in listshift:
- _sino = numpy.roll(_copy_sino, i, axis=1)
- if i >= 0:
- _sino[:, 0:i] = temp_img[:, 0:i]
- else:
- _sino[:, i:] = temp_img[:, i:]
- listmetric[i - smin] = numpy.sum(numpy.abs(numpy.fft.fftshift(
- #pyfftw.interfaces.numpy_fft.fft2(
- # numpy.vstack((sino, _sino)))
- numpy.fft.fft2(numpy.vstack((sino, _sino)))
- )) * mask)
- minpos = numpy.argmin(listmetric)
- return centerfliplr + listshift[minpos] / 2.0
-
- @staticmethod
- def _search_fine(sino, srad, step, init_cen, ratio, drop):
- """
- Fine search for finding the rotation center.
- """
- Nrow, Ncol = sino.shape
- centerfliplr = (Ncol + 1.0) / 2.0 - 1.0
- # Use to shift the sinogram 2 to the raw CoR.
- shiftsino = numpy.int16(2 * (init_cen - centerfliplr))
- _copy_sino = numpy.roll(numpy.fliplr(sino[1:]), shiftsino, axis=1)
- if init_cen <= centerfliplr:
- lefttake = numpy.int16(numpy.ceil(srad + 1))
- righttake = numpy.int16(numpy.floor(2 * init_cen - srad - 1))
- else:
- lefttake = numpy.int16(numpy.ceil(
- init_cen - (Ncol - 1 - init_cen) + srad + 1))
- righttake = numpy.int16(numpy.floor(Ncol - 1 - srad - 1))
- Ncol1 = righttake - lefttake + 1
- mask = CenterOfRotationFinder._create_mask(2 * Nrow - 1, Ncol1,
- 0.5 * ratio * Ncol, drop)
- numshift = numpy.int16((2 * srad) / step) + 1
- listshift = numpy.linspace(-srad, srad, num=numshift)
- listmetric = numpy.zeros(len(listshift), dtype='float32')
- factor1 = numpy.mean(sino[-1, lefttake:righttake])
- num1 = 0
- for i in listshift:
- _sino = ndimage.interpolation.shift(
- _copy_sino, (0, i), prefilter=False)
- factor2 = numpy.mean(_sino[0,lefttake:righttake])
- _sino = _sino * factor1 / factor2
- sinojoin = numpy.vstack((sino, _sino))
- listmetric[num1] = numpy.sum(numpy.abs(numpy.fft.fftshift(
- #pyfftw.interfaces.numpy_fft.fft2(
- # sinojoin[:, lefttake:righttake + 1])
- numpy.fft.fft2(sinojoin[:, lefttake:righttake + 1])
- )) * mask)
- num1 = num1 + 1
- minpos = numpy.argmin(listmetric)
- return init_cen + listshift[minpos] / 2.0
-
- @staticmethod
- def _create_mask(nrow, ncol, radius, drop):
- du = 1.0 / ncol
- dv = (nrow - 1.0) / (nrow * 2.0 * numpy.pi)
- centerrow = numpy.ceil(nrow / 2) - 1
- centercol = numpy.ceil(ncol / 2) - 1
- # added by Edoardo Pasca
- centerrow = int(centerrow)
- centercol = int(centercol)
- mask = numpy.zeros((nrow, ncol), dtype='float32')
- for i in range(nrow):
- num1 = numpy.round(((i - centerrow) * dv / radius) / du)
- (p1, p2) = numpy.int16(numpy.clip(numpy.sort(
- (-num1 + centercol, num1 + centercol)), 0, ncol - 1))
- mask[i, p1:p2 + 1] = numpy.ones(p2 - p1 + 1, dtype='float32')
- if drop < centerrow:
- mask[centerrow - drop:centerrow + drop + 1,
- :] = numpy.zeros((2 * drop + 1, ncol), dtype='float32')
- mask[:,centercol-1:centercol+2] = numpy.zeros((nrow, 3), dtype='float32')
- return mask
-
- def process(self, out=None):
-
- projections = self.get_input()
-
- cor = CenterOfRotationFinder.find_center_vo(projections.as_array())
-
- return cor
-
-
-class AcquisitionDataPadder(DataProcessor):
- '''Normalization based on flat and dark
-
- This processor read in a AcquisitionData and normalises it based on
- the instrument reading with and without incident photons or neutrons.
-
- Input: AcquisitionData
- Parameter: 2D projection with flat field (or stack)
- 2D projection with dark field (or stack)
- Output: AcquisitionDataSetn
- '''
-
- def __init__(self,
- center_of_rotation = None,
- acquisition_geometry = None,
- pad_value = 1e-5):
- kwargs = {
- 'acquisition_geometry' : acquisition_geometry,
- 'center_of_rotation' : center_of_rotation,
- 'pad_value' : pad_value
- }
-
- super(AcquisitionDataPadder, self).__init__(**kwargs)
-
- def check_input(self, dataset):
- if self.acquisition_geometry is None:
- self.acquisition_geometry = dataset.geometry
- if dataset.number_of_dimensions == 3:
- return True
- else:
- raise ValueError("Expected input dimensions is 2 or 3, got {0}"\
- .format(dataset.number_of_dimensions))
-
- def process(self, out=None):
- projections = self.get_input()
- w = projections.get_dimension_size('horizontal')
- delta = w - 2 * self.center_of_rotation
-
- padded_width = int (
- numpy.ceil(abs(delta)) + w
- )
- delta_pix = padded_width - w
-
- voxel_per_pixel = 1
- geom = pbalg.pb_setup_geometry_from_acquisition(projections.as_array(),
- self.acquisition_geometry.angles,
- self.center_of_rotation,
- voxel_per_pixel )
-
- padded_geometry = self.acquisition_geometry.clone()
-
- padded_geometry.pixel_num_h = geom['n_h']
- padded_geometry.pixel_num_v = geom['n_v']
-
- delta_pix_h = padded_geometry.pixel_num_h - self.acquisition_geometry.pixel_num_h
- delta_pix_v = padded_geometry.pixel_num_v - self.acquisition_geometry.pixel_num_v
-
- if delta_pix_h == 0:
- delta_pix_h = delta_pix
- padded_geometry.pixel_num_h = padded_width
- #initialize a new AcquisitionData with values close to 0
- out = AcquisitionData(geometry=padded_geometry)
- out = out + self.pad_value
-
-
- #pad in the horizontal-vertical plane -> slice on angles
- if delta > 0:
- #pad left of middle
- command = "out.array["
- for i in range(out.number_of_dimensions):
- if out.dimension_labels[i] == 'horizontal':
- value = '{0}:{1}'.format(delta_pix_h, delta_pix_h+w)
- command = command + str(value)
- else:
- if out.dimension_labels[i] == 'vertical' :
- value = '{0}:'.format(delta_pix_v)
- command = command + str(value)
- else:
- command = command + ":"
- if i < out.number_of_dimensions -1:
- command = command + ','
- command = command + '] = projections.array'
- #print (command)
- else:
- #pad right of middle
- command = "out.array["
- for i in range(out.number_of_dimensions):
- if out.dimension_labels[i] == 'horizontal':
- value = '{0}:{1}'.format(0, w)
- command = command + str(value)
- else:
- if out.dimension_labels[i] == 'vertical' :
- value = '{0}:'.format(delta_pix_v)
- command = command + str(value)
- else:
- command = command + ":"
- if i < out.number_of_dimensions -1:
- command = command + ','
- command = command + '] = projections.array'
- #print (command)
- #cleaned = eval(command)
- exec(command)
+# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018 Edoardo Pasca + +# 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 + +from ccpi.framework import DataProcessor, DataContainer, AcquisitionData,\ + AcquisitionGeometry, ImageGeometry, ImageData +import numpy +from scipy import ndimage + +class CenterOfRotationFinder(DataProcessor): + '''Processor to find the center of rotation in a parallel beam experiment + + This processor read in a AcquisitionDataSet and finds the center of rotation + based on Nghia Vo's method. https://doi.org/10.1364/OE.22.019078 + + Input: AcquisitionDataSet + + Output: float. center of rotation in pixel coordinate + ''' + + def __init__(self): + kwargs = { + + } + + #DataProcessor.__init__(self, **kwargs) + super(CenterOfRotationFinder, self).__init__(**kwargs) + + def check_input(self, dataset): + if dataset.number_of_dimensions == 3: + if dataset.geometry.geom_type == 'parallel': + return True + else: + raise ValueError('{0} is suitable only for parallel beam geometry'\ + .format(self.__class__.__name__)) + else: + raise ValueError("Expected input dimensions is 3, got {0}"\ + .format(dataset.number_of_dimensions)) + + + # ######################################################################### + # Copyright (c) 2015, UChicago Argonne, LLC. All rights reserved. # + # # + # Copyright 2015. UChicago Argonne, LLC. This software was produced # + # under U.S. Government contract DE-AC02-06CH11357 for Argonne National # + # Laboratory (ANL), which is operated by UChicago Argonne, LLC for the # + # U.S. Department of Energy. The U.S. Government has rights to use, # + # reproduce, and distribute this software. NEITHER THE GOVERNMENT NOR # + # UChicago Argonne, LLC MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR # + # ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE. If software is # + # modified to produce derivative works, such modified software should # + # be clearly marked, so as not to confuse it with the version available # + # from ANL. # + # # + # Additionally, redistribution and use in source and binary forms, with # + # or without modification, are permitted provided that the following # + # conditions are met: # + # # + # * Redistributions of source code must retain the above copyright # + # notice, this list of conditions and the following disclaimer. # + # # + # * Redistributions in binary form must reproduce the above copyright # + # notice, this list of conditions and the following disclaimer in # + # the documentation and/or other materials provided with the # + # distribution. # + # # + # * Neither the name of UChicago Argonne, LLC, Argonne National # + # Laboratory, ANL, the U.S. Government, nor the names of its # + # contributors may be used to endorse or promote products derived # + # from this software without specific prior written permission. # + # # + # THIS SOFTWARE IS PROVIDED BY UChicago Argonne, LLC AND CONTRIBUTORS # + # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # + # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # + # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL UChicago # + # Argonne, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # + # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # + # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # + # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # + # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # + # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # + # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # + # POSSIBILITY OF SUCH DAMAGE. # + # ######################################################################### + + @staticmethod + def as_ndarray(arr, dtype=None, copy=False): + if not isinstance(arr, numpy.ndarray): + arr = numpy.array(arr, dtype=dtype, copy=copy) + return arr + + @staticmethod + def as_dtype(arr, dtype, copy=False): + if not arr.dtype == dtype: + arr = numpy.array(arr, dtype=dtype, copy=copy) + return arr + + @staticmethod + def as_float32(arr): + arr = CenterOfRotationFinder.as_ndarray(arr, numpy.float32) + return CenterOfRotationFinder.as_dtype(arr, numpy.float32) + + + + + @staticmethod + def find_center_vo(tomo, ind=None, smin=-40, smax=40, srad=10, step=0.5, + ratio=2., drop=20): + """ + Find rotation axis location using Nghia Vo's method. :cite:`Vo:14`. + + Parameters + ---------- + tomo : ndarray + 3D tomographic data. + ind : int, optional + Index of the slice to be used for reconstruction. + smin, smax : int, optional + Reference to the horizontal center of the sinogram. + srad : float, optional + Fine search radius. + step : float, optional + Step of fine searching. + ratio : float, optional + The ratio between the FOV of the camera and the size of object. + It's used to generate the mask. + drop : int, optional + Drop lines around vertical center of the mask. + + Returns + ------- + float + Rotation axis location. + + Notes + ----- + The function may not yield a correct estimate, if: + + - the sample size is bigger than the field of view of the camera. + In this case the ``ratio`` argument need to be set larger + than the default of 2.0. + + - there is distortion in the imaging hardware. If there's + no correction applied, the center of the projection image may + yield a better estimate. + + - the sample contrast is weak. Paganin's filter need to be applied + to overcome this. + + - the sample was changed during the scan. + """ + tomo = CenterOfRotationFinder.as_float32(tomo) + + if ind is None: + ind = tomo.shape[1] // 2 + _tomo = tomo[:, ind, :] + + + + # Reduce noise by smooth filters. Use different filters for coarse and fine search + _tomo_cs = ndimage.filters.gaussian_filter(_tomo, (3, 1)) + _tomo_fs = ndimage.filters.median_filter(_tomo, (2, 2)) + + # Coarse and fine searches for finding the rotation center. + if _tomo.shape[0] * _tomo.shape[1] > 4e6: # If data is large (>2kx2k) + #_tomo_coarse = downsample(numpy.expand_dims(_tomo_cs,1), level=2)[:, 0, :] + #init_cen = _search_coarse(_tomo_coarse, smin, smax, ratio, drop) + #fine_cen = _search_fine(_tomo_fs, srad, step, init_cen*4, ratio, drop) + init_cen = CenterOfRotationFinder._search_coarse(_tomo_cs, smin, + smax, ratio, drop) + fine_cen = CenterOfRotationFinder._search_fine(_tomo_fs, srad, + step, init_cen, + ratio, drop) + else: + init_cen = CenterOfRotationFinder._search_coarse(_tomo_cs, + smin, smax, + ratio, drop) + fine_cen = CenterOfRotationFinder._search_fine(_tomo_fs, srad, + step, init_cen, + ratio, drop) + + #logger.debug('Rotation center search finished: %i', fine_cen) + return fine_cen + + + @staticmethod + def _search_coarse(sino, smin, smax, ratio, drop): + """ + Coarse search for finding the rotation center. + """ + (Nrow, Ncol) = sino.shape + centerfliplr = (Ncol - 1.0) / 2.0 + + # Copy the sinogram and flip left right, the purpose is to + # make a full [0;2Pi] sinogram + _copy_sino = numpy.fliplr(sino[1:]) + + # This image is used for compensating the shift of sinogram 2 + temp_img = numpy.zeros((Nrow - 1, Ncol), dtype='float32') + temp_img[:] = sino[-1] + + # Start coarse search in which the shift step is 1 + listshift = numpy.arange(smin, smax + 1) + listmetric = numpy.zeros(len(listshift), dtype='float32') + mask = CenterOfRotationFinder._create_mask(2 * Nrow - 1, Ncol, + 0.5 * ratio * Ncol, drop) + for i in listshift: + _sino = numpy.roll(_copy_sino, i, axis=1) + if i >= 0: + _sino[:, 0:i] = temp_img[:, 0:i] + else: + _sino[:, i:] = temp_img[:, i:] + listmetric[i - smin] = numpy.sum(numpy.abs(numpy.fft.fftshift( + #pyfftw.interfaces.numpy_fft.fft2( + # numpy.vstack((sino, _sino))) + numpy.fft.fft2(numpy.vstack((sino, _sino))) + )) * mask) + minpos = numpy.argmin(listmetric) + return centerfliplr + listshift[minpos] / 2.0 + + @staticmethod + def _search_fine(sino, srad, step, init_cen, ratio, drop): + """ + Fine search for finding the rotation center. + """ + Nrow, Ncol = sino.shape + centerfliplr = (Ncol + 1.0) / 2.0 - 1.0 + # Use to shift the sinogram 2 to the raw CoR. + shiftsino = numpy.int16(2 * (init_cen - centerfliplr)) + _copy_sino = numpy.roll(numpy.fliplr(sino[1:]), shiftsino, axis=1) + if init_cen <= centerfliplr: + lefttake = numpy.int16(numpy.ceil(srad + 1)) + righttake = numpy.int16(numpy.floor(2 * init_cen - srad - 1)) + else: + lefttake = numpy.int16(numpy.ceil( + init_cen - (Ncol - 1 - init_cen) + srad + 1)) + righttake = numpy.int16(numpy.floor(Ncol - 1 - srad - 1)) + Ncol1 = righttake - lefttake + 1 + mask = CenterOfRotationFinder._create_mask(2 * Nrow - 1, Ncol1, + 0.5 * ratio * Ncol, drop) + numshift = numpy.int16((2 * srad) / step) + 1 + listshift = numpy.linspace(-srad, srad, num=numshift) + listmetric = numpy.zeros(len(listshift), dtype='float32') + factor1 = numpy.mean(sino[-1, lefttake:righttake]) + num1 = 0 + for i in listshift: + _sino = ndimage.interpolation.shift( + _copy_sino, (0, i), prefilter=False) + factor2 = numpy.mean(_sino[0,lefttake:righttake]) + _sino = _sino * factor1 / factor2 + sinojoin = numpy.vstack((sino, _sino)) + listmetric[num1] = numpy.sum(numpy.abs(numpy.fft.fftshift( + #pyfftw.interfaces.numpy_fft.fft2( + # sinojoin[:, lefttake:righttake + 1]) + numpy.fft.fft2(sinojoin[:, lefttake:righttake + 1]) + )) * mask) + num1 = num1 + 1 + minpos = numpy.argmin(listmetric) + return init_cen + listshift[minpos] / 2.0 + + @staticmethod + def _create_mask(nrow, ncol, radius, drop): + du = 1.0 / ncol + dv = (nrow - 1.0) / (nrow * 2.0 * numpy.pi) + centerrow = numpy.ceil(nrow / 2) - 1 + centercol = numpy.ceil(ncol / 2) - 1 + # added by Edoardo Pasca + centerrow = int(centerrow) + centercol = int(centercol) + mask = numpy.zeros((nrow, ncol), dtype='float32') + for i in range(nrow): + num1 = numpy.round(((i - centerrow) * dv / radius) / du) + (p1, p2) = numpy.int16(numpy.clip(numpy.sort( + (-num1 + centercol, num1 + centercol)), 0, ncol - 1)) + mask[i, p1:p2 + 1] = numpy.ones(p2 - p1 + 1, dtype='float32') + if drop < centerrow: + mask[centerrow - drop:centerrow + drop + 1, + :] = numpy.zeros((2 * drop + 1, ncol), dtype='float32') + mask[:,centercol-1:centercol+2] = numpy.zeros((nrow, 3), dtype='float32') + return mask + + def process(self, out=None): + + projections = self.get_input() + + cor = CenterOfRotationFinder.find_center_vo(projections.as_array()) + + return cor + + +class AcquisitionDataPadder(DataProcessor): + '''Normalization based on flat and dark + + This processor read in a AcquisitionData and normalises it based on + the instrument reading with and without incident photons or neutrons. + + Input: AcquisitionData + Parameter: 2D projection with flat field (or stack) + 2D projection with dark field (or stack) + Output: AcquisitionDataSetn + ''' + + def __init__(self, + center_of_rotation = None, + acquisition_geometry = None, + pad_value = 1e-5): + kwargs = { + 'acquisition_geometry' : acquisition_geometry, + 'center_of_rotation' : center_of_rotation, + 'pad_value' : pad_value + } + + super(AcquisitionDataPadder, self).__init__(**kwargs) + + def check_input(self, dataset): + if self.acquisition_geometry is None: + self.acquisition_geometry = dataset.geometry + if dataset.number_of_dimensions == 3: + return True + else: + raise ValueError("Expected input dimensions is 2 or 3, got {0}"\ + .format(dataset.number_of_dimensions)) + + def process(self, out=None): + projections = self.get_input() + w = projections.get_dimension_size('horizontal') + delta = w - 2 * self.center_of_rotation + + padded_width = int ( + numpy.ceil(abs(delta)) + w + ) + delta_pix = padded_width - w + + voxel_per_pixel = 1 + geom = pbalg.pb_setup_geometry_from_acquisition(projections.as_array(), + self.acquisition_geometry.angles, + self.center_of_rotation, + voxel_per_pixel ) + + padded_geometry = self.acquisition_geometry.clone() + + padded_geometry.pixel_num_h = geom['n_h'] + padded_geometry.pixel_num_v = geom['n_v'] + + delta_pix_h = padded_geometry.pixel_num_h - self.acquisition_geometry.pixel_num_h + delta_pix_v = padded_geometry.pixel_num_v - self.acquisition_geometry.pixel_num_v + + if delta_pix_h == 0: + delta_pix_h = delta_pix + padded_geometry.pixel_num_h = padded_width + #initialize a new AcquisitionData with values close to 0 + out = AcquisitionData(geometry=padded_geometry) + out = out + self.pad_value + + + #pad in the horizontal-vertical plane -> slice on angles + if delta > 0: + #pad left of middle + command = "out.array[" + for i in range(out.number_of_dimensions): + if out.dimension_labels[i] == 'horizontal': + value = '{0}:{1}'.format(delta_pix_h, delta_pix_h+w) + command = command + str(value) + else: + if out.dimension_labels[i] == 'vertical' : + value = '{0}:'.format(delta_pix_v) + command = command + str(value) + else: + command = command + ":" + if i < out.number_of_dimensions -1: + command = command + ',' + command = command + '] = projections.array' + #print (command) + else: + #pad right of middle + command = "out.array[" + for i in range(out.number_of_dimensions): + if out.dimension_labels[i] == 'horizontal': + value = '{0}:{1}'.format(0, w) + command = command + str(value) + else: + if out.dimension_labels[i] == 'vertical' : + value = '{0}:'.format(delta_pix_v) + command = command + str(value) + else: + command = command + ":" + if i < out.number_of_dimensions -1: + command = command + ',' + command = command + '] = projections.array' + #print (command) + #cleaned = eval(command) + exec(command) return out
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/processors/Normalizer.py b/Wrappers/Python/ccpi/processors/Normalizer.py new file mode 100755 index 0000000..da65e5c --- /dev/null +++ b/Wrappers/Python/ccpi/processors/Normalizer.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018 Edoardo Pasca + +# 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 + +from ccpi.framework import DataProcessor, DataContainer, AcquisitionData,\ + AcquisitionGeometry, ImageGeometry, ImageData +import numpy + +class Normalizer(DataProcessor): + '''Normalization based on flat and dark + + This processor read in a AcquisitionData and normalises it based on + the instrument reading with and without incident photons or neutrons. + + Input: AcquisitionData + Parameter: 2D projection with flat field (or stack) + 2D projection with dark field (or stack) + Output: AcquisitionDataSetn + ''' + + 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(Normalizer, 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 = self.set_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 = self.set_flat_field(df.as_array()) + + @staticmethod + def normalize_projection(projection, flat, dark, tolerance): + a = (projection - dark) + b = (flat-dark) + with numpy.errstate(divide='ignore', invalid='ignore'): + c = numpy.true_divide( a, b ) + c[ ~ numpy.isfinite( c )] = tolerance # set to not zero if 0/0 + return c + + @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): + + projections = self.get_input() + dark = self.dark_field + flat = self.flat_field + + if projections.number_of_dimensions == 3: + if not (projections.shape[1:] == dark.shape and \ + projections.shape[1:] == flat.shape): + raise ValueError('Flats/Dark and projections size do not match.') + + + a = numpy.asarray( + [ Normalizer.normalize_projection( + projection, flat, dark, self.tolerance) \ + for projection in projections.as_array() ] + ) + elif projections.number_of_dimensions == 2: + a = Normalizer.normalize_projection(projections.as_array(), + flat, dark, self.tolerance) + y = type(projections)( a , True, + dimension_labels=projections.dimension_labels, + geometry=projections.geometry) + return y +
\ No newline at end of file diff --git a/Wrappers/Python/ccpi/processors/Resizer.py b/Wrappers/Python/ccpi/processors/Resizer.py new file mode 100755 index 0000000..7509a90 --- /dev/null +++ b/Wrappers/Python/ccpi/processors/Resizer.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018 Edoardo Pasca
+
+# 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
+
+from ccpi.framework import DataProcessor, AcquisitionData, ImageData
+import warnings
+
+
+class Resizer(DataProcessor):
+
+ def __init__(self,
+ roi = -1,
+ binning = 1):
+
+ '''
+ Constructor
+
+ Input:
+
+ roi region-of-interest to crop. If roi = -1 (default), then no crop.
+ Otherwise roi is given by a list with ndim elements,
+ where each element is either -1 if no crop along this
+ dimension or a tuple with beginning and end coodinates to crop to.
+ Example:
+ to crop 4D array along 2nd dimension:
+ roi = [-1, -1, (100, 900), -1]
+
+ binning number of pixels to bin (combine) along each dimension.
+ If binning = 1, then projections in original resolution are loaded.
+ Otherwise, binning is given by a list with ndim integers.
+ Example:
+ to rebin 3D array along 1st direction:
+ binning = [1, 5, 1]
+ '''
+
+ kwargs = {'roi': roi,
+ 'binning': binning}
+
+ super(Resizer, self).__init__(**kwargs)
+
+ def check_input(self, data):
+ if not ((isinstance(data, ImageData)) or
+ (isinstance(data, AcquisitionData))):
+ raise Exception('Processor supports only following data types:\n' +
+ ' - ImageData\n - AcquisitionData')
+ elif (data.geometry == None):
+ raise Exception('Geometry is not defined.')
+ else:
+ return True
+
+ def process(self):
+
+ data = self.get_input()
+ ndim = len(data.dimension_labels)
+
+ geometry_0 = data.geometry
+ geometry = geometry_0.clone()
+
+ if (self.roi == -1):
+ roi_par = [-1] * ndim
+ else:
+ roi_par = self.roi.copy()
+ if (len(roi_par) != ndim):
+ raise Exception('Number of dimensions and number of elements in roi parameter do not match')
+
+ if (self.binning == 1):
+ binning = [1] * ndim
+ else:
+ binning = self.binning.copy()
+ if (len(binning) != ndim):
+ raise Exception('Number of dimensions and number of elements in binning parameter do not match')
+
+ if (isinstance(data, ImageData)):
+ if ((all(x == -1 for x in roi_par)) and (all(x == 1 for x in binning))):
+ for key in data.dimension_labels:
+ if data.dimension_labels[key] == 'channel':
+ geometry.channels = geometry_0.channels
+ roi_par[key] = (0, geometry.channels)
+ elif data.dimension_labels[key] == 'horizontal_y':
+ geometry.voxel_size_y = geometry_0.voxel_size_y
+ geometry.voxel_num_y = geometry_0.voxel_num_y
+ roi_par[key] = (0, geometry.voxel_num_y)
+ elif data.dimension_labels[key] == 'vertical':
+ geometry.voxel_size_z = geometry_0.voxel_size_z
+ geometry.voxel_num_z = geometry_0.voxel_num_z
+ roi_par[key] = (0, geometry.voxel_num_z)
+ elif data.dimension_labels[key] == 'horizontal_x':
+ geometry.voxel_size_x = geometry_0.voxel_size_x
+ geometry.voxel_num_x = geometry_0.voxel_num_x
+ roi_par[key] = (0, geometry.voxel_num_x)
+ else:
+ for key in data.dimension_labels:
+ if data.dimension_labels[key] == 'channel':
+ if (roi_par[key] != -1):
+ geometry.channels = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0] + ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.channels = geometry_0.channels // binning[key]
+ roi_par[key] = (0, geometry.channels * binning[key])
+ elif data.dimension_labels[key] == 'horizontal_y':
+ if (roi_par[key] != -1):
+ geometry.voxel_num_y = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ geometry.voxel_size_y = geometry_0.voxel_size_y * binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0] + ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.voxel_num_y = geometry_0.voxel_num_y // binning[key]
+ geometry.voxel_size_y = geometry_0.voxel_size_y * binning[key]
+ roi_par[key] = (0, geometry.voxel_num_y * binning[key])
+ elif data.dimension_labels[key] == 'vertical':
+ if (roi_par[key] != -1):
+ geometry.voxel_num_z = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ geometry.voxel_size_z = geometry_0.voxel_size_z * binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0] + ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.voxel_num_z = geometry_0.voxel_num_z // binning[key]
+ geometry.voxel_size_z = geometry_0.voxel_size_z * binning[key]
+ roi_par[key] = (0, geometry.voxel_num_z * binning[key])
+ elif data.dimension_labels[key] == 'horizontal_x':
+ if (roi_par[key] != -1):
+ geometry.voxel_num_x = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ geometry.voxel_size_x = geometry_0.voxel_size_x * binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0]+ ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.voxel_num_x = geometry_0.voxel_num_x // binning[key]
+ geometry.voxel_size_x = geometry_0.voxel_size_x * binning[key]
+ roi_par[key] = (0, geometry.voxel_num_x * binning[key])
+
+ else: # AcquisitionData
+ if ((all(x == -1 for x in roi_par)) and (all(x == 1 for x in binning))):
+ for key in data.dimension_labels:
+ if data.dimension_labels[key] == 'channel':
+ geometry.channels = geometry_0.channels
+ roi_par[key] = (0, geometry.channels)
+ elif data.dimension_labels[key] == 'angle':
+ geometry.angles = geometry_0.angles
+ roi_par[key] = (0, len(geometry.angles))
+ elif data.dimension_labels[key] == 'vertical':
+ geometry.pixel_size_v = geometry_0.pixel_size_v
+ geometry.pixel_num_v = geometry_0.pixel_num_v
+ roi_par[key] = (0, geometry.pixel_num_v)
+ elif data.dimension_labels[key] == 'horizontal':
+ geometry.pixel_size_h = geometry_0.pixel_size_h
+ geometry.pixel_num_h = geometry_0.pixel_num_h
+ roi_par[key] = (0, geometry.pixel_num_h)
+ else:
+ for key in data.dimension_labels:
+ if data.dimension_labels[key] == 'channel':
+ if (roi_par[key] != -1):
+ geometry.channels = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0] + ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.channels = geometry_0.channels // binning[key]
+ roi_par[key] = (0, geometry.channels * binning[key])
+ elif data.dimension_labels[key] == 'angle':
+ if (roi_par[key] != -1):
+ geometry.angles = geometry_0.angles[roi_par[key][0]:roi_par[key][1]]
+ else:
+ geometry.angles = geometry_0.angles
+ roi_par[key] = (0, len(geometry.angles))
+ if (binning[key] != 1):
+ binning[key] = 1
+ warnings.warn('Rebinning in angular dimensions is not supported: \n binning[{}] is set to 1.'.format(key))
+ elif data.dimension_labels[key] == 'vertical':
+ if (roi_par[key] != -1):
+ geometry.pixel_num_v = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ geometry.pixel_size_v = geometry_0.pixel_size_v * binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0] + ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.pixel_num_v = geometry_0.pixel_num_v // binning[key]
+ geometry.pixel_size_v = geometry_0.pixel_size_v * binning[key]
+ roi_par[key] = (0, geometry.pixel_num_v * binning[key])
+ elif data.dimension_labels[key] == 'horizontal':
+ if (roi_par[key] != -1):
+ geometry.pixel_num_h = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ geometry.pixel_size_h = geometry_0.pixel_size_h * binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0] + ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.pixel_num_h = geometry_0.pixel_num_h // binning[key]
+ geometry.pixel_size_h = geometry_0.pixel_size_h * binning[key]
+ roi_par[key] = (0, geometry.pixel_num_h * binning[key])
+
+ if ndim == 2:
+ n_pix_0 = (roi_par[0][1] - roi_par[0][0]) // binning[0]
+ n_pix_1 = (roi_par[1][1] - roi_par[1][0]) // binning[1]
+ shape = (n_pix_0, binning[0],
+ n_pix_1, binning[1])
+ data_resized = data.as_array()[roi_par[0][0]:(roi_par[0][0] + n_pix_0 * binning[0]),
+ roi_par[1][0]:(roi_par[1][0] + n_pix_1 * binning[1])].reshape(shape).mean(-1).mean(1)
+ if ndim == 3:
+ n_pix_0 = (roi_par[0][1] - roi_par[0][0]) // binning[0]
+ n_pix_1 = (roi_par[1][1] - roi_par[1][0]) // binning[1]
+ n_pix_2 = (roi_par[2][1] - roi_par[2][0]) // binning[2]
+ shape = (n_pix_0, binning[0],
+ n_pix_1, binning[1],
+ n_pix_2, binning[2])
+ data_resized = data.as_array()[roi_par[0][0]:(roi_par[0][0] + n_pix_0 * binning[0]),
+ roi_par[1][0]:(roi_par[1][0] + n_pix_1 * binning[1]),
+ roi_par[2][0]:(roi_par[2][0] + n_pix_2 * binning[2])].reshape(shape).mean(-1).mean(1).mean(2)
+ if ndim == 4:
+ n_pix_0 = (roi_par[0][1] - roi_par[0][0]) // binning[0]
+ n_pix_1 = (roi_par[1][1] - roi_par[1][0]) // binning[1]
+ n_pix_2 = (roi_par[2][1] - roi_par[2][0]) // binning[2]
+ n_pix_3 = (roi_par[3][1] - roi_par[3][0]) // binning[3]
+ shape = (n_pix_0, binning[0],
+ n_pix_1, binning[1],
+ n_pix_2, binning[2],
+ n_pix_3, binning[3])
+ data_resized = data.as_array()[roi_par[0][0]:(roi_par[0][0] + n_pix_0 * binning[0]),
+ roi_par[1][0]:(roi_par[1][0] + n_pix_1 * binning[1]),
+ roi_par[2][0]:(roi_par[2][0] + n_pix_2 * binning[2]),
+ roi_par[3][0]:(roi_par[3][0] + n_pix_3 * binning[3])].reshape(shape).mean(-1).mean(1).mean(2).mean(3)
+
+ out = type(data)(array = data_resized,
+ deep_copy = False,
+ dimension_labels = data.dimension_labels,
+ geometry = geometry)
+
+ return out
+
+
+'''
+#usage exaample
+ig = ImageGeometry(voxel_num_x = 200,
+ voxel_num_y = 200,
+ voxel_num_z = 200,
+ voxel_size_x = 1,
+ voxel_size_y = 1,
+ voxel_size_z = 1,
+ center_x = 0,
+ center_y = 0,
+ center_z = 0,
+ channels = 200)
+
+im = ImageData(array = numpy.zeros((200, 200, 200, 200)),
+ geometry = ig,
+ deep_copy = False,
+ dimension_labels = ['channel',\
+ 'vertical',\
+ 'horizontal_y',\
+ 'horizontal_x'])
+
+
+resizer = Resizer(binning = [1, 1, 7, 1], roi = -1)
+resizer.input = im
+data_resized = resizer.process()
+print(data_resized)
+'''
diff --git a/Wrappers/Python/ccpi/processors/__init__.py b/Wrappers/Python/ccpi/processors/__init__.py new file mode 100755 index 0000000..cba5897 --- /dev/null +++ b/Wrappers/Python/ccpi/processors/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*-
+"""
+Created on Tue Apr 30 13:51:09 2019
+
+@author: ofn77899
+"""
+
+from .CenterOfRotationFinder import CenterOfRotationFinder
+from .Normalizer import Normalizer
+from .Resizer import Resizer
diff --git a/Wrappers/Python/conda-recipe/conda_build_config.yaml b/Wrappers/Python/conda-recipe/conda_build_config.yaml index 96a211f..393ae18 100644 --- a/Wrappers/Python/conda-recipe/conda_build_config.yaml +++ b/Wrappers/Python/conda-recipe/conda_build_config.yaml @@ -4,5 +4,5 @@ python: - 3.6 numpy: # TODO investigage, as it doesn't currently build with cvxp, requires >1.14 - #- 1.12 - - 1.15 + - 1.11 + - 1.12 diff --git a/Wrappers/Python/conda-recipe/meta.yaml b/Wrappers/Python/conda-recipe/meta.yaml index 8ded429..9d03220 100644 --- a/Wrappers/Python/conda-recipe/meta.yaml +++ b/Wrappers/Python/conda-recipe/meta.yaml @@ -11,7 +11,7 @@ build: test: requires: - python-wget - - cvxpy # [not win] + - cvxpy # [ unix and py36 and np115 ] source_files: - ./test # [win] @@ -24,8 +24,8 @@ test: requirements: build: + - {{ pin_compatible('numpy', max_pin='x.x') }} - python - - numpy {{ numpy }} - setuptools run: @@ -35,6 +35,7 @@ requirements: - scipy - matplotlib - h5py + - pillow about: home: http://www.ccpi.ac.uk diff --git a/Wrappers/Python/data/24737_fd_normalised.nxs b/Wrappers/Python/data/24737_fd_normalised.nxs Binary files differnew file mode 100644 index 0000000..c1fe83d --- /dev/null +++ b/Wrappers/Python/data/24737_fd_normalised.nxs diff --git a/Wrappers/Python/data/boat.tiff b/Wrappers/Python/data/boat.tiff Binary files differnew file mode 100644 index 0000000..fc1205a --- /dev/null +++ b/Wrappers/Python/data/boat.tiff diff --git a/Wrappers/Python/data/camera.png b/Wrappers/Python/data/camera.png Binary files differnew file mode 100644 index 0000000..49be869 --- /dev/null +++ b/Wrappers/Python/data/camera.png diff --git a/Wrappers/Python/data/peppers.tiff b/Wrappers/Python/data/peppers.tiff Binary files differnew file mode 100644 index 0000000..8c956f8 --- /dev/null +++ b/Wrappers/Python/data/peppers.tiff diff --git a/Wrappers/Python/data/resolution_chart.tiff b/Wrappers/Python/data/resolution_chart.tiff Binary files differnew file mode 100755 index 0000000..d09cef3 --- /dev/null +++ b/Wrappers/Python/data/resolution_chart.tiff diff --git a/Wrappers/Python/data/shapes.png b/Wrappers/Python/data/shapes.png Binary files differnew file mode 100644 index 0000000..dd4f680 --- /dev/null +++ b/Wrappers/Python/data/shapes.png diff --git a/Wrappers/Python/data/test_show_data.py b/Wrappers/Python/data/test_show_data.py new file mode 100644 index 0000000..7325c27 --- /dev/null +++ b/Wrappers/Python/data/test_show_data.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Tue May 7 20:43:48 2019 + +@author: evangelos +""" + +from ccpi.data import camera, boat, peppers +import matplotlib.pyplot as plt + + +d = camera(size=(256,256)) + +plt.imshow(d.as_array()) +plt.colorbar() +plt.show() + +d1 = boat(size=(256,256)) + +plt.imshow(d1.as_array()) +plt.colorbar() +plt.show() + + +d2 = peppers(size=(256,256)) + +plt.imshow(d2.as_array()) +plt.colorbar() +plt.show()
\ No newline at end of file diff --git a/Wrappers/Python/demos/CGLS_examples/CGLS_Tikhonov.py b/Wrappers/Python/demos/CGLS_examples/CGLS_Tikhonov.py new file mode 100644 index 0000000..653e191 --- /dev/null +++ b/Wrappers/Python/demos/CGLS_examples/CGLS_Tikhonov.py @@ -0,0 +1,106 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + +""" +Compare solutions of PDHG & "Block CGLS" algorithms for + + +Problem: min_x alpha * ||\grad x ||^{2}_{2} + || A x - g ||_{2}^{2} + + + A: Projection operator + g: Sinogram + +""" + + +from ccpi.framework import AcquisitionGeometry, BlockDataContainer, AcquisitionData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import CGLS +from ccpi.optimisation.operators import BlockOperator, Gradient + +from ccpi.framework import TestData +import os, sys +from ccpi.astra.ops import AstraProjectorSimple + +# Load Data +loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi')) +N = 150 +M = 150 +data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(N,M), scale=(0,1)) + +ig = data.geometry + +detectors = N +angles = np.linspace(0, np.pi, N, dtype=np.float32) + +ag = AcquisitionGeometry('parallel','2D', angles, detectors) + +device = input('Available device: GPU==1 / CPU==0 ') +if device=='1': + dev = 'gpu' +else: + dev = 'cpu' + +Aop = AstraProjectorSimple(ig, ag, dev) +sin = Aop.direct(data) + +noisy_data = AcquisitionData( sin.as_array() + np.random.normal(0,3,ig.shape)) + +# Show Ground Truth and Noisy Data +plt.figure(figsize=(10,10)) +plt.subplot(2,1,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(2,1,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.show() + +# Setup and run the CGLS algorithm +#alpha = 50 +#Grad = Gradient(ig) +# +## Form Tikhonov as a Block CGLS structure +#op_CGLS = BlockOperator( Aop, alpha * Grad, shape=(2,1)) +#block_data = BlockDataContainer(noisy_data, Grad.range_geometry().allocate()) +# +#x_init = ig.allocate() +#cgls = CGLS(x_init=x_init, operator=op_CGLS, data=block_data) +#cgls.max_iteration = 1000 +#cgls.update_objective_interval = 200 +#cgls.run(1000,verbose=False) + +#%% +# Show results +plt.figure(figsize=(5,5)) +plt.imshow(cgls.get_output().as_array()) +plt.title('CGLS reconstruction') +plt.colorbar() +plt.show() + + diff --git a/Wrappers/Python/demos/CompareAlgorithms/CGLS_FISTA_PDHG_LeastSquares.py b/Wrappers/Python/demos/CompareAlgorithms/CGLS_FISTA_PDHG_LeastSquares.py new file mode 100644 index 0000000..672d4bc --- /dev/null +++ b/Wrappers/Python/demos/CompareAlgorithms/CGLS_FISTA_PDHG_LeastSquares.py @@ -0,0 +1,189 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + +""" +Compare solutions of FISTA & PDHG + & CGLS & Astra Built-in algorithms for Least Squares + + +Problem: min_x || A x - g ||_{2}^{2} + + A: Projection operator + g: Sinogram + +""" + + +from ccpi.framework import ImageData, TestData, AcquisitionGeometry + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG, CGLS, FISTA + +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, FunctionOperatorComposition +from ccpi.astra.ops import AstraProjectorSimple +import astra +import os, sys + + +# Load Data +loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi')) + +N = 50 +M = 50 +data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(N,M), scale=(0,1)) +ig = data.geometry + +detectors = N +angles = np.linspace(0, np.pi, N, dtype=np.float32) + +device = input('Available device: GPU==1 / CPU==0 ') +ag = AcquisitionGeometry('parallel','2D', angles, detectors) +if device=='1': + dev = 'gpu' +else: + dev = 'cpu' + +Aop = AstraProjectorSimple(ig, ag, dev) +sin = Aop.direct(data) + +noisy_data = sin + +############################################################################### +# Setup and run Astra CGLS algorithm +vol_geom = astra.create_vol_geom(N, N) +proj_geom = astra.create_proj_geom('parallel', 1.0, detectors, angles) +proj_id = astra.create_projector('linear', proj_geom, vol_geom) + +# Create a sinogram from a phantom +sinogram_id, sinogram = astra.create_sino(data.as_array(), proj_id) + +# Create a data object for the reconstruction +rec_id = astra.data2d.create('-vol', vol_geom) + +cgls_astra = astra.astra_dict('CGLS') +cgls_astra['ReconstructionDataId'] = rec_id +cgls_astra['ProjectionDataId'] = sinogram_id +cgls_astra['ProjectorId'] = proj_id + +# Create the algorithm object from the configuration structure +alg_id = astra.algorithm.create(cgls_astra) + +astra.algorithm.run(alg_id, 1000) + +recon_cgls_astra = astra.data2d.get(rec_id) + +############################################################################### +# Setup and run the CGLS algorithm +x_init = ig.allocate() +cgls = CGLS(x_init=x_init, operator=Aop, data=noisy_data) +cgls.max_iteration = 1000 +cgls.update_objective_interval = 200 +cgls.run(1000, verbose=False) + +############################################################################### +# Setup and run the PDHG algorithm +operator = Aop +f = L2NormSquared(b = noisy_data) +g = ZeroFunction() + +## Compute operator Norm +normK = operator.norm() + +## Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) + +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 1000 +pdhg.update_objective_interval = 200 +pdhg.run(1000, verbose=True) + +############################################################################### +# Setup and run the FISTA algorithm +fidelity = FunctionOperatorComposition(L2NormSquared(b=noisy_data), Aop) +regularizer = ZeroFunction() + +fista = FISTA(x_init=x_init , f=fidelity, g=regularizer) +fista.max_iteration = 1000 +fista.update_objective_interval = 200 +fista.run(1000, verbose=True) + +#%% Show results + +plt.figure(figsize=(10,10)) +plt.suptitle('Reconstructions ', fontsize=16) + +plt.subplot(2,2,1) +plt.imshow(cgls.get_output().as_array()) +plt.colorbar() +plt.title('CGLS reconstruction') + +plt.subplot(2,2,2) +plt.imshow(fista.get_output().as_array()) +plt.colorbar() +plt.title('FISTA reconstruction') + +plt.subplot(2,2,3) +plt.imshow(pdhg.get_output().as_array()) +plt.colorbar() +plt.title('PDHG reconstruction') + +plt.subplot(2,2,4) +plt.imshow(recon_cgls_astra) +plt.colorbar() +plt.title('CGLS astra') + +diff1 = pdhg.get_output() - cgls.get_output() +diff2 = fista.get_output() - cgls.get_output() +diff3 = ImageData(recon_cgls_astra) - cgls.get_output() + +plt.figure(figsize=(15,15)) + +plt.subplot(3,1,1) +plt.imshow(diff1.abs().as_array()) +plt.title('Diff PDHG vs CGLS') +plt.colorbar() + +plt.subplot(3,1,2) +plt.imshow(diff2.abs().as_array()) +plt.title('Diff FISTA vs CGLS') +plt.colorbar() + +plt.subplot(3,1,3) +plt.imshow(diff3.abs().as_array()) +plt.title('Diff CLGS astra vs CGLS') +plt.colorbar() + + +#%% + +print('Primal Objective (FISTA) {} '.format(fista.objective[-1])) +print('Primal Objective (CGLS) {} '.format(cgls.objective[-1])) +print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0])) + + +true_obj = (Aop.direct(cglsd.get_output())-noisy_data).squared_norm() +print('True objective {}'.format(true_obj)) + + diff --git a/Wrappers/Python/demos/CompareAlgorithms/CGLS_PDHG_Tikhonov.py b/Wrappers/Python/demos/CompareAlgorithms/CGLS_PDHG_Tikhonov.py new file mode 100644 index 0000000..9b6d10f --- /dev/null +++ b/Wrappers/Python/demos/CompareAlgorithms/CGLS_PDHG_Tikhonov.py @@ -0,0 +1,133 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + +""" +Compare solutions of PDHG & "Block CGLS" algorithms for + + +Problem: min_x alpha * ||\grad x ||^{2}_{2} + || A x - g ||_{2}^{2} + + + A: Projection operator + g: Sinogram + +""" + + +from ccpi.framework import AcquisitionGeometry, BlockDataContainer, AcquisitionData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG, CGLS +from ccpi.optimisation.operators import BlockOperator, Gradient + +from ccpi.optimisation.functions import ZeroFunction, BlockFunction, L2NormSquared +from ccpi.astra.ops import AstraProjectorSimple +from ccpi.framework import TestData +import os, sys + +loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi')) + +# Create Ground truth phantom and Sinogram +N = 150 +M = 150 +data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(N,M), scale=(0,1)) +ig = data.geometry + +detectors = N +angles = np.linspace(0, np.pi, N, dtype=np.float32) +ag = AcquisitionGeometry('parallel','2D', angles, detectors) + +device = input('Available device: GPU==1 / CPU==0 ') +if device=='1': + dev = 'gpu' +else: + dev = 'cpu' + +Aop = AstraProjectorSimple(ig, ag, dev) +sin = Aop.direct(data) + +noisy_data = AcquisitionData( sin.as_array() + np.random.normal(0,3,ig.shape)) + +# Setup and run the CGLS algorithm +alpha = 50 +Grad = Gradient(ig) + +# Form Tikhonov as a Block CGLS structure +op_CGLS = BlockOperator( Aop, alpha * Grad, shape=(2,1)) +block_data = BlockDataContainer(noisy_data, Grad.range_geometry().allocate()) + +x_init = ig.allocate() +cgls = CGLS(x_init=x_init, operator=op_CGLS, data=block_data) +cgls.max_iteration = 1000 +cgls.update_objective_interval = 200 +cgls.run(1000,verbose=False) + + +#Setup and run the PDHG algorithm + +# Create BlockOperator +op_PDHG = BlockOperator(Grad, Aop, shape=(2,1) ) +# Create functions +f1 = 0.5 * alpha**2 * L2NormSquared() +f2 = 0.5 * L2NormSquared(b = noisy_data) +f = BlockFunction(f1, f2) +g = ZeroFunction() + +## Compute operator Norm +normK = op_PDHG.norm() + +## Primal & dual stepsizes +sigma = 10 +tau = 1/(sigma*normK**2) + +pdhg = PDHG(f=f,g=g,operator=op_PDHG, tau=tau, sigma=sigma) +pdhg.max_iteration = 1000 +pdhg.update_objective_interval = 200 +pdhg.run(1000, verbose=False) + +# Show results +plt.figure(figsize=(10,10)) + +plt.subplot(2,1,1) +plt.imshow(cgls.get_output().as_array()) +plt.title('CGLS reconstruction') + +plt.subplot(2,1,2) +plt.imshow(pdhg.get_output().as_array()) +plt.title('PDHG reconstruction') + +plt.show() + +diff1 = pdhg.get_output() - cgls.get_output() + +plt.imshow(diff1.abs().as_array()) +plt.title('Diff PDHG vs CGLS') +plt.colorbar() +plt.show() + +plt.plot(np.linspace(0,N,M), pdhg.get_output().as_array()[int(N/2),:], label = 'PDHG') +plt.plot(np.linspace(0,N,M), cgls.get_output().as_array()[int(N/2),:], label = 'CGLS') +plt.legend() +plt.title('Middle Line Profiles') +plt.show() diff --git a/Wrappers/Python/demos/CompareAlgorithms/FISTA_vs_PDHG.py b/Wrappers/Python/demos/CompareAlgorithms/FISTA_vs_PDHG.py new file mode 100644 index 0000000..c24ebac --- /dev/null +++ b/Wrappers/Python/demos/CompareAlgorithms/FISTA_vs_PDHG.py @@ -0,0 +1,124 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + + +""" +Compare solutions of FISTA & PDHG algorithms + + +Problem: min_x alpha * ||\grad x ||^{2}_{2} + || x - g ||_{1} + + \alpha: Regularization parameter + + \nabla: Gradient operator + + g: Noisy Data with Salt & Pepper Noise + +""" + +from ccpi.framework import ImageData, ImageGeometry + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import FISTA, PDHG + +from ccpi.optimisation.operators import BlockOperator, Gradient, Identity +from ccpi.optimisation.functions import L2NormSquared, L1Norm, \ + FunctionOperatorComposition, BlockFunction, ZeroFunction + +from skimage.util import random_noise + + +# Create Ground truth and Noisy data +N = 100 + +data = np.zeros((N,N)) +data[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +data[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 +data = ImageData(data) +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) +ag = ig + +n1 = random_noise(data.as_array(), mode = 's&p', salt_vs_pepper = 0.9, amount=0.2) +noisy_data = ImageData(n1) + +# Regularisation Parameter +alpha = 5 + +############################################################################### +# Setup and run the FISTA algorithm +operator = Gradient(ig) +fidelity = L1Norm(b=noisy_data) +regulariser = FunctionOperatorComposition(alpha * L2NormSquared(), operator) + +x_init = ig.allocate() +opt = {'memopt':True} +fista = FISTA(x_init=x_init , f=regulariser, g=fidelity, opt=opt) +fista.max_iteration = 2000 +fista.update_objective_interval = 50 +fista.run(2000, verbose=False) +############################################################################### + + +############################################################################### +# Setup and run the PDHG algorithm +op1 = Gradient(ig) +op2 = Identity(ig, ag) + +operator = BlockOperator(op1, op2, shape=(2,1) ) +f = BlockFunction(alpha * L2NormSquared(), fidelity) +g = ZeroFunction() + +normK = operator.norm() + +sigma = 1 +tau = 1/(sigma*normK**2) + +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 200 +pdhg.run(2000, verbose=False) +############################################################################### + +# Show results + +plt.figure(figsize=(10,10)) + +plt.subplot(2,1,1) +plt.imshow(pdhg.get_output().as_array()) +plt.title('PDHG reconstruction') + +plt.subplot(2,1,2) +plt.imshow(fista.get_output().as_array()) +plt.title('FISTA reconstruction') + +plt.show() + +diff1 = pdhg.get_output() - fista.get_output() + +plt.imshow(diff1.abs().as_array()) +plt.title('Diff PDHG vs FISTA') +plt.colorbar() +plt.show() + + diff --git a/Wrappers/Python/demos/FISTA_examples/FISTA_CGLS.py b/Wrappers/Python/demos/FISTA_examples/FISTA_CGLS.py new file mode 100644 index 0000000..1a96a2d --- /dev/null +++ b/Wrappers/Python/demos/FISTA_examples/FISTA_CGLS.py @@ -0,0 +1,215 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + +from ccpi.framework import AcquisitionGeometry +from ccpi.optimisation.algorithms import FISTA +from ccpi.optimisation.functions import IndicatorBox, ZeroFunction, \ + L2NormSquared, FunctionOperatorComposition +from ccpi.astra.operators import AstraProjectorSimple + +import numpy as np +import matplotlib.pyplot as plt +from ccpi.framework import TestData +import os, sys +from ccpi.optimisation.funcs import Norm2sq + +# Load Data +loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi')) + +N = 50 +M = 50 +data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(N,M), scale=(0,1)) + +ig = data.geometry + +# Show Ground Truth +plt.figure(figsize=(5,5)) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.show() + +#%% +# Set up AcquisitionGeometry object to hold the parameters of the measurement +# setup geometry: # Number of angles, the actual angles from 0 to +# pi for parallel beam and 0 to 2pi for fanbeam, set the width of a detector +# pixel relative to an object pixel, the number of detector pixels, and the +# source-origin and origin-detector distance (here the origin-detector distance +# set to 0 to simulate a "virtual detector" with same detector pixel size as +# object pixel size). + +#device = input('Available device: GPU==1 / CPU==0 ') + +#if device=='1': +# dev = 'gpu' +#else: +# dev = 'cpu' + +test_case = 1 +dev = 'cpu' + +if test_case==1: + + detectors = N + angles_num = N + det_w = 1.0 + + angles = np.linspace(0, np.pi, angles_num, endpoint=False) + ag = AcquisitionGeometry('parallel', + '2D', + angles, + detectors,det_w) + +elif test_case==2: + + SourceOrig = 200 + OrigDetec = 0 + angles = np.linspace(0,2*np.pi,angles_num) + ag = AcquisitionGeometry('cone', + '2D', + angles, + detectors, + det_w, + dist_source_center=SourceOrig, + dist_center_detector=OrigDetec) +else: + NotImplemented + +# Set up Operator object combining the ImageGeometry and AcquisitionGeometry +# wrapping calls to ASTRA as well as specifying whether to use CPU or GPU. +Aop = AstraProjectorSimple(ig, ag, dev) + +# Forward and backprojection are available as methods direct and adjoint. Here +# generate test data b and do simple backprojection to obtain z. +sin = Aop.direct(data) +back_proj = Aop.adjoint(sin) + +plt.figure(figsize=(5,5)) +plt.imshow(sin.array) +plt.title('Simulated data') +plt.show() + +plt.figure(figsize=(5,5)) +plt.imshow(back_proj.array) +plt.title('Backprojected data') +plt.show() + +#%% + +# Using the test data b, different reconstruction methods can now be set up as +# demonstrated in the rest of this file. In general all methods need an initial +# guess and some algorithm options to be set: + +f = FunctionOperatorComposition(L2NormSquared(b=sin), Aop) +#f = Norm2sq(Aop, sin, c=0.5, memopt=True) +g = ZeroFunction() + +x_init = ig.allocate() +fista = FISTA(x_init=x_init, f=f, g=g) +fista.max_iteration = 1000 +fista.update_objective_interval = 100 +fista.run(1000, verbose=True) + +plt.figure() +plt.imshow(fista.get_output().as_array()) +plt.title('FISTA unconstrained') +plt.colorbar() +plt.show() + + +# Run FISTA for least squares with lower/upper bound +fista0 = FISTA(x_init=x_init, f=f, g=IndicatorBox(lower=0,upper=1)) +fista0.max_iteration = 1000 +fista0.update_objective_interval = 100 +fista0.run(1000, verbose=True) + +plt.figure() +plt.imshow(fista0.get_output().as_array()) +plt.title('FISTA constrained in [0,1]') +plt.colorbar() +plt.show() + +#plt.figure() +#plt.semilogy(fista0.objective) +#plt.title('FISTA constrained in [0,1]') +#plt.show() + +#%% Check with CVX solution + +import astra +import numpy + +try: + from cvxpy import * + cvx_not_installable = True +except ImportError: + cvx_not_installable = False + +if cvx_not_installable: + + ##Construct problem + u = Variable(N*N) + + # create matrix representation for Astra operator + vol_geom = astra.create_vol_geom(N, N) + proj_geom = astra.create_proj_geom('parallel', 1.0, detectors, angles) + + proj_id = astra.create_projector('linear', proj_geom, vol_geom) + + matrix_id = astra.projector.matrix(proj_id) + + ProjMat = astra.matrix.get(matrix_id) + + tmp = sin.as_array().ravel() + + fidelity = 0.5 * sum_squares(ProjMat * u - tmp) + + solver = MOSEK + obj = Minimize(fidelity) + constraints = [u>=0, u<=1] + prob = Problem(obj, constraints=constraints) + result = prob.solve(verbose = True, solver = solver) + + diff_cvx = numpy.abs( fista0.get_output().as_array() - np.reshape(u.value, (N,M) )) + + plt.figure(figsize=(15,15)) + plt.subplot(3,1,1) + plt.imshow(fista0.get_output().as_array()) + plt.title('FISTA solution') + plt.colorbar() + plt.subplot(3,1,2) + plt.imshow(np.reshape(u.value, (N, M))) + plt.title('CVX solution') + plt.colorbar() + plt.subplot(3,1,3) + plt.imshow(diff_cvx) + plt.title('Difference') + plt.colorbar() + plt.show() + + plt.plot(np.linspace(0,N,M), fista0.get_output().as_array()[int(N/2),:], label = 'FISTA') + plt.plot(np.linspace(0,N,M), np.reshape(u.value, (N,M) )[int(N/2),:], label = 'CVX') + plt.legend() + plt.title('Middle Line Profiles') + plt.show() + + print('Primal Objective (CVX) {} '.format(obj.value)) + print('Primal Objective (FISTA) {} '.format(fista0.loss[1]))
\ No newline at end of file diff --git a/Wrappers/Python/demos/FISTA_examples/FISTA_Tikhonov_Poisson_Denoising.py b/Wrappers/Python/demos/FISTA_examples/FISTA_Tikhonov_Poisson_Denoising.py new file mode 100644 index 0000000..6007990 --- /dev/null +++ b/Wrappers/Python/demos/FISTA_examples/FISTA_Tikhonov_Poisson_Denoising.py @@ -0,0 +1,201 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + +""" + +"Tikhonov regularization" for Poisson denoising using FISTA algorithm: + +Problem: min_x, x>0 \alpha * ||\nabla x||_{2}^{2} + \int x - g * log(x) + + \alpha: Regularization parameter + + \nabla: Gradient operator + + g: Noisy Data with Poisson Noise + + +""" + +from ccpi.framework import ImageData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import FISTA + +from ccpi.optimisation.operators import Gradient +from ccpi.optimisation.functions import KullbackLeibler, L2NormSquared, FunctionOperatorComposition + +from ccpi.framework import TestData +import os, sys +from skimage.util import random_noise + +loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi')) + +# Load Data +N = 100 +M = 100 +data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(N,M), scale=(0,1)) + +ig = data.geometry +ag = ig + +# Create Noisy data with Poisson noise +n1 = random_noise(data.as_array(), mode = 'poisson', seed = 10) +noisy_data = ImageData(n1) + +# Show Ground Truth and Noisy Data +plt.figure(figsize=(10,10)) +plt.subplot(2,1,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(2,1,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.show() + +# Regularisation Parameter +alpha = 10 + +# Setup and run the FISTA algorithm +operator = Gradient(ig) +fid = KullbackLeibler(noisy_data) + +def KL_Prox_PosCone(x, tau, out=None): + + if out is None: + tmp = 0.5 *( (x - fid.bnoise - tau) + ( (x + fid.bnoise - tau)**2 + 4*tau*fid.b ) .sqrt() ) + return tmp.maximum(0) + else: + tmp = 0.5 *( (x - fid.bnoise - tau) + + ( (x + fid.bnoise - tau)**2 + 4*tau*fid.b ) .sqrt() + ) + x.add(fid.bnoise, out=out) + out -= tau + out *= out + tmp = fid.b * (4 * tau) + out.add(tmp, out=out) + out.sqrt(out=out) + + x.subtract(fid.bnoise, out=tmp) + tmp -= tau + + out += tmp + + out *= 0.5 + + # ADD the constraint here + out.maximum(0, out=out) + +fid.proximal = KL_Prox_PosCone + +reg = FunctionOperatorComposition(alpha * L2NormSquared(), operator) + +x_init = ig.allocate() +fista = FISTA(x_init=x_init , f=reg, g=fid) +fista.max_iteration = 2000 +fista.update_objective_interval = 500 +fista.run(2000, verbose=True) + +# Show results +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(3,1,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.subplot(3,1,3) +plt.imshow(fista.get_output().as_array()) +plt.title('Reconstruction') +plt.colorbar() +plt.show() + +plt.plot(np.linspace(0,N,M), data.as_array()[int(N/2),:], label = 'GTruth') +plt.plot(np.linspace(0,N,M), fista.get_output().as_array()[int(N/2),:], label = 'Reconstruction') +plt.legend() +plt.title('Middle Line Profiles') +plt.show() + + +#%% Check with CVX solution + +from ccpi.optimisation.operators import SparseFiniteDiff + +try: + from cvxpy import * + cvx_not_installable = True +except ImportError: + cvx_not_installable = False + + +if cvx_not_installable: + + ##Construct problem + u1 = Variable(ig.shape) + q = Variable() + + DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann') + DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann') + + regulariser = alpha * sum_squares(norm(vstack([DX.matrix() * vec(u1), DY.matrix() * vec(u1)]), 2, axis = 0)) + fidelity = sum(kl_div(noisy_data.as_array(), u1)) + + constraints = [q>=fidelity, u1>=0] + + solver = SCS + obj = Minimize( regulariser + q) + prob = Problem(obj, constraints) + result = prob.solve(verbose = True, solver = solver) + + diff_cvx = numpy.abs( fista.get_output().as_array() - u1.value ) + + plt.figure(figsize=(15,15)) + plt.subplot(3,1,1) + plt.imshow(fista.get_output().as_array()) + plt.title('FISTA solution') + plt.colorbar() + plt.subplot(3,1,2) + plt.imshow(u1.value) + plt.title('CVX solution') + plt.colorbar() + plt.subplot(3,1,3) + plt.imshow(diff_cvx) + plt.title('Difference') + plt.colorbar() + plt.show() + + plt.plot(np.linspace(0,N,M), fista.get_output().as_array()[int(N/2),:], label = 'FISTA') + plt.plot(np.linspace(0,N,M), u1.value[int(N/2),:], label = 'CVX') + plt.legend() + plt.title('Middle Line Profiles') + plt.show() + + print('Primal Objective (CVX) {} '.format(obj.value)) + print('Primal Objective (FISTA) {} '.format(fista.loss[1])) + + + diff --git a/Wrappers/Python/demos/PDHG_examples/ColorbayDemo.py b/Wrappers/Python/demos/PDHG_examples/ColorbayDemo.py new file mode 100644 index 0000000..e69060f --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/ColorbayDemo.py @@ -0,0 +1,273 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + + +from ccpi.framework import ImageGeometry, ImageData, AcquisitionGeometry, AcquisitionData, BlockDataContainer + +import numpy as numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG, CGLS +from ccpi.optimisation.algs import CGLS as CGLS_old + +from ccpi.optimisation.operators import BlockOperator, Gradient +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \ + MixedL21Norm, BlockFunction + +from ccpi.astra.operators import AstraProjectorMC +from scipy.io import loadmat +import h5py + +#%% + +phantom = 'powder' + +if phantom == 'carbon': + pathname = '/media/newhd/shared/Data/ColourBay/spectral_data_sets/CarbonPd/' + filename = 'carbonPd_full_sinogram_stripes_removed.mat' + X = loadmat(pathname + filename) + X = numpy.transpose(X['SS'],(3,1,2,0)) + X = X[80:100] # delete this to take all channels + +elif phantom == 'powder': + pathname = '/media/newhd/shared/DataProcessed/' + filename = 'S_180.mat' + path = pathname + filename + arrays = {} + f = h5py.File(path) + for k, v in f.items(): + arrays[k] = numpy.array(v) + XX = arrays['S'] + X = numpy.transpose(XX,(0,2,1,3)) + X = X[100:120] + + +#%% Setup Geometry of Colorbay + +num_channels = X.shape[0] +num_pixels_h = X.shape[3] +num_pixels_v = X.shape[2] +num_angles = X.shape[1] + +# Display a single projection in a single channel +plt.imshow(X[5,5,:,:]) +plt.title('Example of a projection image in one channel' ) +plt.show() + +# Set angles to use +angles = numpy.linspace(-numpy.pi,numpy.pi,num_angles,endpoint=False) + +# Define full 3D acquisition geometry and data container. +# Geometric info is taken from the txt-file in the same dir as the mat-file +ag = AcquisitionGeometry('cone', + '3D', + angles, + pixel_num_h=num_pixels_h, + pixel_size_h=0.25, + pixel_num_v=num_pixels_v, + pixel_size_v=0.25, + dist_source_center=233.0, + dist_center_detector=245.0, + channels=num_channels) +data = AcquisitionData(X, geometry=ag) + +# Reduce to central slice by extracting relevant parameters from data and its +# geometry. Perhaps create function to extract central slice automatically? +data2d = data.subset(vertical=40) +ag2d = AcquisitionGeometry('cone', + '2D', + ag.angles, + pixel_num_h=ag.pixel_num_h, + pixel_size_h=ag.pixel_size_h, + pixel_num_v=1, + pixel_size_v=ag.pixel_size_h, + dist_source_center=ag.dist_source_center, + dist_center_detector=ag.dist_center_detector, + channels=ag.channels) +data2d.geometry = ag2d + +# Set up 2D Image Geometry. +# First need the geometric magnification to scale the voxel size relative +# to the detector pixel size. +mag = (ag.dist_source_center + ag.dist_center_detector)/ag.dist_source_center +ig2d = ImageGeometry(voxel_num_x=ag2d.pixel_num_h, + voxel_num_y=ag2d.pixel_num_h, + voxel_size_x=ag2d.pixel_size_h/mag, + voxel_size_y=ag2d.pixel_size_h/mag, + channels=X.shape[0]) + +# Create GPU multichannel projector/backprojector operator with ASTRA. +Aall = AstraProjectorMC(ig2d,ag2d,'gpu') + +# Compute and simple backprojction and display one channel as image. +Xbp = Aall.adjoint(data2d) +plt.imshow(Xbp.subset(channel=5).array) +plt.show() + +#%% CGLS + +def callback(iteration, objective, x): + plt.imshow(x.as_array()[5]) + plt.colorbar() + plt.show() + +x_init = ig2d.allocate() +cgls1 = CGLS(x_init=x_init, operator=Aall, data=data2d) +cgls1.max_iteration = 100 +cgls1.update_objective_interval = 1 +cgls1.run(5,verbose=True, callback = callback) + +plt.imshow(cgls1.get_output().subset(channel=5).array) +plt.title('CGLS') +plt.show() + +#%% Tikhonov + +alpha = 2.5 +Grad = Gradient(ig2d, correlation=Gradient.CORRELATION_SPACE) # use also CORRELATION_SPACECHANNEL + +# Form Tikhonov as a Block CGLS structure +op_CGLS = BlockOperator( Aall, alpha * Grad, shape=(2,1)) +block_data = BlockDataContainer(data2d, Grad.range_geometry().allocate()) + +cgls2 = CGLS(x_init=x_init, operator=op_CGLS, data=block_data) +cgls2.max_iteration = 100 +cgls2.update_objective_interval = 1 + +cgls2.run(10,verbose=True, callback=callback) + +plt.imshow(cgls2.get_output().subset(channel=5).array) +plt.title('Tikhonov') +plt.show() + +#%% Total Variation + +# Regularisation Parameter +#alpha_TV = 0.08 # for carbon +alpha_TV = 0.08 # for powder + +# Create operators +op1 = Gradient(ig2d, correlation=Gradient.CORRELATION_SPACE) +op2 = Aall + +# Create BlockOperator +operator = BlockOperator(op1, op2, shape=(2,1) ) + +# Create functions +f1 = alpha_TV * MixedL21Norm() +f2 = 0.5 * L2NormSquared(b=data2d) +f = BlockFunction(f1, f2) +g = ZeroFunction() + +# Compute operator Norm +#normK = 8.70320267279591 # For powder Run one time no need to compute again takes time +normK = 14.60320657253632 # for carbon + +# Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 100 +pdhg.run(1000, verbose =True, callback=callback) + + +#%% Show sinograms +channel_ind = [10,15,19] + +plt.figure(figsize=(15,15)) + +plt.subplot(4,3,1) +plt.imshow(data2d.subset(channel = channel_ind[0]).as_array()) +plt.title('Channel {}'.format(channel_ind[0])) +plt.colorbar() + +plt.subplot(4,3,2) +plt.imshow(data2d.subset(channel = channel_ind[1]).as_array()) +plt.title('Channel {}'.format(channel_ind[1])) +plt.colorbar() + +plt.subplot(4,3,3) +plt.imshow(data2d.subset(channel = channel_ind[2]).as_array()) +plt.title('Channel {}'.format(channel_ind[2])) +plt.colorbar() + +############################################################################### +# Show CGLS +plt.subplot(4,3,4) +plt.imshow(cgls1.get_output().subset(channel = channel_ind[0]).as_array()) +plt.colorbar() + +plt.subplot(4,3,5) +plt.imshow(cgls1.get_output().subset(channel = channel_ind[1]).as_array()) +plt.colorbar() + +plt.subplot(4,3,6) +plt.imshow(cgls1.get_output().subset(channel = channel_ind[2]).as_array()) +plt.colorbar() + +############################################################################### +# Show Tikhonov + +plt.subplot(4,3,7) +plt.imshow(cgls2.get_output().subset(channel = channel_ind[0]).as_array()) +plt.colorbar() + +plt.subplot(4,3,8) +plt.imshow(cgls2.get_output().subset(channel = channel_ind[1]).as_array()) +plt.colorbar() + +plt.subplot(4,3,9) +plt.imshow(cgls2.get_output().subset(channel = channel_ind[2]).as_array()) +plt.colorbar() + + +############################################################################### +# Show Total variation + +plt.subplot(4,3,10) +plt.imshow(pdhg.get_output().subset(channel = channel_ind[0]).as_array()) +plt.colorbar() + +plt.subplot(4,3,11) +plt.imshow(pdhg.get_output().subset(channel = channel_ind[1]).as_array()) +plt.colorbar() + +plt.subplot(4,3,12) +plt.imshow(pdhg.get_output().subset(channel = channel_ind[2]).as_array()) +plt.colorbar() + + +############################################################################### + + + + + + + + + + + + diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TGV_Denoising.py b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TGV_Denoising.py new file mode 100755 index 0000000..9dbcf3e --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TGV_Denoising.py @@ -0,0 +1,282 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= +""" + +Total Generalised Variation (TGV) Denoising using PDHG algorithm: + + +Problem: min_{u} \alpha * ||\nabla u - w||_{2,1} + + \beta * || E u ||_{2,1} + + Fidelity(u, g) + + \nabla: Gradient operator + E: Symmetrized Gradient operator + \alpha: Regularization parameter + \beta: Regularization parameter + + g: Noisy Data + + Fidelity = 1) L2NormSquarred ( \frac{1}{2} * || u - g ||_{2}^{2} ) if Noise is Gaussian + 2) L1Norm ( ||u - g||_{1} )if Noise is Salt & Pepper + 3) Kullback Leibler (\int u - g * log(u) + Id_{u>0}) if Noise is Poisson + + + Method = 0 ( PDHG - split ) : K = [ \nabla, - Identity + ZeroOperator, E + Identity, ZeroOperator] + + + Method = 1 (PDHG - explicit ): K = [ \nabla, - Identity + ZeroOperator, E ] + + Default: TGV denoising + noise = Gaussian + Fidelity = L2NormSquarred + method = 0 + +""" + +from ccpi.framework import ImageData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Identity, \ + Gradient, SymmetrizedGradient, ZeroOperator +from ccpi.optimisation.functions import ZeroFunction, L1Norm, \ + MixedL21Norm, BlockFunction, KullbackLeibler, L2NormSquared + +from ccpi.framework import TestData +import os, sys +if int(numpy.version.version.split('.')[1]) > 12: + from skimage.util import random_noise +else: + from demoutil import random_noise + +# user supplied input +if len(sys.argv) > 1: + which_noise = int(sys.argv[1]) +else: + which_noise = 0 +print ("Applying {} noise") + +if len(sys.argv) > 2: + method = sys.argv[2] +else: + method = '0' +print ("method ", method) + + +loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi')) +data = loader.load(TestData.SHAPES) +ig = data.geometry +ag = ig + +# Create noisy data. +noises = ['gaussian', 'poisson', 's&p'] +noise = noises[which_noise] +if noise == 's&p': + n1 = random_noise(data.as_array(), mode = noise, salt_vs_pepper = 0.9, amount=0.2, seed=10) +elif noise == 'poisson': + scale = 5 + n1 = random_noise( data.as_array()/scale, mode = noise, seed = 10)*scale +elif noise == 'gaussian': + n1 = random_noise(data.as_array(), mode = noise, seed = 10) +else: + raise ValueError('Unsupported Noise ', noise) +noisy_data = ImageData(n1) + +# Show Ground Truth and Noisy Data +plt.figure(figsize=(10,5)) +plt.subplot(1,2,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(1,2,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.show() + +# Regularisation Parameter depending on the noise distribution +if noise == 's&p': + alpha = 0.8 +elif noise == 'poisson': + alpha = .3 +elif noise == 'gaussian': + alpha = .2 + +# TODO add ref why this choice +beta = 2 * alpha + +# Fidelity +if noise == 's&p': + f3 = L1Norm(b=noisy_data) +elif noise == 'poisson': + f3 = KullbackLeibler(noisy_data) +elif noise == 'gaussian': + f3 = 0.5 * L2NormSquared(b=noisy_data) + +if method == '0': + + # Create operators + op11 = Gradient(ig) + op12 = Identity(op11.range_geometry()) + + op22 = SymmetrizedGradient(op11.domain_geometry()) + op21 = ZeroOperator(ig, op22.range_geometry()) + + op31 = Identity(ig, ag) + op32 = ZeroOperator(op22.domain_geometry(), ag) + + operator = BlockOperator(op11, -1*op12, op21, op22, op31, op32, shape=(3,2) ) + + f1 = alpha * MixedL21Norm() + f2 = beta * MixedL21Norm() + + f = BlockFunction(f1, f2, f3) + g = ZeroFunction() + +else: + + # Create operators + op11 = Gradient(ig) + op12 = Identity(op11.range_geometry()) + op22 = SymmetrizedGradient(op11.domain_geometry()) + op21 = ZeroOperator(ig, op22.range_geometry()) + + operator = BlockOperator(op11, -1*op12, op21, op22, shape=(2,2) ) + + f1 = alpha * MixedL21Norm() + f2 = beta * MixedL21Norm() + + f = BlockFunction(f1, f2) + g = BlockFunction(f3, ZeroFunction()) + +# Compute operator Norm +normK = operator.norm() + +# Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 100 +pdhg.run(2000) + +# Show results +plt.figure(figsize=(20,5)) +plt.subplot(1,4,1) +plt.imshow(data.subset(channel=0).as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(1,4,2) +plt.imshow(noisy_data.subset(channel=0).as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.subplot(1,4,3) +plt.imshow(pdhg.get_output()[0].as_array()) +plt.title('TGV Reconstruction') +plt.colorbar() +plt.subplot(1,4,4) +plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), data.as_array()[int(ig.shape[0]/2),:], label = 'GTruth') +plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output()[0].as_array()[int(ig.shape[0]/2),:], label = 'TGV reconstruction') +plt.legend() +plt.title('Middle Line Profiles') +plt.show() + +#%% Check with CVX solution + +from ccpi.optimisation.operators import SparseFiniteDiff + +try: + from cvxpy import * + cvx_not_installable = True +except ImportError: + cvx_not_installable = False + +if cvx_not_installable: + + u = Variable(ig.shape) + w1 = Variable(ig.shape) + w2 = Variable(ig.shape) + + # create TGV regulariser + DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann') + DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann') + + regulariser = alpha * sum(norm(vstack([DX.matrix() * vec(u) - vec(w1), \ + DY.matrix() * vec(u) - vec(w2)]), 2, axis = 0)) + \ + beta * sum(norm(vstack([ DX.matrix().transpose() * vec(w1), DY.matrix().transpose() * vec(w2), \ + 0.5 * ( DX.matrix().transpose() * vec(w2) + DY.matrix().transpose() * vec(w1) ), \ + 0.5 * ( DX.matrix().transpose() * vec(w2) + DY.matrix().transpose() * vec(w1) ) ]), 2, axis = 0 ) ) + + constraints = [] + + # choose solver + if 'MOSEK' in installed_solvers(): + solver = MOSEK + else: + solver = SCS + + # fidelity + if noise == 's&p': + fidelity = pnorm( u - noisy_data.as_array(),1) + elif noise == 'poisson': + fidelity = sum(kl_div(noisy_data.as_array(), u)) + solver = SCS + elif noise == 'gaussian': + fidelity = 0.5 * sum_squares(noisy_data.as_array() - u) + + obj = Minimize( regulariser + fidelity) + prob = Problem(obj) + result = prob.solve(verbose = True, solver = solver) + + diff_cvx = numpy.abs( pdhg.get_output()[0].as_array() - u.value ) + + plt.figure(figsize=(15,15)) + plt.subplot(3,1,1) + plt.imshow(pdhg.get_output()[0].as_array()) + plt.title('PDHG solution') + plt.colorbar() + plt.subplot(3,1,2) + plt.imshow(u.value) + plt.title('CVX solution') + plt.colorbar() + plt.subplot(3,1,3) + plt.imshow(diff_cvx) + plt.title('Difference') + plt.colorbar() + plt.show() + + plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output()[0].as_array()[int(ig.shape[0]/2),:], label = 'PDHG') + plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), u.value[int(ig.shape[0]/2),:], label = 'CVX') + plt.legend() + plt.title('Middle Line Profiles') + plt.show() + + print('Primal Objective (CVX) {} '.format(obj.value)) + print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0]))
\ No newline at end of file diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising.py b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising.py new file mode 100755 index 0000000..6937fa0 --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising.py @@ -0,0 +1,280 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + +""" + +Total Variation Denoising using PDHG algorithm: + + +Problem: min_{u}, \alpha * ||\nabla u||_{2,1} + Fidelity(u, g) + + \alpha: Regularization parameter + + \nabla: Gradient operator + + g: Noisy Data + + Fidelity = 1) L2NormSquarred ( \frac{1}{2} * || u - g ||_{2}^{2} ) if Noise is Gaussian + 2) L1Norm ( ||u - g||_{1} )if Noise is Salt & Pepper + 3) Kullback Leibler (\int u - g * log(u) + Id_{u>0}) if Noise is Poisson + + Method = 0 ( PDHG - split ) : K = [ \nabla, + Identity] + + + Method = 1 (PDHG - explicit ): K = \nabla + + + Default: ROF denoising + noise = Gaussian + Fidelity = L2NormSquarred + method = 0 + + +""" + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Identity, Gradient +from ccpi.optimisation.functions import ZeroFunction, L1Norm, \ + MixedL21Norm, BlockFunction, L2NormSquared,\ + KullbackLeibler +from ccpi.framework import TestData +import os, sys +#import scipy.io +if int(numpy.version.version.split('.')[1]) > 12: + from skimage.util import random_noise +else: + from demoutil import random_noise + +# user supplied input +if len(sys.argv) > 1: + which_noise = int(sys.argv[1]) +else: + which_noise = 0 +print ("Applying {} noise") + +if len(sys.argv) > 2: + method = sys.argv[2] +else: + method = '0' +print ("method ", method) + + +loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi')) +data = loader.load(TestData.SHAPES, size=(50,50)) +ig = data.geometry +ag = ig + +# Create noisy data. +noises = ['gaussian', 'poisson', 's&p'] +noise = noises[which_noise] +if noise == 's&p': + n1 = random_noise(data.as_array(), mode = noise, salt_vs_pepper = 0.9, amount=0.2) +elif noise == 'poisson': + scale = 5 + n1 = random_noise( data.as_array()/scale, mode = noise, seed = 10)*scale +elif noise == 'gaussian': + n1 = random_noise(data.as_array(), mode = noise, seed = 10) +else: + raise ValueError('Unsupported Noise ', noise) +noisy_data = ig.allocate() +noisy_data.fill(n1) + +# Show Ground Truth and Noisy Data +plt.figure(figsize=(10,5)) +plt.subplot(1,2,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(1,2,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.show() + + +# Regularisation Parameter depending on the noise distribution +if noise == 's&p': + alpha = 0.8 +elif noise == 'poisson': + alpha = 1 +elif noise == 'gaussian': + alpha = .3 + +# fidelity +if noise == 's&p': + f2 = L1Norm(b=noisy_data) +elif noise == 'poisson': + f2 = KullbackLeibler(noisy_data) +elif noise == 'gaussian': + f2 = 0.5 * L2NormSquared(b=noisy_data) + +if method == '0': + + # Create operators + op1 = Gradient(ig, correlation=Gradient.CORRELATION_SPACE) + op2 = Identity(ig, ag) + + # Create BlockOperator + operator = BlockOperator(op1, op2, shape=(2,1) ) + + # Create functions + f = BlockFunction(alpha * MixedL21Norm(), f2) + g = ZeroFunction() + +else: + + operator = Gradient(ig) + f = alpha * MixedL21Norm() + g = f2 + + +# Compute operator Norm +normK = operator.norm() + +# Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) + + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 100 +pdhg.run(2000) + + +if data.geometry.channels > 1: + plt.figure(figsize=(20,15)) + for row in range(data.geometry.channels): + + plt.subplot(3,4,1+row*4) + plt.imshow(data.subset(channel=row).as_array()) + plt.title('Ground Truth') + plt.colorbar() + plt.subplot(3,4,2+row*4) + plt.imshow(noisy_data.subset(channel=row).as_array()) + plt.title('Noisy Data') + plt.colorbar() + plt.subplot(3,4,3+row*4) + plt.imshow(pdhg.get_output().subset(channel=row).as_array()) + plt.title('TV Reconstruction') + plt.colorbar() + plt.subplot(3,4,4+row*4) + plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), data.subset(channel=row).as_array()[int(N/2),:], label = 'GTruth') + plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output().subset(channel=row).as_array()[int(N/2),:], label = 'TV reconstruction') + plt.legend() + plt.title('Middle Line Profiles') + plt.show() + +else: + plt.figure(figsize=(20,5)) + plt.subplot(1,4,1) + plt.imshow(data.subset(channel=0).as_array()) + plt.title('Ground Truth') + plt.colorbar() + plt.subplot(1,4,2) + plt.imshow(noisy_data.subset(channel=0).as_array()) + plt.title('Noisy Data') + plt.colorbar() + plt.subplot(1,4,3) + plt.imshow(pdhg.get_output().subset(channel=0).as_array()) + plt.title('TV Reconstruction') + plt.colorbar() + plt.subplot(1,4,4) + plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), data.as_array()[int(ig.shape[0]/2),:], label = 'GTruth') + plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output().as_array()[int(ig.shape[0]/2),:], label = 'TV reconstruction') + plt.legend() + plt.title('Middle Line Profiles') + plt.show() + + +#%% Check with CVX solution + +from ccpi.optimisation.operators import SparseFiniteDiff + +try: + from cvxpy import * + cvx_not_installable = True +except ImportError: + cvx_not_installable = False + + +if cvx_not_installable: + + ##Construct problem + u = Variable(ig.shape) + + DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann') + DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann') + + # Define Total Variation as a regulariser + regulariser = alpha * sum(norm(vstack([Constant(DX.matrix()) * vec(u), Constant(DY.matrix()) * vec(u)]), 2, axis = 0)) + + # choose solver + if 'MOSEK' in installed_solvers(): + solver = MOSEK + else: + solver = SCS + + # fidelity + if noise == 's&p': + fidelity = pnorm( u - noisy_data.as_array(),1) + elif noise == 'poisson': + fidelity = sum(kl_div(noisy_data.as_array(), u)) + solver = SCS + elif noise == 'gaussian': + fidelity = 0.5 * sum_squares(noisy_data.as_array() - u) + + obj = Minimize( regulariser + fidelity) + prob = Problem(obj) + result = prob.solve(verbose = True, solver = solver) + + diff_cvx = numpy.abs( pdhg.get_output().as_array() - u.value ) + + plt.figure(figsize=(15,15)) + plt.subplot(3,1,1) + plt.imshow(pdhg.get_output().as_array()) + plt.title('PDHG solution') + plt.colorbar() + plt.subplot(3,1,2) + plt.imshow(u.value) + plt.title('CVX solution') + plt.colorbar() + plt.subplot(3,1,3) + plt.imshow(diff_cvx) + plt.title('Difference') + plt.colorbar() + plt.show() + + plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output().as_array()[int(ig.shape[0]/2),:], label = 'PDHG') + plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), u.value[int(ig.shape[0]/2),:], label = 'CVX') + plt.legend() + plt.title('Middle Line Profiles') + plt.show() + + print('Primal Objective (CVX) {} '.format(obj.value)) + print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0])) diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_2D_time.py b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_2D_time.py new file mode 100644 index 0000000..febe76d --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_2D_time.py @@ -0,0 +1,243 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= +""" + +Total Variation (Dynamic) Denoising using PDHG algorithm and Tomophantom: + + +Problem: min_{x} \alpha * ||\nabla x||_{2,1} + \frac{1}{2} * || x - g ||_{2}^{2} + + \alpha: Regularization parameter + + \nabla: Gradient operator + + g: 2D Dynamic noisy data with Gaussian Noise + + K = \nabla + +""" + +from ccpi.framework import ImageData, ImageGeometry + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Gradient, Identity +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \ + MixedL21Norm, BlockFunction + +from ccpi.astra.ops import AstraProjectorMC + +import os +import tomophantom +from tomophantom import TomoP2D + +# Create phantom for TV 2D dynamic tomography + +model = 102 # note that the selected model is temporal (2D + time) +N = 128 # set dimension of the phantom + +path = os.path.dirname(tomophantom.__file__) +path_library2D = os.path.join(path, "Phantom2DLibrary.dat") +phantom_2Dt = TomoP2D.ModelTemporal(model, N, path_library2D) + +plt.close('all') +plt.figure(1) +plt.rcParams.update({'font.size': 21}) +plt.title('{}''{}'.format('2D+t phantom using model no.',model)) +for sl in range(0,np.shape(phantom_2Dt)[0]): + im = phantom_2Dt[sl,:,:] + plt.imshow(im, vmin=0, vmax=1) +# plt.pause(.1) +# plt.draw + +# Setup geometries +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N, channels = np.shape(phantom_2Dt)[0]) +data = ImageData(phantom_2Dt, geometry=ig) +ag = ig + +# Create noisy data. Apply Gaussian noise +np.random.seed(10) +noisy_data = ImageData( data.as_array() + np.random.normal(0, 0.25, size=ig.shape) ) + +# time-frames index +tindex = [8, 16, 24] + +fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 10)) +plt.subplot(1,3,1) +plt.imshow(noisy_data.as_array()[tindex[0],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[0])) +plt.subplot(1,3,2) +plt.imshow(noisy_data.as_array()[tindex[1],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[1])) +plt.subplot(1,3,3) +plt.imshow(noisy_data.as_array()[tindex[2],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[2])) + +fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8, + wspace=0.02, hspace=0.02) + +plt.show() + +# Regularisation Parameter +alpha = 0.3 + +# Create operators +op1 = Gradient(ig, correlation='Space') +op2 = Gradient(ig, correlation='SpaceChannels') +op3 = Identity(ig, ag) + +# Create BlockOperator +operator1 = BlockOperator(op1, op3, shape=(2,1) ) +operator2 = BlockOperator(op2, op3, shape=(2,1) ) + +# Create functions + +f1 = alpha * MixedL21Norm() +f2 = 0.5 * L2NormSquared(b = noisy_data) +f = BlockFunction(f1, f2) + +g = ZeroFunction() + +# Compute operator Norm +normK1 = operator1.norm() +normK2 = operator2.norm() + +#%% +# Primal & dual stepsizes +sigma1 = 1 +tau1 = 1/(sigma1*normK1**2) + +sigma2 = 1 +tau2 = 1/(sigma2*normK2**2) + +# Setup and run the PDHG algorithm +pdhg1 = PDHG(f=f,g=g,operator=operator1, tau=tau1, sigma=sigma1) +pdhg1.max_iteration = 2000 +pdhg1.update_objective_interval = 200 +pdhg1.run(2000) + +# Setup and run the PDHG algorithm +pdhg2 = PDHG(f=f,g=g,operator=operator2, tau=tau2, sigma=sigma2) +pdhg2.max_iteration = 2000 +pdhg2.update_objective_interval = 200 +pdhg2.run(2000) + + +#%% + +tindex = [8, 16, 24] +fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8)) + +plt.subplot(3,3,1) +plt.imshow(phantom_2Dt[tindex[0],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[0])) + +plt.subplot(3,3,2) +plt.imshow(phantom_2Dt[tindex[1],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[1])) + +plt.subplot(3,3,3) +plt.imshow(phantom_2Dt[tindex[2],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[2])) + +plt.subplot(3,3,4) +plt.imshow(pdhg1.get_output().as_array()[tindex[0],:,:]) +plt.axis('off') +plt.subplot(3,3,5) +plt.imshow(pdhg1.get_output().as_array()[tindex[1],:,:]) +plt.axis('off') +plt.subplot(3,3,6) +plt.imshow(pdhg1.get_output().as_array()[tindex[2],:,:]) +plt.axis('off') + + +plt.subplot(3,3,7) +plt.imshow(pdhg2.get_output().as_array()[tindex[0],:,:]) +plt.axis('off') +plt.subplot(3,3,8) +plt.imshow(pdhg2.get_output().as_array()[tindex[1],:,:]) +plt.axis('off') +plt.subplot(3,3,9) +plt.imshow(pdhg2.get_output().as_array()[tindex[2],:,:]) +plt.axis('off') + + +im = plt.imshow(pdhg1.get_output().as_array()[tindex[0],:,:]) + + +fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8, + wspace=0.02, hspace=0.02) + +cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8]) +cbar = fig.colorbar(im, cax=cb_ax) +plt.show() + +#%% +import matplotlib.animation as animation +fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 30)) +ims1 = [] +ims2 = [] +ims3 = [] +for sl in range(0,np.shape(phantom_2Dt)[0]): + + plt.subplot(1,3,1) + im1 = plt.imshow(phantom_2Dt[sl,:,:], animated=True) + + plt.subplot(1,3,2) + im2 = plt.imshow(pdhg1.get_output().as_array()[sl,:,:]) + + plt.subplot(1,3,3) + im3 = plt.imshow(pdhg2.get_output().as_array()[sl,:,:]) + + ims1.append([im1]) + ims2.append([im2]) + ims3.append([im3]) + + +ani1 = animation.ArtistAnimation(fig, ims1, interval=500, + repeat_delay=10) + +ani2 = animation.ArtistAnimation(fig, ims2, interval=500, + repeat_delay=10) + +ani3 = animation.ArtistAnimation(fig, ims3, interval=500, + repeat_delay=10) +plt.show() +# plt.pause(0.25) +# plt.show() + + + + + + + + diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_Gaussian_3D.py b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_Gaussian_3D.py new file mode 100644 index 0000000..15709cd --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_Gaussian_3D.py @@ -0,0 +1,147 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= +""" + +Total Variation (3D) Denoising using PDHG algorithm and Tomophantom: + + +Problem: min_{x} \alpha * ||\nabla x||_{2,1} + \frac{1}{2} * || x - g ||_{2}^{2} + + \alpha: Regularization parameter + + \nabla: Gradient operator + + g: 3D Noisy Data with Gaussian Noise + + Method = 0 ( PDHG - split ) : K = [ \nabla, + Identity] + + + Method = 1 (PDHG - explicit ): K = \nabla + +""" + +from ccpi.framework import ImageData, ImageGeometry +import matplotlib.pyplot as plt +from ccpi.optimisation.algorithms import PDHG +from ccpi.optimisation.operators import Gradient +from ccpi.optimisation.functions import L2NormSquared, MixedL21Norm + +from skimage.util import random_noise + +import timeit +import os +from tomophantom import TomoP3D +import tomophantom + +# Create a phantom from Tomophantom +print ("Building 3D phantom using TomoPhantom software") +tic=timeit.default_timer() +model = 13 # select a model number from the library +N = 64 # Define phantom dimensions using a scalar value (cubic phantom) +path = os.path.dirname(tomophantom.__file__) +path_library3D = os.path.join(path, "Phantom3DLibrary.dat") + +#This will generate a N x N x N phantom (3D) +phantom_tm = TomoP3D.Model(model, N, path_library3D) + +# Create noisy data. Apply Gaussian noise +ig = ImageGeometry(voxel_num_x=N, voxel_num_y=N, voxel_num_z=N) +ag = ig +n1 = random_noise(phantom_tm, mode = 'gaussian', mean=0, var = 0.001, seed=10) +noisy_data = ImageData(n1) + +# Show results +sliceSel = int(0.5*N) +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(noisy_data.as_array()[sliceSel,:,:],vmin=0, vmax=1) +plt.title('Axial View') +plt.colorbar() +plt.subplot(3,1,2) +plt.imshow(noisy_data.as_array()[:,sliceSel,:],vmin=0, vmax=1) +plt.title('Coronal View') +plt.colorbar() +plt.subplot(3,1,3) +plt.imshow(noisy_data.as_array()[:,:,sliceSel],vmin=0, vmax=1) +plt.title('Sagittal View') +plt.colorbar() +plt.show() + +# Regularisation Parameter +alpha = 0.05 + +# Setup and run the PDHG algorithm +operator = Gradient(ig) +f = alpha * MixedL21Norm() +g = 0.5 * L2NormSquared(b = noisy_data) + +normK = operator.norm() + +sigma = 1 +tau = 1/(sigma*normK**2) + +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 1000 +pdhg.update_objective_interval = 200 +pdhg.run(1000, verbose = True) + +# Show results +fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8)) +fig.suptitle('TV Reconstruction',fontsize=20) + +plt.subplot(2,3,1) +plt.imshow(noisy_data.as_array()[sliceSel,:,:],vmin=0, vmax=1) +plt.axis('off') +plt.title('Axial View') + +plt.subplot(2,3,2) +plt.imshow(noisy_data.as_array()[:,sliceSel,:],vmin=0, vmax=1) +plt.axis('off') +plt.title('Coronal View') + +plt.subplot(2,3,3) +plt.imshow(noisy_data.as_array()[:,:,sliceSel],vmin=0, vmax=1) +plt.axis('off') +plt.title('Sagittal View') + + +plt.subplot(2,3,4) +plt.imshow(pdhg.get_output().as_array()[sliceSel,:,:],vmin=0, vmax=1) +plt.axis('off') +plt.subplot(2,3,5) +plt.imshow(pdhg.get_output().as_array()[:,sliceSel,:],vmin=0, vmax=1) +plt.axis('off') +plt.subplot(2,3,6) +plt.imshow(pdhg.get_output().as_array()[:,:,sliceSel],vmin=0, vmax=1) +plt.axis('off') +im = plt.imshow(pdhg.get_output().as_array()[:,:,sliceSel],vmin=0, vmax=1) + + +fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8, + wspace=0.02, hspace=0.02) + +cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8]) +cbar = fig.colorbar(im, cax=cb_ax) + + +plt.show() + diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Tomo2D.py b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Tomo2D.py new file mode 100644 index 0000000..4f7639e --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Tomo2D.py @@ -0,0 +1,173 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + +""" + +Total Variation 2D Tomography Reconstruction using PDHG algorithm: + + +Problem: min_u \alpha * ||\nabla u||_{2,1} + \frac{1}{2}||Au - g||^{2} + min_u, u>0 \alpha * ||\nabla u||_{2,1} + \int A u - g log (Au + \eta) + + \nabla: Gradient operator + A: System Matrix + g: Noisy sinogram + \eta: Background noise + + \alpha: Regularization parameter + +""" + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Gradient +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \ + MixedL21Norm, BlockFunction, KullbackLeibler, IndicatorBox + +from ccpi.astra.ops import AstraProjectorSimple +from ccpi.framework import TestData +from PIL import Image +import os, sys +#if int(numpy.version.version.split('.')[1]) > 12: +from skimage.util import random_noise +#else: +# from demoutil import random_noise + +#import scipy.io + +# user supplied input +if len(sys.argv) > 1: + which_noise = int(sys.argv[1]) +else: + which_noise = 1 + +# Load 256 shepp-logan +data256 = scipy.io.loadmat('phantom.mat')['phantom256'] +data = ImageData(numpy.array(Image.fromarray(data256).resize((256,256)))) +N, M = data.shape +ig = ImageGeometry(voxel_num_x=N, voxel_num_y=M) + +# Add it to testdata or use tomophantom +#loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi')) +#data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(50, 50)) +#ig = data.geometry + +# Create acquisition data and geometry +detectors = N +angles = np.linspace(0, np.pi, 180) +ag = AcquisitionGeometry('parallel','2D',angles, detectors) + +# Select device +device = '0' +#device = input('Available device: GPU==1 / CPU==0 ') +if device=='1': + dev = 'gpu' +else: + dev = 'cpu' + +Aop = AstraProjectorSimple(ig, ag, dev) +sin = Aop.direct(data) + +# Create noisy data. Apply Gaussian noise +noises = ['gaussian', 'poisson'] +noise = noises[which_noise] + +if noise == 'poisson': + scale = 5 + eta = 0 + noisy_data = AcquisitionData(np.random.poisson( scale * (eta + sin.as_array()))/scale, ag) +elif noise == 'gaussian': + n1 = np.random.normal(0, 1, size = ag.shape) + noisy_data = AcquisitionData(n1 + sin.as_array(), ag) +else: + raise ValueError('Unsupported Noise ', noise) + +# Show Ground Truth and Noisy Data +plt.figure(figsize=(10,10)) +plt.subplot(1,2,2) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(1,2,1) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.show() + +# Create operators +op1 = Gradient(ig) +op2 = Aop + +# Create BlockOperator +operator = BlockOperator(op1, op2, shape=(2,1) ) + +# Compute operator Norm +normK = operator.norm() + +# Create functions +if noise == 'poisson': + alpha = 3 + f2 = KullbackLeibler(noisy_data) + g = IndicatorBox(lower=0) + sigma = 1 + tau = 1/(sigma*normK**2) + +elif noise == 'gaussian': + alpha = 20 + f2 = 0.5 * L2NormSquared(b=noisy_data) + g = ZeroFunction() + sigma = 10 + tau = 1/(sigma*normK**2) + +f1 = alpha * MixedL21Norm() +f = BlockFunction(f1, f2) + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 200 +pdhg.run(2000) + +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(3,1,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.subplot(3,1,3) +plt.imshow(pdhg.get_output().as_array()) +plt.title('TV Reconstruction') +plt.colorbar() +plt.show() +plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), data.as_array()[int(N/2),:], label = 'GTruth') +plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output().as_array()[int(N/2),:], label = 'TV reconstruction') +plt.legend() +plt.title('Middle Line Profiles') +plt.show() diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_Tikhonov_Denoising.py b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_Tikhonov_Denoising.py new file mode 100644 index 0000000..78d4980 --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_Tikhonov_Denoising.py @@ -0,0 +1,258 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + +""" + +``Tikhonov`` Regularization Denoising using PDHG algorithm: + + +Problem: min_{u}, \alpha * ||\nabla u||_{2,2} + Fidelity(u, g) + + \alpha: Regularization parameter + + \nabla: Gradient operator + + g: Noisy Data + + Fidelity = 1) L2NormSquarred ( \frac{1}{2} * || u - g ||_{2}^{2} ) if Noise is Gaussian + 2) L1Norm ( ||u - g||_{1} )if Noise is Salt & Pepper + 3) Kullback Leibler (\int u - g * log(u) + Id_{u>0}) if Noise is Poisson + + Method = 0 ( PDHG - split ) : K = [ \nabla, + Identity] + + + Method = 1 (PDHG - explicit ): K = \nabla + + + Default: Tikhonov denoising + noise = Gaussian + Fidelity = L2NormSquarred + method = 0 + +""" + +from ccpi.framework import ImageData, TestData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Identity, Gradient +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared,\ + BlockFunction, KullbackLeibler, L1Norm + +import sys, os +if int(numpy.version.version.split('.')[1]) > 12: + from skimage.util import random_noise +else: + from demoutil import random_noise + + +# user supplied input +if len(sys.argv) > 1: + which_noise = int(sys.argv[1]) +else: + which_noise = 2 +print ("Applying {} noise") + +if len(sys.argv) > 2: + method = sys.argv[2] +else: + method = '0' +print ("method ", method) + +loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi')) +data = loader.load(TestData.SHAPES) +ig = data.geometry +ag = ig + +# Create noisy data. +noises = ['gaussian', 'poisson', 's&p'] +noise = noises[which_noise] +if noise == 's&p': + n1 = random_noise(data.as_array(), mode = noise, salt_vs_pepper = 0.9, amount=0.2) +elif noise == 'poisson': + scale = 5 + n1 = random_noise( data.as_array()/scale, mode = noise, seed = 10)*scale +elif noise == 'gaussian': + n1 = random_noise(data.as_array(), mode = noise, seed = 10) +else: + raise ValueError('Unsupported Noise ', noise) +noisy_data = ImageData(n1) + +# Show Ground Truth and Noisy Data +plt.figure(figsize=(10,5)) +plt.subplot(1,2,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(1,2,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.show() + +# Regularisation Parameter depending on the noise distribution +if noise == 's&p': + alpha = 20 +elif noise == 'poisson': + alpha = 10 +elif noise == 'gaussian': + alpha = 5 + +# fidelity +if noise == 's&p': + f2 = L1Norm(b=noisy_data) +elif noise == 'poisson': + f2 = KullbackLeibler(noisy_data) +elif noise == 'gaussian': + f2 = 0.5 * L2NormSquared(b=noisy_data) + +if method == '0': + + # Create operators + op1 = Gradient(ig) + op2 = Identity(ig, ag) + + # Create BlockOperator + operator = BlockOperator(op1, op2, shape=(2,1) ) + + # Create functions + f1 = alpha * L2NormSquared() + f = BlockFunction(f1, f2) + g = ZeroFunction() + +else: + + operator = Gradient(ig) + g = f2 + + +# Compute operator Norm +normK = operator.norm() + +# Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 100 +pdhg.run(2000) + + +plt.figure(figsize=(20,5)) +plt.subplot(1,4,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(1,4,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.subplot(1,4,3) +plt.imshow(pdhg.get_output().as_array()) +plt.title('TV Reconstruction') +plt.colorbar() +plt.subplot(1,4,4) +plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), data.as_array()[int(ig.shape[0]/2),:], label = 'GTruth') +plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output().as_array()[int(ig.shape[0]/2),:], label = 'Tikhonov reconstruction') +plt.legend() +plt.title('Middle Line Profiles') +plt.show() + + + +##%% Check with CVX solution + +from ccpi.optimisation.operators import SparseFiniteDiff + +try: + from cvxpy import * + cvx_not_installable = True +except ImportError: + cvx_not_installable = False + +if cvx_not_installable: + + ##Construct problem + u = Variable(ig.shape) + + DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann') + DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann') + + # Define Total Variation as a regulariser + + regulariser = alpha * sum_squares(norm(vstack([DX.matrix() * vec(u), DY.matrix() * vec(u)]), 2, axis = 0)) + + # choose solver + if 'MOSEK' in installed_solvers(): + solver = MOSEK + else: + solver = SCS + + # fidelity + if noise == 's&p': + fidelity = pnorm( u - noisy_data.as_array(),1) + elif noise == 'poisson': + fidelity = sum(kl_div(noisy_data.as_array(), u)) + solver = SCS + elif noise == 'gaussian': + fidelity = 0.5 * sum_squares(noisy_data.as_array() - u) + + obj = Minimize( regulariser + fidelity) + prob = Problem(obj) + result = prob.solve(verbose = True, solver = solver) + + diff_cvx = numpy.abs( pdhg.get_output().as_array() - u.value ) + + plt.figure(figsize=(15,15)) + plt.subplot(3,1,1) + plt.imshow(pdhg.get_output().as_array()) + plt.title('PDHG solution') + plt.colorbar() + plt.subplot(3,1,2) + plt.imshow(u.value) + plt.title('CVX solution') + plt.colorbar() + plt.subplot(3,1,3) + plt.imshow(diff_cvx) + plt.title('Difference') + plt.colorbar() + plt.show() + + plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output().as_array()[int(ig.shape[0]/2),:], label = 'PDHG') + plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), u.value[int(ig.shape[0]/2),:], label = 'CVX') + plt.legend() + plt.title('Middle Line Profiles') + plt.show() + + print('Primal Objective (CVX) {} '.format(obj.value)) + print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0])) +# +# +# +# +# diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/phantom.mat b/Wrappers/Python/demos/PDHG_examples/GatherAll/phantom.mat Binary files differnew file mode 100755 index 0000000..c465bbe --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/phantom.mat diff --git a/Wrappers/Python/demos/PDHG_examples/IMATDemo.py b/Wrappers/Python/demos/PDHG_examples/IMATDemo.py new file mode 100644 index 0000000..2051860 --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/IMATDemo.py @@ -0,0 +1,339 @@ + + +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Mon Mar 25 12:50:27 2019 + +@author: vaggelis +""" + +from ccpi.framework import ImageData, ImageGeometry, BlockDataContainer, AcquisitionGeometry, AcquisitionData +from astropy.io import fits +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import CGLS, PDHG +from ccpi.optimisation.functions import MixedL21Norm, L2NormSquared, BlockFunction, ZeroFunction, KullbackLeibler, IndicatorBox +from ccpi.optimisation.operators import Gradient, BlockOperator + +from ccpi.astra.operators import AstraProjectorMC, AstraProjectorSimple + +import pickle + + +# load file + +#filename_sino = '/media/newhd/shared/DataProcessed/IMAT_beamtime_Feb_2019/preprocessed_test_flat/sino/rebin_slice_350/sino_log_rebin_282.fits' +#filename_sino = '/media/newhd/shared/DataProcessed/IMAT_beamtime_Feb_2019/preprocessed_test_flat/sino/rebin_slice_350/sino_log_rebin_564.fits' +#filename_sino = '/media/newhd/shared/DataProcessed/IMAT_beamtime_Feb_2019/preprocessed_test_flat/sino/rebin_slice_350/sino_log_rebin_141.fits' +filename_sino = '/media/newhd/shared/DataProcessed/IMAT_beamtime_Feb_2019/preprocessed_test_flat/sino/rebin_slice_350/sino_log_rebin_80_channels.fits' + +sino_handler = fits.open(filename_sino) +sino = numpy.array(sino_handler[0].data, dtype=float) + +# change axis order: channels, angles, detectors +sino_new = numpy.rollaxis(sino, 2) +sino_handler.close() + + +sino_shape = sino_new.shape + +num_channels = sino_shape[0] # channelss +num_pixels_h = sino_shape[2] # detectors +num_pixels_v = sino_shape[2] # detectors +num_angles = sino_shape[1] # angles + + +ig = ImageGeometry(voxel_num_x = num_pixels_h, voxel_num_y = num_pixels_v, channels = num_channels) + +with open("/media/newhd/vaggelis/CCPi/IMAT_reconstruction/CCPi-Framework/Wrappers/Python/ccpi/optimisation/IMAT_data/golden_angles_new.txt") as f: + angles_string = [line.rstrip() for line in f] + angles = numpy.array(angles_string).astype(float) + + +ag = AcquisitionGeometry('parallel', '2D', angles * numpy.pi / 180, pixel_num_h = num_pixels_h, channels = num_channels) +op_MC = AstraProjectorMC(ig, ag, 'gpu') + +sino_aqdata = AcquisitionData(sino_new, ag) +result_bp = op_MC.adjoint(sino_aqdata) + +#%% + +channel = [40, 60] +for j in range(2): + z4 = sino_aqdata.as_array()[channel[j]] + plt.figure(figsize=(10,6)) + plt.imshow(z4, cmap='viridis') + plt.axis('off') + plt.savefig('Sino_141/Sinogram_ch_{}_.png'.format(channel[j]), bbox_inches='tight', transparent=True) + plt.show() + +#%% + +def callback(iteration, objective, x): + plt.imshow(x.as_array()[40]) + plt.colorbar() + plt.show() + +#%% +# CGLS + +x_init = ig.allocate() +cgls1 = CGLS(x_init=x_init, operator=op_MC, data=sino_aqdata) +cgls1.max_iteration = 100 +cgls1.update_objective_interval = 2 +cgls1.run(20,verbose=True, callback=callback) + +plt.imshow(cgls1.get_output().subset(channel=20).array) +plt.title('CGLS') +plt.colorbar() +plt.show() + +#%% +with open('Sino_141/CGLS/CGLS_{}_iter.pkl'.format(20), 'wb') as f: + z = cgls1.get_output() + pickle.dump(z, f) + +#%% +#% Tikhonov Space + +x_init = ig.allocate() +alpha = [1,3,5,10,20,50] + +for a in alpha: + + Grad = Gradient(ig, correlation = Gradient.CORRELATION_SPACE) + operator = BlockOperator(op_MC, a * Grad, shape=(2,1)) + blockData = BlockDataContainer(sino_aqdata, \ + Grad.range_geometry().allocate()) + cgls2 = CGLS() + cgls2.max_iteration = 500 + cgls2.set_up(x_init, operator, blockData) + cgls2.update_objective_interval = 50 + cgls2.run(100,verbose=True) + + with open('Sino_141/CGLS_Space/CGLS_a_{}.pkl'.format(a), 'wb') as f: + z = cgls2.get_output() + pickle.dump(z, f) + +#% Tikhonov SpaceChannels + +for a1 in alpha: + + Grad1 = Gradient(ig, correlation = Gradient.CORRELATION_SPACECHANNEL) + operator1 = BlockOperator(op_MC, a1 * Grad1, shape=(2,1)) + blockData1 = BlockDataContainer(sino_aqdata, \ + Grad1.range_geometry().allocate()) + cgls3 = CGLS() + cgls3.max_iteration = 500 + cgls3.set_up(x_init, operator1, blockData1) + cgls3.update_objective_interval = 10 + cgls3.run(100, verbose=True) + + with open('Sino_141/CGLS_SpaceChannels/CGLS_a_{}.pkl'.format(a1), 'wb') as f1: + z1 = cgls3.get_output() + pickle.dump(z1, f1) + + + +#%% +# +ig_tmp = ImageGeometry(voxel_num_x = num_pixels_h, voxel_num_y = num_pixels_v) +ag_tmp = AcquisitionGeometry('parallel', '2D', angles * numpy.pi / 180, pixel_num_h = num_pixels_h) +op_tmp = AstraProjectorSimple(ig_tmp, ag_tmp, 'gpu') +normK1 = op_tmp.norm() + +alpha_TV = [2, 5, 10] # for powder + +# Create operators +op1 = Gradient(ig, correlation=Gradient.CORRELATION_SPACECHANNEL) +op2 = op_MC + +# Create BlockOperator +operator = BlockOperator(op1, op2, shape=(2,1) ) + + +for alpha in alpha_TV: +# Create functions + f1 = alpha * MixedL21Norm() + + f2 = KullbackLeibler(sino_aqdata) + f = BlockFunction(f1, f2) + g = IndicatorBox(lower=0) + + # Compute operator Norm + normK = numpy.sqrt(8 + normK1**2) + + # Primal & dual stepsizes + sigma = 1 + tau = 1/(sigma*normK**2) + + # Setup and run the PDHG algorithm + pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma) + pdhg.max_iteration = 5000 + pdhg.update_objective_interval = 500 +# pdhg.run(2000, verbose=True, callback=callback) + pdhg.run(5000, verbose=True, callback=callback) +# + with open('Sino_141/TV_SpaceChannels/TV_a = {}.pkl'.format(alpha), 'wb') as f3: + z3 = pdhg.get_output() + pickle.dump(z3, f3) +# +# +# +# +##%% +# +#ig_tmp = ImageGeometry(voxel_num_x = num_pixels_h, voxel_num_y = num_pixels_v) +#ag_tmp = AcquisitionGeometry('parallel', '2D', angles * numpy.pi / 180, pixel_num_h = num_pixels_h) +#op_tmp = AstraProjectorSimple(ig_tmp, ag_tmp, 'gpu') +#normK1 = op_tmp.norm() +# +#alpha_TV = 10 # for powder +# +## Create operators +#op1 = Gradient(ig, correlation=Gradient.CORRELATION_SPACECHANNEL) +#op2 = op_MC +# +## Create BlockOperator +#operator = BlockOperator(op1, op2, shape=(2,1) ) +# +# +## Create functions +#f1 = alpha_TV * MixedL21Norm() +#f2 = 0.5 * L2NormSquared(b=sino_aqdata) +#f = BlockFunction(f1, f2) +#g = ZeroFunction() +# +## Compute operator Norm +##normK = 8.70320267279591 # For powder Run one time no need to compute again takes time +#normK = numpy.sqrt(8 + normK1**2) # for carbon +# +## Primal & dual stepsizes +#sigma = 0.1 +#tau = 1/(sigma*normK**2) +# +#def callback(iteration, objective, x): +# plt.imshow(x.as_array()[100]) +# plt.colorbar() +# plt.show() +# +## Setup and run the PDHG algorithm +#pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma) +#pdhg.max_iteration = 2000 +#pdhg.update_objective_interval = 100 +#pdhg.run(2000, verbose=True) +# +# +# +# +# +# + + + + + + + + + + +#%% + +#with open('/media/newhd/vaggelis/CCPi/IMAT_reconstruction/CCPi-Framework/Wrappers/Python/ccpi/optimisation/CGLS_Tikhonov/CGLS_Space/CGLS_Space_a = 50.pkl', 'wb') as f: +# z = cgls2.get_output() +# pickle.dump(z, f) +# + + #%% +with open('Sino_141/CGLS_Space/CGLS_Space_a_20.pkl', 'rb') as f1: + x = pickle.load(f1) + +with open('Sino_141/CGLS_SpaceChannels/CGLS_SpaceChannels_a_20.pkl', 'rb') as f1: + x1 = pickle.load(f1) + + + +# +plt.imshow(x.as_array()[40]*mask) +plt.colorbar() +plt.show() + +plt.imshow(x1.as_array()[40]*mask) +plt.colorbar() +plt.show() + +plt.plot(x.as_array()[40,100,:]) +plt.plot(x1.as_array()[40,100,:]) +plt.show() + +#%% + +# Show results + +def circ_mask(h, w, center=None, radius=None): + + if center is None: # use the middle of the image + center = [int(w/2), int(h/2)] + if radius is None: # use the smallest distance between the center and image walls + radius = min(center[0], center[1], w-center[0], h-center[1]) + + Y, X = numpy.ogrid[:h, :w] + dist_from_center = numpy.sqrt((X - center[0])**2 + (Y-center[1])**2) + + mask = dist_from_center <= radius + return mask + +mask = circ_mask(141, 141, center=None, radius = 55) +plt.imshow(numpy.multiply(x.as_array()[40],mask)) +plt.show() +#%% +#channel = [100, 200, 300] +# +#for i in range(3): +# tmp = cgls1.get_output().as_array()[channel[i]] +# +# z = tmp * mask +# plt.figure(figsize=(10,6)) +# plt.imshow(z, vmin=0, cmap='viridis') +# plt.axis('off') +## plt.clim(0, 0.02) +## plt.colorbar() +## del z +# plt.savefig('CGLS_282/CGLS_Chan_{}.png'.format(channel[i]), bbox_inches='tight', transparent=True) +# plt.show() +# +# +##%% Line Profiles +# +#n1, n2, n3 = cgs.get_output().as_array().shape +#mask = circ_mask(564, 564, center=None, radius = 220) +#material = ['Cu', 'Fe', 'Ni'] +#ycoords = [200, 300, 380] +# +#for i in range(3): +# z = cgs.get_output().as_array()[channel[i]] * mask +# +# for k1 in range(len(ycoords)): +# plt.plot(numpy.arange(0,n2), z[ycoords[k1],:]) +# plt.title('Channel {}: {}'.format(channel[i], material[k1])) +# plt.savefig('CGLS/line_profile_chan_{}_material_{}.png'.\ +# format(channel[i], material[k1]), bbox_inches='tight') +# plt.show() +# +# +# +# +# +##%% +# +#%% + + + +#%% + +#plt.imshow(pdhg.get_output().subset(channel=100).as_array()) +#plt.show() diff --git a/Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_2D_time_denoising.py b/Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_2D_time_denoising.py new file mode 100644 index 0000000..045458a --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_2D_time_denoising.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Gradient +from ccpi.optimisation.functions import ZeroFunction, KullbackLeibler, \ + MixedL21Norm, BlockFunction + +from ccpi.astra.ops import AstraProjectorMC + +import os +import tomophantom +from tomophantom import TomoP2D + +# Create phantom for TV 2D dynamic tomography + +model = 102 # note that the selected model is temporal (2D + time) +N = 50 # set dimension of the phantom +# one can specify an exact path to the parameters file +# path_library2D = '../../../PhantomLibrary/models/Phantom2DLibrary.dat' +path = os.path.dirname(tomophantom.__file__) +path_library2D = os.path.join(path, "Phantom2DLibrary.dat") +#This will generate a N_size x N_size x Time frames phantom (2D + time) +phantom_2Dt = TomoP2D.ModelTemporal(model, N, path_library2D) + +plt.close('all') +plt.figure(1) +plt.rcParams.update({'font.size': 21}) +plt.title('{}''{}'.format('2D+t phantom using model no.',model)) +for sl in range(0,np.shape(phantom_2Dt)[0]): + im = phantom_2Dt[sl,:,:] + plt.imshow(im, vmin=0, vmax=1) + plt.pause(.1) + plt.draw + + +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N, channels = np.shape(phantom_2Dt)[0]) +data = ImageData(phantom_2Dt, geometry=ig) + +detectors = N +angles = np.linspace(0,np.pi,N) + +ag = AcquisitionGeometry('parallel','2D', angles, detectors, channels = np.shape(phantom_2Dt)[0]) +Aop = AstraProjectorMC(ig, ag, 'gpu') +sin = Aop.direct(data) + +scale = 2 +n1 = scale * np.random.poisson(sin.as_array()/scale) +noisy_data = AcquisitionData(n1, ag) + +tindex = [3, 6, 10] + +fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 10)) +plt.subplot(1,3,1) +plt.imshow(noisy_data.as_array()[tindex[0],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[0])) +plt.subplot(1,3,2) +plt.imshow(noisy_data.as_array()[tindex[1],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[1])) +plt.subplot(1,3,3) +plt.imshow(noisy_data.as_array()[tindex[2],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[2])) + +fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8, + wspace=0.02, hspace=0.02) + +plt.show() + +#%% +# Regularisation Parameter +alpha = 5 + +# Create operators +#op1 = Gradient(ig) +op1 = Gradient(ig, correlation='SpaceChannels') +op2 = Aop + +# Create BlockOperator +operator = BlockOperator(op1, op2, shape=(2,1) ) + +# Create functions + +f1 = alpha * MixedL21Norm() +f2 = KullbackLeibler(noisy_data) +f = BlockFunction(f1, f2) + +g = ZeroFunction() + +# Compute operator Norm +normK = operator.norm() + +# Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) + + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 200 +pdhg.run(2000) + + +#%% +fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8)) + +plt.subplot(2,3,1) +plt.imshow(phantom_2Dt[tindex[0],:,:],vmin=0, vmax=1) +plt.axis('off') +plt.title('Time {}'.format(tindex[0])) + +plt.subplot(2,3,2) +plt.imshow(phantom_2Dt[tindex[1],:,:],vmin=0, vmax=1) +plt.axis('off') +plt.title('Time {}'.format(tindex[1])) + +plt.subplot(2,3,3) +plt.imshow(phantom_2Dt[tindex[2],:,:],vmin=0, vmax=1) +plt.axis('off') +plt.title('Time {}'.format(tindex[2])) + + +plt.subplot(2,3,4) +plt.imshow(pdhg.get_output().as_array()[tindex[0],:,:]) +plt.axis('off') +plt.subplot(2,3,5) +plt.imshow(pdhg.get_output().as_array()[tindex[1],:,:]) +plt.axis('off') +plt.subplot(2,3,6) +plt.imshow(pdhg.get_output().as_array()[tindex[2],:,:]) +plt.axis('off') +im = plt.imshow(pdhg.get_output().as_array()[tindex[0],:,:]) + + +fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8, + wspace=0.02, hspace=0.02) + +cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8]) +cbar = fig.colorbar(im, cax=cb_ax) + + +plt.show() + diff --git a/Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_TV_Tomo2D_time.py b/Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_TV_Tomo2D_time.py new file mode 100644 index 0000000..045458a --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_TV_Tomo2D_time.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Gradient +from ccpi.optimisation.functions import ZeroFunction, KullbackLeibler, \ + MixedL21Norm, BlockFunction + +from ccpi.astra.ops import AstraProjectorMC + +import os +import tomophantom +from tomophantom import TomoP2D + +# Create phantom for TV 2D dynamic tomography + +model = 102 # note that the selected model is temporal (2D + time) +N = 50 # set dimension of the phantom +# one can specify an exact path to the parameters file +# path_library2D = '../../../PhantomLibrary/models/Phantom2DLibrary.dat' +path = os.path.dirname(tomophantom.__file__) +path_library2D = os.path.join(path, "Phantom2DLibrary.dat") +#This will generate a N_size x N_size x Time frames phantom (2D + time) +phantom_2Dt = TomoP2D.ModelTemporal(model, N, path_library2D) + +plt.close('all') +plt.figure(1) +plt.rcParams.update({'font.size': 21}) +plt.title('{}''{}'.format('2D+t phantom using model no.',model)) +for sl in range(0,np.shape(phantom_2Dt)[0]): + im = phantom_2Dt[sl,:,:] + plt.imshow(im, vmin=0, vmax=1) + plt.pause(.1) + plt.draw + + +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N, channels = np.shape(phantom_2Dt)[0]) +data = ImageData(phantom_2Dt, geometry=ig) + +detectors = N +angles = np.linspace(0,np.pi,N) + +ag = AcquisitionGeometry('parallel','2D', angles, detectors, channels = np.shape(phantom_2Dt)[0]) +Aop = AstraProjectorMC(ig, ag, 'gpu') +sin = Aop.direct(data) + +scale = 2 +n1 = scale * np.random.poisson(sin.as_array()/scale) +noisy_data = AcquisitionData(n1, ag) + +tindex = [3, 6, 10] + +fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 10)) +plt.subplot(1,3,1) +plt.imshow(noisy_data.as_array()[tindex[0],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[0])) +plt.subplot(1,3,2) +plt.imshow(noisy_data.as_array()[tindex[1],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[1])) +plt.subplot(1,3,3) +plt.imshow(noisy_data.as_array()[tindex[2],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[2])) + +fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8, + wspace=0.02, hspace=0.02) + +plt.show() + +#%% +# Regularisation Parameter +alpha = 5 + +# Create operators +#op1 = Gradient(ig) +op1 = Gradient(ig, correlation='SpaceChannels') +op2 = Aop + +# Create BlockOperator +operator = BlockOperator(op1, op2, shape=(2,1) ) + +# Create functions + +f1 = alpha * MixedL21Norm() +f2 = KullbackLeibler(noisy_data) +f = BlockFunction(f1, f2) + +g = ZeroFunction() + +# Compute operator Norm +normK = operator.norm() + +# Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) + + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 200 +pdhg.run(2000) + + +#%% +fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8)) + +plt.subplot(2,3,1) +plt.imshow(phantom_2Dt[tindex[0],:,:],vmin=0, vmax=1) +plt.axis('off') +plt.title('Time {}'.format(tindex[0])) + +plt.subplot(2,3,2) +plt.imshow(phantom_2Dt[tindex[1],:,:],vmin=0, vmax=1) +plt.axis('off') +plt.title('Time {}'.format(tindex[1])) + +plt.subplot(2,3,3) +plt.imshow(phantom_2Dt[tindex[2],:,:],vmin=0, vmax=1) +plt.axis('off') +plt.title('Time {}'.format(tindex[2])) + + +plt.subplot(2,3,4) +plt.imshow(pdhg.get_output().as_array()[tindex[0],:,:]) +plt.axis('off') +plt.subplot(2,3,5) +plt.imshow(pdhg.get_output().as_array()[tindex[1],:,:]) +plt.axis('off') +plt.subplot(2,3,6) +plt.imshow(pdhg.get_output().as_array()[tindex[2],:,:]) +plt.axis('off') +im = plt.imshow(pdhg.get_output().as_array()[tindex[0],:,:]) + + +fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8, + wspace=0.02, hspace=0.02) + +cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8]) +cbar = fig.colorbar(im, cax=cb_ax) + + +plt.show() + diff --git a/Wrappers/Python/demos/PDHG_examples/PDHG_TV_Color_Denoising.py b/Wrappers/Python/demos/PDHG_examples/PDHG_TV_Color_Denoising.py new file mode 100644 index 0000000..ddf5ace --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/PDHG_TV_Color_Denoising.py @@ -0,0 +1,115 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + +""" +Total Variation Denoising using PDHG algorithm: +Problem: min_x, x>0 \alpha * ||\nabla x||_{2,1} + ||x-g||_{1} + \alpha: Regularization parameter + + \nabla: Gradient operator + + g: Noisy Data with Salt & Pepper Noise + + + Method = 0 ( PDHG - split ) : K = [ \nabla, + Identity] + + + Method = 1 (PDHG - explicit ): K = \nabla + + +""" + +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import Gradient, BlockOperator, FiniteDiff +from ccpi.optimisation.functions import MixedL21Norm, MixedL11Norm, L2NormSquared, BlockFunction, L1Norm +from ccpi.framework import TestData, ImageGeometry +import os, sys +if int(numpy.version.version.split('.')[1]) > 12: + from skimage.util import random_noise +else: + from demoutil import random_noise + +loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi')) +data = loader.load(TestData.PEPPERS, size=(256,256)) +ig = data.geometry +ag = ig + +# Create noisy data. +n1 = random_noise(data.as_array(), mode = 'gaussian', var = 0.15, seed = 50) +noisy_data = ig.allocate() +noisy_data.fill(n1) + +# Show Ground Truth and Noisy Data +plt.figure(figsize=(10,5)) +plt.subplot(1,2,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(1,2,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.show() + +# Regularisation Parameter +operator = Gradient(ig, correlation=Gradient.CORRELATION_SPACE) +f1 = 5 * MixedL21Norm() +g = 0.5 * L2NormSquared(b=noisy_data) + +# Compute operator Norm +normK = operator.norm() + +# Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) + +# Setup and run the PDHG algorithm +pdhg1 = PDHG(f=f1,g=g,operator=operator, tau=tau, sigma=sigma) +pdhg1.max_iteration = 2000 +pdhg1.update_objective_interval = 200 +pdhg1.run(1000) + + +# Show results +plt.figure(figsize=(10,10)) +plt.subplot(2,2,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(2,2,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.subplot(2,2,3) +plt.imshow(pdhg1.get_output().as_array()) +plt.title('TV Reconstruction') +plt.colorbar() +plt.subplot(2,2,4) +plt.imshow(pdhg2.get_output().as_array()) +plt.title('TV Reconstruction') +plt.colorbar() +plt.show() + diff --git a/Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_2D_time.py b/Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_2D_time.py new file mode 100644 index 0000000..14608db --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_2D_time.py @@ -0,0 +1,192 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Gradient, Identity +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \ + MixedL21Norm, BlockFunction + +from ccpi.astra.ops import AstraProjectorMC + +import os +import tomophantom +from tomophantom import TomoP2D + +# Create phantom for TV 2D dynamic tomography + +model = 102 # note that the selected model is temporal (2D + time) +N = 128 # set dimension of the phantom +# one can specify an exact path to the parameters file +# path_library2D = '../../../PhantomLibrary/models/Phantom2DLibrary.dat' +path = os.path.dirname(tomophantom.__file__) +path_library2D = os.path.join(path, "Phantom2DLibrary.dat") +#This will generate a N_size x N_size x Time frames phantom (2D + time) +phantom_2Dt = TomoP2D.ModelTemporal(model, N, path_library2D) + +plt.close('all') +plt.figure(1) +plt.rcParams.update({'font.size': 21}) +plt.title('{}''{}'.format('2D+t phantom using model no.',model)) +for sl in range(0,np.shape(phantom_2Dt)[0]): + im = phantom_2Dt[sl,:,:] + plt.imshow(im, vmin=0, vmax=1) +# plt.pause(.1) +# plt.draw + + +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N, channels = np.shape(phantom_2Dt)[0]) +data = ImageData(phantom_2Dt, geometry=ig) +ag = ig + +# Create Noisy data. Add Gaussian noise +np.random.seed(10) +noisy_data = ImageData( data.as_array() + np.random.normal(0, 0.25, size=ig.shape) ) + +tindex = [3, 6, 10] + +fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 10)) +plt.subplot(1,3,1) +plt.imshow(noisy_data.as_array()[tindex[0],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[0])) +plt.subplot(1,3,2) +plt.imshow(noisy_data.as_array()[tindex[1],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[1])) +plt.subplot(1,3,3) +plt.imshow(noisy_data.as_array()[tindex[2],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[2])) + +fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8, + wspace=0.02, hspace=0.02) + +plt.show() + +#%% +# Regularisation Parameter +alpha = 0.3 + +# Create operators +#op1 = Gradient(ig) +op1 = Gradient(ig, correlation='Space') +op2 = Gradient(ig, correlation='SpaceChannels') + +op3 = Identity(ig, ag) + +# Create BlockOperator +operator1 = BlockOperator(op1, op3, shape=(2,1) ) +operator2 = BlockOperator(op2, op3, shape=(2,1) ) + +# Create functions + +f1 = alpha * MixedL21Norm() +f2 = 0.5 * L2NormSquared(b = noisy_data) +f = BlockFunction(f1, f2) + +g = ZeroFunction() + +# Compute operator Norm +normK1 = operator1.norm() +normK2 = operator2.norm() + +#%% +# Primal & dual stepsizes +sigma1 = 1 +tau1 = 1/(sigma1*normK1**2) + +sigma2 = 1 +tau2 = 1/(sigma2*normK2**2) + +# Setup and run the PDHG algorithm +pdhg1 = PDHG(f=f,g=g,operator=operator1, tau=tau1, sigma=sigma1) +pdhg1.max_iteration = 2000 +pdhg1.update_objective_interval = 200 +pdhg1.run(2000) + +# Setup and run the PDHG algorithm +pdhg2 = PDHG(f=f,g=g,operator=operator2, tau=tau2, sigma=sigma2) +pdhg2.max_iteration = 2000 +pdhg2.update_objective_interval = 200 +pdhg2.run(2000) + + +#%% + +tindex = [3, 6, 10] +fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8)) + +plt.subplot(3,3,1) +plt.imshow(phantom_2Dt[tindex[0],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[0])) + +plt.subplot(3,3,2) +plt.imshow(phantom_2Dt[tindex[1],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[1])) + +plt.subplot(3,3,3) +plt.imshow(phantom_2Dt[tindex[2],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[2])) + +plt.subplot(3,3,4) +plt.imshow(pdhg1.get_output().as_array()[tindex[0],:,:]) +plt.axis('off') +plt.subplot(3,3,5) +plt.imshow(pdhg1.get_output().as_array()[tindex[1],:,:]) +plt.axis('off') +plt.subplot(3,3,6) +plt.imshow(pdhg1.get_output().as_array()[tindex[2],:,:]) +plt.axis('off') + + +plt.subplot(3,3,7) +plt.imshow(pdhg2.get_output().as_array()[tindex[0],:,:]) +plt.axis('off') +plt.subplot(3,3,8) +plt.imshow(pdhg2.get_output().as_array()[tindex[1],:,:]) +plt.axis('off') +plt.subplot(3,3,9) +plt.imshow(pdhg2.get_output().as_array()[tindex[2],:,:]) +plt.axis('off') + +#%% +im = plt.imshow(pdhg1.get_output().as_array()[tindex[0],:,:]) + + +fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8, + wspace=0.02, hspace=0.02) + +cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8]) +cbar = fig.colorbar(im, cax=cb_ax) + + +plt.show() + diff --git a/Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_Gaussian_3D.py b/Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_Gaussian_3D.py new file mode 100644 index 0000000..03dc2ef --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_Gaussian_3D.py @@ -0,0 +1,181 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= +""" + +Total Variation (3D) Denoising using PDHG algorithm: + + +Problem: min_{x} \alpha * ||\nabla x||_{2,1} + \frac{1}{2} * || x - g ||_{2}^{2} + + \alpha: Regularization parameter + + \nabla: Gradient operator + + g: Noisy Data with Gaussian Noise + + Method = 0 ( PDHG - split ) : K = [ \nabla, + Identity] + + + Method = 1 (PDHG - explicit ): K = \nabla + +""" + +from ccpi.framework import ImageData, ImageGeometry + +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Identity, Gradient +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \ + MixedL21Norm, BlockFunction + +from skimage.util import random_noise + +# Create phantom for TV Gaussian denoising +import timeit +import os +from tomophantom import TomoP3D +import tomophantom + +print ("Building 3D phantom using TomoPhantom software") +tic=timeit.default_timer() +model = 13 # select a model number from the library +N = 64 # Define phantom dimensions using a scalar value (cubic phantom) +path = os.path.dirname(tomophantom.__file__) +path_library3D = os.path.join(path, "Phantom3DLibrary.dat") + +#This will generate a N x N x N phantom (3D) +phantom_tm = TomoP3D.Model(model, N, path_library3D) + +#%% + +# Create noisy data. Add Gaussian noise +ig = ImageGeometry(voxel_num_x=N, voxel_num_y=N, voxel_num_z=N) +ag = ig +n1 = random_noise(phantom_tm, mode = 'gaussian', mean=0, var = 0.001, seed=10) +noisy_data = ImageData(n1) + +sliceSel = int(0.5*N) +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(noisy_data.as_array()[sliceSel,:,:],vmin=0, vmax=1) +plt.title('Axial View') +plt.colorbar() +plt.subplot(3,1,2) +plt.imshow(noisy_data.as_array()[:,sliceSel,:],vmin=0, vmax=1) +plt.title('Coronal View') +plt.colorbar() +plt.subplot(3,1,3) +plt.imshow(noisy_data.as_array()[:,:,sliceSel],vmin=0, vmax=1) +plt.title('Sagittal View') +plt.colorbar() +plt.show() + +#%% + +# Regularisation Parameter +alpha = 0.05 + +method = '0' + +if method == '0': + + # Create operators + op1 = Gradient(ig) + op2 = Identity(ig, ag) + + # Create BlockOperator + operator = BlockOperator(op1, op2, shape=(2,1) ) + + # Create functions + + f1 = alpha * MixedL21Norm() + f2 = 0.5 * L2NormSquared(b = noisy_data) + f = BlockFunction(f1, f2) + + g = ZeroFunction() + +else: + + # Without the "Block Framework" + operator = Gradient(ig) + f = alpha * MixedL21Norm() + g = 0.5 * L2NormSquared(b = noisy_data) + + +# Compute operator Norm +normK = operator.norm() + +# Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 200 +pdhg.run(2000, verbose = True) + + +#%% +fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8)) +fig.suptitle('TV Reconstruction',fontsize=20) + + +plt.subplot(2,3,1) +plt.imshow(noisy_data.as_array()[sliceSel,:,:],vmin=0, vmax=1) +plt.axis('off') +plt.title('Axial View') + +plt.subplot(2,3,2) +plt.imshow(noisy_data.as_array()[:,sliceSel,:],vmin=0, vmax=1) +plt.axis('off') +plt.title('Coronal View') + +plt.subplot(2,3,3) +plt.imshow(noisy_data.as_array()[:,:,sliceSel],vmin=0, vmax=1) +plt.axis('off') +plt.title('Sagittal View') + + +plt.subplot(2,3,4) +plt.imshow(pdhg.get_output().as_array()[sliceSel,:,:],vmin=0, vmax=1) +plt.axis('off') +plt.subplot(2,3,5) +plt.imshow(pdhg.get_output().as_array()[:,sliceSel,:],vmin=0, vmax=1) +plt.axis('off') +plt.subplot(2,3,6) +plt.imshow(pdhg.get_output().as_array()[:,:,sliceSel],vmin=0, vmax=1) +plt.axis('off') +im = plt.imshow(pdhg.get_output().as_array()[:,:,sliceSel],vmin=0, vmax=1) + + +fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8, + wspace=0.02, hspace=0.02) + +cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8]) +cbar = fig.colorbar(im, cax=cb_ax) + + +plt.show() + diff --git a/Wrappers/Python/demos/PDHG_examples/Tomo/PDHG_Tikhonov_Tomo2D.py b/Wrappers/Python/demos/PDHG_examples/Tomo/PDHG_Tikhonov_Tomo2D.py new file mode 100644 index 0000000..02cd053 --- /dev/null +++ b/Wrappers/Python/demos/PDHG_examples/Tomo/PDHG_Tikhonov_Tomo2D.py @@ -0,0 +1,156 @@ +#======================================================================== +# Copyright 2019 Science Technology Facilities Council +# Copyright 2019 University of Manchester +# +# This work is part of the Core Imaging Library developed by Science Technology +# Facilities Council and 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.txt +# +# 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. +# +#========================================================================= + +""" + +Total Variation Denoising using PDHG algorithm: + +Problem: min_x, x>0 \alpha * ||\nabla x||_{2}^{2} + int A x -g log(Ax + \eta) + + \nabla: Gradient operator + + A: Projection Matrix + g: Noisy sinogram corrupted with Poisson Noise + + \eta: Background Noise + \alpha: Regularization parameter + + + +""" + + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Gradient +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, BlockFunction + +from ccpi.astra.ops import AstraProjectorSimple +from ccpi.framework import TestData +import os, sys + +loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi')) + +# Load Data +N = 100 +M = 100 +data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(N,M), scale=(0,1)) + +ig = data.geometry +ag = ig + +#Create Acquisition Data and apply poisson noise + +detectors = N +angles = np.linspace(0, np.pi, N) + +ag = AcquisitionGeometry('parallel','2D',angles, detectors) + +device = input('Available device: GPU==1 / CPU==0 ') + +if device=='1': + dev = 'gpu' +else: + dev = 'cpu' + +Aop = AstraProjectorSimple(ig, ag, 'cpu') +sin = Aop.direct(data) + +# Create noisy data. Apply Poisson noise +scale = 0.5 +eta = 0 +n1 = scale * np.random.poisson(eta + sin.as_array()/scale) + +noisy_data = AcquisitionData(n1, ag) + +# Show Ground Truth and Noisy Data +plt.figure(figsize=(10,10)) +plt.subplot(2,1,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(2,1,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.show() + + +# Regularisation Parameter +alpha = 1000 + +# Create operators +op1 = Gradient(ig) +op2 = Aop + +# Create BlockOperator +operator = BlockOperator(op1, op2, shape=(2,1) ) + +# Create functions + +f1 = alpha * L2NormSquared() +f2 = 0.5 * L2NormSquared(b=noisy_data) +f = BlockFunction(f1, f2) + +g = ZeroFunction() + +# Compute operator Norm +normK = operator.norm() + +# Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) + + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 500 +pdhg.run(2000) + +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(3,1,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.subplot(3,1,3) +plt.imshow(pdhg.get_output().as_array()) +plt.title('Tikhonov Reconstruction') +plt.colorbar() +plt.show() +## +plt.plot(np.linspace(0,N,M), data.as_array()[int(N/2),:], label = 'GTruth') +plt.plot(np.linspace(0,N,M), pdhg.get_output().as_array()[int(N/2),:], label = 'Tikhonov reconstruction') +plt.legend() +plt.title('Middle Line Profiles') +plt.show() + + diff --git a/Wrappers/Python/demos/pdhg_TV_tomography2Dccpi.py b/Wrappers/Python/demos/pdhg_TV_tomography2Dccpi.py new file mode 100644 index 0000000..854f645 --- /dev/null +++ b/Wrappers/Python/demos/pdhg_TV_tomography2Dccpi.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- + +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Feb 22 14:53:03 2019 + +@author: evangelos +""" + +from ccpi.framework import ImageData, ImageGeometry, BlockDataContainer, \ + AcquisitionGeometry, AcquisitionData + +import numpy as np +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG, PDHG_old + +from ccpi.optimisation.operators import BlockOperator, Identity, Gradient +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \ + MixedL21Norm, BlockFunction, ScaledFunction + +from ccpi.plugins.operators import CCPiProjectorSimple +from timeit import default_timer as timer +from ccpi.reconstruction.parallelbeam import alg as pbalg +import os + +try: + import tomophantom + from tomophantom import TomoP3D + no_tomophantom = False +except ImportError as ie: + no_tomophantom = True + +#%% + +#%%############################################################################### +# Create phantom for TV tomography + +#import os +#import tomophantom +#from tomophantom import TomoP2D +#from tomophantom.supp.qualitymetrics import QualityTools + +#model = 1 # select a model number from the library +#N = 150 # set dimension of the phantom +## one can specify an exact path to the parameters file +## path_library2D = '../../../PhantomLibrary/models/Phantom2DLibrary.dat' +#path = os.path.dirname(tomophantom.__file__) +#path_library2D = os.path.join(path, "Phantom2DLibrary.dat") +##This will generate a N_size x N_size phantom (2D) +#phantom_2D = TomoP2D.Model(model, N, path_library2D) +#ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) +#data = ImageData(phantom_2D, geometry=ig) + +N = 75 +#x = np.zeros((N,N)) + +vert = 4 +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N, voxel_num_z=vert) + +angles_num = 100 +det_w = 1.0 +det_num = N + +angles = np.linspace(-90.,90.,N, dtype=np.float32) +# Inputs: Geometry, 2D or 3D, angles, horz detector pixel count, +# horz detector pixel size, vert detector pixel count, +# vert detector pixel size. +ag = AcquisitionGeometry('parallel', + '3D', + angles, + N, + det_w, + vert, + det_w) + +#no_tomophantom = True +if no_tomophantom: + data = ig.allocate() + Phantom = data + # Populate image data by looping over and filling slices + i = 0 + while i < vert: + if vert > 1: + x = Phantom.subset(vertical=i).array + else: + x = Phantom.array + x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 + x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 0.98 + if vert > 1 : + Phantom.fill(x, vertical=i) + i += 1 + + Aop = CCPiProjectorSimple(ig, ag, 'cpu') + sin = Aop.direct(data) +else: + + model = 13 # select a model number from the library + N_size = N # Define phantom dimensions using a scalar value (cubic phantom) + path = os.path.dirname(tomophantom.__file__) + path_library3D = os.path.join(path, "Phantom3DLibrary.dat") + #This will generate a N_size x N_size x N_size phantom (3D) + phantom_tm = TomoP3D.Model(model, N_size, path_library3D) + + #%% + Horiz_det = int(np.sqrt(2)*N_size) # detector column count (horizontal) + Vert_det = N_size # detector row count (vertical) (no reason for it to be > N) + #angles_num = int(0.5*np.pi*N_size); # angles number + #angles = np.linspace(0.0,179.9,angles_num,dtype='float32') # in degrees + + print ("Building 3D analytical projection data with TomoPhantom") + projData3D_analyt = TomoP3D.ModelSino(model, + N_size, + Horiz_det, + Vert_det, + angles, + path_library3D) + + # tomophantom outputs in [vert,angles,horiz] + # we want [angle,vert,horiz] + data = np.transpose(projData3D_analyt, [1,0,2]) + ag.pixel_num_h = Horiz_det + ag.pixel_num_v = Vert_det + sin = ag.allocate() + sin.fill(data) + ig.voxel_num_y = Vert_det + + Aop = CCPiProjectorSimple(ig, ag, 'cpu') + + +plt.imshow(sin.subset(vertical=0).as_array()) +plt.title('Sinogram') +plt.colorbar() +plt.show() + + +#%% +# Add Gaussian noise to the sinogram data +np.random.seed(10) +n1 = np.random.random(sin.shape) + +noisy_data = sin + ImageData(5*n1) + +plt.imshow(noisy_data.subset(vertical=0).as_array()) +plt.title('Noisy Sinogram') +plt.colorbar() +plt.show() + + +#%% Works only with Composite Operator Structure of PDHG + +#ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) + +# Create operators +op1 = Gradient(ig) +op2 = Aop + +# Form Composite Operator +operator = BlockOperator(op1, op2, shape=(2,1) ) + +alpha = 50 +f = BlockFunction( alpha * MixedL21Norm(), \ + 0.5 * L2NormSquared(b = noisy_data) ) +g = ZeroFunction() + +normK = Aop.norm() + +# Compute operator Norm +normK = operator.norm() + +## Primal & dual stepsizes +diag_precon = False + +if diag_precon: + + def tau_sigma_precond(operator): + + tau = 1/operator.sum_abs_row() + sigma = 1/ operator.sum_abs_col() + + return tau, sigma + + tau, sigma = tau_sigma_precond(operator) + +else: + sigma = 1 + tau = 1/(sigma*normK**2) + +# Compute operator Norm +normK = operator.norm() + +# Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) +niter = 50 +opt = {'niter':niter} +opt1 = {'niter':niter, 'memopt': True} + + + +pdhg1 = PDHG(f=f,g=g, operator=operator, tau=tau, sigma=sigma, max_iteration=niter) +#pdhg1.max_iteration = 2000 +pdhg1.update_objective_interval = 100 + +t1_old = timer() +resold, time, primal, dual, pdgap = PDHG_old(f, g, operator, tau = tau, sigma = sigma, opt = opt) +t2_old = timer() + +pdhg1.run(niter) +print (sum(pdhg1.timing)) +res = pdhg1.get_output().subset(vertical=0) + +#%% +plt.figure() +plt.subplot(1,4,1) +plt.imshow(res.as_array()) +plt.title('Algorithm') +plt.colorbar() +plt.subplot(1,4,2) +plt.imshow(resold.subset(vertical=0).as_array()) +plt.title('function') +plt.colorbar() +plt.subplot(1,4,3) +plt.imshow((res - resold.subset(vertical=0)).abs().as_array()) +plt.title('diff') +plt.colorbar() +plt.subplot(1,4,4) +plt.plot(np.linspace(0,N,N), res.as_array()[int(N/2),:], label = 'Algorithm') +plt.plot(np.linspace(0,N,N), resold.subset(vertical=0).as_array()[int(N/2),:], label = 'function') +plt.legend() +plt.show() +# +print ("Time: No memopt in {}s, \n Time: Memopt in {}s ".format(sum(pdhg1.timing), t2_old -t1_old)) +diff = (res - resold.subset(vertical=0)).abs().as_array().max() +# +print(" Max of abs difference is {}".format(diff)) + diff --git a/Wrappers/Python/environment.yml b/Wrappers/Python/environment.yml new file mode 100644 index 0000000..5cdd6fe --- /dev/null +++ b/Wrappers/Python/environment.yml @@ -0,0 +1,11 @@ +name: test_new +dependencies: + - python=3.6.7=h8dc6b48_1004 + - numpy=1.11.3=py36hdf140aa_1207 + - spyder=3.3.4=py36_0 + - scikit-image=0.15.0=py36h6de7cb9_0 + - scipy=1.2.1=py36hbd7caa9_1 + - astra-toolbox=1.8.3=py36h804c3c0_0 + + + diff --git a/Wrappers/Python/setup.py b/Wrappers/Python/setup.py index eaf124b..8bd33a6 100644 --- a/Wrappers/Python/setup.py +++ b/Wrappers/Python/setup.py @@ -31,8 +31,17 @@ if cil_version == '': setup( name="ccpi-framework", version=cil_version, - packages=['ccpi' , 'ccpi.io', 'ccpi.optimisation', - 'ccpi.optimisation.algorithms'], + packages=['ccpi' , 'ccpi.io', + 'ccpi.framework', 'ccpi.optimisation', + 'ccpi.optimisation.operators', + 'ccpi.optimisation.algorithms', + 'ccpi.optimisation.functions', + 'ccpi.processors', + 'ccpi.contrib','ccpi.contrib.optimisation', + 'ccpi.contrib.optimisation.algorithms'], + data_files = [('share/ccpi', ['data/boat.tiff', 'data/peppers.tiff', + 'data/camera.png', + 'data/resolution_chart.tiff'])], # Project uses reStructuredText, so ensure that the docutils get # installed or upgraded on the target machine @@ -47,8 +56,9 @@ setup( # zip_safe = False, # metadata for upload to PyPI - author="Edoardo Pasca", - author_email="edoardo.pasca@stfc.ac.uk", + author="CCPi developers", + maintainer="Edoardo Pasca", + maintainer_email="edoardo.pasca@stfc.ac.uk", description='CCPi Core Imaging Library - Python Framework Module', license="Apache v2.0", keywords="Python Framework", diff --git a/Wrappers/Python/test/test_BlockDataContainer.py b/Wrappers/Python/test/test_BlockDataContainer.py new file mode 100755 index 0000000..aeb8454 --- /dev/null +++ b/Wrappers/Python/test/test_BlockDataContainer.py @@ -0,0 +1,468 @@ +# -*- coding: utf-8 -*-
+"""
+Created on Tue Mar 5 16:08:23 2019
+
+@author: ofn77899
+"""
+
+import unittest
+import numpy
+from ccpi.framework import ImageGeometry, AcquisitionGeometry
+from ccpi.framework import ImageData, AcquisitionData
+from ccpi.framework import BlockDataContainer, DataContainer
+import functools
+
+from ccpi.optimisation.operators import Gradient, Identity, BlockOperator
+
+class TestBlockDataContainer(unittest.TestCase):
+ def skiptest_BlockDataContainerShape(self):
+ print ("test block data container")
+ ig0 = ImageGeometry(12,42,55,32)
+ ig1 = ImageGeometry(12,42,55,32)
+
+ data0 = ImageData(geometry=ig0)
+ data1 = ImageData(geometry=ig1) + 1
+
+ data2 = ImageData(geometry=ig0) + 2
+ data3 = ImageData(geometry=ig1) + 3
+
+ cp0 = BlockDataContainer(data0,data1)
+ cp1 = BlockDataContainer(data2,data3)
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp0.T.shape == transpose_shape)
+ def skiptest_BlockDataContainerShapeArithmetic(self):
+ print ("test block data container")
+ ig0 = ImageGeometry(2,3,4)
+ ig1 = ImageGeometry(2,3,4)
+
+ data0 = ImageData(geometry=ig0)
+ data1 = ImageData(geometry=ig1) + 1
+
+ data2 = ImageData(geometry=ig0) + 2
+ data3 = ImageData(geometry=ig1) + 3
+
+ cp0 = BlockDataContainer(data0,data1)
+ #cp1 = BlockDataContainer(data2,data3)
+ cp1 = cp0 + 1
+ self.assertTrue(cp1.shape == cp0.shape)
+ cp1 = cp0.T + 1
+
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T - 1
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = (cp0.T + 1)*2
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = (cp0.T + 1)/2
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T.power(2.2)
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T.maximum(3)
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T.abs()
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T.sign()
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T.sqrt()
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T.conjugate()
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ def test_BlockDataContainer(self):
+ print ("test block data container")
+ ig0 = ImageGeometry(2,3,4)
+ ig1 = ImageGeometry(2,3,5)
+
+ data0 = ImageData(geometry=ig0)
+ data1 = ImageData(geometry=ig1) + 1
+
+ data2 = ImageData(geometry=ig0) + 2
+ data3 = ImageData(geometry=ig1) + 3
+
+ cp0 = BlockDataContainer(data0,data1)
+ cp1 = BlockDataContainer(data2,data3)
+
+ cp2 = BlockDataContainer(data0+1, data2+1)
+ d = cp2 + data0
+ self.assertEqual(d.get_item(0).as_array()[0][0][0], 1)
+ try:
+ d = cp2 + data1
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+ d = cp2 - data0
+ self.assertEqual(d.get_item(0).as_array()[0][0][0], 1)
+ try:
+ d = cp2 - data1
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+ d = cp2 * data2
+ self.assertEqual(d.get_item(0).as_array()[0][0][0], 2)
+ try:
+ d = cp2 * data1
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+
+ a = [ (el, ot) for el,ot in zip(cp0.containers,cp1.containers)]
+ print (a[0][0].shape)
+ #cp2 = BlockDataContainer(*a)
+ cp2 = cp0.add(cp1)
+ self.assertEqual (cp2.get_item(0).as_array()[0][0][0] , 2.)
+ self.assertEqual (cp2.get_item(1).as_array()[0][0][0] , 4.)
+
+ cp2 = cp0 + cp1
+ self.assertTrue (cp2.get_item(0).as_array()[0][0][0] == 2.)
+ self.assertTrue (cp2.get_item(1).as_array()[0][0][0] == 4.)
+ cp2 = cp0 + 1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 1. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2., decimal = 5)
+ cp2 = cp0 + [1 ,2]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 1. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 3., decimal = 5)
+ cp2 += cp1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , +3. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , +6., decimal = 5)
+
+ cp2 += 1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , +4. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , +7., decimal = 5)
+
+ cp2 += [-2,-1]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 2. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 6., decimal = 5)
+
+
+ cp2 = cp0.subtract(cp1)
+ assert (cp2.get_item(0).as_array()[0][0][0] == -2.)
+ assert (cp2.get_item(1).as_array()[0][0][0] == -2.)
+ cp2 = cp0 - cp1
+ assert (cp2.get_item(0).as_array()[0][0][0] == -2.)
+ assert (cp2.get_item(1).as_array()[0][0][0] == -2.)
+
+ cp2 = cp0 - 1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , -1. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 0, decimal = 5)
+ cp2 = cp0 - [1 ,2]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , -1. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , -1., decimal = 5)
+
+ cp2 -= cp1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , -3. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , -4., decimal = 5)
+
+ cp2 -= 1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , -4. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , -5., decimal = 5)
+
+ cp2 -= [-2,-1]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , -2. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , -4., decimal = 5)
+
+
+ cp2 = cp0.multiply(cp1)
+ assert (cp2.get_item(0).as_array()[0][0][0] == 0.)
+ assert (cp2.get_item(1).as_array()[0][0][0] == 3.)
+ cp2 = cp0 * cp1
+ assert (cp2.get_item(0).as_array()[0][0][0] == 0.)
+ assert (cp2.get_item(1).as_array()[0][0][0] == 3.)
+
+ cp2 = cp0 * 2
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2, decimal = 5)
+ cp2 = 2 * cp0
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2, decimal = 5)
+ cp2 = cp0 * [3 ,2]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2., decimal = 5)
+ cp2 = cp0 * numpy.asarray([3 ,2])
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2., decimal = 5)
+
+ cp2 = [3,2] * cp0
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2., decimal = 5)
+ cp2 = numpy.asarray([3,2]) * cp0
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2., decimal = 5)
+
+ try:
+ cp2 = [3,2,3] * cp0
+ #numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ #numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2., decimal = 5)
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+ cp2 *= cp1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0 , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , +6., decimal = 5)
+
+ cp2 *= 1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , +6., decimal = 5)
+
+ cp2 *= [-2,-1]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , -6., decimal = 5)
+
+ try:
+ cp2 *= [2,3,5]
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+
+ cp2 = cp0.divide(cp1)
+ assert (cp2.get_item(0).as_array()[0][0][0] == 0.)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0], 1./3., decimal=4)
+ cp2 = cp0/cp1
+ assert (cp2.get_item(0).as_array()[0][0][0] == 0.)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0], 1./3., decimal=4)
+
+ cp2 = cp0 / 2
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 0.5, decimal = 5)
+ cp2 = cp0 / [3 ,2]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 0.5, decimal = 5)
+ cp2 = cp0 / numpy.asarray([3 ,2])
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 0.5, decimal = 5)
+ cp3 = numpy.asarray([3 ,2]) / (cp0+1)
+ numpy.testing.assert_almost_equal(cp3.get_item(0).as_array()[0][0][0] , 3. , decimal=5)
+ numpy.testing.assert_almost_equal(cp3.get_item(1).as_array()[0][0][0] , 1, decimal = 5)
+
+ cp2 += 1
+ cp2 /= cp1
+ # TODO fix inplace division
+
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 1./2 , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 1.5/3., decimal = 5)
+
+ cp2 /= 1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0.5 , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 0.5, decimal = 5)
+
+ cp2 /= [-2,-1]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , -0.5/2. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , -0.5, decimal = 5)
+ ####
+
+ cp2 = cp0.power(cp1)
+ assert (cp2.get_item(0).as_array()[0][0][0] == 0.)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0], 1., decimal=4)
+ cp2 = cp0**cp1
+ assert (cp2.get_item(0).as_array()[0][0][0] == 0.)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0], 1., decimal=4)
+
+ cp2 = cp0 ** 2
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0., decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 1., decimal = 5)
+
+ cp2 = cp0.maximum(cp1)
+ assert (cp2.get_item(0).as_array()[0][0][0] == cp1.get_item(0).as_array()[0][0][0])
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0], cp2.get_item(1).as_array()[0][0][0], decimal=4)
+
+
+ cp2 = cp0.abs()
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0], 0., decimal=4)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0], 1., decimal=4)
+
+ cp2 = cp0.subtract(cp1)
+ s = cp2.sign()
+ numpy.testing.assert_almost_equal(s.get_item(0).as_array()[0][0][0], -1., decimal=4)
+ numpy.testing.assert_almost_equal(s.get_item(1).as_array()[0][0][0], -1., decimal=4)
+
+ cp2 = cp0.add(cp1)
+ s = cp2.sqrt()
+ numpy.testing.assert_almost_equal(s.get_item(0).as_array()[0][0][0], numpy.sqrt(2), decimal=4)
+ numpy.testing.assert_almost_equal(s.get_item(1).as_array()[0][0][0], numpy.sqrt(4), decimal=4)
+
+ s = cp0.sum()
+ size = functools.reduce(lambda x,y: x*y, data1.shape, 1)
+ print ("size" , size)
+ numpy.testing.assert_almost_equal(s, 0 + size, decimal=4)
+ s0 = 1
+ s1 = 1
+ for i in cp0.get_item(0).shape:
+ s0 *= i
+ for i in cp0.get_item(1).shape:
+ s1 *= i
+
+ #numpy.testing.assert_almost_equal(s[1], cp0.get_item(0,0).as_array()[0][0][0]*s0 +cp0.get_item(1,0).as_array()[0][0][0]*s1, decimal=4)
+ def test_Nested_BlockDataContainer(self):
+ print ("test_Nested_BlockDataContainer")
+ ig0 = ImageGeometry(2,3,4)
+ ig1 = ImageGeometry(2,3,4)
+
+ data0 = ImageData(geometry=ig0)
+ data1 = ImageData(geometry=ig1) + 1
+
+ data2 = ImageData(geometry=ig0) + 2
+ data3 = ImageData(geometry=ig1) + 3
+
+ cp0 = BlockDataContainer(data0,data1)
+ cp1 = BlockDataContainer(data2,data3)
+
+ nbdc = BlockDataContainer(cp0, cp1)
+ nbdc2 = nbdc + 2
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(0).as_array()[0][0][0] , 2. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(1).as_array()[0][0][0] , 3. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(0).as_array()[0][0][0] , 4. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(1).as_array()[0][0][0] , 5. , decimal=5)
+
+ nbdc2 = 2 + nbdc
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(0).as_array()[0][0][0] , 2. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(1).as_array()[0][0][0] , 3. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(0).as_array()[0][0][0] , 4. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(1).as_array()[0][0][0] , 5. , decimal=5)
+
+
+ nbdc2 = nbdc * 2
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(1).as_array()[0][0][0] , 2. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(0).as_array()[0][0][0] , 4. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(1).as_array()[0][0][0] , 6. , decimal=5)
+
+ nbdc2 = 2 * nbdc
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(1).as_array()[0][0][0] , 2. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(0).as_array()[0][0][0] , 4. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(1).as_array()[0][0][0] , 6. , decimal=5)
+
+ nbdc2 = nbdc / 2
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(1).as_array()[0][0][0] , .5 , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(0).as_array()[0][0][0] , 1. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(1).as_array()[0][0][0] , 3./2 , decimal=5)
+
+ c5 = nbdc.get_item(0).power(2).sum()
+ c5a = nbdc.power(2).sum()
+ print ("sum", c5a, c5)
+
+ cp0 = BlockDataContainer(data0,data2)
+ a = cp0 * data2
+ b = data2 * cp0
+ self.assertBlockDataContainerEqual(a,b)
+
+
+ print ("test_Nested_BlockDataContainer OK")
+ def stest_NestedBlockDataContainer2(self):
+ M, N = 2, 3
+ ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N)
+ ag = ig
+ u = ig.allocate(1)
+ op1 = Gradient(ig)
+ op2 = Identity(ig, ag)
+
+ operator = BlockOperator(op1, op2, shape=(2,1))
+
+ d1 = op1.direct(u)
+ d2 = op2.direct(u)
+
+ d = operator.direct(u)
+
+ dd = operator.domain_geometry()
+ ww = operator.range_geometry()
+
+ print(d.get_item(0).get_item(0).as_array())
+ print(d.get_item(0).get_item(1).as_array())
+ print(d.get_item(1).as_array())
+
+ c1 = d + d
+
+ c2 = 2*d
+
+ c3 = d / (d+0.0001)
+
+
+ c5 = d.get_item(0).power(2).sum()
+
+ def test_BlockDataContainer_fill(self):
+ print ("test block data container")
+ ig0 = ImageGeometry(2,3,4)
+ ig1 = ImageGeometry(2,3,5)
+
+ data0 = ImageData(geometry=ig0)
+ data1 = ImageData(geometry=ig1) + 1
+
+ data2 = ImageData(geometry=ig0) + 2
+ data3 = ImageData(geometry=ig1) + 3
+
+ cp0 = BlockDataContainer(data0,data1)
+ #cp1 = BlockDataContainer(data2,data3)
+
+ cp2 = BlockDataContainer(data0+1, data1+1)
+
+ data0.fill(data2)
+ self.assertNumpyArrayEqual(data0.as_array(), data2.as_array())
+ data0 = ImageData(geometry=ig0)
+
+ for el,ot in zip(cp0, cp2):
+ print (el.shape, ot.shape)
+ cp0.fill(cp2)
+ self.assertBlockDataContainerEqual(cp0, cp2)
+
+ def test_NestedBlockDataContainer(self):
+ ig0 = ImageGeometry(2,3,4)
+ ig1 = ImageGeometry(2,3,5)
+
+ data0 = ig0.allocate(0)
+ data2 = ig0.allocate(1)
+
+ cp0 = BlockDataContainer(data0,data2)
+ #cp1 = BlockDataContainer(data2,data3)
+
+ nested = BlockDataContainer(cp0, data2, data2)
+ out = BlockDataContainer(BlockDataContainer(data0 , data0), data0, data0)
+ nested.divide(data2,out=out)
+ self.assertBlockDataContainerEqual(out, nested)
+
+
+ def assertBlockDataContainerEqual(self, container1, container2):
+ print ("assert Block Data Container Equal")
+ self.assertTrue(issubclass(container1.__class__, container2.__class__))
+ for col in range(container1.shape[0]):
+ if issubclass(container1.get_item(col).__class__, DataContainer):
+ print ("Checking col ", col)
+ self.assertNumpyArrayEqual(
+ container1.get_item(col).as_array(),
+ container2.get_item(col).as_array()
+ )
+ else:
+ self.assertBlockDataContainerEqual(container1.get_item(col),container2.get_item(col))
+
+ def assertNumpyArrayEqual(self, first, second):
+ res = True
+ try:
+ numpy.testing.assert_array_equal(first, second)
+ except AssertionError as err:
+ res = False
+ print(err)
+ self.assertTrue(res)
+
+
diff --git a/Wrappers/Python/test/test_BlockOperator.py b/Wrappers/Python/test/test_BlockOperator.py new file mode 100644 index 0000000..b82c849 --- /dev/null +++ b/Wrappers/Python/test/test_BlockOperator.py @@ -0,0 +1,359 @@ +import unittest +from ccpi.optimisation.operators import BlockOperator +from ccpi.framework import BlockDataContainer +from ccpi.optimisation.operators import Identity +from ccpi.framework import ImageGeometry, ImageData +import numpy +from ccpi.optimisation.operators import FiniteDiff + + +class TestBlockOperator(unittest.TestCase): + + def test_BlockOperator(self): + print ("test_BlockOperator") + ig = [ ImageGeometry(10,20,30) , \ + ImageGeometry(10,20,30) , \ + ImageGeometry(10,20,30) ] + x = [ g.allocate() for g in ig ] + ops = [ Identity(g) for g in ig ] + + K = BlockOperator(*ops) + X = BlockDataContainer(x[0]) + Y = K.direct(X) + self.assertTrue(Y.shape == K.shape) + + numpy.testing.assert_array_equal(Y.get_item(0).as_array(),X.get_item(0).as_array()) + numpy.testing.assert_array_equal(Y.get_item(1).as_array(),X.get_item(0).as_array()) + #numpy.testing.assert_array_equal(Y.get_item(2).as_array(),X.get_item(2).as_array()) + + X = BlockDataContainer(*x) + 1 + Y = K.T.direct(X) + # K.T (1,3) X (3,1) => output shape (1,1) + self.assertTrue(Y.shape == (1,1)) + zero = numpy.zeros(X.get_item(0).shape) + numpy.testing.assert_array_equal(Y.get_item(0).as_array(),len(x)+zero) + + K2 = BlockOperator(*(ops+ops), shape=(3,2)) + Y = K2.T.direct(X) + # K.T (2,3) X (3,1) => output shape (2,1) + self.assertTrue(Y.shape == (2,1)) + + try: + # this should fail as the domain is not compatible + ig = [ ImageGeometry(10,20,31) , \ + ImageGeometry(10,20,30) , \ + ImageGeometry(10,20,30) ] + x = [ g.allocate() for g in ig ] + ops = [ Identity(g) for g in ig ] + + K = BlockOperator(*ops) + self.assertTrue(False) + except ValueError as ve: + print (ve) + self.assertTrue(True) + + try: + # this should fail as the range is not compatible + ig = [ ImageGeometry(10,20,30) , \ + ImageGeometry(10,20,30) , \ + ImageGeometry(10,20,30) ] + rg0 = [ ImageGeometry(10,20,31) , \ + ImageGeometry(10,20,31) , \ + ImageGeometry(10,20,31) ] + rg1 = [ ImageGeometry(10,22,31) , \ + ImageGeometry(10,22,31) , \ + ImageGeometry(10,20,31) ] + x = [ g.allocate() for g in ig ] + ops = [ Identity(g, gm_range=r) for g,r in zip(ig, rg0) ] + ops += [ Identity(g, gm_range=r) for g,r in zip(ig, rg1) ] + + K = BlockOperator(*ops, shape=(2,3)) + print ("K col comp? " , K.column_wise_compatible()) + print ("K row comp? " , K.row_wise_compatible()) + for op in ops: + print ("range" , op.range_geometry().shape) + for op in ops: + print ("domain" , op.domain_geometry().shape) + self.assertTrue(False) + except ValueError as ve: + print (ve) + self.assertTrue(True) + + def test_ScaledBlockOperatorSingleScalar(self): + ig = [ ImageGeometry(10,20,30) , \ + ImageGeometry(10,20,30) , \ + ImageGeometry(10,20,30) ] + x = [ g.allocate() for g in ig ] + ops = [ Identity(g) for g in ig ] + + val = 1 + # test limit as non Scaled + scalar = 1 + k = BlockOperator(*ops) + K = scalar * k + X = BlockDataContainer(*x) + val + + Y = K.T.direct(X) + self.assertTrue(Y.shape == (1,1)) + zero = numpy.zeros(X.get_item(0).shape) + xx = numpy.asarray([val for _ in x]) + numpy.testing.assert_array_equal(Y.get_item(0).as_array(),((scalar*xx).sum()+zero)) + + scalar = 0.5 + k = BlockOperator(*ops) + K = scalar * k + X = BlockDataContainer(*x) + 1 + + Y = K.T.direct(X) + self.assertTrue(Y.shape == (1,1)) + zero = numpy.zeros(X.get_item(0).shape) + numpy.testing.assert_array_equal(Y.get_item(0).as_array(),scalar*(len(x)+zero)) + + + def test_ScaledBlockOperatorScalarList(self): + ig = [ ImageGeometry(2,3) , \ + #ImageGeometry(10,20,30) , \ + ImageGeometry(2,3 ) ] + x = [ g.allocate() for g in ig ] + ops = [ Identity(g) for g in ig ] + + + # test limit as non Scaled + scalar = numpy.asarray([1 for _ in x]) + k = BlockOperator(*ops) + K = scalar * k + val = 1 + X = BlockDataContainer(*x) + val + + Y = K.T.direct(X) + self.assertTrue(Y.shape == (1,1)) + zero = numpy.zeros(X.get_item(0).shape) + xx = numpy.asarray([val for _ in x]) + numpy.testing.assert_array_equal(Y.get_item(0).as_array(),(scalar*xx).sum()+zero) + + scalar = numpy.asarray([i+1 for i,el in enumerate(x)]) + #scalar = numpy.asarray([6,0]) + k = BlockOperator(*ops) + K = scalar * k + X = BlockDataContainer(*x) + val + Y = K.T.direct(X) + self.assertTrue(Y.shape == (1,1)) + zero = numpy.zeros(X.get_item(0).shape) + xx = numpy.asarray([val for _ in x]) + + + numpy.testing.assert_array_equal(Y.get_item(0).as_array(), + (scalar*xx).sum()+zero) + + + def test_TomoIdentity(self): + ig = ImageGeometry(10,20,30) + img = ig.allocate() + print (img.shape, ig.shape) + self.assertTrue(img.shape == (30,20,10)) + self.assertEqual(img.sum(), 0) + Id = Identity(ig) + y = Id.direct(img) + numpy.testing.assert_array_equal(y.as_array(), img.as_array()) + + def skiptest_CGLS_tikhonov(self): + from ccpi.optimisation.algorithms import CGLS + + from ccpi.plugins.ops import CCPiProjectorSimple + from ccpi.optimisation.ops import PowerMethodNonsquare + from ccpi.optimisation.operators import Identity + from ccpi.optimisation.funcs import Norm2sq, Norm1 + from ccpi.framework import ImageGeometry, AcquisitionGeometry + from ccpi.optimisation.Algorithms import GradientDescent + #from ccpi.optimisation.Algorithms import CGLS + import matplotlib.pyplot as plt + + + # Set up phantom size N x N x vert by creating ImageGeometry, initialising the + # ImageData object with this geometry and empty array and finally put some + # data into its array, and display one slice as image. + + # Image parameters + N = 128 + vert = 4 + + # Set up image geometry + ig = ImageGeometry(voxel_num_x=N, + voxel_num_y=N, + voxel_num_z=vert) + + # Set up empty image data + Phantom = ImageData(geometry=ig, + dimension_labels=['horizontal_x', + 'horizontal_y', + 'vertical']) + Phantom += 0.05 + # Populate image data by looping over and filling slices + i = 0 + while i < vert: + if vert > 1: + x = Phantom.subset(vertical=i).array + else: + x = Phantom.array + x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 + x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 0.94 + if vert > 1 : + Phantom.fill(x, vertical=i) + i += 1 + + + perc = 0.02 + # Set up empty image data + noise = ImageData(numpy.random.normal(loc = 0.04 , + scale = perc , + size = Phantom.shape), geometry=ig, + dimension_labels=['horizontal_x', + 'horizontal_y', + 'vertical']) + Phantom += noise + + # Set up AcquisitionGeometry object to hold the parameters of the measurement + # setup geometry: # Number of angles, the actual angles from 0 to + # pi for parallel beam, set the width of a detector + # pixel relative to an object pixe and the number of detector pixels. + angles_num = 20 + det_w = 1.0 + det_num = N + + angles = numpy.linspace(0,numpy.pi,angles_num,endpoint=False,dtype=numpy.float32)*\ + 180/numpy.pi + + # Inputs: Geometry, 2D or 3D, angles, horz detector pixel count, + # horz detector pixel size, vert detector pixel count, + # vert detector pixel size. + ag = AcquisitionGeometry('parallel', + '3D', + angles, + N, + det_w, + vert, + det_w) + + # Set up Operator object combining the ImageGeometry and AcquisitionGeometry + # wrapping calls to CCPi projector. + A = CCPiProjectorSimple(ig, ag) + + # Forward and backprojection are available as methods direct and adjoint. Here + # generate test data b and some noise + + b = A.direct(Phantom) + + + #z = A.adjoint(b) + + + # Using the test data b, different reconstruction methods can now be set up as + # demonstrated in the rest of this file. In general all methods need an initial + # guess and some algorithm options to be set. Note that 100 iterations for + # some of the methods is a very low number and 1000 or 10000 iterations may be + # needed if one wants to obtain a converged solution. + x_init = ImageData(geometry=ig, + dimension_labels=['horizontal_x','horizontal_y','vertical']) + X_init = BlockDataContainer(x_init) + B = BlockDataContainer(b, + ImageData(geometry=ig, dimension_labels=['horizontal_x','horizontal_y','vertical'])) + + # setup a tomo identity + Ibig = 1e5 * Identity(ig) + Ismall = 1e-5 * Identity(ig) + + # composite operator + Kbig = BlockOperator(A, Ibig, shape=(2,1)) + Ksmall = BlockOperator(A, Ismall, shape=(2,1)) + + #out = K.direct(X_init) + + f = Norm2sq(Kbig,B) + f.L = 0.00003 + + fsmall = Norm2sq(Ksmall,B) + f.L = 0.00003 + + simplef = Norm2sq(A, b) + simplef.L = 0.00003 + + gd = GradientDescent( x_init=x_init, objective_function=simplef, + rate=simplef.L) + gd.max_iteration = 10 + + cg = CGLS() + cg.set_up(X_init, Kbig, B ) + cg.max_iteration = 1 + + cgsmall = CGLS() + cgsmall.set_up(X_init, Ksmall, B ) + cgsmall.max_iteration = 1 + + + cgs = CGLS() + cgs.set_up(x_init, A, b ) + cgs.max_iteration = 6 + # + #out.__isub__(B) + #out2 = K.adjoint(out) + + #(2.0*self.c)*self.A.adjoint( self.A.direct(x) - self.b ) + + #for _ in gd: + # print ("iteration {} {}".format(gd.iteration, gd.get_current_loss())) + + #cg.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val)) ) + + #cgs.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val))) + + #cgsmall.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val))) + #cgsmall.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val))) + # for _ in cg: + # print ("iteration {} {}".format(cg.iteration, cg.get_current_loss())) + # + # fig = plt.figure() + # plt.imshow(cg.get_output().get_item(0,0).subset(vertical=0).as_array()) + # plt.title('Composite CGLS') + # plt.show() + # + # for _ in cgs: + # print ("iteration {} {}".format(cgs.iteration, cgs.get_current_loss())) + # + fig = plt.figure() + plt.subplot(1,5,1) + plt.imshow(Phantom.subset(vertical=0).as_array()) + plt.title('Simulated Phantom') + plt.subplot(1,5,2) + plt.imshow(gd.get_output().subset(vertical=0).as_array()) + plt.title('Simple Gradient Descent') + plt.subplot(1,5,3) + plt.imshow(cgs.get_output().subset(vertical=0).as_array()) + plt.title('Simple CGLS') + plt.subplot(1,5,4) + plt.imshow(cg.get_output().get_item(0,0).subset(vertical=0).as_array()) + plt.title('Composite CGLS\nbig lambda') + plt.subplot(1,5,5) + plt.imshow(cgsmall.get_output().get_item(0,0).subset(vertical=0).as_array()) + plt.title('Composite CGLS\nsmall lambda') + plt.show() + + def test_FiniteDiffOperator(self): + N, M = 200, 300 + + + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N) + u = ig.allocate('random_int') + G = FiniteDiff(ig, direction=0, bnd_cond = 'Neumann') + print(type(u), u.as_array()) + print(G.direct(u).as_array()) + + # Gradient Operator norm, for one direction should be close to 2 + numpy.testing.assert_allclose(G.norm(), numpy.sqrt(4), atol=0.1) + + M1, N1, K1 = 200, 300, 2 + ig1 = ImageGeometry(voxel_num_x = M1, voxel_num_y = N1, channels = K1) + u1 = ig1.allocate('random_int') + G1 = FiniteDiff(ig1, direction=2, bnd_cond = 'Periodic') + print(ig1.shape==u1.shape) + print (G1.norm()) + numpy.testing.assert_allclose(G1.norm(), numpy.sqrt(4), atol=0.1) diff --git a/Wrappers/Python/test/test_DataContainer.py b/Wrappers/Python/test/test_DataContainer.py index f23179c..16f7b86 100755 --- a/Wrappers/Python/test/test_DataContainer.py +++ b/Wrappers/Python/test/test_DataContainer.py @@ -174,7 +174,7 @@ class TestDataContainer(unittest.TestCase): def binary_add(self): print("Test binary add") X, Y, Z = 512, 512, 512 - X, Y, Z = 256, 512, 512 + #X, Y, Z = 1024, 512, 512 steps = [timer()] a = numpy.ones((X, Y, Z), dtype='float32') steps.append(timer()) @@ -183,9 +183,10 @@ class TestDataContainer(unittest.TestCase): #print("a refcount " , sys.getrefcount(a)) ds = DataContainer(a, False, ['X', 'Y', 'Z']) ds1 = ds.copy() + out = ds.copy() steps.append(timer()) - ds.add(ds1, out=ds) + ds.add(ds1, out=out) steps.append(timer()) t1 = dt(steps) print("ds.add(ds1, out=ds)", dt(steps)) @@ -196,20 +197,29 @@ class TestDataContainer(unittest.TestCase): print("ds2 = ds.add(ds1)", dt(steps)) self.assertLess(t1, t2) - self.assertEqual(ds.as_array()[0][0][0], 2.) - + self.assertEqual(out.as_array()[0][0][0], 2.) + self.assertNumpyArrayEqual(out.as_array(), ds2.as_array()) + ds0 = ds - ds0.add(2, out=ds0) - steps.append(timer()) - print("ds0.add(2,out=ds0)", dt(steps), 3, ds0.as_array()[0][0][0]) - self.assertEqual(4., ds0.as_array()[0][0][0]) - - dt1 = dt(steps) - ds3 = ds0.add(2) - steps.append(timer()) - print("ds3 = ds0.add(2)", dt(steps), 5, ds3.as_array()[0][0][0]) - dt2 = dt(steps) + dt1 = 0 + dt2 = 0 + for i in range(10): + steps.append(timer()) + ds0.add(2, out=out) + steps.append(timer()) + print("ds0.add(2,out=out)", dt(steps), 3, ds0.as_array()[0][0][0]) + self.assertEqual(3., out.as_array()[0][0][0]) + + dt1 += dt(steps)/10 + steps.append(timer()) + ds3 = ds0.add(2) + steps.append(timer()) + print("ds3 = ds0.add(2)", dt(steps), 5, ds3.as_array()[0][0][0]) + dt2 += dt(steps)/10 + + self.assertNumpyArrayEqual(out.as_array(), ds3.as_array()) self.assertLess(dt1, dt2) + def binary_subtract(self): print("Test binary subtract") @@ -222,16 +232,17 @@ class TestDataContainer(unittest.TestCase): #print("a refcount " , sys.getrefcount(a)) ds = DataContainer(a, False, ['X', 'Y', 'Z']) ds1 = ds.copy() + out = ds.copy() steps.append(timer()) - ds.subtract(ds1, out=ds) + ds.subtract(ds1, out=out) steps.append(timer()) t1 = dt(steps) print("ds.subtract(ds1, out=ds)", dt(steps)) - self.assertEqual(0., ds.as_array()[0][0][0]) + self.assertEqual(0., out.as_array()[0][0][0]) steps.append(timer()) - ds2 = ds.subtract(ds1) + ds2 = out.subtract(ds1) self.assertEqual(-1., ds2.as_array()[0][0][0]) steps.append(timer()) @@ -247,8 +258,8 @@ class TestDataContainer(unittest.TestCase): #ds0.__isub__( 2 ) steps.append(timer()) print("ds0.subtract(2,out=ds0)", dt( - steps), -2., ds0.as_array()[0][0][0]) - self.assertEqual(-2., ds0.as_array()[0][0][0]) + steps), -1., ds0.as_array()[0][0][0]) + self.assertEqual(-1., ds0.as_array()[0][0][0]) dt1 = dt(steps) ds3 = ds0.subtract(2) @@ -256,8 +267,8 @@ class TestDataContainer(unittest.TestCase): print("ds3 = ds0.subtract(2)", dt(steps), 0., ds3.as_array()[0][0][0]) dt2 = dt(steps) self.assertLess(dt1, dt2) - self.assertEqual(-2., ds0.as_array()[0][0][0]) - self.assertEqual(-4., ds3.as_array()[0][0][0]) + self.assertEqual(-1., ds0.as_array()[0][0][0]) + self.assertEqual(-3., ds3.as_array()[0][0][0]) def binary_multiply(self): print("Test binary multiply") @@ -300,6 +311,9 @@ class TestDataContainer(unittest.TestCase): self.assertLess(dt1, dt2) self.assertEqual(4., ds3.as_array()[0][0][0]) self.assertEqual(2., ds.as_array()[0][0][0]) + + ds.multiply(2.5, out=ds0) + self.assertEqual(2.5*2., ds0.as_array()[0][0][0]) def binary_divide(self): print("Test binary divide") @@ -314,16 +328,19 @@ class TestDataContainer(unittest.TestCase): ds = DataContainer(a, False, ['X', 'Y', 'Z']) ds1 = ds.copy() - steps.append(timer()) - ds.divide(ds1, out=ds) - steps.append(timer()) - t1 = dt(steps) - print("ds.divide(ds1, out=ds)", dt(steps)) - steps.append(timer()) - ds2 = ds.divide(ds1) - steps.append(timer()) - t2 = dt(steps) - print("ds2 = ds.divide(ds1)", dt(steps)) + t1 = 0 + t2 = 0 + for i in range(10): + steps.append(timer()) + ds.divide(ds1, out=ds) + steps.append(timer()) + t1 += dt(steps)/10. + print("ds.divide(ds1, out=ds)", dt(steps)) + steps.append(timer()) + ds2 = ds.divide(ds1) + steps.append(timer()) + t2 += dt(steps)/10. + print("ds2 = ds.divide(ds1)", dt(steps)) self.assertLess(t1, t2) self.assertEqual(ds.as_array()[0][0][0], 1.) @@ -445,6 +462,11 @@ class TestDataContainer(unittest.TestCase): self.assertTrue(False) except ValueError as ve: self.assertTrue(True) + + print ("test dot numpy") + n0 = (ds0 * ds1).sum() + n1 = ds0.as_array().ravel().dot(ds1.as_array().ravel()) + self.assertEqual(n0, n1) @@ -472,6 +494,13 @@ class TestDataContainer(unittest.TestCase): self.assertNumpyArrayEqual(vol1.as_array(), numpy.ones(vol.shape) * 4) self.assertEqual(vol.number_of_dimensions, 3) + + ig2 = ImageGeometry (voxel_num_x=2,voxel_num_y=3,voxel_num_z=4, + dimension_labels=[ImageGeometry.HORIZONTAL_X, ImageGeometry.HORIZONTAL_Y, + ImageGeometry.VERTICAL]) + data = ig2.allocate() + self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ig2.shape)) + self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape) def test_AcquisitionData(self): sgeometry = AcquisitionGeometry(dimension=2, angles=numpy.linspace(0, 180, num=10), @@ -479,6 +508,29 @@ class TestDataContainer(unittest.TestCase): pixel_num_h=5, channels=2) sino = AcquisitionData(geometry=sgeometry) self.assertEqual(sino.shape, (2, 10, 3, 5)) + + ag = AcquisitionGeometry (pixel_num_h=2,pixel_num_v=3,channels=4, dimension=2, angles=numpy.linspace(0, 180, num=10), + geom_type='parallel', ) + print (ag.shape) + print (ag.dimension_labels) + + data = ag.allocate() + self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ag.shape)) + self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape) + + print (data.shape, ag.shape, data.as_array().shape) + + ag2 = AcquisitionGeometry (pixel_num_h=2,pixel_num_v=3,channels=4, dimension=2, angles=numpy.linspace(0, 180, num=10), + geom_type='parallel', + dimension_labels=[AcquisitionGeometry.VERTICAL , + AcquisitionGeometry.ANGLE, AcquisitionGeometry.HORIZONTAL, AcquisitionGeometry.CHANNEL]) + + data = ag2.allocate() + print (data.shape, ag2.shape, data.as_array().shape) + self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ag2.shape)) + self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape) + + def test_ImageGeometry_allocate(self): vgeometry = ImageGeometry(voxel_num_x=4, voxel_num_y=3, channels=2) image = vgeometry.allocate() @@ -494,10 +546,21 @@ class TestDataContainer(unittest.TestCase): self.assertEqual(order[0], image.dimension_labels[0]) self.assertEqual(order[1], image.dimension_labels[1]) self.assertEqual(order[2], image.dimension_labels[2]) + + ig = ImageGeometry(2,3,2) + try: + z = ImageData(numpy.random.randint(10, size=(2,3)), geometry=ig) + self.assertTrue(False) + except ValueError as ve: + print (ve) + self.assertTrue(True) + + #vgeometry.allocate('') def test_AcquisitionGeometry_allocate(self): - ageometry = AcquisitionGeometry(dimension=2, angles=numpy.linspace(0, 180, num=10), - geom_type='parallel', pixel_num_v=3, - pixel_num_h=5, channels=2) + ageometry = AcquisitionGeometry(dimension=2, + angles=numpy.linspace(0, 180, num=10), + geom_type='parallel', pixel_num_v=3, + pixel_num_h=5, channels=2) sino = ageometry.allocate() shape = sino.shape print ("shape", shape) @@ -509,8 +572,8 @@ class TestDataContainer(unittest.TestCase): self.assertEqual(1,sino.as_array()[shape[0]-1][shape[1]-1][shape[2]-1][shape[3]-1]) print (sino.dimension_labels, sino.shape, ageometry) - default_order = ['channel' , ' angle' , - 'vertical' , 'horizontal'] + default_order = ['channel' , 'angle' , + 'vertical' , 'horizontal'] self.assertEqual(default_order[0], sino.dimension_labels[0]) self.assertEqual(default_order[1], sino.dimension_labels[1]) self.assertEqual(default_order[2], sino.dimension_labels[2]) @@ -522,7 +585,15 @@ class TestDataContainer(unittest.TestCase): self.assertEqual(order[1], sino.dimension_labels[1]) self.assertEqual(order[2], sino.dimension_labels[2]) self.assertEqual(order[2], sino.dimension_labels[2]) - + + + try: + z = AcquisitionData(numpy.random.randint(10, size=(2,3)), geometry=ageometry) + self.assertTrue(False) + except ValueError as ve: + print (ve) + self.assertTrue(True) + def assertNumpyArrayEqual(self, first, second): res = True try: @@ -556,9 +627,35 @@ class TestDataContainer(unittest.TestCase): norm = dc.norm() self.assertEqual(sqnorm, 8.0) numpy.testing.assert_almost_equal(norm, numpy.sqrt(8.0), decimal=7) + + def test_multiply_out(self): + print ("test multiply_out") + import functools + ig = ImageGeometry(10,11,12) + u = ig.allocate() + a = numpy.ones(u.shape) + + u.fill(a) + + numpy.testing.assert_array_equal(a, u.as_array()) + + #u = ig.allocate(ImageGeometry.RANDOM_INT, seed=1) + l = functools.reduce(lambda x,y: x*y, (10,11,12), 1) + + a = numpy.zeros((l, ), dtype=numpy.float32) + for i in range(l): + a[i] = numpy.sin(2 * i* 3.1415/l) + b = numpy.reshape(a, u.shape) + u.fill(b) + numpy.testing.assert_array_equal(b, u.as_array()) + + u.multiply(2, out=u) + c = b * 2 + numpy.testing.assert_array_equal(u.as_array(), c) + if __name__ == '__main__': unittest.main() -
\ No newline at end of file + diff --git a/Wrappers/Python/test/test_DataProcessor.py b/Wrappers/Python/test/test_DataProcessor.py index 1c1de3a..3e6a83e 100755 --- a/Wrappers/Python/test/test_DataProcessor.py +++ b/Wrappers/Python/test/test_DataProcessor.py @@ -11,8 +11,32 @@ from timeit import default_timer as timer from ccpi.framework import AX, CastDataContainer, PixelByPixelDataProcessor
+from ccpi.io.reader import NexusReader
+from ccpi.processors import CenterOfRotationFinder
+import wget
+import os
+
class TestDataProcessor(unittest.TestCase):
+ def setUp(self):
+ wget.download('https://github.com/DiamondLightSource/Savu/raw/master/test_data/data/24737_fd.nxs')
+ self.filename = '24737_fd.nxs'
+
+ def tearDown(self):
+ os.remove(self.filename)
+ def test_CenterOfRotation(self):
+ reader = NexusReader(self.filename)
+ ad = reader.get_acquisition_data_whole()
+ print (ad.geometry)
+ cf = CenterOfRotationFinder()
+ cf.set_input(ad)
+ print ("Center of rotation", cf.get_output())
+ self.assertAlmostEqual(86.25, cf.get_output())
+ def test_Normalizer(self):
+ pass
+
+
+
def test_DataProcessorChaining(self):
shape = (2,3,4,5)
size = shape[0]
diff --git a/Wrappers/Python/test/test_Gradient.py b/Wrappers/Python/test/test_Gradient.py new file mode 100755 index 0000000..4b7a034 --- /dev/null +++ b/Wrappers/Python/test/test_Gradient.py @@ -0,0 +1,101 @@ +import unittest +import numpy +from ccpi.framework import ImageGeometry, AcquisitionGeometry +from ccpi.framework import ImageData, AcquisitionData +#from ccpi.optimisation.algorithms import GradientDescent +from ccpi.framework import BlockDataContainer +#from ccpi.optimisation.Algorithms import CGLS +import functools + +from ccpi.optimisation.operators import Gradient, Identity, BlockOperator + +class TestGradient(unittest.TestCase): + def test_Gradient(self): + N, M, K = 20, 30, 40 + channels = 10 + + # check range geometry, examples + + ig1 = ImageGeometry(voxel_num_x = M, voxel_num_y = N) + ig2 = ImageGeometry(voxel_num_x = M, voxel_num_y = N, voxel_num_z = K) + ig3 = ImageGeometry(voxel_num_x = M, voxel_num_y = N, channels = channels) + ig4 = ImageGeometry(voxel_num_x = M, voxel_num_y = N, channels = channels, voxel_num_z= K) + + G1 = Gradient(ig1, correlation = 'Space') + print(G1.range_geometry().shape, '2D no channels') + + G4 = Gradient(ig3, correlation = 'SpaceChannels') + print(G4.range_geometry().shape, '2D with channels corr') + G5 = Gradient(ig3, correlation = 'Space') + print(G5.range_geometry().shape, '2D with channels no corr') + + G6 = Gradient(ig4, correlation = 'Space') + print(G6.range_geometry().shape, '3D with channels no corr') + G7 = Gradient(ig4, correlation = 'SpaceChannels') + print(G7.range_geometry().shape, '3D with channels with corr') + + + u = ig1.allocate(ImageGeometry.RANDOM) + w = G1.range_geometry().allocate(ImageGeometry.RANDOM_INT) + + LHS = (G1.direct(u)*w).sum() + RHS = (u * G1.adjoint(w)).sum() + numpy.testing.assert_approx_equal(LHS, RHS, significant = 1) + numpy.testing.assert_approx_equal(G1.norm(), numpy.sqrt(2*4), significant = 1) + + + u1 = ig3.allocate('random') + w1 = G4.range_geometry().allocate('random') + LHS1 = (G4.direct(u1) * w1).sum() + RHS1 = (u1 * G4.adjoint(w1)).sum() + numpy.testing.assert_approx_equal(LHS1, RHS1, significant=1) + numpy.testing.assert_almost_equal(G4.norm(), numpy.sqrt(3*4), decimal = 0) + + u2 = ig4.allocate('random') + w2 = G7.range_geometry().allocate('random') + LHS2 = (G7.direct(u2) * w2).sum() + RHS2 = (u2 * G7.adjoint(w2)).sum() + numpy.testing.assert_approx_equal(LHS2, RHS2, significant = 3) + numpy.testing.assert_approx_equal(G7.norm(), numpy.sqrt(3*4), significant = 1) + + + #check direct/adjoint for space/channels correlation + + ig_channel = ImageGeometry(voxel_num_x = 2, voxel_num_y = 3, channels = 2) + G_no_channel = Gradient(ig_channel, correlation = 'Space') + G_channel = Gradient(ig_channel, correlation = 'SpaceChannels') + + u3 = ig_channel.allocate('random_int') + res_no_channel = G_no_channel.direct(u3) + res_channel = G_channel.direct(u3) + + print(" Derivative for 3 directions, first is wrt Channel direction\n") + print(res_channel[0].as_array()) + print(res_channel[1].as_array()) + print(res_channel[2].as_array()) + + print(" Derivative for 2 directions, no Channel direction\n") + print(res_no_channel[0].as_array()) + print(res_no_channel[1].as_array()) + + ig2D = ImageGeometry(voxel_num_x = 2, voxel_num_y = 3) + u4 = ig2D.allocate('random_int') + G2D = Gradient(ig2D) + res = G2D.direct(u4) + print(res[0].as_array()) + print(res[1].as_array()) + + M, N = 20, 30 + ig = ImageGeometry(M, N) + arr = ig.allocate('random_int' ) + + # check direct of Gradient and sparse matrix + G = Gradient(ig) + norm1 = G.norm(iterations=300) + print ("should be sqrt(8) {} {}".format(numpy.sqrt(8), norm1)) + numpy.testing.assert_almost_equal(norm1, numpy.sqrt(8), decimal=1) + ig4 = ImageGeometry(M,N, channels=3) + G4 = Gradient(ig4, correlation=Gradient.CORRELATION_SPACECHANNEL) + norm4 = G4.norm(iterations=300) + print ("should be sqrt(12) {} {}".format(numpy.sqrt(12), norm4)) + self.assertTrue((norm4 - numpy.sqrt(12))/norm4 < 0.2) diff --git a/Wrappers/Python/test/test_NexusReader.py b/Wrappers/Python/test/test_NexusReader.py index 55543ba..a498d71 100755 --- a/Wrappers/Python/test/test_NexusReader.py +++ b/Wrappers/Python/test/test_NexusReader.py @@ -21,67 +21,67 @@ class TestNexusReader(unittest.TestCase): def tearDown(self): os.remove(self.filename) - - def testGetDimensions(self): + def testAll(self): + # def testGetDimensions(self): nr = NexusReader(self.filename) self.assertEqual(nr.get_sinogram_dimensions(), (135, 91, 160), "Sinogram dimensions are not correct") - def testGetProjectionDimensions(self): + # def testGetProjectionDimensions(self): nr = NexusReader(self.filename) self.assertEqual(nr.get_projection_dimensions(), (91,135,160), "Projection dimensions are not correct") - def testLoadProjectionWithoutDimensions(self): + # def testLoadProjectionWithoutDimensions(self): nr = NexusReader(self.filename) projections = nr.load_projection() self.assertEqual(projections.shape, (91,135,160), "Loaded projection data dimensions are not correct") - def testLoadProjectionWithDimensions(self): + # def testLoadProjectionWithDimensions(self): nr = NexusReader(self.filename) projections = nr.load_projection((slice(0,1), slice(0,135), slice(0,160))) self.assertEqual(projections.shape, (1,135,160), "Loaded projection data dimensions are not correct") - def testLoadProjectionCompareSingle(self): + # def testLoadProjectionCompareSingle(self): nr = NexusReader(self.filename) projections_full = nr.load_projection() projections_part = nr.load_projection((slice(0,1), slice(0,135), slice(0,160))) numpy.testing.assert_array_equal(projections_part, projections_full[0:1,:,:]) - def testLoadProjectionCompareMulti(self): + # def testLoadProjectionCompareMulti(self): nr = NexusReader(self.filename) projections_full = nr.load_projection() projections_part = nr.load_projection((slice(0,3), slice(0,135), slice(0,160))) numpy.testing.assert_array_equal(projections_part, projections_full[0:3,:,:]) - def testLoadProjectionCompareRandom(self): + # def testLoadProjectionCompareRandom(self): nr = NexusReader(self.filename) projections_full = nr.load_projection() projections_part = nr.load_projection((slice(1,8), slice(5,10), slice(8,20))) numpy.testing.assert_array_equal(projections_part, projections_full[1:8,5:10,8:20]) - def testLoadProjectionCompareFull(self): + # def testLoadProjectionCompareFull(self): nr = NexusReader(self.filename) projections_full = nr.load_projection() projections_part = nr.load_projection((slice(None,None), slice(None,None), slice(None,None))) numpy.testing.assert_array_equal(projections_part, projections_full[:,:,:]) - def testLoadFlatCompareFull(self): + # def testLoadFlatCompareFull(self): nr = NexusReader(self.filename) flats_full = nr.load_flat() flats_part = nr.load_flat((slice(None,None), slice(None,None), slice(None,None))) numpy.testing.assert_array_equal(flats_part, flats_full[:,:,:]) - def testLoadDarkCompareFull(self): + # def testLoadDarkCompareFull(self): nr = NexusReader(self.filename) darks_full = nr.load_dark() darks_part = nr.load_dark((slice(None,None), slice(None,None), slice(None,None))) numpy.testing.assert_array_equal(darks_part, darks_full[:,:,:]) - def testProjectionAngles(self): + # def testProjectionAngles(self): nr = NexusReader(self.filename) angles = nr.get_projection_angles() self.assertEqual(angles.shape, (91,), "Loaded projection number of angles are not correct") - def test_get_acquisition_data_subset(self): + # def test_get_acquisition_data_subset(self): nr = NexusReader(self.filename) key = nr.get_image_keys() sl = nr.get_acquisition_data_subset(0,10) diff --git a/Wrappers/Python/test/test_Operator.py b/Wrappers/Python/test/test_Operator.py new file mode 100644 index 0000000..d890e46 --- /dev/null +++ b/Wrappers/Python/test/test_Operator.py @@ -0,0 +1,466 @@ +import unittest +#from ccpi.optimisation.operators import Operator +#from ccpi.optimisation.ops import TomoIdentity +from ccpi.framework import ImageGeometry, ImageData, BlockDataContainer, DataContainer +from ccpi.optimisation.operators import BlockOperator, BlockScaledOperator,\ + FiniteDiff +import numpy +from timeit import default_timer as timer +from ccpi.framework import ImageGeometry +from ccpi.optimisation.operators import Gradient, Identity, SparseFiniteDiff +from ccpi.optimisation.operators import LinearOperator + +def dt(steps): + return steps[-1] - steps[-2] + +class CCPiTestClass(unittest.TestCase): + def assertBlockDataContainerEqual(self, container1, container2): + print ("assert Block Data Container Equal") + self.assertTrue(issubclass(container1.__class__, container2.__class__)) + for col in range(container1.shape[0]): + if issubclass(container1.get_item(col).__class__, DataContainer): + print ("Checking col ", col) + self.assertNumpyArrayEqual( + container1.get_item(col).as_array(), + container2.get_item(col).as_array() + ) + else: + self.assertBlockDataContainerEqual(container1.get_item(col),container2.get_item(col)) + + def assertNumpyArrayEqual(self, first, second): + res = True + try: + numpy.testing.assert_array_equal(first, second) + except AssertionError as err: + res = False + print(err) + self.assertTrue(res) + + def assertNumpyArrayAlmostEqual(self, first, second, decimal=6): + res = True + try: + numpy.testing.assert_array_almost_equal(first, second, decimal) + except AssertionError as err: + res = False + print(err) + print("expected " , second) + print("actual " , first) + + self.assertTrue(res) + + +class TestOperator(CCPiTestClass): + def test_ScaledOperator(self): + print ("test_ScaledOperator") + ig = ImageGeometry(10,20,30) + img = ig.allocate() + scalar = 0.5 + sid = scalar * Identity(ig) + numpy.testing.assert_array_equal(scalar * img.as_array(), sid.direct(img).as_array()) + + + def test_Identity(self): + print ("test_Identity") + ig = ImageGeometry(10,20,30) + img = ig.allocate() + self.assertTrue(img.shape == (30,20,10)) + self.assertEqual(img.sum(), 0) + Id = Identity(ig) + y = Id.direct(img) + numpy.testing.assert_array_equal(y.as_array(), img.as_array()) + + def test_FiniteDifference(self): + print ("test FiniteDifference") + ## + N, M = 2, 3 + + ig = ImageGeometry(N, M) + Id = Identity(ig) + + FD = FiniteDiff(ig, direction = 0, bnd_cond = 'Neumann') + u = FD.domain_geometry().allocate('random_int') + + + res = FD.domain_geometry().allocate(ImageGeometry.RANDOM_INT) + FD.adjoint(u, out=res) + w = FD.adjoint(u) + + self.assertNumpyArrayEqual(res.as_array(), w.as_array()) + + res = Id.domain_geometry().allocate(ImageGeometry.RANDOM_INT) + Id.adjoint(u, out=res) + w = Id.adjoint(u) + + self.assertNumpyArrayEqual(res.as_array(), w.as_array()) + self.assertNumpyArrayEqual(u.as_array(), w.as_array()) + + G = Gradient(ig) + + u = G.range_geometry().allocate(ImageGeometry.RANDOM_INT) + res = G.domain_geometry().allocate() + G.adjoint(u, out=res) + w = G.adjoint(u) + self.assertNumpyArrayEqual(res.as_array(), w.as_array()) + + u = G.domain_geometry().allocate(ImageGeometry.RANDOM_INT) + res = G.range_geometry().allocate() + G.direct(u, out=res) + w = G.direct(u) + self.assertBlockDataContainerEqual(res, w) + + def test_PowerMethod(self): + print ("test_BlockOperator") + + N, M = 200, 300 + niter = 10 + ig = ImageGeometry(N, M) + Id = Identity(ig) + + G = Gradient(ig) + + uid = Id.domain_geometry().allocate(ImageGeometry.RANDOM_INT, seed=1) + + a = LinearOperator.PowerMethod(Id, niter, uid) + #b = LinearOperator.PowerMethodNonsquare(Id, niter, uid) + b = LinearOperator.PowerMethod(Id, niter) + print ("Edo impl", a[0]) + print ("None impl", b[0]) + + #self.assertAlmostEqual(a[0], b[0]) + self.assertNumpyArrayAlmostEqual(a[0],b[0],decimal=6) + + a = LinearOperator.PowerMethod(G, niter, uid) + b = LinearOperator.PowerMethod(G, niter) + #b = LinearOperator.PowerMethodNonsquare(G, niter, uid) + + print ("Edo impl", a[0]) + #print ("old impl", b[0]) + self.assertNumpyArrayAlmostEqual(a[0],b[0],decimal=2) + #self.assertAlmostEqual(a[0], b[0]) + + def test_Norm(self): + print ("test_BlockOperator") + ## + N, M = 200, 300 + + ig = ImageGeometry(N, M) + G = Gradient(ig) + t0 = timer() + norm = G.norm() + t1 = timer() + norm2 = G.norm() + t2 = timer() + print ("Norm dT1 {} dT2 {}".format(t1-t0,t2-t1)) + self.assertLess(t2-t1, t1-t0) + + + + +class TestBlockOperator(unittest.TestCase): + def assertBlockDataContainerEqual(self, container1, container2): + print ("assert Block Data Container Equal") + self.assertTrue(issubclass(container1.__class__, container2.__class__)) + for col in range(container1.shape[0]): + if issubclass(container1.get_item(col).__class__, DataContainer): + print ("Checking col ", col) + self.assertNumpyArrayEqual( + container1.get_item(col).as_array(), + container2.get_item(col).as_array() + ) + else: + self.assertBlockDataContainerEqual(container1.get_item(col),container2.get_item(col)) + + def assertNumpyArrayEqual(self, first, second): + res = True + try: + numpy.testing.assert_array_equal(first, second) + except AssertionError as err: + res = False + print(err) + self.assertTrue(res) + + def assertNumpyArrayAlmostEqual(self, first, second, decimal=6): + res = True + try: + numpy.testing.assert_array_almost_equal(first, second, decimal) + except AssertionError as err: + res = False + print(err) + print("expected " , second) + print("actual " , first) + + self.assertTrue(res) + + def test_BlockOperator(self): + print ("test_BlockOperator") + + M, N = 3, 4 + ig = ImageGeometry(M, N) + arr = ig.allocate('random_int') + + G = Gradient(ig) + Id = Identity(ig) + + B = BlockOperator(G, Id) + # Nx1 case + u = ig.allocate('random_int') + z1 = B.direct(u) + + res = B.range_geometry().allocate() + #res = z1.copy() + B.direct(u, out=res) + + + print (type(z1), type(res)) + print (z1.shape) + print(z1[0][0].as_array()) + print(res[0][0].as_array()) + self.assertBlockDataContainerEqual(z1, res) + # for col in range(z1.shape[0]): + # a = z1.get_item(col) + # b = res.get_item(col) + # if isinstance(a, BlockDataContainer): + # for col2 in range(a.shape[0]): + # self.assertNumpyArrayEqual( + # a.get_item(col2).as_array(), + # b.get_item(col2).as_array() + # ) + # else: + # self.assertNumpyArrayEqual( + # a.as_array(), + # b.as_array() + # ) + z1 = B.range_geometry().allocate(ImageGeometry.RANDOM_INT) + + res1 = B.adjoint(z1) + res2 = B.domain_geometry().allocate() + B.adjoint(z1, out=res2) + + self.assertNumpyArrayEqual(res1.as_array(), res2.as_array()) + + BB = BlockOperator( Id, 2 * Id) + B = BlockOperator( BB, Id ) + v = B.domain_geometry().allocate() + B.adjoint(res,out=v) + vv = B.adjoint(res) + el1 = B.get_item(0,0).adjoint(z1.get_item(0)) +\ + B.get_item(1,0).adjoint(z1.get_item(1)) + print ("el1" , el1.as_array()) + print ("vv" , vv.as_array()) + print ("v" , v.as_array()) + + self.assertNumpyArrayEqual(v.as_array(),vv.as_array()) + # test adjoint + print ("############ 2x1 #############") + + BB = BlockOperator( Id, 2 * Id) + u = ig.allocate(1) + z1 = BB.direct(u) + print ("z1 shape {} one\n{} two\n{}".format(z1.shape, + z1.get_item(0).as_array(), + z1.get_item(1).as_array())) + res = BB.range_geometry().allocate(0) + BB.direct(u, out=res) + print ("res shape {} one\n{} two\n{}".format(res.shape, + res.get_item(0).as_array(), + res.get_item(1).as_array())) + + + self.assertNumpyArrayEqual(z1.get_item(0).as_array(), + u.as_array()) + self.assertNumpyArrayEqual(z1.get_item(1).as_array(), + 2 * u.as_array()) + self.assertNumpyArrayEqual(res.get_item(0).as_array(), + u.as_array()) + self.assertNumpyArrayEqual(res.get_item(1).as_array(), + 2 * u.as_array()) + + x1 = BB.adjoint(z1) + print("adjoint x1\n",x1.as_array()) + + res1 = BB.domain_geometry().allocate() + BB.adjoint(z1, out=res1) + print("res1\n",res1.as_array()) + self.assertNumpyArrayEqual(x1.as_array(), + res1.as_array()) + + self.assertNumpyArrayEqual(x1.as_array(), + 5 * u.as_array()) + self.assertNumpyArrayEqual(res1.as_array(), + 5 * u.as_array()) + ################################################# + + print ("############ 2x2 #############") + BB = BlockOperator( Id, 2 * Id, 3 * Id, Id, shape=(2,2)) + B = BB + u = ig.allocate(1) + U = BlockDataContainer(u,u) + z1 = B.direct(U) + + + print ("z1 shape {} one\n{} two\n{}".format(z1.shape, + z1.get_item(0).as_array(), + z1.get_item(1).as_array())) + self.assertNumpyArrayEqual(z1.get_item(0).as_array(), + 3 * u.as_array()) + self.assertNumpyArrayEqual(z1.get_item(1).as_array(), + 4 * u.as_array()) + res = B.range_geometry().allocate() + B.direct(U, out=res) + self.assertNumpyArrayEqual(res.get_item(0).as_array(), + 3 * u.as_array()) + self.assertNumpyArrayEqual(res.get_item(1).as_array(), + 4 * u.as_array()) + + + x1 = B.adjoint(z1) + # this should be [15 u, 10 u] + el1 = B.get_item(0,0).adjoint(z1.get_item(0)) + B.get_item(1,0).adjoint(z1.get_item(1)) + el2 = B.get_item(0,1).adjoint(z1.get_item(0)) + B.get_item(1,1).adjoint(z1.get_item(1)) + + shape = B.get_output_shape(z1.shape, adjoint=True) + print ("shape ", shape) + out = B.domain_geometry().allocate() + + for col in range(B.shape[1]): + for row in range(B.shape[0]): + if row == 0: + el = B.get_item(row,col).adjoint(z1.get_item(row)) + else: + el += B.get_item(row,col).adjoint(z1.get_item(row)) + out.get_item(col).fill(el) + + print ("el1 " , el1.as_array()) + print ("el2 " , el2.as_array()) + print ("out shape {} one\n{} two\n{}".format(out.shape, + out.get_item(0).as_array(), + out.get_item(1).as_array())) + + self.assertNumpyArrayEqual(out.get_item(0).as_array(), + 15 * u.as_array()) + self.assertNumpyArrayEqual(out.get_item(1).as_array(), + 10 * u.as_array()) + + res2 = B.domain_geometry().allocate() + #print (res2, res2.as_array()) + B.adjoint(z1, out = res2) + + #print ("adjoint",x1.as_array(),"\n",res2.as_array()) + self.assertNumpyArrayEqual( + out.get_item(0).as_array(), + res2.get_item(0).as_array() + ) + self.assertNumpyArrayEqual( + out.get_item(1).as_array(), + res2.get_item(1).as_array() + ) + + if True: + #B1 = BlockOperator(Id, Id, Id, Id, shape=(2,2)) + B1 = BlockOperator(G, Id) + U = ig.allocate(ImageGeometry.RANDOM_INT) + #U = BlockDataContainer(u,u) + RES1 = B1.range_geometry().allocate() + + Z1 = B1.direct(U) + B1.direct(U, out = RES1) + + self.assertBlockDataContainerEqual(Z1,RES1) + + + + print("U", U.as_array()) + print("Z1", Z1[0][0].as_array()) + print("RES1", RES1[0][0].as_array()) + print("Z1", Z1[0][1].as_array()) + print("RES1", RES1[0][1].as_array()) + def test_timedifference(self): + print ("test_timedifference") + M, N ,W = 100, 512, 512 + ig = ImageGeometry(M, N, W) + arr = ig.allocate('random_int') + + G = Gradient(ig) + Id = Identity(ig) + + B = BlockOperator(G, Id) + + + # Nx1 case + u = ig.allocate('random_int') + steps = [timer()] + i = 0 + n = 2. + t1 = t2 = 0 + res = B.range_geometry().allocate() + + while (i < n): + print ("i ", i) + steps.append(timer()) + z1 = B.direct(u) + steps.append(timer()) + t = dt(steps) + #print ("B.direct(u) " ,t) + t1 += t/n + + steps.append(timer()) + B.direct(u, out = res) + steps.append(timer()) + t = dt(steps) + #print ("B.direct(u, out=res) " ,t) + t2 += t/n + i += 1 + + print ("Time difference ", t1,t2) + self.assertGreater(t1,t2) + + steps = [timer()] + i = 0 + #n = 50. + t1 = t2 = 0 + resd = B.domain_geometry().allocate() + z1 = B.direct(u) + #B.adjoint(z1, out=resd) + + print (type(res)) + while (i < n): + print ("i ", i) + steps.append(timer()) + w1 = B.adjoint(z1) + steps.append(timer()) + t = dt(steps) + #print ("B.adjoint(z1) " ,t) + t1 += t/n + + steps.append(timer()) + B.adjoint(z1, out=resd) + steps.append(timer()) + t = dt(steps) + #print ("B.adjoint(z1, out=res) " ,t) + t2 += t/n + i += 1 + + print ("Time difference ", t1,t2) + self.assertGreater(t1,t2) + + def test_BlockOperatorLinearValidity(self): + print ("test_BlockOperatorLinearValidity") + + M, N = 3, 4 + ig = ImageGeometry(M, N) + arr = ig.allocate('random_int') + + G = Gradient(ig) + Id = Identity(ig) + + B = BlockOperator(G, Id) + # Nx1 case + u = ig.allocate('random_int') + w = B.range_geometry().allocate(ImageGeometry.RANDOM_INT) + w1 = B.direct(u) + u1 = B.adjoint(w) + self.assertEqual((w * w1).sum() , (u1*u).sum()) + + + + diff --git a/Wrappers/Python/test/test_algorithms.py b/Wrappers/Python/test/test_algorithms.py index b5959b5..3bb3d57 100755 --- a/Wrappers/Python/test/test_algorithms.py +++ b/Wrappers/Python/test/test_algorithms.py @@ -12,12 +12,12 @@ from ccpi.framework import ImageData from ccpi.framework import AcquisitionData from ccpi.framework import ImageGeometry from ccpi.framework import AcquisitionGeometry -from ccpi.optimisation.ops import TomoIdentity -from ccpi.optimisation.funcs import Norm2sq +from ccpi.optimisation.operators import Identity +from ccpi.optimisation.functions import Norm2Sq, ZeroFunction, \ + L2NormSquared, FunctionOperatorComposition from ccpi.optimisation.algorithms import GradientDescent from ccpi.optimisation.algorithms import CGLS from ccpi.optimisation.algorithms import FISTA -from ccpi.optimisation.algorithms import FBPD @@ -26,7 +26,7 @@ class TestAlgorithms(unittest.TestCase): def setUp(self): #wget.download('https://github.com/DiamondLightSource/Savu/raw/master/test_data/data/24737_fd.nxs') #self.filename = '24737_fd.nxs' - # we use TomoIdentity as the operator and solve the simple least squares + # we use Identity as the operator and solve the simple least squares # problem for a random-valued ImageData or AcquisitionData b? # Then we know the minimiser is b itself @@ -48,13 +48,15 @@ class TestAlgorithms(unittest.TestCase): # fill with random numbers b.fill(numpy.random.random(x_init.shape)) - identity = TomoIdentity(geometry=ig) + identity = Identity(ig) - norm2sq = Norm2sq(identity, b) + norm2sq = Norm2Sq(identity, b) + rate = 0.3 + rate = norm2sq.L / 3. alg = GradientDescent(x_init=x_init, objective_function=norm2sq, - rate=0.3) + rate=rate) alg.max_iteration = 20 alg.run(20, verbose=True) self.assertNumpyArrayAlmostEqual(alg.x.as_array(), b.as_array()) @@ -62,11 +64,12 @@ class TestAlgorithms(unittest.TestCase): print ("Test CGLS") ig = ImageGeometry(124,153,154) x_init = ImageData(geometry=ig) + x_init = ig.allocate() b = x_init.copy() # fill with random numbers b.fill(numpy.random.random(x_init.shape)) - - identity = TomoIdentity(geometry=ig) + b = ig.allocate('random') + identity = Identity(ig) alg = CGLS(x_init=x_init, operator=identity, data=b) alg.max_iteration = 1 @@ -80,19 +83,19 @@ class TestAlgorithms(unittest.TestCase): b = x_init.copy() # fill with random numbers b.fill(numpy.random.random(x_init.shape)) - x_init = ImageData(geometry=ig) - x_init.fill(numpy.random.random(x_init.shape)) - - identity = TomoIdentity(geometry=ig) + x_init = ig.allocate(ImageGeometry.RANDOM) + identity = Identity(ig) - norm2sq = Norm2sq(identity, b) + #### it seems FISTA does not work with Nowm2Sq + # norm2sq = Norm2Sq(identity, b) + # norm2sq.L = 2 * norm2sq.c * identity.norm()**2 + norm2sq = FunctionOperatorComposition(L2NormSquared(b=b), identity) opt = {'tol': 1e-4, 'memopt':False} - alg = FISTA(x_init=x_init, f=norm2sq, g=None, opt=opt) + print ("initial objective", norm2sq(x_init)) + alg = FISTA(x_init=x_init, f=norm2sq, g=ZeroFunction()) alg.max_iteration = 2 alg.run(20, verbose=True) self.assertNumpyArrayAlmostEqual(alg.x.as_array(), b.as_array()) - alg.run(20, verbose=True) - self.assertNumpyArrayAlmostEqual(alg.x.as_array(), b.as_array()) @@ -120,4 +123,4 @@ class TestAlgorithms(unittest.TestCase): if __name__ == '__main__': unittest.main() -
\ No newline at end of file + diff --git a/Wrappers/Python/test/test_functions.py b/Wrappers/Python/test/test_functions.py new file mode 100644 index 0000000..021ad99 --- /dev/null +++ b/Wrappers/Python/test/test_functions.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Sat Mar 2 19:24:37 2019 + +@author: evangelos +""" + + +import numpy as np +#from ccpi.optimisation.funcs import Function +from ccpi.optimisation.functions import Function, KullbackLeibler +from ccpi.framework import DataContainer, ImageData, ImageGeometry +from ccpi.optimisation.operators import Identity +from ccpi.optimisation.operators import BlockOperator +from ccpi.framework import BlockDataContainer +from numbers import Number +from ccpi.optimisation.operators import Gradient + +from ccpi.optimisation.functions import L2NormSquared +from ccpi.optimisation.functions import L1Norm, MixedL21Norm + +from ccpi.optimisation.functions import Norm2Sq +from ccpi.optimisation.functions import ZeroFunction + +from ccpi.optimisation.functions import FunctionOperatorComposition +import unittest +import numpy + +# + + +class TestFunction(unittest.TestCase): + def assertBlockDataContainerEqual(self, container1, container2): + print ("assert Block Data Container Equal") + self.assertTrue(issubclass(container1.__class__, container2.__class__)) + for col in range(container1.shape[0]): + if issubclass(container1.get_item(col).__class__, DataContainer): + print ("Checking col ", col) + self.assertNumpyArrayEqual( + container1.get_item(col).as_array(), + container2.get_item(col).as_array() + ) + else: + self.assertBlockDataContainerEqual(container1.get_item(col),container2.get_item(col)) + + def assertNumpyArrayEqual(self, first, second): + res = True + try: + numpy.testing.assert_array_equal(first, second) + except AssertionError as err: + res = False + print(err) + self.assertTrue(res) + + def assertNumpyArrayAlmostEqual(self, first, second, decimal=6): + res = True + try: + numpy.testing.assert_array_almost_equal(first, second, decimal) + except AssertionError as err: + res = False + print(err) + print("expected " , second) + print("actual " , first) + + self.assertTrue(res) + def test_Function(self): + + + N = 3 + ig = ImageGeometry(N,N) + ag = ig + op1 = Gradient(ig) + op2 = Identity(ig, ag) + + # Form Composite Operator + operator = BlockOperator(op1, op2 , shape=(2,1) ) + + # Create functions + noisy_data = ag.allocate(ImageGeometry.RANDOM_INT) + + d = ag.allocate(ImageGeometry.RANDOM_INT) + alpha = 0.5 + # scaled function + g = alpha * L2NormSquared(b=noisy_data) + + # Compare call of g + a2 = alpha*(d - noisy_data).power(2).sum() + #print(a2, g(d)) + self.assertEqual(a2, g(d)) + + # Compare convex conjugate of g + a3 = 0.5 * d.squared_norm() + d.dot(noisy_data) + self.assertEqual(a3, g.convex_conjugate(d)) + #print( a3, g.convex_conjugate(d)) + + #test proximal conjugate + + + def test_L2NormSquared(self): + # TESTS for L2 and scalar * L2 + print ("Test L2NormSquared") + + M, N, K = 2,3,5 + ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N, voxel_num_z = K) + u = ig.allocate(ImageGeometry.RANDOM_INT) + b = ig.allocate(ImageGeometry.RANDOM_INT) + + # check grad/call no data + f = L2NormSquared() + a1 = f.gradient(u) + a2 = 2 * u + numpy.testing.assert_array_almost_equal(a1.as_array(), a2.as_array(), decimal=4) + numpy.testing.assert_equal(f(u), u.squared_norm()) + + # check grad/call with data + f1 = L2NormSquared(b=b) + b1 = f1.gradient(u) + b2 = 2 * (u-b) + + numpy.testing.assert_array_almost_equal(b1.as_array(), b2.as_array(), decimal=4) + numpy.testing.assert_equal(f1(u), (u-b).squared_norm()) + + #check convex conjuagate no data + c1 = f.convex_conjugate(u) + c2 = 1/4. * u.squared_norm() + numpy.testing.assert_equal(c1, c2) + + #check convex conjugate with data + d1 = f1.convex_conjugate(u) + d2 = (1./4.) * u.squared_norm() + (u*b).sum() + numpy.testing.assert_equal(d1, d2) + + # check proximal no data + tau = 5 + e1 = f.proximal(u, tau) + e2 = u/(1+2*tau) + numpy.testing.assert_array_almost_equal(e1.as_array(), e2.as_array(), decimal=4) + + # check proximal with data + tau = 5 + h1 = f1.proximal(u, tau) + h2 = (u-b)/(1+2*tau) + b + numpy.testing.assert_array_almost_equal(h1.as_array(), h2.as_array(), decimal=4) + + # check proximal conjugate no data + tau = 0.2 + k1 = f.proximal_conjugate(u, tau) + k2 = u/(1 + tau/2 ) + numpy.testing.assert_array_almost_equal(k1.as_array(), k2.as_array(), decimal=4) + + # check proximal conjugate with data + l1 = f1.proximal_conjugate(u, tau) + l2 = (u - tau * b)/(1 + tau/2 ) + numpy.testing.assert_array_almost_equal(l1.as_array(), l2.as_array(), decimal=4) + + # check scaled function properties + + # scalar + scalar = 100 + f_scaled_no_data = scalar * L2NormSquared() + f_scaled_data = scalar * L2NormSquared(b=b) + + # call + numpy.testing.assert_equal(f_scaled_no_data(u), scalar*f(u)) + numpy.testing.assert_equal(f_scaled_data(u), scalar*f1(u)) + + # grad + numpy.testing.assert_array_almost_equal(f_scaled_no_data.gradient(u).as_array(), scalar*f.gradient(u).as_array(), decimal=4) + numpy.testing.assert_array_almost_equal(f_scaled_data.gradient(u).as_array(), scalar*f1.gradient(u).as_array(), decimal=4) + + # conj + numpy.testing.assert_almost_equal(f_scaled_no_data.convex_conjugate(u), \ + f.convex_conjugate(u/scalar) * scalar, decimal=4) + + numpy.testing.assert_almost_equal(f_scaled_data.convex_conjugate(u), \ + scalar * f1.convex_conjugate(u/scalar), decimal=4) + + # proximal + numpy.testing.assert_array_almost_equal(f_scaled_no_data.proximal(u, tau).as_array(), \ + f.proximal(u, tau*scalar).as_array()) + + + numpy.testing.assert_array_almost_equal(f_scaled_data.proximal(u, tau).as_array(), \ + f1.proximal(u, tau*scalar).as_array()) + + + # proximal conjugate + numpy.testing.assert_array_almost_equal(f_scaled_no_data.proximal_conjugate(u, tau).as_array(), \ + (u/(1 + tau/(2*scalar) )).as_array(), decimal=4) + + numpy.testing.assert_array_almost_equal(f_scaled_data.proximal_conjugate(u, tau).as_array(), \ + ((u - tau * b)/(1 + tau/(2*scalar) )).as_array(), decimal=4) + + def test_L2NormSquaredOut(self): + # TESTS for L2 and scalar * L2 + + M, N, K = 2,3,5 + ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N, voxel_num_z = K) + u = ig.allocate(ImageGeometry.RANDOM_INT) + b = ig.allocate(ImageGeometry.RANDOM_INT) + + # check grad/call no data + f = L2NormSquared() + a1 = f.gradient(u) + a2 = a1 * 0. + f.gradient(u, out=a2) + numpy.testing.assert_array_almost_equal(a1.as_array(), a2.as_array(), decimal=4) + #numpy.testing.assert_equal(f(u), u.squared_norm()) + + # check grad/call with data + f1 = L2NormSquared(b=b) + b1 = f1.gradient(u) + b2 = b1 * 0. + f1.gradient(u, out=b2) + + numpy.testing.assert_array_almost_equal(b1.as_array(), b2.as_array(), decimal=4) + #numpy.testing.assert_equal(f1(u), (u-b).squared_norm()) + + # check proximal no data + tau = 5 + e1 = f.proximal(u, tau) + e2 = e1 * 0. + f.proximal(u, tau, out=e2) + numpy.testing.assert_array_almost_equal(e1.as_array(), e2.as_array(), decimal=4) + + # check proximal with data + tau = 5 + h1 = f1.proximal(u, tau) + h2 = h1 * 0. + f1.proximal(u, tau, out=h2) + numpy.testing.assert_array_almost_equal(h1.as_array(), h2.as_array(), decimal=4) + + # check proximal conjugate no data + tau = 0.2 + k1 = f.proximal_conjugate(u, tau) + k2 = k1 * 0. + f.proximal_conjugate(u, tau, out=k2) + + numpy.testing.assert_array_almost_equal(k1.as_array(), k2.as_array(), decimal=4) + + # check proximal conjugate with data + l1 = f1.proximal_conjugate(u, tau) + l2 = l1 * 0. + f1.proximal_conjugate(u, tau, out=l2) + numpy.testing.assert_array_almost_equal(l1.as_array(), l2.as_array(), decimal=4) + + # check scaled function properties + + # scalar + scalar = 100 + f_scaled_no_data = scalar * L2NormSquared() + f_scaled_data = scalar * L2NormSquared(b=b) + + # grad + w = f_scaled_no_data.gradient(u) + ww = w * 0 + f_scaled_no_data.gradient(u, out=ww) + + numpy.testing.assert_array_almost_equal(w.as_array(), + ww.as_array(), decimal=4) + + # numpy.testing.assert_array_almost_equal(f_scaled_data.gradient(u).as_array(), scalar*f1.gradient(u).as_array(), decimal=4) + + # # conj + # numpy.testing.assert_almost_equal(f_scaled_no_data.convex_conjugate(u), \ + # f.convex_conjugate(u/scalar) * scalar, decimal=4) + + # numpy.testing.assert_almost_equal(f_scaled_data.convex_conjugate(u), \ + # scalar * f1.convex_conjugate(u/scalar), decimal=4) + + # # proximal + w = f_scaled_no_data.proximal(u, tau) + ww = w * 0 + f_scaled_no_data.proximal(u, tau, out=ww) + numpy.testing.assert_array_almost_equal(w.as_array(), \ + ww.as_array()) + + + # numpy.testing.assert_array_almost_equal(f_scaled_data.proximal(u, tau).as_array(), \ + # f1.proximal(u, tau*scalar).as_array()) + + + # proximal conjugate + w = f_scaled_no_data.proximal_conjugate(u, tau) + ww = w * 0 + f_scaled_no_data.proximal_conjugate(u, tau, out=ww) + numpy.testing.assert_array_almost_equal(w.as_array(), \ + ww.as_array(), decimal=4) + + # numpy.testing.assert_array_almost_equal(f_scaled_data.proximal_conjugate(u, tau).as_array(), \ + # ((u - tau * b)/(1 + tau/(2*scalar) )).as_array(), decimal=4) + + def test_Norm2sq_as_FunctionOperatorComposition(self): + M, N, K = 2,3,5 + ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N, voxel_num_z = K) + u = ig.allocate(ImageGeometry.RANDOM_INT) + b = ig.allocate(ImageGeometry.RANDOM_INT) + + A = 0.5 * Identity(ig) + old_chisq = Norm2Sq(A, b, 1.0) + new_chisq = FunctionOperatorComposition(L2NormSquared(b=b),A) + + yold = old_chisq(u) + ynew = new_chisq(u) + self.assertEqual(yold, ynew) + + yold = old_chisq.gradient(u) + ynew = new_chisq.gradient(u) + numpy.testing.assert_array_equal(yold.as_array(), ynew.as_array()) + + def test_mixedL12Norm(self): + M, N, K = 2,3,5 + ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N) + u1 = ig.allocate('random_int') + u2 = ig.allocate('random_int') + + U = BlockDataContainer(u1, u2, shape=(2,1)) + + # Define no scale and scaled + f_no_scaled = MixedL21Norm() + f_scaled = 1 * MixedL21Norm() + + # call + + a1 = f_no_scaled(U) + a2 = f_scaled(U) + self.assertNumpyArrayAlmostEqual(a1,a2) + + + tmp = [ el**2 for el in U.containers ] + self.assertBlockDataContainerEqual(BlockDataContainer(*tmp), + U.power(2)) + + z1 = f_no_scaled.proximal_conjugate(U, 1) + u3 = ig.allocate('random_int') + u4 = ig.allocate('random_int') + + z3 = BlockDataContainer(u3, u4, shape=(2,1)) + + + f_no_scaled.proximal_conjugate(U, 1, out=z3) + self.assertBlockDataContainerEqual(z3,z1) + + def test_KullbackLeibler(self): + print ("test_KullbackLeibler") + N, M = 2,3 + ig = ImageGeometry(N, M) + data = ig.allocate(ImageGeometry.RANDOM_INT) + x = ig.allocate(ImageGeometry.RANDOM_INT) + bnoise = ig.allocate(ImageGeometry.RANDOM_INT) + + out = ig.allocate() + + f = KullbackLeibler(data, bnoise=bnoise) + + grad = f.gradient(x) + f.gradient(x, out=out) + numpy.testing.assert_array_equal(grad.as_array(), out.as_array()) + + prox = f.proximal(x,1.2) + f.proximal(x, 1.2, out=out) + numpy.testing.assert_array_equal(prox.as_array(), out.as_array()) + + proxc = f.proximal_conjugate(x,1.2) + f.proximal_conjugate(x, 1.2, out=out) + numpy.testing.assert_array_equal(proxc.as_array(), out.as_array()) diff --git a/Wrappers/Python/test/test_run_test.py b/Wrappers/Python/test/test_run_test.py index 3c7d9ab..81ee738 100755 --- a/Wrappers/Python/test/test_run_test.py +++ b/Wrappers/Python/test/test_run_test.py @@ -6,18 +6,17 @@ from ccpi.framework import ImageData from ccpi.framework import AcquisitionData from ccpi.framework import ImageGeometry from ccpi.framework import AcquisitionGeometry -from ccpi.optimisation.algs import FISTA -from ccpi.optimisation.algs import FBPD -from ccpi.optimisation.funcs import Norm2sq -from ccpi.optimisation.funcs import ZeroFun -from ccpi.optimisation.funcs import Norm1 -from ccpi.optimisation.funcs import TV2D -from ccpi.optimisation.funcs import Norm2 +from ccpi.optimisation.algorithms import FISTA +#from ccpi.optimisation.algs import FBPD +from ccpi.optimisation.functions import Norm2Sq +from ccpi.optimisation.functions import ZeroFunction +# from ccpi.optimisation.funcs import Norm1 +from ccpi.optimisation.functions import L1Norm -from ccpi.optimisation.ops import LinearOperatorMatrix -from ccpi.optimisation.ops import TomoIdentity -from ccpi.optimisation.ops import Identity -from ccpi.optimisation.ops import PowerMethodNonsquare +from ccpi.optimisation.operators import LinearOperatorMatrix +from ccpi.optimisation.operators import Identity +#from ccpi.optimisation.ops import PowerMethodNonsquare +from ccpi.optimisation.operators import LinearOperator import numpy.testing @@ -81,8 +80,8 @@ class TestAlgorithms(unittest.TestCase): lam = 10 opt = {'memopt': True} # Create object instances with the test data A and b. - f = Norm2sq(A, b, c=0.5, memopt=True) - g0 = ZeroFun() + f = Norm2Sq(A, b, c=0.5, memopt=True) + g0 = ZeroFunction() # Initial guess x_init = DataContainer(np.zeros((n, 1))) @@ -90,12 +89,15 @@ class TestAlgorithms(unittest.TestCase): f.grad(x_init) # Run FISTA for least squares plus zero function. - x_fista0, it0, timing0, criter0 = FISTA(x_init, f, g0, opt=opt) - + #x_fista0, it0, timing0, criter0 = FISTA(x_init, f, g0, opt=opt) + fa = FISTA(x_init=x_init, f=f, g=g0) + fa.max_iteration = 10 + fa.run(10) + # Print solution and final objective/criterion value for comparison print("FISTA least squares plus zero function solution and objective value:") - print(x_fista0.array) - print(criter0[-1]) + print(fa.get_output()) + print(fa.get_last_objective()) # Compare to CVXPY @@ -135,18 +137,22 @@ class TestAlgorithms(unittest.TestCase): # A = Identity() # Change n to equal to m. - - b = DataContainer(bmat) + vgb = VectorGeometry(m) + vgx = VectorGeometry(n) + b = vgb.allocate() + b.fill(bmat) + #b = DataContainer(bmat) # Regularization parameter lam = 10 opt = {'memopt': True} # Create object instances with the test data A and b. - f = Norm2sq(A, b, c=0.5, memopt=True) - g0 = ZeroFun() + f = Norm2Sq(A, b, c=0.5, memopt=True) + g0 = ZeroFunction() # Initial guess - x_init = DataContainer(np.zeros((n, 1))) + #x_init = DataContainer(np.zeros((n, 1))) + x_init = vgx.allocate() # Create 1-norm object instance g1 = Norm1(lam) @@ -155,12 +161,16 @@ class TestAlgorithms(unittest.TestCase): g1.prox(x_init, 0.02) # Combine with least squares and solve using generic FISTA implementation - x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g1, opt=opt) + #x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g1, opt=opt) + fa = FISTA(x_init=x_init, f=f, g=g1) + fa.max_iteration = 10 + fa.run(10) + # Print for comparison print("FISTA least squares plus 1-norm solution and objective value:") - print(x_fista1.as_array().squeeze()) - print(criter1[-1]) + print(fa.get_output()) + print(fa.get_last_objective()) # Compare to CVXPY @@ -212,8 +222,8 @@ class TestAlgorithms(unittest.TestCase): x_init = DataContainer(np.random.randn(n, 1)) # Create object instances with the test data A and b. - f = Norm2sq(A, b, c=0.5, memopt=True) - f.L = PowerMethodNonsquare(A, 25, x_init)[0] + f = Norm2Sq(A, b, c=0.5, memopt=True) + f.L = LinearOperator.PowerMethod(A, 25, x_init)[0] print ("Lipschitz", f.L) g0 = ZeroFun() @@ -280,9 +290,9 @@ class TestAlgorithms(unittest.TestCase): y.array = y.array + 0.1*np.random.randn(N, N) # Data fidelity term - f_denoise = Norm2sq(I, y, c=0.5, memopt=True) + f_denoise = Norm2Sq(I, y, c=0.5, memopt=True) x_init = ImageData(geometry=ig) - f_denoise.L = PowerMethodNonsquare(I, 25, x_init)[0] + f_denoise.L = LinearOperator.PowerMethod(I, 25, x_init)[0] # 1-norm regulariser lam1_denoise = 1.0 @@ -328,43 +338,6 @@ class TestAlgorithms(unittest.TestCase): self.assertNumpyArrayAlmostEqual( x_fbpd1_denoise.array.flatten(), x1_denoise.value, 5) - x1_cvx = x1_denoise.value - x1_cvx.shape = (N, N) - - # Now TV with FBPD - lam_tv = 0.1 - gtv = TV2D(lam_tv) - gtv(gtv.op.direct(x_init_denoise)) - - opt_tv = {'tol': 1e-4, 'iter': 10000} - - x_fbpdtv_denoise, itfbpdtv_denoise, timingfbpdtv_denoise,\ - criterfbpdtv_denoise = \ - FBPD(x_init_denoise, gtv.op, None, f_denoise, gtv, opt=opt_tv) - print(x_fbpdtv_denoise) - print(criterfbpdtv_denoise[-1]) - - # Compare to CVXPY - - # Construct the problem. - xtv_denoise = Variable((N, N)) - objectivetv_denoise = Minimize( - 0.5*sum_squares(xtv_denoise - y.array) + lam_tv*tv(xtv_denoise)) - probtv_denoise = Problem(objectivetv_denoise) - - # The optimal objective is returned by prob.solve(). - resulttv_denoise = probtv_denoise.solve( - verbose=False, solver=SCS, eps=1e-12) - - # The optimal solution for x is stored in x.value and optimal objective value - # is in result as well as in objective.value - print("CVXPY least squares plus 1-norm solution and objective value:") - print(xtv_denoise.value) - print(objectivetv_denoise.value) - - self.assertNumpyArrayAlmostEqual( - x_fbpdtv_denoise.as_array(), xtv_denoise.value, 1) - else: self.assertTrue(cvx_not_installable) diff --git a/Wrappers/Python/wip/CGLS_tikhonov.py b/Wrappers/Python/wip/CGLS_tikhonov.py new file mode 100644 index 0000000..e9bbcd9 --- /dev/null +++ b/Wrappers/Python/wip/CGLS_tikhonov.py @@ -0,0 +1,196 @@ +from ccpi.optimisation.algorithms import CGLS + +from ccpi.plugins.ops import CCPiProjectorSimple +from ccpi.optimisation.ops import PowerMethodNonsquare +from ccpi.optimisation.ops import TomoIdentity +from ccpi.optimisation.funcs import Norm2sq, Norm1 +from ccpi.framework import ImageGeometry, AcquisitionGeometry, ImageData, AcquisitionData +from ccpi.optimisation.algorithms import GradientDescent +#from ccpi.optimisation.algorithms import CGLS +import matplotlib.pyplot as plt +import numpy +from ccpi.framework import BlockDataContainer +from ccpi.optimisation.operators import BlockOperator + +# Set up phantom size N x N x vert by creating ImageGeometry, initialising the +# ImageData object with this geometry and empty array and finally put some +# data into its array, and display one slice as image. + +# Image parameters +N = 128 +vert = 4 + +# Set up image geometry +ig = ImageGeometry(voxel_num_x=N, + voxel_num_y=N, + voxel_num_z=vert) + +# Set up empty image data +Phantom = ImageData(geometry=ig, + dimension_labels=['horizontal_x', + 'horizontal_y', + 'vertical']) +Phantom += 0.05 +# Populate image data by looping over and filling slices +i = 0 +while i < vert: + if vert > 1: + x = Phantom.subset(vertical=i).array + else: + x = Phantom.array + x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 + x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 0.94 + if vert > 1 : + Phantom.fill(x, vertical=i) + i += 1 + + +perc = 0.02 +# Set up empty image data +noise = ImageData(numpy.random.normal(loc = 0.04 , + scale = perc , + size = Phantom.shape), geometry=ig, + dimension_labels=['horizontal_x', + 'horizontal_y', + 'vertical']) +Phantom += noise + +# Set up AcquisitionGeometry object to hold the parameters of the measurement +# setup geometry: # Number of angles, the actual angles from 0 to +# pi for parallel beam, set the width of a detector +# pixel relative to an object pixe and the number of detector pixels. +angles_num = 20 +det_w = 1.0 +det_num = N + +angles = numpy.linspace(0,numpy.pi,angles_num,endpoint=False,dtype=numpy.float32)*\ + 180/numpy.pi + +# Inputs: Geometry, 2D or 3D, angles, horz detector pixel count, +# horz detector pixel size, vert detector pixel count, +# vert detector pixel size. +ag = AcquisitionGeometry('parallel', + '3D', + angles, + N, + det_w, + vert, + det_w) + +# Set up Operator object combining the ImageGeometry and AcquisitionGeometry +# wrapping calls to CCPi projector. +A = CCPiProjectorSimple(ig, ag) + +# Forward and backprojection are available as methods direct and adjoint. Here +# generate test data b and some noise + +b = A.direct(Phantom) + + +#z = A.adjoint(b) + + +# Using the test data b, different reconstruction methods can now be set up as +# demonstrated in the rest of this file. In general all methods need an initial +# guess and some algorithm options to be set. Note that 100 iterations for +# some of the methods is a very low number and 1000 or 10000 iterations may be +# needed if one wants to obtain a converged solution. +x_init = ImageData(geometry=ig, + dimension_labels=['horizontal_x','horizontal_y','vertical']) +X_init = BlockDataContainer(x_init) +B = BlockDataContainer(b, + ImageData(geometry=ig, dimension_labels=['horizontal_x','horizontal_y','vertical'])) + +# setup a tomo identity +Ibig = 1e5 * TomoIdentity(geometry=ig) +Ismall = 1e-5 * TomoIdentity(geometry=ig) +Iok = 1e1 * TomoIdentity(geometry=ig) + +# composite operator +Kbig = BlockOperator(A, Ibig, shape=(2,1)) +Ksmall = BlockOperator(A, Ismall, shape=(2,1)) +Kok = BlockOperator(A, Iok, shape=(2,1)) + +#out = K.direct(X_init) + +f = Norm2sq(Kbig,B) +f.L = 0.00003 + +fsmall = Norm2sq(Ksmall,B) +fsmall.L = 0.00003 + +fok = Norm2sq(Kok,B) +fok.L = 0.00003 + +simplef = Norm2sq(A, b) +simplef.L = 0.00003 + +gd = GradientDescent( x_init=x_init, objective_function=simplef, + rate=simplef.L) +gd.max_iteration = 50 + +Kbig.direct(X_init) +Kbig.adjoint(B) +cg = CGLS() +cg.set_up(X_init, Kbig, B ) +cg.max_iteration = 10 + +cgsmall = CGLS() +cgsmall.set_up(X_init, Ksmall, B ) +cgsmall.max_iteration = 10 + + +cgs = CGLS() +cgs.set_up(x_init, A, b ) +cgs.max_iteration = 10 + +cgok = CGLS() +cgok.set_up(X_init, Kok, B ) +cgok.max_iteration = 10 +# # +#out.__isub__(B) +#out2 = K.adjoint(out) + +#(2.0*self.c)*self.A.adjoint( self.A.direct(x) - self.b ) + +for _ in gd: + print ("iteration {} {}".format(gd.iteration, gd.get_last_loss())) + +cg.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val))) + +cgs.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val))) + +cgsmall.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val))) +cgsmall.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val))) +cgok.run(10, verbose=True) +# # for _ in cg: +# print ("iteration {} {}".format(cg.iteration, cg.get_current_loss())) +# # +# # fig = plt.figure() +# # plt.imshow(cg.get_output().get_item(0,0).subset(vertical=0).as_array()) +# # plt.title('Composite CGLS') +# # plt.show() +# # +# # for _ in cgs: +# print ("iteration {} {}".format(cgs.iteration, cgs.get_current_loss())) +# # +fig = plt.figure() +plt.subplot(2,3,1) +plt.imshow(Phantom.subset(vertical=0).as_array()) +plt.title('Simulated Phantom') +plt.subplot(2,3,2) +plt.imshow(gd.get_output().subset(vertical=0).as_array()) +plt.title('Simple Gradient Descent') +plt.subplot(2,3,3) +plt.imshow(cgs.get_output().subset(vertical=0).as_array()) +plt.title('Simple CGLS') +plt.subplot(2,3,5) +plt.imshow(cg.get_output().get_item(0).subset(vertical=0).as_array()) +plt.title('Composite CGLS\nbig lambda') +plt.subplot(2,3,6) +plt.imshow(cgsmall.get_output().get_item(0).subset(vertical=0).as_array()) +plt.title('Composite CGLS\nsmall lambda') +plt.subplot(2,3,4) +plt.imshow(cgok.get_output().get_item(0).subset(vertical=0).as_array()) +plt.title('Composite CGLS\nok lambda') +plt.show() diff --git a/Wrappers/Python/wip/CreatePhantom.py b/Wrappers/Python/wip/CreatePhantom.py new file mode 100644 index 0000000..4bf6ea4 --- /dev/null +++ b/Wrappers/Python/wip/CreatePhantom.py @@ -0,0 +1,242 @@ +import numpy +import tomophantom +from tomophantom import TomoP3D +from tomophantom.supp.artifacts import ArtifactsClass as Artifact +import os + +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import CGLS +from ccpi.plugins.ops import CCPiProjectorSimple +from ccpi.optimisation.ops import PowerMethodNonsquare +from ccpi.optimisation.ops import TomoIdentity +from ccpi.optimisation.funcs import Norm2sq, Norm1 +from ccpi.framework import ImageGeometry, AcquisitionGeometry, ImageData, AcquisitionData +from ccpi.optimisation.algorithms import GradientDescent +from ccpi.framework import BlockDataContainer +from ccpi.optimisation.operators import BlockOperator + + +model = 13 # select a model number from tomophantom library +N_size = 64 # Define phantom dimensions using a scalar value (cubic phantom) +path = os.path.dirname(tomophantom.__file__) +path_library3D = os.path.join(path, "Phantom3DLibrary.dat") + +#This will generate a N_size x N_size x N_size phantom (3D) +phantom_tm = TomoP3D.Model(model, N_size, path_library3D) + +# detector column count (horizontal) +detector_horiz = int(numpy.sqrt(2)*N_size) +# detector row count (vertical) (no reason for it to be > N) +detector_vert = N_size +# number of projection angles +angles_num = int(0.5*numpy.pi*N_size) +# angles are expressed in degrees +angles = numpy.linspace(0.0, 179.9, angles_num, dtype='float32') + + +acquisition_data_array = TomoP3D.ModelSino(model, N_size, + detector_horiz, detector_vert, + angles, + path_library3D) + +tomophantom_acquisition_axes_order = ['vertical', 'angle', 'horizontal'] + +artifacts = Artifact(acquisition_data_array) + + +tp_acq_data = AcquisitionData(artifacts.noise(0.2, 'Gaussian'), + dimension_labels=tomophantom_acquisition_axes_order) +#print ("size", acquisition_data.shape) +print ("horiz", detector_horiz) +print ("vert", detector_vert) +print ("angles", angles_num) + +tp_acq_geometry = AcquisitionGeometry(geom_type='parallel', dimension='3D', + angles=angles, + pixel_num_h=detector_horiz, + pixel_num_v=detector_vert, + channels=1, + ) + +acq_data = tp_acq_geometry.allocate() +#print (tp_acq_geometry) +print ("AcquisitionData", acq_data.shape) +print ("TomoPhantom", tp_acq_data.shape, tp_acq_data.dimension_labels) + +default_acquisition_axes_order = ['angle', 'vertical', 'horizontal'] + +acq_data2 = tp_acq_data.subset(dimensions=default_acquisition_axes_order) +print ("AcquisitionData", acq_data2.shape, acq_data2.dimension_labels) +print ("AcquisitionData {} TomoPhantom {}".format(id(acq_data2.as_array()), + id(acquisition_data_array))) + +fig = plt.figure() +plt.subplot(1,2,1) +plt.imshow(acquisition_data_array[20]) +plt.title('Sinogram') +plt.subplot(1,2,2) +plt.imshow(tp_acq_data.as_array()[20]) +plt.title('Sinogram + noise') +plt.show() + +# Set up Operator object combining the ImageGeometry and AcquisitionGeometry +# wrapping calls to CCPi projector. + +ig = ImageGeometry(voxel_num_x=detector_horiz, + voxel_num_y=detector_horiz, + voxel_num_z=detector_vert) +A = CCPiProjectorSimple(ig, tp_acq_geometry) +# Forward and backprojection are available as methods direct and adjoint. Here +# generate test data b and some noise + +#b = A.direct(Phantom) +b = acq_data2 + +#z = A.adjoint(b) + + +# Using the test data b, different reconstruction methods can now be set up as +# demonstrated in the rest of this file. In general all methods need an initial +# guess and some algorithm options to be set. Note that 100 iterations for +# some of the methods is a very low number and 1000 or 10000 iterations may be +# needed if one wants to obtain a converged solution. +x_init = ImageData(geometry=ig, + dimension_labels=['horizontal_x','horizontal_y','vertical']) +X_init = BlockDataContainer(x_init) +B = BlockDataContainer(b, + ImageData(geometry=ig, dimension_labels=['horizontal_x','horizontal_y','vertical'])) + +# setup a tomo identity +Ibig = 4e1 * TomoIdentity(geometry=ig) +Ismall = 1e-3 * TomoIdentity(geometry=ig) +Iok = 7.6e0 * TomoIdentity(geometry=ig) + +# composite operator +Kbig = BlockOperator(A, Ibig, shape=(2,1)) +Ksmall = BlockOperator(A, Ismall, shape=(2,1)) +Kok = BlockOperator(A, Iok, shape=(2,1)) + +#out = K.direct(X_init) +#x0 = x_init.copy() +#x0.fill(numpy.random.randn(*x0.shape)) +#lipschitz = PowerMethodNonsquare(A, 5, x0) +#print("lipschitz", lipschitz) + +#%% + +simplef = Norm2sq(A, b, memopt=False) +#simplef.L = lipschitz[0]/3000. +simplef.L = 0.00003 + +f = Norm2sq(Kbig,B) +f.L = 0.00003 + +fsmall = Norm2sq(Ksmall,B) +fsmall.L = 0.00003 + +fok = Norm2sq(Kok,B) +fok.L = 0.00003 + +print("setup gradient descent") +gd = GradientDescent( x_init=x_init, objective_function=simplef, + rate=simplef.L) +gd.max_iteration = 5 +simplef2 = Norm2sq(A, b, memopt=True) +#simplef.L = lipschitz[0]/3000. +simplef2.L = 0.00003 +print("setup gradient descent") +gd2 = GradientDescent( x_init=x_init, objective_function=simplef2, + rate=simplef2.L) +gd2.max_iteration = 5 + +Kbig.direct(X_init) +Kbig.adjoint(B) +print("setup CGLS") +cg = CGLS() +cg.set_up(X_init, Kbig, B ) +cg.max_iteration = 10 + +print("setup CGLS") +cgsmall = CGLS() +cgsmall.set_up(X_init, Ksmall, B ) +cgsmall.max_iteration = 10 + + +print("setup CGLS") +cgs = CGLS() +cgs.set_up(x_init, A, b ) +cgs.max_iteration = 10 + +print("setup CGLS") +cgok = CGLS() +cgok.set_up(X_init, Kok, B ) +cgok.max_iteration = 10 +# # +#out.__isub__(B) +#out2 = K.adjoint(out) + +#(2.0*self.c)*self.A.adjoint( self.A.direct(x) - self.b ) + + +for _ in gd: + print ("GradientDescent iteration {} {}".format(gd.iteration, gd.get_last_loss())) +#gd2.run(5,verbose=True) +print("CGLS block lambda big") +cg.run(10, lambda it,val: print ("CGLS big iteration {} objective {}".format(it,val))) + +print("CGLS standard") +cgs.run(10, lambda it,val: print ("CGLS standard iteration {} objective {}".format(it,val))) + +print("CGLS block lambda small") +cgsmall.run(10, lambda it,val: print ("CGLS small iteration {} objective {}".format(it,val))) +print("CGLS block lambdaok") +cgok.run(10, verbose=True) +# # for _ in cg: +# print ("iteration {} {}".format(cg.iteration, cg.get_current_loss())) +# # +# # fig = plt.figure() +# # plt.imshow(cg.get_output().get_item(0,0).subset(vertical=0).as_array()) +# # plt.title('Composite CGLS') +# # plt.show() +# # +# # for _ in cgs: +# print ("iteration {} {}".format(cgs.iteration, cgs.get_current_loss())) +# # +Phantom = ImageData(phantom_tm) + +theslice=40 + +fig = plt.figure() +plt.subplot(2,3,1) +plt.imshow(numpy.flip(Phantom.subset(vertical=theslice).as_array(),axis=0), cmap='gray') +plt.clim(0,0.7) +plt.title('Simulated Phantom') +plt.subplot(2,3,2) +plt.imshow(gd.get_output().subset(vertical=theslice).as_array(), cmap='gray') +plt.clim(0,0.7) +plt.title('Simple Gradient Descent') +plt.subplot(2,3,3) +plt.imshow(cgs.get_output().subset(vertical=theslice).as_array(), cmap='gray') +plt.clim(0,0.7) +plt.title('Simple CGLS') +plt.subplot(2,3,5) +plt.imshow(cg.get_output().get_item(0).subset(vertical=theslice).as_array(), cmap='gray') +plt.clim(0,0.7) +plt.title('Composite CGLS\nbig lambda') +plt.subplot(2,3,6) +plt.imshow(cgsmall.get_output().get_item(0).subset(vertical=theslice).as_array(), cmap='gray') +plt.clim(0,0.7) +plt.title('Composite CGLS\nsmall lambda') +plt.subplot(2,3,4) +plt.imshow(cgok.get_output().get_item(0).subset(vertical=theslice).as_array(), cmap='gray') +plt.clim(0,0.7) +plt.title('Composite CGLS\nok lambda') +plt.show() + + +#Ibig = 7e1 * TomoIdentity(geometry=ig) +#Kbig = BlockOperator(A, Ibig, shape=(2,1)) +#cg2 = CGLS(x_init=X_init, operator=Kbig, data=B) +#cg2.max_iteration = 10 +#cg2.run(10, verbose=True) diff --git a/Wrappers/Python/wip/Demos/FISTA_vs_CGLS.py b/Wrappers/Python/wip/Demos/FISTA_vs_CGLS.py new file mode 100644 index 0000000..2dcaa89 --- /dev/null +++ b/Wrappers/Python/wip/Demos/FISTA_vs_CGLS.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import FISTA, CGLS + +from ccpi.optimisation.operators import Gradient +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, FunctionOperatorComposition +from skimage.util import random_noise +from ccpi.astra.ops import AstraProjectorSimple + +#%% + +N = 75 +x = np.zeros((N,N)) +x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 + +data = ImageData(x) +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) + +detectors = N +angles = np.linspace(0, np.pi, N, dtype=np.float32) + +ag = AcquisitionGeometry('parallel','2D',angles, detectors) +Aop = AstraProjectorSimple(ig, ag, 'cpu') +sin = Aop.direct(data) + +noisy_data = sin + +fidelity = FunctionOperatorComposition(L2NormSquared(b=noisy_data), Aop) +regularizer = ZeroFunction() + +x_init = ig.allocate() + +## Setup and run the FISTA algorithm +opt = {'tol': 1e-4, 'memopt':True} +fista = FISTA(x_init=x_init , f=fidelity, g=regularizer, opt=opt) +fista.max_iteration = 500 +fista.update_objective_interval = 50 +fista.run(500, verbose=True) + +## Setup and run the CGLS algorithm +cgls = CGLS(x_init=x_init, operator=Aop, data=noisy_data) +cgls.max_iteration = 500 +cgls.update_objective_interval = 50 +cgls.run(500, verbose=True) + +diff = fista.get_output() - cgls.get_output() + + +#%% +print( diff.norm()) + +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(fista.get_output().as_array()) +plt.title('FISTA reconstruction') +plt.colorbar() +plt.subplot(3,1,2) +plt.imshow(cgls.get_output().as_array()) +plt.title('CGLS reconstruction') +plt.colorbar() +plt.subplot(3,1,3) +plt.imshow(diff.abs().as_array()) +plt.title('Difference reconstruction') +plt.colorbar() +plt.show() + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wrappers/Python/wip/Demos/FISTA_vs_PDHG.py b/Wrappers/Python/wip/Demos/FISTA_vs_PDHG.py new file mode 100644 index 0000000..b7777ef --- /dev/null +++ b/Wrappers/Python/wip/Demos/FISTA_vs_PDHG.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.framework import ImageData, ImageGeometry + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import FISTA, PDHG + +from ccpi.optimisation.operators import BlockOperator, Gradient, Identity +from ccpi.optimisation.functions import L2NormSquared, L1Norm, \ + MixedL21Norm, FunctionOperatorComposition, BlockFunction, ZeroFunction + +from skimage.util import random_noise + +# Create phantom for TV Gaussian denoising +N = 100 + +data = np.zeros((N,N)) +data[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +data[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 +data = ImageData(data) +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) +ag = ig + +# Create noisy data. Add Gaussian noise +n1 = random_noise(data.as_array(), mode = 's&p', salt_vs_pepper = 0.9, amount=0.2) +noisy_data = ImageData(n1) + +# Regularisation Parameter +alpha = 5 + +operator = Gradient(ig) + +fidelity = L1Norm(b=noisy_data) +regulariser = FunctionOperatorComposition(alpha * L2NormSquared(), operator) + +x_init = ig.allocate() + +## Setup and run the PDHG algorithm +opt = {'tol': 1e-4, 'memopt':True} +fista = FISTA(x_init=x_init , f=regulariser, g=fidelity, opt=opt) +fista.max_iteration = 2000 +fista.update_objective_interval = 50 +fista.run(2000, verbose=True) + +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(3,1,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.subplot(3,1,3) +plt.imshow(fista.get_output().as_array()) +plt.title('TV Reconstruction') +plt.colorbar() +plt.show() + +# Compare with PDHG +# Create operators +op1 = Gradient(ig) +op2 = Identity(ig, ag) + +# Create BlockOperator +operator = BlockOperator(op1, op2, shape=(2,1) ) +f = BlockFunction(alpha * L2NormSquared(), fidelity) +g = ZeroFunction() + +## Compute operator Norm +normK = operator.norm() +# +## Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) +# +# +## Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 50 +pdhg.run(2000) +# +#%% +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(fista.get_output().as_array()) +plt.title('FISTA') +plt.colorbar() +plt.subplot(3,1,2) +plt.imshow(pdhg.get_output().as_array()) +plt.title('PDHG') +plt.colorbar() +plt.subplot(3,1,3) +plt.imshow(np.abs(pdhg.get_output().as_array()-fista.get_output().as_array())) +plt.title('Diff FISTA-PDHG') +plt.colorbar() +plt.show() + + diff --git a/Wrappers/Python/wip/Demos/IMAT_Reconstruction/TV_WhiteBeam_reconstruction.py b/Wrappers/Python/wip/Demos/IMAT_Reconstruction/TV_WhiteBeam_reconstruction.py new file mode 100644 index 0000000..e67bdb1 --- /dev/null +++ b/Wrappers/Python/wip/Demos/IMAT_Reconstruction/TV_WhiteBeam_reconstruction.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.framework import ImageGeometry, AcquisitionGeometry, AcquisitionData +from astropy.io import fits +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Gradient +from ccpi.optimisation.functions import ZeroFunction, KullbackLeibler, L2NormSquared,\ + MixedL21Norm, BlockFunction + +from ccpi.astra.ops import AstraProjectorSimple + + +# load IMAT file +#filename_sino = '/media/newhd/shared/DataProcessed/IMAT_beamtime_Feb_2019/preprocessed_test_flat/sino/rebin_slice_350/sino_log_rebin_141.fits' +filename_sino = '/media/newhd/shared/DataProcessed/IMAT_beamtime_Feb_2019/preprocessed_test_flat/sino/rebin_slice_350/sino_log_rebin_564.fits' + +sino_handler = fits.open(filename_sino) +sino_tmp = numpy.array(sino_handler[0].data, dtype=float) +# reorder sino coordinate: channels, angles, detectors +sinogram = numpy.rollaxis(sino_tmp, 2) +sino_handler.close() +#%% +# white beam data +sinogram_wb = sinogram.sum(axis=0) + +pixh = sinogram_wb.shape[1] # detectors +pixv = sinogram_wb.shape[1] # detectors + +# WhiteBeam Geometry +igWB = ImageGeometry(voxel_num_x = pixh, voxel_num_y = pixv) + +# Load Golden angles +with open("golden_angles.txt") as f: + angles_string = [line.rstrip() for line in f] + angles = numpy.array(angles_string).astype(float) +agWB = AcquisitionGeometry('parallel', '2D', angles * numpy.pi / 180, pixh) +op_WB = AstraProjectorSimple(igWB, agWB, 'gpu') +sinogram_aqdata = AcquisitionData(sinogram_wb, agWB) + +# BackProjection +result_bp = op_WB.adjoint(sinogram_aqdata) + +plt.imshow(result_bp.subset(channel=50).array) +plt.title('BackProjection') +plt.show() + + + +#%% + +# Regularisation Parameter +alpha = 2000 + +# Create operators +op1 = Gradient(igWB) +op2 = op_WB + +# Create BlockOperator +operator = BlockOperator(op1, op2, shape=(2,1) ) + +# Create functions + +f1 = alpha * MixedL21Norm() +f2 = KullbackLeibler(sinogram_aqdata) +#f2 = L2NormSquared(b = sinogram_aqdata) +f = BlockFunction(f1, f2) + +g = ZeroFunction() + +diag_precon = False + +if diag_precon: + + def tau_sigma_precond(operator): + + tau = 1/operator.sum_abs_row() + sigma = 1/ operator.sum_abs_col() + + return tau, sigma + + tau, sigma = tau_sigma_precond(operator) + +else: + # Compute operator Norm + normK = operator.norm() + print ("normK", normK) + # Primal & dual stepsizes + sigma = 0.1 + tau = 1/(sigma*normK**2) + +#%% + + +## Primal & dual stepsizes +#sigma = 0.1 +#tau = 1/(sigma*normK**2) +# +# +## Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 10000 +pdhg.update_objective_interval = 500 + +def circ_mask(h, w, center=None, radius=None): + + if center is None: # use the middle of the image + center = [int(w/2), int(h/2)] + if radius is None: # use the smallest distance between the center and image walls + radius = min(center[0], center[1], w-center[0], h-center[1]) + + Y, X = numpy.ogrid[:h, :w] + dist_from_center = numpy.sqrt((X - center[0])**2 + (Y-center[1])**2) + + mask = dist_from_center <= radius + return mask + +def show_result(niter, objective, solution): + + mask = circ_mask(pixh, pixv, center=None, radius = 220) # 55 with 141, + plt.imshow(solution.as_array() * mask) + plt.colorbar() + plt.title("Iter: {}".format(niter)) + plt.show() + + + print( "{:04}/{:04} {:<5} {:.4f} {:<5} {:.4f} {:<5} {:.4f}".\ + format(niter, pdhg.max_iteration,'', \ + objective[0],'',\ + objective[1],'',\ + objective[2])) + +pdhg.run(10000, callback = show_result) + +#%% + +mask = circ_mask(pixh, pixv, center=None, radius = 210) # 55 with 141, +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(pdhg.get_output().as_array() * mask) +plt.title('Ground Truth') +plt.colorbar() +plt.show() diff --git a/Wrappers/Python/wip/Demos/IMAT_Reconstruction/golden_angles.txt b/Wrappers/Python/wip/Demos/IMAT_Reconstruction/golden_angles.txt new file mode 100644 index 0000000..95ce73a --- /dev/null +++ b/Wrappers/Python/wip/Demos/IMAT_Reconstruction/golden_angles.txt @@ -0,0 +1,186 @@ +0 +0.9045 +1.809 +2.368 +3.2725 +4.736 +5.6405 +6.1995 +7.104 +8.5675 +9.472 +10.9356 +11.8401 +12.3991 +13.3036 +14.7671 +15.6716 +16.2306 +17.1351 +18.0396 +18.5986 +19.5031 +20.9666 +21.8711 +22.4301 +23.3346 +24.7981 +25.7026 +27.1661 +28.0706 +28.6297 +29.5342 +30.9977 +31.9022 +32.4612 +33.3657 +34.8292 +35.7337 +37.1972 +38.1017 +38.6607 +39.5652 +41.0287 +41.9332 +42.4922 +43.3967 +44.3012 +44.8602 +45.7647 +47.2283 +48.1328 +48.6918 +49.5963 +51.0598 +51.9643 +53.4278 +54.3323 +54.8913 +55.7958 +57.2593 +58.1638 +58.7228 +59.6273 +60.5318 +61.0908 +61.9953 +63.4588 +64.3633 +64.9224 +65.8269 +67.2904 +68.1949 +69.6584 +70.5629 +71.1219 +72.0264 +73.4899 +74.3944 +74.9534 +75.8579 +77.3214 +78.2259 +79.6894 +80.5939 +81.1529 +82.0574 +83.521 +84.4255 +84.9845 +85.889 +86.7935 +87.3525 +88.257 +89.7205 +90.625 +91.184 +92.0885 +93.552 +94.4565 +95.92 +96.8245 +97.3835 +98.288 +99.7516 +100.656 +101.215 +102.12 +103.583 +104.488 +105.951 +106.856 +107.415 +108.319 +109.783 +110.687 +111.246 +112.151 +113.055 +113.614 +114.519 +115.982 +116.887 +117.446 +118.35 +119.814 +120.718 +122.182 +123.086 +123.645 +124.55 +126.013 +126.918 +127.477 +128.381 +129.286 +129.845 +130.749 +132.213 +133.117 +133.676 +134.581 +136.044 +136.949 +138.412 +139.317 +139.876 +140.78 +142.244 +143.148 +143.707 +144.612 +146.075 +146.98 +148.443 +149.348 +149.907 +150.811 +152.275 +153.179 +153.738 +154.643 +155.547 +156.106 +157.011 +158.474 +159.379 +159.938 +160.842 +162.306 +163.21 +164.674 +165.578 +166.137 +167.042 +168.505 +169.41 +169.969 +170.873 +172.337 +173.242 +174.705 +175.609 +176.168 +177.073 +178.536 +179.441 diff --git a/Wrappers/Python/wip/Demos/LeastSq_CGLS_FISTA_PDHG.py b/Wrappers/Python/wip/Demos/LeastSq_CGLS_FISTA_PDHG.py new file mode 100644 index 0000000..97c71ba --- /dev/null +++ b/Wrappers/Python/wip/Demos/LeastSq_CGLS_FISTA_PDHG.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG, CGLS, FISTA + +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, FunctionOperatorComposition +from ccpi.astra.ops import AstraProjectorSimple + +#%% + +N = 68 +x = np.zeros((N,N)) +x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 + +data = ImageData(x) +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) + +detectors = N +angles = np.linspace(0, np.pi, N, dtype=np.float32) + +ag = AcquisitionGeometry('parallel','2D',angles, detectors) +Aop = AstraProjectorSimple(ig, ag, 'cpu') +sin = Aop.direct(data) + +noisy_data = sin + + +#%% +############################################################################### +## Setup and run the CGLS algorithm + +x_init = ig.allocate() +cgls = CGLS(x_init=x_init, operator=Aop, data=noisy_data) +cgls.max_iteration = 500 +cgls.update_objective_interval = 50 +cgls.run(500, verbose=True) + +#%% +plt.imshow(cgls.get_output().as_array()) +#%% +############################################################################### +## Setup and run the PDHG algorithm + +operator = Aop +f = L2NormSquared(b = noisy_data) +g = ZeroFunction() + +## Compute operator Norm +normK = operator.norm() + +## Primal & dual stepsizes +sigma = 0.1 +tau = 1/(sigma*normK**2) + +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 50 +pdhg.run(2000) + + +#%% +############################################################################### +## Setup and run the FISTA algorithm + +fidelity = FunctionOperatorComposition(L2NormSquared(b=noisy_data), Aop) +regularizer = ZeroFunction() + +## Setup and run the FISTA algorithm +opt = {'memopt':True} +fista = FISTA(x_init=x_init , f=fidelity, g=regularizer, opt=opt) +fista.max_iteration = 2000 +fista.update_objective_interval = 200 +fista.run(2000, verbose=True) + +#%% Show results + +diff1 = pdhg.get_output() - cgls.get_output() +diff2 = fista.get_output() - cgls.get_output() + +print( diff1.norm()) +print( diff2.norm()) + +plt.figure(figsize=(10,10)) +plt.subplot(2,3,1) +plt.imshow(cgls.get_output().as_array()) +plt.title('CGLS reconstruction') +plt.subplot(2,3,2) +plt.imshow(pdhg.get_output().as_array()) +plt.title('PDHG reconstruction') +plt.subplot(2,3,3) +plt.imshow(fista.get_output().as_array()) +plt.title('FISTA reconstruction') +plt.subplot(2,3,4) +plt.imshow(diff1.abs().as_array()) +plt.title('Diff PDHG vs CGLS') +plt.colorbar() +plt.subplot(2,3,5) +plt.imshow(diff2.abs().as_array()) +plt.title('Diff FISTA vs CGLS') +plt.colorbar() +plt.show() + + + + + + + + + + + + + + + + + + + + + + +# +# +# +# +# +# +# +# diff --git a/Wrappers/Python/wip/Demos/PDHG_TGV_Denoising_SaltPepper.py b/Wrappers/Python/wip/Demos/PDHG_TGV_Denoising_SaltPepper.py new file mode 100644 index 0000000..7b65c31 --- /dev/null +++ b/Wrappers/Python/wip/Demos/PDHG_TGV_Denoising_SaltPepper.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Feb 22 14:53:03 2019 + +@author: evangelos +""" + +from ccpi.framework import ImageData, ImageGeometry + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Identity, \ + Gradient, SymmetrizedGradient, ZeroOperator +from ccpi.optimisation.functions import ZeroFunction, L1Norm, \ + MixedL21Norm, BlockFunction + +from skimage.util import random_noise + +# Create phantom for TGV SaltPepper denoising + +N = 100 + +data = np.zeros((N,N)) + +x1 = np.linspace(0, int(N/2), N) +x2 = np.linspace(int(N/2), 0., N) +xv, yv = np.meshgrid(x1, x2) + +xv[int(N/4):int(3*N/4)-1, int(N/4):int(3*N/4)-1] = yv[int(N/4):int(3*N/4)-1, int(N/4):int(3*N/4)-1].T + +data = xv +data = ImageData(data/data.max()) + +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) +ag = ig + +# Create noisy data. Add Gaussian noise +n1 = random_noise(data.as_array(), mode = 's&p', salt_vs_pepper = 0.9, amount=0.2) +noisy_data = ImageData(n1) + +# Regularisation Parameters +alpha = 0.8 +beta = numpy.sqrt(2)* alpha + +method = '1' + +if method == '0': + + # Create operators + op11 = Gradient(ig) + op12 = Identity(op11.range_geometry()) + + op22 = SymmetrizedGradient(op11.domain_geometry()) + op21 = ZeroOperator(ig, op22.range_geometry()) + + op31 = Identity(ig, ag) + op32 = ZeroOperator(op22.domain_geometry(), ag) + + operator = BlockOperator(op11, -1*op12, op21, op22, op31, op32, shape=(3,2) ) + + f1 = alpha * MixedL21Norm() + f2 = beta * MixedL21Norm() + f3 = L1Norm(b=noisy_data) + f = BlockFunction(f1, f2, f3) + g = ZeroFunction() + +else: + + # Create operators + op11 = Gradient(ig) + op12 = Identity(op11.range_geometry()) + op22 = SymmetrizedGradient(op11.domain_geometry()) + op21 = ZeroOperator(ig, op22.range_geometry()) + + operator = BlockOperator(op11, -1*op12, op21, op22, shape=(2,2) ) + + f1 = alpha * MixedL21Norm() + f2 = beta * MixedL21Norm() + + f = BlockFunction(f1, f2) + g = BlockFunction(L1Norm(b=noisy_data), ZeroFunction()) + +## Compute operator Norm +normK = operator.norm() +# +# Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) + + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 50 +pdhg.run(2000) + +#%% +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(3,1,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.subplot(3,1,3) +plt.imshow(pdhg.get_output()[0].as_array()) +plt.title('TGV Reconstruction') +plt.colorbar() +plt.show() +## +plt.plot(np.linspace(0,N,N), data.as_array()[int(N/2),:], label = 'GTruth') +plt.plot(np.linspace(0,N,N), pdhg.get_output()[0].as_array()[int(N/2),:], label = 'TV reconstruction') +plt.legend() +plt.title('Middle Line Profiles') +plt.show() + + +#%% Check with CVX solution + +from ccpi.optimisation.operators import SparseFiniteDiff + +try: + from cvxpy import * + cvx_not_installable = True +except ImportError: + cvx_not_installable = False + +if cvx_not_installable: + + u = Variable(ig.shape) + w1 = Variable((N, N)) + w2 = Variable((N, N)) + + # create TGV regulariser + DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann') + DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann') + + regulariser = alpha * sum(norm(vstack([DX.matrix() * vec(u) - vec(w1), \ + DY.matrix() * vec(u) - vec(w2)]), 2, axis = 0)) + \ + beta * sum(norm(vstack([ DX.matrix().transpose() * vec(w1), DY.matrix().transpose() * vec(w2), \ + 0.5 * ( DX.matrix().transpose() * vec(w2) + DY.matrix().transpose() * vec(w1) ), \ + 0.5 * ( DX.matrix().transpose() * vec(w2) + DY.matrix().transpose() * vec(w1) ) ]), 2, axis = 0 ) ) + + constraints = [] + fidelity = pnorm(u - noisy_data.as_array(),1) + solver = MOSEK + + # choose solver + if 'MOSEK' in installed_solvers(): + solver = MOSEK + else: + solver = SCS + + obj = Minimize( regulariser + fidelity) + prob = Problem(obj) + result = prob.solve(verbose = True, solver = solver) + + diff_cvx = numpy.abs( pdhg.get_output()[0].as_array() - u.value ) + + plt.figure(figsize=(15,15)) + plt.subplot(3,1,1) + plt.imshow(pdhg.get_output()[0].as_array()) + plt.title('PDHG solution') + plt.colorbar() + plt.subplot(3,1,2) + plt.imshow(u.value) + plt.title('CVX solution') + plt.colorbar() + plt.subplot(3,1,3) + plt.imshow(diff_cvx) + plt.title('Difference') + plt.colorbar() + plt.show() + + plt.plot(np.linspace(0,N,N), pdhg.get_output()[0].as_array()[int(N/2),:], label = 'PDHG') + plt.plot(np.linspace(0,N,N), u.value[int(N/2),:], label = 'CVX') + plt.legend() + plt.title('Middle Line Profiles') + plt.show() + + print('Primal Objective (CVX) {} '.format(obj.value)) + print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0])) + + + + + diff --git a/Wrappers/Python/wip/Demos/PDHG_TV_Denoising_Gaussian_DiagPrecond.py b/Wrappers/Python/wip/Demos/PDHG_TV_Denoising_Gaussian_DiagPrecond.py new file mode 100644 index 0000000..d65478c --- /dev/null +++ b/Wrappers/Python/wip/Demos/PDHG_TV_Denoising_Gaussian_DiagPrecond.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + + +""" +Total Variation Denoising using PDHG algorithm + +Problem: min_x \alpha * ||\nabla x||_{1} + || x - g ||_{2}^{2} + + \nabla: Gradient operator + g: Noisy Data with Gaussian Noise + \alpha: Regularization parameter + +""" + +from ccpi.framework import ImageData, ImageGeometry + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Identity, Gradient +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \ + MixedL21Norm, BlockFunction + + +# Create phantom for TV Gaussian denoising +N = 400 + +data = np.zeros((N,N)) +data[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +data[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 +data = ImageData(data) +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) +ag = ig + +# Create noisy data. Add Gaussian noise +np.random.seed(10) +noisy_data = ImageData( data.as_array() + np.random.normal(0, 0.05, size=ig.shape) ) + +# Regularisation Parameter +alpha = 2 + +method = '1' + +if method == '0': + + # Create operators + op1 = Gradient(ig) + op2 = Identity(ig, ag) + + # Create BlockOperator + operator = BlockOperator(op1, op2, shape=(2,1) ) + + # Create functions + + f1 = alpha * MixedL21Norm() + f2 = 0.5 * L2NormSquared(b = noisy_data) + f = BlockFunction(f1, f2) + + g = ZeroFunction() + +else: + + # Without the "Block Framework" + operator = Gradient(ig) + f = alpha * MixedL21Norm() + g = 0.5 * L2NormSquared(b = noisy_data) + + +diag_precon = False + + +if diag_precon: + + def tau_sigma_precond(operator): + + tau = 1/operator.sum_abs_col() + sigma = 1/operator.sum_abs_row() + + sigma[0].as_array()[sigma[0].as_array()==np.inf]=0 + sigma[1].as_array()[sigma[1].as_array()==np.inf]=0 + + return tau, sigma + + tau, sigma = tau_sigma_precond(operator) + +else: + # Compute operator Norm + normK = operator.norm() + + # Primal & dual stepsizes + sigma = 1 + tau = 1/(sigma*normK**2) + + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 3000 +pdhg.update_objective_interval = 200 +pdhg.run(3000, verbose=False) + +#%% +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(3,1,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.subplot(3,1,3) +plt.imshow(pdhg.get_output().as_array()) +plt.title('TV Reconstruction') +plt.colorbar() +plt.show() +## +plt.plot(np.linspace(0,N,N), data.as_array()[int(N/2),:], label = 'GTruth') +plt.plot(np.linspace(0,N,N), pdhg.get_output().as_array()[int(N/2),:], label = 'TV reconstruction') +plt.legend() +plt.title('Middle Line Profiles') +plt.show() + + +#%% Check with CVX solution + +from ccpi.optimisation.operators import SparseFiniteDiff + +try: + from cvxpy import * + cvx_not_installable = True +except ImportError: + cvx_not_installable = False + + +if cvx_not_installable: + + ##Construct problem + u = Variable(ig.shape) + + DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann') + DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann') + + # Define Total Variation as a regulariser + regulariser = alpha * sum(norm(vstack([DX.matrix() * vec(u), DY.matrix() * vec(u)]), 2, axis = 0)) + fidelity = 0.5 * sum_squares(u - noisy_data.as_array()) + + # choose solver + if 'MOSEK' in installed_solvers(): + solver = MOSEK + else: + solver = SCS + + obj = Minimize( regulariser + fidelity) + prob = Problem(obj) + result = prob.solve(verbose = True, solver = MOSEK) + + diff_cvx = numpy.abs( pdhg.get_output().as_array() - u.value ) + + plt.figure(figsize=(15,15)) + plt.subplot(3,1,1) + plt.imshow(pdhg.get_output().as_array()) + plt.title('PDHG solution') + plt.colorbar() + plt.subplot(3,1,2) + plt.imshow(u.value) + plt.title('CVX solution') + plt.colorbar() + plt.subplot(3,1,3) + plt.imshow(diff_cvx) + plt.title('Difference') + plt.colorbar() + plt.show() + + plt.plot(np.linspace(0,N,N), pdhg.get_output().as_array()[int(N/2),:], label = 'PDHG') + plt.plot(np.linspace(0,N,N), u.value[int(N/2),:], label = 'CVX') + plt.plot(np.linspace(0,N,N), data.as_array()[int(N/2),:], label = 'Truth') + + plt.legend() + plt.title('Middle Line Profiles') + plt.show() + + print('Primal Objective (CVX) {} '.format(obj.value)) + print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0])) + + + + + diff --git a/Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D.py b/Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D.py new file mode 100644 index 0000000..87d5328 --- /dev/null +++ b/Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Identity, Gradient +from ccpi.optimisation.functions import ZeroFunction, KullbackLeibler, \ + MixedL21Norm, BlockFunction + +from ccpi.astra.ops import AstraProjectorSimple + +""" + +Total Variation Denoising using PDHG algorithm: + + min_{x} max_{y} < K x, y > + g(x) - f^{*}(y) + + +Problem: min_x, x>0 \alpha * ||\nabla x||_{1} + int A x -g log(Ax + \eta) + + \nabla: Gradient operator + + A: Projection Matrix + g: Noisy sinogram corrupted with Poisson Noise + + \eta: Background Noise + \alpha: Regularization parameter + +""" + +# Create phantom for TV 2D tomography +N = 75 +x = np.zeros((N,N)) +x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 + +data = ImageData(x) +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) + +detectors = N +angles = np.linspace(0, np.pi, N, dtype=np.float32) + +ag = AcquisitionGeometry('parallel','2D',angles, detectors) +Aop = AstraProjectorSimple(ig, ag, 'cpu') +sin = Aop.direct(data) + +# Create noisy data. Apply Poisson noise +scale = 2 +n1 = scale * np.random.poisson(sin.as_array()/scale) +noisy_data = AcquisitionData(n1, ag) + +# Regularisation Parameter +alpha = 5 + +# Create operators +op1 = Gradient(ig) +op2 = Aop + +# Create BlockOperator +operator = BlockOperator(op1, op2, shape=(2,1) ) + +# Create functions + +f1 = alpha * MixedL21Norm() +f2 = KullbackLeibler(noisy_data) +f = BlockFunction(f1, f2) + +g = ZeroFunction() + +diag_precon = True + +if diag_precon: + + def tau_sigma_precond(operator): + + tau = 1/operator.sum_abs_row() + sigma = 1/ operator.sum_abs_col() + + return tau, sigma + + tau, sigma = tau_sigma_precond(operator) + +else: + # Compute operator Norm + normK = operator.norm() + # Primal & dual stepsizes + sigma = 10 + tau = 1/(sigma*normK**2) + + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 50 +pdhg.run(2000) + + +#%% +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(3,1,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.subplot(3,1,3) +plt.imshow(pdhg.get_output().as_array()) +plt.title('TV Reconstruction') +plt.colorbar() +plt.show() +## +plt.plot(np.linspace(0,N,N), data.as_array()[int(N/2),:], label = 'GTruth') +plt.plot(np.linspace(0,N,N), pdhg.get_output().as_array()[int(N/2),:], label = 'TV reconstruction') +plt.legend() +plt.title('Middle Line Profiles') +plt.show() + + +#%% Check with CVX solution + +from ccpi.optimisation.operators import SparseFiniteDiff +import astra +import numpy + +try: + from cvxpy import * + cvx_not_installable = True +except ImportError: + cvx_not_installable = False + + +if cvx_not_installable: + + + ##Construct problem + u = Variable(N*N) + #q = Variable() + + DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann') + DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann') + + regulariser = alpha * sum(norm(vstack([DX.matrix() * vec(u), DY.matrix() * vec(u)]), 2, axis = 0)) + + # create matrix representation for Astra operator + + vol_geom = astra.create_vol_geom(N, N) + proj_geom = astra.create_proj_geom('parallel', 1.0, detectors, angles) + + proj_id = astra.create_projector('strip', proj_geom, vol_geom) + + matrix_id = astra.projector.matrix(proj_id) + + ProjMat = astra.matrix.get(matrix_id) + + fidelity = sum( ProjMat * u - noisy_data.as_array().ravel() * log(ProjMat * u)) + #constraints = [q>= fidelity, u>=0] + constraints = [u>=0] + + solver = SCS + obj = Minimize( regulariser + fidelity) + prob = Problem(obj, constraints) + result = prob.solve(verbose = True, solver = solver) + + +##%% Check with CVX solution + +from ccpi.optimisation.operators import SparseFiniteDiff + +try: + from cvxpy import * + cvx_not_installable = True +except ImportError: + cvx_not_installable = False + + +if cvx_not_installable: + + ##Construct problem + u = Variable(ig.shape) + + DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann') + DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann') + + # Define Total Variation as a regulariser + regulariser = alpha * sum(norm(vstack([DX.matrix() * vec(u), DY.matrix() * vec(u)]), 2, axis = 0)) + fidelity = pnorm( u - noisy_data.as_array(),1) + + # choose solver + if 'MOSEK' in installed_solvers(): + solver = MOSEK + else: + solver = SCS + + obj = Minimize( regulariser + fidelity) + prob = Problem(obj) + result = prob.solve(verbose = True, solver = solver) + + + plt.figure(figsize=(15,15)) + plt.subplot(3,1,1) + plt.imshow(pdhg.get_output().as_array()) + plt.title('PDHG solution') + plt.colorbar() + plt.subplot(3,1,2) + plt.imshow(np.reshape(u.value, (N, N))) + plt.title('CVX solution') + plt.colorbar() + plt.subplot(3,1,3) + plt.imshow(diff_cvx) + plt.title('Difference') + plt.colorbar() + plt.show() + + plt.plot(np.linspace(0,N,N), pdhg.get_output().as_array()[int(N/2),:], label = 'PDHG') + plt.plot(np.linspace(0,N,N), u.value[int(N/2),:], label = 'CVX') + plt.legend() + plt.title('Middle Line Profiles') + plt.show() + + print('Primal Objective (CVX) {} '.format(obj.value)) + print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0]))
\ No newline at end of file diff --git a/Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D_time.py b/Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D_time.py new file mode 100644 index 0000000..045458a --- /dev/null +++ b/Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D_time.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Gradient +from ccpi.optimisation.functions import ZeroFunction, KullbackLeibler, \ + MixedL21Norm, BlockFunction + +from ccpi.astra.ops import AstraProjectorMC + +import os +import tomophantom +from tomophantom import TomoP2D + +# Create phantom for TV 2D dynamic tomography + +model = 102 # note that the selected model is temporal (2D + time) +N = 50 # set dimension of the phantom +# one can specify an exact path to the parameters file +# path_library2D = '../../../PhantomLibrary/models/Phantom2DLibrary.dat' +path = os.path.dirname(tomophantom.__file__) +path_library2D = os.path.join(path, "Phantom2DLibrary.dat") +#This will generate a N_size x N_size x Time frames phantom (2D + time) +phantom_2Dt = TomoP2D.ModelTemporal(model, N, path_library2D) + +plt.close('all') +plt.figure(1) +plt.rcParams.update({'font.size': 21}) +plt.title('{}''{}'.format('2D+t phantom using model no.',model)) +for sl in range(0,np.shape(phantom_2Dt)[0]): + im = phantom_2Dt[sl,:,:] + plt.imshow(im, vmin=0, vmax=1) + plt.pause(.1) + plt.draw + + +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N, channels = np.shape(phantom_2Dt)[0]) +data = ImageData(phantom_2Dt, geometry=ig) + +detectors = N +angles = np.linspace(0,np.pi,N) + +ag = AcquisitionGeometry('parallel','2D', angles, detectors, channels = np.shape(phantom_2Dt)[0]) +Aop = AstraProjectorMC(ig, ag, 'gpu') +sin = Aop.direct(data) + +scale = 2 +n1 = scale * np.random.poisson(sin.as_array()/scale) +noisy_data = AcquisitionData(n1, ag) + +tindex = [3, 6, 10] + +fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 10)) +plt.subplot(1,3,1) +plt.imshow(noisy_data.as_array()[tindex[0],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[0])) +plt.subplot(1,3,2) +plt.imshow(noisy_data.as_array()[tindex[1],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[1])) +plt.subplot(1,3,3) +plt.imshow(noisy_data.as_array()[tindex[2],:,:]) +plt.axis('off') +plt.title('Time {}'.format(tindex[2])) + +fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8, + wspace=0.02, hspace=0.02) + +plt.show() + +#%% +# Regularisation Parameter +alpha = 5 + +# Create operators +#op1 = Gradient(ig) +op1 = Gradient(ig, correlation='SpaceChannels') +op2 = Aop + +# Create BlockOperator +operator = BlockOperator(op1, op2, shape=(2,1) ) + +# Create functions + +f1 = alpha * MixedL21Norm() +f2 = KullbackLeibler(noisy_data) +f = BlockFunction(f1, f2) + +g = ZeroFunction() + +# Compute operator Norm +normK = operator.norm() + +# Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) + + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 200 +pdhg.run(2000) + + +#%% +fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8)) + +plt.subplot(2,3,1) +plt.imshow(phantom_2Dt[tindex[0],:,:],vmin=0, vmax=1) +plt.axis('off') +plt.title('Time {}'.format(tindex[0])) + +plt.subplot(2,3,2) +plt.imshow(phantom_2Dt[tindex[1],:,:],vmin=0, vmax=1) +plt.axis('off') +plt.title('Time {}'.format(tindex[1])) + +plt.subplot(2,3,3) +plt.imshow(phantom_2Dt[tindex[2],:,:],vmin=0, vmax=1) +plt.axis('off') +plt.title('Time {}'.format(tindex[2])) + + +plt.subplot(2,3,4) +plt.imshow(pdhg.get_output().as_array()[tindex[0],:,:]) +plt.axis('off') +plt.subplot(2,3,5) +plt.imshow(pdhg.get_output().as_array()[tindex[1],:,:]) +plt.axis('off') +plt.subplot(2,3,6) +plt.imshow(pdhg.get_output().as_array()[tindex[2],:,:]) +plt.axis('off') +im = plt.imshow(pdhg.get_output().as_array()[tindex[0],:,:]) + + +fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8, + wspace=0.02, hspace=0.02) + +cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8]) +cbar = fig.colorbar(im, cax=cb_ax) + + +plt.show() + diff --git a/Wrappers/Python/wip/Demos/PDHG_Tikhonov_Tomo2D.py b/Wrappers/Python/wip/Demos/PDHG_Tikhonov_Tomo2D.py new file mode 100644 index 0000000..f17c4fe --- /dev/null +++ b/Wrappers/Python/wip/Demos/PDHG_Tikhonov_Tomo2D.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Gradient +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, BlockFunction +from skimage.util import random_noise +from ccpi.astra.ops import AstraProjectorSimple + +# Create phantom for TV 2D tomography +N = 75 +x = np.zeros((N,N)) +x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 + +data = ImageData(x) +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) + +detectors = N +angles = np.linspace(0, np.pi, N, dtype=np.float32) + +ag = AcquisitionGeometry('parallel','2D',angles, detectors) +Aop = AstraProjectorSimple(ig, ag, 'gpu') +sin = Aop.direct(data) + +# Create noisy data. Apply Gaussian noise + +np.random.seed(10) +noisy_data = sin + AcquisitionData(np.random.normal(0, 3, sin.shape)) + +# Regularisation Parameter +alpha = 500 + +# Create operators +op1 = Gradient(ig) +op2 = Aop + +# Create BlockOperator +operator = BlockOperator(op1, op2, shape=(2,1) ) + +# Create functions + +f1 = alpha * L2NormSquared() +f2 = 0.5 * L2NormSquared(b=noisy_data) +f = BlockFunction(f1, f2) + +g = ZeroFunction() + +# Compute operator Norm +normK = operator.norm() + +# Primal & dual stepsizes +sigma = 1 +tau = 1/(sigma*normK**2) + + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 5000 +pdhg.update_objective_interval = 50 +pdhg.run(2000) + +#%% +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') +plt.colorbar() +plt.subplot(3,1,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') +plt.colorbar() +plt.subplot(3,1,3) +plt.imshow(pdhg.get_output().as_array()) +plt.title('Tikhonov Reconstruction') +plt.colorbar() +plt.show() +## +plt.plot(np.linspace(0,N,N), data.as_array()[int(N/2),:], label = 'GTruth') +plt.plot(np.linspace(0,N,N), pdhg.get_output().as_array()[int(N/2),:], label = 'Tikhonov reconstruction') +plt.legend() +plt.title('Middle Line Profiles') +plt.show() + + diff --git a/Wrappers/Python/wip/Demos/PDHG_vs_CGLS.py b/Wrappers/Python/wip/Demos/PDHG_vs_CGLS.py new file mode 100644 index 0000000..3155654 --- /dev/null +++ b/Wrappers/Python/wip/Demos/PDHG_vs_CGLS.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG, CGLS + +from ccpi.optimisation.operators import Gradient +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, FunctionOperatorComposition +from skimage.util import random_noise +from ccpi.astra.ops import AstraProjectorSimple + +#%% + +N = 128 +x = np.zeros((N,N)) +x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 + +data = ImageData(x) +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) + +detectors = N +angles = np.linspace(0, np.pi, N, dtype=np.float32) + +ag = AcquisitionGeometry('parallel','2D',angles, detectors) +Aop = AstraProjectorSimple(ig, ag, 'cpu') +sin = Aop.direct(data) + +noisy_data = sin + +x_init = ig.allocate() + +## Setup and run the CGLS algorithm +cgls = CGLS(x_init=x_init, operator=Aop, data=noisy_data) +cgls.max_iteration = 500 +cgls.update_objective_interval = 50 +cgls.run(500, verbose=True) + +# Create BlockOperator +operator = Aop +f = 0.5 * L2NormSquared(b = noisy_data) +g = ZeroFunction() + +## Compute operator Norm +normK = operator.norm() + +## Primal & dual stepsizes +sigma = 0.1 +tau = 1/(sigma*normK**2) +# +# +## Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 2000 +pdhg.update_objective_interval = 50 +pdhg.run(2000) + +#%% + +diff = pdhg.get_output() - cgls.get_output() +print( diff.norm()) +# +plt.figure(figsize=(15,15)) +plt.subplot(3,1,1) +plt.imshow(pdhg.get_output().as_array()) +plt.title('PDHG reconstruction') +plt.colorbar() +plt.subplot(3,1,2) +plt.imshow(cgls.get_output().as_array()) +plt.title('CGLS reconstruction') +plt.colorbar() +plt.subplot(3,1,3) +plt.imshow(diff.abs().as_array()) +plt.title('Difference reconstruction') +plt.colorbar() +plt.show() + + + + + + + + + + + + + + + + + + + + + + +# +# +# +# +# +# +# +# diff --git a/Wrappers/Python/wip/Demos/check_blockOperator_sum_row_cols.py b/Wrappers/Python/wip/Demos/check_blockOperator_sum_row_cols.py new file mode 100644 index 0000000..bdb2c38 --- /dev/null +++ b/Wrappers/Python/wip/Demos/check_blockOperator_sum_row_cols.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri May 3 13:10:09 2019 + +@author: evangelos +""" + +from ccpi.optimisation.operators import FiniteDiff, SparseFiniteDiff, BlockOperator, Gradient +from ccpi.framework import ImageGeometry, AcquisitionGeometry, BlockDataContainer, ImageData +from ccpi.astra.ops import AstraProjectorSimple + +from scipy import sparse +import numpy as np + +N = 3 +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) +u = ig.allocate('random_int') + +# Compare FiniteDiff with SparseFiniteDiff + +DY = FiniteDiff(ig, direction = 0, bnd_cond = 'Neumann') +DX = FiniteDiff(ig, direction = 1, bnd_cond = 'Neumann') + +DXu = DX.direct(u) +DYu = DY.direct(u) + +DX_sparse = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann') +DY_sparse = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann') + +DXu_sparse = DX_sparse.direct(u) +DYu_sparse = DY_sparse.direct(u) + +#np.testing.assert_array_almost_equal(DYu.as_array(), DYu_sparse.as_array(), decimal=4) +#np.testing.assert_array_almost_equal(DXu.as_array(), DXu_sparse.as_array(), decimal=4) + +#%% Tau/ Sigma + +A1 = DY_sparse.matrix() +A2 = DX_sparse.matrix() +A3 = sparse.eye(np.prod(ig.shape)) + +sum_rows1 = np.array(np.sum(abs(A1), axis=1)) +sum_rows2 = np.array(np.sum(abs(A2), axis=1)) +sum_rows3 = np.array(np.sum(abs(A3), axis=1)) + +sum_cols1 = np.array(np.sum(abs(A1), axis=0)) +sum_cols2 = np.array(np.sum(abs(A2), axis=0)) +sum_cols3 = np.array(np.sum(abs(A2), axis=0)) + +# Check if Grad sum row/cols is OK +Grad = Gradient(ig) + +Sum_Block_row = Grad.sum_abs_row() +Sum_Block_col = Grad.sum_abs_col() + +tmp1 = BlockDataContainer( ImageData(np.reshape(sum_rows1, ig.shape, order='F')),\ + ImageData(np.reshape(sum_rows2, ig.shape, order='F'))) + + +#np.testing.assert_array_almost_equal(tmp1[0].as_array(), Sum_Block_row[0].as_array(), decimal=4) +#np.testing.assert_array_almost_equal(tmp1[1].as_array(), Sum_Block_row[1].as_array(), decimal=4) + +tmp2 = ImageData(np.reshape(sum_cols1 + sum_cols2, ig.shape, order='F')) + +#np.testing.assert_array_almost_equal(tmp2.as_array(), Sum_Block_col.as_array(), decimal=4) + + +#%% BlockOperator with Gradient, Identity + +Id = Identity(ig) +Block_GrId = BlockOperator(Grad, Id, shape=(2,1)) + + +Sum_Block_GrId_row = Block_GrId.sum_abs_row() + + + + + + + + + + + + + + diff --git a/Wrappers/Python/wip/Demos/check_precond.py b/Wrappers/Python/wip/Demos/check_precond.py new file mode 100644 index 0000000..8cf95fa --- /dev/null +++ b/Wrappers/Python/wip/Demos/check_precond.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +# This work is part of the Core Imaging Library developed by +# Visual Analytics and Imaging System Group of the Science Technology +# Facilities Council, STFC + +# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca + +# 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. + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry + +import numpy as np +import numpy +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG + +from ccpi.optimisation.operators import BlockOperator, Gradient +from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \ + MixedL21Norm, BlockFunction + +from ccpi.astra.ops import AstraProjectorSimple + +# Create phantom for TV 2D tomography +N = 75 +x = np.zeros((N,N)) +x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 + +data = ImageData(x) +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) + +detectors = N +angles = np.linspace(0, np.pi, N, dtype=np.float32) + +ag = AcquisitionGeometry('parallel','2D',angles, detectors) +Aop = AstraProjectorSimple(ig, ag, 'cpu') +sin = Aop.direct(data) + +# Create noisy data +np.random.seed(10) +n1 = np.random.random(sin.shape) +noisy_data = sin + ImageData(5*n1) + +#%% + +# Regularisation Parameter +alpha = 50 + +# Create operators +op1 = Gradient(ig) +op2 = Aop + +# Create BlockOperator +operator = BlockOperator(op1, op2, shape=(2,1) ) + + + +# Create functions + +f1 = alpha * MixedL21Norm() +f2 = L2NormSquared(b=noisy_data) +f = BlockFunction(f1, f2) + +g = ZeroFunction() + +diag_precon = True + +if diag_precon: + + def tau_sigma_precond(operator): + + tau = 1/operator.sum_abs_row() + sigma = 1/ operator.sum_abs_col() + + return tau, sigma + + tau, sigma = tau_sigma_precond(operator) + +else: + # Compute operator Norm + normK = operator.norm() + # Primal & dual stepsizes + sigma = 10 + tau = 1/(sigma*normK**2) + + +# Setup and run the PDHG algorithm +pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True) +pdhg.max_iteration = 1000 +pdhg.update_objective_interval = 200 +pdhg.run(1000) + +#%% Check with CVX solution + +from ccpi.optimisation.operators import SparseFiniteDiff +import astra +import numpy + +try: + from cvxpy import * + cvx_not_installable = True +except ImportError: + cvx_not_installable = False + + +if cvx_not_installable: + + ##Construct problem + u = Variable(N*N) + + DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann') + DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann') + + regulariser = alpha * sum(norm(vstack([DX.matrix() * vec(u), DY.matrix() * vec(u)]), 2, axis = 0)) + + # create matrix representation for Astra operator + + vol_geom = astra.create_vol_geom(N, N) + proj_geom = astra.create_proj_geom('parallel', 1.0, detectors, angles) + + proj_id = astra.create_projector('line', proj_geom, vol_geom) + + matrix_id = astra.projector.matrix(proj_id) + + ProjMat = astra.matrix.get(matrix_id) + + fidelity = sum_squares( ProjMat * u - noisy_data.as_array().ravel()) + #constraints = [q>=fidelity] +# constraints = [u>=0] + + solver = MOSEK + obj = Minimize( regulariser + fidelity) + prob = Problem(obj) + result = prob.solve(verbose = True, solver = solver) + + +#%% + +plt.figure(figsize=(15,15)) +plt.subplot(2,2,1) +plt.imshow(data.as_array()) +plt.title('Ground Truth') + +plt.subplot(2,2,2) +plt.imshow(noisy_data.as_array()) +plt.title('Noisy Data') + +plt.subplot(2,2,3) +plt.imshow(pdhg.get_output().as_array()) +plt.title('PDHG Reconstruction') + +plt.subplot(2,2,4) +plt.imshow(np.reshape(u.value, ig.shape)) +plt.title('CVX Reconstruction') + +plt.show() + +#%% +plt.plot(np.linspace(0,N,N), pdhg.get_output().as_array()[int(N/2),:], label = 'PDHG') +plt.plot(np.linspace(0,N,N), np.reshape(u.value, ig.shape)[int(N/2),:], label = 'CVX') +plt.legend() +plt.title('Middle Line Profiles') +plt.show() + + + + + + + + diff --git a/Wrappers/Python/wip/compare_CGLS_algos.py b/Wrappers/Python/wip/compare_CGLS_algos.py new file mode 100644 index 0000000..52f3f31 --- /dev/null +++ b/Wrappers/Python/wip/compare_CGLS_algos.py @@ -0,0 +1,133 @@ +# This demo illustrates how to use the SIRT algorithm without and with +# nonnegativity and box constraints. The ASTRA 2D projectors are used. + +# First make all imports +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, \ + AcquisitionData +from ccpi.optimisation.algs import FISTA, FBPD, CGLS, SIRT +from ccpi.astra.operators import AstraProjectorSimple + +from ccpi.optimisation.algorithms import CGLS as CGLSalg + +import numpy as np +import matplotlib.pyplot as plt + +from ccpi.optimisation.functions import Norm2Sq + +# Choose either a parallel-beam (1=parallel2D) or fan-beam (2=cone2D) test case +test_case = 1 + +# Set up phantom size NxN by creating ImageGeometry, initialising the +# ImageData object with this geometry and empty array and finally put some +# data into its array, and display as image. +N = 128 +ig = ImageGeometry(voxel_num_x=N,voxel_num_y=N) +Phantom = ImageData(geometry=ig) + +x = Phantom.as_array() +x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 + +#plt.figure() +#plt.imshow(x) +#plt.title('Phantom image') +#plt.show() + +# Set up AcquisitionGeometry object to hold the parameters of the measurement +# setup geometry: # Number of angles, the actual angles from 0 to +# pi for parallel beam and 0 to 2pi for fanbeam, set the width of a detector +# pixel relative to an object pixel, the number of detector pixels, and the +# source-origin and origin-detector distance (here the origin-detector distance +# set to 0 to simulate a "virtual detector" with same detector pixel size as +# object pixel size). +angles_num = 20 +det_w = 1.0 +det_num = N +SourceOrig = 200 +OrigDetec = 0 + +if test_case==1: + angles = np.linspace(0,np.pi,angles_num,endpoint=False) + ag = AcquisitionGeometry('parallel', + '2D', + angles, + det_num,det_w) +elif test_case==2: + angles = np.linspace(0,2*np.pi,angles_num,endpoint=False) + ag = AcquisitionGeometry('cone', + '2D', + angles, + det_num, + det_w, + dist_source_center=SourceOrig, + dist_center_detector=OrigDetec) +else: + NotImplemented + +# Set up Operator object combining the ImageGeometry and AcquisitionGeometry +# wrapping calls to ASTRA as well as specifying whether to use CPU or GPU. +Aop = AstraProjectorSimple(ig, ag, 'cpu') + +# Forward and backprojection are available as methods direct and adjoint. Here +# generate test data b and do simple backprojection to obtain z. +b = Aop.direct(Phantom) +z = Aop.adjoint(b) + +#plt.figure() +#plt.imshow(b.array) +#plt.title('Simulated data') +#plt.show() + +#plt.figure() +#plt.imshow(z.array) +#plt.title('Backprojected data') +#plt.show() + +# Using the test data b, different reconstruction methods can now be set up as +# demonstrated in the rest of this file. In general all methods need an initial +# guess and some algorithm options to be set: +x_init = ImageData(np.zeros(x.shape),geometry=ig) +opt = {'tol': 1e-4, 'iter': 7} + +# First a CGLS reconstruction using the function version of CGLS can be done: +x_CGLS, it_CGLS, timing_CGLS, criter_CGLS = CGLS(x_init, Aop, b, opt) + +#plt.figure() +#plt.imshow(x_CGLS.array) +#plt.title('CGLS') +#plt.colorbar() +#plt.show() + +#plt.figure() +#plt.semilogy(criter_CGLS) +#plt.title('CGLS criterion') +#plt.show() + +f = Norm2Sq(Aop, b, c=1.) + +def callback(it, objective, solution): + print (objective, f(solution)) + +# Now CLGS using the algorithm class +CGLS_alg = CGLSalg() +CGLS_alg.set_up(x_init, Aop, b ) +CGLS_alg.max_iteration = 500 +CGLS_alg.update_objective_interval = 10 +CGLS_alg.run(300, callback=callback) +x_CGLS_alg = CGLS_alg.get_output() + +plt.figure() +plt.imshow(x_CGLS_alg.as_array()) +plt.title('CGLS ALG') +plt.colorbar() +plt.show() + +plt.figure() +plt.semilogy(CGLS_alg.objective) +plt.title('CGLS criterion') +plt.show() + +print(criter_CGLS) +print(CGLS_alg.objective) + +print((x_CGLS - x_CGLS_alg).norm())
\ No newline at end of file diff --git a/Wrappers/Python/wip/demo_SIRT.py b/Wrappers/Python/wip/demo_SIRT.py new file mode 100644 index 0000000..5a85d41 --- /dev/null +++ b/Wrappers/Python/wip/demo_SIRT.py @@ -0,0 +1,205 @@ +# This demo illustrates how to use the SIRT algorithm without and with +# nonnegativity and box constraints. The ASTRA 2D projectors are used. + +# First make all imports +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, \ + AcquisitionData +from ccpi.optimisation.functions import IndicatorBox +from ccpi.astra.ops import AstraProjectorSimple +from ccpi.optimisation.algorithms import SIRT, CGLS + +import numpy as np +import matplotlib.pyplot as plt + +# Choose either a parallel-beam (1=parallel2D) or fan-beam (2=cone2D) test case +test_case = 1 + +# Set up phantom size NxN by creating ImageGeometry, initialising the +# ImageData object with this geometry and empty array and finally put some +# data into its array, and display as image. +N = 128 +ig = ImageGeometry(voxel_num_x=N,voxel_num_y=N) +Phantom = ImageData(geometry=ig) + +x = Phantom.as_array() +x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 + +plt.figure() +plt.imshow(x) +plt.title('Phantom image') +plt.show() + +# Set up AcquisitionGeometry object to hold the parameters of the measurement +# setup geometry: # Number of angles, the actual angles from 0 to +# pi for parallel beam and 0 to 2pi for fanbeam, set the width of a detector +# pixel relative to an object pixel, the number of detector pixels, and the +# source-origin and origin-detector distance (here the origin-detector distance +# set to 0 to simulate a "virtual detector" with same detector pixel size as +# object pixel size). +angles_num = 20 +det_w = 1.0 +det_num = N +SourceOrig = 200 +OrigDetec = 0 + +if test_case==1: + angles = np.linspace(0,np.pi,angles_num,endpoint=False) + ag = AcquisitionGeometry('parallel', + '2D', + angles, + det_num,det_w) +elif test_case==2: + angles = np.linspace(0,2*np.pi,angles_num,endpoint=False) + ag = AcquisitionGeometry('cone', + '2D', + angles, + det_num, + det_w, + dist_source_center=SourceOrig, + dist_center_detector=OrigDetec) +else: + NotImplemented + +# Set up Operator object combining the ImageGeometry and AcquisitionGeometry +# wrapping calls to ASTRA as well as specifying whether to use CPU or GPU. +Aop = AstraProjectorSimple(ig, ag, 'gpu') + +# Forward and backprojection are available as methods direct and adjoint. Here +# generate test data b and do simple backprojection to obtain z. +b = Aop.direct(Phantom) +z = Aop.adjoint(b) + +plt.figure() +plt.imshow(b.as_array()) +plt.title('Simulated data') +plt.show() + +plt.figure() +plt.imshow(z.as_array()) +plt.title('Backprojected data') +plt.show() + +# Using the test data b, different reconstruction methods can now be set up as +# demonstrated in the rest of this file. In general all methods need an initial +# guess and some algorithm options to be set: +x_init = ImageData(np.zeros(x.shape),geometry=ig) +opt = {'tol': 1e-4, 'iter': 100} + + +# First run a simple CGLS reconstruction: +CGLS_alg = CGLS() +CGLS_alg.set_up(x_init, Aop, b ) +CGLS_alg.max_iteration = 2000 +CGLS_alg.run(opt['iter']) +x_CGLS_alg = CGLS_alg.get_output() + +plt.figure() +plt.imshow(x_CGLS_alg.as_array()) +plt.title('CGLS ALG') +plt.colorbar() +plt.show() + +plt.figure() +plt.semilogy(CGLS_alg.objective) +plt.title('CGLS criterion') +plt.show() + + +# A SIRT reconstruction can be done simply by replacing CGLS by SIRT. +# In the first instance, no constraints are enforced. +SIRT_alg = SIRT() +SIRT_alg.set_up(x_init, Aop, b ) +SIRT_alg.max_iteration = 2000 +SIRT_alg.run(opt['iter']) +x_SIRT_alg = SIRT_alg.get_output() + +plt.figure() +plt.imshow(x_SIRT_alg.as_array()) +plt.title('SIRT unconstrained') +plt.colorbar() +plt.show() + +plt.figure() +plt.semilogy(SIRT_alg.objective) +plt.title('SIRT unconstrained criterion') +plt.show() + +# The SIRT algorithm is stopped after the specified number of iterations has +# been run. It can be resumed by calling the run command again, which will run +# it for the specificed number of iterations +SIRT_alg.run(opt['iter']) +x_SIRT_alg2 = SIRT_alg.get_output() + +plt.figure() +plt.imshow(x_SIRT_alg2.as_array()) +plt.title('SIRT unconstrained, extra iterations') +plt.colorbar() +plt.show() + +plt.figure() +plt.semilogy(SIRT_alg.objective) +plt.title('SIRT unconstrained criterion, extra iterations') +plt.show() + + +# A SIRT nonnegativity constrained reconstruction can be done using the +# additional input "constraint" set to a box indicator function with 0 as the +# lower bound and the default upper bound of infinity. First setup a new +# instance of the SIRT algorithm. +SIRT_alg0 = SIRT() +SIRT_alg0.set_up(x_init, Aop, b, constraint=IndicatorBox(lower=0) ) +SIRT_alg0.max_iteration = 2000 +SIRT_alg0.run(opt['iter']) +x_SIRT_alg0 = SIRT_alg0.get_output() + +plt.figure() +plt.imshow(x_SIRT_alg0.as_array()) +plt.title('SIRT nonnegativity constrained') +plt.colorbar() +plt.show() + +plt.figure() +plt.semilogy(SIRT_alg0.objective) +plt.title('SIRT nonnegativity criterion') +plt.show() + + +# A SIRT reconstruction with box constraints on [0,1] can also be done. +SIRT_alg01 = SIRT() +SIRT_alg01.set_up(x_init, Aop, b, constraint=IndicatorBox(lower=0,upper=1) ) +SIRT_alg01.max_iteration = 2000 +SIRT_alg01.run(opt['iter']) +x_SIRT_alg01 = SIRT_alg01.get_output() + +plt.figure() +plt.imshow(x_SIRT_alg01.as_array()) +plt.title('SIRT boc(0,1)') +plt.colorbar() +plt.show() + +plt.figure() +plt.semilogy(SIRT_alg01.objective) +plt.title('SIRT box(0,1) criterion') +plt.show() + +# The test image has values in the range [0,1], so enforcing values in the +# reconstruction to be within this interval improves a lot. Just for fun +# we can also easily see what happens if we choose a narrower interval as +# constrint in the reconstruction, lower bound 0.2, upper bound 0.8. +SIRT_alg0208 = SIRT() +SIRT_alg0208.set_up(x_init,Aop,b,constraint=IndicatorBox(lower=0.2,upper=0.8)) +SIRT_alg0208.max_iteration = 2000 +SIRT_alg0208.run(opt['iter']) +x_SIRT_alg0208 = SIRT_alg0208.get_output() + +plt.figure() +plt.imshow(x_SIRT_alg0208.as_array()) +plt.title('SIRT boc(0.2,0.8)') +plt.colorbar() +plt.show() + +plt.figure() +plt.semilogy(SIRT_alg0208.objective) +plt.title('SIRT box(0.2,0.8) criterion') +plt.show()
\ No newline at end of file diff --git a/Wrappers/Python/wip/demo_box_constraints_FISTA.py b/Wrappers/Python/wip/demo_box_constraints_FISTA.py new file mode 100644 index 0000000..b15dd45 --- /dev/null +++ b/Wrappers/Python/wip/demo_box_constraints_FISTA.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Apr 17 14:46:21 2019 + +@author: jakob + +Demonstrate the use of box constraints in FISTA +""" + +# First make all imports +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, \ + AcquisitionData +from ccpi.optimisation.algorithms import FISTA +from ccpi.optimisation.functions import Norm2sq, IndicatorBox +from ccpi.astra.ops import AstraProjectorSimple + +from ccpi.optimisation.operators import Identity + +import numpy as np +import matplotlib.pyplot as plt + + +# Set up phantom size NxN by creating ImageGeometry, initialising the +# ImageData object with this geometry and empty array and finally put some +# data into its array, and display as image. +N = 128 +ig = ImageGeometry(voxel_num_x=N,voxel_num_y=N) +Phantom = ImageData(geometry=ig) + +x = Phantom.as_array() +x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 + +plt.figure() +plt.imshow(x) +plt.title('Phantom image') +plt.show() + +# Set up AcquisitionGeometry object to hold the parameters of the measurement +# setup geometry: # Number of angles, the actual angles from 0 to +# pi for parallel beam and 0 to 2pi for fanbeam, set the width of a detector +# pixel relative to an object pixel, the number of detector pixels, and the +# source-origin and origin-detector distance (here the origin-detector distance +# set to 0 to simulate a "virtual detector" with same detector pixel size as +# object pixel size). +angles_num = 20 +det_w = 1.0 +det_num = N +SourceOrig = 200 +OrigDetec = 0 + +test_case = 1 + +if test_case==1: + angles = np.linspace(0,np.pi,angles_num,endpoint=False) + ag = AcquisitionGeometry('parallel', + '2D', + angles, + det_num,det_w) +elif test_case==2: + angles = np.linspace(0,2*np.pi,angles_num,endpoint=False) + ag = AcquisitionGeometry('cone', + '2D', + angles, + det_num, + det_w, + dist_source_center=SourceOrig, + dist_center_detector=OrigDetec) +else: + NotImplemented + +# Set up Operator object combining the ImageGeometry and AcquisitionGeometry +# wrapping calls to ASTRA as well as specifying whether to use CPU or GPU. +Aop = AstraProjectorSimple(ig, ag, 'cpu') + +Aop = Identity(ig,ig) + +# Forward and backprojection are available as methods direct and adjoint. Here +# generate test data b and do simple backprojection to obtain z. +b = Aop.direct(Phantom) +z = Aop.adjoint(b) + +plt.figure() +plt.imshow(b.array) +plt.title('Simulated data') +plt.show() + +plt.figure() +plt.imshow(z.array) +plt.title('Backprojected data') +plt.show() + +# Using the test data b, different reconstruction methods can now be set up as +# demonstrated in the rest of this file. In general all methods need an initial +# guess and some algorithm options to be set: +x_init = ImageData(np.zeros(x.shape),geometry=ig) +opt = {'tol': 1e-4, 'iter': 100} + + + +# Create least squares object instance with projector, test data and a constant +# coefficient of 0.5: +f = Norm2sq(Aop,b,c=0.5) + +# Run FISTA for least squares without constraints +FISTA_alg = FISTA() +FISTA_alg.set_up(x_init=x_init, f=f, opt=opt) +FISTA_alg.max_iteration = 2000 +FISTA_alg.run(opt['iter']) +x_FISTA = FISTA_alg.get_output() + +plt.figure() +plt.imshow(x_FISTA.array) +plt.title('FISTA unconstrained') +plt.colorbar() +plt.show() + +plt.figure() +plt.semilogy(FISTA_alg.objective) +plt.title('FISTA unconstrained criterion') +plt.show() + +# Run FISTA for least squares with lower bound 0.1 +FISTA_alg0 = FISTA() +FISTA_alg0.set_up(x_init=x_init, f=f, g=IndicatorBox(lower=0.1), opt=opt) +FISTA_alg0.max_iteration = 2000 +FISTA_alg0.run(opt['iter']) +x_FISTA0 = FISTA_alg0.get_output() + +plt.figure() +plt.imshow(x_FISTA0.array) +plt.title('FISTA lower bound 0.1') +plt.colorbar() +plt.show() + +plt.figure() +plt.semilogy(FISTA_alg0.objective) +plt.title('FISTA criterion, lower bound 0.1') +plt.show() + +# Run FISTA for least squares with box constraint [0.1,0.8] +FISTA_alg0 = FISTA() +FISTA_alg0.set_up(x_init=x_init, f=f, g=IndicatorBox(lower=0.1,upper=0.8), opt=opt) +FISTA_alg0.max_iteration = 2000 +FISTA_alg0.run(opt['iter']) +x_FISTA0 = FISTA_alg0.get_output() + +plt.figure() +plt.imshow(x_FISTA0.array) +plt.title('FISTA box(0.1,0.8) constrained') +plt.colorbar() +plt.show() + +plt.figure() +plt.semilogy(FISTA_alg0.objective) +plt.title('FISTA criterion, box(0.1,0.8) constrained criterion') +plt.show()
\ No newline at end of file diff --git a/Wrappers/Python/wip/demo_colourbay.py b/Wrappers/Python/wip/demo_colourbay.py index 5dbf2e1..0536b07 100644 --- a/Wrappers/Python/wip/demo_colourbay.py +++ b/Wrappers/Python/wip/demo_colourbay.py @@ -18,7 +18,7 @@ from ccpi.optimisation.funcs import Norm2sq, Norm1 # Permute (numpy.transpose) puts into our default ordering which is # (channel, angle, vertical, horizontal). -pathname = '/media/jakob/050d8d45-fab3-4285-935f-260e6c5f162c1/Data/ColourBay/spectral_data_sets/CarbonPd/' +pathname = '/media/newhd/shared/Data/ColourBay/spectral_data_sets/CarbonPd/' filename = 'carbonPd_full_sinogram_stripes_removed.mat' X = loadmat(pathname + filename) diff --git a/Wrappers/Python/wip/fix_test.py b/Wrappers/Python/wip/fix_test.py new file mode 100755 index 0000000..b1006c0 --- /dev/null +++ b/Wrappers/Python/wip/fix_test.py @@ -0,0 +1,208 @@ +import numpy as np +import numpy +from ccpi.optimisation.operators import * +from ccpi.optimisation.algorithms import * +from ccpi.optimisation.functions import * +from ccpi.framework import * + +def isSizeCorrect(data1 ,data2): + if issubclass(type(data1), DataContainer) and \ + issubclass(type(data2), DataContainer): + # check dimensionality + if data1.check_dimensions(data2): + return True + elif issubclass(type(data1) , numpy.ndarray) and \ + issubclass(type(data2) , numpy.ndarray): + return data1.shape == data2.shape + else: + raise ValueError("{0}: getting two incompatible types: {1} {2}"\ + .format('Function', type(data1), type(data2))) + return False + +class Norm1(Function): + + def __init__(self,gamma): + super(Norm1, self).__init__() + self.gamma = gamma + self.L = 1 + self.sign_x = None + + def __call__(self,x,out=None): + if out is None: + return self.gamma*(x.abs().sum()) + else: + if not x.shape == out.shape: + raise ValueError('Norm1 Incompatible size:', + x.shape, out.shape) + x.abs(out=out) + return out.sum() * self.gamma + + def prox(self,x,tau): + return (x.abs() - tau*self.gamma).maximum(0) * x.sign() + + def proximal(self, x, tau, out=None): + if out is None: + return self.prox(x, tau) + else: + if isSizeCorrect(x,out): + # check dimensionality + if issubclass(type(out), DataContainer): + v = (x.abs() - tau*self.gamma).maximum(0) + x.sign(out=out) + out *= v + #out.fill(self.prox(x,tau)) + elif issubclass(type(out) , numpy.ndarray): + v = (x.abs() - tau*self.gamma).maximum(0) + out[:] = x.sign() + out *= v + #out[:] = self.prox(x,tau) + else: + raise ValueError ('Wrong size: x{0} out{1}'.format(x.shape,out.shape) ) + +opt = {'memopt': True} +# Problem data. +m = 500 +n = 200 + +# if m < n then the problem is under-determined and algorithms will struggle to find a solution. +# One approach is to add regularisation + +#np.random.seed(1) +Amat = np.asarray( np.random.randn(m, n), dtype=numpy.float32) +Amat = np.asarray( np.random.random_integers(1,10, (m, n)), dtype=numpy.float32) +#Amat = np.asarray(np.eye(m), dtype=np.float32) * 2 +A = LinearOperatorMatrix(Amat) +bmat = np.asarray( np.random.randn(m), dtype=numpy.float32) +#bmat *= 0 +#bmat += 2 +print ("bmat", bmat.shape) +print ("A", A.A) +#bmat.shape = (bmat.shape[0], 1) + +# A = Identity() +# Change n to equal to m. +vgb = VectorGeometry(m) +vgx = VectorGeometry(n) +b = vgb.allocate(VectorGeometry.RANDOM_INT, dtype=numpy.float32) +# b.fill(bmat) +#b = DataContainer(bmat) + +# Regularization parameter +lam = 10 +opt = {'memopt': True} +# Create object instances with the test data A and b. +f = Norm2Sq(A, b, c=1., memopt=True) +#f = FunctionOperatorComposition(A, L2NormSquared(b=bmat)) +g0 = ZeroFunction() + +#f.L = 30.003 +x_init = vgx.allocate(VectorGeometry.RANDOM, dtype=numpy.float32) +x_initcgls = x_init.copy() + +a = VectorData(x_init.as_array(), deep_copy=True) + +assert id(x_init.as_array()) != id(a.as_array()) + + +#f.L = LinearOperator.PowerMethod(A, 25, x_init)[0] +#print ('f.L', f.L) +rate = (1 / f.L) / 6 +#f.L *= 12 + +# Initial guess +#x_init = DataContainer(np.zeros((n, 1))) +print ('x_init', x_init.as_array()) +print ('b', b.as_array()) +# Create 1-norm object instance +g1_new = lam * L1Norm() +g1 = Norm1(lam) + +g1 = ZeroFunction() +#g1(x_init) +x = g1.prox(x_init, 1/f.L ) +print ("g1.proximal ", x.as_array()) + +x = g1.prox(x_init, 0.03 ) +print ("g1.proximal ", x.as_array()) +x = g1_new.proximal(x_init, 0.03 ) +print ("g1.proximal ", x.as_array()) + +x1 = vgx.allocate(VectorGeometry.RANDOM, dtype=numpy.float32) +pippo = vgx.allocate() + +print ("x_init", x_init.as_array()) +print ("x1", x1.as_array()) +a = x_init.subtract(x1, out=pippo) + +print ("pippo", pippo.as_array()) +print ("x_init", x_init.as_array()) +print ("x1", x1.as_array()) + +y = A.direct(x_init) +y *= 0 +A.direct(x_init, out=y) + +# Combine with least squares and solve using generic FISTA implementation +#x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g1, opt=opt) +def callback(it, objective, solution): + print ("callback " , it , objective, f(solution)) + +fa = FISTA(x_init=x_init, f=f, g=g1) +fa.max_iteration = 1000 +fa.update_objective_interval = int( fa.max_iteration / 10 ) +fa.run(fa.max_iteration, callback = None, verbose=True) + +gd = GradientDescent(x_init=x_init, objective_function=f, rate = rate ) +gd.max_iteration = 5000 +gd.update_objective_interval = int( gd.max_iteration / 10 ) +gd.run(gd.max_iteration, callback = None, verbose=True) + + + +cgls = CGLS(x_init= x_initcgls, operator=A, data=b) +cgls.max_iteration = 1000 +cgls.update_objective_interval = int( cgls.max_iteration / 10 ) + +#cgls.should_stop = stop_criterion(cgls) +cgls.run(cgls.max_iteration, callback = callback, verbose=True) + + + +# Print for comparison +print("FISTA least squares plus 1-norm solution and objective value:") +print(fa.get_output().as_array()) +print(fa.get_last_objective()) + +print ("data ", b.as_array()) +print ('FISTA ', A.direct(fa.get_output()).as_array()) +print ('GradientDescent', A.direct(gd.get_output()).as_array()) +print ('CGLS ', A.direct(cgls.get_output()).as_array()) + +cond = numpy.linalg.cond(A.A) + +print ("cond" , cond) + +#%% +try: + import cvxpy as cp + # Construct the problem. + x = cp.Variable(n) + objective = cp.Minimize(cp.sum_squares(A.A*x - bmat)) + prob = cp.Problem(objective) + # The optimal objective is returned by prob.solve(). + result = prob.solve(solver = cp.MOSEK) + + print ('CGLS ', cgls.get_output().as_array()) + print ('CVX ', x.value) + + print ('FISTA ', fa.get_output().as_array()) + print ('GD ', gd.get_output().as_array()) +except ImportError as ir: + pass + + #%% + + + + + diff --git a/Wrappers/Python/wip/old_demos/demo_colourbay.py b/Wrappers/Python/wip/old_demos/demo_colourbay.py new file mode 100644 index 0000000..5dbf2e1 --- /dev/null +++ b/Wrappers/Python/wip/old_demos/demo_colourbay.py @@ -0,0 +1,137 @@ +# This script demonstrates how to load a mat-file with UoM colour-bay data +# into the CIL optimisation framework and run (simple) multichannel +# reconstruction methods. + +# All third-party imports. +import numpy +from scipy.io import loadmat +import matplotlib.pyplot as plt + +# All own imports. +from ccpi.framework import AcquisitionData, AcquisitionGeometry, ImageGeometry, ImageData +from ccpi.astra.ops import AstraProjectorMC +from ccpi.optimisation.algs import CGLS, FISTA +from ccpi.optimisation.funcs import Norm2sq, Norm1 + +# Load full data and permute to expected ordering. Change path as necessary. +# The loaded X has dims 80x60x80x150, which is pix x angle x pix x channel. +# Permute (numpy.transpose) puts into our default ordering which is +# (channel, angle, vertical, horizontal). + +pathname = '/media/jakob/050d8d45-fab3-4285-935f-260e6c5f162c1/Data/ColourBay/spectral_data_sets/CarbonPd/' +filename = 'carbonPd_full_sinogram_stripes_removed.mat' + +X = loadmat(pathname + filename) +X = numpy.transpose(X['SS'],(3,1,2,0)) + +# Store geometric variables for reuse +num_channels = X.shape[0] +num_pixels_h = X.shape[3] +num_pixels_v = X.shape[2] +num_angles = X.shape[1] + +# Display a single projection in a single channel +plt.imshow(X[100,5,:,:]) +plt.title('Example of a projection image in one channel' ) +plt.show() + +# Set angles to use +angles = numpy.linspace(-numpy.pi,numpy.pi,num_angles,endpoint=False) + +# Define full 3D acquisition geometry and data container. +# Geometric info is taken from the txt-file in the same dir as the mat-file +ag = AcquisitionGeometry('cone', + '3D', + angles, + pixel_num_h=num_pixels_h, + pixel_size_h=0.25, + pixel_num_v=num_pixels_v, + pixel_size_v=0.25, + dist_source_center=233.0, + dist_center_detector=245.0, + channels=num_channels) +data = AcquisitionData(X, geometry=ag) + +# Reduce to central slice by extracting relevant parameters from data and its +# geometry. Perhaps create function to extract central slice automatically? +data2d = data.subset(vertical=40) +ag2d = AcquisitionGeometry('cone', + '2D', + ag.angles, + pixel_num_h=ag.pixel_num_h, + pixel_size_h=ag.pixel_size_h, + pixel_num_v=1, + pixel_size_v=ag.pixel_size_h, + dist_source_center=ag.dist_source_center, + dist_center_detector=ag.dist_center_detector, + channels=ag.channels) +data2d.geometry = ag2d + +# Set up 2D Image Geometry. +# First need the geometric magnification to scale the voxel size relative +# to the detector pixel size. +mag = (ag.dist_source_center + ag.dist_center_detector)/ag.dist_source_center +ig2d = ImageGeometry(voxel_num_x=ag2d.pixel_num_h, + voxel_num_y=ag2d.pixel_num_h, + voxel_size_x=ag2d.pixel_size_h/mag, + voxel_size_y=ag2d.pixel_size_h/mag, + channels=X.shape[0]) + +# Create GPU multichannel projector/backprojector operator with ASTRA. +Aall = AstraProjectorMC(ig2d,ag2d,'gpu') + +# Compute and simple backprojction and display one channel as image. +Xbp = Aall.adjoint(data2d) +plt.imshow(Xbp.subset(channel=100).array) +plt.show() + +# Set initial guess ImageData with zeros for algorithms, and algorithm options. +x_init = ImageData(numpy.zeros((num_channels,num_pixels_v,num_pixels_h)), + geometry=ig2d, + dimension_labels=['channel','horizontal_y','horizontal_x']) +opt_CGLS = {'tol': 1e-4, 'iter': 5} + +# Run CGLS algorithm and display one channel. +x_CGLS, it_CGLS, timing_CGLS, criter_CGLS = CGLS(x_init, Aall, data2d, opt_CGLS) + +plt.imshow(x_CGLS.subset(channel=100).array) +plt.title('CGLS') +plt.show() + +plt.semilogy(criter_CGLS) +plt.title('CGLS Criterion vs iterations') +plt.show() + +# Create least squares object instance with projector, test data and a constant +# coefficient of 0.5. Note it is least squares over all channels. +f = Norm2sq(Aall,data2d,c=0.5) + +# Options for FISTA algorithm. +opt = {'tol': 1e-4, 'iter': 100} + +# Run FISTA for least squares without regularization and display one channel +# reconstruction as image. +x_fista0, it0, timing0, criter0 = FISTA(x_init, f, None, opt) + +plt.imshow(x_fista0.subset(channel=100).array) +plt.title('FISTA LS') +plt.show() + +plt.semilogy(criter0) +plt.title('FISTA LS Criterion vs iterations') +plt.show() + +# Set up 1-norm regularisation (over all channels), solve with FISTA, and +# display one channel of reconstruction. +lam = 0.1 +g0 = Norm1(lam) + +x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g0, opt) + +plt.imshow(x_fista1.subset(channel=100).array) +plt.title('FISTA LS+1') +plt.show() + +plt.semilogy(criter1) +plt.title('FISTA LS+1 Criterion vs iterations') +plt.show()
\ No newline at end of file diff --git a/Wrappers/Python/wip/old_demos/demo_compare_cvx.py b/Wrappers/Python/wip/old_demos/demo_compare_cvx.py new file mode 100644 index 0000000..27b1c97 --- /dev/null +++ b/Wrappers/Python/wip/old_demos/demo_compare_cvx.py @@ -0,0 +1,306 @@ + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, DataContainer +from ccpi.optimisation.algs import FISTA, FBPD, CGLS +from ccpi.optimisation.funcs import Norm2sq, ZeroFun, Norm1, TV2D, Norm2 + +from ccpi.optimisation.ops import LinearOperatorMatrix, TomoIdentity +from ccpi.optimisation.ops import Identity +from ccpi.optimisation.ops import FiniteDiff2D + +# Requires CVXPY, see http://www.cvxpy.org/ +# CVXPY can be installed in anaconda using +# conda install -c cvxgrp cvxpy libgcc + +# Whether to use or omit CVXPY +use_cvxpy = True +if use_cvxpy: + from cvxpy import * + +import numpy as np +import matplotlib.pyplot as plt + +# Problem data. +m = 30 +n = 20 +np.random.seed(1) +Amat = np.random.randn(m, n) +A = LinearOperatorMatrix(Amat) +bmat = np.random.randn(m) +bmat.shape = (bmat.shape[0],1) + +# A = Identity() +# Change n to equal to m. + +b = DataContainer(bmat) + +# Regularization parameter +lam = 10 +opt = {'memopt':True} +# Create object instances with the test data A and b. +f = Norm2sq(A,b,c=0.5, memopt=True) +g0 = ZeroFun() + +# Initial guess +x_init = DataContainer(np.zeros((n,1))) + +f.grad(x_init) + +# Run FISTA for least squares plus zero function. +x_fista0, it0, timing0, criter0 = FISTA(x_init, f, g0 , opt=opt) + +# Print solution and final objective/criterion value for comparison +print("FISTA least squares plus zero function solution and objective value:") +print(x_fista0.array) +print(criter0[-1]) + +if use_cvxpy: + # Compare to CVXPY + + # Construct the problem. + x0 = Variable(n) + objective0 = Minimize(0.5*sum_squares(Amat*x0 - bmat.T[0]) ) + prob0 = Problem(objective0) + + # The optimal objective is returned by prob.solve(). + result0 = prob0.solve(verbose=False,solver=SCS,eps=1e-9) + + # The optimal solution for x is stored in x.value and optimal objective value + # is in result as well as in objective.value + print("CVXPY least squares plus zero function solution and objective value:") + print(x0.value) + print(objective0.value) + +# Plot criterion curve to see FISTA converge to same value as CVX. +iternum = np.arange(1,1001) +plt.figure() +plt.loglog(iternum[[0,-1]],[objective0.value, objective0.value], label='CVX LS') +plt.loglog(iternum,criter0,label='FISTA LS') +plt.legend() +plt.show() + +# Create 1-norm object instance +g1 = Norm1(lam) + +g1(x_init) +x_rand = DataContainer(np.reshape(np.random.rand(n),(n,1))) +x_rand2 = DataContainer(np.reshape(np.random.rand(n-1),(n-1,1))) +v = g1.prox(x_rand,0.02) +#vv = g1.prox(x_rand2,0.02) +vv = v.copy() +vv *= 0 +print (">>>>>>>>>>vv" , vv.as_array()) +vv.fill(v) +print (">>>>>>>>>>fill" , vv.as_array()) +g1.proximal(x_rand, 0.02, out=vv) +print (">>>>>>>>>>v" , v.as_array()) +print (">>>>>>>>>>gradient" , vv.as_array()) + +print (">>>>>>>>>>" , (v-vv).as_array()) +import sys +#sys.exit(0) +# Combine with least squares and solve using generic FISTA implementation +x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g1,opt=opt) + +# Print for comparison +print("FISTA least squares plus 1-norm solution and objective value:") +print(x_fista1) +print(criter1[-1]) + +if use_cvxpy: + # Compare to CVXPY + + # Construct the problem. + x1 = Variable(n) + objective1 = Minimize(0.5*sum_squares(Amat*x1 - bmat.T[0]) + lam*norm(x1,1) ) + prob1 = Problem(objective1) + + # The optimal objective is returned by prob.solve(). + result1 = prob1.solve(verbose=False,solver=SCS,eps=1e-9) + + # The optimal solution for x is stored in x.value and optimal objective value + # is in result as well as in objective.value + print("CVXPY least squares plus 1-norm solution and objective value:") + print(x1.value) + print(objective1.value) + +# Now try another algorithm FBPD for same problem: +x_fbpd1, itfbpd1, timingfbpd1, criterfbpd1 = FBPD(x_init,Identity(), None, f, g1) +print(x_fbpd1) +print(criterfbpd1[-1]) + +# Plot criterion curve to see both FISTA and FBPD converge to same value. +# Note that FISTA is very efficient for 1-norm minimization so it beats +# FBPD in this test by a lot. But FBPD can handle a larger class of problems +# than FISTA can. +plt.figure() +plt.loglog(iternum[[0,-1]],[objective1.value, objective1.value], label='CVX LS+1') +plt.loglog(iternum,criter1,label='FISTA LS+1') +plt.legend() +plt.show() + +plt.figure() +plt.loglog(iternum[[0,-1]],[objective1.value, objective1.value], label='CVX LS+1') +plt.loglog(iternum,criter1,label='FISTA LS+1') +plt.loglog(iternum,criterfbpd1,label='FBPD LS+1') +plt.legend() +plt.show() + +# Now try 1-norm and TV denoising with FBPD, first 1-norm. + +# Set up phantom size NxN by creating ImageGeometry, initialising the +# ImageData object with this geometry and empty array and finally put some +# data into its array, and display as image. +N = 64 +ig = ImageGeometry(voxel_num_x=N,voxel_num_y=N) +Phantom = ImageData(geometry=ig) + +x = Phantom.as_array() +x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 + +plt.imshow(x) +plt.title('Phantom image') +plt.show() + +# Identity operator for denoising +I = TomoIdentity(ig) + +# Data and add noise +y = I.direct(Phantom) +y.array = y.array + 0.1*np.random.randn(N, N) + +plt.imshow(y.array) +plt.title('Noisy image') +plt.show() + + +################### +# Data fidelity term +f_denoise = Norm2sq(I,y,c=0.5,memopt=True) + +# 1-norm regulariser +lam1_denoise = 1.0 +g1_denoise = Norm1(lam1_denoise) + +# Initial guess +x_init_denoise = ImageData(np.zeros((N,N))) + +# Combine with least squares and solve using generic FISTA implementation +x_fista1_denoise, it1_denoise, timing1_denoise, criter1_denoise = FISTA(x_init_denoise, f_denoise, g1_denoise, opt=opt) + +print(x_fista1_denoise) +print(criter1_denoise[-1]) + +#plt.imshow(x_fista1_denoise.as_array()) +#plt.title('FISTA LS+1') +#plt.show() + +# Now denoise LS + 1-norm with FBPD +x_fbpd1_denoise, itfbpd1_denoise, timingfbpd1_denoise, \ + criterfbpd1_denoise = FBPD(x_init_denoise, I, None, f_denoise, g1_denoise) +print(x_fbpd1_denoise) +print(criterfbpd1_denoise[-1]) + +#plt.imshow(x_fbpd1_denoise.as_array()) +#plt.title('FBPD LS+1') +#plt.show() + +if use_cvxpy: + # Compare to CVXPY + + # Construct the problem. + x1_denoise = Variable(N**2,1) + objective1_denoise = Minimize(0.5*sum_squares(x1_denoise - y.array.flatten()) + lam1_denoise*norm(x1_denoise,1) ) + prob1_denoise = Problem(objective1_denoise) + + # The optimal objective is returned by prob.solve(). + result1_denoise = prob1_denoise.solve(verbose=False,solver=SCS,eps=1e-12) + + # The optimal solution for x is stored in x.value and optimal objective value + # is in result as well as in objective.value + print("CVXPY least squares plus 1-norm solution and objective value:") + print(x1_denoise.value) + print(objective1_denoise.value) + +x1_cvx = x1_denoise.value +x1_cvx.shape = (N,N) + + + +#plt.imshow(x1_cvx) +#plt.title('CVX LS+1') +#plt.show() + +fig = plt.figure() +plt.subplot(1,4,1) +plt.imshow(y.array) +plt.title("LS+1") +plt.subplot(1,4,2) +plt.imshow(x_fista1_denoise.as_array()) +plt.title("fista") +plt.subplot(1,4,3) +plt.imshow(x_fbpd1_denoise.as_array()) +plt.title("fbpd") +plt.subplot(1,4,4) +plt.imshow(x1_cvx) +plt.title("cvx") +plt.show() + +############################################################## +# Now TV with FBPD and Norm2 +lam_tv = 0.1 +gtv = TV2D(lam_tv) +norm2 = Norm2(lam_tv) +op = FiniteDiff2D() +#gtv(gtv.op.direct(x_init_denoise)) + +opt_tv = {'tol': 1e-4, 'iter': 10000} + +x_fbpdtv_denoise, itfbpdtv_denoise, timingfbpdtv_denoise, \ + criterfbpdtv_denoise = FBPD(x_init_denoise, op, None, \ + f_denoise, norm2 ,opt=opt_tv) +print(x_fbpdtv_denoise) +print(criterfbpdtv_denoise[-1]) + +plt.imshow(x_fbpdtv_denoise.as_array()) +plt.title('FBPD TV') +#plt.show() + +if use_cvxpy: + # Compare to CVXPY + + # Construct the problem. + xtv_denoise = Variable((N,N)) + #print (xtv_denoise.value.shape) + objectivetv_denoise = Minimize(0.5*sum_squares(xtv_denoise - y.array) + lam_tv*tv(xtv_denoise) ) + probtv_denoise = Problem(objectivetv_denoise) + + # The optimal objective is returned by prob.solve(). + resulttv_denoise = probtv_denoise.solve(verbose=False,solver=SCS,eps=1e-12) + + # The optimal solution for x is stored in x.value and optimal objective value + # is in result as well as in objective.value + print("CVXPY least squares plus 1-norm solution and objective value:") + print(xtv_denoise.value) + print(objectivetv_denoise.value) + +plt.imshow(xtv_denoise.value) +plt.title('CVX TV') +#plt.show() + +fig = plt.figure() +plt.subplot(1,3,1) +plt.imshow(y.array) +plt.title("TV2D") +plt.subplot(1,3,2) +plt.imshow(x_fbpdtv_denoise.as_array()) +plt.title("fbpd tv denoise") +plt.subplot(1,3,3) +plt.imshow(xtv_denoise.value) +plt.title("CVX tv") +plt.show() + + + +plt.loglog([0,opt_tv['iter']], [objectivetv_denoise.value,objectivetv_denoise.value], label='CVX TV') +plt.loglog(criterfbpdtv_denoise, label='FBPD TV') diff --git a/Wrappers/Python/wip/old_demos/demo_gradient_descent.py b/Wrappers/Python/wip/old_demos/demo_gradient_descent.py new file mode 100755 index 0000000..4d6647e --- /dev/null +++ b/Wrappers/Python/wip/old_demos/demo_gradient_descent.py @@ -0,0 +1,295 @@ + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, DataContainer +from ccpi.optimisation.algs import FISTA, FBPD, CGLS +from ccpi.optimisation.funcs import Norm2sq, ZeroFun, Norm1, TV2D, Norm2 + +from ccpi.optimisation.ops import LinearOperatorMatrix, TomoIdentity +from ccpi.optimisation.ops import Identity +from ccpi.optimisation.ops import FiniteDiff2D + +# Requires CVXPY, see http://www.cvxpy.org/ +# CVXPY can be installed in anaconda using +# conda install -c cvxgrp cvxpy libgcc + +# Whether to use or omit CVXPY + +import numpy as np +import matplotlib.pyplot as plt + +class Algorithm(object): + def __init__(self, *args, **kwargs): + pass + def set_up(self, *args, **kwargs): + raise NotImplementedError() + def update(self): + raise NotImplementedError() + + def should_stop(self): + raise NotImplementedError() + + def __iter__(self): + return self + + def __next__(self): + if self.should_stop(): + raise StopIteration() + else: + self.update() + +class GradientDescent(Algorithm): + x = None + rate = 0 + objective_function = None + regulariser = None + iteration = 0 + stop_cryterion = 'max_iter' + __max_iteration = 0 + __loss = [] + def __init__(self, **kwargs): + args = ['x_init', 'objective_function', 'rate'] + present = True + for k,v in kwargs.items(): + if k in args: + args.pop(args.index(k)) + if len(args) == 0: + return self.set_up(x_init=kwargs['x_init'], + objective_function=kwargs['objective_function'], + rate=kwargs['rate']) + + def should_stop(self): + return self.iteration >= self.max_iteration + + def set_up(self, x_init, objective_function, rate): + self.x = x_init.copy() + self.x_update = x_init.copy() + self.objective_function = objective_function + self.rate = rate + self.__loss.append(objective_function(x_init)) + + def update(self): + + self.objective_function.gradient(self.x, out=self.x_update) + self.x_update *= -self.rate + self.x += self.x_update + self.__loss.append(self.objective_function(self.x)) + self.iteration += 1 + + def get_output(self): + return self.x + def get_current_loss(self): + return self.__loss[-1] + @property + def loss(self): + return self.__loss + @property + def max_iteration(self): + return self.__max_iteration + @max_iteration.setter + def max_iteration(self, value): + assert isinstance(value, int) + self.__max_iteration = value + + + + + +# Problem data. +m = 30 +n = 20 +np.random.seed(1) +Amat = np.random.randn(m, n) +A = LinearOperatorMatrix(Amat) +bmat = np.random.randn(m) +bmat.shape = (bmat.shape[0],1) + +# A = Identity() +# Change n to equal to m. + +b = DataContainer(bmat) + +# Regularization parameter +lam = 10 +opt = {'memopt':True} +# Create object instances with the test data A and b. +f = Norm2sq(A,b,c=0.5, memopt=True) +g0 = ZeroFun() + +# Initial guess +x_init = DataContainer(np.zeros((n,1))) + +f.grad(x_init) + +# Run FISTA for least squares plus zero function. +x_fista0, it0, timing0, criter0 = FISTA(x_init, f, g0 , opt=opt) + +# Print solution and final objective/criterion value for comparison +print("FISTA least squares plus zero function solution and objective value:") +print(x_fista0.array) +print(criter0[-1]) + +gd = GradientDescent(x_init=x_init, objective_function=f, rate=0.001) +gd.max_iteration = 5000 + +for i,el in enumerate(gd): + if i%100 == 0: + print ("\rIteration {} Loss: {}".format(gd.iteration, + gd.get_current_loss())) + + +#%% + + +# +#if use_cvxpy: +# # Compare to CVXPY +# +# # Construct the problem. +# x0 = Variable(n) +# objective0 = Minimize(0.5*sum_squares(Amat*x0 - bmat.T[0]) ) +# prob0 = Problem(objective0) +# +# # The optimal objective is returned by prob.solve(). +# result0 = prob0.solve(verbose=False,solver=SCS,eps=1e-9) +# +# # The optimal solution for x is stored in x.value and optimal objective value +# # is in result as well as in objective.value +# print("CVXPY least squares plus zero function solution and objective value:") +# print(x0.value) +# print(objective0.value) +# +## Plot criterion curve to see FISTA converge to same value as CVX. +#iternum = np.arange(1,1001) +#plt.figure() +#plt.loglog(iternum[[0,-1]],[objective0.value, objective0.value], label='CVX LS') +#plt.loglog(iternum,criter0,label='FISTA LS') +#plt.legend() +#plt.show() +# +## Create 1-norm object instance +#g1 = Norm1(lam) +# +#g1(x_init) +#x_rand = DataContainer(np.reshape(np.random.rand(n),(n,1))) +#x_rand2 = DataContainer(np.reshape(np.random.rand(n-1),(n-1,1))) +#v = g1.prox(x_rand,0.02) +##vv = g1.prox(x_rand2,0.02) +#vv = v.copy() +#vv *= 0 +#print (">>>>>>>>>>vv" , vv.as_array()) +#vv.fill(v) +#print (">>>>>>>>>>fill" , vv.as_array()) +#g1.proximal(x_rand, 0.02, out=vv) +#print (">>>>>>>>>>v" , v.as_array()) +#print (">>>>>>>>>>gradient" , vv.as_array()) +# +#print (">>>>>>>>>>" , (v-vv).as_array()) +#import sys +##sys.exit(0) +## Combine with least squares and solve using generic FISTA implementation +#x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g1,opt=opt) +# +## Print for comparison +#print("FISTA least squares plus 1-norm solution and objective value:") +#print(x_fista1) +#print(criter1[-1]) +# +#if use_cvxpy: +# # Compare to CVXPY +# +# # Construct the problem. +# x1 = Variable(n) +# objective1 = Minimize(0.5*sum_squares(Amat*x1 - bmat.T[0]) + lam*norm(x1,1) ) +# prob1 = Problem(objective1) +# +# # The optimal objective is returned by prob.solve(). +# result1 = prob1.solve(verbose=False,solver=SCS,eps=1e-9) +# +# # The optimal solution for x is stored in x.value and optimal objective value +# # is in result as well as in objective.value +# print("CVXPY least squares plus 1-norm solution and objective value:") +# print(x1.value) +# print(objective1.value) +# +## Now try another algorithm FBPD for same problem: +#x_fbpd1, itfbpd1, timingfbpd1, criterfbpd1 = FBPD(x_init,Identity(), None, f, g1) +#print(x_fbpd1) +#print(criterfbpd1[-1]) +# +## Plot criterion curve to see both FISTA and FBPD converge to same value. +## Note that FISTA is very efficient for 1-norm minimization so it beats +## FBPD in this test by a lot. But FBPD can handle a larger class of problems +## than FISTA can. +#plt.figure() +#plt.loglog(iternum[[0,-1]],[objective1.value, objective1.value], label='CVX LS+1') +#plt.loglog(iternum,criter1,label='FISTA LS+1') +#plt.legend() +#plt.show() +# +#plt.figure() +#plt.loglog(iternum[[0,-1]],[objective1.value, objective1.value], label='CVX LS+1') +#plt.loglog(iternum,criter1,label='FISTA LS+1') +#plt.loglog(iternum,criterfbpd1,label='FBPD LS+1') +#plt.legend() +#plt.show() + +# Now try 1-norm and TV denoising with FBPD, first 1-norm. + +# Set up phantom size NxN by creating ImageGeometry, initialising the +# ImageData object with this geometry and empty array and finally put some +# data into its array, and display as image. +N = 64 +ig = ImageGeometry(voxel_num_x=N,voxel_num_y=N) +Phantom = ImageData(geometry=ig) + +x = Phantom.as_array() +x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 + +plt.imshow(x) +plt.title('Phantom image') +plt.show() + +# Identity operator for denoising +I = TomoIdentity(ig) + +# Data and add noise +y = I.direct(Phantom) +y.array = y.array + 0.1*np.random.randn(N, N) + +plt.imshow(y.array) +plt.title('Noisy image') +plt.show() + + +################### +# Data fidelity term +f_denoise = Norm2sq(I,y,c=0.5,memopt=True) + +# 1-norm regulariser +lam1_denoise = 1.0 +g1_denoise = Norm1(lam1_denoise) + +# Initial guess +x_init_denoise = ImageData(np.zeros((N,N))) + +# Combine with least squares and solve using generic FISTA implementation +x_fista1_denoise, it1_denoise, timing1_denoise, criter1_denoise = \ + FISTA(x_init_denoise, f_denoise, g1_denoise, opt=opt) + +print(x_fista1_denoise) +print(criter1_denoise[-1]) + +f_2 = +gd = GradientDescent(x_init=x_init_denoise, + objective_function=f, rate=0.001) +gd.max_iteration = 5000 + +for i,el in enumerate(gd): + if i%100 == 0: + print ("\rIteration {} Loss: {}".format(gd.iteration, + gd.get_current_loss())) + +plt.imshow(gd.get_output().as_array()) +plt.title('GD image') +plt.show() + diff --git a/Wrappers/Python/wip/old_demos/demo_imat_multichan_RGLTK.py b/Wrappers/Python/wip/old_demos/demo_imat_multichan_RGLTK.py new file mode 100644 index 0000000..8370c78 --- /dev/null +++ b/Wrappers/Python/wip/old_demos/demo_imat_multichan_RGLTK.py @@ -0,0 +1,151 @@ +# This script demonstrates how to load IMAT fits data +# into the CIL optimisation framework and run reconstruction methods. +# +# Demo to reconstruct energy-discretized channels of IMAT data + +# needs dxchange: conda install -c conda-forge dxchange +# needs astropy: conda install astropy + + +# All third-party imports. +import numpy as np +import matplotlib.pyplot as plt +from dxchange.reader import read_fits +from astropy.io import fits + +# All own imports. +from ccpi.framework import AcquisitionData, AcquisitionGeometry, ImageGeometry, ImageData, DataContainer +from ccpi.astra.ops import AstraProjectorSimple, AstraProjector3DSimple +from ccpi.optimisation.algs import CGLS, FISTA +from ccpi.optimisation.funcs import Norm2sq, Norm1 +from ccpi.plugins.regularisers import FGP_TV + +# set main parameters here +n = 512 +totalAngles = 250 # total number of projection angles +# spectral discretization parameter +num_average = 145 # channel discretization frequency (total number of averaged channels) +numChannels = 2970 # 2970 +totChannels = round(numChannels/num_average) # the resulting number of channels +Projections_stack = np.zeros((num_average,n,n),dtype='uint16') +ProjAngleChannels = np.zeros((totalAngles,totChannels,n,n),dtype='float32') + +######################################################################### +print ("Loading the data...") +MainPath = '/media/jakob/050d8d45-fab3-4285-935f-260e6c5f162c1/Data/neutrondata/' # path to data +pathname0 = '{!s}{!s}'.format(MainPath,'PSI_phantom_IMAT/DATA/Sample/') +counterFileName = 4675 +# A main loop over all available angles +for ll in range(0,totalAngles,1): + pathnameData = '{!s}{!s}{!s}/'.format(pathname0,'angle',str(ll)) + filenameCurr = '{!s}{!s}{!s}'.format('IMAT0000',str(counterFileName),'_Tomo_test_000_') + counterT = 0 + # loop over reduced channels (discretized) + for i in range(0,totChannels,1): + sumCount = 0 + # loop over actual channels to obtain averaged one + for j in range(0,num_average,1): + if counterT < 10: + outfile = '{!s}{!s}{!s}{!s}.fits'.format(pathnameData,filenameCurr,'0000',str(counterT)) + if ((counterT >= 10) & (counterT < 100)): + outfile = '{!s}{!s}{!s}{!s}.fits'.format(pathnameData,filenameCurr,'000',str(counterT)) + if ((counterT >= 100) & (counterT < 1000)): + outfile = '{!s}{!s}{!s}{!s}.fits'.format(pathnameData,filenameCurr,'00',str(counterT)) + if ((counterT >= 1000) & (counterT < 10000)): + outfile = '{!s}{!s}{!s}{!s}.fits'.format(pathnameData,filenameCurr,'0',str(counterT)) + try: + Projections_stack[j,:,:] = read_fits(outfile) + except: + print("Fits is corrupted, skipping no.", counterT) + sumCount -= 1 + counterT += 1 + sumCount += 1 + AverageProj=np.sum(Projections_stack,axis=0)/sumCount # averaged projection over "num_average" channels + ProjAngleChannels[ll,i,:,:] = AverageProj + print("Angle is processed", ll) + counterFileName += 1 +######################################################################### + +flat1 = read_fits('{!s}{!s}{!s}'.format(MainPath,'PSI_phantom_IMAT/DATA/','OpenBeam_aft1/IMAT00004932_Tomo_test_000_SummedImg.fits')) +nonzero = flat1 > 0 +# Apply flat field and take negative log +for ll in range(0,totalAngles,1): + for i in range(0,totChannels,1): + ProjAngleChannels[ll,i,nonzero] = ProjAngleChannels[ll,i,nonzero]/flat1[nonzero] + +eqzero = ProjAngleChannels == 0 +ProjAngleChannels[eqzero] = 1 +ProjAngleChannels_NormLog = -np.log(ProjAngleChannels) # normalised and neg-log data + +# extact sinogram over energy channels +selectedVertical_slice = 256 +sino_all_channels = ProjAngleChannels_NormLog[:,:,:,selectedVertical_slice] +# Set angles to use +angles = np.linspace(-np.pi,np.pi,totalAngles,endpoint=False) + +# set the geometry +ig = ImageGeometry(n,n) +ag = AcquisitionGeometry('parallel', + '2D', + angles, + n,1) +Aop = AstraProjectorSimple(ig, ag, 'gpu') + + +# loop to reconstruct energy channels +REC_chan = np.zeros((totChannels, n, n), 'float32') +for i in range(0,totChannels,1): + sino_channel = sino_all_channels[:,i,:] # extract a sinogram for i-th channel + + print ("Initial guess") + x_init = ImageData(geometry=ig) + + # Create least squares object instance with projector and data. + print ("Create least squares object instance with projector and data.") + f = Norm2sq(Aop,DataContainer(sino_channel),c=0.5) + + print ("Run FISTA-TV for least squares") + lamtv = 5 + opt = {'tol': 1e-4, 'iter': 200} + g_fgp = FGP_TV(lambdaReg = lamtv, + iterationsTV=50, + tolerance=1e-6, + methodTV=0, + nonnegativity=0, + printing=0, + device='gpu') + + x_fista_fgp, it1, timing1, criter_fgp = FISTA(x_init, f, g_fgp, opt) + REC_chan[i,:,:] = x_fista_fgp.array + """ + plt.figure() + plt.subplot(121) + plt.imshow(x_fista_fgp.array, vmin=0, vmax=0.05) + plt.title('FISTA FGP TV') + plt.subplot(122) + plt.semilogy(criter_fgp) + plt.show() + """ + """ + print ("Run CGLS for least squares") + opt = {'tol': 1e-4, 'iter': 20} + x_init = ImageData(geometry=ig) + x_CGLS, it_CGLS, timing_CGLS, criter_CGLS = CGLS(x_init, Aop, DataContainer(sino_channel), opt=opt) + + plt.figure() + plt.imshow(x_CGLS.array,vmin=0, vmax=0.05) + plt.title('CGLS') + plt.show() + """ +# Saving images into fits using astrapy if required +counter = 0 +filename = 'FISTA_TV_imat_slice' +for i in range(totChannels): + im = REC_chan[i,:,:] + add_val = np.min(im[:]) + im += abs(add_val) + im = im/np.max(im[:])*65535 + outfile = '{!s}_{!s}_{!s}.fits'.format(filename,str(selectedVertical_slice),str(counter)) + hdu = fits.PrimaryHDU(np.uint16(im)) + hdu.writeto(outfile, overwrite=True) + counter += 1
\ No newline at end of file diff --git a/Wrappers/Python/wip/old_demos/demo_imat_whitebeam.py b/Wrappers/Python/wip/old_demos/demo_imat_whitebeam.py new file mode 100644 index 0000000..e0d213e --- /dev/null +++ b/Wrappers/Python/wip/old_demos/demo_imat_whitebeam.py @@ -0,0 +1,138 @@ +# This script demonstrates how to load IMAT fits data +# into the CIL optimisation framework and run reconstruction methods. +# +# This demo loads the summedImg files which are the non-spectral images +# resulting from summing projections over all spectral channels. + +# needs dxchange: conda install -c conda-forge dxchange +# needs astropy: conda install astropy + + +# All third-party imports. +import numpy +from scipy.io import loadmat +import matplotlib.pyplot as plt +from dxchange.reader import read_fits + +# All own imports. +from ccpi.framework import AcquisitionData, AcquisitionGeometry, ImageGeometry, ImageData +from ccpi.astra.ops import AstraProjectorSimple, AstraProjector3DSimple +from ccpi.optimisation.algs import CGLS, FISTA +from ccpi.optimisation.funcs import Norm2sq, Norm1 + +# Load and display a couple of summed projection as examples +pathname0 = '/media/newhd/shared/Data/neutrondata/PSI_phantom_IMAT/DATA/Sample/angle0/' +filename0 = 'IMAT00004675_Tomo_test_000_SummedImg.fits' + +data0 = read_fits(pathname0 + filename0) + +pathname10 = '/media/newhd/shared/Data/neutrondata/PSI_phantom_IMAT/DATA/Sample/angle10/' +filename10 = 'IMAT00004685_Tomo_test_000_SummedImg.fits' + +data10 = read_fits(pathname10 + filename10) + +# Load a flat field (more are available, should we average over them?) +flat1 = read_fits('/media/newhd/shared/Data/neutrondata/PSI_phantom_IMAT/DATA/OpenBeam_aft1/IMAT00004932_Tomo_test_000_SummedImg.fits') + +# Apply flat field and display after flat-field correction and negative log +data0_rel = numpy.zeros(numpy.shape(flat1), dtype = float) +nonzero = flat1 > 0 +data0_rel[nonzero] = data0[nonzero] / flat1[nonzero] +data10_rel = numpy.zeros(numpy.shape(flat1), dtype = float) +data10_rel[nonzero] = data10[nonzero] / flat1[nonzero] + +plt.imshow(data0_rel) +plt.colorbar() +plt.show() + +plt.imshow(-numpy.log(data0_rel)) +plt.colorbar() +plt.show() + +plt.imshow(data10_rel) +plt.colorbar() +plt.show() + +plt.imshow(-numpy.log(data10_rel)) +plt.colorbar() +plt.show() + +# Set up for loading all summed images at 250 angles. +pathname = '/media/newhd/shared/Data/neutrondata/PSI_phantom_IMAT/DATA/Sample/angle{}/' +filename = 'IMAT0000{}_Tomo_test_000_SummedImg.fits' + +# Dimensions +num_angles = 250 +imsize = 512 + +# Initialise array +data = numpy.zeros((num_angles,imsize,imsize)) + +# Load only 0-249, as 250 is at repetition of zero degrees just like image 0 +for i in range(0,250): + curimfile = (pathname + filename).format(i, i+4675) + data[i,:,:] = read_fits(curimfile) + +# Apply flat field and take negative log +nonzero = flat1 > 0 +for i in range(0,250): + data[i,nonzero] = data[i,nonzero]/flat1[nonzero] + +eqzero = data == 0 +data[eqzero] = 1 + +data_rel = -numpy.log(data) + +# Permute order to get: angles, vertical, horizontal, as default in framework. +data_rel = numpy.transpose(data_rel,(0,2,1)) + +# Set angles to use +angles = numpy.linspace(-numpy.pi,numpy.pi,num_angles,endpoint=False) + +# Create 3D acquisition geometry and acquisition data +ag = AcquisitionGeometry('parallel', + '3D', + angles, + pixel_num_h=imsize, + pixel_num_v=imsize) +b = AcquisitionData(data_rel, geometry=ag) + +# Reduce to single (noncentral) slice by extracting relevant parameters from data and its +# geometry. Perhaps create function to extract central slice automatically? +b2d = b.subset(vertical=128) +ag2d = AcquisitionGeometry('parallel', + '2D', + ag.angles, + pixel_num_h=ag.pixel_num_h) +b2d.geometry = ag2d + +# Create 2D image geometry +ig2d = ImageGeometry(voxel_num_x=ag2d.pixel_num_h, + voxel_num_y=ag2d.pixel_num_h) + +# Create GPU projector/backprojector operator with ASTRA. +Aop = AstraProjectorSimple(ig2d,ag2d,'gpu') + +# Demonstrate operator is working by applying simple backprojection. +z = Aop.adjoint(b2d) +plt.imshow(z.array) +plt.title('Simple backprojection') +plt.colorbar() +plt.show() + +# Set initial guess ImageData with zeros for algorithms, and algorithm options. +x_init = ImageData(numpy.zeros((imsize,imsize)), + geometry=ig2d) +opt_CGLS = {'tol': 1e-4, 'iter': 20} + +# Run CGLS algorithm and display reconstruction. +x_CGLS, it_CGLS, timing_CGLS, criter_CGLS = CGLS(x_init, Aop, b2d, opt_CGLS) + +plt.imshow(x_CGLS.array) +plt.title('CGLS') +plt.colorbar() +plt.show() + +plt.semilogy(criter_CGLS) +plt.title('CGLS Criterion vs iterations') +plt.show()
\ No newline at end of file diff --git a/Wrappers/Python/wip/old_demos/demo_memhandle.py b/Wrappers/Python/wip/old_demos/demo_memhandle.py new file mode 100755 index 0000000..db48d73 --- /dev/null +++ b/Wrappers/Python/wip/old_demos/demo_memhandle.py @@ -0,0 +1,193 @@ + +from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, DataContainer +from ccpi.optimisation.algs import FISTA, FBPD, CGLS +from ccpi.optimisation.funcs import Norm2sq, ZeroFun, Norm1, TV2D + +from ccpi.optimisation.ops import LinearOperatorMatrix, Identity +from ccpi.optimisation.ops import TomoIdentity + +# Requires CVXPY, see http://www.cvxpy.org/ +# CVXPY can be installed in anaconda using +# conda install -c cvxgrp cvxpy libgcc + + +import numpy as np +import matplotlib.pyplot as plt + +# Problem data. +m = 30 +n = 20 +np.random.seed(1) +Amat = np.random.randn(m, n) +A = LinearOperatorMatrix(Amat) +bmat = np.random.randn(m) +bmat.shape = (bmat.shape[0],1) + + + +# A = Identity() +# Change n to equal to m. + +b = DataContainer(bmat) + +# Regularization parameter +lam = 10 + +# Create object instances with the test data A and b. +f = Norm2sq(A,b,c=0.5, memopt=True) +g0 = ZeroFun() + +# Initial guess +x_init = DataContainer(np.zeros((n,1))) + +f.grad(x_init) +opt = {'memopt': True} +# Run FISTA for least squares plus zero function. +x_fista0, it0, timing0, criter0 = FISTA(x_init, f, g0) +x_fista0_m, it0_m, timing0_m, criter0_m = FISTA(x_init, f, g0, opt=opt) + +iternum = [i for i in range(len(criter0))] +# Print solution and final objective/criterion value for comparison +print("FISTA least squares plus zero function solution and objective value:") +print(x_fista0.array) +print(criter0[-1]) + + +# Plot criterion curve to see FISTA converge to same value as CVX. +#iternum = np.arange(1,1001) +plt.figure() +plt.loglog(iternum,criter0,label='FISTA LS') +plt.loglog(iternum,criter0_m,label='FISTA LS memopt') +plt.legend() +plt.show() +#%% +# Create 1-norm object instance +g1 = Norm1(lam) + +g1(x_init) +g1.prox(x_init,0.02) + +# Combine with least squares and solve using generic FISTA implementation +x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g1) +x_fista1_m, it1_m, timing1_m, criter1_m = FISTA(x_init, f, g1, opt=opt) +iternum = [i for i in range(len(criter1))] +# Print for comparison +print("FISTA least squares plus 1-norm solution and objective value:") +print(x_fista1) +print(criter1[-1]) + + +# Now try another algorithm FBPD for same problem: +x_fbpd1, itfbpd1, timingfbpd1, criterfbpd1 = FBPD(x_init, None, f, g1) +iternum = [i for i in range(len(criterfbpd1))] +print(x_fbpd1) +print(criterfbpd1[-1]) + +# Plot criterion curve to see both FISTA and FBPD converge to same value. +# Note that FISTA is very efficient for 1-norm minimization so it beats +# FBPD in this test by a lot. But FBPD can handle a larger class of problems +# than FISTA can. +plt.figure() +plt.loglog(iternum,criter1,label='FISTA LS+1') +plt.loglog(iternum,criter1_m,label='FISTA LS+1 memopt') +plt.legend() +plt.show() + +plt.figure() +plt.loglog(iternum,criter1,label='FISTA LS+1') +plt.loglog(iternum,criterfbpd1,label='FBPD LS+1') +plt.legend() +plt.show() +#%% +# Now try 1-norm and TV denoising with FBPD, first 1-norm. + +# Set up phantom size NxN by creating ImageGeometry, initialising the +# ImageData object with this geometry and empty array and finally put some +# data into its array, and display as image. +N = 1000 +ig = ImageGeometry(voxel_num_x=N,voxel_num_y=N) +Phantom = ImageData(geometry=ig) + +x = Phantom.as_array() +x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 + +plt.imshow(x) +plt.title('Phantom image') +plt.show() + +# Identity operator for denoising +I = TomoIdentity(ig) + +# Data and add noise +y = I.direct(Phantom) +y.array += 0.1*np.random.randn(N, N) + +plt.figure() +plt.imshow(y.array) +plt.title('Noisy image') +plt.show() + +# Data fidelity term +f_denoise = Norm2sq(I,y,c=0.5, memopt=True) + +# 1-norm regulariser +lam1_denoise = 1.0 +g1_denoise = Norm1(lam1_denoise) + +# Initial guess +x_init_denoise = ImageData(np.zeros((N,N))) +opt = {'memopt': False, 'iter' : 50} +# Combine with least squares and solve using generic FISTA implementation +print ("no memopt") +x_fista1_denoise, it1_denoise, timing1_denoise, \ + criter1_denoise = FISTA(x_init_denoise, f_denoise, g1_denoise, opt=opt) +opt = {'memopt': True, 'iter' : 50} +print ("yes memopt") +x_fista1_denoise_m, it1_denoise_m, timing1_denoise_m, \ + criter1_denoise_m = FISTA(x_init_denoise, f_denoise, g1_denoise, opt=opt) + +print(x_fista1_denoise) +print(criter1_denoise[-1]) + +plt.figure() +plt.imshow(x_fista1_denoise.as_array()) +plt.title('FISTA LS+1') +plt.show() + +plt.figure() +plt.imshow(x_fista1_denoise_m.as_array()) +plt.title('FISTA LS+1 memopt') +plt.show() + +plt.figure() +plt.loglog(iternum,criter1_denoise,label='FISTA LS+1') +plt.loglog(iternum,criter1_denoise_m,label='FISTA LS+1 memopt') +plt.legend() +plt.show() +#%% +# Now denoise LS + 1-norm with FBPD +x_fbpd1_denoise, itfbpd1_denoise, timingfbpd1_denoise, criterfbpd1_denoise = FBPD(x_init_denoise, None, f_denoise, g1_denoise) +print(x_fbpd1_denoise) +print(criterfbpd1_denoise[-1]) + +plt.figure() +plt.imshow(x_fbpd1_denoise.as_array()) +plt.title('FBPD LS+1') +plt.show() + + +# Now TV with FBPD +lam_tv = 0.1 +gtv = TV2D(lam_tv) +gtv(gtv.op.direct(x_init_denoise)) + +opt_tv = {'tol': 1e-4, 'iter': 10000} + +x_fbpdtv_denoise, itfbpdtv_denoise, timingfbpdtv_denoise, criterfbpdtv_denoise = FBPD(x_init_denoise, None, f_denoise, gtv,opt=opt_tv) +print(x_fbpdtv_denoise) +print(criterfbpdtv_denoise[-1]) + +plt.imshow(x_fbpdtv_denoise.as_array()) +plt.title('FBPD TV') +plt.show() diff --git a/Wrappers/Python/wip/demo_test_sirt.py b/Wrappers/Python/wip/old_demos/demo_test_sirt.py index 6f5a44d..6f5a44d 100644 --- a/Wrappers/Python/wip/demo_test_sirt.py +++ b/Wrappers/Python/wip/old_demos/demo_test_sirt.py diff --git a/Wrappers/Python/wip/old_demos/multifile_nexus.py b/Wrappers/Python/wip/old_demos/multifile_nexus.py new file mode 100755 index 0000000..d1ad438 --- /dev/null +++ b/Wrappers/Python/wip/old_demos/multifile_nexus.py @@ -0,0 +1,307 @@ +# -*- coding: utf-8 -*-
+"""
+Created on Wed Aug 15 16:00:53 2018
+
+@author: ofn77899
+"""
+
+import os
+from ccpi.io.reader import NexusReader
+
+from sys import getsizeof
+
+import matplotlib.pyplot as plt
+
+from ccpi.framework import DataProcessor, DataContainer
+from ccpi.processors import Normalizer
+from ccpi.processors import CenterOfRotationFinder
+import numpy
+
+
+class averager(object):
+ def __init__(self):
+ self.reset()
+
+ def reset(self):
+ self.N = 0
+ self.avg = 0
+ self.min = 0
+ self.max = 0
+ self.var = 0
+ self.ske = 0
+ self.kur = 0
+
+ def add_reading(self, val):
+
+ if (self.N == 0):
+ self.avg = val;
+ self.min = val;
+ self.max = val;
+ elif (self.N == 1):
+ #//set min/max
+ self.max = val if val > self.max else self.max
+ self.min = val if val < self.min else self.min
+
+
+ thisavg = (self.avg + val)/2
+ #// initial value is in avg
+ self.var = (self.avg - thisavg)*(self.avg-thisavg) + (val - thisavg) * (val-thisavg)
+ self.ske = self.var * (self.avg - thisavg)
+ self.kur = self.var * self.var
+ self.avg = thisavg
+ else:
+ self.max = val if val > self.max else self.max
+ self.min = val if val < self.min else self.min
+
+ M = self.N
+
+ #// b-factor =(<v>_N + v_(N+1)) / (N+1)
+ #float b = (val -avg) / (M+1);
+ b = (val -self.avg) / (M+1)
+
+ self.kur = self.kur + (M *(b*b*b*b) * (1+M*M*M))- (4* b * self.ske) + (6 * b *b * self.var * (M-1))
+
+ self.ske = self.ske + (M * b*b*b *(M-1)*(M+1)) - (3 * b * self.var * (M-1))
+
+ #//var = var * ((M-1)/M) + ((val - avg)*(val - avg)/(M+1)) ;
+ self.var = self.var * ((M-1)/M) + (b * b * (M+1))
+
+ self.avg = self.avg * (M/(M+1)) + val / (M+1)
+
+ self.N += 1
+
+ def stats(self, vector):
+ i = 0
+ while i < vector.size:
+ self.add_reading(vector[i])
+ i+=1
+
+avg = averager()
+a = numpy.linspace(0,39,40)
+avg.stats(a)
+print ("average" , avg.avg, a.mean())
+print ("variance" , avg.var, a.var())
+b = a - a.mean()
+b *= b
+b = numpy.sqrt(sum(b)/(a.size-1))
+print ("std" , numpy.sqrt(avg.var), b)
+#%%
+
+class DataStatMoments(DataProcessor):
+ '''Normalization based on flat and dark
+
+ This processor read in a AcquisitionData and normalises it based on
+ the instrument reading with and without incident photons or neutrons.
+
+ Input: AcquisitionData
+ Parameter: 2D projection with flat field (or stack)
+ 2D projection with dark field (or stack)
+ Output: AcquisitionDataSetn
+ '''
+
+ def __init__(self, axis, skewness=False, kurtosis=False, offset=0):
+ kwargs = {
+ 'axis' : axis,
+ 'skewness' : skewness,
+ 'kurtosis' : kurtosis,
+ 'offset' : offset,
+ }
+ #DataProcessor.__init__(self, **kwargs)
+ super(DataStatMoments, self).__init__(**kwargs)
+
+
+ def check_input(self, dataset):
+ #self.N = dataset.get_dimension_size(self.axis)
+ return True
+
+ @staticmethod
+ def add_sample(dataset, N, axis, stats=None, offset=0):
+ #dataset = self.get_input()
+ if (N == 0):
+ # get the axis number along to calculate the stats
+
+
+ #axs = dataset.get_dimension_size(self.axis)
+ # create a placeholder for the output
+ if stats is None:
+ ax = dataset.get_dimension_axis(axis)
+ shape = [dataset.shape[i] for i in range(len(dataset.shape)) if i != ax]
+ # output has 4 components avg, std, skewness and kurtosis + last avg+ (val-thisavg)
+ shape.insert(0, 4+2)
+ stats = numpy.zeros(shape)
+
+ stats[0] = dataset.subset(**{axis:N-offset}).array[:]
+
+ #avg = val
+ elif (N == 1):
+ # val
+ stats[5] = dataset.subset(**{axis:N-offset}).array
+ stats[4] = stats[0] + stats[5]
+ stats[4] /= 2 # thisavg
+ stats[5] -= stats[4] # (val - thisavg)
+
+ #float thisavg = (avg + val)/2;
+
+ #// initial value is in avg
+ #var = (avg - thisavg)*(avg-thisavg) + (val - thisavg) * (val-thisavg);
+ stats[1] = stats[5] * stats[5] + stats[5] * stats[5]
+ #skewness = var * (avg - thisavg);
+ stats[2] = stats[1] * stats[5]
+ #kurtosis = var * var;
+ stats[3] = stats[1] * stats[1]
+ #avg = thisavg;
+ stats[0] = stats[4]
+ else:
+
+ #float M = (float)N;
+ M = N
+ #val
+ stats[4] = dataset.subset(**{axis:N-offset}).array
+ #// b-factor =(<v>_N + v_(N+1)) / (N+1)
+ stats[5] = stats[4] - stats[0]
+ stats[5] /= (M+1) # b factor
+ #float b = (val -avg) / (M+1);
+
+ #kurtosis = kurtosis + (M *(b*b*b*b) * (1+M*M*M))- (4* b * skewness) + (6 * b *b * var * (M-1));
+ #if self.kurtosis:
+ # stats[3] += (M * stats[5] * stats[5] * stats[5] * stats[5]) - \
+ # (4 * stats[5] * stats[2]) + \
+ # (6 * stats[5] * stats[5] * stats[1] * (M-1))
+
+ #skewness = skewness + (M * b*b*b *(M-1)*(M+1)) - (3 * b * var * (M-1));
+ #if self.skewness:
+ # stats[2] = stats[2] + (M * stats[5]* stats[5] * stats[5] * (M-1)*(M-1) ) -\
+ # 3 * stats[5] * stats[1] * (M-1)
+ #//var = var * ((M-1)/M) + ((val - avg)*(val - avg)/(M+1)) ;
+ #var = var * ((M-1)/M) + (b * b * (M+1));
+ stats[1] = ((M-1)/M) * stats[1] + (stats[5] * stats[5] * (M+1))
+
+ #avg = avg * (M/(M+1)) + val / (M+1)
+ stats[0] = stats[0] * (M/(M+1)) + stats[4] / (M+1)
+
+ N += 1
+ return stats , N
+
+
+ def process(self):
+
+ data = self.get_input()
+
+ #stats, i = add_sample(0)
+ N = data.get_dimension_size(self.axis)
+ ax = data.get_dimension_axis(self.axis)
+ stats = None
+ i = 0
+ while i < N:
+ stats , i = DataStatMoments.add_sample(data, i, self.axis, stats, offset=self.offset)
+ self.offset += N
+ labels = ['StatisticalMoments']
+
+ labels += [data.dimension_labels[i] \
+ for i in range(len(data.dimension_labels)) if i != ax]
+ y = DataContainer( stats[:4] , False,
+ dimension_labels=labels)
+ return y
+
+directory = r'E:\Documents\Dataset\CCPi\Nexus_test'
+data_path="entry1/instrument/pco1_hw_hdf_nochunking/data"
+
+reader = NexusReader(os.path.join( os.path.abspath(directory) , '74331.nxs'))
+
+print ("read flat")
+read_flat = NexusReader(os.path.join( os.path.abspath(directory) , '74240.nxs'))
+read_flat.data_path = data_path
+flatsslice = read_flat.get_acquisition_data_whole()
+avg = DataStatMoments('angle')
+
+avg.set_input(flatsslice)
+flats = avg.get_output()
+
+ave = averager()
+ave.stats(flatsslice.array[:,0,0])
+
+print ("avg" , ave.avg, flats.array[0][0][0])
+print ("var" , ave.var, flats.array[1][0][0])
+
+print ("read dark")
+read_dark = NexusReader(os.path.join( os.path.abspath(directory) , '74243.nxs'))
+read_dark.data_path = data_path
+
+## darks are very many so we proceed in batches
+total_size = read_dark.get_projection_dimensions()[0]
+print ("total_size", total_size)
+
+batchsize = 40
+if batchsize > total_size:
+ batchlimits = [batchsize * (i+1) for i in range(int(total_size/batchsize))] + [total_size-1]
+else:
+ batchlimits = [total_size]
+#avg.N = 0
+avg.offset = 0
+N = 0
+for batch in range(len(batchlimits)):
+ print ("running batch " , batch)
+ bmax = batchlimits[batch]
+ bmin = bmax-batchsize
+
+ darksslice = read_dark.get_acquisition_data_batch(bmin,bmax)
+ if batch == 0:
+ #create stats
+ ax = darksslice.get_dimension_axis('angle')
+ shape = [darksslice.shape[i] for i in range(len(darksslice.shape)) if i != ax]
+ # output has 4 components avg, std, skewness and kurtosis + last avg+ (val-thisavg)
+ shape.insert(0, 4+2)
+ print ("create stats shape ", shape)
+ stats = numpy.zeros(shape)
+ print ("N" , N)
+ #avg.set_input(darksslice)
+ i = bmin
+ while i < bmax:
+ stats , i = DataStatMoments.add_sample(darksslice, i, 'angle', stats, bmin)
+ print ("{0}-{1}-{2}".format(bmin, i , bmax ) )
+
+darks = stats
+#%%
+
+fig = plt.subplot(2,2,1)
+fig.imshow(flats.subset(StatisticalMoments=0).array)
+fig = plt.subplot(2,2,2)
+fig.imshow(numpy.sqrt(flats.subset(StatisticalMoments=1).array))
+fig = plt.subplot(2,2,3)
+fig.imshow(darks[0])
+fig = plt.subplot(2,2,4)
+fig.imshow(numpy.sqrt(darks[1]))
+plt.show()
+
+
+#%%
+norm = Normalizer(flat_field=flats.array[0,200,:], dark_field=darks[0,200,:])
+#norm.set_flat_field(flats.array[0,200,:])
+#norm.set_dark_field(darks.array[0,200,:])
+norm.set_input(reader.get_acquisition_data_slice(200))
+
+n = Normalizer.normalize_projection(norm.get_input().as_array(), flats.array[0,200,:], darks[0,200,:], 1e-5)
+#dn_n= Normalizer.estimate_normalised_error(norm.get_input().as_array(), flats.array[0,200,:], darks[0,200,:],
+# numpy.sqrt(flats.array[1,200,:]), numpy.sqrt(darks[1,200,:]))
+#%%
+
+
+#%%
+fig = plt.subplot(2,1,1)
+
+
+fig.imshow(norm.get_input().as_array())
+fig = plt.subplot(2,1,2)
+fig.imshow(n)
+
+#fig = plt.subplot(3,1,3)
+#fig.imshow(dn_n)
+
+
+plt.show()
+
+
+
+
+
+
diff --git a/Wrappers/Python/wip/pdhg_TV_denoising_precond.py b/Wrappers/Python/wip/pdhg_TV_denoising_precond.py new file mode 100644 index 0000000..3fc9320 --- /dev/null +++ b/Wrappers/Python/wip/pdhg_TV_denoising_precond.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Feb 22 14:53:03 2019 + +@author: evangelos +""" + +from ccpi.framework import ImageData, ImageGeometry, BlockDataContainer + +import numpy as np +import matplotlib.pyplot as plt + +from ccpi.optimisation.algorithms import PDHG, PDHG_old + +from ccpi.optimisation.operators import BlockOperator, Identity, Gradient +from ccpi.optimisation.functions import ZeroFun, L2NormSquared, \ + MixedL21Norm, FunctionOperatorComposition, BlockFunction, ScaledFunction + +from skimage.util import random_noise + + + +# ############################################################################ +# Create phantom for TV denoising + +N = 100 +data = np.zeros((N,N)) +data[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5 +data[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1 + +ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N) +ag = ig + +# Create noisy data. Add Gaussian noise +n1 = random_noise(data, mode='gaussian', seed=10) +noisy_data = ImageData(n1) + + +#%% + +# Regularisation Parameter +alpha = 2 + +#method = input("Enter structure of PDHG (0=Composite or 1=NotComposite): ") +method = '0' +if method == '0': + + # Create operators + op1 = Gradient(ig) + op2 = Identity(ig, ag) + + # Form Composite Operator + operator = BlockOperator(op1, op2, shape=(2,1) ) + + #### Create functions +# f = FunctionComposition_new(operator, mixed_L12Norm(alpha), \ +# L2NormSq(0.5, b = noisy_data) ) + + f1 = alpha * MixedL21Norm() + f2 = 0.5 * L2NormSquared(b = noisy_data) + + f = BlockFunction(f1, f2 ) + g = ZeroFun() + +else: + + ########################################################################### + # No Composite # + ########################################################################### + operator = Gradient(ig) + f = alpha * FunctionOperatorComposition(operator, MixedL21Norm()) + g = 0.5 * L2NormSquared(b = noisy_data) + ########################################################################### +#%% + +diag_precon = True + +if diag_precon: + + def tau_sigma_precond(operator): + + tau = 1/operator.sum_abs_row() + sigma = 1/ operator.sum_abs_col() + + return tau, sigma + + tau, sigma = tau_sigma_precond(operator) + +else: + # Compute operator Norm + normK = operator.norm() + print ("normK", normK) + # Primal & dual stepsizes + sigma = 1/normK + tau = 1/normK +# tau = 1/(sigma*normK**2) + +#%% + +opt = {'niter':2000} + +res, time, primal, dual, pdgap = PDHG_old(f, g, operator, tau = tau, sigma = sigma, opt = opt) + +plt.figure(figsize=(5,5)) +plt.imshow(res.as_array()) +plt.colorbar() +plt.show() + +#aaa = res[0].as_array() +# +#plt.imshow(aaa) +#plt.colorbar() +#plt.show() +#c2 = aaa +#del aaa +#%% + +#c2 = aaa +##%% +#%% +#z = c1 - c2 +#plt.imshow(np.abs(z[0:95,0:95])) +#plt.colorbar() + +#%% +#pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma) +#pdhg.max_iteration = 2000 +#pdhg.update_objective_interval = 10 +# +#pdhg.run(2000) +# +# +# +#sol = pdhg.get_output().as_array() +##sol = result.as_array() +## +#fig = plt.figure() +#plt.subplot(1,2,1) +#plt.imshow(noisy_data.as_array()) +##plt.colorbar() +#plt.subplot(1,2,2) +#plt.imshow(sol) +##plt.colorbar() +#plt.show() +## +# +### +#plt.plot(np.linspace(0,N,N), data[int(N/2),:], label = 'GTruth') +#plt.plot(np.linspace(0,N,N), sol[int(N/2),:], label = 'Recon') +#plt.legend() +#plt.show() +# + +#%% +# |