From af5450ea4c3a47fca0e9917c6739f248bf3a79df Mon Sep 17 00:00:00 2001 From: Edoardo Pasca Date: Wed, 10 Jan 2018 14:57:58 +0000 Subject: Added conda recipe Added Methods to DataSet. Added SliceDataSet and VolumeDataSet (TODO make them useful) Added DataSetProcessor and some examples --- Wrappers/Python/ccpi/__init__.py | 18 +++ Wrappers/Python/ccpi/common.py | 231 +++++++++++++++++++++++++++++++-- Wrappers/Python/conda-recipe/bld.bat | 9 ++ Wrappers/Python/conda-recipe/build.sh | 9 ++ Wrappers/Python/conda-recipe/meta.yaml | 26 ++++ Wrappers/Python/setup.py | 107 +++++++-------- 6 files changed, 330 insertions(+), 70 deletions(-) create mode 100644 Wrappers/Python/ccpi/__init__.py create mode 100644 Wrappers/Python/conda-recipe/bld.bat create mode 100644 Wrappers/Python/conda-recipe/build.sh create mode 100644 Wrappers/Python/conda-recipe/meta.yaml (limited to 'Wrappers/Python') diff --git a/Wrappers/Python/ccpi/__init__.py b/Wrappers/Python/ccpi/__init__.py new file mode 100644 index 0000000..cf2d93d --- /dev/null +++ b/Wrappers/Python/ccpi/__init__.py @@ -0,0 +1,18 @@ +# -*- 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. \ No newline at end of file diff --git a/Wrappers/Python/ccpi/common.py b/Wrappers/Python/ccpi/common.py index 9d7780e..e2816db 100644 --- a/Wrappers/Python/ccpi/common.py +++ b/Wrappers/Python/ccpi/common.py @@ -21,6 +21,7 @@ import numpy import os import sys import time +import vtk if sys.version_info[0] >= 3 and sys.version_info[1] >= 4: ABC = abc.ABC @@ -31,11 +32,18 @@ def find_key(dic, val): """return the key of dictionary dic given the value""" return [k for k, v in dic.items() if v == val][0] -class CCPiBaseClass(object): +class CCPiBaseClass(ABC): def __init__(self, **kwargs): self.acceptedInputKeywords = [] self.pars = {} self.debug = True + # add keyworded arguments as accepted input keywords and add to the + # parameters + for key, value in kwargs.items(): + self.acceptedInputKeywords.append(key) + #print ("key {0}".format(key)) + #self.setParameter(key.__name__=value) + self.setParameter(**{key:value}) def setParameter(self, **kwargs): '''set named parameter for the reconstructor engine @@ -70,8 +78,8 @@ class CCPiBaseClass(object): if self.debug: print ("{0}: {1}".format(self.__class__.__name__, msg)) -class DataSet(ABC): - '''Abstract class to hold data''' +class DataSet(): + '''Generic class to hold data''' def __init__ (self, array, deep_copy=True, dimension_labels=None, **kwargs): @@ -88,11 +96,14 @@ class DataSet(ABC): else: for i in range(self.number_of_dimensions): self.dimension_labels[i] = 'dimension_{0:02}'.format(i) - - if deep_copy: - self.array = array[:] + + if type(array) == numpy.ndarray: + if deep_copy: + self.array = array[:] + else: + self.array = array else: - self.array = array + raise TypeError('Array must be NumpyArray') def as_array(self, dimensions=None): '''Returns the DataSet as Numpy Array @@ -163,11 +174,181 @@ class DataSet(ABC): cleaned = numpy.transpose(cleaned, axes).copy() return DataSet(cleaned , True, dimensions) + + def fill(self, array): + '''fills the internal numpy array with the one provided''' + if numpy.shape(array) != numpy.shape(self.array): + raise ValueError('Cannot fill with the provided array.' + \ + 'Expecting {0} got {1}'.format( + numpy.shape(self.array), + numpy.shape(array))) + self.array = array[:] + + +class SliceData(DataSet): + '''DataSet for holding 2D images''' + def __init__(self, array, deep_copy=True, dimension_labels=None, + **kwargs): + + if type(array) == DataSet: + # if the array is a DataSet get the info from there + if array.number_of_dimensions != 2: + raise ValueError('Number of dimensions are != 2: {0}'\ + .format(array.number_of_dimensions)) + + DataSet.__init__(self, array.as_array(), deep_copy, + array.dimension_labels, **kwargs) + elif type(array) == numpy.ndarray: + if dimension_labels is None: + dimension_labels = ['horizontal_x' , 'horizontal_y' , 'vertical'] + shape = numpy.shape(array) + ndims = len(shape) + if ndims != 3: + raise ValueError('Number of dimensions are != 2: {0}'.format(ndims)) + + DataSet.__init__(self, array, deep_copy, dimension_labels, **kwargs) + + # Metadata + self.origin = [0,0] + self.spacing = [1,1] + + # load metadata from kwargs if present + for key, value in kwargs.items(): + if key == 'origin' : + if type(value) == list and len (value) == 2: + self.origin = value + if key == 'spacing' : + if type(value) == list and len (value) == 2: + self.spacing = value + def rotate(self, center_of_rotation, angle): + pass + + +class VolumeData(DataSet): + '''DataSet for holding 3D images''' + def __init__(self, array, deep_copy=True, dimension_labels=None, + **kwargs): + if type(array) == DataSet: + # if the array is a DataSet get the info from there + if array.number_of_dimensions != 3: + raise ValueError('Number of dimensions are != 3: {0}'\ + .format(array.number_of_dimensions)) + DataSet.__init__(self, array.as_array(), deep_copy, + array.dimension_labels, **kwargs) + elif type(array) == numpy.ndarray: + if dimension_labels is None: + dimension_labels = ['horizontal_x' , 'horizontal_y' , 'vertical'] + shape = numpy.shape(array) + ndims = len(shape) + if ndims != 3: + raise ValueError('Number of dimensions are != 3: {0}'.format(ndims)) + DataSet.__init__(self, array, deep_copy, dimension_labels, **kwargs) + + # Metadata + self.origin = [0,0,0] + self.spacing = [1,1,1] + + # load metadata from kwargs if present + for key, value in kwargs.items(): + if key == 'origin' : + if type(value) == list and len (value) == 3: + self.origin = value + if key == 'spacing' : + if type(value) == list and len (value) == 3: + self.spacing = value + +class DataSetProcessor(CCPiBaseClass): + '''Abstract class for a DataSetProcessor''' + + def __init__(self, number_of_inputs, number_of_outputs, **kwargs): + kwargs['number_of_inputs'] = number_of_inputs + kwargs['number_of_outputs'] = number_of_outputs + + CCPiBaseClass.__init__(self, **kwargs) + + + + def setInput(self, **inData): + '''set the input data for the Processor + + this calls the setParameter method''' + self.setParameter(**inData) + + def getOutput(self): + raise NotImplementedError('The getOutput method is not implemented!') + + def apply(self): + raise NotImplementedError('The apply method is not implemented!') + + + +class AX(DataSetProcessor): + '''Example DataSetProcessor + The AXPY routines perform a vector multiplication operation defined as + + y := a*x + where: + + a is a scalar + + x a DataSet. + ''' + + def __init__(self, scalar, input_dataset): + kwargs = {'scalar':scalar, + 'input_dataset':input_dataset, + 'output_dataset': None} + DataSetProcessor.__init__(self, 2, 1, **kwargs) + + + + def apply(self): + a, x = self.getParameter(['scalar' , 'input_dataset' ]) + + y = DataSet( a * x.as_array() , True, + dimension_labels=x.dimension_labels ) + self.setParameter(output_dataset=y) + + def getOutput(self): + return self.getParameter( 'output_dataset' ) + + +class PixelByPixelDataSetProcessor(DataSetProcessor): + '''Example DataSetProcessor + + This processor applies a python function to each pixel of the DataSet + + f is a python function + + x a DataSet. + ''' + + def __init__(self, pyfunc, input_dataset): + kwargs = {'pyfunc':pyfunc, + 'input_dataset':input_dataset, + 'output_dataset': None} + DataSetProcessor.__init__(self, 2, 1, **kwargs) + + + + def apply(self): + pyfunc, x = self.getParameter(['pyfunc' , 'input_dataset' ]) + + eval_func = numpy.frompyfunc(pyfunc,1,1) + + + y = DataSet( eval_func( x.as_array() ) , True, + dimension_labels=x.dimension_labels ) + self.setParameter(output_dataset=y) + + def getOutput(self): + return self.getParameter( 'output_dataset' ) + if __name__ == '__main__': shape = (2,3,4,5) size = shape[0] @@ -181,10 +362,42 @@ if __name__ == '__main__': b = ds.subset( subset ) print ("b label {0} shape {1}".format(b.dimension_labels, numpy.shape(b.as_array()))) - c = ds.as_array(['Z','W']) + c = ds.subset(['Z','W','X']) + # Create a VolumeData sharing the array with c + volume0 = VolumeData(c.as_array(), False, dimensions = c.dimension_labels) + volume1 = VolumeData(c, False) + print ("volume0 {0} volume1 {1}".format(id(volume0.array), + id(volume1.array))) + + # Create a VolumeData copying the array from c + volume2 = VolumeData(c.as_array(), dimensions = c.dimension_labels) + volume3 = VolumeData(c) + + print ("volume2 {0} volume3 {1}".format(id(volume2.array), + id(volume3.array))) - + # single number DataSet + sn = DataSet(numpy.asarray([1])) + + ax = AX(scalar = 2 , input_dataset=c) + ax.apply() + print ("ax in {0} out {1}".format(c.as_array().flatten(), + ax.getOutput().as_array().flatten())) + axm = AX(scalar = 0.5 , input_dataset=ax.getOutput()) + axm.apply() + print ("axm in {0} out {1}".format(c.as_array(), axm.getOutput().as_array())) + + # create a PixelByPixelDataSetProcessor + + #define a python function which will take only one input (the pixel value) + pyfunc = lambda x: -x if x > 20 else x + clip = PixelByPixelDataSetProcessor(pyfunc,c) + clip.apply() + + print ("clip in {0} out {1}".format(c.as_array(), clip.getOutput().as_array())) + + \ No newline at end of file diff --git a/Wrappers/Python/conda-recipe/bld.bat b/Wrappers/Python/conda-recipe/bld.bat new file mode 100644 index 0000000..2f090eb --- /dev/null +++ b/Wrappers/Python/conda-recipe/bld.bat @@ -0,0 +1,9 @@ +IF NOT DEFINED CIL_VERSION ( +ECHO CIL_VERSION Not Defined. +exit 1 +) + +ROBOCOPY /E "%RECIPE_DIR%\.." "%SRC_DIR%" + +%PYTHON% setup.py build_py +if errorlevel 1 exit 1 diff --git a/Wrappers/Python/conda-recipe/build.sh b/Wrappers/Python/conda-recipe/build.sh new file mode 100644 index 0000000..0c6ad30 --- /dev/null +++ b/Wrappers/Python/conda-recipe/build.sh @@ -0,0 +1,9 @@ +if [ -z "$CIL_VERSION" ]; then + echo "Need to set CIL_VERSION" + exit 1 +fi +mkdir ${SRC_DIR}/ccpi +cp -r "${RECIPE_DIR}/../../../" ${SRC_DIR}/ccpi + +cd ${SRC_DIR}/ccpi/Wrappers/python +$PYTHON setup.py install diff --git a/Wrappers/Python/conda-recipe/meta.yaml b/Wrappers/Python/conda-recipe/meta.yaml new file mode 100644 index 0000000..fc5f09d --- /dev/null +++ b/Wrappers/Python/conda-recipe/meta.yaml @@ -0,0 +1,26 @@ +package: + name: ccpi-common + version: {{ environ['CIL_VERSION'] }} + + +build: + preserve_egg_dir: False + script_env: + - CIL_VERSION +# number: 0 + +requirements: + build: + - python + - numpy + - setuptools + + run: + - python + - numpy + - scipy + +about: + home: http://www.ccpi.ac.uk + license: BSD license + summary: 'CCPi Toolbox' diff --git a/Wrappers/Python/setup.py b/Wrappers/Python/setup.py index 31fe195..8ee6b58 100644 --- a/Wrappers/Python/setup.py +++ b/Wrappers/Python/setup.py @@ -1,13 +1,25 @@ #!/usr/bin/env python +# -*- 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 -import setuptools -from distutils.core import setup -from distutils.extension import Extension -from Cython.Distutils import build_ext +# 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 distutils.core import setup import os -import numpy -import platform import sys cil_version=os.environ['CIL_VERSION'] @@ -15,62 +27,35 @@ if cil_version == '': print("Please set the environmental variable CIL_VERSION") sys.exit(1) -library_include_path = [] -library_lib_path = [] -try: - library_include_path = [ os.environ['LIBRARY_INC'] ] - library_lib_path = [ os.environ['LIBRARY_LIB'] ] -except: - if platform.system() == 'Windows': - pass - else: - try: - library_include_path = [ os.environ['PREFIX']+'/include' ] - library_lib_path = [ os.environ['PREFiX']+'/lib' ] - except: - pass - pass -extra_include_dirs = [numpy.get_include()] -extra_library_dirs = [] -extra_compile_args = [] -extra_link_args = [] -extra_libraries = [] +cil_version=os.environ['CIL_VERSION'] +if cil_version == '': + print("Please set the environmental variable CIL_VERSION") + sys.exit(1) + +setup( + name="ccpi-common", + version=cil_version, + packages=['ccpi'], -if platform.system() == 'Windows': - extra_compile_args += ['/DWIN32','/EHsc','/DBOOST_ALL_NO_LIB', - '/openmp','/DHAS_TIFF','/DCCPiReconstructionIterative_EXPORTS'] - extra_include_dirs += ["..\\..\\Core\\src\\","..\\..\\Core\\src\\Algorithms","..\\..\\Core\\src\\Readers", "."] - extra_include_dirs += library_include_path - extra_library_dirs += library_lib_path - extra_libraries += ['tiff' , 'cilrec'] - if sys.version_info.major == 3 : - extra_libraries += ['boost_python3-vc140-mt-1_64', 'boost_numpy3-vc140-mt-1_64'] - else: - extra_libraries += ['boost_python-vc90-mt-1_64', 'boost_numpy-vc90-mt-1_64'] -else: - extra_include_dirs += ["../../Core/src/","../../Core/src/Algorithms","../../Core/src/Readers", "."] - extra_include_dirs += library_include_path - extra_compile_args += ['-fopenmp','-O2', '-funsigned-char', '-Wall','-Wl,--no-undefined','-DHAS_TIFF','-DCCPiReconstructionIterative_EXPORTS'] - extra_libraries += ['tiff' , 'cilrec'] - if sys.version_info.major == 3 : - extra_libraries += ['boost_python3', 'boost_numpy3','gomp'] - else: - extra_libraries += ['boost_python', 'boost_numpy','gomp'] + # Project uses reStructuredText, so ensure that the docutils get + # installed or upgraded on the target machine + #install_requires=['docutils>=0.3'], +# package_data={ +# # If any package contains *.txt or *.rst files, include them: +# '': ['*.txt', '*.rst'], +# # And include any *.msg files found in the 'hello' package, too: +# 'hello': ['*.msg'], +# }, + # zip_safe = False, -setup( - name='ccpi-reconstruction', - description='This is a CCPi Core Imaging Library package for Iterative Reconstruction codes', - version=cil_version, - cmdclass = {'build_ext': build_ext}, - ext_modules = [Extension("ccpi.reconstruction.parallelbeam", - sources=[ "src/diamond_module.cpp", - "src/diamond_wrapper.cpp"], - include_dirs=extra_include_dirs, library_dirs=extra_library_dirs, extra_compile_args=extra_compile_args, libraries=extra_libraries, extra_link_args=extra_link_args ), - Extension("ccpi.reconstruction.conebeam", - sources=[ "src/conebeam_module.cpp", - "src/conebeam_wrapper.cpp"], - include_dirs=extra_include_dirs, library_dirs=extra_library_dirs, extra_compile_args=extra_compile_args, libraries=extra_libraries ) ], - zip_safe = False, - packages = {'ccpi','ccpi.reconstruction'} + # metadata for upload to PyPI + author="Edoardo Pasca", + author_email="edoardo.pasca@stfc.ac.uk", + description='CCPi Core Imaging Library - Python Framework Module', + license="Apache v2.0", + keywords="Python Framework", + url="http://www.ccpi.ac.uk", # project home page, if any + + # could also include long_description, download_url, classifiers, etc. ) -- cgit v1.2.3