From d21f9bec8970ebbb2673b8e9e422339298762378 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Thu, 8 Feb 2024 14:39:17 +0000 Subject: [PATCH 01/72] Make config file to store important strings. --- Wrappers/Python/cil/framework/label.py | 48 ++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 Wrappers/Python/cil/framework/label.py diff --git a/Wrappers/Python/cil/framework/label.py b/Wrappers/Python/cil/framework/label.py new file mode 100644 index 0000000000..310e157473 --- /dev/null +++ b/Wrappers/Python/cil/framework/label.py @@ -0,0 +1,48 @@ +from typing import TypedDict + + +class ImageLabels(TypedDict): + RANDOM: str + RANDOM_INT: str + CHANNEL: str + VERTICAL: str + HORIZONTAL_X: str + HORIZONTAL_Y: str + + +class AcquisitionLabels(TypedDict): + RANDOM: str + RANDOM_INT: str + ANGLE_UNIT: str + DEGREE: str + RADIAN: str + CHANNEL: str + ANGLE: str + VERTICAL: str + HORIZONTAL: str + PARALLEL: str + CONE: str + DIM2: str + DIM3: str + + +image_labels: ImageLabels = {"RANDOM": "random", + "RANDOM_INT": "random_int", + "CHANNEL": "channel", + "VERTICAL": "vertical", + "HORIZONTAL_X": "horizontal_x", + "HORIZONTAL_Y": "horizontal_y"} + +acquisition_labels: AcquisitionLabels = {"RANDOM": "random", + "RANDOM_INT": "random_int", + "ANGLE_UNIT": "angle_unit", + "DEGREE": "degree", + "RADIAN": "radian", + "CHANNEL": "channel", + "ANGLE": "angle", + "VERTICAL": "vertical", + "HORIZONTAL": "horizontal", + "PARALLEL": "parallel", + "CONE": "cone", + "DIM2": "2D", + "DIM3": "3D"} From 31078a491527942e799af0349f14721e72717202 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Thu, 8 Feb 2024 15:17:40 +0000 Subject: [PATCH 02/72] Switch stuff in framework.py to use constants rather than refer to class constants. --- Wrappers/Python/cil/framework/framework.py | 55 +++++++++++----------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/framework.py index f0ad3c9485..efb714db0c 100644 --- a/Wrappers/Python/cil/framework/framework.py +++ b/Wrappers/Python/cil/framework/framework.py @@ -27,6 +27,7 @@ import math import weakref import logging +from label import image_labels, acquisition_labels from cil.utilities.multiprocessing import NUM_THREADS # check for the extension @@ -252,10 +253,10 @@ class ImageGeometry(object): @property def shape(self): - shape_dict = {ImageGeometry.CHANNEL: self.channels, - ImageGeometry.VERTICAL: self.voxel_num_z, - ImageGeometry.HORIZONTAL_Y: self.voxel_num_y, - ImageGeometry.HORIZONTAL_X: self.voxel_num_x} + shape_dict = {image_labels["CHANNEL"]: self.channels, + image_labels["VERTICAL"]: self.voxel_num_z, + image_labels["HORIZONTAL_Y"]: self.voxel_num_y, + image_labels["HORIZONTAL_X"]: self.voxel_num_x} shape = [] for label in self.dimension_labels: @@ -270,10 +271,10 @@ def shape(self, val): @property def spacing(self): - spacing_dict = {ImageGeometry.CHANNEL: self.channel_spacing, - ImageGeometry.VERTICAL: self.voxel_size_z, - ImageGeometry.HORIZONTAL_Y: self.voxel_size_y, - ImageGeometry.HORIZONTAL_X: self.voxel_size_x} + spacing_dict = {image_labels["CHANNEL"]: self.channel_spacing, + image_labels["VERTICAL"]: self.voxel_size_z, + image_labels["HORIZONTAL_Y"]: self.voxel_size_y, + image_labels["HORIZONTAL_X"]: self.voxel_size_x} spacing = [] for label in self.dimension_labels: @@ -489,7 +490,7 @@ def allocate(self, value=0, **kwargs): # it's created empty, so we make it 0 out.array.fill(value) else: - if value == ImageGeometry.RANDOM: + if value == image_labels["RANDOM"]: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) @@ -498,7 +499,7 @@ def allocate(self, value=0, **kwargs): out.fill(r) else: out.fill(numpy.random.random_sample(self.shape)) - elif value == ImageGeometry.RANDOM_INT: + elif value == image_labels["RANDOM_INT"]: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) @@ -687,7 +688,7 @@ def geometry(self): @geometry.setter def geometry(self,val): - if val != AcquisitionGeometry.CONE and val != AcquisitionGeometry.PARALLEL: + if val != acquisition_labels["CONE"] and val != acquisition_labels["PARALLEL"]: raise ValueError('geom_type = {} not recognised please specify \'cone\' or \'parallel\''.format(val)) else: self._geometry = val @@ -699,7 +700,7 @@ def __init__(self, dof, geometry, units='units'): self.geometry = geometry self.units = units - if geometry == AcquisitionGeometry.PARALLEL: + if geometry == acquisition_labels["PARALLEL"]: self.ray = DirectionVector(dof) else: self.source = PositionVector(dof) @@ -1919,7 +1920,7 @@ def angle_unit(self): @angle_unit.setter def angle_unit(self,val): - if val != AcquisitionGeometry.DEGREE and val != AcquisitionGeometry.RADIAN: + if val != acquisition_labels["DEGREE"] and val != acquisition_labels["RADIAN"]: raise ValueError('angle_unit = {} not recognised please specify \'degree\' or \'radian\''.format(val)) else: self._angle_unit = val @@ -2156,10 +2157,10 @@ def dimension(self): @property def shape(self): - shape_dict = {AcquisitionGeometry.CHANNEL: self.config.channels.num_channels, - AcquisitionGeometry.ANGLE: self.config.angles.num_positions, - AcquisitionGeometry.VERTICAL: self.config.panel.num_pixels[1], - AcquisitionGeometry.HORIZONTAL: self.config.panel.num_pixels[0]} + shape_dict = {acquisition_labels["CHANNEL"]: self.config.channels.num_channels, + acquisition_labels["ANGLE"]: self.config.angles.num_positions, + acquisition_labels["VERTICAL"]: self.config.panel.num_pixels[1], + acquisition_labels["HORIZONTAL"]: self.config.panel.num_pixels[0]} shape = [] for label in self.dimension_labels: shape.append(shape_dict[label]) @@ -2611,7 +2612,7 @@ def get_slice(self, channel=None, angle=None, vertical=None, horizontal=None): geometry_new.config.angles.angle_data = geometry_new.config.angles.angle_data[angle] if vertical is not None: - if geometry_new.geom_type == AcquisitionGeometry.PARALLEL or vertical == 'centre' or abs(geometry_new.pixel_num_v/2 - vertical) < 1e-6: + if geometry_new.geom_type == acquisition_labels["PARALLEL"] or vertical == 'centre' or abs(geometry_new.pixel_num_v/2 - vertical) < 1e-6: geometry_new = geometry_new.get_centre_slice() else: raise ValueError("Can only subset centre slice geometry on cone-beam data. Expected vertical = 'centre'. Got vertical = {0}".format(vertical)) @@ -2642,7 +2643,7 @@ def allocate(self, value=0, **kwargs): # it's created empty, so we make it 0 out.array.fill(value) else: - if value == AcquisitionGeometry.RANDOM: + if value == acquisition_labels["RANDOM"]: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) @@ -2651,7 +2652,7 @@ def allocate(self, value=0, **kwargs): out.fill(r) else: out.fill(numpy.random.random_sample(self.shape)) - elif value == AcquisitionGeometry.RANDOM_INT: + elif value == acquisition_labels["RANDOM_INT"]: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) @@ -4226,13 +4227,13 @@ class DataOrder(): ENGINES = ['astra','tigre','cil'] - ASTRA_IG_LABELS = [ImageGeometry.CHANNEL, ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] - TIGRE_IG_LABELS = [ImageGeometry.CHANNEL, ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] - ASTRA_AG_LABELS = [AcquisitionGeometry.CHANNEL, AcquisitionGeometry.VERTICAL, AcquisitionGeometry.ANGLE, AcquisitionGeometry.HORIZONTAL] - TIGRE_AG_LABELS = [AcquisitionGeometry.CHANNEL, AcquisitionGeometry.ANGLE, AcquisitionGeometry.VERTICAL, AcquisitionGeometry.HORIZONTAL] - CIL_IG_LABELS = [ImageGeometry.CHANNEL, ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] - CIL_AG_LABELS = [AcquisitionGeometry.CHANNEL, AcquisitionGeometry.ANGLE, AcquisitionGeometry.VERTICAL, AcquisitionGeometry.HORIZONTAL] - TOMOPHANTOM_IG_LABELS = [ImageGeometry.CHANNEL, ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] + ASTRA_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] + TIGRE_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] + ASTRA_AG_LABELS = [acquisition_labels["CHANNEL"], acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"], acquisition_labels["HORIZONTAL"]] + TIGRE_AG_LABELS = [acquisition_labels["CHANNEL"], acquisition_labels["ANGLE"], acquisition_labels["VERTICAL"], acquisition_labels["HORIZONTAL"]] + CIL_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] + CIL_AG_LABELS = [acquisition_labels["CHANNEL"], acquisition_labels["ANGLE"], acquisition_labels["VERTICAL"], acquisition_labels["HORIZONTAL"]] + TOMOPHANTOM_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] @staticmethod def get_order_for_engine(engine, geometry): From fee1ac4df4cb05a9f360a439b5a9eaa150585712 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Thu, 8 Feb 2024 15:23:49 +0000 Subject: [PATCH 03/72] Add license and fix import issue. --- Wrappers/Python/cil/framework/framework.py | 2 +- Wrappers/Python/cil/framework/label.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/framework.py index efb714db0c..29f4144ba0 100644 --- a/Wrappers/Python/cil/framework/framework.py +++ b/Wrappers/Python/cil/framework/framework.py @@ -27,7 +27,7 @@ import math import weakref import logging -from label import image_labels, acquisition_labels +from .label import image_labels, acquisition_labels from cil.utilities.multiprocessing import NUM_THREADS # check for the extension diff --git a/Wrappers/Python/cil/framework/label.py b/Wrappers/Python/cil/framework/label.py index 310e157473..8b2f4521dd 100644 --- a/Wrappers/Python/cil/framework/label.py +++ b/Wrappers/Python/cil/framework/label.py @@ -1,3 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 United Kingdom Research and Innovation +# Copyright 2024 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +# Joshua DM Hellier (University of Manchester) +# Nicholas Whyatt (UKRI-STFC) + from typing import TypedDict From ead2e4b75ccd8ac561b575cae21d4d70e080b18f Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Thu, 8 Feb 2024 17:10:50 +0000 Subject: [PATCH 04/72] Attempt to replace AcquisitionGeometry and ImageGeometry member calls with calls to a TypedDict. Remaining bug with Partitioner import in 2 test cases. --- Wrappers/Python/cil/framework/__init__.py | 4 +- Wrappers/Python/cil/framework/framework.py | 19 ----- Wrappers/Python/cil/io/ZEISSDataReader.py | 6 +- Wrappers/Python/cil/plugins/TomoPhantom.py | 7 +- .../utilities/convert_geometry_to_astra.py | 3 +- .../convert_geometry_to_astra_vec_2D.py | 3 +- .../convert_geometry_to_astra_vec_3D.py | 3 +- Wrappers/Python/cil/plugins/tigre/Geometry.py | 4 +- .../cil/plugins/tigre/ProjectionOperator.py | 4 +- Wrappers/Python/cil/recon/FBP.py | 6 +- Wrappers/Python/cil/utilities/dataexample.py | 12 ++-- Wrappers/Python/test/test_DataContainer.py | 71 ++++++++++--------- Wrappers/Python/test/test_Operator.py | 16 ++--- .../Python/test/test_PluginsTomoPhantom.py | 6 +- Wrappers/Python/test/test_algorithms.py | 9 +-- Wrappers/Python/test/test_functions.py | 15 ++-- Wrappers/Python/test/test_io.py | 6 +- Wrappers/Python/test/test_ring_processor.py | 4 +- Wrappers/Python/test/test_subset.py | 71 ++++++++++--------- 19 files changed, 127 insertions(+), 142 deletions(-) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 4571441515..3ea21e4e79 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -32,5 +32,5 @@ from .framework import AX, PixelByPixelDataProcessor, CastDataContainer from .BlockDataContainer import BlockDataContainer from .BlockGeometry import BlockGeometry -from .framework import DataOrder -from .framework import Partitioner +from .framework import DataOrder, Partitioner +from .label import acquisition_labels, image_labels diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/framework.py index 29f4144ba0..b3dcdd923d 100644 --- a/Wrappers/Python/cil/framework/framework.py +++ b/Wrappers/Python/cil/framework/framework.py @@ -243,12 +243,6 @@ def message(cls, msg, *args): return msg.format(*args ) class ImageGeometry(object): - RANDOM = 'random' - RANDOM_INT = 'random_int' - CHANNEL = 'channel' - VERTICAL = 'vertical' - HORIZONTAL_X = 'horizontal_x' - HORIZONTAL_Y = 'horizontal_y' @property def shape(self): @@ -2072,19 +2066,6 @@ class AcquisitionGeometry(object): `AcquisitionGeometry.create_Cone3D()` """ - RANDOM = 'random' - RANDOM_INT = 'random_int' - ANGLE_UNIT = 'angle_unit' - DEGREE = 'degree' - RADIAN = 'radian' - CHANNEL = 'channel' - ANGLE = 'angle' - VERTICAL = 'vertical' - HORIZONTAL = 'horizontal' - PARALLEL = 'parallel' - CONE = 'cone' - DIM2 = '2D' - DIM3 = '3D' #for backwards compatibility @property diff --git a/Wrappers/Python/cil/io/ZEISSDataReader.py b/Wrappers/Python/cil/io/ZEISSDataReader.py index 06e05d5bdd..8772f94485 100644 --- a/Wrappers/Python/cil/io/ZEISSDataReader.py +++ b/Wrappers/Python/cil/io/ZEISSDataReader.py @@ -19,7 +19,7 @@ # Andrew Shartis (UES, Inc.) -from cil.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry, DataOrder +from cil.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry, DataOrder, acquisition_labels import numpy as np import os import olefile @@ -234,11 +234,11 @@ def _setup_acq_geometry(self): ) \ .set_panel([self._metadata['image_width'], self._metadata['image_height']],\ pixel_size=[self._metadata['detector_pixel_size']/1000,self._metadata['detector_pixel_size']/1000])\ - .set_angles(self._metadata['thetas'],angle_unit=AcquisitionGeometry.RADIAN) + .set_angles(self._metadata['thetas'],angle_unit=acquisition_labels["RADIAN"]) else: self._geometry = AcquisitionGeometry.create_Parallel3D()\ .set_panel([self._metadata['image_width'], self._metadata['image_height']])\ - .set_angles(self._metadata['thetas'],angle_unit=AcquisitionGeometry.RADIAN) + .set_angles(self._metadata['thetas'],angle_unit=acquisition_labels["RADIAN"]) self._geometry.dimension_labels = ['angle', 'vertical', 'horizontal'] def _setup_image_geometry(self): diff --git a/Wrappers/Python/cil/plugins/TomoPhantom.py b/Wrappers/Python/cil/plugins/TomoPhantom.py index 72c42de3c1..3f1e23c35d 100644 --- a/Wrappers/Python/cil/plugins/TomoPhantom.py +++ b/Wrappers/Python/cil/plugins/TomoPhantom.py @@ -17,8 +17,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import ImageGeometry, AcquisitionGeometry -from cil.framework import ImageData, AcquisitionData, DataOrder +from cil.framework import ImageData, DataOrder, image_labels import tomophantom from tomophantom import TomoP2D, TomoP3D import os @@ -140,7 +139,7 @@ def get_ImageData(num_model, geometry): ag.set_panel((N,N-2)) ag.set_channels(channels) - ag.set_angles(angles, angle_unit=AcquisitionGeometry.DEGREE) + ag.set_angles(angles, angle_unit=acquisition_labels["DEGREE"]) ig = ag.get_ImageGeometry() @@ -153,7 +152,7 @@ def get_ImageData(num_model, geometry): ig.set_labels(DataOrder.TOMOPHANTOM_IG_LABELS) num_dims = len(ig.dimension_labels) - if ImageGeometry.CHANNEL in ig.dimension_labels: + if image_labels["CHANNEL"] in ig.dimension_labels: if not is_model_temporal(num_model): raise ValueError('Selected model {} is not a temporal model, please change your selection'.format(num_model)) if num_dims == 4: diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py index 777fe7ba7f..c111f4b666 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py @@ -20,6 +20,7 @@ import astra import numpy as np +from cil.framework import acquisition_labels def convert_geometry_to_astra(volume_geometry, sinogram_geometry): """ @@ -49,7 +50,7 @@ def convert_geometry_to_astra(volume_geometry, sinogram_geometry): #get units - if sinogram_geometry.config.angles.angle_unit == sinogram_geometry.DEGREE: + if sinogram_geometry.config.angles.angle_unit == acquisition_labels["DEGREE"]: angles_rad = sinogram_geometry.config.angles.angle_data * np.pi / 180.0 else: angles_rad = sinogram_geometry.config.angles.angle_data diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py index 9f0c6995db..c6bdea5509 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py @@ -20,6 +20,7 @@ import astra import numpy as np +from cil.framework import acquisition_labels def convert_geometry_to_astra_vec_2D(volume_geometry, sinogram_geometry_in): @@ -53,7 +54,7 @@ def convert_geometry_to_astra_vec_2D(volume_geometry, sinogram_geometry_in): panel = sinogram_geometry.config.panel #get units - degrees = angles.angle_unit == sinogram_geometry.DEGREE + degrees = angles.angle_unit == acquisition_labels["DEGREE"] #create a 2D astra geom from 2D CIL geometry, 2D astra geometry has axis flipped compared to 3D volume_geometry_temp = volume_geometry.copy() diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py index 0c8f0d6076..f14e443747 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py @@ -20,6 +20,7 @@ import astra import numpy as np +from cil.framework import acquisition_labels def convert_geometry_to_astra_vec_3D(volume_geometry, sinogram_geometry_in): @@ -55,7 +56,7 @@ def convert_geometry_to_astra_vec_3D(volume_geometry, sinogram_geometry_in): panel = sinogram_geometry.config.panel #get units - degrees = angles.angle_unit == sinogram_geometry.DEGREE + degrees = angles.angle_unit == acquisition_labels["DEGREE"] if sinogram_geometry.dimension == '2D': #create a 3D astra geom from 2D CIL geometry diff --git a/Wrappers/Python/cil/plugins/tigre/Geometry.py b/Wrappers/Python/cil/plugins/tigre/Geometry.py index 3d82793d12..39d22f1933 100644 --- a/Wrappers/Python/cil/plugins/tigre/Geometry.py +++ b/Wrappers/Python/cil/plugins/tigre/Geometry.py @@ -17,7 +17,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import AcquisitionGeometry, ImageGeometry +from cil.framework import acquisition_labels import numpy as np try: @@ -34,7 +34,7 @@ def getTIGREGeometry(ig, ag): #angles angles = ag.config.angles.angle_data + ag.config.angles.initial_angle - if ag.config.angles.angle_unit == AcquisitionGeometry.DEGREE: + if ag.config.angles.angle_unit == acquisition_labels["DEGREE"]: angles *= (np.pi/180.) #convert CIL to TIGRE angles s diff --git a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py index 9840b0e0e2..84f0726fd5 100644 --- a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py @@ -18,7 +18,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import ImageData, AcquisitionData, AcquisitionGeometry -from cil.framework import DataOrder +from cil.framework import DataOrder, acquisition_labels from cil.framework.BlockGeometry import BlockGeometry from cil.optimisation.operators import BlockOperator from cil.optimisation.operators import LinearOperator @@ -231,7 +231,7 @@ def adjoint(self, x, out=None): data = x.as_array() #if single angle projection add the dimension in for TIGRE - if x.dimension_labels[0] != AcquisitionGeometry.ANGLE: + if x.dimension_labels[0] != acquisition_labels["ANGLE"]: data = np.expand_dims(data, axis=0) if self.tigre_geom.is2D: diff --git a/Wrappers/Python/cil/recon/FBP.py b/Wrappers/Python/cil/recon/FBP.py index 94b4d72c93..3a10595e21 100644 --- a/Wrappers/Python/cil/recon/FBP.py +++ b/Wrappers/Python/cil/recon/FBP.py @@ -18,7 +18,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import cilacc -from cil.framework import AcquisitionGeometry +from cil.framework import acquisition_labels from cil.recon import Reconstructor from scipy.fft import fftfreq @@ -351,7 +351,7 @@ def __init__ (self, input, image_geometry=None, filter='ram-lak'): #call parent initialiser super().__init__(input, image_geometry, filter, backend='tigre') - if input.geometry.geom_type != AcquisitionGeometry.CONE: + if input.geometry.geom_type != acquisition_labels["CONE"]: raise TypeError("This reconstructor is for cone-beam data only.") @@ -460,7 +460,7 @@ def __init__ (self, input, image_geometry=None, filter='ram-lak', backend='tigre super().__init__(input, image_geometry, filter, backend) self.set_split_processing(False) - if input.geometry.geom_type != AcquisitionGeometry.PARALLEL: + if input.geometry.geom_type != acquisition_labels["PARALLEL"]: raise TypeError("This reconstructor is for parallel-beam data only.") diff --git a/Wrappers/Python/cil/utilities/dataexample.py b/Wrappers/Python/cil/utilities/dataexample.py index b922332e1a..1c2dcdf531 100644 --- a/Wrappers/Python/cil/utilities/dataexample.py +++ b/Wrappers/Python/cil/utilities/dataexample.py @@ -17,7 +17,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import ImageData, ImageGeometry, DataContainer +from cil.framework import ImageData, ImageGeometry, image_labels import numpy import numpy as np from PIL import Image @@ -216,7 +216,7 @@ def load(self, which, size=None, scale=(0,1), **kwargs): sdata = numpy.zeros((N, M)) sdata[int(round(N/4)):int(round(3*N/4)), int(round(M/4)):int(round(3*M/4))] = 0.5 sdata[int(round(N/8)):int(round(7*N/8)), int(round(3*M/8)):int(round(5*M/8))] = 1 - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]]) data = ig.allocate() data.fill(sdata) @@ -231,7 +231,7 @@ def load(self, which, size=None, scale=(0,1), **kwargs): N = size[0] M = size[1] - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]]) data = ig.allocate() tmp = numpy.array(f.convert('L').resize((M,N))) data.fill(tmp/numpy.max(tmp)) @@ -253,13 +253,13 @@ def load(self, which, size=None, scale=(0,1), **kwargs): bands = tmp.getbands() ig = ImageGeometry(voxel_num_x=M, voxel_num_y=N, channels=len(bands), - dimension_labels=[ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X,ImageGeometry.CHANNEL]) + dimension_labels=[image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"],image_labels["CHANNEL"]]) data = ig.allocate() data.fill(numpy.array(tmp.resize((M,N)))) - data.reorder([ImageGeometry.CHANNEL,ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X]) + data.reorder([image_labels["CHANNEL"],image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]]) data.geometry.channel_labels = bands else: - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]]) data = ig.allocate() data.fill(numpy.array(tmp.resize((M,N)))) diff --git a/Wrappers/Python/test/test_DataContainer.py b/Wrappers/Python/test/test_DataContainer.py index 1940b5af10..d9fdab42cc 100644 --- a/Wrappers/Python/test/test_DataContainer.py +++ b/Wrappers/Python/test/test_DataContainer.py @@ -26,6 +26,7 @@ from cil.framework import AcquisitionData from cil.framework import ImageGeometry, BlockGeometry, VectorGeometry from cil.framework import AcquisitionGeometry +from cil.framework import acquisition_labels, image_labels from timeit import default_timer as timer import logging from testclass import CCPiTestClass @@ -459,8 +460,8 @@ def test_ImageData(self): 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]) + dimension_labels=[image_labels["HORIZONTAL_X"], image_labels["HORIZONTAL_Y"], + image_labels["VERTICAL"]]) data = ig2.allocate() self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ig2.shape)) self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape) @@ -505,8 +506,8 @@ def test_AcquisitionData(self): self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape) ag2 = AcquisitionGeometry.create_Parallel3D().set_angles(numpy.linspace(0, 180, num=10)).set_panel((2,3)).set_channels(4)\ - .set_labels([AcquisitionGeometry.VERTICAL , - AcquisitionGeometry.ANGLE, AcquisitionGeometry.HORIZONTAL, AcquisitionGeometry.CHANNEL]) + .set_labels([acquisition_labels["VERTICAL"] , + acquisition_labels["ANGLE"], acquisition_labels["HORIZONTAL"], acquisition_labels["CHANNEL"]]) data = ag2.allocate() self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ag2.shape)) @@ -700,27 +701,27 @@ def test_AcquisitionDataSubset(self): # expected dimension_labels - self.assertListEqual([AcquisitionGeometry.CHANNEL , - AcquisitionGeometry.ANGLE , AcquisitionGeometry.VERTICAL , - AcquisitionGeometry.HORIZONTAL], + self.assertListEqual([acquisition_labels["CHANNEL"] , + acquisition_labels["ANGLE"] , acquisition_labels["VERTICAL"] , + acquisition_labels["HORIZONTAL"]], list(sgeometry.dimension_labels)) sino = sgeometry.allocate() # test reshape - new_order = [AcquisitionGeometry.HORIZONTAL , - AcquisitionGeometry.CHANNEL , AcquisitionGeometry.VERTICAL , - AcquisitionGeometry.ANGLE] + new_order = [acquisition_labels["HORIZONTAL"] , + acquisition_labels["CHANNEL"] , acquisition_labels["VERTICAL"] , + acquisition_labels["ANGLE"]] sino.reorder(new_order) self.assertListEqual(new_order, list(sino.geometry.dimension_labels)) ss1 = sino.get_slice(vertical = 0) - self.assertListEqual([AcquisitionGeometry.HORIZONTAL , - AcquisitionGeometry.CHANNEL , - AcquisitionGeometry.ANGLE], list(ss1.geometry.dimension_labels)) + self.assertListEqual([acquisition_labels["HORIZONTAL"] , + acquisition_labels["CHANNEL"] , + acquisition_labels["ANGLE"]], list(ss1.geometry.dimension_labels)) ss2 = sino.get_slice(vertical = 0, channel=0) - self.assertListEqual([AcquisitionGeometry.HORIZONTAL , - AcquisitionGeometry.ANGLE], list(ss2.geometry.dimension_labels)) + self.assertListEqual([acquisition_labels["HORIZONTAL"] , + acquisition_labels["ANGLE"]], list(ss2.geometry.dimension_labels)) def test_ImageDataSubset(self): @@ -744,42 +745,42 @@ def test_ImageDataSubset(self): self.assertListEqual(['channel', 'horizontal_y'], list(ss1.geometry.dimension_labels)) vg = ImageGeometry(3,4,5,channels=2) - self.assertListEqual([ImageGeometry.CHANNEL, ImageGeometry.VERTICAL, - ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X], + self.assertListEqual([image_labels["CHANNEL"], image_labels["VERTICAL"], + image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]], list(vg.dimension_labels)) ss2 = vg.allocate() ss3 = vol.get_slice(channel=0) - self.assertListEqual([ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X], list(ss3.geometry.dimension_labels)) + self.assertListEqual([image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]], list(ss3.geometry.dimension_labels)) def test_DataContainerSubset(self): dc = DataContainer(numpy.ones((2,3,4,5))) - dc.dimension_labels =[AcquisitionGeometry.CHANNEL , - AcquisitionGeometry.ANGLE , AcquisitionGeometry.VERTICAL , - AcquisitionGeometry.HORIZONTAL] + dc.dimension_labels =[acquisition_labels["CHANNEL"] , + acquisition_labels["ANGLE"] , acquisition_labels["VERTICAL"] , + acquisition_labels["HORIZONTAL"]] # test reshape - new_order = [AcquisitionGeometry.HORIZONTAL , - AcquisitionGeometry.CHANNEL , AcquisitionGeometry.VERTICAL , - AcquisitionGeometry.ANGLE] + new_order = [acquisition_labels["HORIZONTAL"] , + acquisition_labels["CHANNEL"] , acquisition_labels["VERTICAL"] , + acquisition_labels["ANGLE"]] dc.reorder(new_order) self.assertListEqual(new_order, list(dc.dimension_labels)) ss1 = dc.get_slice(vertical=0) - self.assertListEqual([AcquisitionGeometry.HORIZONTAL , - AcquisitionGeometry.CHANNEL , - AcquisitionGeometry.ANGLE], list(ss1.dimension_labels)) + self.assertListEqual([acquisition_labels["HORIZONTAL"] , + acquisition_labels["CHANNEL"] , + acquisition_labels["ANGLE"]], list(ss1.dimension_labels)) ss2 = dc.get_slice(vertical=0, channel=0) - self.assertListEqual([AcquisitionGeometry.HORIZONTAL , - AcquisitionGeometry.ANGLE], list(ss2.dimension_labels)) + self.assertListEqual([acquisition_labels["HORIZONTAL"] , + acquisition_labels["ANGLE"]], list(ss2.dimension_labels)) # Check we can get slice still even if force parameter is passed: ss3 = dc.get_slice(vertical=0, channel=0, force=True) - self.assertListEqual([AcquisitionGeometry.HORIZONTAL , - AcquisitionGeometry.ANGLE], list(ss3.dimension_labels)) + self.assertListEqual([acquisition_labels["HORIZONTAL"] , + acquisition_labels["ANGLE"]], list(ss3.dimension_labels)) def test_DataContainerChaining(self): @@ -926,7 +927,7 @@ def test_multiply_out(self): numpy.testing.assert_array_equal(a, u.as_array()) - #u = ig.allocate(ImageGeometry.RANDOM_INT, seed=1) + #u = ig.allocate(image_labels["RANDOM_INT"], seed=1) l = functools.reduce(lambda x,y: x*y, (10,11,12), 1) a = numpy.zeros((l, ), dtype=numpy.float32) @@ -1235,7 +1236,7 @@ def test_fill_dimension_ImageData(self): ig = ImageGeometry(2,3,4) u = ig.allocate(0) a = numpy.ones((4,2)) - # default_labels = [ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] + # default_labels = [image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] data = u.as_array() axis_number = u.get_dimension_axis('horizontal_y') @@ -1263,7 +1264,7 @@ def test_fill_dimension_AcquisitionData(self): ag.set_labels(('horizontal','angle','vertical','channel')) u = ag.allocate(0) a = numpy.ones((4,2)) - # default_labels = [ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] + # default_labels = [image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] data = u.as_array() axis_number = u.get_dimension_axis('horizontal_y') @@ -1297,7 +1298,7 @@ def test_fill_dimension_AcquisitionData(self): u = ag.allocate(0) # (2, 5, 3, 4) a = numpy.ones((2,5)) - # default_labels = [ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] + # default_labels = [image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] b = u.get_slice(channel=0, vertical=0) data = u.as_array() diff --git a/Wrappers/Python/test/test_Operator.py b/Wrappers/Python/test/test_Operator.py index 07f72e23ad..ea10e59b32 100644 --- a/Wrappers/Python/test/test_Operator.py +++ b/Wrappers/Python/test/test_Operator.py @@ -20,7 +20,7 @@ import unittest from unittest.mock import Mock from utils import initialise_tests -from cil.framework import ImageGeometry, BlockGeometry, VectorGeometry, BlockDataContainer, DataContainer +from cil.framework import ImageGeometry, BlockGeometry, VectorGeometry, BlockDataContainer, DataContainer, image_labels from cil.optimisation.operators import BlockOperator,\ FiniteDifferenceOperator, SymmetrisedGradientOperator import numpy @@ -239,13 +239,13 @@ def test_FiniteDifference(self): FD = FiniteDifferenceOperator(ig, direction = 0, bnd_cond = 'Neumann') u = FD.domain_geometry().allocate('random') - res = FD.domain_geometry().allocate(ImageGeometry.RANDOM) + res = FD.domain_geometry().allocate(image_labels["RANDOM"]) FD.adjoint(u, out=res) w = FD.adjoint(u) self.assertNumpyArrayEqual(res.as_array(), w.as_array()) - res = Id.domain_geometry().allocate(ImageGeometry.RANDOM) + res = Id.domain_geometry().allocate(image_labels["RANDOM"]) Id.adjoint(u, out=res) w = Id.adjoint(u) @@ -254,14 +254,14 @@ def test_FiniteDifference(self): G = GradientOperator(ig) - u = G.range_geometry().allocate(ImageGeometry.RANDOM) + u = G.range_geometry().allocate(image_labels["RANDOM"]) 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) + u = G.domain_geometry().allocate(image_labels["RANDOM"]) res = G.range_geometry().allocate() G.direct(u, out=res) w = G.direct(u) @@ -644,7 +644,7 @@ def test_BlockOperator(self): self.assertBlockDataContainerEqual(z1, res) - z1 = B.range_geometry().allocate(ImageGeometry.RANDOM) + z1 = B.range_geometry().allocate(image_labels["RANDOM"]) res1 = B.adjoint(z1) res2 = B.domain_geometry().allocate() @@ -745,7 +745,7 @@ def test_BlockOperator(self): ) B1 = BlockOperator(G, Id) - U = ig.allocate(ImageGeometry.RANDOM) + U = ig.allocate(image_labels["RANDOM"]) #U = BlockDataContainer(u,u) RES1 = B1.range_geometry().allocate() @@ -828,7 +828,7 @@ def test_BlockOperatorLinearValidity(self): B = BlockOperator(G, Id) # Nx1 case u = ig.allocate('random', seed=2) - w = B.range_geometry().allocate(ImageGeometry.RANDOM, seed=3) + w = B.range_geometry().allocate(image_labels["RANDOM"], seed=3) w1 = B.direct(u) u1 = B.adjoint(w) self.assertAlmostEqual((w * w1).sum() , (u1*u).sum(), places=5) diff --git a/Wrappers/Python/test/test_PluginsTomoPhantom.py b/Wrappers/Python/test/test_PluginsTomoPhantom.py index d8f2e18a26..326be9d71f 100644 --- a/Wrappers/Python/test/test_PluginsTomoPhantom.py +++ b/Wrappers/Python/test/test_PluginsTomoPhantom.py @@ -18,7 +18,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt import unittest -from cil.framework import ImageGeometry, AcquisitionGeometry +from cil.framework import AcquisitionGeometry, acquisition_labels import numpy as np from utils import has_tomophantom, initialise_tests @@ -37,7 +37,7 @@ def setUp(self): ag = AcquisitionGeometry.create_Cone2D((offset,-100), (offset,100)) ag.set_panel(N) - ag.set_angles(angles, angle_unit=AcquisitionGeometry.DEGREE) + ag.set_angles(angles, angle_unit=acquisition_labels["DEGREE"]) ig = ag.get_ImageGeometry() self.ag = ag self.ig = ig @@ -104,7 +104,7 @@ def setUp(self): ag = AcquisitionGeometry.create_Cone3D((offset,-100,0), (offset,100,0)) ag.set_panel((N,N/2)) - ag.set_angles(angles, angle_unit=AcquisitionGeometry.DEGREE) + ag.set_angles(angles, angle_unit=acquisition_labels["DEGREE"]) ig = ag.get_ImageGeometry() self.ag = ag self.ig = ig diff --git a/Wrappers/Python/test/test_algorithms.py b/Wrappers/Python/test/test_algorithms.py index 893469b813..7c5ecb7bc4 100644 --- a/Wrappers/Python/test/test_algorithms.py +++ b/Wrappers/Python/test/test_algorithms.py @@ -29,6 +29,7 @@ from cil.framework import AcquisitionGeometry from cil.framework import BlockDataContainer from cil.framework import BlockGeometry +from cil.framework import image_labels from cil.optimisation.operators import IdentityOperator from cil.optimisation.operators import GradientOperator, BlockOperator, MatrixOperator @@ -200,7 +201,7 @@ def test_FISTA(self): b = initial.copy() # fill with random numbers b.fill(numpy.random.random(initial.shape)) - initial = ig.allocate(ImageGeometry.RANDOM) + initial = ig.allocate(image_labels["RANDOM"]) identity = IdentityOperator(ig) norm2sq = OperatorCompositionFunction(L2NormSquared(b=b), identity) @@ -298,9 +299,9 @@ def test_FISTA_update(self): def test_FISTA_Norm2Sq(self): ig = ImageGeometry(127,139,149) - b = ig.allocate(ImageGeometry.RANDOM) + b = ig.allocate(image_labels["RANDOM"]) # fill with random numbers - initial = ig.allocate(ImageGeometry.RANDOM) + initial = ig.allocate(image_labels["RANDOM"]) identity = IdentityOperator(ig) norm2sq = LeastSquares(identity, b) @@ -326,7 +327,7 @@ def test_FISTA_catch_Lipschitz(self): b = initial.copy() # fill with random numbers b.fill(numpy.random.random(initial.shape)) - initial = ig.allocate(ImageGeometry.RANDOM) + initial = ig.allocate(image_labels["RANDOM"]) identity = IdentityOperator(ig) norm2sq = LeastSquares(identity, b) diff --git a/Wrappers/Python/test/test_functions.py b/Wrappers/Python/test/test_functions.py index 7f626d6903..a83af8c706 100644 --- a/Wrappers/Python/test/test_functions.py +++ b/Wrappers/Python/test/test_functions.py @@ -22,8 +22,7 @@ from cil.optimisation.functions.Function import ScaledFunction import numpy as np -from cil.framework import ImageGeometry, \ - VectorGeometry, VectorData, BlockDataContainer, DataContainer +from cil.framework import VectorGeometry, VectorData, BlockDataContainer, DataContainer, image_labels from cil.optimisation.operators import IdentityOperator, MatrixOperator, CompositionOperator, DiagonalOperator, BlockOperator from cil.optimisation.functions import Function, KullbackLeibler, ConstantFunction, TranslateFunction, soft_shrinkage from cil.optimisation.operators import GradientOperator @@ -79,9 +78,9 @@ def test_Function(self): operator = BlockOperator(op1, op2, shape=(2, 1)) # Create functions - noisy_data = ag.allocate(ImageGeometry.RANDOM, dtype=numpy.float64) + noisy_data = ag.allocate(image_labels["RANDOM"], dtype=numpy.float64) - d = ag.allocate(ImageGeometry.RANDOM, dtype=numpy.float64) + d = ag.allocate(image_labels["RANDOM"], dtype=numpy.float64) alpha = 0.5 # scaled function @@ -101,8 +100,8 @@ def test_L2NormSquared(self): numpy.random.seed(1) 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) - b = ig.allocate(ImageGeometry.RANDOM) + u = ig.allocate(image_labels["RANDOM"]) + b = ig.allocate(image_labels["RANDOM"]) # check grad/call no data f = L2NormSquared() @@ -212,8 +211,8 @@ def test_L2NormSquaredOut(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, seed=1) - b = ig.allocate(ImageGeometry.RANDOM, seed=2) + u = ig.allocate(image_labels["RANDOM"], seed=1) + b = ig.allocate(image_labels["RANDOM"], seed=2) # check grad/call no data f = L2NormSquared() diff --git a/Wrappers/Python/test/test_io.py b/Wrappers/Python/test/test_io.py index 41c89e8c11..e8ca299d9e 100644 --- a/Wrappers/Python/test/test_io.py +++ b/Wrappers/Python/test/test_io.py @@ -20,10 +20,10 @@ import unittest from unittest.mock import patch from utils import initialise_tests -from cil.framework import AcquisitionGeometry + import numpy as np import os -from cil.framework import ImageGeometry +from cil.framework import ImageGeometry, acquisition_labels from cil.io import TXRMDataReader, NEXUSDataReader, NikonDataReader, ZEISSDataReader from cil.io import TIFFWriter, TIFFStackReader from cil.io.utilities import HDF5_utilities @@ -98,7 +98,7 @@ def setUp(self): logging.info ("has_file {}".format(has_file)) if has_file: self.reader = TXRMDataReader() - angle_unit = AcquisitionGeometry.RADIAN + angle_unit = acquisition_labels["RADIAN"] self.reader.set_up(file_name=filename, angle_unit=angle_unit) diff --git a/Wrappers/Python/test/test_ring_processor.py b/Wrappers/Python/test/test_ring_processor.py index 807ae3e2cd..5443990719 100644 --- a/Wrappers/Python/test/test_ring_processor.py +++ b/Wrappers/Python/test/test_ring_processor.py @@ -19,7 +19,7 @@ import unittest from cil.processors import RingRemover, TransmissionAbsorptionConverter, Slicer -from cil.framework import ImageData, ImageGeometry, AcquisitionGeometry +from cil.framework import ImageGeometry, AcquisitionGeometry, acquisition_labels from cil.utilities import dataexample from cil.utilities.quality_measures import mse @@ -62,7 +62,7 @@ def test_L1Norm_2D(self): angles = np.linspace(0, 180, 120, dtype=np.float32) ag = AcquisitionGeometry.create_Parallel2D()\ - .set_angles(angles, angle_unit=AcquisitionGeometry.DEGREE)\ + .set_angles(angles, angle_unit=acquisition_labels["DEGREE"])\ .set_panel(detectors) sin = ag.allocate(None) sino = TomoP2D.ModelSino(model, detectors, detectors, angles, path_library2D) diff --git a/Wrappers/Python/test/test_subset.py b/Wrappers/Python/test/test_subset.py index 6ff93657ff..246022b12a 100644 --- a/Wrappers/Python/test/test_subset.py +++ b/Wrappers/Python/test/test_subset.py @@ -25,6 +25,7 @@ from cil.framework import AcquisitionData from cil.framework import ImageGeometry from cil.framework import AcquisitionGeometry +from cil.framework import acquisition_labels, image_labels from timeit import default_timer as timer initialise_tests() @@ -213,8 +214,8 @@ def setUp(self): def test_ImageDataAllocate1a(self): data = self.ig.allocate() - default_dimension_labels = [ImageGeometry.CHANNEL, ImageGeometry.VERTICAL, - ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X] + default_dimension_labels = [image_labels["CHANNEL"], image_labels["VERTICAL"], + image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] self.assertTrue( default_dimension_labels == list(data.dimension_labels) ) def test_ImageDataAllocate1b(self): @@ -222,64 +223,64 @@ def test_ImageDataAllocate1b(self): self.assertTrue( data.shape == (5,4,3,2)) def test_ImageDataAllocate2a(self): - non_default_dimension_labels = [ ImageGeometry.HORIZONTAL_X, ImageGeometry.VERTICAL, - ImageGeometry.HORIZONTAL_Y, ImageGeometry.CHANNEL] + non_default_dimension_labels = [ image_labels["HORIZONTAL_X"], image_labels["VERTICAL"], + image_labels["HORIZONTAL_Y"], image_labels["CHANNEL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() self.assertTrue( non_default_dimension_labels == list(data.dimension_labels) ) def test_ImageDataAllocate2b(self): - non_default_dimension_labels = [ ImageGeometry.HORIZONTAL_X, ImageGeometry.VERTICAL, - ImageGeometry.HORIZONTAL_Y, ImageGeometry.CHANNEL] + non_default_dimension_labels = [ image_labels["HORIZONTAL_X"], image_labels["VERTICAL"], + image_labels["HORIZONTAL_Y"], image_labels["CHANNEL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() self.assertTrue( data.shape == (2,4,3,5)) def test_ImageDataSubset1a(self): - non_default_dimension_labels = [ImageGeometry.HORIZONTAL_X, ImageGeometry.CHANNEL, ImageGeometry.HORIZONTAL_Y, - ImageGeometry.VERTICAL] + non_default_dimension_labels = [image_labels["HORIZONTAL_X"], image_labels["CHANNEL"], image_labels["HORIZONTAL_Y"], + image_labels["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(horizontal_y = 1) self.assertTrue( sub.shape == (2,5,4)) def test_ImageDataSubset2a(self): - non_default_dimension_labels = [ImageGeometry.HORIZONTAL_X, ImageGeometry.CHANNEL, ImageGeometry.HORIZONTAL_Y, - ImageGeometry.VERTICAL] + non_default_dimension_labels = [image_labels["HORIZONTAL_X"], image_labels["CHANNEL"], image_labels["HORIZONTAL_Y"], + image_labels["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(horizontal_x = 1) self.assertTrue( sub.shape == (5,3,4)) def test_ImageDataSubset3a(self): - non_default_dimension_labels = [ImageGeometry.HORIZONTAL_X, ImageGeometry.CHANNEL, ImageGeometry.HORIZONTAL_Y, - ImageGeometry.VERTICAL] + non_default_dimension_labels = [image_labels["HORIZONTAL_X"], image_labels["CHANNEL"], image_labels["HORIZONTAL_Y"], + image_labels["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(channel = 1) self.assertTrue( sub.shape == (2,3,4)) def test_ImageDataSubset4a(self): - non_default_dimension_labels = [ImageGeometry.HORIZONTAL_X, ImageGeometry.CHANNEL, ImageGeometry.HORIZONTAL_Y, - ImageGeometry.VERTICAL] + non_default_dimension_labels = [image_labels["HORIZONTAL_X"], image_labels["CHANNEL"], image_labels["HORIZONTAL_Y"], + image_labels["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(vertical = 1) self.assertTrue( sub.shape == (2,5,3)) def test_ImageDataSubset5a(self): - non_default_dimension_labels = [ImageGeometry.HORIZONTAL_X, ImageGeometry.HORIZONTAL_Y] + non_default_dimension_labels = [image_labels["HORIZONTAL_X"], image_labels["HORIZONTAL_Y"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(horizontal_y = 1) self.assertTrue( sub.shape == (2,)) def test_ImageDataSubset1b(self): - non_default_dimension_labels = [ImageGeometry.HORIZONTAL_X, ImageGeometry.CHANNEL, ImageGeometry.HORIZONTAL_Y, - ImageGeometry.VERTICAL] + non_default_dimension_labels = [image_labels["HORIZONTAL_X"], image_labels["CHANNEL"], image_labels["HORIZONTAL_Y"], + image_labels["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() - new_dimension_labels = [ImageGeometry.HORIZONTAL_Y, ImageGeometry.CHANNEL, ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_X] + new_dimension_labels = [image_labels["HORIZONTAL_Y"], image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_X"]] data.reorder(new_dimension_labels) self.assertTrue( data.shape == (3,5,4,2)) @@ -291,9 +292,9 @@ def test_ImageDataSubset1c(self): def test_AcquisitionDataAllocate1a(self): data = self.ag.allocate() - default_dimension_labels = [AcquisitionGeometry.CHANNEL , - AcquisitionGeometry.ANGLE , AcquisitionGeometry.VERTICAL , - AcquisitionGeometry.HORIZONTAL] + default_dimension_labels = [acquisition_labels["CHANNEL"] , + acquisition_labels["ANGLE"] , acquisition_labels["VERTICAL"] , + acquisition_labels["HORIZONTAL"]] self.assertTrue( default_dimension_labels == list(data.dimension_labels) ) def test_AcquisitionDataAllocate1b(self): @@ -301,8 +302,8 @@ def test_AcquisitionDataAllocate1b(self): self.assertTrue( data.shape == (4,3,2,20)) def test_AcquisitionDataAllocate2a(self): - non_default_dimension_labels = [AcquisitionGeometry.CHANNEL, AcquisitionGeometry.HORIZONTAL, - AcquisitionGeometry.VERTICAL, AcquisitionGeometry.ANGLE] + non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], + acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() @@ -310,15 +311,15 @@ def test_AcquisitionDataAllocate2a(self): self.assertTrue( non_default_dimension_labels == list(data.dimension_labels) ) def test_AcquisitionDataAllocate2b(self): - non_default_dimension_labels = [AcquisitionGeometry.CHANNEL, AcquisitionGeometry.HORIZONTAL, - AcquisitionGeometry.VERTICAL, AcquisitionGeometry.ANGLE] + non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], + acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() self.assertTrue( data.shape == (4,20,2,3)) def test_AcquisitionDataSubset1a(self): - non_default_dimension_labels = [AcquisitionGeometry.CHANNEL, AcquisitionGeometry.HORIZONTAL, - AcquisitionGeometry.VERTICAL, AcquisitionGeometry.ANGLE] + non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], + acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) @@ -326,24 +327,24 @@ def test_AcquisitionDataSubset1a(self): self.assertTrue( sub.shape == (4,20,3)) def test_AcquisitionDataSubset1b(self): - non_default_dimension_labels = [AcquisitionGeometry.CHANNEL, AcquisitionGeometry.HORIZONTAL, - AcquisitionGeometry.VERTICAL, AcquisitionGeometry.ANGLE] + non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], + acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) sub = data.get_slice(channel = 0) self.assertTrue( sub.shape == (20,2,3)) def test_AcquisitionDataSubset1c(self): - non_default_dimension_labels = [AcquisitionGeometry.CHANNEL, AcquisitionGeometry.HORIZONTAL, - AcquisitionGeometry.VERTICAL, AcquisitionGeometry.ANGLE] + non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], + acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) sub = data.get_slice(horizontal = 0, force=True) self.assertTrue( sub.shape == (4,2,3)) def test_AcquisitionDataSubset1d(self): - non_default_dimension_labels = [AcquisitionGeometry.CHANNEL, AcquisitionGeometry.HORIZONTAL, - AcquisitionGeometry.VERTICAL, AcquisitionGeometry.ANGLE] + non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], + acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) @@ -353,8 +354,8 @@ def test_AcquisitionDataSubset1d(self): self.assertTrue( sub.shape == (4,20,2) ) self.assertTrue( sub.geometry.angles[0] == data.geometry.angles[sliceme]) def test_AcquisitionDataSubset1e(self): - non_default_dimension_labels = [AcquisitionGeometry.CHANNEL, AcquisitionGeometry.HORIZONTAL, - AcquisitionGeometry.VERTICAL, AcquisitionGeometry.ANGLE] + non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], + acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) From 16255c1ca3f6a5bc417adf2e4f6b0f0afd7d6367 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Fri, 9 Feb 2024 11:08:31 +0000 Subject: [PATCH 05/72] Pull static methods out of DataOrder and make them plain functions. --- Wrappers/Python/cil/framework/__init__.py | 2 +- Wrappers/Python/cil/framework/framework.py | 69 ++++++++++--------- .../astra/operators/ProjectionOperator.py | 5 +- .../astra/processors/AstraBackProjector3D.py | 6 +- .../processors/AstraForwardProjector3D.py | 6 +- .../cil/plugins/astra/processors/FBP.py | 6 +- .../functions/regularisers.py | 5 +- Wrappers/Python/cil/plugins/tigre/FBP.py | 9 ++- .../cil/plugins/tigre/ProjectionOperator.py | 6 +- .../cil/processors/CofR_image_sharpness.py | 4 +- Wrappers/Python/cil/recon/Reconstructor.py | 4 +- Wrappers/Python/test/utils_projectors.py | 31 ++++----- 12 files changed, 76 insertions(+), 77 deletions(-) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 3ea21e4e79..d00a8302c7 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -32,5 +32,5 @@ from .framework import AX, PixelByPixelDataProcessor, CastDataContainer from .BlockDataContainer import BlockDataContainer from .BlockGeometry import BlockGeometry -from .framework import DataOrder, Partitioner +from .framework import DataOrder, Partitioner, get_order_for_engine, check_order_for_engine from .label import acquisition_labels, image_labels diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/framework.py index b3dcdd923d..48fc0170d4 100644 --- a/Wrappers/Python/cil/framework/framework.py +++ b/Wrappers/Python/cil/framework/framework.py @@ -2815,7 +2815,7 @@ def reorder(self, order=None): ''' if order in DataOrder.ENGINES: - order = DataOrder.get_order_for_engine(order, self.geometry) + order = get_order_for_engine(order, self.geometry) try: if len(order) != len(self.shape): @@ -4204,7 +4204,8 @@ def allocate(self, value=0, **kwargs): raise ValueError('Value {} unknown'.format(value)) return out -class DataOrder(): + +class DataOrder: ENGINES = ['astra','tigre','cil'] @@ -4216,42 +4217,42 @@ class DataOrder(): CIL_AG_LABELS = [acquisition_labels["CHANNEL"], acquisition_labels["ANGLE"], acquisition_labels["VERTICAL"], acquisition_labels["HORIZONTAL"]] TOMOPHANTOM_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] - @staticmethod - def get_order_for_engine(engine, geometry): - if engine == 'astra': - if isinstance(geometry, AcquisitionGeometry): - dim_order = DataOrder.ASTRA_AG_LABELS - else: - dim_order = DataOrder.ASTRA_IG_LABELS - elif engine == 'tigre': - if isinstance(geometry, AcquisitionGeometry): - dim_order = DataOrder.TIGRE_AG_LABELS - else: - dim_order = DataOrder.TIGRE_IG_LABELS - elif engine == 'cil': - if isinstance(geometry, AcquisitionGeometry): - dim_order = DataOrder.CIL_AG_LABELS - else: - dim_order = DataOrder.CIL_IG_LABELS + +def get_order_for_engine(engine, geometry): + if engine == 'astra': + if isinstance(geometry, AcquisitionGeometry): + dim_order = DataOrder.ASTRA_AG_LABELS else: - raise ValueError("Unknown engine expected one of {0} got {1}".format(DataOrder.ENGINES, engine)) - - dimensions = [] - for label in dim_order: - if label in geometry.dimension_labels: - dimensions.append(label) + dim_order = DataOrder.ASTRA_IG_LABELS + elif engine == 'tigre': + if isinstance(geometry, AcquisitionGeometry): + dim_order = DataOrder.TIGRE_AG_LABELS + else: + dim_order = DataOrder.TIGRE_IG_LABELS + elif engine == 'cil': + if isinstance(geometry, AcquisitionGeometry): + dim_order = DataOrder.CIL_AG_LABELS + else: + dim_order = DataOrder.CIL_IG_LABELS + else: + raise ValueError("Unknown engine expected one of {0} got {1}".format(DataOrder.ENGINES, engine)) - return dimensions + dimensions = [] + for label in dim_order: + if label in geometry.dimension_labels: + dimensions.append(label) - @staticmethod - def check_order_for_engine(engine, geometry): - order_requested = DataOrder.get_order_for_engine(engine, geometry) + return dimensions - if order_requested == list(geometry.dimension_labels): - return True - else: - raise ValueError("Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" - .format(order_requested, list(geometry.dimension_labels), engine)) + +def check_order_for_engine(engine, geometry): + order_requested = get_order_for_engine(engine, geometry) + + if order_requested == list(geometry.dimension_labels): + return True + else: + raise ValueError("Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" + .format(order_requested, list(geometry.dimension_labels), engine)) diff --git a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py index 2e921178fd..9724a97fa3 100644 --- a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py @@ -20,6 +20,7 @@ from cil.framework import DataOrder from cil.optimisation.operators import LinearOperator, ChannelwiseOperator from cil.framework.BlockGeometry import BlockGeometry +from cil.framework import check_order_for_engine from cil.optimisation.operators import BlockOperator from cil.plugins.astra.operators import AstraProjector3D from cil.plugins.astra.operators import AstraProjector2D @@ -116,8 +117,8 @@ def __init__(self, self).__init__(domain_geometry=image_geometry, range_geometry=acquisition_geometry) - DataOrder.check_order_for_engine('astra', image_geometry) - DataOrder.check_order_for_engine('astra', acquisition_geometry) + check_order_for_engine('astra', image_geometry) + check_order_for_engine('astra', acquisition_geometry) self.volume_geometry = image_geometry self.sinogram_geometry = acquisition_geometry diff --git a/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py b/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py index 6e69e91624..6033f88b8c 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py +++ b/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py @@ -18,7 +18,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import DataProcessor, ImageData, DataOrder +from cil.framework import DataProcessor, ImageData, check_order_for_engine from cil.plugins.astra.utilities import convert_geometry_to_astra_vec_3D import astra from astra import astra_dict, algorithm, data3d @@ -67,7 +67,7 @@ def check_input(self, dataset): def set_ImageGeometry(self, volume_geometry): - DataOrder.check_order_for_engine('astra', volume_geometry) + check_order_for_engine('astra', volume_geometry) if len(volume_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(volume_geometry.number_of_dimensions)) @@ -76,7 +76,7 @@ def set_ImageGeometry(self, volume_geometry): def set_AcquisitionGeometry(self, sinogram_geometry): - DataOrder.check_order_for_engine('astra', sinogram_geometry) + check_order_for_engine('astra', sinogram_geometry) if len(sinogram_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(sinogram_geometry.number_of_dimensions)) diff --git a/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py b/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py index 48f7b30412..e09134e726 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py +++ b/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py @@ -18,7 +18,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import DataProcessor, AcquisitionData, DataOrder +from cil.framework import DataProcessor, AcquisitionData, check_order_for_engine from cil.plugins.astra.utilities import convert_geometry_to_astra_vec_3D import astra from astra import astra_dict, algorithm, data3d @@ -65,7 +65,7 @@ def check_input(self, dataset): def set_ImageGeometry(self, volume_geometry): - DataOrder.check_order_for_engine('astra', volume_geometry) + check_order_for_engine('astra', volume_geometry) if len(volume_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(volume_geometry.number_of_dimensions)) @@ -74,7 +74,7 @@ def set_ImageGeometry(self, volume_geometry): def set_AcquisitionGeometry(self, sinogram_geometry): - DataOrder.check_order_for_engine('astra', sinogram_geometry) + check_order_for_engine('astra', sinogram_geometry) if len(sinogram_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(sinogram_geometry.number_of_dimensions)) diff --git a/Wrappers/Python/cil/plugins/astra/processors/FBP.py b/Wrappers/Python/cil/plugins/astra/processors/FBP.py index e5dc50d4ae..b5076ec02b 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/FBP.py +++ b/Wrappers/Python/cil/plugins/astra/processors/FBP.py @@ -18,7 +18,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import DataProcessor -from cil.framework import DataOrder +from cil.framework import check_order_for_engine from cil.plugins.astra.processors.FBP_Flexible import FBP_Flexible from cil.plugins.astra.processors.FDK_Flexible import FDK_Flexible from cil.plugins.astra.processors.FBP_Flexible import FBP_CPU @@ -81,8 +81,8 @@ def __init__(self, image_geometry=None, acquisition_geometry=None, device='gpu', if image_geometry is None: image_geometry = acquisition_geometry.get_ImageGeometry() - DataOrder.check_order_for_engine('astra', image_geometry) - DataOrder.check_order_for_engine('astra', acquisition_geometry) + check_order_for_engine('astra', image_geometry) + check_order_for_engine('astra', acquisition_geometry) if device == 'gpu': if acquisition_geometry.geom_type == 'parallel': diff --git a/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py b/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py index b2a4d50d0e..ba5296879a 100644 --- a/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py +++ b/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py @@ -27,8 +27,7 @@ "Minimal version is 20.04") -from cil.framework import DataOrder -from cil.framework import DataContainer +from cil.framework import check_order_for_engine, DataContainer from cil.optimisation.functions import Function import numpy as np import warnings @@ -482,7 +481,7 @@ def __rmul__(self, scalar): def check_input(self, input): '''TNV requires 2D+channel data with the first dimension as the channel dimension''' if isinstance(input, DataContainer): - DataOrder.check_order_for_engine('cil', input.geometry) + check_order_for_engine('cil', input.geometry) if ( input.geometry.channels == 1 ) or ( not input.geometry.ndim == 3) : raise ValueError('TNV requires 2D+channel data. Got {}'.format(input.geometry.dimension_labels)) else: diff --git a/Wrappers/Python/cil/plugins/tigre/FBP.py b/Wrappers/Python/cil/plugins/tigre/FBP.py index 190bb78987..17d0af93bb 100644 --- a/Wrappers/Python/cil/plugins/tigre/FBP.py +++ b/Wrappers/Python/cil/plugins/tigre/FBP.py @@ -17,8 +17,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import DataProcessor, ImageData -from cil.framework import DataOrder +from cil.framework import DataProcessor, ImageData, check_order_for_engine from cil.plugins.tigre import CIL2TIGREGeometry import logging import numpy as np @@ -76,8 +75,8 @@ def __init__(self, image_geometry=None, acquisition_geometry=None, **kwargs): if device != 'gpu': raise ValueError("TIGRE FBP is GPU only. Got device = {}".format(device)) - DataOrder.check_order_for_engine('tigre', image_geometry) - DataOrder.check_order_for_engine('tigre', acquisition_geometry) + check_order_for_engine('tigre', image_geometry) + check_order_for_engine('tigre', acquisition_geometry) tigre_geom, tigre_angles = CIL2TIGREGeometry.getTIGREGeometry(image_geometry,acquisition_geometry) @@ -91,7 +90,7 @@ def check_input(self, dataset): raise ValueError("Expected input data to be single channel, got {0}"\ .format(self.acquisition_geometry.channels)) - DataOrder.check_order_for_engine('tigre', dataset.geometry) + check_order_for_engine('tigre', dataset.geometry) return True def process(self, out=None): diff --git a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py index 84f0726fd5..325d4f0c46 100644 --- a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py @@ -18,7 +18,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import ImageData, AcquisitionData, AcquisitionGeometry -from cil.framework import DataOrder, acquisition_labels +from cil.framework import acquisition_labels, check_order_for_engine from cil.framework.BlockGeometry import BlockGeometry from cil.optimisation.operators import BlockOperator from cil.optimisation.operators import LinearOperator @@ -155,8 +155,8 @@ def __init__(self, "TIGRE projectors are GPU only. Got device = {}".format( device)) - DataOrder.check_order_for_engine('tigre', image_geometry) - DataOrder.check_order_for_engine('tigre', acquisition_geometry) + check_order_for_engine('tigre', image_geometry) + check_order_for_engine('tigre', acquisition_geometry) super(ProjectionOperator,self).__init__(domain_geometry=image_geometry,\ range_geometry=acquisition_geometry) diff --git a/Wrappers/Python/cil/processors/CofR_image_sharpness.py b/Wrappers/Python/cil/processors/CofR_image_sharpness.py index 504d30fffc..de07cda0f1 100644 --- a/Wrappers/Python/cil/processors/CofR_image_sharpness.py +++ b/Wrappers/Python/cil/processors/CofR_image_sharpness.py @@ -17,7 +17,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import Processor, AcquisitionData, DataOrder +from cil.framework import Processor, AcquisitionData, check_order_for_engine import matplotlib.pyplot as plt import scipy import numpy as np @@ -143,7 +143,7 @@ def check_input(self, data): else: test_geom = data.geometry - if not DataOrder.check_order_for_engine(self.backend, test_geom): + if not check_order_for_engine(self.backend, test_geom): raise ValueError("Input data must be reordered for use with selected backend. Use input.reorder{'{0}')".format(self.backend)) return True diff --git a/Wrappers/Python/cil/recon/Reconstructor.py b/Wrappers/Python/cil/recon/Reconstructor.py index 9bbda4cb85..4782e66ba3 100644 --- a/Wrappers/Python/cil/recon/Reconstructor.py +++ b/Wrappers/Python/cil/recon/Reconstructor.py @@ -17,7 +17,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import AcquisitionData, ImageGeometry, DataOrder +from cil.framework import AcquisitionData, ImageGeometry, check_order_for_engine import importlib import weakref @@ -106,7 +106,7 @@ def _configure_for_backend(self, backend='tigre'): if backend not in self.supported_backends: raise ValueError("Backend unsupported. Supported backends: {}".format(self.supported_backends)) - if not DataOrder.check_order_for_engine(backend, self.acquisition_geometry): + if not check_order_for_engine(backend, self.acquisition_geometry): raise ValueError("Input data must be reordered for use with selected backend. Use input.reorder{'{0}')".format(backend)) #set ProjectionOperator class from backend diff --git a/Wrappers/Python/test/utils_projectors.py b/Wrappers/Python/test/utils_projectors.py index 5a2d36ad41..e1b2f1f86e 100644 --- a/Wrappers/Python/test/utils_projectors.py +++ b/Wrappers/Python/test/utils_projectors.py @@ -20,8 +20,7 @@ import numpy as np from cil.optimisation.operators import LinearOperator from cil.utilities import dataexample -from cil.framework import AcquisitionGeometry -from cil.framework import DataOrder +from cil.framework import AcquisitionGeometry, get_order_for_engine class SimData(object): @@ -140,7 +139,7 @@ def Cone3D(self): ag_test_1 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,0,0])\ .set_panel([16,16],[1,1])\ .set_angles([0]) - ag_test_1.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() @@ -151,7 +150,7 @@ def Cone3D(self): ag_test_2 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,0,0])\ .set_panel([16,16],[2,2])\ .set_angles([0]) - ag_test_2.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() norm_2 = 8 @@ -161,7 +160,7 @@ def Cone3D(self): ag_test_3 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,0,0])\ .set_panel([16,16],[0.5,0.5])\ .set_angles([0]) - ag_test_3.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() norm_3 = 2 @@ -171,7 +170,7 @@ def Cone3D(self): ag_test_4 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,1000,0])\ .set_panel([16,16],[0.5,0.5])\ .set_angles([0]) - ag_test_4.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_4)) + ag_test_4.set_labels(get_order_for_engine(self.backend, ag_test_4)) ig_test_4 = ag_test_4.get_ImageGeometry() norm_4 = 1 @@ -187,7 +186,7 @@ def Cone2D(self): ag_test_1 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,0])\ .set_panel(16,1)\ .set_angles([0]) - ag_test_1.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() @@ -198,7 +197,7 @@ def Cone2D(self): ag_test_2 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,0])\ .set_panel(16,2)\ .set_angles([0]) - ag_test_2.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() norm_2 = 8 @@ -208,7 +207,7 @@ def Cone2D(self): ag_test_3 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,0])\ .set_panel(16,0.5)\ .set_angles([0]) - ag_test_3.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() norm_3 = 2 @@ -218,7 +217,7 @@ def Cone2D(self): ag_test_4 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,1000])\ .set_panel(16,0.5)\ .set_angles([0]) - ag_test_4.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_4)) + ag_test_4.set_labels(get_order_for_engine(self.backend, ag_test_4)) ig_test_4 = ag_test_4.get_ImageGeometry() norm_4 = 1 @@ -234,7 +233,7 @@ def Parallel3D(self): ag_test_1 = AcquisitionGeometry.create_Parallel3D()\ .set_panel([16,16],[1,1])\ .set_angles([0]) - ag_test_1.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() @@ -244,7 +243,7 @@ def Parallel3D(self): ag_test_2 = AcquisitionGeometry.create_Parallel3D()\ .set_panel([16,16],[2,2])\ .set_angles([0]) - ag_test_2.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() @@ -255,7 +254,7 @@ def Parallel3D(self): ag_test_3 = AcquisitionGeometry.create_Parallel3D()\ .set_panel([16,16],[0.5,0.5])\ .set_angles([0]) - ag_test_3.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() @@ -273,7 +272,7 @@ def Parallel2D(self): .set_panel(16,1)\ .set_angles([0]) - ag_test_1.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() norm_1 = 4 @@ -282,7 +281,7 @@ def Parallel2D(self): ag_test_2 = AcquisitionGeometry.create_Parallel2D()\ .set_panel(16,2)\ .set_angles([0]) - ag_test_2.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() @@ -293,7 +292,7 @@ def Parallel2D(self): ag_test_3 = AcquisitionGeometry.create_Parallel2D()\ .set_panel(16,0.5)\ .set_angles([0]) - ag_test_3.set_labels(DataOrder.get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() norm_3 = 2 From 069398774168ff0deb4801d8200ada014850d852 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Fri, 9 Feb 2024 11:23:12 +0000 Subject: [PATCH 06/72] Make AcquisitionGeometry inherit from a blank base class. --- Wrappers/Python/cil/framework/framework.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/framework.py index 48fc0170d4..a3ec6ae1ef 100644 --- a/Wrappers/Python/cil/framework/framework.py +++ b/Wrappers/Python/cil/framework/framework.py @@ -2052,7 +2052,13 @@ def __eq__(self, other): return False -class AcquisitionGeometry(object): +class BaseAcquisitionGeometry: + """This class only exists to give get_order_for_engine something to typecheck for. At some point separate interface + stuff into here or refactor get_order_for_engine to not need it.""" + pass + + +class AcquisitionGeometry(BaseAcquisitionGeometry): """This class holds the AcquisitionGeometry of the system. Please initialise the AcquisitionGeometry using the using the static methods: From 156b146b288a9886d7831a22c15a3a1ccb380aea Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Fri, 9 Feb 2024 11:24:47 +0000 Subject: [PATCH 07/72] Switch get_order_for_engine's check to look for BaseAcquisitionGeometry instead of AcquisitionGeometry. --- Wrappers/Python/cil/framework/framework.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/framework.py index a3ec6ae1ef..e84d8ac82f 100644 --- a/Wrappers/Python/cil/framework/framework.py +++ b/Wrappers/Python/cil/framework/framework.py @@ -4226,17 +4226,17 @@ class DataOrder: def get_order_for_engine(engine, geometry): if engine == 'astra': - if isinstance(geometry, AcquisitionGeometry): + if isinstance(geometry, BaseAcquisitionGeometry): dim_order = DataOrder.ASTRA_AG_LABELS else: dim_order = DataOrder.ASTRA_IG_LABELS elif engine == 'tigre': - if isinstance(geometry, AcquisitionGeometry): + if isinstance(geometry, BaseAcquisitionGeometry): dim_order = DataOrder.TIGRE_AG_LABELS else: dim_order = DataOrder.TIGRE_IG_LABELS elif engine == 'cil': - if isinstance(geometry, AcquisitionGeometry): + if isinstance(geometry, BaseAcquisitionGeometry): dim_order = DataOrder.CIL_AG_LABELS else: dim_order = DataOrder.CIL_IG_LABELS From 41c71ab6f873e809fb09f010eb2dde132d0461c7 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Fri, 9 Feb 2024 13:21:44 +0000 Subject: [PATCH 08/72] Move BaseAcquisitionGeometry to a new file. --- Wrappers/Python/cil/framework/base.py | 4 ++++ Wrappers/Python/cil/framework/framework.py | 8 ++------ 2 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 Wrappers/Python/cil/framework/base.py diff --git a/Wrappers/Python/cil/framework/base.py b/Wrappers/Python/cil/framework/base.py new file mode 100644 index 0000000000..0c5e7e9755 --- /dev/null +++ b/Wrappers/Python/cil/framework/base.py @@ -0,0 +1,4 @@ +class BaseAcquisitionGeometry: + """This class only exists to give get_order_for_engine something to typecheck for. At some point separate interface + stuff into here or refactor get_order_for_engine to not need it.""" + pass diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/framework.py index e84d8ac82f..7640f3dab9 100644 --- a/Wrappers/Python/cil/framework/framework.py +++ b/Wrappers/Python/cil/framework/framework.py @@ -27,6 +27,8 @@ import math import weakref import logging + +from .base import BaseAcquisitionGeometry from .label import image_labels, acquisition_labels from cil.utilities.multiprocessing import NUM_THREADS @@ -2052,12 +2054,6 @@ def __eq__(self, other): return False -class BaseAcquisitionGeometry: - """This class only exists to give get_order_for_engine something to typecheck for. At some point separate interface - stuff into here or refactor get_order_for_engine to not need it.""" - pass - - class AcquisitionGeometry(BaseAcquisitionGeometry): """This class holds the AcquisitionGeometry of the system. From 68916b60526e3a7db75086990f17ebee3eeb3848 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Fri, 9 Feb 2024 13:37:05 +0000 Subject: [PATCH 09/72] Move DataOrder and associated functions to label.py. --- Wrappers/Python/cil/framework/__init__.py | 4 +- Wrappers/Python/cil/framework/framework.py | 53 +------------------ Wrappers/Python/cil/framework/label.py | 52 ++++++++++++++++++ .../astra/operators/ProjectionOperator.py | 3 +- .../cil/plugins/astra/processors/FBP.py | 3 +- .../cil/plugins/tigre/ProjectionOperator.py | 3 +- 6 files changed, 59 insertions(+), 59 deletions(-) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index d00a8302c7..bcb6539327 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -32,5 +32,5 @@ from .framework import AX, PixelByPixelDataProcessor, CastDataContainer from .BlockDataContainer import BlockDataContainer from .BlockGeometry import BlockGeometry -from .framework import DataOrder, Partitioner, get_order_for_engine, check_order_for_engine -from .label import acquisition_labels, image_labels +from .framework import Partitioner +from .label import acquisition_labels, image_labels, DataOrder, get_order_for_engine, check_order_for_engine diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/framework.py index 7640f3dab9..06b507455a 100644 --- a/Wrappers/Python/cil/framework/framework.py +++ b/Wrappers/Python/cil/framework/framework.py @@ -28,8 +28,9 @@ import weakref import logging + from .base import BaseAcquisitionGeometry -from .label import image_labels, acquisition_labels +from .label import image_labels, acquisition_labels, DataOrder, get_order_for_engine from cil.utilities.multiprocessing import NUM_THREADS # check for the extension @@ -4207,55 +4208,5 @@ def allocate(self, value=0, **kwargs): return out -class DataOrder: - - ENGINES = ['astra','tigre','cil'] - - ASTRA_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] - TIGRE_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] - ASTRA_AG_LABELS = [acquisition_labels["CHANNEL"], acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"], acquisition_labels["HORIZONTAL"]] - TIGRE_AG_LABELS = [acquisition_labels["CHANNEL"], acquisition_labels["ANGLE"], acquisition_labels["VERTICAL"], acquisition_labels["HORIZONTAL"]] - CIL_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] - CIL_AG_LABELS = [acquisition_labels["CHANNEL"], acquisition_labels["ANGLE"], acquisition_labels["VERTICAL"], acquisition_labels["HORIZONTAL"]] - TOMOPHANTOM_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] - - -def get_order_for_engine(engine, geometry): - if engine == 'astra': - if isinstance(geometry, BaseAcquisitionGeometry): - dim_order = DataOrder.ASTRA_AG_LABELS - else: - dim_order = DataOrder.ASTRA_IG_LABELS - elif engine == 'tigre': - if isinstance(geometry, BaseAcquisitionGeometry): - dim_order = DataOrder.TIGRE_AG_LABELS - else: - dim_order = DataOrder.TIGRE_IG_LABELS - elif engine == 'cil': - if isinstance(geometry, BaseAcquisitionGeometry): - dim_order = DataOrder.CIL_AG_LABELS - else: - dim_order = DataOrder.CIL_IG_LABELS - else: - raise ValueError("Unknown engine expected one of {0} got {1}".format(DataOrder.ENGINES, engine)) - - dimensions = [] - for label in dim_order: - if label in geometry.dimension_labels: - dimensions.append(label) - - return dimensions - - -def check_order_for_engine(engine, geometry): - order_requested = get_order_for_engine(engine, geometry) - - if order_requested == list(geometry.dimension_labels): - return True - else: - raise ValueError("Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" - .format(order_requested, list(geometry.dimension_labels), engine)) - - diff --git a/Wrappers/Python/cil/framework/label.py b/Wrappers/Python/cil/framework/label.py index 8b2f4521dd..bdf7f67e6b 100644 --- a/Wrappers/Python/cil/framework/label.py +++ b/Wrappers/Python/cil/framework/label.py @@ -21,6 +21,8 @@ from typing import TypedDict +from .base import BaseAcquisitionGeometry + class ImageLabels(TypedDict): RANDOM: str @@ -67,3 +69,53 @@ class AcquisitionLabels(TypedDict): "CONE": "cone", "DIM2": "2D", "DIM3": "3D"} + + +class DataOrder: + + ENGINES = ['astra','tigre','cil'] + + ASTRA_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] + TIGRE_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] + ASTRA_AG_LABELS = [acquisition_labels["CHANNEL"], acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"], acquisition_labels["HORIZONTAL"]] + TIGRE_AG_LABELS = [acquisition_labels["CHANNEL"], acquisition_labels["ANGLE"], acquisition_labels["VERTICAL"], acquisition_labels["HORIZONTAL"]] + CIL_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] + CIL_AG_LABELS = [acquisition_labels["CHANNEL"], acquisition_labels["ANGLE"], acquisition_labels["VERTICAL"], acquisition_labels["HORIZONTAL"]] + TOMOPHANTOM_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] + + +def get_order_for_engine(engine, geometry): + if engine == 'astra': + if isinstance(geometry, BaseAcquisitionGeometry): + dim_order = DataOrder.ASTRA_AG_LABELS + else: + dim_order = DataOrder.ASTRA_IG_LABELS + elif engine == 'tigre': + if isinstance(geometry, BaseAcquisitionGeometry): + dim_order = DataOrder.TIGRE_AG_LABELS + else: + dim_order = DataOrder.TIGRE_IG_LABELS + elif engine == 'cil': + if isinstance(geometry, BaseAcquisitionGeometry): + dim_order = DataOrder.CIL_AG_LABELS + else: + dim_order = DataOrder.CIL_IG_LABELS + else: + raise ValueError("Unknown engine expected one of {0} got {1}".format(DataOrder.ENGINES, engine)) + + dimensions = [] + for label in dim_order: + if label in geometry.dimension_labels: + dimensions.append(label) + + return dimensions + + +def check_order_for_engine(engine, geometry): + order_requested = get_order_for_engine(engine, geometry) + + if order_requested == list(geometry.dimension_labels): + return True + else: + raise ValueError("Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" + .format(order_requested, list(geometry.dimension_labels), engine)) diff --git a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py index 9724a97fa3..51d70ca0de 100644 --- a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py @@ -17,10 +17,9 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import DataOrder +from cil.framework import check_order_for_engine from cil.optimisation.operators import LinearOperator, ChannelwiseOperator from cil.framework.BlockGeometry import BlockGeometry -from cil.framework import check_order_for_engine from cil.optimisation.operators import BlockOperator from cil.plugins.astra.operators import AstraProjector3D from cil.plugins.astra.operators import AstraProjector2D diff --git a/Wrappers/Python/cil/plugins/astra/processors/FBP.py b/Wrappers/Python/cil/plugins/astra/processors/FBP.py index b5076ec02b..dbe3fc5f96 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/FBP.py +++ b/Wrappers/Python/cil/plugins/astra/processors/FBP.py @@ -17,8 +17,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import DataProcessor -from cil.framework import check_order_for_engine +from cil.framework import DataProcessor, check_order_for_engine from cil.plugins.astra.processors.FBP_Flexible import FBP_Flexible from cil.plugins.astra.processors.FDK_Flexible import FDK_Flexible from cil.plugins.astra.processors.FBP_Flexible import FBP_CPU diff --git a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py index 325d4f0c46..f29bd41c1a 100644 --- a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py @@ -17,8 +17,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import ImageData, AcquisitionData, AcquisitionGeometry -from cil.framework import acquisition_labels, check_order_for_engine +from cil.framework import ImageData, AcquisitionData, AcquisitionGeometry, check_order_for_engine, acquisition_labels from cil.framework.BlockGeometry import BlockGeometry from cil.optimisation.operators import BlockOperator from cil.optimisation.operators import LinearOperator From 1b5e76270790d8498bb2c53e1748cfce99bdaa03 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Fri, 9 Feb 2024 14:01:36 +0000 Subject: [PATCH 10/72] Destroy DataOrder. --- Wrappers/Python/cil/framework/__init__.py | 2 +- Wrappers/Python/cil/framework/framework.py | 12 ++--- Wrappers/Python/cil/framework/label.py | 57 +++++++++++++--------- Wrappers/Python/cil/io/ZEISSDataReader.py | 6 +-- Wrappers/Python/cil/plugins/TomoPhantom.py | 4 +- 5 files changed, 47 insertions(+), 34 deletions(-) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index bcb6539327..422ce9ecd3 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -33,4 +33,4 @@ from .BlockDataContainer import BlockDataContainer from .BlockGeometry import BlockGeometry from .framework import Partitioner -from .label import acquisition_labels, image_labels, DataOrder, get_order_for_engine, check_order_for_engine +from .label import acquisition_labels, image_labels, data_order, get_order_for_engine, check_order_for_engine diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/framework.py index 06b507455a..66368acae1 100644 --- a/Wrappers/Python/cil/framework/framework.py +++ b/Wrappers/Python/cil/framework/framework.py @@ -30,7 +30,7 @@ from .base import BaseAcquisitionGeometry -from .label import image_labels, acquisition_labels, DataOrder, get_order_for_engine +from .label import image_labels, acquisition_labels, data_order, get_order_for_engine from cil.utilities.multiprocessing import NUM_THREADS # check for the extension @@ -290,7 +290,7 @@ def ndim(self): @property def dimension_labels(self): - labels_default = DataOrder.CIL_IG_LABELS + labels_default = data_order["CIL_IG_LABELS"] shape_default = [ self.channels, self.voxel_num_z, @@ -315,7 +315,7 @@ def dimension_labels(self, val): self.set_labels(val) def set_labels(self, labels): - labels_default = DataOrder.CIL_IG_LABELS + labels_default = data_order["CIL_IG_LABELS"] #check input and store. This value is not used directly if labels is not None: @@ -2153,7 +2153,7 @@ def shape(self): @property def dimension_labels(self): - labels_default = DataOrder.CIL_AG_LABELS + labels_default = data_order["CIL_AG_LABELS"] shape_default = [self.config.channels.num_channels, self.config.angles.num_positions, @@ -2180,7 +2180,7 @@ def dimension_labels(self): @dimension_labels.setter def dimension_labels(self, val): - labels_default = DataOrder.CIL_AG_LABELS + labels_default = data_order["CIL_AG_LABELS"] #check input and store. This value is not used directly if val is not None: @@ -2817,7 +2817,7 @@ def reorder(self, order=None): :type order: list, sting ''' - if order in DataOrder.ENGINES: + if order in data_order["ENGINES"]: order = get_order_for_engine(order, self.geometry) try: diff --git a/Wrappers/Python/cil/framework/label.py b/Wrappers/Python/cil/framework/label.py index bdf7f67e6b..d065bf7dff 100644 --- a/Wrappers/Python/cil/framework/label.py +++ b/Wrappers/Python/cil/framework/label.py @@ -19,7 +19,7 @@ # Joshua DM Hellier (University of Manchester) # Nicholas Whyatt (UKRI-STFC) -from typing import TypedDict +from typing import TypedDict, List from .base import BaseAcquisitionGeometry @@ -49,6 +49,20 @@ class AcquisitionLabels(TypedDict): DIM3: str +class DataOrder(TypedDict): + ENGINES: List[str] + ASTRA_IG_LABELS: List[str] + TIGRE_IG_LABELS: List[str] + ASTRA_AG_LABELS: List[str] + TIGRE_AG_LABELS: List[str] + CIL_IG_LABELS: List[str] + CIL_AG_LABELS: List[str] + TOMOPHANTOM_IG_LABELS: List[str] + + + + + image_labels: ImageLabels = {"RANDOM": "random", "RANDOM_INT": "random_int", "CHANNEL": "channel", @@ -70,38 +84,36 @@ class AcquisitionLabels(TypedDict): "DIM2": "2D", "DIM3": "3D"} - -class DataOrder: - - ENGINES = ['astra','tigre','cil'] - - ASTRA_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] - TIGRE_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] - ASTRA_AG_LABELS = [acquisition_labels["CHANNEL"], acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"], acquisition_labels["HORIZONTAL"]] - TIGRE_AG_LABELS = [acquisition_labels["CHANNEL"], acquisition_labels["ANGLE"], acquisition_labels["VERTICAL"], acquisition_labels["HORIZONTAL"]] - CIL_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] - CIL_AG_LABELS = [acquisition_labels["CHANNEL"], acquisition_labels["ANGLE"], acquisition_labels["VERTICAL"], acquisition_labels["HORIZONTAL"]] - TOMOPHANTOM_IG_LABELS = [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] +data_order: DataOrder = \ + {"ENGINES": ["astra", "tigre", "cil"], + "ASTRA_IG_LABELS": [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]], + "TIGRE_IG_LABELS": [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]], + "ASTRA_AG_LABELS": [acquisition_labels["CHANNEL"], acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"], acquisition_labels["HORIZONTAL"]], + "TIGRE_AG_LABELS": [acquisition_labels["CHANNEL"], acquisition_labels["ANGLE"], acquisition_labels["VERTICAL"], acquisition_labels["HORIZONTAL"]], + "CIL_IG_LABELS": [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]], + "CIL_AG_LABELS": [acquisition_labels["CHANNEL"], acquisition_labels["ANGLE"], acquisition_labels["VERTICAL"], acquisition_labels["HORIZONTAL"]], + "TOMOPHANTOM_IG_LABELS": [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] + } def get_order_for_engine(engine, geometry): if engine == 'astra': if isinstance(geometry, BaseAcquisitionGeometry): - dim_order = DataOrder.ASTRA_AG_LABELS + dim_order = data_order["ASTRA_AG_LABELS"] else: - dim_order = DataOrder.ASTRA_IG_LABELS + dim_order = data_order["ASTRA_IG_LABELS"] elif engine == 'tigre': if isinstance(geometry, BaseAcquisitionGeometry): - dim_order = DataOrder.TIGRE_AG_LABELS + dim_order = data_order["TIGRE_AG_LABELS"] else: - dim_order = DataOrder.TIGRE_IG_LABELS + dim_order = data_order["TIGRE_IG_LABELS"] elif engine == 'cil': if isinstance(geometry, BaseAcquisitionGeometry): - dim_order = DataOrder.CIL_AG_LABELS + dim_order = data_order["CIL_AG_LABELS"] else: - dim_order = DataOrder.CIL_IG_LABELS + dim_order = data_order["CIL_IG_LABELS"] else: - raise ValueError("Unknown engine expected one of {0} got {1}".format(DataOrder.ENGINES, engine)) + raise ValueError("Unknown engine expected one of {0} got {1}".format(data_order["ENGINES"], engine)) dimensions = [] for label in dim_order: @@ -117,5 +129,6 @@ def check_order_for_engine(engine, geometry): if order_requested == list(geometry.dimension_labels): return True else: - raise ValueError("Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" - .format(order_requested, list(geometry.dimension_labels), engine)) + raise ValueError( + "Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" + .format(order_requested, list(geometry.dimension_labels), engine)) diff --git a/Wrappers/Python/cil/io/ZEISSDataReader.py b/Wrappers/Python/cil/io/ZEISSDataReader.py index 8772f94485..84788aad25 100644 --- a/Wrappers/Python/cil/io/ZEISSDataReader.py +++ b/Wrappers/Python/cil/io/ZEISSDataReader.py @@ -19,7 +19,7 @@ # Andrew Shartis (UES, Inc.) -from cil.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry, DataOrder, acquisition_labels +from cil.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry, acquisition_labels, data_order import numpy as np import os import olefile @@ -129,10 +129,10 @@ def set_up(self, if roi is not None: if metadata['data geometry'] == 'acquisition': - allowed_labels = DataOrder.CIL_AG_LABELS + allowed_labels = data_order["CIL_AG_LABELS"] zeiss_data_order = {'angle':0, 'vertical':1, 'horizontal':2} else: - allowed_labels = DataOrder.CIL_IG_LABELS + allowed_labels = data_order["CIL_IG_LABELS"] zeiss_data_order = {'vertical':0, 'horizontal_y':1, 'horizontal_x':2} # check roi labels and create tuple for slicing diff --git a/Wrappers/Python/cil/plugins/TomoPhantom.py b/Wrappers/Python/cil/plugins/TomoPhantom.py index 3f1e23c35d..367c88f6cd 100644 --- a/Wrappers/Python/cil/plugins/TomoPhantom.py +++ b/Wrappers/Python/cil/plugins/TomoPhantom.py @@ -17,7 +17,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import ImageData, DataOrder, image_labels +from cil.framework import ImageData, image_labels, data_order import tomophantom from tomophantom import TomoP2D, TomoP3D import os @@ -149,7 +149,7 @@ def get_ImageData(num_model, geometry): ''' ig = geometry.copy() - ig.set_labels(DataOrder.TOMOPHANTOM_IG_LABELS) + ig.set_labels(data_order["TOMOPHANTOM_IG_LABELS"]) num_dims = len(ig.dimension_labels) if image_labels["CHANNEL"] in ig.dimension_labels: From b3671e2f506bb4d11bb10acfc687ed1dffb36220 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Fri, 9 Feb 2024 14:18:25 +0000 Subject: [PATCH 11/72] Adjust whitespace. --- Wrappers/Python/cil/framework/label.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Wrappers/Python/cil/framework/label.py b/Wrappers/Python/cil/framework/label.py index d065bf7dff..932116a54d 100644 --- a/Wrappers/Python/cil/framework/label.py +++ b/Wrappers/Python/cil/framework/label.py @@ -60,9 +60,6 @@ class DataOrder(TypedDict): TOMOPHANTOM_IG_LABELS: List[str] - - - image_labels: ImageLabels = {"RANDOM": "random", "RANDOM_INT": "random_int", "CHANNEL": "channel", From 16f0cc33ca6637d06c85172bc8aed70effeefb82 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Fri, 9 Feb 2024 14:29:17 +0000 Subject: [PATCH 12/72] Move cilacc out of framework.py . --- Wrappers/Python/cil/framework/__init__.py | 2 +- Wrappers/Python/cil/framework/cilacc.py | 16 ++++++++ Wrappers/Python/cil/framework/framework.py | 44 +++++++--------------- 3 files changed, 31 insertions(+), 31 deletions(-) create mode 100644 Wrappers/Python/cil/framework/cilacc.py diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 422ce9ecd3..91c5142aa8 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -22,7 +22,7 @@ from datetime import timedelta, datetime import warnings from functools import reduce -from .framework import cilacc +from .cilacc import cilacc from .framework import DataContainer from .framework import ImageData, AcquisitionData from .framework import ImageGeometry, AcquisitionGeometry diff --git a/Wrappers/Python/cil/framework/cilacc.py b/Wrappers/Python/cil/framework/cilacc.py new file mode 100644 index 0000000000..8b457a58aa --- /dev/null +++ b/Wrappers/Python/cil/framework/cilacc.py @@ -0,0 +1,16 @@ +import ctypes +import platform +from ctypes import util +# check for the extension + +if platform.system() == 'Linux': + dll = 'libcilacc.so' +elif platform.system() == 'Windows': + dll_file = 'cilacc.dll' + dll = util.find_library(dll_file) +elif platform.system() == 'Darwin': + dll = 'libcilacc.dylib' +else: + raise ValueError('Not supported platform, ', platform.system()) + +cilacc = ctypes.cdll.LoadLibrary(dll) diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/framework.py index 66368acae1..cac72361c7 100644 --- a/Wrappers/Python/cil/framework/framework.py +++ b/Wrappers/Python/cil/framework/framework.py @@ -22,32 +22,16 @@ import warnings from functools import reduce from numbers import Number -import ctypes, platform -from ctypes import util +import ctypes import math import weakref import logging - +from . import cilacc from .base import BaseAcquisitionGeometry from .label import image_labels, acquisition_labels, data_order, get_order_for_engine - from cil.utilities.multiprocessing import NUM_THREADS -# check for the extension - -if platform.system() == 'Linux': - dll = 'libcilacc.so' -elif platform.system() == 'Windows': - dll_file = 'cilacc.dll' - dll = util.find_library(dll_file) -elif platform.system() == 'Darwin': - dll = 'libcilacc.dylib' -else: - raise ValueError('Not supported platform, ', platform.system()) - -cilacc = ctypes.cdll.LoadLibrary(dll) - -from cil.framework.BlockGeometry import BlockGeometry +from .BlockGeometry import BlockGeometry class Partitioner(object): '''Interface for AcquisitionData to be able to partition itself in a number of batches. @@ -3264,19 +3248,19 @@ def _axpby(self, a, b, y, out, dtype=numpy.float32, num_threads=NUM_THREADS): ctypes.POINTER(ctypes.c_float), # pointer to the second array ctypes.POINTER(ctypes.c_float), # pointer to the third array ctypes.POINTER(ctypes.c_float), # pointer to A - ctypes.c_int, # type of type of A selector (int) + ctypes.c_int, # type of type of A selector (int) ctypes.POINTER(ctypes.c_float), # pointer to B - ctypes.c_int, # type of type of B selector (int) - ctypes.c_longlong, # type of size of first array + ctypes.c_int, # type of type of B selector (int) + ctypes.c_longlong, # type of size of first array ctypes.c_int] # number of threads - cilacc.daxpby.argtypes = [ctypes.POINTER(ctypes.c_double), # pointer to the first array - ctypes.POINTER(ctypes.c_double), # pointer to the second array - ctypes.POINTER(ctypes.c_double), # pointer to the third array - ctypes.POINTER(ctypes.c_double), # type of A (c_double) - ctypes.c_int, # type of type of A selector (int) - ctypes.POINTER(ctypes.c_double), # type of B (c_double) - ctypes.c_int, # type of type of B selector (int) - ctypes.c_longlong, # type of size of first array + cilacc.daxpby.argtypes = [ctypes.POINTER(ctypes.c_double), # pointer to the first array + ctypes.POINTER(ctypes.c_double), # pointer to the second array + ctypes.POINTER(ctypes.c_double), # pointer to the third array + ctypes.POINTER(ctypes.c_double), # type of A (c_double) + ctypes.c_int, # type of type of A selector (int) + ctypes.POINTER(ctypes.c_double), # type of B (c_double) + ctypes.c_int, # type of type of B selector (int) + ctypes.c_longlong, # type of size of first array ctypes.c_int] # number of threads if f(x_p, y_p, out_p, a_p, a_vec, b_p, b_vec, ndx.size, num_threads) != 0: From aeb9985dad2cb687bde1a6d76a0dc7937b23e0f6 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Fri, 9 Feb 2024 15:19:43 +0000 Subject: [PATCH 13/72] Move DataContainer and ImageGeometry into separate file (still coupled). --- .../Python/cil/framework/BlockGeometry.py | 2 +- .../Python/cil/framework/DataContainer.py | 1502 +++++++++++++++ Wrappers/Python/cil/framework/__init__.py | 9 +- Wrappers/Python/cil/framework/framework.py | 1625 +---------------- .../operators/BlurringOperator.py | 6 +- .../operators/ChannelwiseOperator.py | 4 +- .../operators/GradientOperator.py | 3 +- .../SparseFiniteDifferenceOperator.py | 3 +- Wrappers/Python/cil/processors/Padder.py | 4 +- Wrappers/Python/cil/processors/Slicer.py | 4 +- Wrappers/Python/cil/utilities/display.py | 3 +- .../Python/test/test_BlockDataContainer.py | 4 +- Wrappers/Python/test/test_BlockOperator.py | 3 +- Wrappers/Python/test/test_DataContainer.py | 5 +- Wrappers/Python/test/test_DataProcessor.py | 6 +- .../Python/test/test_PluginsTigre_General.py | 3 +- Wrappers/Python/test/test_SIRF.py | 38 +- Wrappers/Python/test/test_algorithms.py | 6 +- Wrappers/Python/test/test_dataexample.py | 3 +- Wrappers/Python/test/test_functions.py | 4 +- Wrappers/Python/test/test_run_test.py | 3 +- Wrappers/Python/test/test_subset.py | 4 +- 22 files changed, 1623 insertions(+), 1621 deletions(-) create mode 100644 Wrappers/Python/cil/framework/DataContainer.py diff --git a/Wrappers/Python/cil/framework/BlockGeometry.py b/Wrappers/Python/cil/framework/BlockGeometry.py index b2a47a6621..f7eb743757 100644 --- a/Wrappers/Python/cil/framework/BlockGeometry.py +++ b/Wrappers/Python/cil/framework/BlockGeometry.py @@ -18,7 +18,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt import functools -from cil.framework.BlockDataContainer import BlockDataContainer +from .BlockDataContainer import BlockDataContainer class BlockGeometry(object): diff --git a/Wrappers/Python/cil/framework/DataContainer.py b/Wrappers/Python/cil/framework/DataContainer.py new file mode 100644 index 0000000000..df92ae824c --- /dev/null +++ b/Wrappers/Python/cil/framework/DataContainer.py @@ -0,0 +1,1502 @@ +import copy +import ctypes +import logging +import warnings +from functools import reduce +from numbers import Number + +import numpy + +from .label import image_labels, data_order, get_order_for_engine +from .cilacc import cilacc +from cil.utilities.multiprocessing import NUM_THREADS + + +def message(cls, msg, *args): + msg = "{0}: " + msg + for i in range(len(args)): + msg += " {%d}" %(i+1) + args = list(args) + args.insert(0, cls.__name__ ) + + return msg.format(*args ) + + +class ImageGeometry(object): + + @property + def shape(self): + + shape_dict = {image_labels["CHANNEL"]: self.channels, + image_labels["VERTICAL"]: self.voxel_num_z, + image_labels["HORIZONTAL_Y"]: self.voxel_num_y, + image_labels["HORIZONTAL_X"]: self.voxel_num_x} + + shape = [] + for label in self.dimension_labels: + shape.append(shape_dict[label]) + + return tuple(shape) + + @shape.setter + def shape(self, val): + print("Deprecated - shape will be set automatically") + + @property + def spacing(self): + + spacing_dict = {image_labels["CHANNEL"]: self.channel_spacing, + image_labels["VERTICAL"]: self.voxel_size_z, + image_labels["HORIZONTAL_Y"]: self.voxel_size_y, + image_labels["HORIZONTAL_X"]: self.voxel_size_x} + + spacing = [] + for label in self.dimension_labels: + spacing.append(spacing_dict[label]) + + return tuple(spacing) + + @property + def length(self): + return len(self.dimension_labels) + + @property + def ndim(self): + return len(self.dimension_labels) + + @property + def dimension_labels(self): + + labels_default = data_order["CIL_IG_LABELS"] + + shape_default = [ self.channels, + self.voxel_num_z, + self.voxel_num_y, + self.voxel_num_x] + + try: + labels = list(self._dimension_labels) + except AttributeError: + labels = labels_default.copy() + + for i, x in enumerate(shape_default): + if x == 0 or x==1: + try: + labels.remove(labels_default[i]) + except ValueError: + pass #if not in custom list carry on + return tuple(labels) + + @dimension_labels.setter + def dimension_labels(self, val): + self.set_labels(val) + + def set_labels(self, labels): + labels_default = data_order["CIL_IG_LABELS"] + + #check input and store. This value is not used directly + if labels is not None: + for x in labels: + if x not in labels_default: + raise ValueError('Requested axis are not possible. Accepted label names {},\ngot {}'\ + .format(labels_default,labels)) + + self._dimension_labels = tuple(labels) + + def __eq__(self, other): + + if not isinstance(other, self.__class__): + return False + + if self.voxel_num_x == other.voxel_num_x \ + and self.voxel_num_y == other.voxel_num_y \ + and self.voxel_num_z == other.voxel_num_z \ + and self.voxel_size_x == other.voxel_size_x \ + and self.voxel_size_y == other.voxel_size_y \ + and self.voxel_size_z == other.voxel_size_z \ + and self.center_x == other.center_x \ + and self.center_y == other.center_y \ + and self.center_z == other.center_z \ + and self.channels == other.channels \ + and self.channel_spacing == other.channel_spacing \ + and self.dimension_labels == other.dimension_labels: + + return True + + return False + + @property + def dtype(self): + return self._dtype + + @dtype.setter + def dtype(self, val): + self._dtype = val + + def __init__(self, + voxel_num_x=0, + voxel_num_y=0, + voxel_num_z=0, + voxel_size_x=1, + voxel_size_y=1, + voxel_size_z=1, + center_x=0, + center_y=0, + center_z=0, + channels=1, + **kwargs): + + self.voxel_num_x = int(voxel_num_x) + self.voxel_num_y = int(voxel_num_y) + self.voxel_num_z = int(voxel_num_z) + self.voxel_size_x = float(voxel_size_x) + self.voxel_size_y = float(voxel_size_y) + self.voxel_size_z = float(voxel_size_z) + self.center_x = center_x + self.center_y = center_y + self.center_z = center_z + self.channels = channels + self.channel_labels = None + self.channel_spacing = 1.0 + self.dimension_labels = kwargs.get('dimension_labels', None) + self.dtype = kwargs.get('dtype', numpy.float32) + + + def get_slice(self,channel=None, vertical=None, horizontal_x=None, horizontal_y=None): + ''' + Returns a new ImageGeometry of a single slice of in the requested direction. + ''' + geometry_new = self.copy() + if channel is not None: + geometry_new.channels = 1 + + try: + geometry_new.channel_labels = [self.channel_labels[channel]] + except: + geometry_new.channel_labels = None + + if vertical is not None: + geometry_new.voxel_num_z = 0 + + if horizontal_y is not None: + geometry_new.voxel_num_y = 0 + + if horizontal_x is not None: + geometry_new.voxel_num_x = 0 + + return geometry_new + + def get_order_by_label(self, dimension_labels, default_dimension_labels): + order = [] + for i, el in enumerate(default_dimension_labels): + for j, ek in enumerate(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 + + def get_max_x(self): + return self.center_x + 0.5*self.voxel_num_x*self.voxel_size_x + + def get_min_y(self): + return self.center_y - 0.5*self.voxel_num_y*self.voxel_size_y + + def get_max_y(self): + return self.center_y + 0.5*self.voxel_num_y*self.voxel_size_y + + def get_min_z(self): + if not self.voxel_num_z == 0: + return self.center_z - 0.5*self.voxel_num_z*self.voxel_size_z + else: + return 0 + + def get_max_z(self): + if not self.voxel_num_z == 0: + return self.center_z + 0.5*self.voxel_num_z*self.voxel_size_z + else: + return 0 + + def clone(self): + '''returns a copy of the ImageGeometry''' + return copy.deepcopy(self) + + def copy(self): + '''alias of clone''' + return self.clone() + + def __str__ (self): + repres = "" + repres += "Number of channels: {0}\n".format(self.channels) + repres += "channel_spacing: {0}\n".format(self.channel_spacing) + + if self.voxel_num_z > 0: + repres += "voxel_num : x{0},y{1},z{2}\n".format(self.voxel_num_x, self.voxel_num_y, self.voxel_num_z) + 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) + else: + repres += "voxel_num : x{0},y{1}\n".format(self.voxel_num_x, self.voxel_num_y) + repres += "voxel_size : x{0},y{1}\n".format(self.voxel_size_x, self.voxel_size_y) + repres += "center : x{0},y{1}\n".format(self.center_x, self.center_y) + + return repres + def allocate(self, value=0, **kwargs): + '''allocates an ImageData according to the size expressed in the instance + + :param value: accepts numbers to allocate an uniform array, or a string as 'random' or 'random_int' to create a random array or None. + :type value: number or string, default None allocates empty memory block, default 0 + :param dtype: numerical type to allocate + :type dtype: numpy type, default numpy.float32 + ''' + + dtype = kwargs.get('dtype', self.dtype) + + if kwargs.get('dimension_labels', None) is not None: + raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") + + out = ImageData(geometry=self.copy(), + dtype=dtype, + suppress_warning=True) + + if isinstance(value, Number): + # it's created empty, so we make it 0 + out.array.fill(value) + else: + if value == image_labels["RANDOM"]: + seed = kwargs.get('seed', None) + if seed is not None: + numpy.random.seed(seed) + if numpy.iscomplexobj(out.array): + r = numpy.random.random_sample(self.shape) + 1j * numpy.random.random_sample(self.shape) + out.fill(r) + else: + out.fill(numpy.random.random_sample(self.shape)) + elif value == image_labels["RANDOM_INT"]: + seed = kwargs.get('seed', None) + if seed is not None: + numpy.random.seed(seed) + max_value = kwargs.get('max_value', 100) + r = numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) + out.fill(numpy.asarray(r, dtype=self.dtype)) + elif value is None: + pass + 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''' + + @property + def geometry(self): + return None + + @geometry.setter + def geometry(self, val): + if val is not None: + raise TypeError("DataContainers cannot hold a geometry, use ImageData or AcquisitionData instead") + + @property + def dimension_labels(self): + + if self._dimension_labels is None: + default_labels = [0]*self.number_of_dimensions + for i in range(self.number_of_dimensions): + default_labels[i] = 'dimension_{0:02}'.format(i) + return tuple(default_labels) + else: + return self._dimension_labels + + @dimension_labels.setter + def dimension_labels(self, val): + if val is None: + self._dimension_labels = None + elif len(list(val))==self.number_of_dimensions: + self._dimension_labels = tuple(val) + else: + raise ValueError("dimension_labels expected a list containing {0} strings got {1}".format(self.number_of_dimensions, val)) + + @property + def shape(self): + '''Returns the shape of the DataContainer''' + return self.array.shape + + @property + def ndim(self): + '''Returns the ndim of the DataContainer''' + return self.array.ndim + + @shape.setter + def shape(self, val): + print("Deprecated - shape will be set automatically") + + @property + def number_of_dimensions(self): + '''Returns the shape of the of the DataContainer''' + return len(self.array.shape) + + @property + def dtype(self): + '''Returns the dtype of the data array.''' + return self.array.dtype + + @property + def size(self): + '''Returns the number of elements of the DataContainer''' + return self.array.size + + __container_priority__ = 1 + def __init__ (self, array, deep_copy=True, dimension_labels=None, + **kwargs): + '''Holds the data''' + + if type(array) == numpy.ndarray: + if deep_copy: + self.array = array.copy() + else: + self.array = array + else: + raise TypeError('Array must be NumpyArray, passed {0}'\ + .format(type(array))) + + #Don't set for derived classes + if type(self) is DataContainer: + self.dimension_labels = dimension_labels + + # finally copy the geometry, and force dtype of the geometry of the data = the dype of the data + if 'geometry' in kwargs.keys(): + self.geometry = kwargs['geometry'] + try: + self.geometry.dtype = self.dtype + except: + pass + + def get_dimension_size(self, dimension_label): + + if dimension_label in self.dimension_labels: + i = self.dimension_labels.index(dimension_label) + return self.shape[i] + else: + raise ValueError('Unknown dimension {0}. Should be one of {1}'.format(dimension_label, + self.dimension_labels)) + + def get_dimension_axis(self, dimension_label): + """ + Returns the axis index of the DataContainer array if the specified dimension_label(s) match + any dimension_labels of the DataContainer or their indices + + Parameters + ---------- + dimension_label: string or int or tuple of strings or ints + Specify dimension_label(s) or index of the DataContainer from which to check and return the axis index + + Returns + ------- + int or tuple of ints + The axis index of the DataContainer matching the specified dimension_label + """ + if isinstance(dimension_label,(tuple,list)): + return tuple(self.get_dimension_axis(x) for x in dimension_label) + + if dimension_label in self.dimension_labels: + return self.dimension_labels.index(dimension_label) + elif isinstance(dimension_label, int) and dimension_label >= 0 and dimension_label < self.ndim: + return dimension_label + else: + raise ValueError('Unknown dimension {0}. Should be one of {1}, or an integer in range {2} - {3}'.format(dimension_label, + self.dimension_labels, 0, self.ndim)) + + + def as_array(self): + '''Returns the pointer to the array. + ''' + return self.array + + + def get_slice(self, **kw): + ''' + Returns a new DataContainer containing a single slice in the requested direction. \ + Pass keyword arguments =index + ''' + # Force is not relevant for a DataContainer: + kw.pop('force', None) + + new_array = None + + #get ordered list of current dimensions + dimension_labels_list = list(self.dimension_labels) + + #remove axes from array and labels + for key, value in kw.items(): + if value is not None: + axis = dimension_labels_list.index(key) + dimension_labels_list.remove(key) + if new_array is None: + new_array = self.as_array().take(indices=value, axis=axis) + else: + new_array = new_array.take(indices=value, axis=axis) + + if new_array.ndim > 1: + return DataContainer(new_array, False, dimension_labels_list, suppress_warning=True) + else: + return VectorData(new_array, dimension_labels=dimension_labels_list) + + def reorder(self, order=None): + ''' + reorders the data in memory as requested. + + :param order: ordered list of labels from self.dimension_labels, or order for engine 'astra' or 'tigre' + :type order: list, sting + ''' + + if order in data_order["ENGINES"]: + order = get_order_for_engine(order, self.geometry) + + try: + if len(order) != len(self.shape): + raise ValueError('The axes list for resorting must have {0} dimensions. Got {1}'.format(len(self.shape), len(order))) + except TypeError as ae: + raise ValueError('The order must be an iterable with __len__ implemented, like a list or a tuple. Got {}'.format(type(order))) + + correct = True + for el in order: + correct = correct and el in self.dimension_labels + if not correct: + raise ValueError('The axes list for resorting must contain the dimension_labels {0} got {1}'.format(self.dimension_labels, order)) + + new_order = [0]*len(self.shape) + dimension_labels_new = [0]*len(self.shape) + + for i, axis in enumerate(order): + new_order[i] = self.dimension_labels.index(axis) + dimension_labels_new[i] = axis + + self.array = numpy.ascontiguousarray(numpy.transpose(self.array, new_order)) + + if self.geometry is None: + self.dimension_labels = dimension_labels_new + else: + self.geometry.set_labels(dimension_labels_new) + + def fill(self, array, **dimension): + '''fills the internal data array with the DataContainer, numpy array or number provided + + :param array: number, numpy array or DataContainer to copy into the DataContainer + :type array: DataContainer or subclasses, numpy array or number + :param dimension: dictionary, optional + + if the passed numpy array points to the same array that is contained in the DataContainer, + it just returns + + In case a DataContainer or subclass is passed, there will be a check of the geometry, + if present, and the array will be resorted if the data is not in the appropriate order. + + User may pass a named parameter to specify in which axis the fill should happen: + + dc.fill(some_data, vertical=1, horizontal_x=32) + will copy the data in some_data into the data container. + ''' + if id(array) == id(self.array): + return + if dimension == {}: + if isinstance(array, numpy.ndarray): + if array.shape != self.shape: + raise ValueError('Cannot fill with the provided array.' + \ + 'Expecting shape {0} got {1}'.format( + self.shape,array.shape)) + numpy.copyto(self.array, array) + elif isinstance(array, Number): + self.array.fill(array) + elif issubclass(array.__class__ , DataContainer): + + try: + if self.dimension_labels != array.dimension_labels: + raise ValueError('Input array is not in the same order as destination array. Use "array.reorder()"') + except AttributeError: + pass + + if self.array.shape == array.shape: + numpy.copyto(self.array, array.array) + else: + raise ValueError('Cannot fill with the provided array.' + \ + 'Expecting shape {0} got {1}'.format( + self.shape,array.shape)) + else: + raise TypeError('Can fill only with number, numpy array or DataContainer and subclasses. Got {}'.format(type(array))) + else: + + axis = [':']* self.number_of_dimensions + dimension_labels = list(self.dimension_labels) + for k,v in dimension.items(): + i = dimension_labels.index(k) + axis[i] = v + + command = 'self.array[' + i = 0 + for el in axis: + if i > 0: + command += ',' + command += str(el) + i+=1 + + if isinstance(array, numpy.ndarray): + command = command + "] = array[:]" + elif issubclass(array.__class__, DataContainer): + command = command + "] = array.as_array()[:]" + elif isinstance (array, Number): + command = command + "] = array" + else: + raise TypeError('Can fill only with number, numpy array or DataContainer and subclasses. Got {}'.format(type(array))) + exec(command) + + + def check_dimensions(self, other): + return self.shape == other.shape + + ## algebra + + def __add__(self, other): + return self.add(other) + def __mul__(self, other): + return self.multiply(other) + def __sub__(self, other): + return self.subtract(other) + def __div__(self, other): + return self.divide(other) + def __truediv__(self, other): + return self.divide(other) + def __pow__(self, other): + return self.power(other) + + + # reverse operand + def __radd__(self, other): + return self + other + # __radd__ + + def __rsub__(self, other): + return (-1 * self) + other + # __rsub__ + + def __rmul__(self, other): + return self * other + # __rmul__ + + def __rdiv__(self, other): + tmp = self.power(-1) + tmp *= other + return tmp + # __rdiv__ + def __rtruediv__(self, other): + return self.__rdiv__(other) + + def __rpow__(self, other): + if isinstance(other, Number) : + fother = numpy.ones(numpy.shape(self.array)) * other + return type(self)(fother ** self.array , + dimension_labels=self.dimension_labels, + geometry=self.geometry) + # __rpow__ + + # in-place arithmetic operators: + # (+=, -=, *=, /= , //=, + # must return self + + def __iadd__(self, other): + kw = {'out':self} + return self.add(other, **kw) + + def __imul__(self, other): + kw = {'out':self} + return self.multiply(other, **kw) + + def __isub__(self, other): + kw = {'out':self} + return self.subtract(other, **kw) + + def __idiv__(self, other): + kw = {'out':self} + return self.divide(other, **kw) + + def __itruediv__(self, other): + kw = {'out':self} + return self.divide(other, **kw) + + def __neg__(self): + '''negation operator''' + return -1 * self + + def __str__ (self, representation=False): + repres = "" + repres += "Number of dimensions: {0}\n".format(self.number_of_dimensions) + repres += "Shape: {0}\n".format(self.shape) + repres += "Axis labels: {0}\n".format(self.dimension_labels) + if representation: + repres += "Representation: \n{0}\n".format(self.array) + return repres + + def get_data_axes_order(self,new_order=None): + '''returns the axes label of self as a list + + If new_order is None returns the labels of the axes as a sorted-by-key list. + If new_order is a list of length number_of_dimensions, returns a list + with the indices of the axes in new_order with respect to those in + self.dimension_labels: i.e. + >>> self.dimension_labels = {0:'horizontal',1:'vertical'} + >>> new_order = ['vertical','horizontal'] + returns [1,0] + ''' + if new_order is None: + return self.dimension_labels + else: + if len(new_order) == self.number_of_dimensions: + + axes_order = [0]*len(self.shape) + for i, axis in enumerate(new_order): + axes_order[i] = self.dimension_labels.index(axis) + return axes_order + else: + raise ValueError('Expecting {0} axes, got {2}'\ + .format(len(self.shape),len(new_order))) + + def clone(self): + '''returns a copy of DataContainer''' + return copy.deepcopy(self) + + def copy(self): + '''alias of clone''' + return self.clone() + + ## binary operations + + def pixel_wise_binary(self, pwop, x2, *args, **kwargs): + out = kwargs.get('out', None) + + if out is None: + if isinstance(x2, Number): + out = pwop(self.as_array() , x2 , *args, **kwargs ) + elif issubclass(x2.__class__ , DataContainer): + out = pwop(self.as_array() , x2.as_array() , *args, **kwargs ) + elif isinstance(x2, numpy.ndarray): + out = pwop(self.as_array() , x2 , *args, **kwargs ) + else: + raise TypeError('Expected x2 type as number or DataContainer, got {}'.format(type(x2))) + geom = self.geometry + if geom is not None: + geom = self.geometry.copy() + return type(self)(out, + deep_copy=False, + dimension_labels=self.dimension_labels, + geometry= None if self.geometry is None else self.geometry.copy(), + suppress_warning=True) + + + elif issubclass(type(out), DataContainer) and issubclass(type(x2), DataContainer): + if self.check_dimensions(out) and self.check_dimensions(x2): + 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 {} x2 {} expected {}".format( out.shape,x2.shape ,self.shape))) + elif issubclass(type(out), DataContainer) and \ + isinstance(x2, (Number, numpy.ndarray)): + if self.check_dimensions(out): + if isinstance(x2, numpy.ndarray) and\ + not (x2.shape == self.shape and x2.dtype == self.dtype): + raise ValueError(message(type(self), + "Wrong size for data memory: out {} x2 {} expected {}"\ + .format( out.shape,x2.shape ,self.shape))) + 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: + kwargs['out'] = out + pwop(self.as_array(), x2, *args, **kwargs) + #return type(self)(out, + # deep_copy=False, + # dimension_labels=self.dimension_labels, + # geometry=self.geometry) + else: + raise ValueError (message(type(self), "incompatible class:" , pwop.__name__, type(out))) + + 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 sapyb(self, a, y, b, out=None, num_threads=NUM_THREADS): + '''performs a*self + b * y. Can be done in-place + + Parameters + ---------- + a : multiplier for self, can be a number or a numpy array or a DataContainer + y : DataContainer + b : multiplier for y, can be a number or a numpy array or a DataContainer + out : return DataContainer, if None a new DataContainer is returned, default None. + out can be self or y. + num_threads : number of threads to use during the calculation, using the CIL C library + It will try to use the CIL C library and default to numpy operations, in case the C library does not handle the types. + + + Example + ------- + + >>> a = 2 + >>> b = 3 + >>> ig = ImageGeometry(10,11) + >>> x = ig.allocate(1) + >>> y = ig.allocate(2) + >>> out = x.sapyb(a,y,b) + ''' + ret_out = False + + if out is None: + out = self * 0. + ret_out = True + + if out.dtype in [ numpy.float32, numpy.float64 ]: + # handle with C-lib _axpby + try: + self._axpby(a, b, y, out, out.dtype, num_threads) + if ret_out: + return out + return + except RuntimeError as rte: + warnings.warn("sapyb defaulting to Python due to: {}".format(rte)) + except TypeError as te: + warnings.warn("sapyb defaulting to Python due to: {}".format(te)) + finally: + pass + + + # cannot be handled by _axpby + ax = self * a + y.multiply(b, out=out) + out.add(ax, out=out) + + if ret_out: + return out + + def _axpby(self, a, b, y, out, dtype=numpy.float32, num_threads=NUM_THREADS): + '''performs axpby with cilacc C library, can be done in-place. + + Does the operation .. math:: a*x+b*y and stores the result in out, where x is self + + :param a: scalar + :type a: float + :param b: scalar + :type b: float + :param y: DataContainer + :param out: DataContainer instance to store the result + :param dtype: data type of the DataContainers + :type dtype: numpy type, optional, default numpy.float32 + :param num_threads: number of threads to run on + :type num_threads: int, optional, default 1/2 CPU of the system + ''' + + c_float_p = ctypes.POINTER(ctypes.c_float) + c_double_p = ctypes.POINTER(ctypes.c_double) + + #convert a and b to numpy arrays and get the reference to the data (length = 1 or ndx.size) + try: + nda = a.as_array() + except: + nda = numpy.asarray(a) + + try: + ndb = b.as_array() + except: + ndb = numpy.asarray(b) + + a_vec = 0 + if nda.size > 1: + a_vec = 1 + + b_vec = 0 + if ndb.size > 1: + b_vec = 1 + + # get the reference to the data + ndx = self.as_array() + ndy = y.as_array() + ndout = out.as_array() + + if ndout.dtype != dtype: + raise Warning("out array of type {0} does not match requested dtype {1}. Using {0}".format(ndout.dtype, dtype)) + dtype = ndout.dtype + if ndx.dtype != dtype: + ndx = ndx.astype(dtype, casting='safe') + if ndy.dtype != dtype: + ndy = ndy.astype(dtype, casting='safe') + if nda.dtype != dtype: + nda = nda.astype(dtype, casting='same_kind') + if ndb.dtype != dtype: + ndb = ndb.astype(dtype, casting='same_kind') + + if dtype == numpy.float32: + x_p = ndx.ctypes.data_as(c_float_p) + y_p = ndy.ctypes.data_as(c_float_p) + out_p = ndout.ctypes.data_as(c_float_p) + a_p = nda.ctypes.data_as(c_float_p) + b_p = ndb.ctypes.data_as(c_float_p) + f = cilacc.saxpby + + elif dtype == numpy.float64: + x_p = ndx.ctypes.data_as(c_double_p) + y_p = ndy.ctypes.data_as(c_double_p) + out_p = ndout.ctypes.data_as(c_double_p) + a_p = nda.ctypes.data_as(c_double_p) + b_p = ndb.ctypes.data_as(c_double_p) + f = cilacc.daxpby + + else: + raise TypeError('Unsupported type {}. Expecting numpy.float32 or numpy.float64'.format(dtype)) + + #out = numpy.empty_like(a) + + + # int psaxpby(float * x, float * y, float * out, float a, float b, long size) + cilacc.saxpby.argtypes = [ctypes.POINTER(ctypes.c_float), # pointer to the first array + ctypes.POINTER(ctypes.c_float), # pointer to the second array + ctypes.POINTER(ctypes.c_float), # pointer to the third array + ctypes.POINTER(ctypes.c_float), # pointer to A + ctypes.c_int, # type of type of A selector (int) + ctypes.POINTER(ctypes.c_float), # pointer to B + ctypes.c_int, # type of type of B selector (int) + ctypes.c_longlong, # type of size of first array + ctypes.c_int] # number of threads + cilacc.daxpby.argtypes = [ctypes.POINTER(ctypes.c_double), # pointer to the first array + ctypes.POINTER(ctypes.c_double), # pointer to the second array + ctypes.POINTER(ctypes.c_double), # pointer to the third array + ctypes.POINTER(ctypes.c_double), # type of A (c_double) + ctypes.c_int, # type of type of A selector (int) + ctypes.POINTER(ctypes.c_double), # type of B (c_double) + ctypes.c_int, # type of type of B selector (int) + ctypes.c_longlong, # type of size of first array + ctypes.c_int] # number of threads + + if f(x_p, y_p, out_p, a_p, a_vec, b_p, b_vec, ndx.size, num_threads) != 0: + raise RuntimeError('axpby execution failed') + + + ## unary operations + 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, + deep_copy=False, + dimension_labels=self.dimension_labels, + geometry=self.geometry, + suppress_warning=True) + elif issubclass(type(out), DataContainer): + if self.check_dimensions(out): + 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: + kwargs['out'] = out + pwop(self.as_array(), *args, **kwargs) + else: + raise ValueError (message(type(self), "incompatible class:" , pwop.__name__, type(out))) + + def abs(self, *args, **kwargs): + return self.pixel_wise_unary(numpy.abs, *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 exp(self, *args, **kwargs): + '''Applies exp pixel-wise to the DataContainer''' + return self.pixel_wise_unary(numpy.exp, *args, **kwargs) + + def log(self, *args, **kwargs): + '''Applies log pixel-wise to the DataContainer''' + return self.pixel_wise_unary(numpy.log, *args, **kwargs) + + ## reductions + def squared_norm(self, **kwargs): + '''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 self.dot(self) + #return self.dot(self) + def norm(self, **kwargs): + '''return the euclidean norm of the DataContainer viewed as a vector''' + return numpy.sqrt(self.squared_norm(**kwargs)) + + def dot(self, other, *args, **kwargs): + '''returns the inner product of 2 DataContainers viewed as vectors. Suitable for real and complex data. + For complex data, the dot method returns a.dot(b.conjugate()) + ''' + 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: + if method == 'numpy': + return numpy.dot(self.as_array().ravel(), other.as_array().ravel().conjugate()) + 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().conjugate()), + 0) + return sf + else: + raise ValueError('Shapes are not aligned: {} != {}'.format(self.shape, other.shape)) + + def _directional_reduction_unary(self, reduction_function, axis=None, out=None, *args, **kwargs): + """ + Returns the result of a unary function, considering the direction from an axis argument to the function + + Parameters + ---------- + reduction_function : function + The unary function to be evaluated + axis : string or tuple of strings or int or tuple of ints, optional + Specify the axis or axes to calculate 'reduction_function' along. Can be specified as + string(s) of dimension_labels or int(s) of indices + Default None calculates the function over the whole array + out: ndarray or DataContainer, optional + Provide an object in which to place the result. The object must have the correct dimensions and + (for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See + `Output type determination `_ for more details. + Default is None + + Returns + ------- + scalar or ndarray + The result of the unary function + """ + if axis is not None: + axis = self.get_dimension_axis(axis) + + if out is None: + result = reduction_function(self.as_array(), axis=axis, *args, **kwargs) + if isinstance(result, numpy.ndarray): + new_dimensions = numpy.array(self.dimension_labels) + new_dimensions = numpy.delete(new_dimensions, axis) + return DataContainer(result, dimension_labels=new_dimensions) + else: + return result + else: + if hasattr(out,'array'): + out_arr = out.array + else: + out_arr = out + + reduction_function(self.as_array(), out=out_arr, axis=axis, *args, **kwargs) + + def sum(self, axis=None, out=None, *args, **kwargs): + """ + Returns the sum of values in the DataContainer + + Parameters + ---------- + axis : string or tuple of strings or int or tuple of ints, optional + Specify the axis or axes to calculate 'sum' along. Can be specified as + string(s) of dimension_labels or int(s) of indices + Default None calculates the function over the whole array + out : ndarray or DataContainer, optional + Provide an object in which to place the result. The object must have the correct dimensions and + (for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See + `Output type determination `_ for more details. + Default is None + + Returns + ------- + scalar or DataContainer + The sum as a scalar or inside a DataContainer with reduced dimension_labels + Default is to accumulate and return data as float64 or complex128 + """ + if kwargs.get('dtype') is not None: + logging.WARNING("dtype argument is ignored, using float64 or complex128") + + if numpy.isrealobj(self.array): + kwargs['dtype'] = numpy.float64 + else: + kwargs['dtype'] = numpy.complex128 + + return self._directional_reduction_unary(numpy.sum, axis=axis, out=out, *args, **kwargs) + + def min(self, axis=None, out=None, *args, **kwargs): + """ + Returns the minimum pixel value in the DataContainer + + Parameters + ---------- + axis : string or tuple of strings or int or tuple of ints, optional + Specify the axis or axes to calculate 'min' along. Can be specified as + string(s) of dimension_labels or int(s) of indices + Default None calculates the function over the whole array + out : ndarray or DataContainer, optional + Provide an object in which to place the result. The object must have the correct dimensions and + (for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See + `Output type determination `_ for more details. + Default is None + + Returns + ------- + scalar or DataContainer + The min as a scalar or inside a DataContainer with reduced dimension_labels + """ + return self._directional_reduction_unary(numpy.min, axis=axis, out=out, *args, **kwargs) + + def max(self, axis=None, out=None, *args, **kwargs): + """ + Returns the maximum pixel value in the DataContainer + + Parameters + ---------- + axis : string or tuple of strings or int or tuple of ints, optional + Specify the axis or axes to calculate 'max' along. Can be specified as + string(s) of dimension_labels or int(s) of indices + Default None calculates the function over the whole array + out : ndarray or DataContainer, optional + Provide an object in which to place the result. The object must have the correct dimensions and + (for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See + `Output type determination `_ for more details. + Default is None + + Returns + ------- + scalar or DataContainer + The max as a scalar or inside a DataContainer with reduced dimension_labels + """ + return self._directional_reduction_unary(numpy.max, axis=axis, out=out, *args, **kwargs) + + def mean(self, axis=None, out=None, *args, **kwargs): + """ + Returns the mean pixel value of the DataContainer + + Parameters + ---------- + axis : string or tuple of strings or int or tuple of ints, optional + Specify the axis or axes to calculate 'mean' along. Can be specified as + string(s) of dimension_labels or int(s) of indices + Default None calculates the function over the whole array + out : ndarray or DataContainer, optional + Provide an object in which to place the result. The object must have the correct dimensions and + (for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See + `Output type determination `_ for more details. + Default is None + + Returns + ------- + scalar or DataContainer + The mean as a scalar or inside a DataContainer with reduced dimension_labels + Default is to accumulate and return data as float64 or complex128 + """ + + if kwargs.get('dtype', None) is not None: + logging.WARNING("dtype argument is ignored, using float64 or complex128") + + if numpy.isrealobj(self.array): + kwargs['dtype'] = numpy.float64 + else: + kwargs['dtype'] = numpy.complex128 + + return self._directional_reduction_unary(numpy.mean, axis=axis, out=out, *args, **kwargs) + + # Logic operators between DataContainers and floats + def __le__(self, other): + '''Returns boolean array of DataContainer less or equal than DataContainer/float''' + if isinstance(other, DataContainer): + return self.as_array()<=other.as_array() + return self.as_array()<=other + + def __lt__(self, other): + '''Returns boolean array of DataContainer less than DataContainer/float''' + if isinstance(other, DataContainer): + return self.as_array()=other.as_array() + return self.as_array()>=other + + def __gt__(self, other): + '''Returns boolean array of DataContainer greater than DataContainer/float''' + if isinstance(other, DataContainer): + return self.as_array()>other.as_array() + return self.as_array()>other + + def __eq__(self, other): + '''Returns boolean array of DataContainer equal to DataContainer/float''' + if isinstance(other, DataContainer): + return self.as_array()==other.as_array() + return self.as_array()==other + + def __ne__(self, other): + '''Returns boolean array of DataContainer negative to DataContainer/float''' + if isinstance(other, DataContainer): + return self.as_array()!=other.as_array() + return self.as_array()!=other + + +class ImageData(DataContainer): + '''DataContainer for holding 2D or 3D DataContainer''' + __container_priority__ = 1 + + @property + def geometry(self): + return self._geometry + + @geometry.setter + def geometry(self, val): + self._geometry = val + + @property + def dimension_labels(self): + return self.geometry.dimension_labels + + @dimension_labels.setter + def dimension_labels(self, val): + if val is not None: + raise ValueError("Unable to set the dimension_labels directly. Use geometry.set_labels() instead") + + def __init__(self, + array = None, + deep_copy=False, + geometry=None, + **kwargs): + + dtype = kwargs.get('dtype', numpy.float32) + + + if geometry is None: + raise AttributeError("ImageData requires a geometry") + + + labels = kwargs.get('dimension_labels', None) + if labels is not None and labels != geometry.dimension_labels: + raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") + + if array is None: + array = numpy.empty(geometry.shape, dtype=dtype) + elif issubclass(type(array) , DataContainer): + array = array.as_array() + elif issubclass(type(array) , numpy.ndarray): + pass + else: + raise TypeError('array must be a CIL type DataContainer or numpy.ndarray got {}'.format(type(array))) + + if array.shape != geometry.shape: + raise ValueError('Shape mismatch {} {}'.format(array.shape, geometry.shape)) + + if array.ndim not in [2,3,4]: + raise ValueError('Number of dimensions are not 2 or 3 or 4 : {0}'.format(array.ndim)) + + super(ImageData, self).__init__(array, deep_copy, geometry=geometry, **kwargs) + + + def get_slice(self,channel=None, vertical=None, horizontal_x=None, horizontal_y=None, force=False): + ''' + Returns a new ImageData of a single slice of in the requested direction. + ''' + try: + geometry_new = self.geometry.get_slice(channel=channel, vertical=vertical, horizontal_x=horizontal_x, horizontal_y=horizontal_y) + except ValueError: + if force: + geometry_new = None + else: + raise ValueError ("Unable to return slice of requested ImageData. Use 'force=True' to return DataContainer instead.") + + #if vertical = 'centre' slice convert to index and subset, this will interpolate 2 rows to get the center slice value + if vertical == 'centre': + dim = self.geometry.dimension_labels.index('vertical') + centre_slice_pos = (self.geometry.shape[dim]-1) / 2. + ind0 = int(numpy.floor(centre_slice_pos)) + + w2 = centre_slice_pos - ind0 + out = DataContainer.get_slice(self, channel=channel, vertical=ind0, horizontal_x=horizontal_x, horizontal_y=horizontal_y) + + if w2 > 0: + out2 = DataContainer.get_slice(self, channel=channel, vertical=ind0 + 1, horizontal_x=horizontal_x, horizontal_y=horizontal_y) + out = out * (1 - w2) + out2 * w2 + else: + out = DataContainer.get_slice(self, channel=channel, vertical=vertical, horizontal_x=horizontal_x, horizontal_y=horizontal_y) + + if len(out.shape) == 1 or geometry_new is None: + return out + else: + return ImageData(out.array, deep_copy=False, geometry=geometry_new, suppress_warning=True) + + + def apply_circular_mask(self, radius=0.99, in_place=True): + """ + + Apply a circular mask to the horizontal_x and horizontal_y slices. Values outside this mask will be set to zero. + + This will most commonly be used to mask edge artefacts from standard CT reconstructions with FBP. + + Parameters + ---------- + radius : float, default 0.99 + radius of mask by percentage of size of horizontal_x or horizontal_y, whichever is greater + + in_place : boolean, default True + If `True` masks the current data, if `False` returns a new `ImageData` object. + + + Returns + ------- + ImageData + If `in_place = False` returns a new ImageData object with the masked data + + """ + ig = self.geometry + + # grid + y_range = (ig.voxel_num_y-1)/2 + x_range = (ig.voxel_num_x-1)/2 + + Y, X = numpy.ogrid[-y_range:y_range+1,-x_range:x_range+1] + + # use centre from geometry in units distance to account for aspect ratio of pixels + dist_from_center = numpy.sqrt((X*ig.voxel_size_x+ ig.center_x)**2 + (Y*ig.voxel_size_y+ig.center_y)**2) + + size_x = ig.voxel_num_x * ig.voxel_size_x + size_y = ig.voxel_num_y * ig.voxel_size_y + + if size_x > size_y: + radius_applied =radius * size_x/2 + else: + radius_applied =radius * size_y/2 + + # approximate the voxel as a circle and get the radius + # ie voxel area = 1, circle of area=1 has r = 0.56 + r=((ig.voxel_size_x * ig.voxel_size_y )/numpy.pi)**(1/2) + + # we have the voxel centre distance to mask. voxels with distance greater than |r| are fully inside or outside. + # values on the border region between -r and r are preserved + mask =(radius_applied-dist_from_center).clip(-r,r) + + # rescale to -pi/2->+pi/2 + mask *= (0.5*numpy.pi)/r + + # the sin of the linear distance gives us an approximation of area of the circle to include in the mask + numpy.sin(mask, out = mask) + + # rescale the data 0 - 1 + mask = 0.5 + mask * 0.5 + + # reorder dataset so 'horizontal_y' and 'horizontal_x' are the final dimensions + labels_orig = self.dimension_labels + labels = list(labels_orig) + + labels.remove('horizontal_y') + labels.remove('horizontal_x') + labels.append('horizontal_y') + labels.append('horizontal_x') + + + if in_place == True: + self.reorder(labels) + numpy.multiply(self.array, mask, out=self.array) + self.reorder(labels_orig) + + else: + image_data_out = self.copy() + image_data_out.reorder(labels) + numpy.multiply(image_data_out.array, mask, out=image_data_out.array) + image_data_out.reorder(labels_orig) + + return image_data_out + + +class VectorData(DataContainer): + '''DataContainer to contain 1D array''' + + @property + def geometry(self): + return self._geometry + + @geometry.setter + def geometry(self, val): + self._geometry = val + + @property + def dimension_labels(self): + if hasattr(self,'geometry'): + return self.geometry.dimension_labels + else: + return self._dimension_labels + + @dimension_labels.setter + def dimension_labels(self, val): + if hasattr(self,'geometry'): + self.geometry.dimension_labels = val + + self._dimension_labels = val + + def __init__(self, array=None, **kwargs): + self.geometry = kwargs.get('geometry', None) + + 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], **kwargs) + self.length = self.geometry.length + else: + self.length = self.geometry.length + + if array is None: + out = numpy.zeros((self.length,), dtype=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 + # need to pass the geometry, othewise None + super(VectorData, self).__init__(out, deep_copy, self.geometry.dimension_labels, geometry = self.geometry) + + +class VectorGeometry(object): + '''Geometry describing VectorData to contain 1D array''' + RANDOM = 'random' + RANDOM_INT = 'random_int' + + @property + def dtype(self): + return self._dtype + + @dtype.setter + def dtype(self, val): + self._dtype = val + + def __init__(self, + length, **kwargs): + + self.length = int(length) + self.shape = (length, ) + self.dtype = kwargs.get('dtype', numpy.float32) + self.dimension_labels = kwargs.get('dimension_labels', None) + + def clone(self): + '''returns a copy of VectorGeometry''' + return copy.deepcopy(self) + + def copy(self): + '''alias of clone''' + return self.clone() + + def __eq__(self, other): + + if not isinstance(other, self.__class__): + return False + + if self.length == other.length \ + and self.shape == other.shape \ + and self.dimension_labels == other.dimension_labels: + return True + return False + + def __str__ (self): + repres = "" + repres += "Length: {0}\n".format(self.length) + repres += "Shape: {0}\n".format(self.shape) + repres += "Dimension_labels: {0}\n".format(self.dimension_labels) + + return repres + + def allocate(self, value=0, **kwargs): + '''allocates an VectorData according to the size expressed in the instance + + :param value: accepts numbers to allocate an uniform array, or a string as 'random' or 'random_int' to create a random array or None. + :type value: number or string, default None allocates empty memory block + :param dtype: numerical type to allocate + :type dtype: numpy type, default numpy.float32 + :param seed: seed for the random number generator + :type seed: int, default None + :param max_value: max value of the random int array + :type max_value: int, default 100''' + + dtype = kwargs.get('dtype', self.dtype) + # self.dtype = kwargs.get('dtype', numpy.float32) + out = VectorData(geometry=self.copy(), dtype=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) + r = numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) + out.fill(numpy.asarray(r, dtype=self.dtype)) + elif value is None: + pass + else: + raise ValueError('Value {} unknown'.format(value)) + return out diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 91c5142aa8..1f380e6aa0 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -23,11 +23,10 @@ import warnings from functools import reduce from .cilacc import cilacc -from .framework import DataContainer -from .framework import ImageData, AcquisitionData -from .framework import ImageGeometry, AcquisitionGeometry -from .framework import VectorData, VectorGeometry -from .framework import find_key, message +from .framework import AcquisitionData +from .framework import AcquisitionGeometry +from .framework import find_key +from .DataContainer import message, ImageGeometry, DataContainer, ImageData, VectorData, VectorGeometry from .framework import DataProcessor, Processor from .framework import AX, PixelByPixelDataProcessor, CastDataContainer from .BlockDataContainer import BlockDataContainer diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/framework.py index cac72361c7..9cbde76270 100644 --- a/Wrappers/Python/cil/framework/framework.py +++ b/Wrappers/Python/cil/framework/framework.py @@ -19,18 +19,14 @@ import copy import numpy -import warnings -from functools import reduce from numbers import Number -import ctypes import math import weakref import logging -from . import cilacc +from .DataContainer import ImageGeometry, DataContainer from .base import BaseAcquisitionGeometry -from .label import image_labels, acquisition_labels, data_order, get_order_for_engine -from cil.utilities.multiprocessing import NUM_THREADS +from .label import acquisition_labels, data_order from .BlockGeometry import BlockGeometry class Partitioner(object): @@ -220,280 +216,7 @@ 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] -def message(cls, msg, *args): - msg = "{0}: " + msg - for i in range(len(args)): - msg += " {%d}" %(i+1) - args = list(args) - args.insert(0, cls.__name__ ) - - return msg.format(*args ) - -class ImageGeometry(object): - - @property - def shape(self): - - shape_dict = {image_labels["CHANNEL"]: self.channels, - image_labels["VERTICAL"]: self.voxel_num_z, - image_labels["HORIZONTAL_Y"]: self.voxel_num_y, - image_labels["HORIZONTAL_X"]: self.voxel_num_x} - - shape = [] - for label in self.dimension_labels: - shape.append(shape_dict[label]) - - return tuple(shape) - - @shape.setter - def shape(self, val): - print("Deprecated - shape will be set automatically") - - @property - def spacing(self): - - spacing_dict = {image_labels["CHANNEL"]: self.channel_spacing, - image_labels["VERTICAL"]: self.voxel_size_z, - image_labels["HORIZONTAL_Y"]: self.voxel_size_y, - image_labels["HORIZONTAL_X"]: self.voxel_size_x} - - spacing = [] - for label in self.dimension_labels: - spacing.append(spacing_dict[label]) - - return tuple(spacing) - - @property - def length(self): - return len(self.dimension_labels) - - @property - def ndim(self): - return len(self.dimension_labels) - - @property - def dimension_labels(self): - - labels_default = data_order["CIL_IG_LABELS"] - - shape_default = [ self.channels, - self.voxel_num_z, - self.voxel_num_y, - self.voxel_num_x] - - try: - labels = list(self._dimension_labels) - except AttributeError: - labels = labels_default.copy() - - for i, x in enumerate(shape_default): - if x == 0 or x==1: - try: - labels.remove(labels_default[i]) - except ValueError: - pass #if not in custom list carry on - return tuple(labels) - - @dimension_labels.setter - def dimension_labels(self, val): - self.set_labels(val) - - def set_labels(self, labels): - labels_default = data_order["CIL_IG_LABELS"] - - #check input and store. This value is not used directly - if labels is not None: - for x in labels: - if x not in labels_default: - raise ValueError('Requested axis are not possible. Accepted label names {},\ngot {}'\ - .format(labels_default,labels)) - - self._dimension_labels = tuple(labels) - - def __eq__(self, other): - - if not isinstance(other, self.__class__): - return False - - if self.voxel_num_x == other.voxel_num_x \ - and self.voxel_num_y == other.voxel_num_y \ - and self.voxel_num_z == other.voxel_num_z \ - and self.voxel_size_x == other.voxel_size_x \ - and self.voxel_size_y == other.voxel_size_y \ - and self.voxel_size_z == other.voxel_size_z \ - and self.center_x == other.center_x \ - and self.center_y == other.center_y \ - and self.center_z == other.center_z \ - and self.channels == other.channels \ - and self.channel_spacing == other.channel_spacing \ - and self.dimension_labels == other.dimension_labels: - - return True - - return False - - @property - def dtype(self): - return self._dtype - - @dtype.setter - def dtype(self, val): - self._dtype = val - - def __init__(self, - voxel_num_x=0, - voxel_num_y=0, - voxel_num_z=0, - voxel_size_x=1, - voxel_size_y=1, - voxel_size_z=1, - center_x=0, - center_y=0, - center_z=0, - channels=1, - **kwargs): - - self.voxel_num_x = int(voxel_num_x) - self.voxel_num_y = int(voxel_num_y) - self.voxel_num_z = int(voxel_num_z) - self.voxel_size_x = float(voxel_size_x) - self.voxel_size_y = float(voxel_size_y) - self.voxel_size_z = float(voxel_size_z) - self.center_x = center_x - self.center_y = center_y - self.center_z = center_z - self.channels = channels - self.channel_labels = None - self.channel_spacing = 1.0 - self.dimension_labels = kwargs.get('dimension_labels', None) - self.dtype = kwargs.get('dtype', numpy.float32) - - - def get_slice(self,channel=None, vertical=None, horizontal_x=None, horizontal_y=None): - ''' - Returns a new ImageGeometry of a single slice of in the requested direction. - ''' - geometry_new = self.copy() - if channel is not None: - geometry_new.channels = 1 - - try: - geometry_new.channel_labels = [self.channel_labels[channel]] - except: - geometry_new.channel_labels = None - - if vertical is not None: - geometry_new.voxel_num_z = 0 - - if horizontal_y is not None: - geometry_new.voxel_num_y = 0 - - if horizontal_x is not None: - geometry_new.voxel_num_x = 0 - - return geometry_new - - def get_order_by_label(self, dimension_labels, default_dimension_labels): - order = [] - for i, el in enumerate(default_dimension_labels): - for j, ek in enumerate(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 - - def get_max_x(self): - return self.center_x + 0.5*self.voxel_num_x*self.voxel_size_x - - def get_min_y(self): - return self.center_y - 0.5*self.voxel_num_y*self.voxel_size_y - - def get_max_y(self): - return self.center_y + 0.5*self.voxel_num_y*self.voxel_size_y - - def get_min_z(self): - if not self.voxel_num_z == 0: - return self.center_z - 0.5*self.voxel_num_z*self.voxel_size_z - else: - return 0 - - def get_max_z(self): - if not self.voxel_num_z == 0: - return self.center_z + 0.5*self.voxel_num_z*self.voxel_size_z - else: - return 0 - - def clone(self): - '''returns a copy of the ImageGeometry''' - return copy.deepcopy(self) - - def copy(self): - '''alias of clone''' - return self.clone() - - def __str__ (self): - repres = "" - repres += "Number of channels: {0}\n".format(self.channels) - repres += "channel_spacing: {0}\n".format(self.channel_spacing) - - if self.voxel_num_z > 0: - repres += "voxel_num : x{0},y{1},z{2}\n".format(self.voxel_num_x, self.voxel_num_y, self.voxel_num_z) - 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) - else: - repres += "voxel_num : x{0},y{1}\n".format(self.voxel_num_x, self.voxel_num_y) - repres += "voxel_size : x{0},y{1}\n".format(self.voxel_size_x, self.voxel_size_y) - repres += "center : x{0},y{1}\n".format(self.center_x, self.center_y) - - return repres - def allocate(self, value=0, **kwargs): - '''allocates an ImageData according to the size expressed in the instance - - :param value: accepts numbers to allocate an uniform array, or a string as 'random' or 'random_int' to create a random array or None. - :type value: number or string, default None allocates empty memory block, default 0 - :param dtype: numerical type to allocate - :type dtype: numpy type, default numpy.float32 - ''' - - dtype = kwargs.get('dtype', self.dtype) - - if kwargs.get('dimension_labels', None) is not None: - raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") - out = ImageData(geometry=self.copy(), - dtype=dtype, - suppress_warning=True) - - if isinstance(value, Number): - # it's created empty, so we make it 0 - out.array.fill(value) - else: - if value == image_labels["RANDOM"]: - seed = kwargs.get('seed', None) - if seed is not None: - numpy.random.seed(seed) - if numpy.iscomplexobj(out.array): - r = numpy.random.random_sample(self.shape) + 1j * numpy.random.random_sample(self.shape) - out.fill(r) - else: - out.fill(numpy.random.random_sample(self.shape)) - elif value == image_labels["RANDOM_INT"]: - seed = kwargs.get('seed', None) - if seed is not None: - numpy.random.seed(seed) - max_value = kwargs.get('max_value', 100) - r = numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) - out.fill(numpy.asarray(r, dtype=self.dtype)) - elif value is None: - pass - else: - raise ValueError('Value {} unknown'.format(value)) - - return out - class ComponentDescription(object): r'''This class enables the creation of vectors and unit vectors used to describe the components of a tomography system ''' @@ -2634,1164 +2357,92 @@ def allocate(self, value=0, **kwargs): return out -class DataContainer(object): - '''Generic class to hold data - - Data is currently held in a numpy arrays''' + +class AcquisitionData(DataContainer, Partitioner): + '''DataContainer for holding 2D or 3D sinogram''' + __container_priority__ = 1 @property def geometry(self): - return None + return self._geometry @geometry.setter def geometry(self, val): - if val is not None: - raise TypeError("DataContainers cannot hold a geometry, use ImageData or AcquisitionData instead") + self._geometry = val @property def dimension_labels(self): + return self.geometry.dimension_labels - if self._dimension_labels is None: - default_labels = [0]*self.number_of_dimensions - for i in range(self.number_of_dimensions): - default_labels[i] = 'dimension_{0:02}'.format(i) - return tuple(default_labels) - else: - return self._dimension_labels - @dimension_labels.setter def dimension_labels(self, val): - if val is None: - self._dimension_labels = None - elif len(list(val))==self.number_of_dimensions: - self._dimension_labels = tuple(val) - else: - raise ValueError("dimension_labels expected a list containing {0} strings got {1}".format(self.number_of_dimensions, val)) - - @property - def shape(self): - '''Returns the shape of the DataContainer''' - return self.array.shape - - @property - def ndim(self): - '''Returns the ndim of the DataContainer''' - return self.array.ndim - - @shape.setter - def shape(self, val): - print("Deprecated - shape will be set automatically") - - @property - def number_of_dimensions(self): - '''Returns the shape of the of the DataContainer''' - return len(self.array.shape) - - @property - def dtype(self): - '''Returns the dtype of the data array.''' - return self.array.dtype - - @property - def size(self): - '''Returns the number of elements of the DataContainer''' - return self.array.size + if val is not None: + raise ValueError("Unable to set the dimension_labels directly. Use geometry.set_labels() instead") - __container_priority__ = 1 - def __init__ (self, array, deep_copy=True, dimension_labels=None, - **kwargs): - '''Holds the data''' - - if type(array) == numpy.ndarray: - if deep_copy: - self.array = array.copy() - else: - self.array = array - else: - raise TypeError('Array must be NumpyArray, passed {0}'\ - .format(type(array))) + def __init__(self, + array = None, + deep_copy=True, + geometry = None, + **kwargs): - #Don't set for derived classes - if type(self) is DataContainer: - self.dimension_labels = dimension_labels + dtype = kwargs.get('dtype', numpy.float32) - # finally copy the geometry, and force dtype of the geometry of the data = the dype of the data - if 'geometry' in kwargs.keys(): - self.geometry = kwargs['geometry'] - try: - self.geometry.dtype = self.dtype - except: - pass + if geometry is None: + raise AttributeError("AcquisitionData requires a geometry") - def get_dimension_size(self, dimension_label): + labels = kwargs.get('dimension_labels', None) + if labels is not None and labels != geometry.dimension_labels: + raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") - if dimension_label in self.dimension_labels: - i = self.dimension_labels.index(dimension_label) - return self.shape[i] + if array is None: + array = numpy.empty(geometry.shape, dtype=dtype) + elif issubclass(type(array) , DataContainer): + array = array.as_array() + elif issubclass(type(array) , numpy.ndarray): + pass else: - raise ValueError('Unknown dimension {0}. Should be one of {1}'.format(dimension_label, - self.dimension_labels)) + raise TypeError('array must be a CIL type DataContainer or numpy.ndarray got {}'.format(type(array))) + + if array.shape != geometry.shape: + raise ValueError('Shape mismatch got {} expected {}'.format(array.shape, geometry.shape)) - def get_dimension_axis(self, dimension_label): - """ - Returns the axis index of the DataContainer array if the specified dimension_label(s) match - any dimension_labels of the DataContainer or their indices - - Parameters - ---------- - dimension_label: string or int or tuple of strings or ints - Specify dimension_label(s) or index of the DataContainer from which to check and return the axis index - - Returns - ------- - int or tuple of ints - The axis index of the DataContainer matching the specified dimension_label - """ - if isinstance(dimension_label,(tuple,list)): - return tuple(self.get_dimension_axis(x) for x in dimension_label) - - if dimension_label in self.dimension_labels: - return self.dimension_labels.index(dimension_label) - elif isinstance(dimension_label, int) and dimension_label >= 0 and dimension_label < self.ndim: - return dimension_label - else: - raise ValueError('Unknown dimension {0}. Should be one of {1}, or an integer in range {2} - {3}'.format(dimension_label, - self.dimension_labels, 0, self.ndim)) - - - def as_array(self): - '''Returns the pointer to the array. - ''' - return self.array - + super(AcquisitionData, self).__init__(array, deep_copy, geometry=geometry,**kwargs) + - def get_slice(self, **kw): + def get_slice(self,channel=None, angle=None, vertical=None, horizontal=None, force=False): ''' - Returns a new DataContainer containing a single slice in the requested direction. \ - Pass keyword arguments =index + Returns a new dataset of a single slice of in the requested direction. \ ''' - # Force is not relevant for a DataContainer: - kw.pop('force', None) + try: + geometry_new = self.geometry.get_slice(channel=channel, angle=angle, vertical=vertical, horizontal=horizontal) + except ValueError: + if force: + geometry_new = None + else: + raise ValueError ("Unable to return slice of requested AcquisitionData. Use 'force=True' to return DataContainer instead.") - new_array = None + #get new data + #if vertical = 'centre' slice convert to index and subset, this will interpolate 2 rows to get the center slice value + if vertical == 'centre': + dim = self.geometry.dimension_labels.index('vertical') + + centre_slice_pos = (self.geometry.shape[dim]-1) / 2. + ind0 = int(numpy.floor(centre_slice_pos)) + w2 = centre_slice_pos - ind0 + out = DataContainer.get_slice(self, channel=channel, angle=angle, vertical=ind0, horizontal=horizontal) + + if w2 > 0: + out2 = DataContainer.get_slice(self, channel=channel, angle=angle, vertical=ind0 + 1, horizontal=horizontal) + out = out * (1 - w2) + out2 * w2 + else: + out = DataContainer.get_slice(self, channel=channel, angle=angle, vertical=vertical, horizontal=horizontal) - #get ordered list of current dimensions - dimension_labels_list = list(self.dimension_labels) + if len(out.shape) == 1 or geometry_new is None: + return out + else: + return AcquisitionData(out.array, deep_copy=False, geometry=geometry_new, suppress_warning=True) - #remove axes from array and labels - for key, value in kw.items(): - if value is not None: - axis = dimension_labels_list.index(key) - dimension_labels_list.remove(key) - if new_array is None: - new_array = self.as_array().take(indices=value, axis=axis) - else: - new_array = new_array.take(indices=value, axis=axis) - - if new_array.ndim > 1: - return DataContainer(new_array, False, dimension_labels_list, suppress_warning=True) - else: - return VectorData(new_array, dimension_labels=dimension_labels_list) - - def reorder(self, order=None): - ''' - reorders the data in memory as requested. - - :param order: ordered list of labels from self.dimension_labels, or order for engine 'astra' or 'tigre' - :type order: list, sting - ''' - - if order in data_order["ENGINES"]: - order = get_order_for_engine(order, self.geometry) - - try: - if len(order) != len(self.shape): - raise ValueError('The axes list for resorting must have {0} dimensions. Got {1}'.format(len(self.shape), len(order))) - except TypeError as ae: - raise ValueError('The order must be an iterable with __len__ implemented, like a list or a tuple. Got {}'.format(type(order))) - - correct = True - for el in order: - correct = correct and el in self.dimension_labels - if not correct: - raise ValueError('The axes list for resorting must contain the dimension_labels {0} got {1}'.format(self.dimension_labels, order)) - - new_order = [0]*len(self.shape) - dimension_labels_new = [0]*len(self.shape) - - for i, axis in enumerate(order): - new_order[i] = self.dimension_labels.index(axis) - dimension_labels_new[i] = axis - - self.array = numpy.ascontiguousarray(numpy.transpose(self.array, new_order)) - - if self.geometry is None: - self.dimension_labels = dimension_labels_new - else: - self.geometry.set_labels(dimension_labels_new) - - def fill(self, array, **dimension): - '''fills the internal data array with the DataContainer, numpy array or number provided - - :param array: number, numpy array or DataContainer to copy into the DataContainer - :type array: DataContainer or subclasses, numpy array or number - :param dimension: dictionary, optional - - if the passed numpy array points to the same array that is contained in the DataContainer, - it just returns - - In case a DataContainer or subclass is passed, there will be a check of the geometry, - if present, and the array will be resorted if the data is not in the appropriate order. - - User may pass a named parameter to specify in which axis the fill should happen: - - dc.fill(some_data, vertical=1, horizontal_x=32) - will copy the data in some_data into the data container. - ''' - if id(array) == id(self.array): - return - if dimension == {}: - if isinstance(array, numpy.ndarray): - if array.shape != self.shape: - raise ValueError('Cannot fill with the provided array.' + \ - 'Expecting shape {0} got {1}'.format( - self.shape,array.shape)) - numpy.copyto(self.array, array) - elif isinstance(array, Number): - self.array.fill(array) - elif issubclass(array.__class__ , DataContainer): - - try: - if self.dimension_labels != array.dimension_labels: - raise ValueError('Input array is not in the same order as destination array. Use "array.reorder()"') - except AttributeError: - pass - - if self.array.shape == array.shape: - numpy.copyto(self.array, array.array) - else: - raise ValueError('Cannot fill with the provided array.' + \ - 'Expecting shape {0} got {1}'.format( - self.shape,array.shape)) - else: - raise TypeError('Can fill only with number, numpy array or DataContainer and subclasses. Got {}'.format(type(array))) - else: - - axis = [':']* self.number_of_dimensions - dimension_labels = list(self.dimension_labels) - for k,v in dimension.items(): - i = dimension_labels.index(k) - axis[i] = v - - command = 'self.array[' - i = 0 - for el in axis: - if i > 0: - command += ',' - command += str(el) - i+=1 - - if isinstance(array, numpy.ndarray): - command = command + "] = array[:]" - elif issubclass(array.__class__, DataContainer): - command = command + "] = array.as_array()[:]" - elif isinstance (array, Number): - command = command + "] = array" - else: - raise TypeError('Can fill only with number, numpy array or DataContainer and subclasses. Got {}'.format(type(array))) - exec(command) - - - def check_dimensions(self, other): - return self.shape == other.shape - - ## algebra - - def __add__(self, other): - return self.add(other) - def __mul__(self, other): - return self.multiply(other) - def __sub__(self, other): - return self.subtract(other) - def __div__(self, other): - return self.divide(other) - def __truediv__(self, other): - return self.divide(other) - def __pow__(self, other): - return self.power(other) - - - # reverse operand - def __radd__(self, other): - return self + other - # __radd__ - - def __rsub__(self, other): - return (-1 * self) + other - # __rsub__ - - def __rmul__(self, other): - return self * other - # __rmul__ - - def __rdiv__(self, other): - tmp = self.power(-1) - tmp *= other - return tmp - # __rdiv__ - def __rtruediv__(self, other): - return self.__rdiv__(other) - - def __rpow__(self, other): - if isinstance(other, Number) : - fother = numpy.ones(numpy.shape(self.array)) * other - return type(self)(fother ** self.array , - dimension_labels=self.dimension_labels, - geometry=self.geometry) - # __rpow__ - - # in-place arithmetic operators: - # (+=, -=, *=, /= , //=, - # must return self - - def __iadd__(self, other): - kw = {'out':self} - return self.add(other, **kw) - - def __imul__(self, other): - kw = {'out':self} - return self.multiply(other, **kw) - - def __isub__(self, other): - kw = {'out':self} - return self.subtract(other, **kw) - - def __idiv__(self, other): - kw = {'out':self} - return self.divide(other, **kw) - - def __itruediv__(self, other): - kw = {'out':self} - return self.divide(other, **kw) - - def __neg__(self): - '''negation operator''' - return -1 * self - - def __str__ (self, representation=False): - repres = "" - repres += "Number of dimensions: {0}\n".format(self.number_of_dimensions) - repres += "Shape: {0}\n".format(self.shape) - repres += "Axis labels: {0}\n".format(self.dimension_labels) - if representation: - repres += "Representation: \n{0}\n".format(self.array) - return repres - - def get_data_axes_order(self,new_order=None): - '''returns the axes label of self as a list - - If new_order is None returns the labels of the axes as a sorted-by-key list. - If new_order is a list of length number_of_dimensions, returns a list - with the indices of the axes in new_order with respect to those in - self.dimension_labels: i.e. - >>> self.dimension_labels = {0:'horizontal',1:'vertical'} - >>> new_order = ['vertical','horizontal'] - returns [1,0] - ''' - if new_order is None: - return self.dimension_labels - else: - if len(new_order) == self.number_of_dimensions: - - axes_order = [0]*len(self.shape) - for i, axis in enumerate(new_order): - axes_order[i] = self.dimension_labels.index(axis) - return axes_order - else: - raise ValueError('Expecting {0} axes, got {2}'\ - .format(len(self.shape),len(new_order))) - - def clone(self): - '''returns a copy of DataContainer''' - return copy.deepcopy(self) - - def copy(self): - '''alias of clone''' - return self.clone() - - ## binary operations - - def pixel_wise_binary(self, pwop, x2, *args, **kwargs): - out = kwargs.get('out', None) - - if out is None: - if isinstance(x2, Number): - out = pwop(self.as_array() , x2 , *args, **kwargs ) - elif issubclass(x2.__class__ , DataContainer): - out = pwop(self.as_array() , x2.as_array() , *args, **kwargs ) - elif isinstance(x2, numpy.ndarray): - out = pwop(self.as_array() , x2 , *args, **kwargs ) - else: - raise TypeError('Expected x2 type as number or DataContainer, got {}'.format(type(x2))) - geom = self.geometry - if geom is not None: - geom = self.geometry.copy() - return type(self)(out, - deep_copy=False, - dimension_labels=self.dimension_labels, - geometry= None if self.geometry is None else self.geometry.copy(), - suppress_warning=True) - - - elif issubclass(type(out), DataContainer) and issubclass(type(x2), DataContainer): - if self.check_dimensions(out) and self.check_dimensions(x2): - 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 {} x2 {} expected {}".format( out.shape,x2.shape ,self.shape))) - elif issubclass(type(out), DataContainer) and \ - isinstance(x2, (Number, numpy.ndarray)): - if self.check_dimensions(out): - if isinstance(x2, numpy.ndarray) and\ - not (x2.shape == self.shape and x2.dtype == self.dtype): - raise ValueError(message(type(self), - "Wrong size for data memory: out {} x2 {} expected {}"\ - .format( out.shape,x2.shape ,self.shape))) - 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: - kwargs['out'] = out - pwop(self.as_array(), x2, *args, **kwargs) - #return type(self)(out, - # deep_copy=False, - # dimension_labels=self.dimension_labels, - # geometry=self.geometry) - else: - raise ValueError (message(type(self), "incompatible class:" , pwop.__name__, type(out))) - - 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 sapyb(self, a, y, b, out=None, num_threads=NUM_THREADS): - '''performs a*self + b * y. Can be done in-place - - Parameters - ---------- - a : multiplier for self, can be a number or a numpy array or a DataContainer - y : DataContainer - b : multiplier for y, can be a number or a numpy array or a DataContainer - out : return DataContainer, if None a new DataContainer is returned, default None. - out can be self or y. - num_threads : number of threads to use during the calculation, using the CIL C library - It will try to use the CIL C library and default to numpy operations, in case the C library does not handle the types. - - - Example - ------- - - >>> a = 2 - >>> b = 3 - >>> ig = ImageGeometry(10,11) - >>> x = ig.allocate(1) - >>> y = ig.allocate(2) - >>> out = x.sapyb(a,y,b) - ''' - ret_out = False - - if out is None: - out = self * 0. - ret_out = True - - if out.dtype in [ numpy.float32, numpy.float64 ]: - # handle with C-lib _axpby - try: - self._axpby(a, b, y, out, out.dtype, num_threads) - if ret_out: - return out - return - except RuntimeError as rte: - warnings.warn("sapyb defaulting to Python due to: {}".format(rte)) - except TypeError as te: - warnings.warn("sapyb defaulting to Python due to: {}".format(te)) - finally: - pass - - - # cannot be handled by _axpby - ax = self * a - y.multiply(b, out=out) - out.add(ax, out=out) - - if ret_out: - return out - - def _axpby(self, a, b, y, out, dtype=numpy.float32, num_threads=NUM_THREADS): - '''performs axpby with cilacc C library, can be done in-place. - - Does the operation .. math:: a*x+b*y and stores the result in out, where x is self - - :param a: scalar - :type a: float - :param b: scalar - :type b: float - :param y: DataContainer - :param out: DataContainer instance to store the result - :param dtype: data type of the DataContainers - :type dtype: numpy type, optional, default numpy.float32 - :param num_threads: number of threads to run on - :type num_threads: int, optional, default 1/2 CPU of the system - ''' - - c_float_p = ctypes.POINTER(ctypes.c_float) - c_double_p = ctypes.POINTER(ctypes.c_double) - - #convert a and b to numpy arrays and get the reference to the data (length = 1 or ndx.size) - try: - nda = a.as_array() - except: - nda = numpy.asarray(a) - - try: - ndb = b.as_array() - except: - ndb = numpy.asarray(b) - - a_vec = 0 - if nda.size > 1: - a_vec = 1 - - b_vec = 0 - if ndb.size > 1: - b_vec = 1 - - # get the reference to the data - ndx = self.as_array() - ndy = y.as_array() - ndout = out.as_array() - - if ndout.dtype != dtype: - raise Warning("out array of type {0} does not match requested dtype {1}. Using {0}".format(ndout.dtype, dtype)) - dtype = ndout.dtype - if ndx.dtype != dtype: - ndx = ndx.astype(dtype, casting='safe') - if ndy.dtype != dtype: - ndy = ndy.astype(dtype, casting='safe') - if nda.dtype != dtype: - nda = nda.astype(dtype, casting='same_kind') - if ndb.dtype != dtype: - ndb = ndb.astype(dtype, casting='same_kind') - - if dtype == numpy.float32: - x_p = ndx.ctypes.data_as(c_float_p) - y_p = ndy.ctypes.data_as(c_float_p) - out_p = ndout.ctypes.data_as(c_float_p) - a_p = nda.ctypes.data_as(c_float_p) - b_p = ndb.ctypes.data_as(c_float_p) - f = cilacc.saxpby - - elif dtype == numpy.float64: - x_p = ndx.ctypes.data_as(c_double_p) - y_p = ndy.ctypes.data_as(c_double_p) - out_p = ndout.ctypes.data_as(c_double_p) - a_p = nda.ctypes.data_as(c_double_p) - b_p = ndb.ctypes.data_as(c_double_p) - f = cilacc.daxpby - - else: - raise TypeError('Unsupported type {}. Expecting numpy.float32 or numpy.float64'.format(dtype)) - - #out = numpy.empty_like(a) - - - # int psaxpby(float * x, float * y, float * out, float a, float b, long size) - cilacc.saxpby.argtypes = [ctypes.POINTER(ctypes.c_float), # pointer to the first array - ctypes.POINTER(ctypes.c_float), # pointer to the second array - ctypes.POINTER(ctypes.c_float), # pointer to the third array - ctypes.POINTER(ctypes.c_float), # pointer to A - ctypes.c_int, # type of type of A selector (int) - ctypes.POINTER(ctypes.c_float), # pointer to B - ctypes.c_int, # type of type of B selector (int) - ctypes.c_longlong, # type of size of first array - ctypes.c_int] # number of threads - cilacc.daxpby.argtypes = [ctypes.POINTER(ctypes.c_double), # pointer to the first array - ctypes.POINTER(ctypes.c_double), # pointer to the second array - ctypes.POINTER(ctypes.c_double), # pointer to the third array - ctypes.POINTER(ctypes.c_double), # type of A (c_double) - ctypes.c_int, # type of type of A selector (int) - ctypes.POINTER(ctypes.c_double), # type of B (c_double) - ctypes.c_int, # type of type of B selector (int) - ctypes.c_longlong, # type of size of first array - ctypes.c_int] # number of threads - - if f(x_p, y_p, out_p, a_p, a_vec, b_p, b_vec, ndx.size, num_threads) != 0: - raise RuntimeError('axpby execution failed') - - - ## unary operations - 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, - deep_copy=False, - dimension_labels=self.dimension_labels, - geometry=self.geometry, - suppress_warning=True) - elif issubclass(type(out), DataContainer): - if self.check_dimensions(out): - 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: - kwargs['out'] = out - pwop(self.as_array(), *args, **kwargs) - else: - raise ValueError (message(type(self), "incompatible class:" , pwop.__name__, type(out))) - - def abs(self, *args, **kwargs): - return self.pixel_wise_unary(numpy.abs, *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 exp(self, *args, **kwargs): - '''Applies exp pixel-wise to the DataContainer''' - return self.pixel_wise_unary(numpy.exp, *args, **kwargs) - - def log(self, *args, **kwargs): - '''Applies log pixel-wise to the DataContainer''' - return self.pixel_wise_unary(numpy.log, *args, **kwargs) - - ## reductions - def squared_norm(self, **kwargs): - '''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 self.dot(self) - #return self.dot(self) - def norm(self, **kwargs): - '''return the euclidean norm of the DataContainer viewed as a vector''' - return numpy.sqrt(self.squared_norm(**kwargs)) - - def dot(self, other, *args, **kwargs): - '''returns the inner product of 2 DataContainers viewed as vectors. Suitable for real and complex data. - For complex data, the dot method returns a.dot(b.conjugate()) - ''' - 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: - if method == 'numpy': - return numpy.dot(self.as_array().ravel(), other.as_array().ravel().conjugate()) - 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().conjugate()), - 0) - return sf - else: - raise ValueError('Shapes are not aligned: {} != {}'.format(self.shape, other.shape)) - - def _directional_reduction_unary(self, reduction_function, axis=None, out=None, *args, **kwargs): - """ - Returns the result of a unary function, considering the direction from an axis argument to the function - - Parameters - ---------- - reduction_function : function - The unary function to be evaluated - axis : string or tuple of strings or int or tuple of ints, optional - Specify the axis or axes to calculate 'reduction_function' along. Can be specified as - string(s) of dimension_labels or int(s) of indices - Default None calculates the function over the whole array - out: ndarray or DataContainer, optional - Provide an object in which to place the result. The object must have the correct dimensions and - (for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See - `Output type determination `_ for more details. - Default is None - - Returns - ------- - scalar or ndarray - The result of the unary function - """ - if axis is not None: - axis = self.get_dimension_axis(axis) - - if out is None: - result = reduction_function(self.as_array(), axis=axis, *args, **kwargs) - if isinstance(result, numpy.ndarray): - new_dimensions = numpy.array(self.dimension_labels) - new_dimensions = numpy.delete(new_dimensions, axis) - return DataContainer(result, dimension_labels=new_dimensions) - else: - return result - else: - if hasattr(out,'array'): - out_arr = out.array - else: - out_arr = out - - reduction_function(self.as_array(), out=out_arr, axis=axis, *args, **kwargs) - - def sum(self, axis=None, out=None, *args, **kwargs): - """ - Returns the sum of values in the DataContainer - - Parameters - ---------- - axis : string or tuple of strings or int or tuple of ints, optional - Specify the axis or axes to calculate 'sum' along. Can be specified as - string(s) of dimension_labels or int(s) of indices - Default None calculates the function over the whole array - out : ndarray or DataContainer, optional - Provide an object in which to place the result. The object must have the correct dimensions and - (for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See - `Output type determination `_ for more details. - Default is None - - Returns - ------- - scalar or DataContainer - The sum as a scalar or inside a DataContainer with reduced dimension_labels - Default is to accumulate and return data as float64 or complex128 - """ - if kwargs.get('dtype') is not None: - logging.WARNING("dtype argument is ignored, using float64 or complex128") - - if numpy.isrealobj(self.array): - kwargs['dtype'] = numpy.float64 - else: - kwargs['dtype'] = numpy.complex128 - - return self._directional_reduction_unary(numpy.sum, axis=axis, out=out, *args, **kwargs) - - def min(self, axis=None, out=None, *args, **kwargs): - """ - Returns the minimum pixel value in the DataContainer - - Parameters - ---------- - axis : string or tuple of strings or int or tuple of ints, optional - Specify the axis or axes to calculate 'min' along. Can be specified as - string(s) of dimension_labels or int(s) of indices - Default None calculates the function over the whole array - out : ndarray or DataContainer, optional - Provide an object in which to place the result. The object must have the correct dimensions and - (for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See - `Output type determination `_ for more details. - Default is None - - Returns - ------- - scalar or DataContainer - The min as a scalar or inside a DataContainer with reduced dimension_labels - """ - return self._directional_reduction_unary(numpy.min, axis=axis, out=out, *args, **kwargs) - - def max(self, axis=None, out=None, *args, **kwargs): - """ - Returns the maximum pixel value in the DataContainer - - Parameters - ---------- - axis : string or tuple of strings or int or tuple of ints, optional - Specify the axis or axes to calculate 'max' along. Can be specified as - string(s) of dimension_labels or int(s) of indices - Default None calculates the function over the whole array - out : ndarray or DataContainer, optional - Provide an object in which to place the result. The object must have the correct dimensions and - (for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See - `Output type determination `_ for more details. - Default is None - - Returns - ------- - scalar or DataContainer - The max as a scalar or inside a DataContainer with reduced dimension_labels - """ - return self._directional_reduction_unary(numpy.max, axis=axis, out=out, *args, **kwargs) - - def mean(self, axis=None, out=None, *args, **kwargs): - """ - Returns the mean pixel value of the DataContainer - - Parameters - ---------- - axis : string or tuple of strings or int or tuple of ints, optional - Specify the axis or axes to calculate 'mean' along. Can be specified as - string(s) of dimension_labels or int(s) of indices - Default None calculates the function over the whole array - out : ndarray or DataContainer, optional - Provide an object in which to place the result. The object must have the correct dimensions and - (for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See - `Output type determination `_ for more details. - Default is None - - Returns - ------- - scalar or DataContainer - The mean as a scalar or inside a DataContainer with reduced dimension_labels - Default is to accumulate and return data as float64 or complex128 - """ - - if kwargs.get('dtype', None) is not None: - logging.WARNING("dtype argument is ignored, using float64 or complex128") - - if numpy.isrealobj(self.array): - kwargs['dtype'] = numpy.float64 - else: - kwargs['dtype'] = numpy.complex128 - - return self._directional_reduction_unary(numpy.mean, axis=axis, out=out, *args, **kwargs) - - # Logic operators between DataContainers and floats - def __le__(self, other): - '''Returns boolean array of DataContainer less or equal than DataContainer/float''' - if isinstance(other, DataContainer): - return self.as_array()<=other.as_array() - return self.as_array()<=other - - def __lt__(self, other): - '''Returns boolean array of DataContainer less than DataContainer/float''' - if isinstance(other, DataContainer): - return self.as_array()=other.as_array() - return self.as_array()>=other - - def __gt__(self, other): - '''Returns boolean array of DataContainer greater than DataContainer/float''' - if isinstance(other, DataContainer): - return self.as_array()>other.as_array() - return self.as_array()>other - - def __eq__(self, other): - '''Returns boolean array of DataContainer equal to DataContainer/float''' - if isinstance(other, DataContainer): - return self.as_array()==other.as_array() - return self.as_array()==other - - def __ne__(self, other): - '''Returns boolean array of DataContainer negative to DataContainer/float''' - if isinstance(other, DataContainer): - return self.as_array()!=other.as_array() - return self.as_array()!=other - -class ImageData(DataContainer): - '''DataContainer for holding 2D or 3D DataContainer''' - __container_priority__ = 1 - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, val): - self._geometry = val - - @property - def dimension_labels(self): - return self.geometry.dimension_labels - - @dimension_labels.setter - def dimension_labels(self, val): - if val is not None: - raise ValueError("Unable to set the dimension_labels directly. Use geometry.set_labels() instead") - - def __init__(self, - array = None, - deep_copy=False, - geometry=None, - **kwargs): - - dtype = kwargs.get('dtype', numpy.float32) - - - if geometry is None: - raise AttributeError("ImageData requires a geometry") - - - labels = kwargs.get('dimension_labels', None) - if labels is not None and labels != geometry.dimension_labels: - raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") - - if array is None: - array = numpy.empty(geometry.shape, dtype=dtype) - elif issubclass(type(array) , DataContainer): - array = array.as_array() - elif issubclass(type(array) , numpy.ndarray): - pass - else: - raise TypeError('array must be a CIL type DataContainer or numpy.ndarray got {}'.format(type(array))) - - if array.shape != geometry.shape: - raise ValueError('Shape mismatch {} {}'.format(array.shape, geometry.shape)) - - if array.ndim not in [2,3,4]: - raise ValueError('Number of dimensions are not 2 or 3 or 4 : {0}'.format(array.ndim)) - - super(ImageData, self).__init__(array, deep_copy, geometry=geometry, **kwargs) - - - def get_slice(self,channel=None, vertical=None, horizontal_x=None, horizontal_y=None, force=False): - ''' - Returns a new ImageData of a single slice of in the requested direction. - ''' - try: - geometry_new = self.geometry.get_slice(channel=channel, vertical=vertical, horizontal_x=horizontal_x, horizontal_y=horizontal_y) - except ValueError: - if force: - geometry_new = None - else: - raise ValueError ("Unable to return slice of requested ImageData. Use 'force=True' to return DataContainer instead.") - - #if vertical = 'centre' slice convert to index and subset, this will interpolate 2 rows to get the center slice value - if vertical == 'centre': - dim = self.geometry.dimension_labels.index('vertical') - centre_slice_pos = (self.geometry.shape[dim]-1) / 2. - ind0 = int(numpy.floor(centre_slice_pos)) - - w2 = centre_slice_pos - ind0 - out = DataContainer.get_slice(self, channel=channel, vertical=ind0, horizontal_x=horizontal_x, horizontal_y=horizontal_y) - - if w2 > 0: - out2 = DataContainer.get_slice(self, channel=channel, vertical=ind0 + 1, horizontal_x=horizontal_x, horizontal_y=horizontal_y) - out = out * (1 - w2) + out2 * w2 - else: - out = DataContainer.get_slice(self, channel=channel, vertical=vertical, horizontal_x=horizontal_x, horizontal_y=horizontal_y) - - if len(out.shape) == 1 or geometry_new is None: - return out - else: - return ImageData(out.array, deep_copy=False, geometry=geometry_new, suppress_warning=True) - - - def apply_circular_mask(self, radius=0.99, in_place=True): - """ - - Apply a circular mask to the horizontal_x and horizontal_y slices. Values outside this mask will be set to zero. - - This will most commonly be used to mask edge artefacts from standard CT reconstructions with FBP. - - Parameters - ---------- - radius : float, default 0.99 - radius of mask by percentage of size of horizontal_x or horizontal_y, whichever is greater - - in_place : boolean, default True - If `True` masks the current data, if `False` returns a new `ImageData` object. - - - Returns - ------- - ImageData - If `in_place = False` returns a new ImageData object with the masked data - - """ - ig = self.geometry - - # grid - y_range = (ig.voxel_num_y-1)/2 - x_range = (ig.voxel_num_x-1)/2 - - Y, X = numpy.ogrid[-y_range:y_range+1,-x_range:x_range+1] - - # use centre from geometry in units distance to account for aspect ratio of pixels - dist_from_center = numpy.sqrt((X*ig.voxel_size_x+ ig.center_x)**2 + (Y*ig.voxel_size_y+ig.center_y)**2) - - size_x = ig.voxel_num_x * ig.voxel_size_x - size_y = ig.voxel_num_y * ig.voxel_size_y - - if size_x > size_y: - radius_applied =radius * size_x/2 - else: - radius_applied =radius * size_y/2 - - # approximate the voxel as a circle and get the radius - # ie voxel area = 1, circle of area=1 has r = 0.56 - r=((ig.voxel_size_x * ig.voxel_size_y )/numpy.pi)**(1/2) - - # we have the voxel centre distance to mask. voxels with distance greater than |r| are fully inside or outside. - # values on the border region between -r and r are preserved - mask =(radius_applied-dist_from_center).clip(-r,r) - - # rescale to -pi/2->+pi/2 - mask *= (0.5*numpy.pi)/r - - # the sin of the linear distance gives us an approximation of area of the circle to include in the mask - numpy.sin(mask, out = mask) - - # rescale the data 0 - 1 - mask = 0.5 + mask * 0.5 - - # reorder dataset so 'horizontal_y' and 'horizontal_x' are the final dimensions - labels_orig = self.dimension_labels - labels = list(labels_orig) - - labels.remove('horizontal_y') - labels.remove('horizontal_x') - labels.append('horizontal_y') - labels.append('horizontal_x') - - - if in_place == True: - self.reorder(labels) - numpy.multiply(self.array, mask, out=self.array) - self.reorder(labels_orig) - - else: - image_data_out = self.copy() - image_data_out.reorder(labels) - numpy.multiply(image_data_out.array, mask, out=image_data_out.array) - image_data_out.reorder(labels_orig) - - return image_data_out - - -class AcquisitionData(DataContainer, Partitioner): - '''DataContainer for holding 2D or 3D sinogram''' - __container_priority__ = 1 - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, val): - self._geometry = val - - @property - def dimension_labels(self): - return self.geometry.dimension_labels - - @dimension_labels.setter - def dimension_labels(self, val): - if val is not None: - raise ValueError("Unable to set the dimension_labels directly. Use geometry.set_labels() instead") - - def __init__(self, - array = None, - deep_copy=True, - geometry = None, - **kwargs): - - dtype = kwargs.get('dtype', numpy.float32) - - if geometry is None: - raise AttributeError("AcquisitionData requires a geometry") - - labels = kwargs.get('dimension_labels', None) - if labels is not None and labels != geometry.dimension_labels: - raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") - - if array is None: - array = numpy.empty(geometry.shape, dtype=dtype) - elif issubclass(type(array) , DataContainer): - array = array.as_array() - elif issubclass(type(array) , numpy.ndarray): - pass - else: - raise TypeError('array must be a CIL type DataContainer or numpy.ndarray got {}'.format(type(array))) - - if array.shape != geometry.shape: - raise ValueError('Shape mismatch got {} expected {}'.format(array.shape, geometry.shape)) - - super(AcquisitionData, self).__init__(array, deep_copy, geometry=geometry,**kwargs) - - - def get_slice(self,channel=None, angle=None, vertical=None, horizontal=None, force=False): - ''' - Returns a new dataset of a single slice of in the requested direction. \ - ''' - try: - geometry_new = self.geometry.get_slice(channel=channel, angle=angle, vertical=vertical, horizontal=horizontal) - except ValueError: - if force: - geometry_new = None - else: - raise ValueError ("Unable to return slice of requested AcquisitionData. Use 'force=True' to return DataContainer instead.") - - #get new data - #if vertical = 'centre' slice convert to index and subset, this will interpolate 2 rows to get the center slice value - if vertical == 'centre': - dim = self.geometry.dimension_labels.index('vertical') - - centre_slice_pos = (self.geometry.shape[dim]-1) / 2. - ind0 = int(numpy.floor(centre_slice_pos)) - w2 = centre_slice_pos - ind0 - out = DataContainer.get_slice(self, channel=channel, angle=angle, vertical=ind0, horizontal=horizontal) - - if w2 > 0: - out2 = DataContainer.get_slice(self, channel=channel, angle=angle, vertical=ind0 + 1, horizontal=horizontal) - out = out * (1 - w2) + out2 * w2 - else: - out = DataContainer.get_slice(self, channel=channel, angle=angle, vertical=vertical, horizontal=horizontal) - - if len(out.shape) == 1 or geometry_new is None: - return out - else: - return AcquisitionData(out.array, deep_copy=False, geometry=geometry_new, suppress_warning=True) - -class Processor(object): +class Processor(object): '''Defines a generic DataContainer processor @@ -3847,7 +2498,7 @@ def set_input(self, dataset): else: raise ValueError('Input data not compatible') else: - raise TypeError("Input type mismatch: got {0} expecting {1}"\ + raise TypeError("Input type mismatch: got {0} expecting {1}" \ .format(type(dataset), DataContainer)) @@ -3974,8 +2625,8 @@ def process(self, out=None): dsi = self.get_input() a = self.scalar if out is None: - y = DataContainer( a * dsi.as_array() , True, - dimension_labels=dsi.dimension_labels ) + y = DataContainer(a * dsi.as_array(), True, + dimension_labels=dsi.dimension_labels) #self.setParameter(output_dataset=y) return y else: @@ -4047,149 +2698,9 @@ def process(self, out=None): eval_func = numpy.frompyfunc(pyfunc,1,1) - y = DataContainer( eval_func( dsi.as_array() ) , True, - dimension_labels=dsi.dimension_labels ) + y = DataContainer(eval_func(dsi.as_array()), True, + dimension_labels=dsi.dimension_labels) return y - - -class VectorData(DataContainer): - '''DataContainer to contain 1D array''' - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, val): - self._geometry = val - - @property - def dimension_labels(self): - if hasattr(self,'geometry'): - return self.geometry.dimension_labels - else: - return self._dimension_labels - - @dimension_labels.setter - def dimension_labels(self, val): - if hasattr(self,'geometry'): - self.geometry.dimension_labels = val - - self._dimension_labels = val - - def __init__(self, array=None, **kwargs): - self.geometry = kwargs.get('geometry', None) - - 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], **kwargs) - self.length = self.geometry.length - else: - self.length = self.geometry.length - - if array is None: - out = numpy.zeros((self.length,), dtype=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 - # need to pass the geometry, othewise None - super(VectorData, self).__init__(out, deep_copy, self.geometry.dimension_labels, geometry = self.geometry) - - -class VectorGeometry(object): - '''Geometry describing VectorData to contain 1D array''' - RANDOM = 'random' - RANDOM_INT = 'random_int' - - @property - def dtype(self): - return self._dtype - - @dtype.setter - def dtype(self, val): - self._dtype = val - - def __init__(self, - length, **kwargs): - - self.length = int(length) - self.shape = (length, ) - self.dtype = kwargs.get('dtype', numpy.float32) - self.dimension_labels = kwargs.get('dimension_labels', None) - - def clone(self): - '''returns a copy of VectorGeometry''' - return copy.deepcopy(self) - - def copy(self): - '''alias of clone''' - return self.clone() - - def __eq__(self, other): - - if not isinstance(other, self.__class__): - return False - - if self.length == other.length \ - and self.shape == other.shape \ - and self.dimension_labels == other.dimension_labels: - return True - return False - - def __str__ (self): - repres = "" - repres += "Length: {0}\n".format(self.length) - repres += "Shape: {0}\n".format(self.shape) - repres += "Dimension_labels: {0}\n".format(self.dimension_labels) - - return repres - - def allocate(self, value=0, **kwargs): - '''allocates an VectorData according to the size expressed in the instance - - :param value: accepts numbers to allocate an uniform array, or a string as 'random' or 'random_int' to create a random array or None. - :type value: number or string, default None allocates empty memory block - :param dtype: numerical type to allocate - :type dtype: numpy type, default numpy.float32 - :param seed: seed for the random number generator - :type seed: int, default None - :param max_value: max value of the random int array - :type max_value: int, default 100''' - - dtype = kwargs.get('dtype', self.dtype) - # self.dtype = kwargs.get('dtype', numpy.float32) - out = VectorData(geometry=self.copy(), dtype=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) - r = numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) - out.fill(numpy.asarray(r, dtype=self.dtype)) - elif value is None: - pass - else: - raise ValueError('Value {} unknown'.format(value)) - return out diff --git a/Wrappers/Python/cil/optimisation/operators/BlurringOperator.py b/Wrappers/Python/cil/optimisation/operators/BlurringOperator.py index 4541ecd60f..aa0a45f5e1 100644 --- a/Wrappers/Python/cil/optimisation/operators/BlurringOperator.py +++ b/Wrappers/Python/cil/optimisation/operators/BlurringOperator.py @@ -18,6 +18,8 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt import numpy as np + +from cil.framework import ImageGeometry, AcquisitionGeometry from cil.optimisation.operators import LinearOperator import cil @@ -42,8 +44,8 @@ def __init__(self, PSF, geometry): else: raise TypeError('PSF must be a number array with same number of dimensions as geometry.') - if not (isinstance(geometry,cil.framework.framework.ImageGeometry) or \ - isinstance(geometry,cil.framework.framework.AcquisitionGeometry)): + if not (isinstance(geometry, ImageGeometry) or \ + isinstance(geometry, AcquisitionGeometry)): raise TypeError('geometry must be an ImageGeometry or AcquisitionGeometry.') diff --git a/Wrappers/Python/cil/optimisation/operators/ChannelwiseOperator.py b/Wrappers/Python/cil/optimisation/operators/ChannelwiseOperator.py index c32db1f19f..63e159b6f6 100644 --- a/Wrappers/Python/cil/optimisation/operators/ChannelwiseOperator.py +++ b/Wrappers/Python/cil/optimisation/operators/ChannelwiseOperator.py @@ -17,11 +17,9 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -import numpy as np -from cil.framework import ImageData +from cil.framework import ImageGeometry, AcquisitionGeometry, BlockGeometry from cil.optimisation.operators import LinearOperator -from cil.framework import ImageGeometry, AcquisitionGeometry, BlockGeometry class ChannelwiseOperator(LinearOperator): diff --git a/Wrappers/Python/cil/optimisation/operators/GradientOperator.py b/Wrappers/Python/cil/optimisation/operators/GradientOperator.py index b311ed01a2..be3cf734aa 100644 --- a/Wrappers/Python/cil/optimisation/operators/GradientOperator.py +++ b/Wrappers/Python/cil/optimisation/operators/GradientOperator.py @@ -19,10 +19,9 @@ from cil.optimisation.operators import LinearOperator from cil.optimisation.operators import FiniteDifferenceOperator -from cil.framework import BlockGeometry +from cil.framework import BlockGeometry, ImageGeometry import logging from cil.utilities.multiprocessing import NUM_THREADS -from cil.framework import ImageGeometry import numpy as np NEUMANN = 'Neumann' diff --git a/Wrappers/Python/cil/optimisation/operators/SparseFiniteDifferenceOperator.py b/Wrappers/Python/cil/optimisation/operators/SparseFiniteDifferenceOperator.py index 622d594f66..c129fd90bf 100644 --- a/Wrappers/Python/cil/optimisation/operators/SparseFiniteDifferenceOperator.py +++ b/Wrappers/Python/cil/optimisation/operators/SparseFiniteDifferenceOperator.py @@ -19,7 +19,7 @@ import scipy.sparse as sp import numpy as np -from cil.framework import ImageData +from cil.framework import ImageData, ImageGeometry from cil.optimisation.operators import Operator class SparseFiniteDifferenceOperator(Operator): @@ -91,7 +91,6 @@ def sum_abs_col(self): return ImageData(res) if __name__ == '__main__': - from cil.framework import ImageGeometry M, N= 2, 3 ig = ImageGeometry(M, N) arr = ig.allocate('random_int') diff --git a/Wrappers/Python/cil/processors/Padder.py b/Wrappers/Python/cil/processors/Padder.py index 73f71420b6..e958fd27e3 100644 --- a/Wrappers/Python/cil/processors/Padder.py +++ b/Wrappers/Python/cil/processors/Padder.py @@ -18,9 +18,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import DataProcessor, AcquisitionData, ImageData -from numbers import Number -from cil.framework import DataContainer, AcquisitionGeometry, ImageGeometry +from cil.framework import DataProcessor, AcquisitionData, ImageData, ImageGeometry, DataContainer, AcquisitionGeometry import numpy as np import weakref diff --git a/Wrappers/Python/cil/processors/Slicer.py b/Wrappers/Python/cil/processors/Slicer.py index 8aceca3d17..8b084b738c 100644 --- a/Wrappers/Python/cil/processors/Slicer.py +++ b/Wrappers/Python/cil/processors/Slicer.py @@ -17,8 +17,8 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import DataProcessor, AcquisitionData, ImageData, DataContainer -from cil.framework import AcquisitionGeometry, ImageGeometry, VectorGeometry +from cil.framework import (DataProcessor, AcquisitionData, ImageData, DataContainer, ImageGeometry, VectorGeometry, + AcquisitionGeometry) import numpy as np import weakref import logging diff --git a/Wrappers/Python/cil/utilities/display.py b/Wrappers/Python/cil/utilities/display.py index c65a885c3a..fdfd1d179e 100644 --- a/Wrappers/Python/cil/utilities/display.py +++ b/Wrappers/Python/cil/utilities/display.py @@ -20,8 +20,7 @@ #%% -from cil.framework import AcquisitionGeometry, AcquisitionData, ImageData -from cil.framework import DataContainer, BlockDataContainer +from cil.framework import AcquisitionGeometry, AcquisitionData, ImageData, DataContainer, BlockDataContainer import numpy as np import warnings diff --git a/Wrappers/Python/test/test_BlockDataContainer.py b/Wrappers/Python/test/test_BlockDataContainer.py index 57c11ae886..c5a914c7c5 100644 --- a/Wrappers/Python/test/test_BlockDataContainer.py +++ b/Wrappers/Python/test/test_BlockDataContainer.py @@ -20,8 +20,8 @@ import unittest from utils import initialise_tests import numpy as np -from cil.framework import ImageGeometry, AcquisitionGeometry, VectorGeometry -from cil.framework import ImageData, AcquisitionData, Partitioner +from cil.framework import ImageGeometry, AcquisitionGeometry, VectorGeometry, ImageData +from cil.framework import AcquisitionData, Partitioner from cil.framework import BlockDataContainer, BlockGeometry import functools diff --git a/Wrappers/Python/test/test_BlockOperator.py b/Wrappers/Python/test/test_BlockOperator.py index 34219054e8..759cdfb72b 100644 --- a/Wrappers/Python/test/test_BlockOperator.py +++ b/Wrappers/Python/test/test_BlockOperator.py @@ -21,9 +21,8 @@ from utils import initialise_tests import logging from cil.optimisation.operators import BlockOperator, GradientOperator -from cil.framework import BlockDataContainer +from cil.framework import BlockDataContainer, ImageGeometry, ImageData from cil.optimisation.operators import IdentityOperator -from cil.framework import ImageGeometry, ImageData import numpy from cil.optimisation.operators import FiniteDifferenceOperator diff --git a/Wrappers/Python/test/test_DataContainer.py b/Wrappers/Python/test/test_DataContainer.py index d9fdab42cc..bd50c0e366 100644 --- a/Wrappers/Python/test/test_DataContainer.py +++ b/Wrappers/Python/test/test_DataContainer.py @@ -21,10 +21,9 @@ from utils import initialise_tests import sys import numpy -from cil.framework import DataContainer -from cil.framework import ImageData +from cil.framework import DataContainer, ImageGeometry, ImageData, VectorGeometry from cil.framework import AcquisitionData -from cil.framework import ImageGeometry, BlockGeometry, VectorGeometry +from cil.framework import BlockGeometry from cil.framework import AcquisitionGeometry from cil.framework import acquisition_labels, image_labels from timeit import default_timer as timer diff --git a/Wrappers/Python/test/test_DataProcessor.py b/Wrappers/Python/test/test_DataProcessor.py index 8462c7219f..f7cd08cf8d 100644 --- a/Wrappers/Python/test/test_DataProcessor.py +++ b/Wrappers/Python/test/test_DataProcessor.py @@ -19,9 +19,9 @@ import unittest import numpy -from cil.framework import DataContainer -from cil.framework import ImageGeometry, VectorGeometry, AcquisitionGeometry -from cil.framework import ImageData, AcquisitionData +from cil.framework import DataContainer, ImageGeometry, ImageData, VectorGeometry +from cil.framework import AcquisitionGeometry +from cil.framework import AcquisitionData from cil.utilities import dataexample from timeit import default_timer as timer diff --git a/Wrappers/Python/test/test_PluginsTigre_General.py b/Wrappers/Python/test/test_PluginsTigre_General.py index 04b5d579de..21d055a29f 100644 --- a/Wrappers/Python/test/test_PluginsTigre_General.py +++ b/Wrappers/Python/test/test_PluginsTigre_General.py @@ -18,8 +18,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt import unittest -from cil.framework import AcquisitionGeometry -from cil.framework.framework import ImageGeometry +from cil.framework import AcquisitionGeometry, ImageGeometry import numpy as np from cil.utilities.display import show2D from cil.utilities import dataexample diff --git a/Wrappers/Python/test/test_SIRF.py b/Wrappers/Python/test/test_SIRF.py index 347adc7a3b..c83a4c5b3d 100644 --- a/Wrappers/Python/test/test_SIRF.py +++ b/Wrappers/Python/test/test_SIRF.py @@ -18,6 +18,8 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt import unittest + +import cil.framework.DataContainer from utils import initialise_tests import numpy as np from numpy.linalg import norm @@ -57,7 +59,7 @@ class KullbackLeiblerSIRF(object): def setUp(self): if has_sirf: - self.image1 = pet.ImageData(os.path.join( + self.image1 = cil.framework.DataContainer.ImageData(os.path.join( examples_data_path('PET'),'thorax_single_slice','emission.hv') ) @@ -148,12 +150,12 @@ def test_Gradient(self): for i in range(len(res1)): - if isinstance(self.image1, pet.ImageData): - self.assertTrue(isinstance(res1[i], pet.ImageData)) - self.assertTrue(isinstance(res2[i], pet.ImageData)) + if isinstance(self.image1, cil.framework.DataContainer.ImageData): + self.assertTrue(isinstance(res1[i], cil.framework.DataContainer.ImageData)) + self.assertTrue(isinstance(res2[i], cil.framework.DataContainer.ImageData)) else: - self.assertTrue(isinstance(res1[i], mr.ImageData)) - self.assertTrue(isinstance(res2[i], mr.ImageData)) + self.assertTrue(isinstance(res1[i], cil.framework.DataContainer.ImageData)) + self.assertTrue(isinstance(res2[i], cil.framework.DataContainer.ImageData)) # test direct with and without out np.testing.assert_array_almost_equal(res1[i].as_array(), res2[i].as_array()) @@ -210,7 +212,7 @@ class TestGradientPET_2D(unittest.TestCase, GradientSIRF): def setUp(self): if has_sirf: - self.image1 = pet.ImageData(os.path.join( + self.image1 = cil.framework.DataContainer.ImageData(os.path.join( examples_data_path('PET'),'thorax_single_slice','emission.hv') ) @@ -222,7 +224,7 @@ class TestGradientPET_3D(unittest.TestCase, GradientSIRF): def setUp(self): if has_sirf: - self.image1 = pet.ImageData(os.path.join( + self.image1 = cil.framework.DataContainer.ImageData(os.path.join( examples_data_path('PET'),'brain','emission.hv') ) @@ -292,8 +294,8 @@ def tearDown(self): @unittest.skipUnless(has_sirf, "Has SIRF") def test_BlockDataContainer_with_SIRF_DataContainer_divide(self): os.chdir(self.cwd) - image1 = pet.ImageData('emission.hv') - image2 = pet.ImageData('emission.hv') + image1 = cil.framework.DataContainer.ImageData('emission.hv') + image2 = cil.framework.DataContainer.ImageData('emission.hv') image1.fill(1.) image2.fill(2.) @@ -315,8 +317,8 @@ def test_BlockDataContainer_with_SIRF_DataContainer_divide(self): @unittest.skipUnless(has_sirf, "Has SIRF") def test_BlockDataContainer_with_SIRF_DataContainer_multiply(self): os.chdir(self.cwd) - image1 = pet.ImageData('emission.hv') - image2 = pet.ImageData('emission.hv') + image1 = cil.framework.DataContainer.ImageData('emission.hv') + image2 = cil.framework.DataContainer.ImageData('emission.hv') image1.fill(1.) image2.fill(2.) @@ -338,8 +340,8 @@ def test_BlockDataContainer_with_SIRF_DataContainer_multiply(self): @unittest.skipUnless(has_sirf, "Has SIRF") def test_BlockDataContainer_with_SIRF_DataContainer_add(self): os.chdir(self.cwd) - image1 = pet.ImageData('emission.hv') - image2 = pet.ImageData('emission.hv') + image1 = cil.framework.DataContainer.ImageData('emission.hv') + image2 = cil.framework.DataContainer.ImageData('emission.hv') image1.fill(0) image2.fill(1) @@ -364,8 +366,8 @@ def test_BlockDataContainer_with_SIRF_DataContainer_add(self): @unittest.skipUnless(has_sirf, "Has SIRF") def test_BlockDataContainer_with_SIRF_DataContainer_subtract(self): os.chdir(self.cwd) - image1 = pet.ImageData('emission.hv') - image2 = pet.ImageData('emission.hv') + image1 = cil.framework.DataContainer.ImageData('emission.hv') + image2 = cil.framework.DataContainer.ImageData('emission.hv') image1.fill(2) image2.fill(1) @@ -459,7 +461,7 @@ def test_TNV_proximal_works(self): class TestPETRegularisation(unittest.TestCase, CCPiRegularisationWithSIRFTests): skip_TNV_on_2D = True def setUp(self): - self.image1 = pet.ImageData(os.path.join( + self.image1 = cil.framework.DataContainer.ImageData(os.path.join( examples_data_path('PET'),'thorax_single_slice','emission.hv' )) self.image2 = self.image1 * 0.5 @@ -474,7 +476,7 @@ def test_TNV_proximal_works(self): class TestRegRegularisation(unittest.TestCase, CCPiRegularisationWithSIRFTests): def setUp(self): - self.image1 = reg.ImageData(os.path.join(examples_data_path('Registration'),'test2.nii.gz')) + self.image1 = cil.framework.DataContainer.ImageData(os.path.join(examples_data_path('Registration'), 'test2.nii.gz')) self.image2 = self.image1 * 0.5 class TestMRRegularisation(unittest.TestCase, CCPiRegularisationWithSIRFTests): diff --git a/Wrappers/Python/test/test_algorithms.py b/Wrappers/Python/test/test_algorithms.py index 7c5ecb7bc4..1f6496e183 100644 --- a/Wrappers/Python/test/test_algorithms.py +++ b/Wrappers/Python/test/test_algorithms.py @@ -22,10 +22,8 @@ import numpy import numpy as np from numpy import nan, inf -from cil.framework import VectorData -from cil.framework import ImageData +from cil.framework import VectorData, ImageGeometry, ImageData, VectorGeometry from cil.framework import AcquisitionData -from cil.framework import ImageGeometry from cil.framework import AcquisitionGeometry from cil.framework import BlockDataContainer from cil.framework import BlockGeometry @@ -54,7 +52,7 @@ import time import warnings from cil.optimisation.functions import Rosenbrock -from cil.framework import VectorData, VectorGeometry +from cil.framework import VectorData from cil.utilities.quality_measures import mae, mse, psnr diff --git a/Wrappers/Python/test/test_dataexample.py b/Wrappers/Python/test/test_dataexample.py index 850b37a831..d09b03d1fc 100644 --- a/Wrappers/Python/test/test_dataexample.py +++ b/Wrappers/Python/test/test_dataexample.py @@ -19,7 +19,8 @@ import unittest from utils import initialise_tests -from cil.framework.framework import ImageGeometry,AcquisitionGeometry +from cil.framework.framework import AcquisitionGeometry +from cil.framework import ImageGeometry from cil.utilities import dataexample from cil.utilities import noise import os, sys diff --git a/Wrappers/Python/test/test_functions.py b/Wrappers/Python/test/test_functions.py index a83af8c706..2aff2de60f 100644 --- a/Wrappers/Python/test/test_functions.py +++ b/Wrappers/Python/test/test_functions.py @@ -22,7 +22,7 @@ from cil.optimisation.functions.Function import ScaledFunction import numpy as np -from cil.framework import VectorGeometry, VectorData, BlockDataContainer, DataContainer, image_labels +from cil.framework import VectorGeometry, VectorData, BlockDataContainer, DataContainer, image_labels, ImageGeometry from cil.optimisation.operators import IdentityOperator, MatrixOperator, CompositionOperator, DiagonalOperator, BlockOperator from cil.optimisation.functions import Function, KullbackLeibler, ConstantFunction, TranslateFunction, soft_shrinkage from cil.optimisation.operators import GradientOperator @@ -37,7 +37,7 @@ import numpy import scipy.special -from cil.framework import ImageGeometry, BlockGeometry +from cil.framework import BlockGeometry from cil.optimisation.functions import TranslateFunction from timeit import default_timer as timer diff --git a/Wrappers/Python/test/test_run_test.py b/Wrappers/Python/test/test_run_test.py index 46bbb815c4..6114f0b9a7 100644 --- a/Wrappers/Python/test/test_run_test.py +++ b/Wrappers/Python/test/test_run_test.py @@ -20,8 +20,7 @@ import unittest import numpy import numpy as np -from cil.framework import ImageData -from cil.framework import ImageGeometry +from cil.framework import ImageData, ImageGeometry import numpy.testing diff --git a/Wrappers/Python/test/test_subset.py b/Wrappers/Python/test/test_subset.py index 246022b12a..b02d402663 100644 --- a/Wrappers/Python/test/test_subset.py +++ b/Wrappers/Python/test/test_subset.py @@ -20,10 +20,8 @@ import unittest from utils import initialise_tests import numpy -from cil.framework import DataContainer -from cil.framework import ImageData +from cil.framework import DataContainer, ImageGeometry, ImageData from cil.framework import AcquisitionData -from cil.framework import ImageGeometry from cil.framework import AcquisitionGeometry from cil.framework import acquisition_labels, image_labels from timeit import default_timer as timer From b71c063b093515a913d3f1b01136925c73e990bd Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Sat, 10 Feb 2024 16:47:04 +0000 Subject: [PATCH 14/72] Move Partitioner and AcquisitionData to new files. --- .../Python/cil/framework/AcquisitionData.py | 88 ++++++ Wrappers/Python/cil/framework/Partitioner.py | 189 ++++++++++++ Wrappers/Python/cil/framework/__init__.py | 4 +- Wrappers/Python/cil/framework/framework.py | 274 +----------------- .../Python/test/test_AcquisitionGeometry.py | 3 +- .../Python/test/test_BlockDataContainer.py | 3 +- Wrappers/Python/test/test_DataContainer.py | 3 +- Wrappers/Python/test/test_DataProcessor.py | 3 +- Wrappers/Python/test/test_SIRF.py | 7 +- Wrappers/Python/test/test_algorithms.py | 3 +- Wrappers/Python/test/test_subset.py | 3 +- 11 files changed, 293 insertions(+), 287 deletions(-) create mode 100644 Wrappers/Python/cil/framework/AcquisitionData.py create mode 100644 Wrappers/Python/cil/framework/Partitioner.py diff --git a/Wrappers/Python/cil/framework/AcquisitionData.py b/Wrappers/Python/cil/framework/AcquisitionData.py new file mode 100644 index 0000000000..0a0ca4235d --- /dev/null +++ b/Wrappers/Python/cil/framework/AcquisitionData.py @@ -0,0 +1,88 @@ +import numpy + +from .DataContainer import DataContainer +from .Partitioner import Partitioner + +class AcquisitionData(DataContainer, Partitioner): + '''DataContainer for holding 2D or 3D sinogram''' + __container_priority__ = 1 + + @property + def geometry(self): + return self._geometry + + @geometry.setter + def geometry(self, val): + self._geometry = val + + @property + def dimension_labels(self): + return self.geometry.dimension_labels + + @dimension_labels.setter + def dimension_labels(self, val): + if val is not None: + raise ValueError("Unable to set the dimension_labels directly. Use geometry.set_labels() instead") + + def __init__(self, + array = None, + deep_copy=True, + geometry = None, + **kwargs): + + dtype = kwargs.get('dtype', numpy.float32) + + if geometry is None: + raise AttributeError("AcquisitionData requires a geometry") + + labels = kwargs.get('dimension_labels', None) + if labels is not None and labels != geometry.dimension_labels: + raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") + + if array is None: + array = numpy.empty(geometry.shape, dtype=dtype) + elif issubclass(type(array) , DataContainer): + array = array.as_array() + elif issubclass(type(array) , numpy.ndarray): + pass + else: + raise TypeError('array must be a CIL type DataContainer or numpy.ndarray got {}'.format(type(array))) + + if array.shape != geometry.shape: + raise ValueError('Shape mismatch got {} expected {}'.format(array.shape, geometry.shape)) + + super(AcquisitionData, self).__init__(array, deep_copy, geometry=geometry,**kwargs) + + + def get_slice(self,channel=None, angle=None, vertical=None, horizontal=None, force=False): + ''' + Returns a new dataset of a single slice of in the requested direction. \ + ''' + try: + geometry_new = self.geometry.get_slice(channel=channel, angle=angle, vertical=vertical, horizontal=horizontal) + except ValueError: + if force: + geometry_new = None + else: + raise ValueError ("Unable to return slice of requested AcquisitionData. Use 'force=True' to return DataContainer instead.") + + #get new data + #if vertical = 'centre' slice convert to index and subset, this will interpolate 2 rows to get the center slice value + if vertical == 'centre': + dim = self.geometry.dimension_labels.index('vertical') + + centre_slice_pos = (self.geometry.shape[dim]-1) / 2. + ind0 = int(numpy.floor(centre_slice_pos)) + w2 = centre_slice_pos - ind0 + out = DataContainer.get_slice(self, channel=channel, angle=angle, vertical=ind0, horizontal=horizontal) + + if w2 > 0: + out2 = DataContainer.get_slice(self, channel=channel, angle=angle, vertical=ind0 + 1, horizontal=horizontal) + out = out * (1 - w2) + out2 * w2 + else: + out = DataContainer.get_slice(self, channel=channel, angle=angle, vertical=vertical, horizontal=horizontal) + + if len(out.shape) == 1 or geometry_new is None: + return out + else: + return AcquisitionData(out.array, deep_copy=False, geometry=geometry_new, suppress_warning=True) diff --git a/Wrappers/Python/cil/framework/Partitioner.py b/Wrappers/Python/cil/framework/Partitioner.py new file mode 100644 index 0000000000..717df5d35b --- /dev/null +++ b/Wrappers/Python/cil/framework/Partitioner.py @@ -0,0 +1,189 @@ +import math + +import numpy + +from .BlockGeometry import BlockGeometry + + +class Partitioner(object): + '''Interface for AcquisitionData to be able to partition itself in a number of batches. + + This class, by multiple inheritance with AcquisitionData, allows the user to partition the data, + by using the method ``partition``. + The partitioning will generate a ``BlockDataContainer`` with appropriate ``AcquisitionData``. + + ''' + # modes of partitioning + SEQUENTIAL = 'sequential' + STAGGERED = 'staggered' + RANDOM_PERMUTATION = 'random_permutation' + + def _partition_indices(self, num_batches, indices, stagger=False): + """Partition a list of indices into num_batches of indices. + + Parameters + ---------- + num_batches : int + The number of batches to partition the indices into. + indices : list of int, int + The indices to partition. If passed a list, this list will be partitioned in ``num_batches`` + partitions. If passed an int the indices will be generated automatically using ``range(indices)``. + stagger : bool, default False + If True, the indices will be staggered across the batches. + + Returns + -------- + list of list of int + A list of batches of indices. + """ + + # Partition the indices into batches. + if isinstance(indices, int): + indices = list(range(indices)) + + num_indices = len(indices) + # sanity check + if num_indices < num_batches: + raise ValueError( + 'The number of batches must be less than or equal to the number of indices.' + ) + + if stagger: + batches = [indices[i::num_batches] for i in range(num_batches)] + + else: + # we split the indices with floor(N/M)+1 indices in N%M groups + # and floor(N/M) indices in the remaining M - N%M groups. + + # rename num_indices to N for brevity + N = num_indices + # rename num_batches to M for brevity + M = num_batches + batches = [ + indices[j:j + math.floor(N / M) + 1] for j in range(N % M) + ] + offset = N % M * (math.floor(N / M) + 1) + for i in range(M - N % M): + start = offset + i * math.floor(N / M) + end = start + math.floor(N / M) + batches.append(indices[start:end]) + + return batches + + def _construct_BlockGeometry_from_indices(self, indices): + '''Convert a list of boolean masks to a list of BlockGeometry. + + Parameters + ---------- + indices : list of lists of indices + + Returns + ------- + BlockGeometry + ''' + ags = [] + for mask in indices: + ag = self.geometry.copy() + ag.config.angles.angle_data = numpy.take(self.geometry.angles, mask, axis=0) + ags.append(ag) + return BlockGeometry(*ags) + + def partition(self, num_batches, mode, seed=None): + '''Partition the data into ``num_batches`` batches using the specified ``mode``. + + + The modes are + + 1. ``sequential`` - The data will be partitioned into ``num_batches`` batches of sequential indices. + + 2. ``staggered`` - The data will be partitioned into ``num_batches`` batches of sequential indices, with stride equal to ``num_batches``. + + 3. ``random_permutation`` - The data will be partitioned into ``num_batches`` batches of random indices. + + Parameters + ---------- + num_batches : int + The number of batches to partition the data into. + mode : str + The mode to use for partitioning. Must be one of ``sequential``, ``staggered`` or ``random_permutation``. + seed : int, optional + The seed to use for the random permutation. If not specified, the random number + generator will not be seeded. + + + Returns + ------- + BlockDataContainer + Block of `AcquisitionData` objects containing the data requested in each batch + + Example + ------- + + Partitioning a list of ints [0, 1, 2, 3, 4, 5, 6, 7, 8] into 4 batches will return: + + 1. [[0, 1, 2], [3, 4], [5, 6], [7, 8]] with ``sequential`` + 2. [[0, 4, 8], [1, 5], [2, 6], [3, 7]] with ``staggered`` + 3. [[8, 2, 6], [7, 1], [0, 4], [3, 5]] with ``random_permutation`` and seed 1 + + ''' + if mode == Partitioner.SEQUENTIAL: + return self._partition_deterministic(num_batches, stagger=False) + elif mode == Partitioner.STAGGERED: + return self._partition_deterministic(num_batches, stagger=True) + elif mode == Partitioner.RANDOM_PERMUTATION: + return self._partition_random_permutation(num_batches, seed=seed) + else: + raise ValueError('Unknown partition mode {}'.format(mode)) + + def _partition_deterministic(self, num_batches, stagger=False, indices=None): + '''Partition the data into ``num_batches`` batches. + + Parameters + ---------- + num_batches : int + The number of batches to partition the data into. + stagger : bool, optional + If ``True``, the batches will be staggered. Default is ``False``. + indices : list of int, optional + The indices to partition. If not specified, the indices will be generated from the number of projections. + ''' + if indices is None: + indices = self.geometry.num_projections + partition_indices = self._partition_indices(num_batches, indices, stagger) + blk_geo = self._construct_BlockGeometry_from_indices(partition_indices) + + # copy data + out = blk_geo.allocate(None) + out.geometry = blk_geo + axis = self.dimension_labels.index('angle') + + for i in range(num_batches): + out[i].fill( + numpy.squeeze( + numpy.take(self.array, partition_indices[i], axis=axis) + ) + ) + + return out + + def _partition_random_permutation(self, num_batches, seed=None): + '''Partition the data into ``num_batches`` batches using a random permutation. + + Parameters + ---------- + num_batches : int + The number of batches to partition the data into. + seed : int, optional + The seed to use for the random permutation. If not specified, the random number generator + will not be seeded. + + ''' + if seed is not None: + numpy.random.seed(seed) + + indices = numpy.arange(self.geometry.num_projections) + numpy.random.shuffle(indices) + + indices = list(indices) + + return self._partition_deterministic(num_batches, stagger=False, indices=indices) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 1f380e6aa0..08f049a849 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -23,7 +23,7 @@ import warnings from functools import reduce from .cilacc import cilacc -from .framework import AcquisitionData +from .AcquisitionData import AcquisitionData from .framework import AcquisitionGeometry from .framework import find_key from .DataContainer import message, ImageGeometry, DataContainer, ImageData, VectorData, VectorGeometry @@ -31,5 +31,5 @@ from .framework import AX, PixelByPixelDataProcessor, CastDataContainer from .BlockDataContainer import BlockDataContainer from .BlockGeometry import BlockGeometry -from .framework import Partitioner +from .Partitioner import Partitioner from .label import acquisition_labels, image_labels, data_order, get_order_for_engine, check_order_for_engine diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/framework.py index 9cbde76270..c3ad95365a 100644 --- a/Wrappers/Python/cil/framework/framework.py +++ b/Wrappers/Python/cil/framework/framework.py @@ -24,193 +24,11 @@ import weakref import logging +from . import AcquisitionData from .DataContainer import ImageGeometry, DataContainer from .base import BaseAcquisitionGeometry from .label import acquisition_labels, data_order -from .BlockGeometry import BlockGeometry -class Partitioner(object): - '''Interface for AcquisitionData to be able to partition itself in a number of batches. - - This class, by multiple inheritance with AcquisitionData, allows the user to partition the data, - by using the method ``partition``. - The partitioning will generate a ``BlockDataContainer`` with appropriate ``AcquisitionData``. - - ''' - # modes of partitioning - SEQUENTIAL = 'sequential' - STAGGERED = 'staggered' - RANDOM_PERMUTATION = 'random_permutation' - - def _partition_indices(self, num_batches, indices, stagger=False): - """Partition a list of indices into num_batches of indices. - - Parameters - ---------- - num_batches : int - The number of batches to partition the indices into. - indices : list of int, int - The indices to partition. If passed a list, this list will be partitioned in ``num_batches`` - partitions. If passed an int the indices will be generated automatically using ``range(indices)``. - stagger : bool, default False - If True, the indices will be staggered across the batches. - - Returns - -------- - list of list of int - A list of batches of indices. - """ - - # Partition the indices into batches. - if isinstance(indices, int): - indices = list(range(indices)) - - num_indices = len(indices) - # sanity check - if num_indices < num_batches: - raise ValueError( - 'The number of batches must be less than or equal to the number of indices.' - ) - - if stagger: - batches = [indices[i::num_batches] for i in range(num_batches)] - - else: - # we split the indices with floor(N/M)+1 indices in N%M groups - # and floor(N/M) indices in the remaining M - N%M groups. - - # rename num_indices to N for brevity - N = num_indices - # rename num_batches to M for brevity - M = num_batches - batches = [ - indices[j:j + math.floor(N / M) + 1] for j in range(N % M) - ] - offset = N % M * (math.floor(N / M) + 1) - for i in range(M - N % M): - start = offset + i * math.floor(N / M) - end = start + math.floor(N / M) - batches.append(indices[start:end]) - - return batches - - def _construct_BlockGeometry_from_indices(self, indices): - '''Convert a list of boolean masks to a list of BlockGeometry. - - Parameters - ---------- - indices : list of lists of indices - - Returns - ------- - BlockGeometry - ''' - ags = [] - for mask in indices: - ag = self.geometry.copy() - ag.config.angles.angle_data = numpy.take(self.geometry.angles, mask, axis=0) - ags.append(ag) - return BlockGeometry(*ags) - - def partition(self, num_batches, mode, seed=None): - '''Partition the data into ``num_batches`` batches using the specified ``mode``. - - - The modes are - - 1. ``sequential`` - The data will be partitioned into ``num_batches`` batches of sequential indices. - - 2. ``staggered`` - The data will be partitioned into ``num_batches`` batches of sequential indices, with stride equal to ``num_batches``. - - 3. ``random_permutation`` - The data will be partitioned into ``num_batches`` batches of random indices. - - Parameters - ---------- - num_batches : int - The number of batches to partition the data into. - mode : str - The mode to use for partitioning. Must be one of ``sequential``, ``staggered`` or ``random_permutation``. - seed : int, optional - The seed to use for the random permutation. If not specified, the random number - generator will not be seeded. - - - Returns - ------- - BlockDataContainer - Block of `AcquisitionData` objects containing the data requested in each batch - - Example - ------- - - Partitioning a list of ints [0, 1, 2, 3, 4, 5, 6, 7, 8] into 4 batches will return: - - 1. [[0, 1, 2], [3, 4], [5, 6], [7, 8]] with ``sequential`` - 2. [[0, 4, 8], [1, 5], [2, 6], [3, 7]] with ``staggered`` - 3. [[8, 2, 6], [7, 1], [0, 4], [3, 5]] with ``random_permutation`` and seed 1 - - ''' - if mode == Partitioner.SEQUENTIAL: - return self._partition_deterministic(num_batches, stagger=False) - elif mode == Partitioner.STAGGERED: - return self._partition_deterministic(num_batches, stagger=True) - elif mode == Partitioner.RANDOM_PERMUTATION: - return self._partition_random_permutation(num_batches, seed=seed) - else: - raise ValueError('Unknown partition mode {}'.format(mode)) - - def _partition_deterministic(self, num_batches, stagger=False, indices=None): - '''Partition the data into ``num_batches`` batches. - - Parameters - ---------- - num_batches : int - The number of batches to partition the data into. - stagger : bool, optional - If ``True``, the batches will be staggered. Default is ``False``. - indices : list of int, optional - The indices to partition. If not specified, the indices will be generated from the number of projections. - ''' - if indices is None: - indices = self.geometry.num_projections - partition_indices = self._partition_indices(num_batches, indices, stagger) - blk_geo = self._construct_BlockGeometry_from_indices(partition_indices) - - # copy data - out = blk_geo.allocate(None) - out.geometry = blk_geo - axis = self.dimension_labels.index('angle') - - for i in range(num_batches): - out[i].fill( - numpy.squeeze( - numpy.take(self.array, partition_indices[i], axis=axis) - ) - ) - - return out - - def _partition_random_permutation(self, num_batches, seed=None): - '''Partition the data into ``num_batches`` batches using a random permutation. - - Parameters - ---------- - num_batches : int - The number of batches to partition the data into. - seed : int, optional - The seed to use for the random permutation. If not specified, the random number generator - will not be seeded. - - ''' - if seed is not None: - numpy.random.seed(seed) - - indices = numpy.arange(self.geometry.num_projections) - numpy.random.shuffle(indices) - - indices = list(indices) - - return self._partition_deterministic(num_batches, stagger=False, indices=indices) def find_key(dic, val): """return the key of dictionary dic given the value""" @@ -2326,9 +2144,9 @@ def allocate(self, value=0, **kwargs): if kwargs.get('dimension_labels', None) is not None: raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") - out = AcquisitionData(geometry=self.copy(), - dtype=dtype, - suppress_warning=True) + out = AcquisitionData(geometry=self.copy(), + dtype=dtype, + suppress_warning=True) if isinstance(value, Number): # it's created empty, so we make it 0 @@ -2358,90 +2176,6 @@ def allocate(self, value=0, **kwargs): return out -class AcquisitionData(DataContainer, Partitioner): - '''DataContainer for holding 2D or 3D sinogram''' - __container_priority__ = 1 - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, val): - self._geometry = val - - @property - def dimension_labels(self): - return self.geometry.dimension_labels - - @dimension_labels.setter - def dimension_labels(self, val): - if val is not None: - raise ValueError("Unable to set the dimension_labels directly. Use geometry.set_labels() instead") - - def __init__(self, - array = None, - deep_copy=True, - geometry = None, - **kwargs): - - dtype = kwargs.get('dtype', numpy.float32) - - if geometry is None: - raise AttributeError("AcquisitionData requires a geometry") - - labels = kwargs.get('dimension_labels', None) - if labels is not None and labels != geometry.dimension_labels: - raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") - - if array is None: - array = numpy.empty(geometry.shape, dtype=dtype) - elif issubclass(type(array) , DataContainer): - array = array.as_array() - elif issubclass(type(array) , numpy.ndarray): - pass - else: - raise TypeError('array must be a CIL type DataContainer or numpy.ndarray got {}'.format(type(array))) - - if array.shape != geometry.shape: - raise ValueError('Shape mismatch got {} expected {}'.format(array.shape, geometry.shape)) - - super(AcquisitionData, self).__init__(array, deep_copy, geometry=geometry,**kwargs) - - - def get_slice(self,channel=None, angle=None, vertical=None, horizontal=None, force=False): - ''' - Returns a new dataset of a single slice of in the requested direction. \ - ''' - try: - geometry_new = self.geometry.get_slice(channel=channel, angle=angle, vertical=vertical, horizontal=horizontal) - except ValueError: - if force: - geometry_new = None - else: - raise ValueError ("Unable to return slice of requested AcquisitionData. Use 'force=True' to return DataContainer instead.") - - #get new data - #if vertical = 'centre' slice convert to index and subset, this will interpolate 2 rows to get the center slice value - if vertical == 'centre': - dim = self.geometry.dimension_labels.index('vertical') - - centre_slice_pos = (self.geometry.shape[dim]-1) / 2. - ind0 = int(numpy.floor(centre_slice_pos)) - w2 = centre_slice_pos - ind0 - out = DataContainer.get_slice(self, channel=channel, angle=angle, vertical=ind0, horizontal=horizontal) - - if w2 > 0: - out2 = DataContainer.get_slice(self, channel=channel, angle=angle, vertical=ind0 + 1, horizontal=horizontal) - out = out * (1 - w2) + out2 * w2 - else: - out = DataContainer.get_slice(self, channel=channel, angle=angle, vertical=vertical, horizontal=horizontal) - - if len(out.shape) == 1 or geometry_new is None: - return out - else: - return AcquisitionData(out.array, deep_copy=False, geometry=geometry_new, suppress_warning=True) - class Processor(object): '''Defines a generic DataContainer processor diff --git a/Wrappers/Python/test/test_AcquisitionGeometry.py b/Wrappers/Python/test/test_AcquisitionGeometry.py index 22fd2db3e9..0bbc1c42a9 100644 --- a/Wrappers/Python/test/test_AcquisitionGeometry.py +++ b/Wrappers/Python/test/test_AcquisitionGeometry.py @@ -24,9 +24,8 @@ import re import io import sys -from cil.framework import AcquisitionGeometry, ImageGeometry, BlockGeometry, AcquisitionData +from cil.framework import AcquisitionGeometry, ImageGeometry, BlockGeometry, AcquisitionData, Partitioner from cil.framework.framework import SystemConfiguration -from cil.framework import Partitioner initialise_tests() diff --git a/Wrappers/Python/test/test_BlockDataContainer.py b/Wrappers/Python/test/test_BlockDataContainer.py index c5a914c7c5..353645a522 100644 --- a/Wrappers/Python/test/test_BlockDataContainer.py +++ b/Wrappers/Python/test/test_BlockDataContainer.py @@ -20,8 +20,7 @@ import unittest from utils import initialise_tests import numpy as np -from cil.framework import ImageGeometry, AcquisitionGeometry, VectorGeometry, ImageData -from cil.framework import AcquisitionData, Partitioner +from cil.framework import ImageGeometry, AcquisitionGeometry, VectorGeometry, ImageData, Partitioner, AcquisitionData from cil.framework import BlockDataContainer, BlockGeometry import functools diff --git a/Wrappers/Python/test/test_DataContainer.py b/Wrappers/Python/test/test_DataContainer.py index bd50c0e366..a95a6f61bd 100644 --- a/Wrappers/Python/test/test_DataContainer.py +++ b/Wrappers/Python/test/test_DataContainer.py @@ -21,8 +21,7 @@ from utils import initialise_tests import sys import numpy -from cil.framework import DataContainer, ImageGeometry, ImageData, VectorGeometry -from cil.framework import AcquisitionData +from cil.framework import DataContainer, ImageGeometry, ImageData, VectorGeometry, AcquisitionData from cil.framework import BlockGeometry from cil.framework import AcquisitionGeometry from cil.framework import acquisition_labels, image_labels diff --git a/Wrappers/Python/test/test_DataProcessor.py b/Wrappers/Python/test/test_DataProcessor.py index f7cd08cf8d..f7172253b6 100644 --- a/Wrappers/Python/test/test_DataProcessor.py +++ b/Wrappers/Python/test/test_DataProcessor.py @@ -19,9 +19,8 @@ import unittest import numpy -from cil.framework import DataContainer, ImageGeometry, ImageData, VectorGeometry +from cil.framework import DataContainer, ImageGeometry, ImageData, VectorGeometry, AcquisitionData from cil.framework import AcquisitionGeometry -from cil.framework import AcquisitionData from cil.utilities import dataexample from timeit import default_timer as timer diff --git a/Wrappers/Python/test/test_SIRF.py b/Wrappers/Python/test/test_SIRF.py index c83a4c5b3d..205fc90a22 100644 --- a/Wrappers/Python/test/test_SIRF.py +++ b/Wrappers/Python/test/test_SIRF.py @@ -19,6 +19,7 @@ import unittest +import cil.framework.AcquisitionData import cil.framework.DataContainer from utils import initialise_tests import numpy as np @@ -236,9 +237,9 @@ class TestGradientMR_2D(unittest.TestCase, GradientSIRF): def setUp(self): if has_sirf: - acq_data = mr.AcquisitionData(os.path.join + acq_data = cil.framework.AcquisitionData.AcquisitionData(os.path.join (examples_data_path('MR'),'simulated_MR_2D_cartesian.h5') - ) + ) preprocessed_data = mr.preprocess_acquisition_data(acq_data) recon = mr.FullySampledReconstructor() recon.set_input(preprocessed_data) @@ -481,7 +482,7 @@ def setUp(self): class TestMRRegularisation(unittest.TestCase, CCPiRegularisationWithSIRFTests): def setUp(self): - acq_data = mr.AcquisitionData(os.path.join(examples_data_path('MR'),'simulated_MR_2D_cartesian.h5')) + acq_data = cil.framework.AcquisitionData.AcquisitionData(os.path.join(examples_data_path('MR'), 'simulated_MR_2D_cartesian.h5')) preprocessed_data = mr.preprocess_acquisition_data(acq_data) recon = mr.FullySampledReconstructor() recon.set_input(preprocessed_data) diff --git a/Wrappers/Python/test/test_algorithms.py b/Wrappers/Python/test/test_algorithms.py index 1f6496e183..f049a997fa 100644 --- a/Wrappers/Python/test/test_algorithms.py +++ b/Wrappers/Python/test/test_algorithms.py @@ -22,8 +22,7 @@ import numpy import numpy as np from numpy import nan, inf -from cil.framework import VectorData, ImageGeometry, ImageData, VectorGeometry -from cil.framework import AcquisitionData +from cil.framework import VectorData, ImageGeometry, ImageData, VectorGeometry, AcquisitionData from cil.framework import AcquisitionGeometry from cil.framework import BlockDataContainer from cil.framework import BlockGeometry diff --git a/Wrappers/Python/test/test_subset.py b/Wrappers/Python/test/test_subset.py index b02d402663..8a53d2f682 100644 --- a/Wrappers/Python/test/test_subset.py +++ b/Wrappers/Python/test/test_subset.py @@ -20,8 +20,7 @@ import unittest from utils import initialise_tests import numpy -from cil.framework import DataContainer, ImageGeometry, ImageData -from cil.framework import AcquisitionData +from cil.framework import DataContainer, ImageGeometry, ImageData, AcquisitionData from cil.framework import AcquisitionGeometry from cil.framework import acquisition_labels, image_labels from timeit import default_timer as timer From 3b2a528a33b08a29da46969a7236c56551290168 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Sat, 10 Feb 2024 17:23:00 +0000 Subject: [PATCH 15/72] Move AcquisitionGeometry to a new file. --- Wrappers/Python/cil/framework/__init__.py | 2 +- .../cil/framework/acquisition_geometry.py | 2161 +++++++++++++++++ Wrappers/Python/cil/framework/framework.py | 2150 +--------------- .../Python/test/test_AcquisitionGeometry.py | 3 +- Wrappers/Python/test/test_DataContainer.py | 6 +- Wrappers/Python/test/test_DataProcessor.py | 3 +- Wrappers/Python/test/test_algorithms.py | 7 +- Wrappers/Python/test/test_dataexample.py | 3 +- Wrappers/Python/test/test_functions.py | 5 +- Wrappers/Python/test/test_subset.py | 4 +- 10 files changed, 2174 insertions(+), 2170 deletions(-) create mode 100644 Wrappers/Python/cil/framework/acquisition_geometry.py diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 08f049a849..969c47f53d 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -24,7 +24,7 @@ from functools import reduce from .cilacc import cilacc from .AcquisitionData import AcquisitionData -from .framework import AcquisitionGeometry +from .acquisition_geometry import AcquisitionGeometry, SystemConfiguration from .framework import find_key from .DataContainer import message, ImageGeometry, DataContainer, ImageData, VectorData, VectorGeometry from .framework import DataProcessor, Processor diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py new file mode 100644 index 0000000000..594646d6d2 --- /dev/null +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -0,0 +1,2161 @@ +import copy +import logging +import math +from numbers import Number + +import numpy + +from .label import acquisition_labels, data_order +from .AcquisitionData import AcquisitionData +from .base import BaseAcquisitionGeometry +from .DataContainer import ImageGeometry + + +class ComponentDescription(object): + r'''This class enables the creation of vectors and unit vectors used to describe the components of a tomography system + ''' + def __init__ (self, dof): + self._dof = dof + + @staticmethod + def create_vector(val): + try: + vec = numpy.array(val, dtype=numpy.float64).reshape(len(val)) + except: + raise ValueError("Can't convert to numpy array") + + return vec + + @staticmethod + def create_unit_vector(val): + vec = ComponentDescription.create_vector(val) + dot_product = vec.dot(vec) + if abs(dot_product)>1e-8: + vec = (vec/numpy.sqrt(dot_product)) + else: + raise ValueError("Can't return a unit vector of a zero magnitude vector") + return vec + + def length_check(self, val): + try: + val_length = len(val) + except: + raise ValueError("Vectors for {0}D geometries must have length = {0}. Got {1}".format(self._dof,val)) + + if val_length != self._dof: + raise ValueError("Vectors for {0}D geometries must have length = {0}. Got {1}".format(self._dof,val)) + + @staticmethod + def test_perpendicular(vector1, vector2): + dor_prod = vector1.dot(vector2) + if abs(dor_prod) <1e-10: + return True + return False + + @staticmethod + def test_parallel(vector1, vector2): + '''For unit vectors only. Returns true if directions are opposite''' + dor_prod = vector1.dot(vector2) + if 1- abs(dor_prod) <1e-10: + return True + return False + + +class PositionVector(ComponentDescription): + r'''This class creates a component of a tomography system with a position attribute + ''' + @property + def position(self): + try: + return self._position + except: + raise AttributeError + + @position.setter + def position(self, val): + self.length_check(val) + self._position = ComponentDescription.create_vector(val) + + +class DirectionVector(ComponentDescription): + r'''This class creates a component of a tomography system with a direction attribute + ''' + @property + def direction(self): + try: + return self._direction + except: + raise AttributeError + + @direction.setter + def direction(self, val): + self.length_check(val) + self._direction = ComponentDescription.create_unit_vector(val) + + +class PositionDirectionVector(PositionVector, DirectionVector): + r'''This class creates a component of a tomography system with position and direction attributes + ''' + pass + + +class Detector1D(PositionVector): + r'''This class creates a component of a tomography system with position and direction_x attributes used for 1D panels + ''' + @property + def direction_x(self): + try: + return self._direction_x + except: + raise AttributeError + + @direction_x.setter + def direction_x(self, val): + self.length_check(val) + self._direction_x = ComponentDescription.create_unit_vector(val) + + @property + def normal(self): + try: + return ComponentDescription.create_unit_vector([self._direction_x[1], -self._direction_x[0]]) + except: + raise AttributeError + + +class Detector2D(PositionVector): + r'''This class creates a component of a tomography system with position, direction_x and direction_y attributes used for 2D panels + ''' + @property + def direction_x(self): + try: + return self._direction_x + except: + raise AttributeError + + @property + def direction_y(self): + try: + return self._direction_y + except: + raise AttributeError + + @property + def normal(self): + try: + return numpy.cross(self._direction_x, self._direction_y) + except: + raise AttributeError + + def set_direction(self, x, y): + self.length_check(x) + x = ComponentDescription.create_unit_vector(x) + + self.length_check(y) + y = ComponentDescription.create_unit_vector(y) + + dot_product = x.dot(y) + if not numpy.isclose(dot_product, 0): + raise ValueError("vectors detector.direction_x and detector.direction_y must be orthogonal") + + self._direction_y = y + self._direction_x = x + + +class SystemConfiguration(object): + r'''This is a generic class to hold the description of a tomography system + ''' + + SYSTEM_SIMPLE = 'simple' + SYSTEM_OFFSET = 'offset' + SYSTEM_ADVANCED = 'advanced' + + @property + def dimension(self): + if self._dimension == 2: + return '2D' + else: + return '3D' + + @dimension.setter + def dimension(self,val): + if val != 2 and val != 3: + raise ValueError('Can set up 2D and 3D systems only. got {0}D'.format(val)) + else: + self._dimension = val + + @property + def geometry(self): + return self._geometry + + @geometry.setter + def geometry(self,val): + if val != acquisition_labels["CONE"] and val != acquisition_labels["PARALLEL"]: + raise ValueError('geom_type = {} not recognised please specify \'cone\' or \'parallel\''.format(val)) + else: + self._geometry = val + + def __init__(self, dof, geometry, units='units'): + """Initialises the system component attributes for the acquisition type + """ + self.dimension = dof + self.geometry = geometry + self.units = units + + if geometry == acquisition_labels["PARALLEL"]: + self.ray = DirectionVector(dof) + else: + self.source = PositionVector(dof) + + if dof == 2: + self.detector = Detector1D(dof) + self.rotation_axis = PositionVector(dof) + else: + self.detector = Detector2D(dof) + self.rotation_axis = PositionDirectionVector(dof) + + def __str__(self): + """Implements the string representation of the system configuration + """ + raise NotImplementedError + + def __eq__(self, other): + """Implements the equality check of the system configuration + """ + raise NotImplementedError + + @staticmethod + def rotation_vec_to_y(vec): + ''' returns a rotation matrix that will rotate the projection of vec on the x-y plane to the +y direction [0,1, Z]''' + + vec = ComponentDescription.create_unit_vector(vec) + + axis_rotation = numpy.eye(len(vec)) + + if numpy.allclose(vec[:2],[0,1]): + pass + elif numpy.allclose(vec[:2],[0,-1]): + axis_rotation[0][0] = -1 + axis_rotation[1][1] = -1 + else: + theta = math.atan2(vec[0],vec[1]) + axis_rotation[0][0] = axis_rotation[1][1] = math.cos(theta) + axis_rotation[0][1] = -math.sin(theta) + axis_rotation[1][0] = math.sin(theta) + + return axis_rotation + + @staticmethod + def rotation_vec_to_z(vec): + ''' returns a rotation matrix that will align vec with the z-direction [0,0,1]''' + + vec = ComponentDescription.create_unit_vector(vec) + + if len(vec) == 2: + return numpy.array([[1, 0],[0, 1]]) + + elif len(vec) == 3: + axis_rotation = numpy.eye(3) + + if numpy.allclose(vec,[0,0,1]): + pass + elif numpy.allclose(vec,[0,0,-1]): + axis_rotation = numpy.eye(3) + axis_rotation[1][1] = -1 + axis_rotation[2][2] = -1 + else: + vx = numpy.array([[0, 0, -vec[0]], [0, 0, -vec[1]], [vec[0], vec[1], 0]]) + axis_rotation = numpy.eye(3) + vx + vx.dot(vx) * 1 / (1 + vec[2]) + + else: + raise ValueError("Vec must have length 3, got {}".format(len(vec))) + + return axis_rotation + + def update_reference_frame(self): + r'''Transforms the system origin to the rotation_axis position + ''' + self.set_origin(self.rotation_axis.position) + + + def set_origin(self, origin): + r'''Transforms the system origin to the input origin + ''' + translation = origin.copy() + if hasattr(self,'source'): + self.source.position -= translation + + self.detector.position -= translation + self.rotation_axis.position -= translation + + + def get_centre_slice(self): + """Returns the 2D system configuration corresponding to the centre slice + """ + raise NotImplementedError + + def calculate_magnification(self): + r'''Calculates the magnification of the system using the source to rotate axis, + and source to detector distance along the direction. + + :return: returns [dist_source_center, dist_center_detector, magnification], [0] distance from the source to the rotate axis, [1] distance from the rotate axis to the detector, [2] magnification of the system + :rtype: list + ''' + raise NotImplementedError + + def system_description(self): + r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, + \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets + \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets + ''' + raise NotImplementedError + + def copy(self): + '''returns a copy of SystemConfiguration''' + return copy.deepcopy(self) + + +class Parallel2D(SystemConfiguration): + r'''This class creates the SystemConfiguration of a parallel beam 2D tomographic system + + :param ray_direction: A 2D vector describing the x-ray direction (x,y) + :type ray_direction: list, tuple, ndarray + :param detector_pos: A 2D vector describing the position of the centre of the detector (x,y) + :type detector_pos: list, tuple, ndarray + :param detector_direction_x: A 2D vector describing the direction of the detector_x (x,y) + :type detector_direction_x: list, tuple, ndarray + :param rotation_axis_pos: A 2D vector describing the position of the axis of rotation (x,y) + :type rotation_axis_pos: list, tuple, ndarray + :param units: Label the units of distance used for the configuration + :type units: string + ''' + + def __init__ (self, ray_direction, detector_pos, detector_direction_x, rotation_axis_pos, units='units'): + """Constructor method + """ + super(Parallel2D, self).__init__(dof=2, geometry = 'parallel', units=units) + + #source + self.ray.direction = ray_direction + + #detector + self.detector.position = detector_pos + self.detector.direction_x = detector_direction_x + + #rotate axis + self.rotation_axis.position = rotation_axis_pos + + + def align_reference_frame(self, definition='cil'): + r'''Transforms and rotates the system to backend definitions + + 'cil' sets the origin to the rotation axis and aligns the y axis with the ray-direction + 'tigre' sets the origin to the rotation axis and aligns the y axis with the ray-direction + ''' + #in this instance definitions are the same + if definition not in ['cil','tigre']: + raise ValueError("Geometry can be configured for definition = 'cil' or 'tigre' only. Got {}".format(definition)) + + self.set_origin(self.rotation_axis.position) + + rotation_matrix = SystemConfiguration.rotation_vec_to_y(self.ray.direction) + + self.ray.direction = rotation_matrix.dot(self.ray.direction.reshape(2,1)) + self.detector.position = rotation_matrix.dot(self.detector.position.reshape(2,1)) + self.detector.direction_x = rotation_matrix.dot(self.detector.direction_x.reshape(2,1)) + + + def system_description(self): + r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, + \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets + \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets + ''' + + + rays_perpendicular_detector = ComponentDescription.test_parallel(self.ray.direction, self.detector.normal) + + #rotation axis position + ray direction hits detector position + if numpy.allclose(self.rotation_axis.position, self.detector.position): #points are equal so on ray path + rotation_axis_centred = True + else: + vec_a = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position) + rotation_axis_centred = ComponentDescription.test_parallel(self.ray.direction, vec_a) + + if not rays_perpendicular_detector: + config = SystemConfiguration.SYSTEM_ADVANCED + elif not rotation_axis_centred: + config = SystemConfiguration.SYSTEM_OFFSET + else: + config = SystemConfiguration.SYSTEM_SIMPLE + + return config + + + def rotation_axis_on_detector(self): + """ + Calculates the position, on the detector, of the projection of the rotation axis in the world coordinate system + + Returns + ------- + PositionVector + Position in the 3D system + """ + Pv = self.rotation_axis.position + ratio = (self.detector.position - Pv).dot(self.detector.normal) / self.ray.direction.dot(self.detector.normal) + out = PositionVector(2) + out.position = Pv + self.ray.direction * ratio + return out + + def calculate_centre_of_rotation(self): + """ + Calculates the position, on the detector, of the projection of the rotation axis in the detector coordinate system + + Note + ---- + - Origin is in the centre of the detector + - Axes directions are specified by detector.direction_x, detector.direction_y + - Units are the units of distance used to specify the component's positions + + Returns + ------- + Float + Offset position along the detector x_axis at y=0 + Float + Angle between the y_axis and the rotation axis projection, in radians + """ + + #convert to the detector coordinate system + dp1 = self.rotation_axis_on_detector().position - self.detector.position + offset = self.detector.direction_x.dot(dp1) + + return (offset, 0.0) + + def set_centre_of_rotation(self, offset): + """ Configures the geometry to have the requested centre of rotation offset at the detector + """ + offset_current = self.calculate_centre_of_rotation()[0] + offset_new = offset - offset_current + + self.rotation_axis.position = self.rotation_axis.position + offset_new * self.detector.direction_x + + def __str__(self): + def csv(val): + return numpy.array2string(val, separator=', ') + + repres = "2D Parallel-beam tomography\n" + repres += "System configuration:\n" + repres += "\tRay direction: {0}\n".format(csv(self.ray.direction)) + repres += "\tRotation axis position: {0}\n".format(csv(self.rotation_axis.position)) + repres += "\tDetector position: {0}\n".format(csv(self.detector.position)) + repres += "\tDetector direction x: {0}\n".format(csv(self.detector.direction_x)) + return repres + + def __eq__(self, other): + + if not isinstance(other, self.__class__): + return False + + if numpy.allclose(self.ray.direction, other.ray.direction) \ + and numpy.allclose(self.detector.position, other.detector.position)\ + and numpy.allclose(self.detector.direction_x, other.detector.direction_x)\ + and numpy.allclose(self.rotation_axis.position, other.rotation_axis.position): + return True + + return False + + def get_centre_slice(self): + return self + + def calculate_magnification(self): + return [None, None, 1.0] + + +class Parallel3D(SystemConfiguration): + r'''This class creates the SystemConfiguration of a parallel beam 3D tomographic system + + :param ray_direction: A 3D vector describing the x-ray direction (x,y,z) + :type ray_direction: list, tuple, ndarray + :param detector_pos: A 3D vector describing the position of the centre of the detector (x,y,z) + :type detector_pos: list, tuple, ndarray + :param detector_direction_x: A 3D vector describing the direction of the detector_x (x,y,z) + :type detector_direction_x: list, tuple, ndarray + :param detector_direction_y: A 3D vector describing the direction of the detector_y (x,y,z) + :type detector_direction_y: list, tuple, ndarray + :param rotation_axis_pos: A 3D vector describing the position of the axis of rotation (x,y,z) + :type rotation_axis_pos: list, tuple, ndarray + :param rotation_axis_direction: A 3D vector describing the direction of the axis of rotation (x,y,z) + :type rotation_axis_direction: list, tuple, ndarray + :param units: Label the units of distance used for the configuration + :type units: string + ''' + + def __init__ (self, ray_direction, detector_pos, detector_direction_x, detector_direction_y, rotation_axis_pos, rotation_axis_direction, units='units'): + """Constructor method + """ + super(Parallel3D, self).__init__(dof=3, geometry = 'parallel', units=units) + + #source + self.ray.direction = ray_direction + + #detector + self.detector.position = detector_pos + self.detector.set_direction(detector_direction_x, detector_direction_y) + + #rotate axis + self.rotation_axis.position = rotation_axis_pos + self.rotation_axis.direction = rotation_axis_direction + + def align_z(self): + r'''Transforms the system origin to the rotate axis with z direction aligned to the rotate axis direction + ''' + self.set_origin(self.rotation_axis.position) + + #calculate rotation matrix to align rotation axis direction with z + rotation_matrix = SystemConfiguration.rotation_vec_to_z(self.rotation_axis.direction) + + #apply transform + self.rotation_axis.direction = [0,0,1] + self.ray.direction = rotation_matrix.dot(self.ray.direction.reshape(3,1)) + self.detector.position = rotation_matrix.dot(self.detector.position.reshape(3,1)) + new_x = rotation_matrix.dot(self.detector.direction_x.reshape(3,1)) + new_y = rotation_matrix.dot(self.detector.direction_y.reshape(3,1)) + self.detector.set_direction(new_x, new_y) + + + def align_reference_frame(self, definition='cil'): + r'''Transforms and rotates the system to backend definitions + ''' + #in this instance definitions are the same + if definition not in ['cil','tigre']: + raise ValueError("Geometry can be configured for definition = 'cil' or 'tigre' only. Got {}".format(definition)) + + self.align_z() + rotation_matrix = SystemConfiguration.rotation_vec_to_y(self.ray.direction) + + self.ray.direction = rotation_matrix.dot(self.ray.direction.reshape(3,1)) + self.detector.position = rotation_matrix.dot(self.detector.position.reshape(3,1)) + new_direction_x = rotation_matrix.dot(self.detector.direction_x.reshape(3,1)) + new_direction_y = rotation_matrix.dot(self.detector.direction_y.reshape(3,1)) + self.detector.set_direction(new_direction_x, new_direction_y) + + + def system_description(self): + r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, + \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets + \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets + ''' + + + ''' + simple + - rays perpendicular to detector + - rotation axis parallel to detector y + - rotation axis position + ray direction hits detector with no x offset (y offsets allowed) + offset + - rays perpendicular to detector + - rotation axis parallel to detector y + rolled + - rays perpendicular to detector + - rays perpendicular to rotation axis + advanced + - not rays perpendicular to detector (for parallel just equates to an effective pixel size change?) + or + - not rays perpendicular to rotation axis (tilted, i.e. laminography) + ''' + + rays_perpendicular_detector = ComponentDescription.test_parallel(self.ray.direction, self.detector.normal) + rays_perpendicular_rotation = ComponentDescription.test_perpendicular(self.ray.direction, self.rotation_axis.direction) + rotation_parallel_detector_y = ComponentDescription.test_parallel(self.rotation_axis.direction, self.detector.direction_y) + + #rotation axis to detector is parallel with ray + if numpy.allclose(self.rotation_axis.position, self.detector.position): #points are equal so on ray path + rotation_axis_centred = True + else: + vec_a = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position ) + rotation_axis_centred = ComponentDescription.test_parallel(self.ray.direction, vec_a) + + if not rays_perpendicular_detector or\ + not rays_perpendicular_rotation or\ + not rotation_parallel_detector_y: + config = SystemConfiguration.SYSTEM_ADVANCED + elif not rotation_axis_centred: + config = SystemConfiguration.SYSTEM_OFFSET + else: + config = SystemConfiguration.SYSTEM_SIMPLE + + return config + + + def __str__(self): + def csv(val): + return numpy.array2string(val, separator=', ') + + repres = "3D Parallel-beam tomography\n" + repres += "System configuration:\n" + repres += "\tRay direction: {0}\n".format(csv(self.ray.direction)) + repres += "\tRotation axis position: {0}\n".format(csv(self.rotation_axis.position)) + repres += "\tRotation axis direction: {0}\n".format(csv(self.rotation_axis.direction)) + repres += "\tDetector position: {0}\n".format(csv(self.detector.position)) + repres += "\tDetector direction x: {0}\n".format(csv(self.detector.direction_x)) + repres += "\tDetector direction y: {0}\n".format(csv(self.detector.direction_y)) + return repres + + def __eq__(self, other): + + if not isinstance(other, self.__class__): + return False + + if numpy.allclose(self.ray.direction, other.ray.direction) \ + and numpy.allclose(self.detector.position, other.detector.position)\ + and numpy.allclose(self.detector.direction_x, other.detector.direction_x)\ + and numpy.allclose(self.detector.direction_y, other.detector.direction_y)\ + and numpy.allclose(self.rotation_axis.position, other.rotation_axis.position)\ + and numpy.allclose(self.rotation_axis.direction, other.rotation_axis.direction): + + return True + + return False + + def calculate_magnification(self): + return [None, None, 1.0] + + def get_centre_slice(self): + """Returns the 2D system configuration corresponding to the centre slice + """ + dp1 = self.rotation_axis.direction.dot(self.ray.direction) + dp2 = self.rotation_axis.direction.dot(self.detector.direction_x) + + if numpy.isclose(dp1, 0) and numpy.isclose(dp2, 0): + temp = self.copy() + + #convert to rotation axis reference frame + temp.align_reference_frame() + + ray_direction = temp.ray.direction[0:2] + detector_position = temp.detector.position[0:2] + detector_direction_x = temp.detector.direction_x[0:2] + rotation_axis_position = temp.rotation_axis.position[0:2] + + return Parallel2D(ray_direction, detector_position, detector_direction_x, rotation_axis_position) + + else: + raise ValueError('Cannot convert geometry to 2D. Requires axis of rotation to be perpendicular to ray direction and the detector direction x.') + + + def rotation_axis_on_detector(self): + """ + Calculates the position, on the detector, of the projection of the rotation axis in the world coordinate system + + Returns + ------- + PositionDirectionVector + Position and direction in the 3D system + """ + #calculate the rotation axis line with the detector + vec_a = self.ray.direction + + #calculate the intersection with the detector + Pv = self.rotation_axis.position + ratio = (self.detector.position - Pv).dot(self.detector.normal) / vec_a.dot(self.detector.normal) + point1 = Pv + vec_a * ratio + + Pv = self.rotation_axis.position + self.rotation_axis.direction + ratio = (self.detector.position - Pv).dot(self.detector.normal) / vec_a.dot(self.detector.normal) + point2 = Pv + vec_a * ratio + + out = PositionDirectionVector(3) + out.position = point1 + out.direction = point2 - point1 + return out + + + def calculate_centre_of_rotation(self): + """ + Calculates the position, on the detector, of the projection of the rotation axis in the detector coordinate system + + Note + ---- + - Origin is in the centre of the detector + - Axes directions are specified by detector.direction_x, detector.direction_y + - Units are the units of distance used to specify the component's positions + + Returns + ------- + Float + Offset position along the detector x_axis at y=0 + Float + Angle between the y_axis and the rotation axis projection, in radians + """ + rotate_axis_projection = self.rotation_axis_on_detector() + + p1 = rotate_axis_projection.position + p2 = p1 + rotate_axis_projection.direction + + #point1 and point2 are on the detector plane. need to return them in the detector coordinate system + dp1 = p1 - self.detector.position + x1 = self.detector.direction_x.dot(dp1) + y1 = self.detector.direction_y.dot(dp1) + dp2 = p2 - self.detector.position + x2 = self.detector.direction_x.dot(dp2) + y2 = self.detector.direction_y.dot(dp2) + + #y = m * x + c + #c = y1 - m * x1 + #when y is 0 + #x=-c/m + #x_y0 = -y1/m + x1 + offset_x_y0 = x1 -y1 * (x2 - x1)/(y2-y1) + + angle = math.atan2(x2 - x1, y2 - y1) + offset = offset_x_y0 + + return (offset, angle) + + def set_centre_of_rotation(self, offset, angle): + """ Configures the geometry to have the requested centre of rotation offset at the detector + """ + + #two points on the detector + x1 = offset + y1 = 0 + x2 = offset + math.tan(angle) + y2 = 1 + + #convert to 3d coordinates in system frame + p1 = self.detector.position + x1 * self.detector.direction_x + y1 * self.detector.direction_y + p2 = self.detector.position + x2 * self.detector.direction_x + y2 * self.detector.direction_y + + # find where vec p1 + t * ray dirn intersects plane defined by rotate axis (pos and dir) and det_x direction + + vector_pos=p1 + vec_dirn=self.ray.direction + plane_pos=self.rotation_axis.position + plane_normal = numpy.cross(self.detector.direction_x, self.rotation_axis.direction) + + + ratio = (plane_pos - vector_pos).dot(plane_normal) / vec_dirn.dot(plane_normal) + p1_on_plane = vector_pos + vec_dirn * ratio + + vector_pos=p2 + ratio = (plane_pos - vector_pos).dot(plane_normal) / vec_dirn.dot(plane_normal) + p2_on_plane = vector_pos + vec_dirn * ratio + + self.rotation_axis.position = p1_on_plane + self.rotation_axis.direction = p2_on_plane - p1_on_plane + + +class Cone2D(SystemConfiguration): + r'''This class creates the SystemConfiguration of a cone beam 2D tomographic system + + :param source_pos: A 2D vector describing the position of the source (x,y) + :type source_pos: list, tuple, ndarray + :param detector_pos: A 2D vector describing the position of the centre of the detector (x,y) + :type detector_pos: list, tuple, ndarray + :param detector_direction_x: A 2D vector describing the direction of the detector_x (x,y) + :type detector_direction_x: list, tuple, ndarray + :param rotation_axis_pos: A 2D vector describing the position of the axis of rotation (x,y) + :type rotation_axis_pos: list, tuple, ndarray + :param units: Label the units of distance used for the configuration + :type units: string + ''' + + def __init__ (self, source_pos, detector_pos, detector_direction_x, rotation_axis_pos, units='units'): + """Constructor method + """ + super(Cone2D, self).__init__(dof=2, geometry = 'cone', units=units) + + #source + self.source.position = source_pos + + #detector + self.detector.position = detector_pos + self.detector.direction_x = detector_direction_x + + #rotate axis + self.rotation_axis.position = rotation_axis_pos + + + def align_reference_frame(self, definition='cil'): + r'''Transforms and rotates the system to backend definitions + ''' + self.set_origin(self.rotation_axis.position) + + if definition=='cil': + rotation_matrix = SystemConfiguration.rotation_vec_to_y(self.detector.position - self.source.position) + elif definition=='tigre': + rotation_matrix = SystemConfiguration.rotation_vec_to_y(self.rotation_axis.position - self.source.position) + else: + raise ValueError("Geometry can be configured for definition = 'cil' or 'tigre' only. Got {}".format(definition)) + + self.source.position = rotation_matrix.dot(self.source.position.reshape(2,1)) + self.detector.position = rotation_matrix.dot(self.detector.position.reshape(2,1)) + self.detector.direction_x = rotation_matrix.dot(self.detector.direction_x.reshape(2,1)) + + + def system_description(self): + r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, + \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets + \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets + ''' + + vec_src2det = ComponentDescription.create_unit_vector(self.detector.position - self.source.position) + + principal_ray_centred = ComponentDescription.test_parallel(vec_src2det, self.detector.normal) + + #rotation axis to detector is parallel with centre ray + if numpy.allclose(self.rotation_axis.position, self.detector.position): #points are equal + rotation_axis_centred = True + else: + vec_b = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position ) + rotation_axis_centred = ComponentDescription.test_parallel(vec_src2det, vec_b) + + if not principal_ray_centred: + config = SystemConfiguration.SYSTEM_ADVANCED + elif not rotation_axis_centred: + config = SystemConfiguration.SYSTEM_OFFSET + else: + config = SystemConfiguration.SYSTEM_SIMPLE + + return config + + def __str__(self): + def csv(val): + return numpy.array2string(val, separator=', ') + + repres = "2D Cone-beam tomography\n" + repres += "System configuration:\n" + repres += "\tSource position: {0}\n".format(csv(self.source.position)) + repres += "\tRotation axis position: {0}\n".format(csv(self.rotation_axis.position)) + repres += "\tDetector position: {0}\n".format(csv(self.detector.position)) + repres += "\tDetector direction x: {0}\n".format(csv(self.detector.direction_x)) + return repres + + def __eq__(self, other): + + if not isinstance(other, self.__class__): + return False + + if numpy.allclose(self.source.position, other.source.position) \ + and numpy.allclose(self.detector.position, other.detector.position)\ + and numpy.allclose(self.detector.direction_x, other.detector.direction_x)\ + and numpy.allclose(self.rotation_axis.position, other.rotation_axis.position): + return True + + return False + + def get_centre_slice(self): + return self + + def calculate_magnification(self): + + ab = (self.rotation_axis.position - self.source.position) + dist_source_center = float(numpy.sqrt(ab.dot(ab))) + + ab_unit = ab / numpy.sqrt(ab.dot(ab)) + + n = self.detector.normal + + #perpendicular distance between source and detector centre + sd = float((self.detector.position - self.source.position).dot(n)) + ratio = float(ab_unit.dot(n)) + + source_to_detector = sd / ratio + dist_center_detector = source_to_detector - dist_source_center + magnification = (dist_center_detector + dist_source_center) / dist_source_center + + return [dist_source_center, dist_center_detector, magnification] + + def rotation_axis_on_detector(self): + """ + Calculates the position, on the detector, of the projection of the rotation axis in the world coordinate system + + Returns + ------- + PositionVector + Position in the 3D system + """ + #calculate the point the rotation axis intersects with the detector + vec_a = self.rotation_axis.position - self.source.position + + Pv = self.rotation_axis.position + ratio = (self.detector.position - Pv).dot(self.detector.normal) / vec_a.dot(self.detector.normal) + + out = PositionVector(2) + out.position = Pv + vec_a * ratio + + return out + + + def calculate_centre_of_rotation(self): + """ + Calculates the position, on the detector, of the projection of the rotation axis in the detector coordinate system + + Note + ---- + - Origin is in the centre of the detector + - Axes directions are specified by detector.direction_x, detector.direction_y + - Units are the units of distance used to specify the component's positions + + Returns + ------- + Float + Offset position along the detector x_axis at y=0 + Float + Angle between the y_axis and the rotation axis projection, in radians + """ + #convert to the detector coordinate system + dp1 = self.rotation_axis_on_detector().position - self.detector.position + offset = self.detector.direction_x.dot(dp1) + + return (offset, 0.0) + + def set_centre_of_rotation(self, offset): + """ Configures the geometry to have the requested centre of rotation offset at the detector + """ + offset_current = self.calculate_centre_of_rotation()[0] + offset_new = offset - offset_current + + cofr_shift = offset_new * self.detector.direction_x /self.calculate_magnification()[2] + self.rotation_axis.position =self.rotation_axis.position + cofr_shift + + +class Cone3D(SystemConfiguration): + r'''This class creates the SystemConfiguration of a cone beam 3D tomographic system + + :param source_pos: A 3D vector describing the position of the source (x,y,z) + :type source_pos: list, tuple, ndarray + :param detector_pos: A 3D vector describing the position of the centre of the detector (x,y,z) + :type detector_pos: list, tuple, ndarray + :param detector_direction_x: A 3D vector describing the direction of the detector_x (x,y,z) + :type detector_direction_x: list, tuple, ndarray + :param detector_direction_y: A 3D vector describing the direction of the detector_y (x,y,z) + :type detector_direction_y: list, tuple, ndarray + :param rotation_axis_pos: A 3D vector describing the position of the axis of rotation (x,y,z) + :type rotation_axis_pos: list, tuple, ndarray + :param rotation_axis_direction: A 3D vector describing the direction of the axis of rotation (x,y,z) + :type rotation_axis_direction: list, tuple, ndarray + :param units: Label the units of distance used for the configuration + :type units: string + ''' + + def __init__ (self, source_pos, detector_pos, detector_direction_x, detector_direction_y, rotation_axis_pos, rotation_axis_direction, units='units'): + """Constructor method + """ + super(Cone3D, self).__init__(dof=3, geometry = 'cone', units=units) + + #source + self.source.position = source_pos + + #detector + self.detector.position = detector_pos + self.detector.set_direction(detector_direction_x, detector_direction_y) + + #rotate axis + self.rotation_axis.position = rotation_axis_pos + self.rotation_axis.direction = rotation_axis_direction + + def align_z(self): + r'''Transforms the system origin to the rotate axis with z direction aligned to the rotate axis direction + ''' + self.set_origin(self.rotation_axis.position) + rotation_matrix = SystemConfiguration.rotation_vec_to_z(self.rotation_axis.direction) + + #apply transform + self.rotation_axis.direction = [0,0,1] + self.source.position = rotation_matrix.dot(self.source.position.reshape(3,1)) + self.detector.position = rotation_matrix.dot(self.detector.position.reshape(3,1)) + new_x = rotation_matrix.dot(self.detector.direction_x.reshape(3,1)) + new_y = rotation_matrix.dot(self.detector.direction_y.reshape(3,1)) + self.detector.set_direction(new_x, new_y) + + + def align_reference_frame(self, definition='cil'): + r'''Transforms and rotates the system to backend definitions + ''' + + self.align_z() + + if definition=='cil': + rotation_matrix = SystemConfiguration.rotation_vec_to_y(self.detector.position - self.source.position) + elif definition=='tigre': + rotation_matrix = SystemConfiguration.rotation_vec_to_y(self.rotation_axis.position - self.source.position) + else: + raise ValueError("Geometry can be configured for definition = 'cil' or 'tigre' only. Got {}".format(definition)) + + self.source.position = rotation_matrix.dot(self.source.position.reshape(3,1)) + self.detector.position = rotation_matrix.dot(self.detector.position.reshape(3,1)) + new_direction_x = rotation_matrix.dot(self.detector.direction_x.reshape(3,1)) + new_direction_y = rotation_matrix.dot(self.detector.direction_y.reshape(3,1)) + self.detector.set_direction(new_direction_x, new_direction_y) + + + def system_description(self): + r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, + \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets + \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets + ''' + + vec_src2det = ComponentDescription.create_unit_vector(self.detector.position - self.source.position) + + principal_ray_centred = ComponentDescription.test_parallel(vec_src2det, self.detector.normal) + centre_ray_perpendicular_rotation = ComponentDescription.test_perpendicular(vec_src2det, self.rotation_axis.direction) + rotation_parallel_detector_y = ComponentDescription.test_parallel(self.rotation_axis.direction, self.detector.direction_y) + + #rotation axis to detector is parallel with centre ray + if numpy.allclose(self.rotation_axis.position, self.detector.position): #points are equal + rotation_axis_centred = True + else: + vec_b = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position ) + rotation_axis_centred = ComponentDescription.test_parallel(vec_src2det, vec_b) + + if not principal_ray_centred or\ + not centre_ray_perpendicular_rotation or\ + not rotation_parallel_detector_y: + config = SystemConfiguration.SYSTEM_ADVANCED + elif not rotation_axis_centred: + config = SystemConfiguration.SYSTEM_OFFSET + else: + config = SystemConfiguration.SYSTEM_SIMPLE + + return config + + def get_centre_slice(self): + """Returns the 2D system configuration corresponding to the centre slice + """ + #requires the rotate axis to be perpendicular to the normal of the detector, and perpendicular to detector_direction_x + dp1 = self.rotation_axis.direction.dot(self.detector.normal) + dp2 = self.rotation_axis.direction.dot(self.detector.direction_x) + + if numpy.isclose(dp1, 0) and numpy.isclose(dp2, 0): + temp = self.copy() + temp.align_reference_frame() + source_position = temp.source.position[0:2] + detector_position = temp.detector.position[0:2] + detector_direction_x = temp.detector.direction_x[0:2] + rotation_axis_position = temp.rotation_axis.position[0:2] + + return Cone2D(source_position, detector_position, detector_direction_x, rotation_axis_position) + else: + raise ValueError('Cannot convert geometry to 2D. Requires axis of rotation to be perpendicular to the detector.') + + def __str__(self): + def csv(val): + return numpy.array2string(val, separator=', ') + + repres = "3D Cone-beam tomography\n" + repres += "System configuration:\n" + repres += "\tSource position: {0}\n".format(csv(self.source.position)) + repres += "\tRotation axis position: {0}\n".format(csv(self.rotation_axis.position)) + repres += "\tRotation axis direction: {0}\n".format(csv(self.rotation_axis.direction)) + repres += "\tDetector position: {0}\n".format(csv(self.detector.position)) + repres += "\tDetector direction x: {0}\n".format(csv(self.detector.direction_x)) + repres += "\tDetector direction y: {0}\n".format(csv(self.detector.direction_y)) + return repres + + def __eq__(self, other): + + if not isinstance(other, self.__class__): + return False + + if numpy.allclose(self.source.position, other.source.position) \ + and numpy.allclose(self.detector.position, other.detector.position)\ + and numpy.allclose(self.detector.direction_x, other.detector.direction_x)\ + and numpy.allclose(self.detector.direction_y, other.detector.direction_y)\ + and numpy.allclose(self.rotation_axis.position, other.rotation_axis.position)\ + and numpy.allclose(self.rotation_axis.direction, other.rotation_axis.direction): + + return True + + return False + + def calculate_magnification(self): + + ab = (self.rotation_axis.position - self.source.position) + dist_source_center = float(numpy.sqrt(ab.dot(ab))) + + ab_unit = ab / numpy.sqrt(ab.dot(ab)) + + n = self.detector.normal + + #perpendicular distance between source and detector centre + sd = float((self.detector.position - self.source.position).dot(n)) + ratio = float(ab_unit.dot(n)) + + source_to_detector = sd / ratio + dist_center_detector = source_to_detector - dist_source_center + magnification = (dist_center_detector + dist_source_center) / dist_source_center + + return [dist_source_center, dist_center_detector, magnification] + + def rotation_axis_on_detector(self): + """ + Calculates the position, on the detector, of the projection of the rotation axis in the world coordinate system + + Returns + ------- + PositionDirectionVector + Position and direction in the 3D system + """ + #calculate the intersection with the detector, of source to pv + Pv = self.rotation_axis.position + vec_a = Pv - self.source.position + ratio = (self.detector.position - Pv).dot(self.detector.normal) / vec_a.dot(self.detector.normal) + point1 = Pv + vec_a * ratio + + #calculate the intersection with the detector, of source to pv + Pv = self.rotation_axis.position + self.rotation_axis.direction + vec_a = Pv - self.source.position + ratio = (self.detector.position - Pv).dot(self.detector.normal) / vec_a.dot(self.detector.normal) + point2 = Pv + vec_a * ratio + + out = PositionDirectionVector(3) + out.position = point1 + out.direction = point2 - point1 + return out + + def calculate_centre_of_rotation(self): + """ + Calculates the position, on the detector, of the projection of the rotation axis in the detector coordinate system + + Note + ---- + - Origin is in the centre of the detector + - Axes directions are specified by detector.direction_x, detector.direction_y + - Units are the units of distance used to specify the component's positions + + Returns + ------- + Float + Offset position along the detector x_axis at y=0 + Float + Angle between the y_axis and the rotation axis projection, in radians + """ + rotate_axis_projection = self.rotation_axis_on_detector() + + p1 = rotate_axis_projection.position + p2 = p1 + rotate_axis_projection.direction + + #point1 and point2 are on the detector plane. need to return them in the detector coordinate system + dp1 = p1 - self.detector.position + x1 = self.detector.direction_x.dot(dp1) + y1 = self.detector.direction_y.dot(dp1) + dp2 = p2 - self.detector.position + x2 = self.detector.direction_x.dot(dp2) + y2 = self.detector.direction_y.dot(dp2) + + #y = m * x + c + #c = y1 - m * x1 + #when y is 0 + #x=-c/m + #x_y0 = -y1/m + x1 + offset_x_y0 = x1 -y1 * (x2 - x1)/(y2-y1) + + angle = math.atan2(x2 - x1, y2 - y1) + offset = offset_x_y0 + + return (offset, angle) + + + def set_centre_of_rotation(self, offset, angle): + """ Configures the geometry to have the requested centre of rotation offset at the detector + """ + #two points on the detector + x1 = offset + y1 = 0 + x2 = offset + math.tan(angle) + y2 = 1 + + #convert to 3d coordinates in system frame + p1 = self.detector.position + x1 * self.detector.direction_x + y1 * self.detector.direction_y + p2 = self.detector.position + x2 * self.detector.direction_x + y2 * self.detector.direction_y + + # vectors from source define plane + sp1 = p1 - self.source.position + sp2 = p2 - self.source.position + + #find vector intersection with a plane defined by rotate axis (pos and dir) and det_x direction + plane_normal = numpy.cross(self.rotation_axis.direction, self.detector.direction_x) + + ratio = (self.rotation_axis.position - self.source.position).dot(plane_normal) / sp1.dot(plane_normal) + p1_on_plane = self.source.position + sp1 * ratio + + ratio = (self.rotation_axis.position - self.source.position).dot(plane_normal) / sp2.dot(plane_normal) + p2_on_plane = self.source.position + sp2 * ratio + + self.rotation_axis.position = p1_on_plane + self.rotation_axis.direction = p2_on_plane - p1_on_plane + + +class Panel(object): + r'''This is a class describing the panel of the system. + + :param num_pixels: num_pixels_h or (num_pixels_h, num_pixels_v) containing the number of pixels of the panel + :type num_pixels: int, list, tuple + :param pixel_size: pixel_size_h or (pixel_size_h, pixel_size_v) containing the size of the pixels of the panel + :type pixel_size: int, lust, tuple + :param origin: the position of pixel 0 (the data origin) of the panel `top-left`, `top-right`, `bottom-left`, `bottom-right` + :type origin: string + ''' + + @property + def num_pixels(self): + return self._num_pixels + + @num_pixels.setter + def num_pixels(self, val): + + if isinstance(val,int): + num_pixels_temp = [val, 1] + else: + try: + length_val = len(val) + except: + raise TypeError('num_pixels expected int x or [int x, int y]. Got {}'.format(type(val))) + + + if length_val == 2: + try: + val0 = int(val[0]) + val1 = int(val[1]) + except: + raise TypeError('num_pixels expected int x or [int x, int y]. Got {0},{1}'.format(type(val[0]), type(val[1]))) + + num_pixels_temp = [val0, val1] + else: + raise ValueError('num_pixels expected int x or [int x, int y]. Got {}'.format(val)) + + if num_pixels_temp[1] > 1 and self._dimension == 2: + raise ValueError('2D acquisitions expects a 1D panel. Expected num_pixels[1] = 1. Got {}'.format(num_pixels_temp[1])) + if num_pixels_temp[0] < 1 or num_pixels_temp[1] < 1: + raise ValueError('num_pixels (x,y) must be >= (1,1). Got {}'.format(num_pixels_temp)) + else: + self._num_pixels = numpy.array(num_pixels_temp, dtype=numpy.int16) + + @property + def pixel_size(self): + return self._pixel_size + + @pixel_size.setter + def pixel_size(self, val): + + if val is None: + pixel_size_temp = [1.0,1.0] + else: + try: + length_val = len(val) + except: + try: + temp = float(val) + pixel_size_temp = [temp, temp] + + except: + raise TypeError('pixel_size expected float xy or [float x, float y]. Got {}'.format(val)) + else: + if length_val == 2: + try: + temp0 = float(val[0]) + temp1 = float(val[1]) + pixel_size_temp = [temp0, temp1] + except: + raise ValueError('pixel_size expected float xy or [float x, float y]. Got {}'.format(val)) + else: + raise ValueError('pixel_size expected float xy or [float x, float y]. Got {}'.format(val)) + + if pixel_size_temp[0] <= 0 or pixel_size_temp[1] <= 0: + raise ValueError('pixel_size (x,y) at must be > (0.,0.). Got {}'.format(pixel_size_temp)) + + self._pixel_size = numpy.array(pixel_size_temp) + + @property + def origin(self): + return self._origin + + @origin.setter + def origin(self, val): + allowed = ['top-left', 'top-right','bottom-left','bottom-right'] + if val in allowed: + self._origin=val + else: + raise ValueError('origin expected one of {0}. Got {1}'.format(allowed, val)) + + def __str__(self): + repres = "Panel configuration:\n" + repres += "\tNumber of pixels: {0}\n".format(self.num_pixels) + repres += "\tPixel size: {0}\n".format(self.pixel_size) + repres += "\tPixel origin: {0}\n".format(self.origin) + return repres + + def __eq__(self, other): + + if not isinstance(other, self.__class__): + return False + + if numpy.array_equal(self.num_pixels, other.num_pixels) \ + and numpy.allclose(self.pixel_size, other.pixel_size) \ + and self.origin == other.origin: + return True + + return False + + def __init__ (self, num_pixels, pixel_size, origin, dimension): + """Constructor method + """ + self._dimension = dimension + self.num_pixels = num_pixels + self.pixel_size = pixel_size + self.origin = origin + + +class Channels(object): + r'''This is a class describing the channels of the data. + This will be created on initialisation of AcquisitionGeometry. + + :param num_channels: The number of channels of data + :type num_channels: int + :param channel_labels: A list of channel labels + :type channel_labels: list, optional + ''' + + @property + def num_channels(self): + return self._num_channels + + @num_channels.setter + def num_channels(self, val): + try: + val = int(val) + except TypeError: + raise ValueError('num_channels expected a positive integer. Got {}'.format(type(val))) + + if val > 0: + self._num_channels = val + else: + raise ValueError('num_channels expected a positive integer. Got {}'.format(val)) + + @property + def channel_labels(self): + return self._channel_labels + + @channel_labels.setter + def channel_labels(self, val): + if val is None or len(val) == self._num_channels: + self._channel_labels = val + else: + raise ValueError('labels expected to have length {0}. Got {1}'.format(self._num_channels, len(val))) + + def __str__(self): + repres = "Channel configuration:\n" + repres += "\tNumber of channels: {0}\n".format(self.num_channels) + + num_print=min(10,self.num_channels) + if hasattr(self, 'channel_labels'): + repres += "\tChannel labels 0-{0}: {1}\n".format(num_print, self.channel_labels[0:num_print]) + + return repres + + def __eq__(self, other): + + if not isinstance(other, self.__class__): + return False + + if self.num_channels != other.num_channels: + return False + + if hasattr(self,'channel_labels'): + if self.channel_labels != other.channel_labels: + return False + + return True + + def __init__ (self, num_channels, channel_labels): + """Constructor method + """ + self.num_channels = num_channels + if channel_labels is not None: + self.channel_labels = channel_labels + + +class Angles(object): + r'''This is a class describing the angles of the data. + + :param angles: The angular positions of the acquisition data + :type angles: list, ndarray + :param initial_angle: The angular offset of the object from the reference frame + :type initial_angle: float, optional + :param angle_unit: The units of the stored angles 'degree' or 'radian' + :type angle_unit: string + ''' + + @property + def angle_data(self): + return self._angle_data + + @angle_data.setter + def angle_data(self, val): + if val is None: + raise ValueError('angle_data expected to be a list of floats') + else: + try: + self.num_positions = len(val) + + except TypeError: + self.num_positions = 1 + val = [val] + + finally: + try: + self._angle_data = numpy.asarray(val, dtype=numpy.float32) + except: + raise ValueError('angle_data expected to be a list of floats') + + @property + def initial_angle(self): + return self._initial_angle + + @initial_angle.setter + def initial_angle(self, val): + try: + val = float(val) + except: + raise TypeError('initial_angle expected a float. Got {0}'.format(type(val))) + + self._initial_angle = val + + @property + def angle_unit(self): + return self._angle_unit + + @angle_unit.setter + def angle_unit(self,val): + if val != acquisition_labels["DEGREE"] and val != acquisition_labels["RADIAN"]: + raise ValueError('angle_unit = {} not recognised please specify \'degree\' or \'radian\''.format(val)) + else: + self._angle_unit = val + + def __str__(self): + repres = "Acquisition description:\n" + repres += "\tNumber of positions: {0}\n".format(self.num_positions) + # max_num_print = 30 + if self.num_positions < 31: + repres += "\tAngles 0-{0} in {1}s: {2}\n".format(self.num_positions-1, self.angle_unit, numpy.array2string(self.angle_data[0:self.num_positions], separator=', ')) + else: + repres += "\tAngles 0-9 in {0}s: {1}\n".format(self.angle_unit, numpy.array2string(self.angle_data[0:10], separator=', ')) + repres += "\tAngles {0}-{1} in {2}s: {3}\n".format(self.num_positions-10, self.num_positions-1, self.angle_unit, numpy.array2string(self.angle_data[self.num_positions-10:self.num_positions], separator=', ')) + repres += "\tFull angular array can be accessed with acquisition_data.geometry.angles\n" + return repres + + def __eq__(self, other): + + if not isinstance(other, self.__class__): + return False + + if self.angle_unit != other.angle_unit: + return False + + if self.initial_angle != other.initial_angle: + return False + + if not numpy.allclose(self.angle_data, other.angle_data): + return False + + return True + + def __init__ (self, angles, initial_angle, angle_unit): + """Constructor method + """ + self.angle_data = angles + self.initial_angle = initial_angle + self.angle_unit = angle_unit + + +class Configuration(object): + r'''This class holds the description of the system components. + ''' + + def __init__(self, units_distance='units distance'): + self.system = None #has distances + self.angles = None #has angles + self.panel = None #has distances + self.channels = Channels(1, None) + self.units = units_distance + + @property + def configured(self): + if self.system is None: + print("Please configure AcquisitionGeometry using one of the following methods:\ + \n\tAcquisitionGeometry.create_Parallel2D()\ + \n\tAcquisitionGeometry.create_Cone3D()\ + \n\tAcquisitionGeometry.create_Parallel2D()\ + \n\tAcquisitionGeometry.create_Cone3D()") + return False + + configured = True + if self.angles is None: + print("Please configure angular data using the set_angles() method") + configured = False + if self.panel is None: + print("Please configure the panel using the set_panel() method") + configured = False + return configured + + def shift_detector_in_plane(self, + pixel_offset, + direction='horizontal'): + """ + Adjusts the position of the detector in a specified direction within the imaging plane. + + Parameters: + ----------- + pixel_offset : float + The number of pixels to adjust the detector's position by. + direction : {'horizontal', 'vertical'}, optional + The direction in which to adjust the detector's position. Defaults to 'horizontal'. + + Notes: + ------ + - If `direction` is 'horizontal': + - If the panel's origin is 'left', positive offsets translate the detector to the right. + - If the panel's origin is 'right', positive offsets translate the detector to the left. + + - If `direction` is 'vertical': + - If the panel's origin is 'bottom', positive offsets translate the detector upward. + - If the panel's origin is 'top', positive offsets translate the detector downward. + + Returns: + -------- + None + """ + + if direction == 'horizontal': + pixel_size = self.panel.pixel_size[0] + pixel_direction = self.system.detector.direction_x + + elif direction == 'vertical': + pixel_size = self.panel.pixel_size[1] + pixel_direction = self.system.detector.direction_y + + if 'bottom' in self.panel.origin or 'left' in self.panel.origin: + self.system.detector.position -= pixel_offset * pixel_direction * pixel_size + else: + self.system.detector.position += pixel_offset * pixel_direction * pixel_size + + + def __str__(self): + repres = "" + if self.configured: + repres += str(self.system) + repres += str(self.panel) + repres += str(self.channels) + repres += str(self.angles) + + repres += "Distances in units: {}".format(self.units) + + return repres + + def __eq__(self, other): + + if not isinstance(other, self.__class__): + return False + + if self.system == other.system\ + and self.panel == other.panel\ + and self.channels == other.channels\ + and self.angles == other.angles: + return True + + return False + + +class AcquisitionGeometry(BaseAcquisitionGeometry): + """This class holds the AcquisitionGeometry of the system. + + Please initialise the AcquisitionGeometry using the using the static methods: + + `AcquisitionGeometry.create_Parallel2D()` + + `AcquisitionGeometry.create_Cone2D()` + + `AcquisitionGeometry.create_Parallel3D()` + + `AcquisitionGeometry.create_Cone3D()` + """ + + + #for backwards compatibility + @property + def geom_type(self): + return self.config.system.geometry + + @property + def num_projections(self): + return len(self.angles) + + @property + def pixel_num_h(self): + return self.config.panel.num_pixels[0] + + @pixel_num_h.setter + def pixel_num_h(self, val): + self.config.panel.num_pixels[0] = val + + @property + def pixel_num_v(self): + return self.config.panel.num_pixels[1] + + @pixel_num_v.setter + def pixel_num_v(self, val): + self.config.panel.num_pixels[1] = val + + @property + def pixel_size_h(self): + return self.config.panel.pixel_size[0] + + @pixel_size_h.setter + def pixel_size_h(self, val): + self.config.panel.pixel_size[0] = val + + @property + def pixel_size_v(self): + return self.config.panel.pixel_size[1] + + @pixel_size_v.setter + def pixel_size_v(self, val): + self.config.panel.pixel_size[1] = val + + @property + def channels(self): + return self.config.channels.num_channels + + @property + def angles(self): + return self.config.angles.angle_data + + @property + def dist_source_center(self): + out = self.config.system.calculate_magnification() + return out[0] + + @property + def dist_center_detector(self): + out = self.config.system.calculate_magnification() + return out[1] + + @property + def magnification(self): + out = self.config.system.calculate_magnification() + return out[2] + + @property + def dimension(self): + return self.config.system.dimension + + @property + def shape(self): + + shape_dict = {acquisition_labels["CHANNEL"]: self.config.channels.num_channels, + acquisition_labels["ANGLE"]: self.config.angles.num_positions, + acquisition_labels["VERTICAL"]: self.config.panel.num_pixels[1], + acquisition_labels["HORIZONTAL"]: self.config.panel.num_pixels[0]} + shape = [] + for label in self.dimension_labels: + shape.append(shape_dict[label]) + + return tuple(shape) + + @property + def dimension_labels(self): + labels_default = data_order["CIL_AG_LABELS"] + + shape_default = [self.config.channels.num_channels, + self.config.angles.num_positions, + self.config.panel.num_pixels[1], + self.config.panel.num_pixels[0] + ] + + try: + labels = list(self._dimension_labels) + except AttributeError: + labels = labels_default.copy() + + #remove from list labels where len == 1 + # + for i, x in enumerate(shape_default): + if x == 0 or x==1: + try: + labels.remove(labels_default[i]) + except ValueError: + pass #if not in custom list carry on + + return tuple(labels) + + @dimension_labels.setter + def dimension_labels(self, val): + + labels_default = data_order["CIL_AG_LABELS"] + + #check input and store. This value is not used directly + if val is not None: + for x in val: + if x not in labels_default: + raise ValueError('Requested axis are not possible. Accepted label names {},\ngot {}'.format(labels_default,val)) + + self._dimension_labels = tuple(val) + + @property + def ndim(self): + return len(self.dimension_labels) + + @property + def system_description(self): + return self.config.system.system_description() + + @property + def dtype(self): + return self._dtype + + @dtype.setter + def dtype(self, val): + self._dtype = val + + + def __init__(self): + self._dtype = numpy.float32 + + + def get_centre_of_rotation(self, distance_units='default', angle_units='radian'): + """ + Returns the system centre of rotation offset at the detector + + Note + ---- + - Origin is in the centre of the detector + - Axes directions are specified by detector.direction_x, detector.direction_y + + Parameters + ---------- + distance_units : string, default='default' + Units of distance used to calculate the return values. + 'default' uses the same units the system and panel were specified in. + 'pixels' uses pixels sizes in the horizontal and vertical directions as appropriate. + angle_units : string + Units to return the angle in. Can take 'radian' or 'degree'. + + Returns + ------- + Dictionary + {'offset': (offset, distance_units), 'angle': (angle, angle_units)} + where, + 'offset' gives the position along the detector x_axis at y=0 + 'angle' gives the angle between the y_axis and the projection of the rotation axis on the detector + """ + + if hasattr(self.config.system, 'calculate_centre_of_rotation'): + offset_distance, angle_rad = self.config.system.calculate_centre_of_rotation() + else: + raise NotImplementedError + + if distance_units == 'default': + offset = offset_distance + offset_units = self.config.units + elif distance_units == 'pixels': + + offset = offset_distance/ self.config.panel.pixel_size[0] + offset_units = 'pixels' + + if self.dimension == '3D' and self.config.panel.pixel_size[0] != self.config.panel.pixel_size[1]: + #if aspect ratio of pixels isn't 1:1 need to convert angle by new ratio + y_pix = 1 /self.config.panel.pixel_size[1] + x_pix = math.tan(angle_rad)/self.config.panel.pixel_size[0] + angle_rad = math.atan2(x_pix,y_pix) + else: + raise ValueError("`distance_units` is not recognised. Must be 'default' or 'pixels'. Got {}".format(distance_units)) + + if angle_units == 'radian': + angle = angle_rad + ang_units = 'radian' + elif angle_units == 'degree': + angle = numpy.degrees(angle_rad) + ang_units = 'degree' + else: + raise ValueError("`angle_units` is not recognised. Must be 'radian' or 'degree'. Got {}".format(angle_units)) + + return {'offset': (offset, offset_units), 'angle': (angle, ang_units)} + + + def set_centre_of_rotation(self, offset=0.0, distance_units='default', angle=0.0, angle_units='radian'): + """ + Configures the system geometry to have the requested centre of rotation offset at the detector. + + Note + ---- + - Origin is in the centre of the detector + - Axes directions are specified by detector.direction_x, detector.direction_y + + Parameters + ---------- + offset: float, default 0.0 + The position of the centre of rotation along the detector x_axis at y=0 + + distance_units : string, default='default' + Units the offset is specified in. Can be 'default'or 'pixels'. + 'default' interprets the input as same units the system and panel were specified in. + 'pixels' interprets the input in horizontal pixels. + + angle: float, default=0.0 + The angle between the detector y_axis and the rotation axis direction on the detector + + Notes + ----- + If aspect ratio of pixels is not 1:1 ensure the angle is calculated from the x and y values in the correct units. + + angle_units : string, default='radian' + Units the angle is specified in. Can take 'radian' or 'degree'. + + """ + + if not hasattr(self.config.system, 'set_centre_of_rotation'): + raise NotImplementedError() + + if angle_units == 'radian': + angle_rad = angle + elif angle_units == 'degree': + angle_rad = numpy.radians(angle) + else: + raise ValueError("`angle_units` is not recognised. Must be 'radian' or 'degree'. Got {}".format(angle_units)) + + if distance_units =='default': + offset_distance = offset + elif distance_units =='pixels': + offset_distance = offset * self.config.panel.pixel_size[0] + else: + raise ValueError("`distance_units` is not recognised. Must be 'default' or 'pixels'. Got {}".format(distance_units)) + + if self.dimension == '2D': + self.config.system.set_centre_of_rotation(offset_distance) + else: + self.config.system.set_centre_of_rotation(offset_distance, angle_rad) + + + def set_centre_of_rotation_by_slice(self, offset1, slice_index1=None, offset2=None, slice_index2=None): + """ + Configures the system geometry to have the requested centre of rotation offset at the detector. + + If two slices are passed the rotation axis will be rotated to pass through both points. + + Note + ---- + - Offset is specified in pixels + - Offset can be sub-pixels + - Offset direction is specified by detector.direction_x + + Parameters + ---------- + offset1: float + The offset from the centre of the detector to the projected rotation position at slice_index_1 + + slice_index1: int, optional + The slice number of offset1 + + offset2: float, optional + The offset from the centre of the detector to the projected rotation position at slice_index_2 + + slice_index2: int, optional + The slice number of offset2 + """ + + + if not hasattr(self.config.system, 'set_centre_of_rotation'): + raise NotImplementedError() + + if self.dimension == '2D': + if offset2 is not None: + logging.WARNING("Only offset1 is being used") + self.set_centre_of_rotation(offset1) + + if offset2 is None or offset1 == offset2: + offset_x_y0 = offset1 + angle = 0 + else: + if slice_index1 is None or slice_index2 is None or slice_index1 == slice_index2: + raise ValueError("Cannot calculate angle. Please specify `slice_index1` and `slice_index2` to define a rotated axis") + + offset_x_y0 = offset1 -slice_index1 * (offset2 - offset1)/(slice_index2-slice_index1) + angle = math.atan2(offset2 - offset1, slice_index2 - slice_index1) + + self.set_centre_of_rotation(offset_x_y0, 'pixels', angle, 'radian') + + + def set_angles(self, angles, initial_angle=0, angle_unit='degree'): + r'''This method configures the angular information of an AcquisitionGeometry object. + + :param angles: The angular positions of the acquisition data + :type angles: list, ndarray + :param initial_angle: The angular offset of the object from the reference frame + :type initial_angle: float, optional + :param angle_unit: The units of the stored angles 'degree' or 'radian' + :type angle_unit: string + :return: returns a configured AcquisitionGeometry object + :rtype: AcquisitionGeometry + ''' + self.config.angles = Angles(angles, initial_angle, angle_unit) + return self + + def set_panel(self, num_pixels, pixel_size=(1,1), origin='bottom-left'): + + r'''This method configures the panel information of an AcquisitionGeometry object. + + :param num_pixels: num_pixels_h or (num_pixels_h, num_pixels_v) containing the number of pixels of the panel + :type num_pixels: int, list, tuple + :param pixel_size: pixel_size_h or (pixel_size_h, pixel_size_v) containing the size of the pixels of the panel + :type pixel_size: int, list, tuple, optional + :param origin: the position of pixel 0 (the data origin) of the panel 'top-left', 'top-right', 'bottom-left', 'bottom-right' + :type origin: string, default 'bottom-left' + :return: returns a configured AcquisitionGeometry object + :rtype: AcquisitionGeometry + ''' + self.config.panel = Panel(num_pixels, pixel_size, origin, self.config.system._dimension) + return self + + def set_channels(self, num_channels=1, channel_labels=None): + r'''This method configures the channel information of an AcquisitionGeometry object. + + :param num_channels: The number of channels of data + :type num_channels: int, optional + :param channel_labels: A list of channel labels + :type channel_labels: list, optional + :return: returns a configured AcquisitionGeometry object + :rtype: AcquisitionGeometry + ''' + self.config.channels = Channels(num_channels, channel_labels) + return self + + def set_labels(self, labels=None): + r'''This method configures the dimension labels of an AcquisitionGeometry object. + + :param labels: The order of the dimensions describing the data.\ + Expects a list containing at least one of the unique labels: 'channel' 'angle' 'vertical' 'horizontal' + default = ['channel','angle','vertical','horizontal'] + :type labels: list, optional + :return: returns a configured AcquisitionGeometry object + :rtype: AcquisitionGeometry + ''' + self.dimension_labels = labels + return self + + @staticmethod + def create_Parallel2D(ray_direction=[0, 1], detector_position=[0, 0], detector_direction_x=[1, 0], rotation_axis_position=[0, 0], units='units distance'): + r'''This creates the AcquisitionGeometry for a parallel beam 2D tomographic system + + :param ray_direction: A 2D vector describing the x-ray direction (x,y) + :type ray_direction: list, tuple, ndarray, optional + :param detector_position: A 2D vector describing the position of the centre of the detector (x,y) + :type detector_position: list, tuple, ndarray, optional + :param detector_direction_x: A 2D vector describing the direction of the detector_x (x,y) + :type detector_direction_x: list, tuple, ndarray + :param rotation_axis_position: A 2D vector describing the position of the axis of rotation (x,y) + :type rotation_axis_position: list, tuple, ndarray, optional + :param units: Label the units of distance used for the configuration, these should be consistent for the geometry and panel + :type units: string + :return: returns a configured AcquisitionGeometry object + :rtype: AcquisitionGeometry + ''' + AG = AcquisitionGeometry() + AG.config = Configuration(units) + AG.config.system = Parallel2D(ray_direction, detector_position, detector_direction_x, rotation_axis_position, units) + return AG + + @staticmethod + def create_Cone2D(source_position, detector_position, detector_direction_x=[1,0], rotation_axis_position=[0,0], units='units distance'): + r'''This creates the AcquisitionGeometry for a cone beam 2D tomographic system + + :param source_position: A 2D vector describing the position of the source (x,y) + :type source_position: list, tuple, ndarray + :param detector_position: A 2D vector describing the position of the centre of the detector (x,y) + :type detector_position: list, tuple, ndarray + :param detector_direction_x: A 2D vector describing the direction of the detector_x (x,y) + :type detector_direction_x: list, tuple, ndarray + :param rotation_axis_position: A 2D vector describing the position of the axis of rotation (x,y) + :type rotation_axis_position: list, tuple, ndarray, optional + :param units: Label the units of distance used for the configuration, these should be consistent for the geometry and panel + :type units: string + :return: returns a configured AcquisitionGeometry object + :rtype: AcquisitionGeometry + ''' + AG = AcquisitionGeometry() + AG.config = Configuration(units) + AG.config.system = Cone2D(source_position, detector_position, detector_direction_x, rotation_axis_position, units) + return AG + + @staticmethod + def create_Parallel3D(ray_direction=[0,1,0], detector_position=[0,0,0], detector_direction_x=[1,0,0], detector_direction_y=[0,0,1], rotation_axis_position=[0,0,0], rotation_axis_direction=[0,0,1], units='units distance'): + r'''This creates the AcquisitionGeometry for a parallel beam 3D tomographic system + + :param ray_direction: A 3D vector describing the x-ray direction (x,y,z) + :type ray_direction: list, tuple, ndarray, optional + :param detector_position: A 3D vector describing the position of the centre of the detector (x,y,z) + :type detector_position: list, tuple, ndarray, optional + :param detector_direction_x: A 3D vector describing the direction of the detector_x (x,y,z) + :type detector_direction_x: list, tuple, ndarray + :param detector_direction_y: A 3D vector describing the direction of the detector_y (x,y,z) + :type detector_direction_y: list, tuple, ndarray + :param rotation_axis_position: A 3D vector describing the position of the axis of rotation (x,y,z) + :type rotation_axis_position: list, tuple, ndarray, optional + :param rotation_axis_direction: A 3D vector describing the direction of the axis of rotation (x,y,z) + :type rotation_axis_direction: list, tuple, ndarray, optional + :param units: Label the units of distance used for the configuration, these should be consistent for the geometry and panel + :type units: string + :return: returns a configured AcquisitionGeometry object + :rtype: AcquisitionGeometry + ''' + AG = AcquisitionGeometry() + AG.config = Configuration(units) + AG.config.system = Parallel3D(ray_direction, detector_position, detector_direction_x, detector_direction_y, rotation_axis_position, rotation_axis_direction, units) + return AG + + @staticmethod + def create_Cone3D(source_position, detector_position, detector_direction_x=[1,0,0], detector_direction_y=[0,0,1], rotation_axis_position=[0,0,0], rotation_axis_direction=[0,0,1], units='units distance'): + r'''This creates the AcquisitionGeometry for a cone beam 3D tomographic system + + :param source_position: A 3D vector describing the position of the source (x,y,z) + :type source_position: list, tuple, ndarray, optional + :param detector_position: A 3D vector describing the position of the centre of the detector (x,y,z) + :type detector_position: list, tuple, ndarray, optional + :param detector_direction_x: A 3D vector describing the direction of the detector_x (x,y,z) + :type detector_direction_x: list, tuple, ndarray + :param detector_direction_y: A 3D vector describing the direction of the detector_y (x,y,z) + :type detector_direction_y: list, tuple, ndarray + :param rotation_axis_position: A 3D vector describing the position of the axis of rotation (x,y,z) + :type rotation_axis_position: list, tuple, ndarray, optional + :param rotation_axis_direction: A 3D vector describing the direction of the axis of rotation (x,y,z) + :type rotation_axis_direction: list, tuple, ndarray, optional + :param units: Label the units of distance used for the configuration, these should be consistent for the geometry and panel + :type units: string + :return: returns a configured AcquisitionGeometry object + :rtype: AcquisitionGeometry + ''' + AG = AcquisitionGeometry() + AG.config = Configuration(units) + AG.config.system = Cone3D(source_position, detector_position, detector_direction_x, detector_direction_y, rotation_axis_position, rotation_axis_direction, units) + return AG + + def get_order_by_label(self, dimension_labels, default_dimension_labels): + order = [] + for i, el in enumerate(default_dimension_labels): + for j, ek in enumerate(dimension_labels): + if el == ek: + order.append(j) + break + return order + + def __eq__(self, other): + + if isinstance(other, self.__class__) and self.config == other.config : + return True + return False + + def clone(self): + '''returns a copy of the AcquisitionGeometry''' + return copy.deepcopy(self) + + def copy(self): + '''alias of clone''' + return self.clone() + + def get_centre_slice(self): + '''returns a 2D AcquisitionGeometry that corresponds to the centre slice of the input''' + + if self.dimension == '2D': + return self + + AG_2D = copy.deepcopy(self) + AG_2D.config.system = self.config.system.get_centre_slice() + AG_2D.config.panel.num_pixels[1] = 1 + AG_2D.config.panel.pixel_size[1] = abs(self.config.system.detector.direction_y[2]) * self.config.panel.pixel_size[1] + return AG_2D + + def get_ImageGeometry(self, resolution=1.0): + '''returns a default configured ImageGeometry object based on the AcquisitionGeomerty''' + + num_voxel_xy = int(numpy.ceil(self.config.panel.num_pixels[0] * resolution)) + voxel_size_xy = self.config.panel.pixel_size[0] / (resolution * self.magnification) + + if self.dimension == '3D': + num_voxel_z = int(numpy.ceil(self.config.panel.num_pixels[1] * resolution)) + voxel_size_z = self.config.panel.pixel_size[1] / (resolution * self.magnification) + else: + num_voxel_z = 0 + voxel_size_z = 1 + + return ImageGeometry(num_voxel_xy, num_voxel_xy, num_voxel_z, voxel_size_xy, voxel_size_xy, voxel_size_z, channels=self.channels) + + def __str__ (self): + return str(self.config) + + + def get_slice(self, channel=None, angle=None, vertical=None, horizontal=None): + ''' + Returns a new AcquisitionGeometry of a single slice of in the requested direction. Will only return reconstructable geometries. + ''' + geometry_new = self.copy() + + if channel is not None: + geometry_new.config.channels.num_channels = 1 + if hasattr(geometry_new.config.channels,'channel_labels'): + geometry_new.config.panel.channel_labels = geometry_new.config.panel.channel_labels[channel] + + if angle is not None: + geometry_new.config.angles.angle_data = geometry_new.config.angles.angle_data[angle] + + if vertical is not None: + if geometry_new.geom_type == acquisition_labels["PARALLEL"] or vertical == 'centre' or abs(geometry_new.pixel_num_v/2 - vertical) < 1e-6: + geometry_new = geometry_new.get_centre_slice() + else: + raise ValueError("Can only subset centre slice geometry on cone-beam data. Expected vertical = 'centre'. Got vertical = {0}".format(vertical)) + + if horizontal is not None: + raise ValueError("Cannot calculate system geometry for a horizontal slice") + + return geometry_new + + def allocate(self, value=0, **kwargs): + '''allocates an AcquisitionData according to the size expressed in the instance + + :param value: accepts numbers to allocate an uniform array, or a string as 'random' or 'random_int' to create a random array or None. + :type value: number or string, default None allocates empty memory block + :param dtype: numerical type to allocate + :type dtype: numpy type, default numpy.float32 + ''' + dtype = kwargs.get('dtype', self.dtype) + + if kwargs.get('dimension_labels', None) is not None: + raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") + + out = AcquisitionData(geometry=self.copy(), + dtype=dtype, + suppress_warning=True) + + if isinstance(value, Number): + # it's created empty, so we make it 0 + out.array.fill(value) + else: + if value == acquisition_labels["RANDOM"]: + seed = kwargs.get('seed', None) + if seed is not None: + numpy.random.seed(seed) + if numpy.iscomplexobj(out.array): + r = numpy.random.random_sample(self.shape) + 1j * numpy.random.random_sample(self.shape) + out.fill(r) + else: + out.fill(numpy.random.random_sample(self.shape)) + elif value == acquisition_labels["RANDOM_INT"]: + seed = kwargs.get('seed', None) + if seed is not None: + numpy.random.seed(seed) + max_value = kwargs.get('max_value', 100) + r = numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) + out.fill(numpy.asarray(r, dtype=self.dtype)) + elif value is None: + pass + else: + raise ValueError('Value {} unknown'.format(value)) + + return out diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/framework.py index c3ad95365a..22f158e34a 100644 --- a/Wrappers/Python/cil/framework/framework.py +++ b/Wrappers/Python/cil/framework/framework.py @@ -17,17 +17,10 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -import copy import numpy -from numbers import Number -import math import weakref -import logging -from . import AcquisitionData -from .DataContainer import ImageGeometry, DataContainer -from .base import BaseAcquisitionGeometry -from .label import acquisition_labels, data_order +from .DataContainer import DataContainer def find_key(dic, val): @@ -35,2147 +28,6 @@ def find_key(dic, val): return [k for k, v in dic.items() if v == val][0] -class ComponentDescription(object): - r'''This class enables the creation of vectors and unit vectors used to describe the components of a tomography system - ''' - def __init__ (self, dof): - self._dof = dof - - @staticmethod - def create_vector(val): - try: - vec = numpy.array(val, dtype=numpy.float64).reshape(len(val)) - except: - raise ValueError("Can't convert to numpy array") - - return vec - - @staticmethod - def create_unit_vector(val): - vec = ComponentDescription.create_vector(val) - dot_product = vec.dot(vec) - if abs(dot_product)>1e-8: - vec = (vec/numpy.sqrt(dot_product)) - else: - raise ValueError("Can't return a unit vector of a zero magnitude vector") - return vec - - def length_check(self, val): - try: - val_length = len(val) - except: - raise ValueError("Vectors for {0}D geometries must have length = {0}. Got {1}".format(self._dof,val)) - - if val_length != self._dof: - raise ValueError("Vectors for {0}D geometries must have length = {0}. Got {1}".format(self._dof,val)) - - @staticmethod - def test_perpendicular(vector1, vector2): - dor_prod = vector1.dot(vector2) - if abs(dor_prod) <1e-10: - return True - return False - - @staticmethod - def test_parallel(vector1, vector2): - '''For unit vectors only. Returns true if directions are opposite''' - dor_prod = vector1.dot(vector2) - if 1- abs(dor_prod) <1e-10: - return True - return False - -class PositionVector(ComponentDescription): - r'''This class creates a component of a tomography system with a position attribute - ''' - @property - def position(self): - try: - return self._position - except: - raise AttributeError - - @position.setter - def position(self, val): - self.length_check(val) - self._position = ComponentDescription.create_vector(val) - - -class DirectionVector(ComponentDescription): - r'''This class creates a component of a tomography system with a direction attribute - ''' - @property - def direction(self): - try: - return self._direction - except: - raise AttributeError - - @direction.setter - def direction(self, val): - self.length_check(val) - self._direction = ComponentDescription.create_unit_vector(val) - - -class PositionDirectionVector(PositionVector, DirectionVector): - r'''This class creates a component of a tomography system with position and direction attributes - ''' - pass - -class Detector1D(PositionVector): - r'''This class creates a component of a tomography system with position and direction_x attributes used for 1D panels - ''' - @property - def direction_x(self): - try: - return self._direction_x - except: - raise AttributeError - - @direction_x.setter - def direction_x(self, val): - self.length_check(val) - self._direction_x = ComponentDescription.create_unit_vector(val) - - @property - def normal(self): - try: - return ComponentDescription.create_unit_vector([self._direction_x[1], -self._direction_x[0]]) - except: - raise AttributeError - - -class Detector2D(PositionVector): - r'''This class creates a component of a tomography system with position, direction_x and direction_y attributes used for 2D panels - ''' - @property - def direction_x(self): - try: - return self._direction_x - except: - raise AttributeError - - @property - def direction_y(self): - try: - return self._direction_y - except: - raise AttributeError - - @property - def normal(self): - try: - return numpy.cross(self._direction_x, self._direction_y) - except: - raise AttributeError - - def set_direction(self, x, y): - self.length_check(x) - x = ComponentDescription.create_unit_vector(x) - - self.length_check(y) - y = ComponentDescription.create_unit_vector(y) - - dot_product = x.dot(y) - if not numpy.isclose(dot_product, 0): - raise ValueError("vectors detector.direction_x and detector.direction_y must be orthogonal") - - self._direction_y = y - self._direction_x = x - -class SystemConfiguration(object): - r'''This is a generic class to hold the description of a tomography system - ''' - - SYSTEM_SIMPLE = 'simple' - SYSTEM_OFFSET = 'offset' - SYSTEM_ADVANCED = 'advanced' - - @property - def dimension(self): - if self._dimension == 2: - return '2D' - else: - return '3D' - - @dimension.setter - def dimension(self,val): - if val != 2 and val != 3: - raise ValueError('Can set up 2D and 3D systems only. got {0}D'.format(val)) - else: - self._dimension = val - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self,val): - if val != acquisition_labels["CONE"] and val != acquisition_labels["PARALLEL"]: - raise ValueError('geom_type = {} not recognised please specify \'cone\' or \'parallel\''.format(val)) - else: - self._geometry = val - - def __init__(self, dof, geometry, units='units'): - """Initialises the system component attributes for the acquisition type - """ - self.dimension = dof - self.geometry = geometry - self.units = units - - if geometry == acquisition_labels["PARALLEL"]: - self.ray = DirectionVector(dof) - else: - self.source = PositionVector(dof) - - if dof == 2: - self.detector = Detector1D(dof) - self.rotation_axis = PositionVector(dof) - else: - self.detector = Detector2D(dof) - self.rotation_axis = PositionDirectionVector(dof) - - def __str__(self): - """Implements the string representation of the system configuration - """ - raise NotImplementedError - - def __eq__(self, other): - """Implements the equality check of the system configuration - """ - raise NotImplementedError - - @staticmethod - def rotation_vec_to_y(vec): - ''' returns a rotation matrix that will rotate the projection of vec on the x-y plane to the +y direction [0,1, Z]''' - - vec = ComponentDescription.create_unit_vector(vec) - - axis_rotation = numpy.eye(len(vec)) - - if numpy.allclose(vec[:2],[0,1]): - pass - elif numpy.allclose(vec[:2],[0,-1]): - axis_rotation[0][0] = -1 - axis_rotation[1][1] = -1 - else: - theta = math.atan2(vec[0],vec[1]) - axis_rotation[0][0] = axis_rotation[1][1] = math.cos(theta) - axis_rotation[0][1] = -math.sin(theta) - axis_rotation[1][0] = math.sin(theta) - - return axis_rotation - - @staticmethod - def rotation_vec_to_z(vec): - ''' returns a rotation matrix that will align vec with the z-direction [0,0,1]''' - - vec = ComponentDescription.create_unit_vector(vec) - - if len(vec) == 2: - return numpy.array([[1, 0],[0, 1]]) - - elif len(vec) == 3: - axis_rotation = numpy.eye(3) - - if numpy.allclose(vec,[0,0,1]): - pass - elif numpy.allclose(vec,[0,0,-1]): - axis_rotation = numpy.eye(3) - axis_rotation[1][1] = -1 - axis_rotation[2][2] = -1 - else: - vx = numpy.array([[0, 0, -vec[0]], [0, 0, -vec[1]], [vec[0], vec[1], 0]]) - axis_rotation = numpy.eye(3) + vx + vx.dot(vx) * 1 / (1 + vec[2]) - - else: - raise ValueError("Vec must have length 3, got {}".format(len(vec))) - - return axis_rotation - - def update_reference_frame(self): - r'''Transforms the system origin to the rotation_axis position - ''' - self.set_origin(self.rotation_axis.position) - - - def set_origin(self, origin): - r'''Transforms the system origin to the input origin - ''' - translation = origin.copy() - if hasattr(self,'source'): - self.source.position -= translation - - self.detector.position -= translation - self.rotation_axis.position -= translation - - - def get_centre_slice(self): - """Returns the 2D system configuration corresponding to the centre slice - """ - raise NotImplementedError - - def calculate_magnification(self): - r'''Calculates the magnification of the system using the source to rotate axis, - and source to detector distance along the direction. - - :return: returns [dist_source_center, dist_center_detector, magnification], [0] distance from the source to the rotate axis, [1] distance from the rotate axis to the detector, [2] magnification of the system - :rtype: list - ''' - raise NotImplementedError - - def system_description(self): - r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, - \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets - \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets - ''' - raise NotImplementedError - - def copy(self): - '''returns a copy of SystemConfiguration''' - return copy.deepcopy(self) - -class Parallel2D(SystemConfiguration): - r'''This class creates the SystemConfiguration of a parallel beam 2D tomographic system - - :param ray_direction: A 2D vector describing the x-ray direction (x,y) - :type ray_direction: list, tuple, ndarray - :param detector_pos: A 2D vector describing the position of the centre of the detector (x,y) - :type detector_pos: list, tuple, ndarray - :param detector_direction_x: A 2D vector describing the direction of the detector_x (x,y) - :type detector_direction_x: list, tuple, ndarray - :param rotation_axis_pos: A 2D vector describing the position of the axis of rotation (x,y) - :type rotation_axis_pos: list, tuple, ndarray - :param units: Label the units of distance used for the configuration - :type units: string - ''' - - def __init__ (self, ray_direction, detector_pos, detector_direction_x, rotation_axis_pos, units='units'): - """Constructor method - """ - super(Parallel2D, self).__init__(dof=2, geometry = 'parallel', units=units) - - #source - self.ray.direction = ray_direction - - #detector - self.detector.position = detector_pos - self.detector.direction_x = detector_direction_x - - #rotate axis - self.rotation_axis.position = rotation_axis_pos - - - def align_reference_frame(self, definition='cil'): - r'''Transforms and rotates the system to backend definitions - - 'cil' sets the origin to the rotation axis and aligns the y axis with the ray-direction - 'tigre' sets the origin to the rotation axis and aligns the y axis with the ray-direction - ''' - #in this instance definitions are the same - if definition not in ['cil','tigre']: - raise ValueError("Geometry can be configured for definition = 'cil' or 'tigre' only. Got {}".format(definition)) - - self.set_origin(self.rotation_axis.position) - - rotation_matrix = SystemConfiguration.rotation_vec_to_y(self.ray.direction) - - self.ray.direction = rotation_matrix.dot(self.ray.direction.reshape(2,1)) - self.detector.position = rotation_matrix.dot(self.detector.position.reshape(2,1)) - self.detector.direction_x = rotation_matrix.dot(self.detector.direction_x.reshape(2,1)) - - - def system_description(self): - r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, - \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets - \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets - ''' - - - rays_perpendicular_detector = ComponentDescription.test_parallel(self.ray.direction, self.detector.normal) - - #rotation axis position + ray direction hits detector position - if numpy.allclose(self.rotation_axis.position, self.detector.position): #points are equal so on ray path - rotation_axis_centred = True - else: - vec_a = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position) - rotation_axis_centred = ComponentDescription.test_parallel(self.ray.direction, vec_a) - - if not rays_perpendicular_detector: - config = SystemConfiguration.SYSTEM_ADVANCED - elif not rotation_axis_centred: - config = SystemConfiguration.SYSTEM_OFFSET - else: - config = SystemConfiguration.SYSTEM_SIMPLE - - return config - - - def rotation_axis_on_detector(self): - """ - Calculates the position, on the detector, of the projection of the rotation axis in the world coordinate system - - Returns - ------- - PositionVector - Position in the 3D system - """ - Pv = self.rotation_axis.position - ratio = (self.detector.position - Pv).dot(self.detector.normal) / self.ray.direction.dot(self.detector.normal) - out = PositionVector(2) - out.position = Pv + self.ray.direction * ratio - return out - - def calculate_centre_of_rotation(self): - """ - Calculates the position, on the detector, of the projection of the rotation axis in the detector coordinate system - - Note - ---- - - Origin is in the centre of the detector - - Axes directions are specified by detector.direction_x, detector.direction_y - - Units are the units of distance used to specify the component's positions - - Returns - ------- - Float - Offset position along the detector x_axis at y=0 - Float - Angle between the y_axis and the rotation axis projection, in radians - """ - - #convert to the detector coordinate system - dp1 = self.rotation_axis_on_detector().position - self.detector.position - offset = self.detector.direction_x.dot(dp1) - - return (offset, 0.0) - - def set_centre_of_rotation(self, offset): - """ Configures the geometry to have the requested centre of rotation offset at the detector - """ - offset_current = self.calculate_centre_of_rotation()[0] - offset_new = offset - offset_current - - self.rotation_axis.position = self.rotation_axis.position + offset_new * self.detector.direction_x - - def __str__(self): - def csv(val): - return numpy.array2string(val, separator=', ') - - repres = "2D Parallel-beam tomography\n" - repres += "System configuration:\n" - repres += "\tRay direction: {0}\n".format(csv(self.ray.direction)) - repres += "\tRotation axis position: {0}\n".format(csv(self.rotation_axis.position)) - repres += "\tDetector position: {0}\n".format(csv(self.detector.position)) - repres += "\tDetector direction x: {0}\n".format(csv(self.detector.direction_x)) - return repres - - def __eq__(self, other): - - if not isinstance(other, self.__class__): - return False - - if numpy.allclose(self.ray.direction, other.ray.direction) \ - and numpy.allclose(self.detector.position, other.detector.position)\ - and numpy.allclose(self.detector.direction_x, other.detector.direction_x)\ - and numpy.allclose(self.rotation_axis.position, other.rotation_axis.position): - return True - - return False - - def get_centre_slice(self): - return self - - def calculate_magnification(self): - return [None, None, 1.0] - -class Parallel3D(SystemConfiguration): - r'''This class creates the SystemConfiguration of a parallel beam 3D tomographic system - - :param ray_direction: A 3D vector describing the x-ray direction (x,y,z) - :type ray_direction: list, tuple, ndarray - :param detector_pos: A 3D vector describing the position of the centre of the detector (x,y,z) - :type detector_pos: list, tuple, ndarray - :param detector_direction_x: A 3D vector describing the direction of the detector_x (x,y,z) - :type detector_direction_x: list, tuple, ndarray - :param detector_direction_y: A 3D vector describing the direction of the detector_y (x,y,z) - :type detector_direction_y: list, tuple, ndarray - :param rotation_axis_pos: A 3D vector describing the position of the axis of rotation (x,y,z) - :type rotation_axis_pos: list, tuple, ndarray - :param rotation_axis_direction: A 3D vector describing the direction of the axis of rotation (x,y,z) - :type rotation_axis_direction: list, tuple, ndarray - :param units: Label the units of distance used for the configuration - :type units: string - ''' - - def __init__ (self, ray_direction, detector_pos, detector_direction_x, detector_direction_y, rotation_axis_pos, rotation_axis_direction, units='units'): - """Constructor method - """ - super(Parallel3D, self).__init__(dof=3, geometry = 'parallel', units=units) - - #source - self.ray.direction = ray_direction - - #detector - self.detector.position = detector_pos - self.detector.set_direction(detector_direction_x, detector_direction_y) - - #rotate axis - self.rotation_axis.position = rotation_axis_pos - self.rotation_axis.direction = rotation_axis_direction - - def align_z(self): - r'''Transforms the system origin to the rotate axis with z direction aligned to the rotate axis direction - ''' - self.set_origin(self.rotation_axis.position) - - #calculate rotation matrix to align rotation axis direction with z - rotation_matrix = SystemConfiguration.rotation_vec_to_z(self.rotation_axis.direction) - - #apply transform - self.rotation_axis.direction = [0,0,1] - self.ray.direction = rotation_matrix.dot(self.ray.direction.reshape(3,1)) - self.detector.position = rotation_matrix.dot(self.detector.position.reshape(3,1)) - new_x = rotation_matrix.dot(self.detector.direction_x.reshape(3,1)) - new_y = rotation_matrix.dot(self.detector.direction_y.reshape(3,1)) - self.detector.set_direction(new_x, new_y) - - - def align_reference_frame(self, definition='cil'): - r'''Transforms and rotates the system to backend definitions - ''' - #in this instance definitions are the same - if definition not in ['cil','tigre']: - raise ValueError("Geometry can be configured for definition = 'cil' or 'tigre' only. Got {}".format(definition)) - - self.align_z() - rotation_matrix = SystemConfiguration.rotation_vec_to_y(self.ray.direction) - - self.ray.direction = rotation_matrix.dot(self.ray.direction.reshape(3,1)) - self.detector.position = rotation_matrix.dot(self.detector.position.reshape(3,1)) - new_direction_x = rotation_matrix.dot(self.detector.direction_x.reshape(3,1)) - new_direction_y = rotation_matrix.dot(self.detector.direction_y.reshape(3,1)) - self.detector.set_direction(new_direction_x, new_direction_y) - - - def system_description(self): - r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, - \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets - \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets - ''' - - - ''' - simple - - rays perpendicular to detector - - rotation axis parallel to detector y - - rotation axis position + ray direction hits detector with no x offset (y offsets allowed) - offset - - rays perpendicular to detector - - rotation axis parallel to detector y - rolled - - rays perpendicular to detector - - rays perpendicular to rotation axis - advanced - - not rays perpendicular to detector (for parallel just equates to an effective pixel size change?) - or - - not rays perpendicular to rotation axis (tilted, i.e. laminography) - ''' - - rays_perpendicular_detector = ComponentDescription.test_parallel(self.ray.direction, self.detector.normal) - rays_perpendicular_rotation = ComponentDescription.test_perpendicular(self.ray.direction, self.rotation_axis.direction) - rotation_parallel_detector_y = ComponentDescription.test_parallel(self.rotation_axis.direction, self.detector.direction_y) - - #rotation axis to detector is parallel with ray - if numpy.allclose(self.rotation_axis.position, self.detector.position): #points are equal so on ray path - rotation_axis_centred = True - else: - vec_a = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position ) - rotation_axis_centred = ComponentDescription.test_parallel(self.ray.direction, vec_a) - - if not rays_perpendicular_detector or\ - not rays_perpendicular_rotation or\ - not rotation_parallel_detector_y: - config = SystemConfiguration.SYSTEM_ADVANCED - elif not rotation_axis_centred: - config = SystemConfiguration.SYSTEM_OFFSET - else: - config = SystemConfiguration.SYSTEM_SIMPLE - - return config - - - def __str__(self): - def csv(val): - return numpy.array2string(val, separator=', ') - - repres = "3D Parallel-beam tomography\n" - repres += "System configuration:\n" - repres += "\tRay direction: {0}\n".format(csv(self.ray.direction)) - repres += "\tRotation axis position: {0}\n".format(csv(self.rotation_axis.position)) - repres += "\tRotation axis direction: {0}\n".format(csv(self.rotation_axis.direction)) - repres += "\tDetector position: {0}\n".format(csv(self.detector.position)) - repres += "\tDetector direction x: {0}\n".format(csv(self.detector.direction_x)) - repres += "\tDetector direction y: {0}\n".format(csv(self.detector.direction_y)) - return repres - - def __eq__(self, other): - - if not isinstance(other, self.__class__): - return False - - if numpy.allclose(self.ray.direction, other.ray.direction) \ - and numpy.allclose(self.detector.position, other.detector.position)\ - and numpy.allclose(self.detector.direction_x, other.detector.direction_x)\ - and numpy.allclose(self.detector.direction_y, other.detector.direction_y)\ - and numpy.allclose(self.rotation_axis.position, other.rotation_axis.position)\ - and numpy.allclose(self.rotation_axis.direction, other.rotation_axis.direction): - - return True - - return False - - def calculate_magnification(self): - return [None, None, 1.0] - - def get_centre_slice(self): - """Returns the 2D system configuration corresponding to the centre slice - """ - dp1 = self.rotation_axis.direction.dot(self.ray.direction) - dp2 = self.rotation_axis.direction.dot(self.detector.direction_x) - - if numpy.isclose(dp1, 0) and numpy.isclose(dp2, 0): - temp = self.copy() - - #convert to rotation axis reference frame - temp.align_reference_frame() - - ray_direction = temp.ray.direction[0:2] - detector_position = temp.detector.position[0:2] - detector_direction_x = temp.detector.direction_x[0:2] - rotation_axis_position = temp.rotation_axis.position[0:2] - - return Parallel2D(ray_direction, detector_position, detector_direction_x, rotation_axis_position) - - else: - raise ValueError('Cannot convert geometry to 2D. Requires axis of rotation to be perpendicular to ray direction and the detector direction x.') - - - def rotation_axis_on_detector(self): - """ - Calculates the position, on the detector, of the projection of the rotation axis in the world coordinate system - - Returns - ------- - PositionDirectionVector - Position and direction in the 3D system - """ - #calculate the rotation axis line with the detector - vec_a = self.ray.direction - - #calculate the intersection with the detector - Pv = self.rotation_axis.position - ratio = (self.detector.position - Pv).dot(self.detector.normal) / vec_a.dot(self.detector.normal) - point1 = Pv + vec_a * ratio - - Pv = self.rotation_axis.position + self.rotation_axis.direction - ratio = (self.detector.position - Pv).dot(self.detector.normal) / vec_a.dot(self.detector.normal) - point2 = Pv + vec_a * ratio - - out = PositionDirectionVector(3) - out.position = point1 - out.direction = point2 - point1 - return out - - - def calculate_centre_of_rotation(self): - """ - Calculates the position, on the detector, of the projection of the rotation axis in the detector coordinate system - - Note - ---- - - Origin is in the centre of the detector - - Axes directions are specified by detector.direction_x, detector.direction_y - - Units are the units of distance used to specify the component's positions - - Returns - ------- - Float - Offset position along the detector x_axis at y=0 - Float - Angle between the y_axis and the rotation axis projection, in radians - """ - rotate_axis_projection = self.rotation_axis_on_detector() - - p1 = rotate_axis_projection.position - p2 = p1 + rotate_axis_projection.direction - - #point1 and point2 are on the detector plane. need to return them in the detector coordinate system - dp1 = p1 - self.detector.position - x1 = self.detector.direction_x.dot(dp1) - y1 = self.detector.direction_y.dot(dp1) - dp2 = p2 - self.detector.position - x2 = self.detector.direction_x.dot(dp2) - y2 = self.detector.direction_y.dot(dp2) - - #y = m * x + c - #c = y1 - m * x1 - #when y is 0 - #x=-c/m - #x_y0 = -y1/m + x1 - offset_x_y0 = x1 -y1 * (x2 - x1)/(y2-y1) - - angle = math.atan2(x2 - x1, y2 - y1) - offset = offset_x_y0 - - return (offset, angle) - - def set_centre_of_rotation(self, offset, angle): - """ Configures the geometry to have the requested centre of rotation offset at the detector - """ - - #two points on the detector - x1 = offset - y1 = 0 - x2 = offset + math.tan(angle) - y2 = 1 - - #convert to 3d coordinates in system frame - p1 = self.detector.position + x1 * self.detector.direction_x + y1 * self.detector.direction_y - p2 = self.detector.position + x2 * self.detector.direction_x + y2 * self.detector.direction_y - - # find where vec p1 + t * ray dirn intersects plane defined by rotate axis (pos and dir) and det_x direction - - vector_pos=p1 - vec_dirn=self.ray.direction - plane_pos=self.rotation_axis.position - plane_normal = numpy.cross(self.detector.direction_x, self.rotation_axis.direction) - - - ratio = (plane_pos - vector_pos).dot(plane_normal) / vec_dirn.dot(plane_normal) - p1_on_plane = vector_pos + vec_dirn * ratio - - vector_pos=p2 - ratio = (plane_pos - vector_pos).dot(plane_normal) / vec_dirn.dot(plane_normal) - p2_on_plane = vector_pos + vec_dirn * ratio - - self.rotation_axis.position = p1_on_plane - self.rotation_axis.direction = p2_on_plane - p1_on_plane - - -class Cone2D(SystemConfiguration): - r'''This class creates the SystemConfiguration of a cone beam 2D tomographic system - - :param source_pos: A 2D vector describing the position of the source (x,y) - :type source_pos: list, tuple, ndarray - :param detector_pos: A 2D vector describing the position of the centre of the detector (x,y) - :type detector_pos: list, tuple, ndarray - :param detector_direction_x: A 2D vector describing the direction of the detector_x (x,y) - :type detector_direction_x: list, tuple, ndarray - :param rotation_axis_pos: A 2D vector describing the position of the axis of rotation (x,y) - :type rotation_axis_pos: list, tuple, ndarray - :param units: Label the units of distance used for the configuration - :type units: string - ''' - - def __init__ (self, source_pos, detector_pos, detector_direction_x, rotation_axis_pos, units='units'): - """Constructor method - """ - super(Cone2D, self).__init__(dof=2, geometry = 'cone', units=units) - - #source - self.source.position = source_pos - - #detector - self.detector.position = detector_pos - self.detector.direction_x = detector_direction_x - - #rotate axis - self.rotation_axis.position = rotation_axis_pos - - - def align_reference_frame(self, definition='cil'): - r'''Transforms and rotates the system to backend definitions - ''' - self.set_origin(self.rotation_axis.position) - - if definition=='cil': - rotation_matrix = SystemConfiguration.rotation_vec_to_y(self.detector.position - self.source.position) - elif definition=='tigre': - rotation_matrix = SystemConfiguration.rotation_vec_to_y(self.rotation_axis.position - self.source.position) - else: - raise ValueError("Geometry can be configured for definition = 'cil' or 'tigre' only. Got {}".format(definition)) - - self.source.position = rotation_matrix.dot(self.source.position.reshape(2,1)) - self.detector.position = rotation_matrix.dot(self.detector.position.reshape(2,1)) - self.detector.direction_x = rotation_matrix.dot(self.detector.direction_x.reshape(2,1)) - - - def system_description(self): - r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, - \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets - \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets - ''' - - vec_src2det = ComponentDescription.create_unit_vector(self.detector.position - self.source.position) - - principal_ray_centred = ComponentDescription.test_parallel(vec_src2det, self.detector.normal) - - #rotation axis to detector is parallel with centre ray - if numpy.allclose(self.rotation_axis.position, self.detector.position): #points are equal - rotation_axis_centred = True - else: - vec_b = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position ) - rotation_axis_centred = ComponentDescription.test_parallel(vec_src2det, vec_b) - - if not principal_ray_centred: - config = SystemConfiguration.SYSTEM_ADVANCED - elif not rotation_axis_centred: - config = SystemConfiguration.SYSTEM_OFFSET - else: - config = SystemConfiguration.SYSTEM_SIMPLE - - return config - - def __str__(self): - def csv(val): - return numpy.array2string(val, separator=', ') - - repres = "2D Cone-beam tomography\n" - repres += "System configuration:\n" - repres += "\tSource position: {0}\n".format(csv(self.source.position)) - repres += "\tRotation axis position: {0}\n".format(csv(self.rotation_axis.position)) - repres += "\tDetector position: {0}\n".format(csv(self.detector.position)) - repres += "\tDetector direction x: {0}\n".format(csv(self.detector.direction_x)) - return repres - - def __eq__(self, other): - - if not isinstance(other, self.__class__): - return False - - if numpy.allclose(self.source.position, other.source.position) \ - and numpy.allclose(self.detector.position, other.detector.position)\ - and numpy.allclose(self.detector.direction_x, other.detector.direction_x)\ - and numpy.allclose(self.rotation_axis.position, other.rotation_axis.position): - return True - - return False - - def get_centre_slice(self): - return self - - def calculate_magnification(self): - - ab = (self.rotation_axis.position - self.source.position) - dist_source_center = float(numpy.sqrt(ab.dot(ab))) - - ab_unit = ab / numpy.sqrt(ab.dot(ab)) - - n = self.detector.normal - - #perpendicular distance between source and detector centre - sd = float((self.detector.position - self.source.position).dot(n)) - ratio = float(ab_unit.dot(n)) - - source_to_detector = sd / ratio - dist_center_detector = source_to_detector - dist_source_center - magnification = (dist_center_detector + dist_source_center) / dist_source_center - - return [dist_source_center, dist_center_detector, magnification] - - def rotation_axis_on_detector(self): - """ - Calculates the position, on the detector, of the projection of the rotation axis in the world coordinate system - - Returns - ------- - PositionVector - Position in the 3D system - """ - #calculate the point the rotation axis intersects with the detector - vec_a = self.rotation_axis.position - self.source.position - - Pv = self.rotation_axis.position - ratio = (self.detector.position - Pv).dot(self.detector.normal) / vec_a.dot(self.detector.normal) - - out = PositionVector(2) - out.position = Pv + vec_a * ratio - - return out - - - def calculate_centre_of_rotation(self): - """ - Calculates the position, on the detector, of the projection of the rotation axis in the detector coordinate system - - Note - ---- - - Origin is in the centre of the detector - - Axes directions are specified by detector.direction_x, detector.direction_y - - Units are the units of distance used to specify the component's positions - - Returns - ------- - Float - Offset position along the detector x_axis at y=0 - Float - Angle between the y_axis and the rotation axis projection, in radians - """ - #convert to the detector coordinate system - dp1 = self.rotation_axis_on_detector().position - self.detector.position - offset = self.detector.direction_x.dot(dp1) - - return (offset, 0.0) - - def set_centre_of_rotation(self, offset): - """ Configures the geometry to have the requested centre of rotation offset at the detector - """ - offset_current = self.calculate_centre_of_rotation()[0] - offset_new = offset - offset_current - - cofr_shift = offset_new * self.detector.direction_x /self.calculate_magnification()[2] - self.rotation_axis.position =self.rotation_axis.position + cofr_shift - -class Cone3D(SystemConfiguration): - r'''This class creates the SystemConfiguration of a cone beam 3D tomographic system - - :param source_pos: A 3D vector describing the position of the source (x,y,z) - :type source_pos: list, tuple, ndarray - :param detector_pos: A 3D vector describing the position of the centre of the detector (x,y,z) - :type detector_pos: list, tuple, ndarray - :param detector_direction_x: A 3D vector describing the direction of the detector_x (x,y,z) - :type detector_direction_x: list, tuple, ndarray - :param detector_direction_y: A 3D vector describing the direction of the detector_y (x,y,z) - :type detector_direction_y: list, tuple, ndarray - :param rotation_axis_pos: A 3D vector describing the position of the axis of rotation (x,y,z) - :type rotation_axis_pos: list, tuple, ndarray - :param rotation_axis_direction: A 3D vector describing the direction of the axis of rotation (x,y,z) - :type rotation_axis_direction: list, tuple, ndarray - :param units: Label the units of distance used for the configuration - :type units: string - ''' - - def __init__ (self, source_pos, detector_pos, detector_direction_x, detector_direction_y, rotation_axis_pos, rotation_axis_direction, units='units'): - """Constructor method - """ - super(Cone3D, self).__init__(dof=3, geometry = 'cone', units=units) - - #source - self.source.position = source_pos - - #detector - self.detector.position = detector_pos - self.detector.set_direction(detector_direction_x, detector_direction_y) - - #rotate axis - self.rotation_axis.position = rotation_axis_pos - self.rotation_axis.direction = rotation_axis_direction - - def align_z(self): - r'''Transforms the system origin to the rotate axis with z direction aligned to the rotate axis direction - ''' - self.set_origin(self.rotation_axis.position) - rotation_matrix = SystemConfiguration.rotation_vec_to_z(self.rotation_axis.direction) - - #apply transform - self.rotation_axis.direction = [0,0,1] - self.source.position = rotation_matrix.dot(self.source.position.reshape(3,1)) - self.detector.position = rotation_matrix.dot(self.detector.position.reshape(3,1)) - new_x = rotation_matrix.dot(self.detector.direction_x.reshape(3,1)) - new_y = rotation_matrix.dot(self.detector.direction_y.reshape(3,1)) - self.detector.set_direction(new_x, new_y) - - - def align_reference_frame(self, definition='cil'): - r'''Transforms and rotates the system to backend definitions - ''' - - self.align_z() - - if definition=='cil': - rotation_matrix = SystemConfiguration.rotation_vec_to_y(self.detector.position - self.source.position) - elif definition=='tigre': - rotation_matrix = SystemConfiguration.rotation_vec_to_y(self.rotation_axis.position - self.source.position) - else: - raise ValueError("Geometry can be configured for definition = 'cil' or 'tigre' only. Got {}".format(definition)) - - self.source.position = rotation_matrix.dot(self.source.position.reshape(3,1)) - self.detector.position = rotation_matrix.dot(self.detector.position.reshape(3,1)) - new_direction_x = rotation_matrix.dot(self.detector.direction_x.reshape(3,1)) - new_direction_y = rotation_matrix.dot(self.detector.direction_y.reshape(3,1)) - self.detector.set_direction(new_direction_x, new_direction_y) - - - def system_description(self): - r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, - \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets - \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets - ''' - - vec_src2det = ComponentDescription.create_unit_vector(self.detector.position - self.source.position) - - principal_ray_centred = ComponentDescription.test_parallel(vec_src2det, self.detector.normal) - centre_ray_perpendicular_rotation = ComponentDescription.test_perpendicular(vec_src2det, self.rotation_axis.direction) - rotation_parallel_detector_y = ComponentDescription.test_parallel(self.rotation_axis.direction, self.detector.direction_y) - - #rotation axis to detector is parallel with centre ray - if numpy.allclose(self.rotation_axis.position, self.detector.position): #points are equal - rotation_axis_centred = True - else: - vec_b = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position ) - rotation_axis_centred = ComponentDescription.test_parallel(vec_src2det, vec_b) - - if not principal_ray_centred or\ - not centre_ray_perpendicular_rotation or\ - not rotation_parallel_detector_y: - config = SystemConfiguration.SYSTEM_ADVANCED - elif not rotation_axis_centred: - config = SystemConfiguration.SYSTEM_OFFSET - else: - config = SystemConfiguration.SYSTEM_SIMPLE - - return config - - def get_centre_slice(self): - """Returns the 2D system configuration corresponding to the centre slice - """ - #requires the rotate axis to be perpendicular to the normal of the detector, and perpendicular to detector_direction_x - dp1 = self.rotation_axis.direction.dot(self.detector.normal) - dp2 = self.rotation_axis.direction.dot(self.detector.direction_x) - - if numpy.isclose(dp1, 0) and numpy.isclose(dp2, 0): - temp = self.copy() - temp.align_reference_frame() - source_position = temp.source.position[0:2] - detector_position = temp.detector.position[0:2] - detector_direction_x = temp.detector.direction_x[0:2] - rotation_axis_position = temp.rotation_axis.position[0:2] - - return Cone2D(source_position, detector_position, detector_direction_x, rotation_axis_position) - else: - raise ValueError('Cannot convert geometry to 2D. Requires axis of rotation to be perpendicular to the detector.') - - def __str__(self): - def csv(val): - return numpy.array2string(val, separator=', ') - - repres = "3D Cone-beam tomography\n" - repres += "System configuration:\n" - repres += "\tSource position: {0}\n".format(csv(self.source.position)) - repres += "\tRotation axis position: {0}\n".format(csv(self.rotation_axis.position)) - repres += "\tRotation axis direction: {0}\n".format(csv(self.rotation_axis.direction)) - repres += "\tDetector position: {0}\n".format(csv(self.detector.position)) - repres += "\tDetector direction x: {0}\n".format(csv(self.detector.direction_x)) - repres += "\tDetector direction y: {0}\n".format(csv(self.detector.direction_y)) - return repres - - def __eq__(self, other): - - if not isinstance(other, self.__class__): - return False - - if numpy.allclose(self.source.position, other.source.position) \ - and numpy.allclose(self.detector.position, other.detector.position)\ - and numpy.allclose(self.detector.direction_x, other.detector.direction_x)\ - and numpy.allclose(self.detector.direction_y, other.detector.direction_y)\ - and numpy.allclose(self.rotation_axis.position, other.rotation_axis.position)\ - and numpy.allclose(self.rotation_axis.direction, other.rotation_axis.direction): - - return True - - return False - - def calculate_magnification(self): - - ab = (self.rotation_axis.position - self.source.position) - dist_source_center = float(numpy.sqrt(ab.dot(ab))) - - ab_unit = ab / numpy.sqrt(ab.dot(ab)) - - n = self.detector.normal - - #perpendicular distance between source and detector centre - sd = float((self.detector.position - self.source.position).dot(n)) - ratio = float(ab_unit.dot(n)) - - source_to_detector = sd / ratio - dist_center_detector = source_to_detector - dist_source_center - magnification = (dist_center_detector + dist_source_center) / dist_source_center - - return [dist_source_center, dist_center_detector, magnification] - - def rotation_axis_on_detector(self): - """ - Calculates the position, on the detector, of the projection of the rotation axis in the world coordinate system - - Returns - ------- - PositionDirectionVector - Position and direction in the 3D system - """ - #calculate the intersection with the detector, of source to pv - Pv = self.rotation_axis.position - vec_a = Pv - self.source.position - ratio = (self.detector.position - Pv).dot(self.detector.normal) / vec_a.dot(self.detector.normal) - point1 = Pv + vec_a * ratio - - #calculate the intersection with the detector, of source to pv - Pv = self.rotation_axis.position + self.rotation_axis.direction - vec_a = Pv - self.source.position - ratio = (self.detector.position - Pv).dot(self.detector.normal) / vec_a.dot(self.detector.normal) - point2 = Pv + vec_a * ratio - - out = PositionDirectionVector(3) - out.position = point1 - out.direction = point2 - point1 - return out - - def calculate_centre_of_rotation(self): - """ - Calculates the position, on the detector, of the projection of the rotation axis in the detector coordinate system - - Note - ---- - - Origin is in the centre of the detector - - Axes directions are specified by detector.direction_x, detector.direction_y - - Units are the units of distance used to specify the component's positions - - Returns - ------- - Float - Offset position along the detector x_axis at y=0 - Float - Angle between the y_axis and the rotation axis projection, in radians - """ - rotate_axis_projection = self.rotation_axis_on_detector() - - p1 = rotate_axis_projection.position - p2 = p1 + rotate_axis_projection.direction - - #point1 and point2 are on the detector plane. need to return them in the detector coordinate system - dp1 = p1 - self.detector.position - x1 = self.detector.direction_x.dot(dp1) - y1 = self.detector.direction_y.dot(dp1) - dp2 = p2 - self.detector.position - x2 = self.detector.direction_x.dot(dp2) - y2 = self.detector.direction_y.dot(dp2) - - #y = m * x + c - #c = y1 - m * x1 - #when y is 0 - #x=-c/m - #x_y0 = -y1/m + x1 - offset_x_y0 = x1 -y1 * (x2 - x1)/(y2-y1) - - angle = math.atan2(x2 - x1, y2 - y1) - offset = offset_x_y0 - - return (offset, angle) - - - def set_centre_of_rotation(self, offset, angle): - """ Configures the geometry to have the requested centre of rotation offset at the detector - """ - #two points on the detector - x1 = offset - y1 = 0 - x2 = offset + math.tan(angle) - y2 = 1 - - #convert to 3d coordinates in system frame - p1 = self.detector.position + x1 * self.detector.direction_x + y1 * self.detector.direction_y - p2 = self.detector.position + x2 * self.detector.direction_x + y2 * self.detector.direction_y - - # vectors from source define plane - sp1 = p1 - self.source.position - sp2 = p2 - self.source.position - - #find vector intersection with a plane defined by rotate axis (pos and dir) and det_x direction - plane_normal = numpy.cross(self.rotation_axis.direction, self.detector.direction_x) - - ratio = (self.rotation_axis.position - self.source.position).dot(plane_normal) / sp1.dot(plane_normal) - p1_on_plane = self.source.position + sp1 * ratio - - ratio = (self.rotation_axis.position - self.source.position).dot(plane_normal) / sp2.dot(plane_normal) - p2_on_plane = self.source.position + sp2 * ratio - - self.rotation_axis.position = p1_on_plane - self.rotation_axis.direction = p2_on_plane - p1_on_plane - - -class Panel(object): - r'''This is a class describing the panel of the system. - - :param num_pixels: num_pixels_h or (num_pixels_h, num_pixels_v) containing the number of pixels of the panel - :type num_pixels: int, list, tuple - :param pixel_size: pixel_size_h or (pixel_size_h, pixel_size_v) containing the size of the pixels of the panel - :type pixel_size: int, lust, tuple - :param origin: the position of pixel 0 (the data origin) of the panel `top-left`, `top-right`, `bottom-left`, `bottom-right` - :type origin: string - ''' - - @property - def num_pixels(self): - return self._num_pixels - - @num_pixels.setter - def num_pixels(self, val): - - if isinstance(val,int): - num_pixels_temp = [val, 1] - else: - try: - length_val = len(val) - except: - raise TypeError('num_pixels expected int x or [int x, int y]. Got {}'.format(type(val))) - - - if length_val == 2: - try: - val0 = int(val[0]) - val1 = int(val[1]) - except: - raise TypeError('num_pixels expected int x or [int x, int y]. Got {0},{1}'.format(type(val[0]), type(val[1]))) - - num_pixels_temp = [val0, val1] - else: - raise ValueError('num_pixels expected int x or [int x, int y]. Got {}'.format(val)) - - if num_pixels_temp[1] > 1 and self._dimension == 2: - raise ValueError('2D acquisitions expects a 1D panel. Expected num_pixels[1] = 1. Got {}'.format(num_pixels_temp[1])) - if num_pixels_temp[0] < 1 or num_pixels_temp[1] < 1: - raise ValueError('num_pixels (x,y) must be >= (1,1). Got {}'.format(num_pixels_temp)) - else: - self._num_pixels = numpy.array(num_pixels_temp, dtype=numpy.int16) - - @property - def pixel_size(self): - return self._pixel_size - - @pixel_size.setter - def pixel_size(self, val): - - if val is None: - pixel_size_temp = [1.0,1.0] - else: - try: - length_val = len(val) - except: - try: - temp = float(val) - pixel_size_temp = [temp, temp] - - except: - raise TypeError('pixel_size expected float xy or [float x, float y]. Got {}'.format(val)) - else: - if length_val == 2: - try: - temp0 = float(val[0]) - temp1 = float(val[1]) - pixel_size_temp = [temp0, temp1] - except: - raise ValueError('pixel_size expected float xy or [float x, float y]. Got {}'.format(val)) - else: - raise ValueError('pixel_size expected float xy or [float x, float y]. Got {}'.format(val)) - - if pixel_size_temp[0] <= 0 or pixel_size_temp[1] <= 0: - raise ValueError('pixel_size (x,y) at must be > (0.,0.). Got {}'.format(pixel_size_temp)) - - self._pixel_size = numpy.array(pixel_size_temp) - - @property - def origin(self): - return self._origin - - @origin.setter - def origin(self, val): - allowed = ['top-left', 'top-right','bottom-left','bottom-right'] - if val in allowed: - self._origin=val - else: - raise ValueError('origin expected one of {0}. Got {1}'.format(allowed, val)) - - def __str__(self): - repres = "Panel configuration:\n" - repres += "\tNumber of pixels: {0}\n".format(self.num_pixels) - repres += "\tPixel size: {0}\n".format(self.pixel_size) - repres += "\tPixel origin: {0}\n".format(self.origin) - return repres - - def __eq__(self, other): - - if not isinstance(other, self.__class__): - return False - - if numpy.array_equal(self.num_pixels, other.num_pixels) \ - and numpy.allclose(self.pixel_size, other.pixel_size) \ - and self.origin == other.origin: - return True - - return False - - def __init__ (self, num_pixels, pixel_size, origin, dimension): - """Constructor method - """ - self._dimension = dimension - self.num_pixels = num_pixels - self.pixel_size = pixel_size - self.origin = origin - -class Channels(object): - r'''This is a class describing the channels of the data. - This will be created on initialisation of AcquisitionGeometry. - - :param num_channels: The number of channels of data - :type num_channels: int - :param channel_labels: A list of channel labels - :type channel_labels: list, optional - ''' - - @property - def num_channels(self): - return self._num_channels - - @num_channels.setter - def num_channels(self, val): - try: - val = int(val) - except TypeError: - raise ValueError('num_channels expected a positive integer. Got {}'.format(type(val))) - - if val > 0: - self._num_channels = val - else: - raise ValueError('num_channels expected a positive integer. Got {}'.format(val)) - - @property - def channel_labels(self): - return self._channel_labels - - @channel_labels.setter - def channel_labels(self, val): - if val is None or len(val) == self._num_channels: - self._channel_labels = val - else: - raise ValueError('labels expected to have length {0}. Got {1}'.format(self._num_channels, len(val))) - - def __str__(self): - repres = "Channel configuration:\n" - repres += "\tNumber of channels: {0}\n".format(self.num_channels) - - num_print=min(10,self.num_channels) - if hasattr(self, 'channel_labels'): - repres += "\tChannel labels 0-{0}: {1}\n".format(num_print, self.channel_labels[0:num_print]) - - return repres - - def __eq__(self, other): - - if not isinstance(other, self.__class__): - return False - - if self.num_channels != other.num_channels: - return False - - if hasattr(self,'channel_labels'): - if self.channel_labels != other.channel_labels: - return False - - return True - - def __init__ (self, num_channels, channel_labels): - """Constructor method - """ - self.num_channels = num_channels - if channel_labels is not None: - self.channel_labels = channel_labels - -class Angles(object): - r'''This is a class describing the angles of the data. - - :param angles: The angular positions of the acquisition data - :type angles: list, ndarray - :param initial_angle: The angular offset of the object from the reference frame - :type initial_angle: float, optional - :param angle_unit: The units of the stored angles 'degree' or 'radian' - :type angle_unit: string - ''' - - @property - def angle_data(self): - return self._angle_data - - @angle_data.setter - def angle_data(self, val): - if val is None: - raise ValueError('angle_data expected to be a list of floats') - else: - try: - self.num_positions = len(val) - - except TypeError: - self.num_positions = 1 - val = [val] - - finally: - try: - self._angle_data = numpy.asarray(val, dtype=numpy.float32) - except: - raise ValueError('angle_data expected to be a list of floats') - - @property - def initial_angle(self): - return self._initial_angle - - @initial_angle.setter - def initial_angle(self, val): - try: - val = float(val) - except: - raise TypeError('initial_angle expected a float. Got {0}'.format(type(val))) - - self._initial_angle = val - - @property - def angle_unit(self): - return self._angle_unit - - @angle_unit.setter - def angle_unit(self,val): - if val != acquisition_labels["DEGREE"] and val != acquisition_labels["RADIAN"]: - raise ValueError('angle_unit = {} not recognised please specify \'degree\' or \'radian\''.format(val)) - else: - self._angle_unit = val - - def __str__(self): - repres = "Acquisition description:\n" - repres += "\tNumber of positions: {0}\n".format(self.num_positions) - # max_num_print = 30 - if self.num_positions < 31: - repres += "\tAngles 0-{0} in {1}s: {2}\n".format(self.num_positions-1, self.angle_unit, numpy.array2string(self.angle_data[0:self.num_positions], separator=', ')) - else: - repres += "\tAngles 0-9 in {0}s: {1}\n".format(self.angle_unit, numpy.array2string(self.angle_data[0:10], separator=', ')) - repres += "\tAngles {0}-{1} in {2}s: {3}\n".format(self.num_positions-10, self.num_positions-1, self.angle_unit, numpy.array2string(self.angle_data[self.num_positions-10:self.num_positions], separator=', ')) - repres += "\tFull angular array can be accessed with acquisition_data.geometry.angles\n" - return repres - - def __eq__(self, other): - - if not isinstance(other, self.__class__): - return False - - if self.angle_unit != other.angle_unit: - return False - - if self.initial_angle != other.initial_angle: - return False - - if not numpy.allclose(self.angle_data, other.angle_data): - return False - - return True - - def __init__ (self, angles, initial_angle, angle_unit): - """Constructor method - """ - self.angle_data = angles - self.initial_angle = initial_angle - self.angle_unit = angle_unit - -class Configuration(object): - r'''This class holds the description of the system components. - ''' - - def __init__(self, units_distance='units distance'): - self.system = None #has distances - self.angles = None #has angles - self.panel = None #has distances - self.channels = Channels(1, None) - self.units = units_distance - - @property - def configured(self): - if self.system is None: - print("Please configure AcquisitionGeometry using one of the following methods:\ - \n\tAcquisitionGeometry.create_Parallel2D()\ - \n\tAcquisitionGeometry.create_Cone3D()\ - \n\tAcquisitionGeometry.create_Parallel2D()\ - \n\tAcquisitionGeometry.create_Cone3D()") - return False - - configured = True - if self.angles is None: - print("Please configure angular data using the set_angles() method") - configured = False - if self.panel is None: - print("Please configure the panel using the set_panel() method") - configured = False - return configured - - def shift_detector_in_plane(self, - pixel_offset, - direction='horizontal'): - """ - Adjusts the position of the detector in a specified direction within the imaging plane. - - Parameters: - ----------- - pixel_offset : float - The number of pixels to adjust the detector's position by. - direction : {'horizontal', 'vertical'}, optional - The direction in which to adjust the detector's position. Defaults to 'horizontal'. - - Notes: - ------ - - If `direction` is 'horizontal': - - If the panel's origin is 'left', positive offsets translate the detector to the right. - - If the panel's origin is 'right', positive offsets translate the detector to the left. - - - If `direction` is 'vertical': - - If the panel's origin is 'bottom', positive offsets translate the detector upward. - - If the panel's origin is 'top', positive offsets translate the detector downward. - - Returns: - -------- - None - """ - - if direction == 'horizontal': - pixel_size = self.panel.pixel_size[0] - pixel_direction = self.system.detector.direction_x - - elif direction == 'vertical': - pixel_size = self.panel.pixel_size[1] - pixel_direction = self.system.detector.direction_y - - if 'bottom' in self.panel.origin or 'left' in self.panel.origin: - self.system.detector.position -= pixel_offset * pixel_direction * pixel_size - else: - self.system.detector.position += pixel_offset * pixel_direction * pixel_size - - - def __str__(self): - repres = "" - if self.configured: - repres += str(self.system) - repres += str(self.panel) - repres += str(self.channels) - repres += str(self.angles) - - repres += "Distances in units: {}".format(self.units) - - return repres - - def __eq__(self, other): - - if not isinstance(other, self.__class__): - return False - - if self.system == other.system\ - and self.panel == other.panel\ - and self.channels == other.channels\ - and self.angles == other.angles: - return True - - return False - - -class AcquisitionGeometry(BaseAcquisitionGeometry): - """This class holds the AcquisitionGeometry of the system. - - Please initialise the AcquisitionGeometry using the using the static methods: - - `AcquisitionGeometry.create_Parallel2D()` - - `AcquisitionGeometry.create_Cone2D()` - - `AcquisitionGeometry.create_Parallel3D()` - - `AcquisitionGeometry.create_Cone3D()` - """ - - - #for backwards compatibility - @property - def geom_type(self): - return self.config.system.geometry - - @property - def num_projections(self): - return len(self.angles) - - @property - def pixel_num_h(self): - return self.config.panel.num_pixels[0] - - @pixel_num_h.setter - def pixel_num_h(self, val): - self.config.panel.num_pixels[0] = val - - @property - def pixel_num_v(self): - return self.config.panel.num_pixels[1] - - @pixel_num_v.setter - def pixel_num_v(self, val): - self.config.panel.num_pixels[1] = val - - @property - def pixel_size_h(self): - return self.config.panel.pixel_size[0] - - @pixel_size_h.setter - def pixel_size_h(self, val): - self.config.panel.pixel_size[0] = val - - @property - def pixel_size_v(self): - return self.config.panel.pixel_size[1] - - @pixel_size_v.setter - def pixel_size_v(self, val): - self.config.panel.pixel_size[1] = val - - @property - def channels(self): - return self.config.channels.num_channels - - @property - def angles(self): - return self.config.angles.angle_data - - @property - def dist_source_center(self): - out = self.config.system.calculate_magnification() - return out[0] - - @property - def dist_center_detector(self): - out = self.config.system.calculate_magnification() - return out[1] - - @property - def magnification(self): - out = self.config.system.calculate_magnification() - return out[2] - - @property - def dimension(self): - return self.config.system.dimension - - @property - def shape(self): - - shape_dict = {acquisition_labels["CHANNEL"]: self.config.channels.num_channels, - acquisition_labels["ANGLE"]: self.config.angles.num_positions, - acquisition_labels["VERTICAL"]: self.config.panel.num_pixels[1], - acquisition_labels["HORIZONTAL"]: self.config.panel.num_pixels[0]} - shape = [] - for label in self.dimension_labels: - shape.append(shape_dict[label]) - - return tuple(shape) - - @property - def dimension_labels(self): - labels_default = data_order["CIL_AG_LABELS"] - - shape_default = [self.config.channels.num_channels, - self.config.angles.num_positions, - self.config.panel.num_pixels[1], - self.config.panel.num_pixels[0] - ] - - try: - labels = list(self._dimension_labels) - except AttributeError: - labels = labels_default.copy() - - #remove from list labels where len == 1 - # - for i, x in enumerate(shape_default): - if x == 0 or x==1: - try: - labels.remove(labels_default[i]) - except ValueError: - pass #if not in custom list carry on - - return tuple(labels) - - @dimension_labels.setter - def dimension_labels(self, val): - - labels_default = data_order["CIL_AG_LABELS"] - - #check input and store. This value is not used directly - if val is not None: - for x in val: - if x not in labels_default: - raise ValueError('Requested axis are not possible. Accepted label names {},\ngot {}'.format(labels_default,val)) - - self._dimension_labels = tuple(val) - - @property - def ndim(self): - return len(self.dimension_labels) - - @property - def system_description(self): - return self.config.system.system_description() - - @property - def dtype(self): - return self._dtype - - @dtype.setter - def dtype(self, val): - self._dtype = val - - - def __init__(self): - self._dtype = numpy.float32 - - - def get_centre_of_rotation(self, distance_units='default', angle_units='radian'): - """ - Returns the system centre of rotation offset at the detector - - Note - ---- - - Origin is in the centre of the detector - - Axes directions are specified by detector.direction_x, detector.direction_y - - Parameters - ---------- - distance_units : string, default='default' - Units of distance used to calculate the return values. - 'default' uses the same units the system and panel were specified in. - 'pixels' uses pixels sizes in the horizontal and vertical directions as appropriate. - angle_units : string - Units to return the angle in. Can take 'radian' or 'degree'. - - Returns - ------- - Dictionary - {'offset': (offset, distance_units), 'angle': (angle, angle_units)} - where, - 'offset' gives the position along the detector x_axis at y=0 - 'angle' gives the angle between the y_axis and the projection of the rotation axis on the detector - """ - - if hasattr(self.config.system, 'calculate_centre_of_rotation'): - offset_distance, angle_rad = self.config.system.calculate_centre_of_rotation() - else: - raise NotImplementedError - - if distance_units == 'default': - offset = offset_distance - offset_units = self.config.units - elif distance_units == 'pixels': - - offset = offset_distance/ self.config.panel.pixel_size[0] - offset_units = 'pixels' - - if self.dimension == '3D' and self.config.panel.pixel_size[0] != self.config.panel.pixel_size[1]: - #if aspect ratio of pixels isn't 1:1 need to convert angle by new ratio - y_pix = 1 /self.config.panel.pixel_size[1] - x_pix = math.tan(angle_rad)/self.config.panel.pixel_size[0] - angle_rad = math.atan2(x_pix,y_pix) - else: - raise ValueError("`distance_units` is not recognised. Must be 'default' or 'pixels'. Got {}".format(distance_units)) - - if angle_units == 'radian': - angle = angle_rad - ang_units = 'radian' - elif angle_units == 'degree': - angle = numpy.degrees(angle_rad) - ang_units = 'degree' - else: - raise ValueError("`angle_units` is not recognised. Must be 'radian' or 'degree'. Got {}".format(angle_units)) - - return {'offset': (offset, offset_units), 'angle': (angle, ang_units)} - - - def set_centre_of_rotation(self, offset=0.0, distance_units='default', angle=0.0, angle_units='radian'): - """ - Configures the system geometry to have the requested centre of rotation offset at the detector. - - Note - ---- - - Origin is in the centre of the detector - - Axes directions are specified by detector.direction_x, detector.direction_y - - Parameters - ---------- - offset: float, default 0.0 - The position of the centre of rotation along the detector x_axis at y=0 - - distance_units : string, default='default' - Units the offset is specified in. Can be 'default'or 'pixels'. - 'default' interprets the input as same units the system and panel were specified in. - 'pixels' interprets the input in horizontal pixels. - - angle: float, default=0.0 - The angle between the detector y_axis and the rotation axis direction on the detector - - Notes - ----- - If aspect ratio of pixels is not 1:1 ensure the angle is calculated from the x and y values in the correct units. - - angle_units : string, default='radian' - Units the angle is specified in. Can take 'radian' or 'degree'. - - """ - - if not hasattr(self.config.system, 'set_centre_of_rotation'): - raise NotImplementedError() - - if angle_units == 'radian': - angle_rad = angle - elif angle_units == 'degree': - angle_rad = numpy.radians(angle) - else: - raise ValueError("`angle_units` is not recognised. Must be 'radian' or 'degree'. Got {}".format(angle_units)) - - if distance_units =='default': - offset_distance = offset - elif distance_units =='pixels': - offset_distance = offset * self.config.panel.pixel_size[0] - else: - raise ValueError("`distance_units` is not recognised. Must be 'default' or 'pixels'. Got {}".format(distance_units)) - - if self.dimension == '2D': - self.config.system.set_centre_of_rotation(offset_distance) - else: - self.config.system.set_centre_of_rotation(offset_distance, angle_rad) - - - def set_centre_of_rotation_by_slice(self, offset1, slice_index1=None, offset2=None, slice_index2=None): - """ - Configures the system geometry to have the requested centre of rotation offset at the detector. - - If two slices are passed the rotation axis will be rotated to pass through both points. - - Note - ---- - - Offset is specified in pixels - - Offset can be sub-pixels - - Offset direction is specified by detector.direction_x - - Parameters - ---------- - offset1: float - The offset from the centre of the detector to the projected rotation position at slice_index_1 - - slice_index1: int, optional - The slice number of offset1 - - offset2: float, optional - The offset from the centre of the detector to the projected rotation position at slice_index_2 - - slice_index2: int, optional - The slice number of offset2 - """ - - - if not hasattr(self.config.system, 'set_centre_of_rotation'): - raise NotImplementedError() - - if self.dimension == '2D': - if offset2 is not None: - logging.WARNING("Only offset1 is being used") - self.set_centre_of_rotation(offset1) - - if offset2 is None or offset1 == offset2: - offset_x_y0 = offset1 - angle = 0 - else: - if slice_index1 is None or slice_index2 is None or slice_index1 == slice_index2: - raise ValueError("Cannot calculate angle. Please specify `slice_index1` and `slice_index2` to define a rotated axis") - - offset_x_y0 = offset1 -slice_index1 * (offset2 - offset1)/(slice_index2-slice_index1) - angle = math.atan2(offset2 - offset1, slice_index2 - slice_index1) - - self.set_centre_of_rotation(offset_x_y0, 'pixels', angle, 'radian') - - - def set_angles(self, angles, initial_angle=0, angle_unit='degree'): - r'''This method configures the angular information of an AcquisitionGeometry object. - - :param angles: The angular positions of the acquisition data - :type angles: list, ndarray - :param initial_angle: The angular offset of the object from the reference frame - :type initial_angle: float, optional - :param angle_unit: The units of the stored angles 'degree' or 'radian' - :type angle_unit: string - :return: returns a configured AcquisitionGeometry object - :rtype: AcquisitionGeometry - ''' - self.config.angles = Angles(angles, initial_angle, angle_unit) - return self - - def set_panel(self, num_pixels, pixel_size=(1,1), origin='bottom-left'): - - r'''This method configures the panel information of an AcquisitionGeometry object. - - :param num_pixels: num_pixels_h or (num_pixels_h, num_pixels_v) containing the number of pixels of the panel - :type num_pixels: int, list, tuple - :param pixel_size: pixel_size_h or (pixel_size_h, pixel_size_v) containing the size of the pixels of the panel - :type pixel_size: int, list, tuple, optional - :param origin: the position of pixel 0 (the data origin) of the panel 'top-left', 'top-right', 'bottom-left', 'bottom-right' - :type origin: string, default 'bottom-left' - :return: returns a configured AcquisitionGeometry object - :rtype: AcquisitionGeometry - ''' - self.config.panel = Panel(num_pixels, pixel_size, origin, self.config.system._dimension) - return self - - def set_channels(self, num_channels=1, channel_labels=None): - r'''This method configures the channel information of an AcquisitionGeometry object. - - :param num_channels: The number of channels of data - :type num_channels: int, optional - :param channel_labels: A list of channel labels - :type channel_labels: list, optional - :return: returns a configured AcquisitionGeometry object - :rtype: AcquisitionGeometry - ''' - self.config.channels = Channels(num_channels, channel_labels) - return self - - def set_labels(self, labels=None): - r'''This method configures the dimension labels of an AcquisitionGeometry object. - - :param labels: The order of the dimensions describing the data.\ - Expects a list containing at least one of the unique labels: 'channel' 'angle' 'vertical' 'horizontal' - default = ['channel','angle','vertical','horizontal'] - :type labels: list, optional - :return: returns a configured AcquisitionGeometry object - :rtype: AcquisitionGeometry - ''' - self.dimension_labels = labels - return self - - @staticmethod - def create_Parallel2D(ray_direction=[0, 1], detector_position=[0, 0], detector_direction_x=[1, 0], rotation_axis_position=[0, 0], units='units distance'): - r'''This creates the AcquisitionGeometry for a parallel beam 2D tomographic system - - :param ray_direction: A 2D vector describing the x-ray direction (x,y) - :type ray_direction: list, tuple, ndarray, optional - :param detector_position: A 2D vector describing the position of the centre of the detector (x,y) - :type detector_position: list, tuple, ndarray, optional - :param detector_direction_x: A 2D vector describing the direction of the detector_x (x,y) - :type detector_direction_x: list, tuple, ndarray - :param rotation_axis_position: A 2D vector describing the position of the axis of rotation (x,y) - :type rotation_axis_position: list, tuple, ndarray, optional - :param units: Label the units of distance used for the configuration, these should be consistent for the geometry and panel - :type units: string - :return: returns a configured AcquisitionGeometry object - :rtype: AcquisitionGeometry - ''' - AG = AcquisitionGeometry() - AG.config = Configuration(units) - AG.config.system = Parallel2D(ray_direction, detector_position, detector_direction_x, rotation_axis_position, units) - return AG - - @staticmethod - def create_Cone2D(source_position, detector_position, detector_direction_x=[1,0], rotation_axis_position=[0,0], units='units distance'): - r'''This creates the AcquisitionGeometry for a cone beam 2D tomographic system - - :param source_position: A 2D vector describing the position of the source (x,y) - :type source_position: list, tuple, ndarray - :param detector_position: A 2D vector describing the position of the centre of the detector (x,y) - :type detector_position: list, tuple, ndarray - :param detector_direction_x: A 2D vector describing the direction of the detector_x (x,y) - :type detector_direction_x: list, tuple, ndarray - :param rotation_axis_position: A 2D vector describing the position of the axis of rotation (x,y) - :type rotation_axis_position: list, tuple, ndarray, optional - :param units: Label the units of distance used for the configuration, these should be consistent for the geometry and panel - :type units: string - :return: returns a configured AcquisitionGeometry object - :rtype: AcquisitionGeometry - ''' - AG = AcquisitionGeometry() - AG.config = Configuration(units) - AG.config.system = Cone2D(source_position, detector_position, detector_direction_x, rotation_axis_position, units) - return AG - - @staticmethod - def create_Parallel3D(ray_direction=[0,1,0], detector_position=[0,0,0], detector_direction_x=[1,0,0], detector_direction_y=[0,0,1], rotation_axis_position=[0,0,0], rotation_axis_direction=[0,0,1], units='units distance'): - r'''This creates the AcquisitionGeometry for a parallel beam 3D tomographic system - - :param ray_direction: A 3D vector describing the x-ray direction (x,y,z) - :type ray_direction: list, tuple, ndarray, optional - :param detector_position: A 3D vector describing the position of the centre of the detector (x,y,z) - :type detector_position: list, tuple, ndarray, optional - :param detector_direction_x: A 3D vector describing the direction of the detector_x (x,y,z) - :type detector_direction_x: list, tuple, ndarray - :param detector_direction_y: A 3D vector describing the direction of the detector_y (x,y,z) - :type detector_direction_y: list, tuple, ndarray - :param rotation_axis_position: A 3D vector describing the position of the axis of rotation (x,y,z) - :type rotation_axis_position: list, tuple, ndarray, optional - :param rotation_axis_direction: A 3D vector describing the direction of the axis of rotation (x,y,z) - :type rotation_axis_direction: list, tuple, ndarray, optional - :param units: Label the units of distance used for the configuration, these should be consistent for the geometry and panel - :type units: string - :return: returns a configured AcquisitionGeometry object - :rtype: AcquisitionGeometry - ''' - AG = AcquisitionGeometry() - AG.config = Configuration(units) - AG.config.system = Parallel3D(ray_direction, detector_position, detector_direction_x, detector_direction_y, rotation_axis_position, rotation_axis_direction, units) - return AG - - @staticmethod - def create_Cone3D(source_position, detector_position, detector_direction_x=[1,0,0], detector_direction_y=[0,0,1], rotation_axis_position=[0,0,0], rotation_axis_direction=[0,0,1], units='units distance'): - r'''This creates the AcquisitionGeometry for a cone beam 3D tomographic system - - :param source_position: A 3D vector describing the position of the source (x,y,z) - :type source_position: list, tuple, ndarray, optional - :param detector_position: A 3D vector describing the position of the centre of the detector (x,y,z) - :type detector_position: list, tuple, ndarray, optional - :param detector_direction_x: A 3D vector describing the direction of the detector_x (x,y,z) - :type detector_direction_x: list, tuple, ndarray - :param detector_direction_y: A 3D vector describing the direction of the detector_y (x,y,z) - :type detector_direction_y: list, tuple, ndarray - :param rotation_axis_position: A 3D vector describing the position of the axis of rotation (x,y,z) - :type rotation_axis_position: list, tuple, ndarray, optional - :param rotation_axis_direction: A 3D vector describing the direction of the axis of rotation (x,y,z) - :type rotation_axis_direction: list, tuple, ndarray, optional - :param units: Label the units of distance used for the configuration, these should be consistent for the geometry and panel - :type units: string - :return: returns a configured AcquisitionGeometry object - :rtype: AcquisitionGeometry - ''' - AG = AcquisitionGeometry() - AG.config = Configuration(units) - AG.config.system = Cone3D(source_position, detector_position, detector_direction_x, detector_direction_y, rotation_axis_position, rotation_axis_direction, units) - return AG - - def get_order_by_label(self, dimension_labels, default_dimension_labels): - order = [] - for i, el in enumerate(default_dimension_labels): - for j, ek in enumerate(dimension_labels): - if el == ek: - order.append(j) - break - return order - - def __eq__(self, other): - - if isinstance(other, self.__class__) and self.config == other.config : - return True - return False - - def clone(self): - '''returns a copy of the AcquisitionGeometry''' - return copy.deepcopy(self) - - def copy(self): - '''alias of clone''' - return self.clone() - - def get_centre_slice(self): - '''returns a 2D AcquisitionGeometry that corresponds to the centre slice of the input''' - - if self.dimension == '2D': - return self - - AG_2D = copy.deepcopy(self) - AG_2D.config.system = self.config.system.get_centre_slice() - AG_2D.config.panel.num_pixels[1] = 1 - AG_2D.config.panel.pixel_size[1] = abs(self.config.system.detector.direction_y[2]) * self.config.panel.pixel_size[1] - return AG_2D - - def get_ImageGeometry(self, resolution=1.0): - '''returns a default configured ImageGeometry object based on the AcquisitionGeomerty''' - - num_voxel_xy = int(numpy.ceil(self.config.panel.num_pixels[0] * resolution)) - voxel_size_xy = self.config.panel.pixel_size[0] / (resolution * self.magnification) - - if self.dimension == '3D': - num_voxel_z = int(numpy.ceil(self.config.panel.num_pixels[1] * resolution)) - voxel_size_z = self.config.panel.pixel_size[1] / (resolution * self.magnification) - else: - num_voxel_z = 0 - voxel_size_z = 1 - - return ImageGeometry(num_voxel_xy, num_voxel_xy, num_voxel_z, voxel_size_xy, voxel_size_xy, voxel_size_z, channels=self.channels) - - def __str__ (self): - return str(self.config) - - - def get_slice(self, channel=None, angle=None, vertical=None, horizontal=None): - ''' - Returns a new AcquisitionGeometry of a single slice of in the requested direction. Will only return reconstructable geometries. - ''' - geometry_new = self.copy() - - if channel is not None: - geometry_new.config.channels.num_channels = 1 - if hasattr(geometry_new.config.channels,'channel_labels'): - geometry_new.config.panel.channel_labels = geometry_new.config.panel.channel_labels[channel] - - if angle is not None: - geometry_new.config.angles.angle_data = geometry_new.config.angles.angle_data[angle] - - if vertical is not None: - if geometry_new.geom_type == acquisition_labels["PARALLEL"] or vertical == 'centre' or abs(geometry_new.pixel_num_v/2 - vertical) < 1e-6: - geometry_new = geometry_new.get_centre_slice() - else: - raise ValueError("Can only subset centre slice geometry on cone-beam data. Expected vertical = 'centre'. Got vertical = {0}".format(vertical)) - - if horizontal is not None: - raise ValueError("Cannot calculate system geometry for a horizontal slice") - - return geometry_new - - def allocate(self, value=0, **kwargs): - '''allocates an AcquisitionData according to the size expressed in the instance - - :param value: accepts numbers to allocate an uniform array, or a string as 'random' or 'random_int' to create a random array or None. - :type value: number or string, default None allocates empty memory block - :param dtype: numerical type to allocate - :type dtype: numpy type, default numpy.float32 - ''' - dtype = kwargs.get('dtype', self.dtype) - - if kwargs.get('dimension_labels', None) is not None: - raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") - - out = AcquisitionData(geometry=self.copy(), - dtype=dtype, - suppress_warning=True) - - if isinstance(value, Number): - # it's created empty, so we make it 0 - out.array.fill(value) - else: - if value == acquisition_labels["RANDOM"]: - seed = kwargs.get('seed', None) - if seed is not None: - numpy.random.seed(seed) - if numpy.iscomplexobj(out.array): - r = numpy.random.random_sample(self.shape) + 1j * numpy.random.random_sample(self.shape) - out.fill(r) - else: - out.fill(numpy.random.random_sample(self.shape)) - elif value == acquisition_labels["RANDOM_INT"]: - seed = kwargs.get('seed', None) - if seed is not None: - numpy.random.seed(seed) - max_value = kwargs.get('max_value', 100) - r = numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) - out.fill(numpy.asarray(r, dtype=self.dtype)) - elif value is None: - pass - else: - raise ValueError('Value {} unknown'.format(value)) - - return out - - class Processor(object): '''Defines a generic DataContainer processor diff --git a/Wrappers/Python/test/test_AcquisitionGeometry.py b/Wrappers/Python/test/test_AcquisitionGeometry.py index 0bbc1c42a9..b512ad3d1f 100644 --- a/Wrappers/Python/test/test_AcquisitionGeometry.py +++ b/Wrappers/Python/test/test_AcquisitionGeometry.py @@ -24,8 +24,7 @@ import re import io import sys -from cil.framework import AcquisitionGeometry, ImageGeometry, BlockGeometry, AcquisitionData, Partitioner -from cil.framework.framework import SystemConfiguration +from cil.framework import AcquisitionGeometry, ImageGeometry, AcquisitionData, Partitioner, SystemConfiguration initialise_tests() diff --git a/Wrappers/Python/test/test_DataContainer.py b/Wrappers/Python/test/test_DataContainer.py index a95a6f61bd..66d599739a 100644 --- a/Wrappers/Python/test/test_DataContainer.py +++ b/Wrappers/Python/test/test_DataContainer.py @@ -21,10 +21,8 @@ from utils import initialise_tests import sys import numpy -from cil.framework import DataContainer, ImageGeometry, ImageData, VectorGeometry, AcquisitionData -from cil.framework import BlockGeometry -from cil.framework import AcquisitionGeometry -from cil.framework import acquisition_labels, image_labels +from cil.framework import (DataContainer, ImageGeometry, ImageData, VectorGeometry, AcquisitionData, + AcquisitionGeometry, BlockGeometry, acquisition_labels, image_labels) from timeit import default_timer as timer import logging from testclass import CCPiTestClass diff --git a/Wrappers/Python/test/test_DataProcessor.py b/Wrappers/Python/test/test_DataProcessor.py index f7172253b6..9a7cd7934b 100644 --- a/Wrappers/Python/test/test_DataProcessor.py +++ b/Wrappers/Python/test/test_DataProcessor.py @@ -19,8 +19,7 @@ import unittest import numpy -from cil.framework import DataContainer, ImageGeometry, ImageData, VectorGeometry, AcquisitionData -from cil.framework import AcquisitionGeometry +from cil.framework import DataContainer, ImageGeometry, ImageData, VectorGeometry, AcquisitionData, AcquisitionGeometry from cil.utilities import dataexample from timeit import default_timer as timer diff --git a/Wrappers/Python/test/test_algorithms.py b/Wrappers/Python/test/test_algorithms.py index f049a997fa..c8192832bd 100644 --- a/Wrappers/Python/test/test_algorithms.py +++ b/Wrappers/Python/test/test_algorithms.py @@ -22,11 +22,8 @@ import numpy import numpy as np from numpy import nan, inf -from cil.framework import VectorData, ImageGeometry, ImageData, VectorGeometry, AcquisitionData -from cil.framework import AcquisitionGeometry -from cil.framework import BlockDataContainer -from cil.framework import BlockGeometry -from cil.framework import image_labels +from cil.framework import (ImageGeometry, ImageData, VectorGeometry, AcquisitionData, AcquisitionGeometry, + BlockDataContainer, BlockGeometry, image_labels) from cil.optimisation.operators import IdentityOperator from cil.optimisation.operators import GradientOperator, BlockOperator, MatrixOperator diff --git a/Wrappers/Python/test/test_dataexample.py b/Wrappers/Python/test/test_dataexample.py index d09b03d1fc..fa4ebabd31 100644 --- a/Wrappers/Python/test/test_dataexample.py +++ b/Wrappers/Python/test/test_dataexample.py @@ -19,8 +19,7 @@ import unittest from utils import initialise_tests -from cil.framework.framework import AcquisitionGeometry -from cil.framework import ImageGeometry +from cil.framework import ImageGeometry, AcquisitionGeometry from cil.utilities import dataexample from cil.utilities import noise import os, sys diff --git a/Wrappers/Python/test/test_functions.py b/Wrappers/Python/test/test_functions.py index 2aff2de60f..8c76aadd03 100644 --- a/Wrappers/Python/test/test_functions.py +++ b/Wrappers/Python/test/test_functions.py @@ -22,7 +22,8 @@ from cil.optimisation.functions.Function import ScaledFunction import numpy as np -from cil.framework import VectorGeometry, VectorData, BlockDataContainer, DataContainer, image_labels, ImageGeometry +from cil.framework import VectorGeometry, VectorData, BlockDataContainer, DataContainer, image_labels, ImageGeometry, \ + AcquisitionGeometry from cil.optimisation.operators import IdentityOperator, MatrixOperator, CompositionOperator, DiagonalOperator, BlockOperator from cil.optimisation.functions import Function, KullbackLeibler, ConstantFunction, TranslateFunction, soft_shrinkage from cil.optimisation.operators import GradientOperator @@ -896,7 +897,7 @@ def test_Lipschitz4(self): assert f4.L == 2 * f2.L def test_proximal_conjugate(self): - from cil.framework import AcquisitionGeometry, BlockGeometry + from cil.framework import BlockGeometry ag = AcquisitionGeometry.create_Parallel2D() angles = np.linspace(0, 360, 10, dtype=np.float32) diff --git a/Wrappers/Python/test/test_subset.py b/Wrappers/Python/test/test_subset.py index 8a53d2f682..5713333ea0 100644 --- a/Wrappers/Python/test/test_subset.py +++ b/Wrappers/Python/test/test_subset.py @@ -20,9 +20,7 @@ import unittest from utils import initialise_tests import numpy -from cil.framework import DataContainer, ImageGeometry, ImageData, AcquisitionData -from cil.framework import AcquisitionGeometry -from cil.framework import acquisition_labels, image_labels +from cil.framework import DataContainer, ImageGeometry, AcquisitionGeometry, acquisition_labels, image_labels from timeit import default_timer as timer initialise_tests() From 054b2e01f77b372874f4702a9a6858a73f5e38dc Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Sat, 10 Feb 2024 17:26:34 +0000 Subject: [PATCH 16/72] Move a bunch of stuff relating to SystemConfiguration into a new file. --- Wrappers/Python/cil/framework/__init__.py | 3 +- .../cil/framework/acquisition_geometry.py | 314 +----------------- .../cil/framework/system_configuration.py | 309 +++++++++++++++++ 3 files changed, 317 insertions(+), 309 deletions(-) create mode 100644 Wrappers/Python/cil/framework/system_configuration.py diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 969c47f53d..f6d4b489fa 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -24,7 +24,8 @@ from functools import reduce from .cilacc import cilacc from .AcquisitionData import AcquisitionData -from .acquisition_geometry import AcquisitionGeometry, SystemConfiguration +from .acquisition_geometry import AcquisitionGeometry +from .system_configuration import SystemConfiguration from .framework import find_key from .DataContainer import message, ImageGeometry, DataContainer, ImageData, VectorData, VectorGeometry from .framework import DataProcessor, Processor diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 594646d6d2..e712b72ef0 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -9,309 +9,7 @@ from .AcquisitionData import AcquisitionData from .base import BaseAcquisitionGeometry from .DataContainer import ImageGeometry - - -class ComponentDescription(object): - r'''This class enables the creation of vectors and unit vectors used to describe the components of a tomography system - ''' - def __init__ (self, dof): - self._dof = dof - - @staticmethod - def create_vector(val): - try: - vec = numpy.array(val, dtype=numpy.float64).reshape(len(val)) - except: - raise ValueError("Can't convert to numpy array") - - return vec - - @staticmethod - def create_unit_vector(val): - vec = ComponentDescription.create_vector(val) - dot_product = vec.dot(vec) - if abs(dot_product)>1e-8: - vec = (vec/numpy.sqrt(dot_product)) - else: - raise ValueError("Can't return a unit vector of a zero magnitude vector") - return vec - - def length_check(self, val): - try: - val_length = len(val) - except: - raise ValueError("Vectors for {0}D geometries must have length = {0}. Got {1}".format(self._dof,val)) - - if val_length != self._dof: - raise ValueError("Vectors for {0}D geometries must have length = {0}. Got {1}".format(self._dof,val)) - - @staticmethod - def test_perpendicular(vector1, vector2): - dor_prod = vector1.dot(vector2) - if abs(dor_prod) <1e-10: - return True - return False - - @staticmethod - def test_parallel(vector1, vector2): - '''For unit vectors only. Returns true if directions are opposite''' - dor_prod = vector1.dot(vector2) - if 1- abs(dor_prod) <1e-10: - return True - return False - - -class PositionVector(ComponentDescription): - r'''This class creates a component of a tomography system with a position attribute - ''' - @property - def position(self): - try: - return self._position - except: - raise AttributeError - - @position.setter - def position(self, val): - self.length_check(val) - self._position = ComponentDescription.create_vector(val) - - -class DirectionVector(ComponentDescription): - r'''This class creates a component of a tomography system with a direction attribute - ''' - @property - def direction(self): - try: - return self._direction - except: - raise AttributeError - - @direction.setter - def direction(self, val): - self.length_check(val) - self._direction = ComponentDescription.create_unit_vector(val) - - -class PositionDirectionVector(PositionVector, DirectionVector): - r'''This class creates a component of a tomography system with position and direction attributes - ''' - pass - - -class Detector1D(PositionVector): - r'''This class creates a component of a tomography system with position and direction_x attributes used for 1D panels - ''' - @property - def direction_x(self): - try: - return self._direction_x - except: - raise AttributeError - - @direction_x.setter - def direction_x(self, val): - self.length_check(val) - self._direction_x = ComponentDescription.create_unit_vector(val) - - @property - def normal(self): - try: - return ComponentDescription.create_unit_vector([self._direction_x[1], -self._direction_x[0]]) - except: - raise AttributeError - - -class Detector2D(PositionVector): - r'''This class creates a component of a tomography system with position, direction_x and direction_y attributes used for 2D panels - ''' - @property - def direction_x(self): - try: - return self._direction_x - except: - raise AttributeError - - @property - def direction_y(self): - try: - return self._direction_y - except: - raise AttributeError - - @property - def normal(self): - try: - return numpy.cross(self._direction_x, self._direction_y) - except: - raise AttributeError - - def set_direction(self, x, y): - self.length_check(x) - x = ComponentDescription.create_unit_vector(x) - - self.length_check(y) - y = ComponentDescription.create_unit_vector(y) - - dot_product = x.dot(y) - if not numpy.isclose(dot_product, 0): - raise ValueError("vectors detector.direction_x and detector.direction_y must be orthogonal") - - self._direction_y = y - self._direction_x = x - - -class SystemConfiguration(object): - r'''This is a generic class to hold the description of a tomography system - ''' - - SYSTEM_SIMPLE = 'simple' - SYSTEM_OFFSET = 'offset' - SYSTEM_ADVANCED = 'advanced' - - @property - def dimension(self): - if self._dimension == 2: - return '2D' - else: - return '3D' - - @dimension.setter - def dimension(self,val): - if val != 2 and val != 3: - raise ValueError('Can set up 2D and 3D systems only. got {0}D'.format(val)) - else: - self._dimension = val - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self,val): - if val != acquisition_labels["CONE"] and val != acquisition_labels["PARALLEL"]: - raise ValueError('geom_type = {} not recognised please specify \'cone\' or \'parallel\''.format(val)) - else: - self._geometry = val - - def __init__(self, dof, geometry, units='units'): - """Initialises the system component attributes for the acquisition type - """ - self.dimension = dof - self.geometry = geometry - self.units = units - - if geometry == acquisition_labels["PARALLEL"]: - self.ray = DirectionVector(dof) - else: - self.source = PositionVector(dof) - - if dof == 2: - self.detector = Detector1D(dof) - self.rotation_axis = PositionVector(dof) - else: - self.detector = Detector2D(dof) - self.rotation_axis = PositionDirectionVector(dof) - - def __str__(self): - """Implements the string representation of the system configuration - """ - raise NotImplementedError - - def __eq__(self, other): - """Implements the equality check of the system configuration - """ - raise NotImplementedError - - @staticmethod - def rotation_vec_to_y(vec): - ''' returns a rotation matrix that will rotate the projection of vec on the x-y plane to the +y direction [0,1, Z]''' - - vec = ComponentDescription.create_unit_vector(vec) - - axis_rotation = numpy.eye(len(vec)) - - if numpy.allclose(vec[:2],[0,1]): - pass - elif numpy.allclose(vec[:2],[0,-1]): - axis_rotation[0][0] = -1 - axis_rotation[1][1] = -1 - else: - theta = math.atan2(vec[0],vec[1]) - axis_rotation[0][0] = axis_rotation[1][1] = math.cos(theta) - axis_rotation[0][1] = -math.sin(theta) - axis_rotation[1][0] = math.sin(theta) - - return axis_rotation - - @staticmethod - def rotation_vec_to_z(vec): - ''' returns a rotation matrix that will align vec with the z-direction [0,0,1]''' - - vec = ComponentDescription.create_unit_vector(vec) - - if len(vec) == 2: - return numpy.array([[1, 0],[0, 1]]) - - elif len(vec) == 3: - axis_rotation = numpy.eye(3) - - if numpy.allclose(vec,[0,0,1]): - pass - elif numpy.allclose(vec,[0,0,-1]): - axis_rotation = numpy.eye(3) - axis_rotation[1][1] = -1 - axis_rotation[2][2] = -1 - else: - vx = numpy.array([[0, 0, -vec[0]], [0, 0, -vec[1]], [vec[0], vec[1], 0]]) - axis_rotation = numpy.eye(3) + vx + vx.dot(vx) * 1 / (1 + vec[2]) - - else: - raise ValueError("Vec must have length 3, got {}".format(len(vec))) - - return axis_rotation - - def update_reference_frame(self): - r'''Transforms the system origin to the rotation_axis position - ''' - self.set_origin(self.rotation_axis.position) - - - def set_origin(self, origin): - r'''Transforms the system origin to the input origin - ''' - translation = origin.copy() - if hasattr(self,'source'): - self.source.position -= translation - - self.detector.position -= translation - self.rotation_axis.position -= translation - - - def get_centre_slice(self): - """Returns the 2D system configuration corresponding to the centre slice - """ - raise NotImplementedError - - def calculate_magnification(self): - r'''Calculates the magnification of the system using the source to rotate axis, - and source to detector distance along the direction. - - :return: returns [dist_source_center, dist_center_detector, magnification], [0] distance from the source to the rotate axis, [1] distance from the rotate axis to the detector, [2] magnification of the system - :rtype: list - ''' - raise NotImplementedError - - def system_description(self): - r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, - \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets - \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets - ''' - raise NotImplementedError - - def copy(self): - '''returns a copy of SystemConfiguration''' - return copy.deepcopy(self) +from .system_configuration import ComponentDescription, PositionVector, PositionDirectionVector, SystemConfiguration class Parallel2D(SystemConfiguration): @@ -396,7 +94,7 @@ def rotation_axis_on_detector(self): Returns ------- - PositionVector + cil.framework.system_configuration.PositionVector Position in the 3D system """ Pv = self.rotation_axis.position @@ -570,7 +268,7 @@ def system_description(self): if numpy.allclose(self.rotation_axis.position, self.detector.position): #points are equal so on ray path rotation_axis_centred = True else: - vec_a = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position ) + vec_a = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position) rotation_axis_centred = ComponentDescription.test_parallel(self.ray.direction, vec_a) if not rays_perpendicular_detector or\ @@ -647,7 +345,7 @@ def rotation_axis_on_detector(self): Returns ------- - PositionDirectionVector + cil.framework.system_configuration.PositionDirectionVector Position and direction in the 3D system """ #calculate the rotation axis line with the detector @@ -805,7 +503,7 @@ def system_description(self): if numpy.allclose(self.rotation_axis.position, self.detector.position): #points are equal rotation_axis_centred = True else: - vec_b = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position ) + vec_b = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position) rotation_axis_centred = ComponentDescription.test_parallel(vec_src2det, vec_b) if not principal_ray_centred: @@ -1004,7 +702,7 @@ def system_description(self): if numpy.allclose(self.rotation_axis.position, self.detector.position): #points are equal rotation_axis_centred = True else: - vec_b = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position ) + vec_b = ComponentDescription.create_unit_vector(self.detector.position - self.rotation_axis.position) rotation_axis_centred = ComponentDescription.test_parallel(vec_src2det, vec_b) if not principal_ray_centred or\ diff --git a/Wrappers/Python/cil/framework/system_configuration.py b/Wrappers/Python/cil/framework/system_configuration.py new file mode 100644 index 0000000000..e1d2827329 --- /dev/null +++ b/Wrappers/Python/cil/framework/system_configuration.py @@ -0,0 +1,309 @@ +import copy +import math + +import numpy + +from cil.framework import acquisition_labels + + +class ComponentDescription(object): + r'''This class enables the creation of vectors and unit vectors used to describe the components of a tomography system + ''' + def __init__ (self, dof): + self._dof = dof + + @staticmethod + def create_vector(val): + try: + vec = numpy.array(val, dtype=numpy.float64).reshape(len(val)) + except: + raise ValueError("Can't convert to numpy array") + + return vec + + @staticmethod + def create_unit_vector(val): + vec = ComponentDescription.create_vector(val) + dot_product = vec.dot(vec) + if abs(dot_product)>1e-8: + vec = (vec/numpy.sqrt(dot_product)) + else: + raise ValueError("Can't return a unit vector of a zero magnitude vector") + return vec + + def length_check(self, val): + try: + val_length = len(val) + except: + raise ValueError("Vectors for {0}D geometries must have length = {0}. Got {1}".format(self._dof,val)) + + if val_length != self._dof: + raise ValueError("Vectors for {0}D geometries must have length = {0}. Got {1}".format(self._dof,val)) + + @staticmethod + def test_perpendicular(vector1, vector2): + dor_prod = vector1.dot(vector2) + if abs(dor_prod) <1e-10: + return True + return False + + @staticmethod + def test_parallel(vector1, vector2): + '''For unit vectors only. Returns true if directions are opposite''' + dor_prod = vector1.dot(vector2) + if 1- abs(dor_prod) <1e-10: + return True + return False + + +class PositionVector(ComponentDescription): + r'''This class creates a component of a tomography system with a position attribute + ''' + @property + def position(self): + try: + return self._position + except: + raise AttributeError + + @position.setter + def position(self, val): + self.length_check(val) + self._position = ComponentDescription.create_vector(val) + + +class DirectionVector(ComponentDescription): + r'''This class creates a component of a tomography system with a direction attribute + ''' + @property + def direction(self): + try: + return self._direction + except: + raise AttributeError + + @direction.setter + def direction(self, val): + self.length_check(val) + self._direction = ComponentDescription.create_unit_vector(val) + + +class PositionDirectionVector(PositionVector, DirectionVector): + r'''This class creates a component of a tomography system with position and direction attributes + ''' + pass + + +class Detector1D(PositionVector): + r'''This class creates a component of a tomography system with position and direction_x attributes used for 1D panels + ''' + @property + def direction_x(self): + try: + return self._direction_x + except: + raise AttributeError + + @direction_x.setter + def direction_x(self, val): + self.length_check(val) + self._direction_x = ComponentDescription.create_unit_vector(val) + + @property + def normal(self): + try: + return ComponentDescription.create_unit_vector([self._direction_x[1], -self._direction_x[0]]) + except: + raise AttributeError + + +class Detector2D(PositionVector): + r'''This class creates a component of a tomography system with position, direction_x and direction_y attributes used for 2D panels + ''' + @property + def direction_x(self): + try: + return self._direction_x + except: + raise AttributeError + + @property + def direction_y(self): + try: + return self._direction_y + except: + raise AttributeError + + @property + def normal(self): + try: + return numpy.cross(self._direction_x, self._direction_y) + except: + raise AttributeError + + def set_direction(self, x, y): + self.length_check(x) + x = ComponentDescription.create_unit_vector(x) + + self.length_check(y) + y = ComponentDescription.create_unit_vector(y) + + dot_product = x.dot(y) + if not numpy.isclose(dot_product, 0): + raise ValueError("vectors detector.direction_x and detector.direction_y must be orthogonal") + + self._direction_y = y + self._direction_x = x + + +class SystemConfiguration(object): + r'''This is a generic class to hold the description of a tomography system + ''' + + SYSTEM_SIMPLE = 'simple' + SYSTEM_OFFSET = 'offset' + SYSTEM_ADVANCED = 'advanced' + + @property + def dimension(self): + if self._dimension == 2: + return '2D' + else: + return '3D' + + @dimension.setter + def dimension(self,val): + if val != 2 and val != 3: + raise ValueError('Can set up 2D and 3D systems only. got {0}D'.format(val)) + else: + self._dimension = val + + @property + def geometry(self): + return self._geometry + + @geometry.setter + def geometry(self,val): + if val != acquisition_labels["CONE"] and val != acquisition_labels["PARALLEL"]: + raise ValueError('geom_type = {} not recognised please specify \'cone\' or \'parallel\''.format(val)) + else: + self._geometry = val + + def __init__(self, dof, geometry, units='units'): + """Initialises the system component attributes for the acquisition type + """ + self.dimension = dof + self.geometry = geometry + self.units = units + + if geometry == acquisition_labels["PARALLEL"]: + self.ray = DirectionVector(dof) + else: + self.source = PositionVector(dof) + + if dof == 2: + self.detector = Detector1D(dof) + self.rotation_axis = PositionVector(dof) + else: + self.detector = Detector2D(dof) + self.rotation_axis = PositionDirectionVector(dof) + + def __str__(self): + """Implements the string representation of the system configuration + """ + raise NotImplementedError + + def __eq__(self, other): + """Implements the equality check of the system configuration + """ + raise NotImplementedError + + @staticmethod + def rotation_vec_to_y(vec): + ''' returns a rotation matrix that will rotate the projection of vec on the x-y plane to the +y direction [0,1, Z]''' + + vec = ComponentDescription.create_unit_vector(vec) + + axis_rotation = numpy.eye(len(vec)) + + if numpy.allclose(vec[:2],[0,1]): + pass + elif numpy.allclose(vec[:2],[0,-1]): + axis_rotation[0][0] = -1 + axis_rotation[1][1] = -1 + else: + theta = math.atan2(vec[0],vec[1]) + axis_rotation[0][0] = axis_rotation[1][1] = math.cos(theta) + axis_rotation[0][1] = -math.sin(theta) + axis_rotation[1][0] = math.sin(theta) + + return axis_rotation + + @staticmethod + def rotation_vec_to_z(vec): + ''' returns a rotation matrix that will align vec with the z-direction [0,0,1]''' + + vec = ComponentDescription.create_unit_vector(vec) + + if len(vec) == 2: + return numpy.array([[1, 0],[0, 1]]) + + elif len(vec) == 3: + axis_rotation = numpy.eye(3) + + if numpy.allclose(vec,[0,0,1]): + pass + elif numpy.allclose(vec,[0,0,-1]): + axis_rotation = numpy.eye(3) + axis_rotation[1][1] = -1 + axis_rotation[2][2] = -1 + else: + vx = numpy.array([[0, 0, -vec[0]], [0, 0, -vec[1]], [vec[0], vec[1], 0]]) + axis_rotation = numpy.eye(3) + vx + vx.dot(vx) * 1 / (1 + vec[2]) + + else: + raise ValueError("Vec must have length 3, got {}".format(len(vec))) + + return axis_rotation + + def update_reference_frame(self): + r'''Transforms the system origin to the rotation_axis position + ''' + self.set_origin(self.rotation_axis.position) + + + def set_origin(self, origin): + r'''Transforms the system origin to the input origin + ''' + translation = origin.copy() + if hasattr(self,'source'): + self.source.position -= translation + + self.detector.position -= translation + self.rotation_axis.position -= translation + + + def get_centre_slice(self): + """Returns the 2D system configuration corresponding to the centre slice + """ + raise NotImplementedError + + def calculate_magnification(self): + r'''Calculates the magnification of the system using the source to rotate axis, + and source to detector distance along the direction. + + :return: returns [dist_source_center, dist_center_detector, magnification], [0] distance from the source to the rotate axis, [1] distance from the rotate axis to the detector, [2] magnification of the system + :rtype: list + ''' + raise NotImplementedError + + def system_description(self): + r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, + \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets + \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets + ''' + raise NotImplementedError + + def copy(self): + '''returns a copy of SystemConfiguration''' + return copy.deepcopy(self) From d9d0bd88eaa7208e52a718cd50675e7f39495e03 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Sat, 10 Feb 2024 17:33:29 +0000 Subject: [PATCH 17/72] Rename framework.py to processors.py to better reflect its new purpose. --- Wrappers/Python/cil/framework/__init__.py | 9 +-------- .../Python/cil/framework/{framework.py => processors.py} | 0 2 files changed, 1 insertion(+), 8 deletions(-) rename Wrappers/Python/cil/framework/{framework.py => processors.py} (100%) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index f6d4b489fa..f7b47dc93f 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -17,19 +17,12 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -import numpy -import sys -from datetime import timedelta, datetime -import warnings -from functools import reduce from .cilacc import cilacc from .AcquisitionData import AcquisitionData from .acquisition_geometry import AcquisitionGeometry from .system_configuration import SystemConfiguration -from .framework import find_key from .DataContainer import message, ImageGeometry, DataContainer, ImageData, VectorData, VectorGeometry -from .framework import DataProcessor, Processor -from .framework import AX, PixelByPixelDataProcessor, CastDataContainer +from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer, find_key from .BlockDataContainer import BlockDataContainer from .BlockGeometry import BlockGeometry from .Partitioner import Partitioner diff --git a/Wrappers/Python/cil/framework/framework.py b/Wrappers/Python/cil/framework/processors.py similarity index 100% rename from Wrappers/Python/cil/framework/framework.py rename to Wrappers/Python/cil/framework/processors.py From cd145c0eb00d5aa80b04f0335da872a04f44ee26 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Sat, 10 Feb 2024 17:35:29 +0000 Subject: [PATCH 18/72] Bugfix (bad import). --- Wrappers/Python/cil/framework/system_configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wrappers/Python/cil/framework/system_configuration.py b/Wrappers/Python/cil/framework/system_configuration.py index e1d2827329..cd52b4cb08 100644 --- a/Wrappers/Python/cil/framework/system_configuration.py +++ b/Wrappers/Python/cil/framework/system_configuration.py @@ -3,7 +3,7 @@ import numpy -from cil.framework import acquisition_labels +from .label import acquisition_labels class ComponentDescription(object): From 1d5579232ea743a088df705e6e43640f501dabca Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Sat, 10 Feb 2024 18:01:00 +0000 Subject: [PATCH 19/72] Rename AquisitionData.py file to acquisition_data.py to avoid import ambiguity. --- Wrappers/Python/cil/framework/__init__.py | 2 +- .../cil/framework/{AcquisitionData.py => acquisition_data.py} | 0 Wrappers/Python/cil/framework/acquisition_geometry.py | 2 +- Wrappers/Python/test/test_SIRF.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename Wrappers/Python/cil/framework/{AcquisitionData.py => acquisition_data.py} (100%) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index f7b47dc93f..5a25eb0768 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -18,7 +18,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from .cilacc import cilacc -from .AcquisitionData import AcquisitionData +from .acquisition_data import AcquisitionData from .acquisition_geometry import AcquisitionGeometry from .system_configuration import SystemConfiguration from .DataContainer import message, ImageGeometry, DataContainer, ImageData, VectorData, VectorGeometry diff --git a/Wrappers/Python/cil/framework/AcquisitionData.py b/Wrappers/Python/cil/framework/acquisition_data.py similarity index 100% rename from Wrappers/Python/cil/framework/AcquisitionData.py rename to Wrappers/Python/cil/framework/acquisition_data.py diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index e712b72ef0..e716e67144 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -6,7 +6,7 @@ import numpy from .label import acquisition_labels, data_order -from .AcquisitionData import AcquisitionData +from .acquisition_data import AcquisitionData from .base import BaseAcquisitionGeometry from .DataContainer import ImageGeometry from .system_configuration import ComponentDescription, PositionVector, PositionDirectionVector, SystemConfiguration diff --git a/Wrappers/Python/test/test_SIRF.py b/Wrappers/Python/test/test_SIRF.py index 205fc90a22..2457d33279 100644 --- a/Wrappers/Python/test/test_SIRF.py +++ b/Wrappers/Python/test/test_SIRF.py @@ -19,7 +19,7 @@ import unittest -import cil.framework.AcquisitionData +import cil.framework.acquisition_data import cil.framework.DataContainer from utils import initialise_tests import numpy as np From f561cd05b22fa3f532b5d46751afeafb4359dd56 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Sat, 10 Feb 2024 18:02:42 +0000 Subject: [PATCH 20/72] Rename BlockDataContainer.py file to block_data_container.py to avoid import ambiguity. --- Wrappers/Python/cil/framework/BlockGeometry.py | 2 +- Wrappers/Python/cil/framework/__init__.py | 2 +- .../{BlockDataContainer.py => block_data_container.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename Wrappers/Python/cil/framework/{BlockDataContainer.py => block_data_container.py} (100%) diff --git a/Wrappers/Python/cil/framework/BlockGeometry.py b/Wrappers/Python/cil/framework/BlockGeometry.py index f7eb743757..69a96acd0a 100644 --- a/Wrappers/Python/cil/framework/BlockGeometry.py +++ b/Wrappers/Python/cil/framework/BlockGeometry.py @@ -18,7 +18,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt import functools -from .BlockDataContainer import BlockDataContainer +from .block_data_container import BlockDataContainer class BlockGeometry(object): diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 5a25eb0768..520a04e441 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -23,7 +23,7 @@ from .system_configuration import SystemConfiguration from .DataContainer import message, ImageGeometry, DataContainer, ImageData, VectorData, VectorGeometry from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer, find_key -from .BlockDataContainer import BlockDataContainer +from .block_data_container import BlockDataContainer from .BlockGeometry import BlockGeometry from .Partitioner import Partitioner from .label import acquisition_labels, image_labels, data_order, get_order_for_engine, check_order_for_engine diff --git a/Wrappers/Python/cil/framework/BlockDataContainer.py b/Wrappers/Python/cil/framework/block_data_container.py similarity index 100% rename from Wrappers/Python/cil/framework/BlockDataContainer.py rename to Wrappers/Python/cil/framework/block_data_container.py From d049b9b6b4722fc538cf6d54292469927ec2a9a8 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Sat, 10 Feb 2024 18:03:33 +0000 Subject: [PATCH 21/72] Rename Partitioner.py file to partitioner.py to avoid import ambiguity. --- Wrappers/Python/cil/framework/__init__.py | 2 +- Wrappers/Python/cil/framework/acquisition_data.py | 2 +- .../Python/cil/framework/{Partitioner.py => partitioner.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename Wrappers/Python/cil/framework/{Partitioner.py => partitioner.py} (100%) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 520a04e441..9b35cc5712 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -25,5 +25,5 @@ from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer, find_key from .block_data_container import BlockDataContainer from .BlockGeometry import BlockGeometry -from .Partitioner import Partitioner +from .partitioner import Partitioner from .label import acquisition_labels, image_labels, data_order, get_order_for_engine, check_order_for_engine diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index 0a0ca4235d..18370aa703 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -1,7 +1,7 @@ import numpy from .DataContainer import DataContainer -from .Partitioner import Partitioner +from .partitioner import Partitioner class AcquisitionData(DataContainer, Partitioner): '''DataContainer for holding 2D or 3D sinogram''' diff --git a/Wrappers/Python/cil/framework/Partitioner.py b/Wrappers/Python/cil/framework/partitioner.py similarity index 100% rename from Wrappers/Python/cil/framework/Partitioner.py rename to Wrappers/Python/cil/framework/partitioner.py From 82c059a1ba43fcc09b3f56c45ba12f70292d3182 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Sat, 10 Feb 2024 18:07:59 +0000 Subject: [PATCH 22/72] Rename BlockGeometry.py file to block_geometry.py to avoid import ambiguity. --- Wrappers/Python/cil/framework/__init__.py | 2 +- .../{BlockGeometry.py => block_geometry.py} | 212 +++++++++--------- Wrappers/Python/cil/framework/partitioner.py | 2 +- .../astra/operators/ProjectionOperator.py | 10 +- .../cil/plugins/tigre/ProjectionOperator.py | 7 +- 5 files changed, 115 insertions(+), 118 deletions(-) rename Wrappers/Python/cil/framework/{BlockGeometry.py => block_geometry.py} (97%) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 9b35cc5712..d66ff10abf 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -24,6 +24,6 @@ from .DataContainer import message, ImageGeometry, DataContainer, ImageData, VectorData, VectorGeometry from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer, find_key from .block_data_container import BlockDataContainer -from .BlockGeometry import BlockGeometry +from .block_geometry import BlockGeometry from .partitioner import Partitioner from .label import acquisition_labels, image_labels, data_order, get_order_for_engine, check_order_for_engine diff --git a/Wrappers/Python/cil/framework/BlockGeometry.py b/Wrappers/Python/cil/framework/block_geometry.py similarity index 97% rename from Wrappers/Python/cil/framework/BlockGeometry.py rename to Wrappers/Python/cil/framework/block_geometry.py index 69a96acd0a..9a3946e371 100644 --- a/Wrappers/Python/cil/framework/BlockGeometry.py +++ b/Wrappers/Python/cil/framework/block_geometry.py @@ -1,107 +1,107 @@ -# -*- coding: utf-8 -*- -# Copyright 2019 United Kingdom Research and Innovation -# Copyright 2019 The University of Manchester -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Authors: -# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt - -import functools -from .block_data_container import BlockDataContainer - -class BlockGeometry(object): - - RANDOM = 'random' - RANDOM_INT = 'random_int' - - @property - def dtype(self): - return tuple(i.dtype for i in self.geometries) - - '''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, **kwargs): - - '''Allocates a BlockDataContainer according to geometries contained in the BlockGeometry''' - - symmetry = kwargs.get('symmetry',False) - containers = [geom.allocate(value, **kwargs) 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\ c - # 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) - - def __iter__(self): - '''BlockGeometry is an iterable''' - return self - - def __next__(self): - '''BlockGeometry is an iterable''' - if self.index < len(self.geometries): - result = self.geometries[self.index] - self.index += 1 - return result - else: - self.index = 0 +# -*- coding: utf-8 -*- +# Copyright 2019 United Kingdom Research and Innovation +# Copyright 2019 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt + +import functools +from .block_data_container import BlockDataContainer + +class BlockGeometry(object): + + RANDOM = 'random' + RANDOM_INT = 'random_int' + + @property + def dtype(self): + return tuple(i.dtype for i in self.geometries) + + '''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, **kwargs): + + '''Allocates a BlockDataContainer according to geometries contained in the BlockGeometry''' + + symmetry = kwargs.get('symmetry',False) + containers = [geom.allocate(value, **kwargs) 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\ c + # 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) + + def __iter__(self): + '''BlockGeometry is an iterable''' + return self + + def __next__(self): + '''BlockGeometry is an iterable''' + if self.index < len(self.geometries): + result = self.geometries[self.index] + self.index += 1 + return result + else: + self.index = 0 raise StopIteration \ No newline at end of file diff --git a/Wrappers/Python/cil/framework/partitioner.py b/Wrappers/Python/cil/framework/partitioner.py index 717df5d35b..5e428ba06d 100644 --- a/Wrappers/Python/cil/framework/partitioner.py +++ b/Wrappers/Python/cil/framework/partitioner.py @@ -2,7 +2,7 @@ import numpy -from .BlockGeometry import BlockGeometry +from .block_geometry import BlockGeometry class Partitioner(object): diff --git a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py index 51d70ca0de..1e80053b6d 100644 --- a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py @@ -17,14 +17,12 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import check_order_for_engine -from cil.optimisation.operators import LinearOperator, ChannelwiseOperator -from cil.framework.BlockGeometry import BlockGeometry -from cil.optimisation.operators import BlockOperator -from cil.plugins.astra.operators import AstraProjector3D -from cil.plugins.astra.operators import AstraProjector2D import logging +from cil.framework import check_order_for_engine, BlockGeometry +from cil.optimisation.operators import BlockOperator, LinearOperator, ChannelwiseOperator +from cil.plugins.astra.operators import AstraProjector2D, AstraProjector3D + class ProjectionOperator(LinearOperator): """ diff --git a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py index f29bd41c1a..2e0e3cc29a 100644 --- a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py @@ -17,10 +17,9 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import ImageData, AcquisitionData, AcquisitionGeometry, check_order_for_engine, acquisition_labels -from cil.framework.BlockGeometry import BlockGeometry -from cil.optimisation.operators import BlockOperator -from cil.optimisation.operators import LinearOperator +from cil.framework import (ImageData, AcquisitionData, AcquisitionGeometry, check_order_for_engine, acquisition_labels, + BlockGeometry) +from cil.optimisation.operators import BlockOperator, LinearOperator from cil.plugins.tigre import CIL2TIGREGeometry import numpy as np import logging From f289e6e9cfda02925aa4ddf77ad52c3db0390f5b Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Sat, 10 Feb 2024 18:09:23 +0000 Subject: [PATCH 23/72] Rename DataContainer.py file to data_container.py to avoid import ambiguity. --- Wrappers/Python/cil/framework/__init__.py | 2 +- Wrappers/Python/cil/framework/acquisition_data.py | 2 +- Wrappers/Python/cil/framework/acquisition_geometry.py | 2 +- .../cil/framework/{DataContainer.py => data_container.py} | 0 Wrappers/Python/cil/framework/processors.py | 2 +- Wrappers/Python/test/test_SIRF.py | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename Wrappers/Python/cil/framework/{DataContainer.py => data_container.py} (100%) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index d66ff10abf..1467066a9e 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -21,7 +21,7 @@ from .acquisition_data import AcquisitionData from .acquisition_geometry import AcquisitionGeometry from .system_configuration import SystemConfiguration -from .DataContainer import message, ImageGeometry, DataContainer, ImageData, VectorData, VectorGeometry +from .data_container import message, ImageGeometry, DataContainer, ImageData, VectorData, VectorGeometry from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer, find_key from .block_data_container import BlockDataContainer from .block_geometry import BlockGeometry diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index 18370aa703..5bef682c06 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -1,6 +1,6 @@ import numpy -from .DataContainer import DataContainer +from .data_container import DataContainer from .partitioner import Partitioner class AcquisitionData(DataContainer, Partitioner): diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index e716e67144..9f2a42001a 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -8,7 +8,7 @@ from .label import acquisition_labels, data_order from .acquisition_data import AcquisitionData from .base import BaseAcquisitionGeometry -from .DataContainer import ImageGeometry +from .data_container import ImageGeometry from .system_configuration import ComponentDescription, PositionVector, PositionDirectionVector, SystemConfiguration diff --git a/Wrappers/Python/cil/framework/DataContainer.py b/Wrappers/Python/cil/framework/data_container.py similarity index 100% rename from Wrappers/Python/cil/framework/DataContainer.py rename to Wrappers/Python/cil/framework/data_container.py diff --git a/Wrappers/Python/cil/framework/processors.py b/Wrappers/Python/cil/framework/processors.py index 22f158e34a..960371f8c0 100644 --- a/Wrappers/Python/cil/framework/processors.py +++ b/Wrappers/Python/cil/framework/processors.py @@ -20,7 +20,7 @@ import numpy import weakref -from .DataContainer import DataContainer +from .data_container import DataContainer def find_key(dic, val): diff --git a/Wrappers/Python/test/test_SIRF.py b/Wrappers/Python/test/test_SIRF.py index 2457d33279..3d9812e5c6 100644 --- a/Wrappers/Python/test/test_SIRF.py +++ b/Wrappers/Python/test/test_SIRF.py @@ -20,7 +20,7 @@ import unittest import cil.framework.acquisition_data -import cil.framework.DataContainer +import cil.framework.data_container from utils import initialise_tests import numpy as np from numpy.linalg import norm From 9423e47363a3d49bf397434bc3848600a40a18bd Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Sat, 10 Feb 2024 18:26:01 +0000 Subject: [PATCH 24/72] Amend licenses in files considerably changed by refactoring process. --- Wrappers/Python/cil/framework/__init__.py | 5 +++-- .../Python/cil/framework/acquisition_data.py | 20 +++++++++++++++++++ .../cil/framework/acquisition_geometry.py | 20 +++++++++++++++++++ Wrappers/Python/cil/framework/base.py | 20 +++++++++++++++++++ Wrappers/Python/cil/framework/cilacc.py | 20 +++++++++++++++++++ .../Python/cil/framework/data_container.py | 20 +++++++++++++++++++ Wrappers/Python/cil/framework/partitioner.py | 20 +++++++++++++++++++ Wrappers/Python/cil/framework/processors.py | 5 +++-- .../cil/framework/system_configuration.py | 20 +++++++++++++++++++ 9 files changed, 146 insertions(+), 4 deletions(-) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 1467066a9e..7604334eab 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright 2019 United Kingdom Research and Innovation -# Copyright 2019 The University of Manchester +# Copyright 2024 United Kingdom Research and Innovation +# Copyright 2024 The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +# Joshua DM Hellier (University of Manchester) [refactorer] from .cilacc import cilacc from .acquisition_data import AcquisitionData diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index 5bef682c06..22aa3cdf16 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 United Kingdom Research and Innovation +# Copyright 2024 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +# Joshua DM Hellier (University of Manchester) [refactorer] + import numpy from .data_container import DataContainer diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 9f2a42001a..25ccd50f21 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 United Kingdom Research and Innovation +# Copyright 2024 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +# Joshua DM Hellier (University of Manchester) [refactorer] + import copy import logging import math diff --git a/Wrappers/Python/cil/framework/base.py b/Wrappers/Python/cil/framework/base.py index 0c5e7e9755..a4c196a122 100644 --- a/Wrappers/Python/cil/framework/base.py +++ b/Wrappers/Python/cil/framework/base.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 United Kingdom Research and Innovation +# Copyright 2024 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +# Joshua DM Hellier (University of Manchester) + class BaseAcquisitionGeometry: """This class only exists to give get_order_for_engine something to typecheck for. At some point separate interface stuff into here or refactor get_order_for_engine to not need it.""" diff --git a/Wrappers/Python/cil/framework/cilacc.py b/Wrappers/Python/cil/framework/cilacc.py index 8b457a58aa..f96185d8f7 100644 --- a/Wrappers/Python/cil/framework/cilacc.py +++ b/Wrappers/Python/cil/framework/cilacc.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 United Kingdom Research and Innovation +# Copyright 2024 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +# Joshua DM Hellier (University of Manchester) [refactorer] + import ctypes import platform from ctypes import util diff --git a/Wrappers/Python/cil/framework/data_container.py b/Wrappers/Python/cil/framework/data_container.py index df92ae824c..07e2dd8591 100644 --- a/Wrappers/Python/cil/framework/data_container.py +++ b/Wrappers/Python/cil/framework/data_container.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 United Kingdom Research and Innovation +# Copyright 2024 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +# Joshua DM Hellier (University of Manchester) [refactorer] + import copy import ctypes import logging diff --git a/Wrappers/Python/cil/framework/partitioner.py b/Wrappers/Python/cil/framework/partitioner.py index 5e428ba06d..0bf032f1b0 100644 --- a/Wrappers/Python/cil/framework/partitioner.py +++ b/Wrappers/Python/cil/framework/partitioner.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 United Kingdom Research and Innovation +# Copyright 2024 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +# Joshua DM Hellier (University of Manchester) [refactorer] + import math import numpy diff --git a/Wrappers/Python/cil/framework/processors.py b/Wrappers/Python/cil/framework/processors.py index 960371f8c0..29393a976c 100644 --- a/Wrappers/Python/cil/framework/processors.py +++ b/Wrappers/Python/cil/framework/processors.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright 2018 United Kingdom Research and Innovation -# Copyright 2018 The University of Manchester +# Copyright 2024 United Kingdom Research and Innovation +# Copyright 2024 The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +# Joshua DM Hellier (University of Manchester) [refactorer] import numpy import weakref diff --git a/Wrappers/Python/cil/framework/system_configuration.py b/Wrappers/Python/cil/framework/system_configuration.py index cd52b4cb08..2a3c5d555e 100644 --- a/Wrappers/Python/cil/framework/system_configuration.py +++ b/Wrappers/Python/cil/framework/system_configuration.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 United Kingdom Research and Innovation +# Copyright 2024 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +# Joshua DM Hellier (University of Manchester) [refactorer] + import copy import math From a906387f4d4bd920b7aea0fd8a26adf3bedca5a9 Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Wed, 14 Feb 2024 16:51:22 +0000 Subject: [PATCH 25/72] Fix import issues with SIRF tests, and refactor imports. --- Wrappers/Python/test/test_SIRF.py | 62 ++++++++++++++----------------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/Wrappers/Python/test/test_SIRF.py b/Wrappers/Python/test/test_SIRF.py index 3d9812e5c6..1256ff840e 100644 --- a/Wrappers/Python/test/test_SIRF.py +++ b/Wrappers/Python/test/test_SIRF.py @@ -17,25 +17,18 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -import unittest - -import cil.framework.acquisition_data -import cil.framework.data_container -from utils import initialise_tests -import numpy as np -from numpy.linalg import norm import os import shutil -from cil.framework import BlockDataContainer -from cil.optimisation.operators import GradientOperator, LinearOperator -from cil.optimisation.functions import TotalVariation, L2NormSquared, KullbackLeibler -from cil.optimisation.algorithms import FISTA +import unittest -import os -from cil.utilities.display import show2D +import numpy as np +from cil.framework import AcquisitionData, ImageData, BlockDataContainer +from cil.optimisation.algorithms import FISTA +from cil.optimisation.functions import TotalVariation, L2NormSquared, KullbackLeibler +from cil.optimisation.operators import GradientOperator, LinearOperator from testclass import CCPiTestClass -from utils import has_nvidia, has_ccpi_regularisation, initialise_tests +from utils import has_ccpi_regularisation, initialise_tests initialise_tests() @@ -50,7 +43,6 @@ has_sirf = False if has_ccpi_regularisation: - from ccpi.filters import regularisers from cil.plugins.ccpi_regularisation.functions import FGP_TV, TGV, FGP_dTV, TNV @@ -60,7 +52,7 @@ class KullbackLeiblerSIRF(object): def setUp(self): if has_sirf: - self.image1 = cil.framework.DataContainer.ImageData(os.path.join( + self.image1 = ImageData(os.path.join( examples_data_path('PET'),'thorax_single_slice','emission.hv') ) @@ -151,12 +143,12 @@ def test_Gradient(self): for i in range(len(res1)): - if isinstance(self.image1, cil.framework.DataContainer.ImageData): - self.assertTrue(isinstance(res1[i], cil.framework.DataContainer.ImageData)) - self.assertTrue(isinstance(res2[i], cil.framework.DataContainer.ImageData)) + if isinstance(self.image1, ImageData): + self.assertTrue(isinstance(res1[i], ImageData)) + self.assertTrue(isinstance(res2[i], ImageData)) else: - self.assertTrue(isinstance(res1[i], cil.framework.DataContainer.ImageData)) - self.assertTrue(isinstance(res2[i], cil.framework.DataContainer.ImageData)) + self.assertTrue(isinstance(res1[i], ImageData)) + self.assertTrue(isinstance(res2[i], ImageData)) # test direct with and without out np.testing.assert_array_almost_equal(res1[i].as_array(), res2[i].as_array()) @@ -213,7 +205,7 @@ class TestGradientPET_2D(unittest.TestCase, GradientSIRF): def setUp(self): if has_sirf: - self.image1 = cil.framework.DataContainer.ImageData(os.path.join( + self.image1 = ImageData(os.path.join( examples_data_path('PET'),'thorax_single_slice','emission.hv') ) @@ -225,7 +217,7 @@ class TestGradientPET_3D(unittest.TestCase, GradientSIRF): def setUp(self): if has_sirf: - self.image1 = cil.framework.DataContainer.ImageData(os.path.join( + self.image1 = ImageData(os.path.join( examples_data_path('PET'),'brain','emission.hv') ) @@ -237,7 +229,7 @@ class TestGradientMR_2D(unittest.TestCase, GradientSIRF): def setUp(self): if has_sirf: - acq_data = cil.framework.AcquisitionData.AcquisitionData(os.path.join + acq_data = AcquisitionData(os.path.join (examples_data_path('MR'),'simulated_MR_2D_cartesian.h5') ) preprocessed_data = mr.preprocess_acquisition_data(acq_data) @@ -295,8 +287,8 @@ def tearDown(self): @unittest.skipUnless(has_sirf, "Has SIRF") def test_BlockDataContainer_with_SIRF_DataContainer_divide(self): os.chdir(self.cwd) - image1 = cil.framework.DataContainer.ImageData('emission.hv') - image2 = cil.framework.DataContainer.ImageData('emission.hv') + image1 = ImageData('emission.hv') + image2 = ImageData('emission.hv') image1.fill(1.) image2.fill(2.) @@ -318,8 +310,8 @@ def test_BlockDataContainer_with_SIRF_DataContainer_divide(self): @unittest.skipUnless(has_sirf, "Has SIRF") def test_BlockDataContainer_with_SIRF_DataContainer_multiply(self): os.chdir(self.cwd) - image1 = cil.framework.DataContainer.ImageData('emission.hv') - image2 = cil.framework.DataContainer.ImageData('emission.hv') + image1 = ImageData('emission.hv') + image2 = ImageData('emission.hv') image1.fill(1.) image2.fill(2.) @@ -341,8 +333,8 @@ def test_BlockDataContainer_with_SIRF_DataContainer_multiply(self): @unittest.skipUnless(has_sirf, "Has SIRF") def test_BlockDataContainer_with_SIRF_DataContainer_add(self): os.chdir(self.cwd) - image1 = cil.framework.DataContainer.ImageData('emission.hv') - image2 = cil.framework.DataContainer.ImageData('emission.hv') + image1 = ImageData('emission.hv') + image2 = ImageData('emission.hv') image1.fill(0) image2.fill(1) @@ -367,8 +359,8 @@ def test_BlockDataContainer_with_SIRF_DataContainer_add(self): @unittest.skipUnless(has_sirf, "Has SIRF") def test_BlockDataContainer_with_SIRF_DataContainer_subtract(self): os.chdir(self.cwd) - image1 = cil.framework.DataContainer.ImageData('emission.hv') - image2 = cil.framework.DataContainer.ImageData('emission.hv') + image1 = ImageData('emission.hv') + image2 = ImageData('emission.hv') image1.fill(2) image2.fill(1) @@ -462,7 +454,7 @@ def test_TNV_proximal_works(self): class TestPETRegularisation(unittest.TestCase, CCPiRegularisationWithSIRFTests): skip_TNV_on_2D = True def setUp(self): - self.image1 = cil.framework.DataContainer.ImageData(os.path.join( + self.image1 = ImageData(os.path.join( examples_data_path('PET'),'thorax_single_slice','emission.hv' )) self.image2 = self.image1 * 0.5 @@ -477,12 +469,12 @@ def test_TNV_proximal_works(self): class TestRegRegularisation(unittest.TestCase, CCPiRegularisationWithSIRFTests): def setUp(self): - self.image1 = cil.framework.DataContainer.ImageData(os.path.join(examples_data_path('Registration'), 'test2.nii.gz')) + self.image1 = ImageData(os.path.join(examples_data_path('Registration'), 'test2.nii.gz')) self.image2 = self.image1 * 0.5 class TestMRRegularisation(unittest.TestCase, CCPiRegularisationWithSIRFTests): def setUp(self): - acq_data = cil.framework.AcquisitionData.AcquisitionData(os.path.join(examples_data_path('MR'), 'simulated_MR_2D_cartesian.h5')) + acq_data = AcquisitionData(os.path.join(examples_data_path('MR'), 'simulated_MR_2D_cartesian.h5')) preprocessed_data = mr.preprocess_acquisition_data(acq_data) recon = mr.FullySampledReconstructor() recon.set_input(preprocessed_data) From 945e823672b9fd3adfe452d6ccc0642330b5a1ed Mon Sep 17 00:00:00 2001 From: Joshua DM Hellier Date: Wed, 14 Feb 2024 17:40:36 +0000 Subject: [PATCH 26/72] Revert test_SIRF.py back to the master edition (less stuff affected than thought). --- Wrappers/Python/test/test_SIRF.py | 44 +++++++++++++++---------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Wrappers/Python/test/test_SIRF.py b/Wrappers/Python/test/test_SIRF.py index 1256ff840e..7ecaa70513 100644 --- a/Wrappers/Python/test/test_SIRF.py +++ b/Wrappers/Python/test/test_SIRF.py @@ -23,7 +23,7 @@ import numpy as np -from cil.framework import AcquisitionData, ImageData, BlockDataContainer +from cil.framework import BlockDataContainer from cil.optimisation.algorithms import FISTA from cil.optimisation.functions import TotalVariation, L2NormSquared, KullbackLeibler from cil.optimisation.operators import GradientOperator, LinearOperator @@ -52,7 +52,7 @@ class KullbackLeiblerSIRF(object): def setUp(self): if has_sirf: - self.image1 = ImageData(os.path.join( + self.image1 = pet.ImageData(os.path.join( examples_data_path('PET'),'thorax_single_slice','emission.hv') ) @@ -143,12 +143,12 @@ def test_Gradient(self): for i in range(len(res1)): - if isinstance(self.image1, ImageData): - self.assertTrue(isinstance(res1[i], ImageData)) - self.assertTrue(isinstance(res2[i], ImageData)) + if isinstance(self.image1, pet.ImageData): + self.assertTrue(isinstance(res1[i], pet.ImageData)) + self.assertTrue(isinstance(res2[i], pet.ImageData)) else: - self.assertTrue(isinstance(res1[i], ImageData)) - self.assertTrue(isinstance(res2[i], ImageData)) + self.assertTrue(isinstance(res1[i], mr.ImageData)) + self.assertTrue(isinstance(res2[i], mr.ImageData)) # test direct with and without out np.testing.assert_array_almost_equal(res1[i].as_array(), res2[i].as_array()) @@ -205,7 +205,7 @@ class TestGradientPET_2D(unittest.TestCase, GradientSIRF): def setUp(self): if has_sirf: - self.image1 = ImageData(os.path.join( + self.image1 = pet.ImageData(os.path.join( examples_data_path('PET'),'thorax_single_slice','emission.hv') ) @@ -217,7 +217,7 @@ class TestGradientPET_3D(unittest.TestCase, GradientSIRF): def setUp(self): if has_sirf: - self.image1 = ImageData(os.path.join( + self.image1 = pet.ImageData(os.path.join( examples_data_path('PET'),'brain','emission.hv') ) @@ -229,9 +229,9 @@ class TestGradientMR_2D(unittest.TestCase, GradientSIRF): def setUp(self): if has_sirf: - acq_data = AcquisitionData(os.path.join + acq_data = mr.AcquisitionData(os.path.join (examples_data_path('MR'),'simulated_MR_2D_cartesian.h5') - ) + ) preprocessed_data = mr.preprocess_acquisition_data(acq_data) recon = mr.FullySampledReconstructor() recon.set_input(preprocessed_data) @@ -287,8 +287,8 @@ def tearDown(self): @unittest.skipUnless(has_sirf, "Has SIRF") def test_BlockDataContainer_with_SIRF_DataContainer_divide(self): os.chdir(self.cwd) - image1 = ImageData('emission.hv') - image2 = ImageData('emission.hv') + image1 = pet.ImageData('emission.hv') + image2 = pet.ImageData('emission.hv') image1.fill(1.) image2.fill(2.) @@ -310,8 +310,8 @@ def test_BlockDataContainer_with_SIRF_DataContainer_divide(self): @unittest.skipUnless(has_sirf, "Has SIRF") def test_BlockDataContainer_with_SIRF_DataContainer_multiply(self): os.chdir(self.cwd) - image1 = ImageData('emission.hv') - image2 = ImageData('emission.hv') + image1 = pet.ImageData('emission.hv') + image2 = pet.ImageData('emission.hv') image1.fill(1.) image2.fill(2.) @@ -333,8 +333,8 @@ def test_BlockDataContainer_with_SIRF_DataContainer_multiply(self): @unittest.skipUnless(has_sirf, "Has SIRF") def test_BlockDataContainer_with_SIRF_DataContainer_add(self): os.chdir(self.cwd) - image1 = ImageData('emission.hv') - image2 = ImageData('emission.hv') + image1 = pet.ImageData('emission.hv') + image2 = pet.ImageData('emission.hv') image1.fill(0) image2.fill(1) @@ -359,8 +359,8 @@ def test_BlockDataContainer_with_SIRF_DataContainer_add(self): @unittest.skipUnless(has_sirf, "Has SIRF") def test_BlockDataContainer_with_SIRF_DataContainer_subtract(self): os.chdir(self.cwd) - image1 = ImageData('emission.hv') - image2 = ImageData('emission.hv') + image1 = pet.ImageData('emission.hv') + image2 = pet.ImageData('emission.hv') image1.fill(2) image2.fill(1) @@ -454,7 +454,7 @@ def test_TNV_proximal_works(self): class TestPETRegularisation(unittest.TestCase, CCPiRegularisationWithSIRFTests): skip_TNV_on_2D = True def setUp(self): - self.image1 = ImageData(os.path.join( + self.image1 = pet.ImageData(os.path.join( examples_data_path('PET'),'thorax_single_slice','emission.hv' )) self.image2 = self.image1 * 0.5 @@ -469,12 +469,12 @@ def test_TNV_proximal_works(self): class TestRegRegularisation(unittest.TestCase, CCPiRegularisationWithSIRFTests): def setUp(self): - self.image1 = ImageData(os.path.join(examples_data_path('Registration'), 'test2.nii.gz')) + self.image1 = reg.ImageData(os.path.join(examples_data_path('Registration'),'test2.nii.gz')) self.image2 = self.image1 * 0.5 class TestMRRegularisation(unittest.TestCase, CCPiRegularisationWithSIRFTests): def setUp(self): - acq_data = AcquisitionData(os.path.join(examples_data_path('MR'), 'simulated_MR_2D_cartesian.h5')) + acq_data = mr.AcquisitionData(os.path.join(examples_data_path('MR'),'simulated_MR_2D_cartesian.h5')) preprocessed_data = mr.preprocess_acquisition_data(acq_data) recon = mr.FullySampledReconstructor() recon.set_input(preprocessed_data) From 96c26705107658a6ac3dcfa8aa95033565053b6f Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 27 Mar 2024 11:08:23 +0000 Subject: [PATCH 27/72] backward-compat: DataOrder.get_order_for_engine Signed-off-by: Casper da Costa-Luis --- Wrappers/Python/cil/framework/label.py | 53 +++++++++++++------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/Wrappers/Python/cil/framework/label.py b/Wrappers/Python/cil/framework/label.py index 932116a54d..3e8bfc65ba 100644 --- a/Wrappers/Python/cil/framework/label.py +++ b/Wrappers/Python/cil/framework/label.py @@ -59,6 +59,32 @@ class DataOrder(TypedDict): CIL_AG_LABELS: List[str] TOMOPHANTOM_IG_LABELS: List[str] + @staticmethod + def get_order_for_engine(engine, geometry): + if engine == 'astra': + if isinstance(geometry, BaseAcquisitionGeometry): + dim_order = data_order["ASTRA_AG_LABELS"] + else: + dim_order = data_order["ASTRA_IG_LABELS"] + elif engine == 'tigre': + if isinstance(geometry, BaseAcquisitionGeometry): + dim_order = data_order["TIGRE_AG_LABELS"] + else: + dim_order = data_order["TIGRE_IG_LABELS"] + elif engine == 'cil': + if isinstance(geometry, BaseAcquisitionGeometry): + dim_order = data_order["CIL_AG_LABELS"] + else: + dim_order = data_order["CIL_IG_LABELS"] + else: + raise ValueError("Unknown engine expected one of {0} got {1}".format(data_order["ENGINES"], engine)) + + dimensions = [] + for label in dim_order: + if label in geometry.dimension_labels: + dimensions.append(label) + + return dimensions image_labels: ImageLabels = {"RANDOM": "random", "RANDOM_INT": "random_int", @@ -92,32 +118,7 @@ class DataOrder(TypedDict): "TOMOPHANTOM_IG_LABELS": [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] } - -def get_order_for_engine(engine, geometry): - if engine == 'astra': - if isinstance(geometry, BaseAcquisitionGeometry): - dim_order = data_order["ASTRA_AG_LABELS"] - else: - dim_order = data_order["ASTRA_IG_LABELS"] - elif engine == 'tigre': - if isinstance(geometry, BaseAcquisitionGeometry): - dim_order = data_order["TIGRE_AG_LABELS"] - else: - dim_order = data_order["TIGRE_IG_LABELS"] - elif engine == 'cil': - if isinstance(geometry, BaseAcquisitionGeometry): - dim_order = data_order["CIL_AG_LABELS"] - else: - dim_order = data_order["CIL_IG_LABELS"] - else: - raise ValueError("Unknown engine expected one of {0} got {1}".format(data_order["ENGINES"], engine)) - - dimensions = [] - for label in dim_order: - if label in geometry.dimension_labels: - dimensions.append(label) - - return dimensions +get_order_for_engine = DataOrder.get_order_for_engine def check_order_for_engine(engine, geometry): From 2db4e2c3d6c999c1046e7f5c0de4e1a0558020a7 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 27 Mar 2024 11:13:45 +0000 Subject: [PATCH 28/72] backward-compat: AcquisitionGeometry & ImageGeometry properties Signed-off-by: Casper da Costa-Luis --- .../cil/framework/acquisition_geometry.py | 31 +++++++++++++++++++ .../Python/cil/framework/data_container.py | 29 +++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 25ccd50f21..92745f9be5 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -21,6 +21,7 @@ import copy import logging import math +import warnings from numbers import Number import numpy @@ -1299,6 +1300,36 @@ class AcquisitionGeometry(BaseAcquisitionGeometry): #for backwards compatibility + @property + def ANGLE(self): + warnings.warn("use acquisition_labels['ANGLE'] instead", DeprecationWarning, stacklevel=2) + return acquisition_labels['ANGLE'] + + @property + def CHANNEL(self): + warnings.warn("use acquisition_labels['CHANNEL'] instead", DeprecationWarning, stacklevel=2) + return acquisition_labels['CHANNEL'] + + @property + def DEGREE(self): + warnings.warn("use acquisition_labels['DEGREE'] instead", DeprecationWarning, stacklevel=2) + return acquisition_labels['DEGREE'] + + @property + def HORIZONTAL(self): + warnings.warn("use acquisition_labels['HORIZONTAL'] instead", DeprecationWarning, stacklevel=2) + return acquisition_labels['HORIZONTAL'] + + @property + def RADIAN(self): + warnings.warn("use acquisition_labels['RADIAN'] instead", DeprecationWarning, stacklevel=2) + return acquisition_labels['RADIAN'] + + @property + def VERTICAL(self): + warnings.warn("use acquisition_labels['VERTICAL'] instead", DeprecationWarning, stacklevel=2) + return acquisition_labels['VERTICAL'] + @property def geom_type(self): return self.config.system.geometry diff --git a/Wrappers/Python/cil/framework/data_container.py b/Wrappers/Python/cil/framework/data_container.py index 07e2dd8591..34cb8ab1be 100644 --- a/Wrappers/Python/cil/framework/data_container.py +++ b/Wrappers/Python/cil/framework/data_container.py @@ -43,6 +43,35 @@ def message(cls, msg, *args): class ImageGeometry(object): + @property + def CHANNEL(self): + warnings.warn("use image_labels['CHANNEL'] instead", DeprecationWarning, stacklevel=2) + return image_labels['CHANNEL'] + + @property + def HORIZONTAL_X(self): + warnings.warn("use image_labels['HORIZONTAL_X'] instead", DeprecationWarning, stacklevel=2) + return image_labels['HORIZONTAL_X'] + + @property + def HORIZONTAL_Y(self): + warnings.warn("use image_labels['HORIZONTAL_Y'] instead", DeprecationWarning, stacklevel=2) + return image_labels['HORIZONTAL_Y'] + + @property + def RANDOM(self): + warnings.warn("use image_labels['RANDOM'] instead", DeprecationWarning, stacklevel=2) + return image_labels['RANDOM'] + + @property + def RANDOM_INT(self): + warnings.warn("use image_labels['RANDOM_INT'] instead", DeprecationWarning, stacklevel=2) + return image_labels['RANDOM_INT'] + + @property + def VERTICAL(self): + warnings.warn("use image_labels['VERTICAL'] instead", DeprecationWarning, stacklevel=2) + return image_labels['VERTICAL'] @property def shape(self): From f3f2b1732f113de7b4e289368e05a8df5d048461 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 27 Mar 2024 11:26:01 +0000 Subject: [PATCH 29/72] fix mypy --- Wrappers/Python/cil/framework/data_container.py | 11 +++++------ Wrappers/Python/cil/framework/label.py | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Wrappers/Python/cil/framework/data_container.py b/Wrappers/Python/cil/framework/data_container.py index 34cb8ab1be..2a0fcf5aa6 100644 --- a/Wrappers/Python/cil/framework/data_container.py +++ b/Wrappers/Python/cil/framework/data_container.py @@ -376,15 +376,15 @@ def shape(self): '''Returns the shape of the DataContainer''' return self.array.shape + @shape.setter + def shape(self, val): + print("Deprecated - shape will be set automatically") + @property def ndim(self): '''Returns the ndim of the DataContainer''' return self.array.ndim - @shape.setter - def shape(self, val): - print("Deprecated - shape will be set automatically") - @property def number_of_dimensions(self): '''Returns the shape of the of the DataContainer''' @@ -711,8 +711,7 @@ def get_data_axes_order(self,new_order=None): axes_order[i] = self.dimension_labels.index(axis) return axes_order else: - raise ValueError('Expecting {0} axes, got {2}'\ - .format(len(self.shape),len(new_order))) + raise ValueError(f"Expecting {len(self.shape)} axes, got {len(new_order)}") def clone(self): '''returns a copy of DataContainer''' diff --git a/Wrappers/Python/cil/framework/label.py b/Wrappers/Python/cil/framework/label.py index 3e8bfc65ba..6606814404 100644 --- a/Wrappers/Python/cil/framework/label.py +++ b/Wrappers/Python/cil/framework/label.py @@ -59,7 +59,7 @@ class DataOrder(TypedDict): CIL_AG_LABELS: List[str] TOMOPHANTOM_IG_LABELS: List[str] - @staticmethod + @staticmethod # type: ignore[misc] def get_order_for_engine(engine, geometry): if engine == 'astra': if isinstance(geometry, BaseAcquisitionGeometry): @@ -118,7 +118,7 @@ def get_order_for_engine(engine, geometry): "TOMOPHANTOM_IG_LABELS": [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] } -get_order_for_engine = DataOrder.get_order_for_engine +get_order_for_engine = DataOrder.get_order_for_engine # type: ignore[attr-defined] def check_order_for_engine(engine, geometry): From fcb3858435e7cc6f877d7cf80388b63681cfaf30 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Tue, 9 Apr 2024 11:20:28 +0100 Subject: [PATCH 30/72] fix merge conflicts - logging vs warnings (#1769) - complexobj (#1645) --- .../cil/framework/acquisition_geometry.py | 12 +++++++----- .../Python/cil/framework/data_container.py | 18 +++++++++++------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 92745f9be5..7b5fbeea79 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -19,7 +19,6 @@ # Joshua DM Hellier (University of Manchester) [refactorer] import copy -import logging import math import warnings from numbers import Number @@ -277,7 +276,7 @@ def system_description(self): - rays perpendicular to rotation axis advanced - not rays perpendicular to detector (for parallel just equates to an effective pixel size change?) - or + or - not rays perpendicular to rotation axis (tilted, i.e. laminography) ''' @@ -1617,7 +1616,7 @@ def set_centre_of_rotation_by_slice(self, offset1, slice_index1=None, offset2=No if self.dimension == '2D': if offset2 is not None: - logging.WARNING("Only offset1 is being used") + warnings.warn("2D so offset2 is ingored", UserWarning, stacklevel=2) self.set_centre_of_rotation(offset1) if offset2 is None or offset1 == offset2: @@ -1900,8 +1899,11 @@ def allocate(self, value=0, **kwargs): if seed is not None: numpy.random.seed(seed) max_value = kwargs.get('max_value', 100) - r = numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) - out.fill(numpy.asarray(r, dtype=self.dtype)) + if numpy.iscomplexobj(out.array): + r = numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) + 1j*numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) + else: + r = numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) + out.fill(numpy.asarray(r, dtype=dtype)) elif value is None: pass else: diff --git a/Wrappers/Python/cil/framework/data_container.py b/Wrappers/Python/cil/framework/data_container.py index 2a0fcf5aa6..3130633553 100644 --- a/Wrappers/Python/cil/framework/data_container.py +++ b/Wrappers/Python/cil/framework/data_container.py @@ -20,7 +20,6 @@ import copy import ctypes -import logging import warnings from functools import reduce from numbers import Number @@ -327,8 +326,10 @@ def allocate(self, value=0, **kwargs): if seed is not None: numpy.random.seed(seed) max_value = kwargs.get('max_value', 100) - r = numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) - out.fill(numpy.asarray(r, dtype=self.dtype)) + if numpy.iscomplexobj(out.array): + out.fill(numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) + 1.j*numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32)) + else: + out.fill(numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32)) elif value is None: pass else: @@ -1113,7 +1114,7 @@ def sum(self, axis=None, out=None, *args, **kwargs): Default is to accumulate and return data as float64 or complex128 """ if kwargs.get('dtype') is not None: - logging.WARNING("dtype argument is ignored, using float64 or complex128") + warnings.warn("dtype is ignored (auto-using float64 or complex128)", DeprecationWarning, stacklevel=2) if numpy.isrealobj(self.array): kwargs['dtype'] = numpy.float64 @@ -1191,8 +1192,8 @@ def mean(self, axis=None, out=None, *args, **kwargs): Default is to accumulate and return data as float64 or complex128 """ - if kwargs.get('dtype', None) is not None: - logging.WARNING("dtype argument is ignored, using float64 or complex128") + if kwargs.get('dtype') is not None: + warnings.warn("dtype is ignored (auto-using float64 or complex128)", DeprecationWarning, stacklevel=2) if numpy.isrealobj(self.array): kwargs['dtype'] = numpy.float64 @@ -1535,7 +1536,10 @@ def allocate(self, value=0, **kwargs): seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) - out.fill(numpy.random.random_sample(self.shape)) + if numpy.iscomplexobj(out.array): + out.fill(numpy.random.random_sample(self.shape) + 1.j*numpy.random.random_sample(self.shape)) + else: + out.fill(numpy.random.random_sample(self.shape)) elif value == VectorGeometry.RANDOM_INT: seed = kwargs.get('seed', None) if seed is not None: From e516c7f93016552c8ef0212d7289ee458ee7be8d Mon Sep 17 00:00:00 2001 From: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com> Date: Tue, 16 Apr 2024 10:00:09 +0100 Subject: [PATCH 31/72] Update Wrappers/Python/cil/framework/data_container.py Signed-off-by: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com> --- Wrappers/Python/cil/framework/data_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wrappers/Python/cil/framework/data_container.py b/Wrappers/Python/cil/framework/data_container.py index 3130633553..4f2677a327 100644 --- a/Wrappers/Python/cil/framework/data_container.py +++ b/Wrappers/Python/cil/framework/data_container.py @@ -17,7 +17,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt # Joshua DM Hellier (University of Manchester) [refactorer] - +# Nicholas Whyatt (UKRI-STFC) [refactorer] import copy import ctypes import warnings From daae9d2eda5a6d627e2cf08cf10ff2121b4c444a Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 12 Aug 2024 10:31:57 +0100 Subject: [PATCH 32/72] manual merge resolution --- .../Python/cil/framework/acquisition_data.py | 7 +- .../cil/framework/acquisition_geometry.py | 41 ++- Wrappers/Python/cil/framework/base.py | 7 +- Wrappers/Python/cil/framework/cilacc.py | 7 +- .../Python/cil/framework/data_container.py | 16 +- Wrappers/Python/cil/framework/label.py | 8 +- Wrappers/Python/cil/framework/partitioner.py | 10 +- Wrappers/Python/cil/framework/processors.py | 109 ++++--- .../cil/framework/system_configuration.py | 7 +- .../cil/optimisation/functions/L1Sparsity.py | 30 +- .../optimisation/operators/WaveletOperator.py | 18 +- .../Python/cil/processors/PaganinProcessor.py | 272 +++++++++--------- Wrappers/Python/test/test_DataContainer.py | 6 +- Wrappers/Python/test/test_io.py | 8 +- 14 files changed, 265 insertions(+), 281 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index 22aa3cdf16..479715649d 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- -# Copyright 2024 United Kingdom Research and Innovation -# Copyright 2024 The University of Manchester +# Copyright 2018 United Kingdom Research and Innovation +# Copyright 2018 The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,8 +15,6 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -# Joshua DM Hellier (University of Manchester) [refactorer] - import numpy from .data_container import DataContainer diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 7b5fbeea79..039a40db66 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- -# Copyright 2024 United Kingdom Research and Innovation -# Copyright 2024 The University of Manchester +# Copyright 2018 United Kingdom Research and Innovation +# Copyright 2018 The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,8 +15,6 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -# Joshua DM Hellier (University of Manchester) [refactorer] - import copy import math import warnings @@ -184,7 +181,22 @@ def get_centre_slice(self): return self def calculate_magnification(self): - return [None, None, 1.0] + '''Method to calculate magnification and distance from the sample to + the detector using the detector positions and the rotation axis. + For parallel beam geometry magnification = 1 + + Returns + ------- + list + A list containing the [0] distance from the source to the rotate + axis, [1] distance from the rotate axis to the detector, + [2] magnification of the system + + ''' + ab = (self.rotation_axis.position - self.detector.position) + dist_center_detector = float(numpy.sqrt(ab.dot(ab))) + + return [None, dist_center_detector, 1.0] class Parallel3D(SystemConfiguration): @@ -334,7 +346,22 @@ def __eq__(self, other): return False def calculate_magnification(self): - return [None, None, 1.0] + '''Method to calculate magnification and distance from the sample to + the detector using the detector positions and the rotation axis. + For parallel beam geometry magnification = 1 + + Returns + ------- + list + A list containing the [0] distance from the source to the rotate + axis, [1] distance from the rotate axis to the detector, + [2] magnification of the system + + ''' + ab = (self.rotation_axis.position - self.detector.position) + dist_center_detector = float(numpy.sqrt(ab.dot(ab))) + + return [None, dist_center_detector, 1.0] def get_centre_slice(self): """Returns the 2D system configuration corresponding to the centre slice diff --git a/Wrappers/Python/cil/framework/base.py b/Wrappers/Python/cil/framework/base.py index a4c196a122..5f738b0a84 100644 --- a/Wrappers/Python/cil/framework/base.py +++ b/Wrappers/Python/cil/framework/base.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- -# Copyright 2024 United Kingdom Research and Innovation -# Copyright 2024 The University of Manchester +# Copyright 2018 United Kingdom Research and Innovation +# Copyright 2018 The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,8 +15,6 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -# Joshua DM Hellier (University of Manchester) - class BaseAcquisitionGeometry: """This class only exists to give get_order_for_engine something to typecheck for. At some point separate interface stuff into here or refactor get_order_for_engine to not need it.""" diff --git a/Wrappers/Python/cil/framework/cilacc.py b/Wrappers/Python/cil/framework/cilacc.py index f96185d8f7..fa96ba4c51 100644 --- a/Wrappers/Python/cil/framework/cilacc.py +++ b/Wrappers/Python/cil/framework/cilacc.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- -# Copyright 2024 United Kingdom Research and Innovation -# Copyright 2024 The University of Manchester +# Copyright 2018 United Kingdom Research and Innovation +# Copyright 2018 The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,8 +15,6 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -# Joshua DM Hellier (University of Manchester) [refactorer] - import ctypes import platform from ctypes import util diff --git a/Wrappers/Python/cil/framework/data_container.py b/Wrappers/Python/cil/framework/data_container.py index 4f2677a327..72607c52d0 100644 --- a/Wrappers/Python/cil/framework/data_container.py +++ b/Wrappers/Python/cil/framework/data_container.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- -# Copyright 2024 United Kingdom Research and Innovation -# Copyright 2024 The University of Manchester +# Copyright 2018 United Kingdom Research and Innovation +# Copyright 2018 The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,8 +15,6 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -# Joshua DM Hellier (University of Manchester) [refactorer] -# Nicholas Whyatt (UKRI-STFC) [refactorer] import copy import ctypes import warnings @@ -839,19 +836,15 @@ def sapyb(self, a, y, b, out=None, num_threads=NUM_THREADS): >>> y = ig.allocate(2) >>> out = x.sapyb(a,y,b) ''' - ret_out = False if out is None: out = self * 0. - ret_out = True if out.dtype in [ numpy.float32, numpy.float64 ]: # handle with C-lib _axpby try: self._axpby(a, b, y, out, out.dtype, num_threads) - if ret_out: - return out - return + return out except RuntimeError as rte: warnings.warn("sapyb defaulting to Python due to: {}".format(rte)) except TypeError as te: @@ -865,8 +858,7 @@ def sapyb(self, a, y, b, out=None, num_threads=NUM_THREADS): y.multiply(b, out=out) out.add(ax, out=out) - if ret_out: - return out + return out def _axpby(self, a, b, y, out, dtype=numpy.float32, num_threads=NUM_THREADS): '''performs axpby with cilacc C library, can be done in-place. diff --git a/Wrappers/Python/cil/framework/label.py b/Wrappers/Python/cil/framework/label.py index 6606814404..1fcb3d4786 100644 --- a/Wrappers/Python/cil/framework/label.py +++ b/Wrappers/Python/cil/framework/label.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- -# Copyright 2024 United Kingdom Research and Innovation -# Copyright 2024 The University of Manchester +# Copyright 2018 United Kingdom Research and Innovation +# Copyright 2018 The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,9 +15,6 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -# Joshua DM Hellier (University of Manchester) -# Nicholas Whyatt (UKRI-STFC) - from typing import TypedDict, List from .base import BaseAcquisitionGeometry diff --git a/Wrappers/Python/cil/framework/partitioner.py b/Wrappers/Python/cil/framework/partitioner.py index 0bf032f1b0..02d3791729 100644 --- a/Wrappers/Python/cil/framework/partitioner.py +++ b/Wrappers/Python/cil/framework/partitioner.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- -# Copyright 2024 United Kingdom Research and Innovation -# Copyright 2024 The University of Manchester +# Copyright 2018 United Kingdom Research and Innovation +# Copyright 2018 The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,13 +15,11 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -# Joshua DM Hellier (University of Manchester) [refactorer] - import math import numpy -from .block_geometry import BlockGeometry +from .block import BlockGeometry class Partitioner(object): @@ -174,7 +171,6 @@ def _partition_deterministic(self, num_batches, stagger=False, indices=None): # copy data out = blk_geo.allocate(None) - out.geometry = blk_geo axis = self.dimension_labels.index('angle') for i in range(num_batches): diff --git a/Wrappers/Python/cil/framework/processors.py b/Wrappers/Python/cil/framework/processors.py index 29393a976c..2a16b6eaef 100644 --- a/Wrappers/Python/cil/framework/processors.py +++ b/Wrappers/Python/cil/framework/processors.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- -# Copyright 2024 United Kingdom Research and Innovation -# Copyright 2024 The University of Manchester +# Copyright 2018 United Kingdom Research and Innovation +# Copyright 2018 The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,8 +15,6 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -# Joshua DM Hellier (University of Manchester) [refactorer] - import numpy import weakref @@ -32,7 +29,7 @@ def find_key(dic, val): class Processor(object): '''Defines a generic DataContainer processor - + accepts a DataContainer as input returns a DataContainer `__setattr__` allows additional attributes to be defined @@ -51,7 +48,7 @@ def __init__(self, **attributes): for key, value in attributes.items(): self.__dict__[key] = value - + def __setattr__(self, name, value): if name == 'input': self.set_input(value) @@ -63,11 +60,11 @@ def __setattr__(self, name, value): pass elif name == 'output': self.__dict__['shouldRun'] = False - else: + else: self.__dict__['shouldRun'] = True else: raise KeyError('Attribute {0} not found'.format(name)) - + def set_input(self, dataset): """ Set the input data to the processor @@ -87,16 +84,16 @@ def set_input(self, dataset): else: raise TypeError("Input type mismatch: got {0} expecting {1}" \ .format(type(dataset), DataContainer)) - + def check_input(self, dataset): '''Checks parameters of the input DataContainer - + Should raise an Error if the DataContainer does not match expectation, e.g. if the expected input DataContainer is 3D and the Processor expects 2D. ''' raise NotImplementedError('Implement basic checks for input DataContainer') - + def get_output(self, out=None): """ Runs the configured processor and returns the processed data @@ -105,7 +102,7 @@ def get_output(self, out=None): ---------- out : DataContainer, optional Fills the referenced DataContainer with the processed data and suppresses the return - + Returns ------- DataContainer @@ -117,25 +114,25 @@ def get_output(self, out=None): else: self.process(out=out) - if self.store_output: + if self.store_output: self.output = out.copy() - + return out else: return self.output.copy() - - + + def set_input_processor(self, processor): if issubclass(type(processor), DataProcessor): self.__dict__['input'] = weakref.ref(processor) else: raise TypeError("Input type mismatch: got {0} expecting {1}"\ .format(type(processor), DataProcessor)) - + def get_input(self): '''returns the input DataContainer - + It is useful in the case the user has provided a DataProcessor as input ''' @@ -146,16 +143,16 @@ def get_input(self): else: dsi = self.input() return dsi - + def process(self, out=None): raise NotImplementedError('process must be implemented') - + def __call__(self, x, out=None): - - self.set_input(x) + + self.set_input(x) if out is None: - out = self.get_output() + out = self.get_output() else: self.get_output(out=out) @@ -169,10 +166,10 @@ class DataProcessor(Processor): class DataProcessor23D(DataProcessor): '''Regularizers DataProcessor ''' - + def check_input(self, dataset): '''Checks number of dimensions input DataContainer - + Expected input is 2D or 3D ''' if dataset.number_of_dimensions == 2 or \ @@ -181,7 +178,7 @@ def check_input(self, dataset): else: raise ValueError("Expected input dimensions is 2 or 3, got {0}"\ .format(dataset.number_of_dimensions)) - + ###### Example of DataProcessors class AX(DataProcessor): @@ -195,20 +192,20 @@ class AX(DataProcessor): x a DataContainer. ''' - + def __init__(self): - kwargs = {'scalar':None, - 'input':None, + kwargs = {'scalar':None, + 'input':None, } - + #DataProcessor.__init__(self, **kwargs) super(AX, self).__init__(**kwargs) - + def check_input(self, dataset): return True - + def process(self, out=None): - + dsi = self.get_input() a = self.scalar if out is None: @@ -218,7 +215,7 @@ def process(self, out=None): return y else: out.fill(a * dsi.as_array()) - + ###### Example of DataProcessors @@ -233,62 +230,58 @@ class CastDataContainer(DataProcessor): x a DataContainer. ''' - + def __init__(self, dtype=None): - kwargs = {'dtype':dtype, - 'input':None, + kwargs = {'dtype':dtype, + 'input':None, } - + #DataProcessor.__init__(self, **kwargs) super(CastDataContainer, self).__init__(**kwargs) - + def check_input(self, dataset): return True - + def process(self, out=None): - + dsi = self.get_input() dtype = self.dtype if out is None: y = numpy.asarray(dsi.as_array(), dtype=dtype) - + return type(dsi)(numpy.asarray(dsi.as_array(), dtype=dtype), dimension_labels=dsi.dimension_labels ) else: out.fill(numpy.asarray(dsi.as_array(), dtype=dtype)) - + class PixelByPixelDataProcessor(DataProcessor): '''Example DataProcessor - + This processor applies a python function to each pixel of the DataContainer - + f is a python function x a DataSet. ''' - + def __init__(self): - kwargs = {'pyfunc':None, - 'input':None, + kwargs = {'pyfunc':None, + 'input':None, } #DataProcessor.__init__(self, **kwargs) super(PixelByPixelDataProcessor, self).__init__(**kwargs) - + def check_input(self, dataset): return True - + def process(self, out=None): - + pyfunc = self.pyfunc dsi = self.get_input() - + eval_func = numpy.frompyfunc(pyfunc,1,1) - + y = DataContainer(eval_func(dsi.as_array()), True, dimension_labels=dsi.dimension_labels) return y - - - - diff --git a/Wrappers/Python/cil/framework/system_configuration.py b/Wrappers/Python/cil/framework/system_configuration.py index 2a3c5d555e..ffab2913d2 100644 --- a/Wrappers/Python/cil/framework/system_configuration.py +++ b/Wrappers/Python/cil/framework/system_configuration.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- -# Copyright 2024 United Kingdom Research and Innovation -# Copyright 2024 The University of Manchester +# Copyright 2018 United Kingdom Research and Innovation +# Copyright 2018 The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,8 +15,6 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -# Joshua DM Hellier (University of Manchester) [refactorer] - import copy import math diff --git a/Wrappers/Python/cil/optimisation/functions/L1Sparsity.py b/Wrappers/Python/cil/optimisation/functions/L1Sparsity.py index 7f4ffa3705..201af17bdd 100644 --- a/Wrappers/Python/cil/optimisation/functions/L1Sparsity.py +++ b/Wrappers/Python/cil/optimisation/functions/L1Sparsity.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2023 United Kingdom Research and Innovation # Copyright 2023 The University of Manchester # @@ -16,7 +15,6 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt - from cil.optimisation.functions import Function, L1Norm import warnings @@ -24,7 +22,7 @@ class L1Sparsity(Function): r"""L1Sparsity function - Calculates the following cases, depending on if the optional parameter `weight` or data `b` is passed. For `weight=None`: + Calculates the following cases, depending on if the optional parameter `weight` or data `b` is passed. For `weight=None`: a) .. math:: F(x) = ||Qx||_{1} @@ -36,14 +34,14 @@ class L1Sparsity(Function): b) .. math:: F(x) = ||Qx - b||_{L^1(w)} with :math:`||x||_{L^1(w)} = || x \cdot w||_1 = \sum_{i=1}^{n} |x_i| w_i`. - - In all cases :math:`Q` is an orthogonal operator. - + + In all cases :math:`Q` is an orthogonal operator. + Parameters --------- - Q: orthogonal Operator - Note that for the correct calculation of the proximal the provided operator must be orthogonal - b : Data, DataContainer, default is None + Q: orthogonal Operator + Note that for the correct calculation of the proximal the provided operator must be orthogonal + b : Data, DataContainer, default is None weight: array, optional, default=None non-negative weight array matching the size of the range of operator :math:`Q`. """ @@ -52,7 +50,7 @@ def __init__(self, Q, b=None, weight=None): '''creator ''' - if not Q.is_orthogonal(): + if not Q.is_orthogonal(): warnings.warn( f"Invalid operator: `{Q}`. L1Sparsity is properly defined only for orthogonal operators!", UserWarning) @@ -74,19 +72,19 @@ def __call__(self, x): a) .. math:: F(x) = ||Qx||_{L^1(w)} b) .. math:: F(x) = ||Qx - b||_{L^1(w)} - with :math:`|| y ||_{L^1(w)} = || y w ||_1 = \sum_{i=1}^{n} | y_i | w_i`. - + with :math:`|| y ||_{L^1(w)} = || y w ||_1 = \sum_{i=1}^{n} | y_i | w_i`. + """ y = self.Q.direct(x) return self.l1norm(y) def convex_conjugate(self, x): r"""Returns the value of the convex conjugate of the L1Sparsity function at x. - Here, we need to use the convex conjugate of L1Sparsity, which is the Indicator of the unit + Here, we need to use the convex conjugate of L1Sparsity, which is the Indicator of the unit :math:`\ell^{\infty}` norm on the range of the (bijective) operator Q. - Consider the non-weighted case: + Consider the non-weighted case: a) .. math:: F^{*}(x^{*}) = \mathbb{I}_{\{\|\cdot\|_{\infty}\leq1\}}(Qx^{*}) @@ -109,8 +107,8 @@ def convex_conjugate(self, x): b) .. math:: F^{*}(x^{*}) = \mathbb{I}_{\{\|\cdot\|_{L^\infty(w^{-1})}\leq 1\}}(Qx^{*}) + \langle Qx^{*},b\rangle with :math:`\|x\|_{L^\infty(w^{-1})} = \max_{i} \frac{|x_i|}{w_i}` and possible cases of 0 / 0 are defined to be 1. - - + + """ y = self.Q.direct(x) return self.l1norm.convex_conjugate(y) diff --git a/Wrappers/Python/cil/optimisation/operators/WaveletOperator.py b/Wrappers/Python/cil/optimisation/operators/WaveletOperator.py index 04496316a1..8e8264c58e 100644 --- a/Wrappers/Python/cil/optimisation/operators/WaveletOperator.py +++ b/Wrappers/Python/cil/optimisation/operators/WaveletOperator.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2023 United Kingdom Research and Innovation # Copyright 2023 The University of Manchester # @@ -16,7 +15,6 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt - import numpy as np import pywt # PyWavelets module import warnings @@ -27,12 +25,12 @@ class WaveletOperator(LinearOperator): - r''' + r''' Computes forward or inverse (adjoint) discrete wavelet transform (DWT) of the input Parameters ---------- - domain_geometry: cil geometry + domain_geometry: cil geometry Domain geometry for the WaveletOperator range_geometry: cil geometry, optional Output geometry for the WaveletOperator. Default = domain_geometry with the right coefficient array size deduced from pywavelets @@ -63,7 +61,7 @@ class WaveletOperator(LinearOperator): Note ---- - We currently do not support wavelets that are not orthogonal or bi-orthogonal. + We currently do not support wavelets that are not orthogonal or bi-orthogonal. ''' def __init__(self, domain_geometry, @@ -195,7 +193,7 @@ def direct(self, x, out=None): Returns -------- - DataContainer, the value of the WaveletOperator applied to :math:`x` or `None` if `out` + DataContainer, the value of the WaveletOperator applied to :math:`x` or `None` if `out` """ @@ -226,7 +224,7 @@ def adjoint(self, Wx, out=None): Returns -------- - DataContainer, the value of the adjoint of the WaveletOperator applied to :math:`x` or `None` if `out` + DataContainer, the value of the adjoint of the WaveletOperator applied to :math:`x` or `None` if `out` """ @@ -256,7 +254,7 @@ def calculate_norm(self): Returns -------- - norm: float + norm: float ''' if self._wavelet.orthogonal: norm = 1.0 @@ -266,9 +264,9 @@ def calculate_norm(self): def is_orthogonal(self): '''Returns if the operator is orthogonal - + Returns ------- `Bool` ''' - return self._wavelet.orthogonal \ No newline at end of file + return self._wavelet.orthogonal diff --git a/Wrappers/Python/cil/processors/PaganinProcessor.py b/Wrappers/Python/cil/processors/PaganinProcessor.py index 33ad87841b..acea978413 100644 --- a/Wrappers/Python/cil/processors/PaganinProcessor.py +++ b/Wrappers/Python/cil/processors/PaganinProcessor.py @@ -14,10 +14,10 @@ # limitations under the License. # # Authors: -# CIL Developers, listed at: +# CIL Developers, listed at: # https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import Processor, AcquisitionData, DataOrder +from cil.framework import Processor, AcquisitionData, get_order_for_engine import numpy as np from scipy.fft import fft2 @@ -32,51 +32,51 @@ class PaganinProcessor(Processor): r""" - Processor to retrieve quantitative information from phase contrast images + Processor to retrieve quantitative information from phase contrast images using the Paganin phase retrieval algorithm described in [1] - + Parameters ---------- delta: float (optional) - Real part of the deviation of the material refractive index from 1, + Real part of the deviation of the material refractive index from 1, where refractive index :math:`n = (1 - \delta) + i \beta` energy- - dependent refractive index information for x-ray wavelengths can be + dependent refractive index information for x-ray wavelengths can be found at [2], default is 1 - + beta: float (optional) - Complex part of the material refractive index, where refractive index - :math:`n = (1 - \delta) + i \beta` energy-dependent refractive index + Complex part of the material refractive index, where refractive index + :math:`n = (1 - \delta) + i \beta` energy-dependent refractive index information for x-ray wavelengths can be found at [2], default is 1e-2 - + energy: float (optional) Energy of the incident photon, default is 40000 energy_units: string (optional) Energy units, default is 'eV' - + full_retrieval : bool, optional - If True, perform the full phase retrieval and return the thickness. If + If True, perform the full phase retrieval and return the thickness. If False, return a filtered image, default is True filter_type: string (optional) - The form of the Paganin filter to use, either 'paganin_method' - (default) or 'generalised_paganin_method' as described in [3] + The form of the Paganin filter to use, either 'paganin_method' + (default) or 'generalised_paganin_method' as described in [3] pad: int (optional) - Number of pixels to pad the image in Fourier space to reduce aliasing, - default is 0 + Number of pixels to pad the image in Fourier space to reduce aliasing, + default is 0 return_units: string (optional) - The distance units to return the sample thickness in, must be one of - 'm', 'cm', 'mm' or 'um'. Only applies if full_retrieval=True (default + The distance units to return the sample thickness in, must be one of + 'm', 'cm', 'mm' or 'um'. Only applies if full_retrieval=True (default is'cm') Returns ------- AcquisitionData - AcquisitionData corrected for phase effects, retrieved sample thickness - or (if :code:`full_retrieval=False`) filtered data - + AcquisitionData corrected for phase effects, retrieved sample thickness + or (if :code:`full_retrieval=False`) filtered data + Example ------- >>> processor = PaganinProcessor(delta=5, beta=0.05, energy=18000) @@ -100,77 +100,77 @@ class PaganinProcessor(Processor): ----- This processor will work most efficiently using the cil data order with `data.reorder('cil')` - + Notes ----- - This processor uses the phase retrieval algorithm described by Paganin et + This processor uses the phase retrieval algorithm described by Paganin et al. [1] to retrieve the sample thickness - - .. math:: T(x,y) = - \frac{1}{\mu}\ln\left (\mathcal{F}^{-1}\left - (\frac{\mathcal{F}\left ( M^2I_{norm}(x, y,z = \Delta) \right )}{1 + + + .. math:: T(x,y) = - \frac{1}{\mu}\ln\left (\mathcal{F}^{-1}\left + (\frac{\mathcal{F}\left ( M^2I_{norm}(x, y,z = \Delta) \right )}{1 + \alpha\left ( k_x^2 + k_y^2 \right )} \right )\right ), - + where - :math:`T`, is the sample thickness, - - :math:`\mu = \frac{4\pi\beta}{\lambda}` is the material linear - attenuation coefficient where :math:`\beta` is the complex part of the - material refractive index and :math:`\lambda=\frac{hc}{E}` is the probe + - :math:`\mu = \frac{4\pi\beta}{\lambda}` is the material linear + attenuation coefficient where :math:`\beta` is the complex part of the + material refractive index and :math:`\lambda=\frac{hc}{E}` is the probe wavelength, - :math:`M` is the magnification at the detector, - - :math:`I_{norm}` is the input image which is expected to be the - normalised transmission data, + - :math:`I_{norm}` is the input image which is expected to be the + normalised transmission data, - :math:`\Delta` is the propagation distance, - - :math:`\alpha = \frac{\Delta\delta}{\mu}` is a parameter determining - the strength of the filter to be applied in Fourier space where - :math:`\delta` is the real part of the deviation of the material - refractive index from 1 - - :math:`k_x, k_y = \left ( \frac{2\pi p}{N_xW}, \frac{2\pi q}{N_yW} - \right )` where :math:`p` and :math:`q` are co-ordinates in a Fourier - mesh in the range :math:`-N_x/2` to :math:`N_x/2` for an image with + - :math:`\alpha = \frac{\Delta\delta}{\mu}` is a parameter determining + the strength of the filter to be applied in Fourier space where + :math:`\delta` is the real part of the deviation of the material + refractive index from 1 + - :math:`k_x, k_y = \left ( \frac{2\pi p}{N_xW}, \frac{2\pi q}{N_yW} + \right )` where :math:`p` and :math:`q` are co-ordinates in a Fourier + mesh in the range :math:`-N_x/2` to :math:`N_x/2` for an image with size :math:`N_x, N_y` and pixel size :math:`W`. - - A generalised form of the Paganin phase retrieval method can be called - using :code:`filter_type='generalised_paganin_method'`, which uses the + + A generalised form of the Paganin phase retrieval method can be called + using :code:`filter_type='generalised_paganin_method'`, which uses the form of the algorithm described in [2] - + .. math:: T(x,y) = -\frac{1}{\mu}\ln\left (\mathcal{F}^{-1}\left (\frac{ \mathcal{F}\left ( M^2I_{norm}(x, y,z = \Delta) \right )}{1 - \frac{2 \alpha}{W^2}\left ( \cos(Wk_x) + \cos(Wk_y) -2 \right )} \right ) \right ) - + The phase retrieval is valid under the following assumptions - - used with paraxial propagation-induced phase contrast images which + - used with paraxial propagation-induced phase contrast images which can be assumed to be single-material locally - using intensity data which has been flat field corrected - - and under the assumption that the Fresnel number + - and under the assumption that the Fresnel number :math:`F_N = W^2/(\lambda\Delta) >> 1` - - To apply a filter to images using the Paganin method, call - :code:`full_retrieval=False`. In this case the pre-scaling and conversion - to absorption is not applied so the requirement to supply flat field + + To apply a filter to images using the Paganin method, call + :code:`full_retrieval=False`. In this case the pre-scaling and conversion + to absorption is not applied so the requirement to supply flat field corrected intensity data is relaxed, - - .. math:: I_{filt} = \mathcal{F}^{-1}\left (\frac{\mathcal{F}\left ( + + .. math:: I_{filt} = \mathcal{F}^{-1}\left (\frac{\mathcal{F}\left ( I(x, y,z = \Delta) \right )} {1 - \alpha\left ( k_x^2 + k_y^2 \right )} \right ) References --------- - - [1] https://doi.org/10.1046/j.1365-2818.2002.01010.x + - [1] https://doi.org/10.1046/j.1365-2818.2002.01010.x - [2] https://henke.lbl.gov/optical_constants/getdb2.html - [3] https://iopscience.iop.org/article/10.1088/2040-8986/abbab9 - With thanks to colleagues at DTU for help with the initial implementation + With thanks to colleagues at DTU for help with the initial implementation of the phase retrieval algorithm """ def __init__(self, delta=1, beta=1e-2, energy=40000, - energy_units='eV', full_retrieval=True, - filter_type='paganin_method', pad=0, + energy_units='eV', full_retrieval=True, + filter_type='paganin_method', pad=0, return_units='cm'): - + kwargs = { 'energy' : energy, 'wavelength' : self._energy_to_wavelength(energy, energy_units, @@ -194,24 +194,24 @@ def __init__(self, delta=1, beta=1e-2, energy=40000, 'override_filter' : None, 'return_units' : return_units } - + super(PaganinProcessor, self).__init__(**kwargs) def check_input(self, data): if not isinstance(data, (AcquisitionData)): raise TypeError('Processor only supports AcquisitionData') - + return True - + def process(self, out=None): data = self.get_input() - cil_order = tuple(DataOrder.get_order_for_engine('cil', data.geometry)) + cil_order = tuple(get_order_for_engine('cil', data.geometry)) if data.dimension_labels != cil_order: log.warning(msg="This processor will work most efficiently using\ \nCIL data order, consider using `data.reorder('cil')`") - # set the geometry parameters to use from data.geometry unless the + # set the geometry parameters to use from data.geometry unless the # geometry is overridden with an override_geometry self._set_geometry(data.geometry, self.override_geometry) @@ -222,7 +222,7 @@ def process(self, out=None): slice_proj = [slice(None)]*len(data.shape) angle_axis = data.get_dimension_axis('angle') slice_proj[angle_axis] = 0 - + if data.geometry.channels>1: channel_axis = data.get_dimension_axis('channel') slice_proj[channel_axis] = 0 @@ -236,24 +236,24 @@ def process(self, out=None): data.array = np.expand_dims(data.array, len(data.shape)) slice_proj.append(slice(None)) data_proj = data.as_array()[tuple(slice_proj)] - + elif len(data_proj.shape) == 2: pass else: raise(ValueError('Data must be 2D or 3D per channel')) - + # create a filter based on the shape of the data filter_shape = np.shape(data_proj) self.filter_Nx = filter_shape[0]+self.pad*2 self.filter_Ny = filter_shape[1]+self.pad*2 self._create_filter(self.override_filter) - + # pre-calculate the scaling factor scaling_factor = -(1/self.mu) # allocate padded buffer padded_buffer = np.zeros(tuple(x+self.pad*2 for x in data_proj.shape)) - + # make slice indices to unpad the data if self.pad>0: slice_pad = tuple([slice(self.pad,-self.pad)] @@ -266,12 +266,12 @@ def process(self, out=None): slice_proj[channel_axis] = j # loop over the projections for i in tqdm(range(len(out.geometry.angles))): - + slice_proj[angle_axis] = i padded_buffer[slice_pad] = data.array[(tuple(slice_proj))] - + if self.full_retrieval==True: - # apply the filter in fourier space, apply log and scale + # apply the filter in fourier space, apply log and scale # by magnification fI = fft2(self.magnification**2*padded_buffer) iffI = ifft2(fI*self.filter) @@ -282,13 +282,13 @@ def process(self, out=None): fI = fft2(padded_buffer) padded_buffer = ifft2(fI*self.filter) if data.geometry.channels>1: - out.fill(np.squeeze(padded_buffer[slice_pad]), angle = i, + out.fill(np.squeeze(padded_buffer[slice_pad]), angle = i, channel=j) else: out.fill(np.squeeze(padded_buffer[slice_pad]), angle = i) data.array = np.squeeze(data.array) return out - + def set_input(self, dataset): """ Set the input data to the processor @@ -299,38 +299,38 @@ def set_input(self, dataset): The input AcquisitionData """ return super().set_input(dataset) - - def get_output(self, out=None, override_geometry=None, + + def get_output(self, out=None, override_geometry=None, override_filter=None): r''' Function to get output from the PaganinProcessor - + Parameters ---------- out : DataContainer, optional Fills the referenced DataContainer with the processed data override_geometry: dict, optional - Geometry parameters to use in the phase retrieval if you want to - over-ride values found in `data.geometry`. Specify parameters as a - dictionary :code:`{'parameter':value}` where parameter is - :code:`'magnification', 'propagation_distance'` or - :code:`'pixel_size'` and value is the new value to use. Specify - distance parameters in the same units as :code:`return_units` + Geometry parameters to use in the phase retrieval if you want to + over-ride values found in `data.geometry`. Specify parameters as a + dictionary :code:`{'parameter':value}` where parameter is + :code:`'magnification', 'propagation_distance'` or + :code:`'pixel_size'` and value is the new value to use. Specify + distance parameters in the same units as :code:`return_units` (default is cm). override_filter: dict, optional - Over-ride the filter parameters to use in the phase retrieval. - Specify parameters as :code:`{'parameter':value}` where parameter - is :code:`'delta', 'beta'` or :code:`'alpha'` and value is the new + Over-ride the filter parameters to use in the phase retrieval. + Specify parameters as :code:`{'parameter':value}` where parameter + is :code:`'delta', 'beta'` or :code:`'alpha'` and value is the new value to use. Returns ------- AcquisitionData - AcquisitionData corrected for phase effects, retrieved sample - thickness or (if :code:`full_retrieval=False`) filtered data - + AcquisitionData corrected for phase effects, retrieved sample + thickness or (if :code:`full_retrieval=False`) filtered data + Example ------- >>> processor = PaganinProcessor(delta=5, beta=0.05, energy=18000) @@ -339,7 +339,7 @@ def get_output(self, out=None, override_geometry=None, Example ------- - >>> processor = PaganinProcessor(delta=1,beta=10e2, + >>> processor = PaganinProcessor(delta=1,beta=10e2, full_retrieval=False) >>> processor.set_input(data) >>> filtered_image = processor.get_output() @@ -353,48 +353,48 @@ def get_output(self, out=None, override_geometry=None, Notes ----- - If :code:`'alpha'` is specified in override_filter the new value will - be used and delta will be ignored but beta will still be used to - calculate :math:`\mu = \frac{4\pi\beta}{\lambda}` which is used for - scaling the thickness, therefore it is only recommended to specify - alpha when also using :code:`get_output(full_retrieval=False)`, or - re-scaling the result by :math:`\mu` e.g. - :code:`thickness*processor.mu` If :code:`alpha` is not specified, + If :code:`'alpha'` is specified in override_filter the new value will + be used and delta will be ignored but beta will still be used to + calculate :math:`\mu = \frac{4\pi\beta}{\lambda}` which is used for + scaling the thickness, therefore it is only recommended to specify + alpha when also using :code:`get_output(full_retrieval=False)`, or + re-scaling the result by :math:`\mu` e.g. + :code:`thickness*processor.mu` If :code:`alpha` is not specified, it will be calculated :math:`\frac{\Delta\delta\lambda}{4\pi\beta}` ''' self.override_geometry = override_geometry self.override_filter = override_filter - + return super().get_output(out) - - def __call__(self, x, out=None, override_geometry=None, + + def __call__(self, x, out=None, override_geometry=None, override_filter=None): self.set_input(x) if out is None: - out = self.get_output(override_geometry=override_geometry, + out = self.get_output(override_geometry=override_geometry, override_filter=override_filter) else: - self.get_output(out=out, override_geometry=override_geometry, + self.get_output(out=out, override_geometry=override_geometry, override_filter=override_filter) return out def _set_geometry(self, geometry, override_geometry=None): ''' - Function to set the geometry parameters for the processor. Values are - from the data geometry unless the geometry is overridden with an + Function to set the geometry parameters for the processor. Values are + from the data geometry unless the geometry is overridden with an override_geometry dictionary. ''' - + parameters = ['magnification', 'propagation_distance', 'pixel_size'] # specify parameter names as defined in geometry - geometry_parameters = ['magnification', 'dist_center_detector', + geometry_parameters = ['magnification', 'dist_center_detector', ('pixel_size_h', 'pixel_size_v')] # specify if parameter requires unit conversion convert_units = [False, True, True] - + if override_geometry is None: override_geometry = {} @@ -409,13 +409,13 @@ def _set_geometry(self, geometry, override_geometry=None): data.geometry.{} or over-ride with \ processor.get_output(override_geometry= \ {{ '{}' : value }} )"\ - .format(parameter, str(getattr(self, parameter)), + .format(parameter, str(getattr(self, parameter)), geometry_parameters[i], parameter)) else: self.__setattr__(parameter, override_geometry[parameter]) - # get and check parameters from geometry if they are not in the + # get and check parameters from geometry if they are not in the # over-ride geometry dictionary for i, parameter in enumerate(parameters): if parameter not in override_geometry: @@ -429,37 +429,37 @@ def _set_geometry(self, geometry, override_geometry=None): data.geometry.{} or over-ride with \ processor.get_output(\ override_geometry={{ '{}' : value }})" - .format(parameter, str(param1), - str(param2), - geometry_parameters[i][0], - geometry_parameters[i][1], + .format(parameter, str(param1), + str(param2), + geometry_parameters[i][0], + geometry_parameters[i][1], parameter)) else: param1 = getattr(geometry, geometry_parameters[i]) - + if (param1 is None) | (param1 == 0): raise ValueError("Parameter {} cannot be {}, please update\ data.geometry.{} or over-ride with \ processor.get_output(override_geometry\ ={{ '{}' : value }} )" - .format(parameter, str(param1), + .format(parameter, str(param1), str(geometry_parameters[i]), parameter)) else: if convert_units[i]: param1 = self._convert_units(param1, 'distance', - geometry.config.units, + geometry.config.units, self.return_units) self.__setattr__(parameter, param1) - + def _create_filter(self, override_filter=None): ''' - Function to create the Paganin filter, either using the paganin [1] or + Function to create the Paganin filter, either using the paganin [1] or generalised paganin [2] method The filter is created on a mesh in Fourier space kx, ky [1] https://doi.org/10.1046/j.1365-2818.2002.01010.x - [2] https://iopscience.iop.org/article/10.1088/2040-8986/abbab9 + [2] https://iopscience.iop.org/article/10.1088/2040-8986/abbab9 ''' if override_filter is None: override_filter = {} @@ -473,7 +473,7 @@ def _create_filter(self, override_filter=None): self.delta = override_filter['delta'] else: self.delta = self._delta_user - + if ('beta' in override_filter): self.beta = override_filter['beta'] else: @@ -485,30 +485,30 @@ def _create_filter(self, override_filter=None): self.alpha = override_filter['alpha'] else: self._calculate_alpha() - + # create the Fourier mesh - kx,ky = np.meshgrid( - np.arange(-self.filter_Nx/2, self.filter_Nx/2, 1, dtype=np.float64) + kx,ky = np.meshgrid( + np.arange(-self.filter_Nx/2, self.filter_Nx/2, 1, dtype=np.float64) * (2*np.pi)/(self.filter_Nx*self.pixel_size), - np.arange(-self.filter_Ny/2, self.filter_Ny/2, 1, dtype=np.float64) + np.arange(-self.filter_Ny/2, self.filter_Ny/2, 1, dtype=np.float64) * (2*np.pi)/(self.filter_Ny*self.pixel_size), - sparse=False, + sparse=False, indexing='ij' ) - + # create the filter using either paganin or generalised paganin method if self.filter_type == 'paganin_method': self.filter = ifftshift(1/(1. + self.alpha*(kx**2 + ky**2))) - elif self.filter_type == 'generalised_paganin_method': + elif self.filter_type == 'generalised_paganin_method': self.filter = ifftshift(1/(1. - (2*self.alpha/self.pixel_size**2) - *(np.cos(self.pixel_size*kx) + *(np.cos(self.pixel_size*kx) + np.cos(self.pixel_size*ky) -2))) else: raise ValueError("filter_type not recognised: got {0} expected one\ of 'paganin_method' or \ 'generalised_paganin_method'" .format(self.filter_type)) - + def _calculate_mu(self): ''' Function to calculate the linear attenutation coefficient mu @@ -517,37 +517,37 @@ def _calculate_mu(self): def _calculate_alpha(self): ''' - Function to calculate alpha, a constant defining the Paganin filter + Function to calculate alpha, a constant defining the Paganin filter strength ''' self.alpha = self.propagation_distance*self.delta/self.mu - + def _energy_to_wavelength(self, energy, energy_units, return_units): ''' Function to convert photon energy in eV to wavelength in return_units - + Parameters ---------- energy: float Photon energy - + energy_units Energy units return_units Distance units in which to return the wavelength - + Returns ------- float Photon wavelength in return_units ''' - top = self._convert_units(constants.h*constants.speed_of_light, + top = self._convert_units(constants.h*constants.speed_of_light, 'distance', 'm', return_units) bottom = self._convert_units(energy, 'energy', energy_units, 'J') return top/bottom - + def _convert_units(self, value, unit_type, input_unit, output_unit): unit_types = ['distance','energy','angle'] @@ -569,6 +569,6 @@ def _convert_units(self, value, unit_type, input_unit, output_unit): raise ValueError("Unit '{}' not recognised, must be one of {}.\ \nGeometry units can be updated using geometry.config.units" .format(x, unit_list)) - + return value*unit_multipliers[unit_list.index(input_unit)]\ - /unit_multipliers[unit_list.index(output_unit)] \ No newline at end of file + /unit_multipliers[unit_list.index(output_unit)] diff --git a/Wrappers/Python/test/test_DataContainer.py b/Wrappers/Python/test/test_DataContainer.py index 7127f9f7a9..2b88369f0f 100644 --- a/Wrappers/Python/test/test_DataContainer.py +++ b/Wrappers/Python/test/test_DataContainer.py @@ -15,8 +15,6 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt - -import unittest from utils import initialise_tests import sys import numpy @@ -812,11 +810,11 @@ def test_DataContainerSubset(self): self.assertListEqual([acquisition_labels["HORIZONTAL"] , acquisition_labels["CHANNEL"] , acquisition_labels["ANGLE"]], list(ss1.dimension_labels)) - + ss2 = dc.get_slice(vertical=0, channel=0) self.assertListEqual([acquisition_labels["HORIZONTAL"] , acquisition_labels["ANGLE"]], list(ss2.dimension_labels)) - + # Check we can get slice still even if force parameter is passed: ss3 = dc.get_slice(vertical=0, channel=0, force=True) self.assertListEqual([acquisition_labels["HORIZONTAL"] , diff --git a/Wrappers/Python/test/test_io.py b/Wrappers/Python/test/test_io.py index d346748f4c..0682477405 100644 --- a/Wrappers/Python/test/test_io.py +++ b/Wrappers/Python/test/test_io.py @@ -15,7 +15,7 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt - +import sys import unittest from unittest.mock import patch from utils import initialise_tests @@ -28,10 +28,8 @@ from cil.io.utilities import HDF5_utilities from cil.processors import Slicer from utils import has_astra, has_nvidia -from cil.utilities.dataexample import data_dir from cil.utilities.quality_measures import mse from cil.utilities import dataexample -import shutil import logging import glob import json @@ -65,7 +63,7 @@ # change basedir to point to the location of the walnut dataset which can # be downloaded from https://zenodo.org/record/4822516 # basedir = os.path.abspath('/home/edo/scratch/Data/Walnut/valnut_2014-03-21_643_28/tomo-A/') -basedir = data_dir +basedir = data_dir = os.path.abspath(os.path.join(sys.prefix, 'share','cil')) filename = os.path.join(basedir, "valnut_tomo-A.txrm") has_file = os.path.isfile(filename) @@ -162,7 +160,7 @@ def test_read_and_reconstruct_2D(self): np.testing.assert_almost_equal(qm, 0, decimal=3) fname = os.path.join(data_dir, 'walnut_slice512.nxs') os.remove(fname) - + def test_file_not_found_error(self): with self.assertRaises(FileNotFoundError): reader = ZEISSDataReader(file_name='no-file') From 81e9bab74edee964ec1a8bf04b1e7c2e6c807cf0 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 12 Aug 2024 13:20:11 +0100 Subject: [PATCH 33/72] fix missing refactor --- Wrappers/Python/cil/framework/data_container.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Wrappers/Python/cil/framework/data_container.py b/Wrappers/Python/cil/framework/data_container.py index 72607c52d0..ed15a7600f 100644 --- a/Wrappers/Python/cil/framework/data_container.py +++ b/Wrappers/Python/cil/framework/data_container.py @@ -1537,8 +1537,10 @@ def allocate(self, value=0, **kwargs): if seed is not None: numpy.random.seed(seed) max_value = kwargs.get('max_value', 100) - r = numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) - out.fill(numpy.asarray(r, dtype=self.dtype)) + if numpy.iscomplexobj(out.array): + out.fill(numpy.random.randint(max_value, size=self.shape, dtype=numpy.int32) + 1.j*numpy.random.randint(max_value, size=self.shape, dtype=numpy.int32)) + else: + out.fill(numpy.random.randint(max_value, size=self.shape, dtype=numpy.int32)) elif value is None: pass else: From f712ea4447761a9d8efb7e73eaefbc902b6140ec Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 14 Aug 2024 08:28:28 +0100 Subject: [PATCH 34/72] move {Image,Vector}{Data,Geometry} --- Wrappers/Python/cil/framework/__init__.py | 6 +- .../cil/framework/acquisition_geometry.py | 2 +- .../Python/cil/framework/data_container.py | 624 +----------------- Wrappers/Python/cil/framework/image_data.py | 191 ++++++ .../Python/cil/framework/image_geometry.py | 322 +++++++++ Wrappers/Python/cil/framework/vector_data.py | 74 +++ .../Python/cil/framework/vector_geometry.py | 113 ++++ 7 files changed, 711 insertions(+), 621 deletions(-) create mode 100644 Wrappers/Python/cil/framework/image_data.py create mode 100644 Wrappers/Python/cil/framework/image_geometry.py create mode 100644 Wrappers/Python/cil/framework/vector_data.py create mode 100644 Wrappers/Python/cil/framework/vector_geometry.py diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 10ad668873..a436e589a5 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -20,7 +20,11 @@ from .acquisition_data import AcquisitionData from .acquisition_geometry import AcquisitionGeometry from .system_configuration import SystemConfiguration -from .data_container import message, ImageGeometry, DataContainer, ImageData, VectorData, VectorGeometry +from .data_container import message, DataContainer +from .image_data import ImageData +from .image_geometry import ImageGeometry +from .vector_data import VectorData +from .vector_geometry import VectorGeometry from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer, find_key from .block import BlockDataContainer, BlockGeometry from .partitioner import Partitioner diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 039a40db66..dabd380b3d 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -25,7 +25,7 @@ from .label import acquisition_labels, data_order from .acquisition_data import AcquisitionData from .base import BaseAcquisitionGeometry -from .data_container import ImageGeometry +from .image_geometry import ImageGeometry from .system_configuration import ComponentDescription, PositionVector, PositionDirectionVector, SystemConfiguration diff --git a/Wrappers/Python/cil/framework/data_container.py b/Wrappers/Python/cil/framework/data_container.py index ed15a7600f..8f55b59397 100644 --- a/Wrappers/Python/cil/framework/data_container.py +++ b/Wrappers/Python/cil/framework/data_container.py @@ -23,7 +23,7 @@ import numpy -from .label import image_labels, data_order, get_order_for_engine +from .label import data_order, get_order_for_engine from .cilacc import cilacc from cil.utilities.multiprocessing import NUM_THREADS @@ -38,303 +38,6 @@ def message(cls, msg, *args): return msg.format(*args ) -class ImageGeometry(object): - @property - def CHANNEL(self): - warnings.warn("use image_labels['CHANNEL'] instead", DeprecationWarning, stacklevel=2) - return image_labels['CHANNEL'] - - @property - def HORIZONTAL_X(self): - warnings.warn("use image_labels['HORIZONTAL_X'] instead", DeprecationWarning, stacklevel=2) - return image_labels['HORIZONTAL_X'] - - @property - def HORIZONTAL_Y(self): - warnings.warn("use image_labels['HORIZONTAL_Y'] instead", DeprecationWarning, stacklevel=2) - return image_labels['HORIZONTAL_Y'] - - @property - def RANDOM(self): - warnings.warn("use image_labels['RANDOM'] instead", DeprecationWarning, stacklevel=2) - return image_labels['RANDOM'] - - @property - def RANDOM_INT(self): - warnings.warn("use image_labels['RANDOM_INT'] instead", DeprecationWarning, stacklevel=2) - return image_labels['RANDOM_INT'] - - @property - def VERTICAL(self): - warnings.warn("use image_labels['VERTICAL'] instead", DeprecationWarning, stacklevel=2) - return image_labels['VERTICAL'] - - @property - def shape(self): - - shape_dict = {image_labels["CHANNEL"]: self.channels, - image_labels["VERTICAL"]: self.voxel_num_z, - image_labels["HORIZONTAL_Y"]: self.voxel_num_y, - image_labels["HORIZONTAL_X"]: self.voxel_num_x} - - shape = [] - for label in self.dimension_labels: - shape.append(shape_dict[label]) - - return tuple(shape) - - @shape.setter - def shape(self, val): - print("Deprecated - shape will be set automatically") - - @property - def spacing(self): - - spacing_dict = {image_labels["CHANNEL"]: self.channel_spacing, - image_labels["VERTICAL"]: self.voxel_size_z, - image_labels["HORIZONTAL_Y"]: self.voxel_size_y, - image_labels["HORIZONTAL_X"]: self.voxel_size_x} - - spacing = [] - for label in self.dimension_labels: - spacing.append(spacing_dict[label]) - - return tuple(spacing) - - @property - def length(self): - return len(self.dimension_labels) - - @property - def ndim(self): - return len(self.dimension_labels) - - @property - def dimension_labels(self): - - labels_default = data_order["CIL_IG_LABELS"] - - shape_default = [ self.channels, - self.voxel_num_z, - self.voxel_num_y, - self.voxel_num_x] - - try: - labels = list(self._dimension_labels) - except AttributeError: - labels = labels_default.copy() - - for i, x in enumerate(shape_default): - if x == 0 or x==1: - try: - labels.remove(labels_default[i]) - except ValueError: - pass #if not in custom list carry on - return tuple(labels) - - @dimension_labels.setter - def dimension_labels(self, val): - self.set_labels(val) - - def set_labels(self, labels): - labels_default = data_order["CIL_IG_LABELS"] - - #check input and store. This value is not used directly - if labels is not None: - for x in labels: - if x not in labels_default: - raise ValueError('Requested axis are not possible. Accepted label names {},\ngot {}'\ - .format(labels_default,labels)) - - self._dimension_labels = tuple(labels) - - def __eq__(self, other): - - if not isinstance(other, self.__class__): - return False - - if self.voxel_num_x == other.voxel_num_x \ - and self.voxel_num_y == other.voxel_num_y \ - and self.voxel_num_z == other.voxel_num_z \ - and self.voxel_size_x == other.voxel_size_x \ - and self.voxel_size_y == other.voxel_size_y \ - and self.voxel_size_z == other.voxel_size_z \ - and self.center_x == other.center_x \ - and self.center_y == other.center_y \ - and self.center_z == other.center_z \ - and self.channels == other.channels \ - and self.channel_spacing == other.channel_spacing \ - and self.dimension_labels == other.dimension_labels: - - return True - - return False - - @property - def dtype(self): - return self._dtype - - @dtype.setter - def dtype(self, val): - self._dtype = val - - def __init__(self, - voxel_num_x=0, - voxel_num_y=0, - voxel_num_z=0, - voxel_size_x=1, - voxel_size_y=1, - voxel_size_z=1, - center_x=0, - center_y=0, - center_z=0, - channels=1, - **kwargs): - - self.voxel_num_x = int(voxel_num_x) - self.voxel_num_y = int(voxel_num_y) - self.voxel_num_z = int(voxel_num_z) - self.voxel_size_x = float(voxel_size_x) - self.voxel_size_y = float(voxel_size_y) - self.voxel_size_z = float(voxel_size_z) - self.center_x = center_x - self.center_y = center_y - self.center_z = center_z - self.channels = channels - self.channel_labels = None - self.channel_spacing = 1.0 - self.dimension_labels = kwargs.get('dimension_labels', None) - self.dtype = kwargs.get('dtype', numpy.float32) - - - def get_slice(self,channel=None, vertical=None, horizontal_x=None, horizontal_y=None): - ''' - Returns a new ImageGeometry of a single slice of in the requested direction. - ''' - geometry_new = self.copy() - if channel is not None: - geometry_new.channels = 1 - - try: - geometry_new.channel_labels = [self.channel_labels[channel]] - except: - geometry_new.channel_labels = None - - if vertical is not None: - geometry_new.voxel_num_z = 0 - - if horizontal_y is not None: - geometry_new.voxel_num_y = 0 - - if horizontal_x is not None: - geometry_new.voxel_num_x = 0 - - return geometry_new - - def get_order_by_label(self, dimension_labels, default_dimension_labels): - order = [] - for i, el in enumerate(default_dimension_labels): - for j, ek in enumerate(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 - - def get_max_x(self): - return self.center_x + 0.5*self.voxel_num_x*self.voxel_size_x - - def get_min_y(self): - return self.center_y - 0.5*self.voxel_num_y*self.voxel_size_y - - def get_max_y(self): - return self.center_y + 0.5*self.voxel_num_y*self.voxel_size_y - - def get_min_z(self): - if not self.voxel_num_z == 0: - return self.center_z - 0.5*self.voxel_num_z*self.voxel_size_z - else: - return 0 - - def get_max_z(self): - if not self.voxel_num_z == 0: - return self.center_z + 0.5*self.voxel_num_z*self.voxel_size_z - else: - return 0 - - def clone(self): - '''returns a copy of the ImageGeometry''' - return copy.deepcopy(self) - - def copy(self): - '''alias of clone''' - return self.clone() - - def __str__ (self): - repres = "" - repres += "Number of channels: {0}\n".format(self.channels) - repres += "channel_spacing: {0}\n".format(self.channel_spacing) - - if self.voxel_num_z > 0: - repres += "voxel_num : x{0},y{1},z{2}\n".format(self.voxel_num_x, self.voxel_num_y, self.voxel_num_z) - 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) - else: - repres += "voxel_num : x{0},y{1}\n".format(self.voxel_num_x, self.voxel_num_y) - repres += "voxel_size : x{0},y{1}\n".format(self.voxel_size_x, self.voxel_size_y) - repres += "center : x{0},y{1}\n".format(self.center_x, self.center_y) - - return repres - def allocate(self, value=0, **kwargs): - '''allocates an ImageData according to the size expressed in the instance - - :param value: accepts numbers to allocate an uniform array, or a string as 'random' or 'random_int' to create a random array or None. - :type value: number or string, default None allocates empty memory block, default 0 - :param dtype: numerical type to allocate - :type dtype: numpy type, default numpy.float32 - ''' - - dtype = kwargs.get('dtype', self.dtype) - - if kwargs.get('dimension_labels', None) is not None: - raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") - - out = ImageData(geometry=self.copy(), - dtype=dtype, - suppress_warning=True) - - if isinstance(value, Number): - # it's created empty, so we make it 0 - out.array.fill(value) - else: - if value == image_labels["RANDOM"]: - seed = kwargs.get('seed', None) - if seed is not None: - numpy.random.seed(seed) - if numpy.iscomplexobj(out.array): - r = numpy.random.random_sample(self.shape) + 1j * numpy.random.random_sample(self.shape) - out.fill(r) - else: - out.fill(numpy.random.random_sample(self.shape)) - elif value == image_labels["RANDOM_INT"]: - seed = kwargs.get('seed', None) - if seed is not None: - numpy.random.seed(seed) - max_value = kwargs.get('max_value', 100) - if numpy.iscomplexobj(out.array): - out.fill(numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) + 1.j*numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32)) - else: - out.fill(numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32)) - elif value is None: - pass - else: - raise ValueError('Value {} unknown'.format(value)) - - return out - - class DataContainer(object): '''Generic class to hold data @@ -485,14 +188,13 @@ def get_slice(self, **kw): axis = dimension_labels_list.index(key) dimension_labels_list.remove(key) if new_array is None: - new_array = self.as_array().take(indices=value, axis=axis) - else: - new_array = new_array.take(indices=value, axis=axis) + new_array = self.as_array() + new_array = new_array.take(indices=value, axis=axis) if new_array.ndim > 1: return DataContainer(new_array, False, dimension_labels_list, suppress_warning=True) - else: - return VectorData(new_array, dimension_labels=dimension_labels_list) + from .vector_data import VectorData + return VectorData(new_array, dimension_labels=dimension_labels_list) def reorder(self, order=None): ''' @@ -1230,319 +932,3 @@ def __ne__(self, other): if isinstance(other, DataContainer): return self.as_array()!=other.as_array() return self.as_array()!=other - - -class ImageData(DataContainer): - '''DataContainer for holding 2D or 3D DataContainer''' - __container_priority__ = 1 - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, val): - self._geometry = val - - @property - def dimension_labels(self): - return self.geometry.dimension_labels - - @dimension_labels.setter - def dimension_labels(self, val): - if val is not None: - raise ValueError("Unable to set the dimension_labels directly. Use geometry.set_labels() instead") - - def __init__(self, - array = None, - deep_copy=False, - geometry=None, - **kwargs): - - dtype = kwargs.get('dtype', numpy.float32) - - - if geometry is None: - raise AttributeError("ImageData requires a geometry") - - - labels = kwargs.get('dimension_labels', None) - if labels is not None and labels != geometry.dimension_labels: - raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") - - if array is None: - array = numpy.empty(geometry.shape, dtype=dtype) - elif issubclass(type(array) , DataContainer): - array = array.as_array() - elif issubclass(type(array) , numpy.ndarray): - pass - else: - raise TypeError('array must be a CIL type DataContainer or numpy.ndarray got {}'.format(type(array))) - - if array.shape != geometry.shape: - raise ValueError('Shape mismatch {} {}'.format(array.shape, geometry.shape)) - - if array.ndim not in [2,3,4]: - raise ValueError('Number of dimensions are not 2 or 3 or 4 : {0}'.format(array.ndim)) - - super(ImageData, self).__init__(array, deep_copy, geometry=geometry, **kwargs) - - - def get_slice(self,channel=None, vertical=None, horizontal_x=None, horizontal_y=None, force=False): - ''' - Returns a new ImageData of a single slice of in the requested direction. - ''' - try: - geometry_new = self.geometry.get_slice(channel=channel, vertical=vertical, horizontal_x=horizontal_x, horizontal_y=horizontal_y) - except ValueError: - if force: - geometry_new = None - else: - raise ValueError ("Unable to return slice of requested ImageData. Use 'force=True' to return DataContainer instead.") - - #if vertical = 'centre' slice convert to index and subset, this will interpolate 2 rows to get the center slice value - if vertical == 'centre': - dim = self.geometry.dimension_labels.index('vertical') - centre_slice_pos = (self.geometry.shape[dim]-1) / 2. - ind0 = int(numpy.floor(centre_slice_pos)) - - w2 = centre_slice_pos - ind0 - out = DataContainer.get_slice(self, channel=channel, vertical=ind0, horizontal_x=horizontal_x, horizontal_y=horizontal_y) - - if w2 > 0: - out2 = DataContainer.get_slice(self, channel=channel, vertical=ind0 + 1, horizontal_x=horizontal_x, horizontal_y=horizontal_y) - out = out * (1 - w2) + out2 * w2 - else: - out = DataContainer.get_slice(self, channel=channel, vertical=vertical, horizontal_x=horizontal_x, horizontal_y=horizontal_y) - - if len(out.shape) == 1 or geometry_new is None: - return out - else: - return ImageData(out.array, deep_copy=False, geometry=geometry_new, suppress_warning=True) - - - def apply_circular_mask(self, radius=0.99, in_place=True): - """ - - Apply a circular mask to the horizontal_x and horizontal_y slices. Values outside this mask will be set to zero. - - This will most commonly be used to mask edge artefacts from standard CT reconstructions with FBP. - - Parameters - ---------- - radius : float, default 0.99 - radius of mask by percentage of size of horizontal_x or horizontal_y, whichever is greater - - in_place : boolean, default True - If `True` masks the current data, if `False` returns a new `ImageData` object. - - - Returns - ------- - ImageData - If `in_place = False` returns a new ImageData object with the masked data - - """ - ig = self.geometry - - # grid - y_range = (ig.voxel_num_y-1)/2 - x_range = (ig.voxel_num_x-1)/2 - - Y, X = numpy.ogrid[-y_range:y_range+1,-x_range:x_range+1] - - # use centre from geometry in units distance to account for aspect ratio of pixels - dist_from_center = numpy.sqrt((X*ig.voxel_size_x+ ig.center_x)**2 + (Y*ig.voxel_size_y+ig.center_y)**2) - - size_x = ig.voxel_num_x * ig.voxel_size_x - size_y = ig.voxel_num_y * ig.voxel_size_y - - if size_x > size_y: - radius_applied =radius * size_x/2 - else: - radius_applied =radius * size_y/2 - - # approximate the voxel as a circle and get the radius - # ie voxel area = 1, circle of area=1 has r = 0.56 - r=((ig.voxel_size_x * ig.voxel_size_y )/numpy.pi)**(1/2) - - # we have the voxel centre distance to mask. voxels with distance greater than |r| are fully inside or outside. - # values on the border region between -r and r are preserved - mask =(radius_applied-dist_from_center).clip(-r,r) - - # rescale to -pi/2->+pi/2 - mask *= (0.5*numpy.pi)/r - - # the sin of the linear distance gives us an approximation of area of the circle to include in the mask - numpy.sin(mask, out = mask) - - # rescale the data 0 - 1 - mask = 0.5 + mask * 0.5 - - # reorder dataset so 'horizontal_y' and 'horizontal_x' are the final dimensions - labels_orig = self.dimension_labels - labels = list(labels_orig) - - labels.remove('horizontal_y') - labels.remove('horizontal_x') - labels.append('horizontal_y') - labels.append('horizontal_x') - - - if in_place == True: - self.reorder(labels) - numpy.multiply(self.array, mask, out=self.array) - self.reorder(labels_orig) - - else: - image_data_out = self.copy() - image_data_out.reorder(labels) - numpy.multiply(image_data_out.array, mask, out=image_data_out.array) - image_data_out.reorder(labels_orig) - - return image_data_out - - -class VectorData(DataContainer): - '''DataContainer to contain 1D array''' - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, val): - self._geometry = val - - @property - def dimension_labels(self): - if hasattr(self,'geometry'): - return self.geometry.dimension_labels - else: - return self._dimension_labels - - @dimension_labels.setter - def dimension_labels(self, val): - if hasattr(self,'geometry'): - self.geometry.dimension_labels = val - - self._dimension_labels = val - - def __init__(self, array=None, **kwargs): - self.geometry = kwargs.get('geometry', None) - - 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], **kwargs) - self.length = self.geometry.length - else: - self.length = self.geometry.length - - if array is None: - out = numpy.zeros((self.length,), dtype=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 - # need to pass the geometry, othewise None - super(VectorData, self).__init__(out, deep_copy, self.geometry.dimension_labels, geometry = self.geometry) - - -class VectorGeometry(object): - '''Geometry describing VectorData to contain 1D array''' - RANDOM = 'random' - RANDOM_INT = 'random_int' - - @property - def dtype(self): - return self._dtype - - @dtype.setter - def dtype(self, val): - self._dtype = val - - def __init__(self, - length, **kwargs): - - self.length = int(length) - self.shape = (length, ) - self.dtype = kwargs.get('dtype', numpy.float32) - self.dimension_labels = kwargs.get('dimension_labels', None) - - def clone(self): - '''returns a copy of VectorGeometry''' - return copy.deepcopy(self) - - def copy(self): - '''alias of clone''' - return self.clone() - - def __eq__(self, other): - - if not isinstance(other, self.__class__): - return False - - if self.length == other.length \ - and self.shape == other.shape \ - and self.dimension_labels == other.dimension_labels: - return True - return False - - def __str__ (self): - repres = "" - repres += "Length: {0}\n".format(self.length) - repres += "Shape: {0}\n".format(self.shape) - repres += "Dimension_labels: {0}\n".format(self.dimension_labels) - - return repres - - def allocate(self, value=0, **kwargs): - '''allocates an VectorData according to the size expressed in the instance - - :param value: accepts numbers to allocate an uniform array, or a string as 'random' or 'random_int' to create a random array or None. - :type value: number or string, default None allocates empty memory block - :param dtype: numerical type to allocate - :type dtype: numpy type, default numpy.float32 - :param seed: seed for the random number generator - :type seed: int, default None - :param max_value: max value of the random int array - :type max_value: int, default 100''' - - dtype = kwargs.get('dtype', self.dtype) - # self.dtype = kwargs.get('dtype', numpy.float32) - out = VectorData(geometry=self.copy(), dtype=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) - if numpy.iscomplexobj(out.array): - out.fill(numpy.random.random_sample(self.shape) + 1.j*numpy.random.random_sample(self.shape)) - else: - 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) - if numpy.iscomplexobj(out.array): - out.fill(numpy.random.randint(max_value, size=self.shape, dtype=numpy.int32) + 1.j*numpy.random.randint(max_value, size=self.shape, dtype=numpy.int32)) - else: - out.fill(numpy.random.randint(max_value, size=self.shape, dtype=numpy.int32)) - elif value is None: - pass - else: - raise ValueError('Value {} unknown'.format(value)) - return out diff --git a/Wrappers/Python/cil/framework/image_data.py b/Wrappers/Python/cil/framework/image_data.py new file mode 100644 index 0000000000..b579b06fd7 --- /dev/null +++ b/Wrappers/Python/cil/framework/image_data.py @@ -0,0 +1,191 @@ +# Copyright 2018 United Kingdom Research and Innovation +# Copyright 2018 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +import numpy + +from .data_container import DataContainer + + +class ImageData(DataContainer): + '''DataContainer for holding 2D or 3D DataContainer''' + __container_priority__ = 1 + + @property + def geometry(self): + return self._geometry + + @geometry.setter + def geometry(self, val): + self._geometry = val + + @property + def dimension_labels(self): + return self.geometry.dimension_labels + + @dimension_labels.setter + def dimension_labels(self, val): + if val is not None: + raise ValueError("Unable to set the dimension_labels directly. Use geometry.set_labels() instead") + + def __init__(self, + array = None, + deep_copy=False, + geometry=None, + **kwargs): + + dtype = kwargs.get('dtype', numpy.float32) + + + if geometry is None: + raise AttributeError("ImageData requires a geometry") + + + labels = kwargs.get('dimension_labels', None) + if labels is not None and labels != geometry.dimension_labels: + raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") + + if array is None: + array = numpy.empty(geometry.shape, dtype=dtype) + elif issubclass(type(array) , DataContainer): + array = array.as_array() + elif issubclass(type(array) , numpy.ndarray): + pass + else: + raise TypeError('array must be a CIL type DataContainer or numpy.ndarray got {}'.format(type(array))) + + if array.shape != geometry.shape: + raise ValueError('Shape mismatch {} {}'.format(array.shape, geometry.shape)) + + if array.ndim not in [2,3,4]: + raise ValueError('Number of dimensions are not 2 or 3 or 4 : {0}'.format(array.ndim)) + + super(ImageData, self).__init__(array, deep_copy, geometry=geometry, **kwargs) + + + def get_slice(self,channel=None, vertical=None, horizontal_x=None, horizontal_y=None, force=False): + ''' + Returns a new ImageData of a single slice of in the requested direction. + ''' + try: + geometry_new = self.geometry.get_slice(channel=channel, vertical=vertical, horizontal_x=horizontal_x, horizontal_y=horizontal_y) + except ValueError: + if force: + geometry_new = None + else: + raise ValueError ("Unable to return slice of requested ImageData. Use 'force=True' to return DataContainer instead.") + + #if vertical = 'centre' slice convert to index and subset, this will interpolate 2 rows to get the center slice value + if vertical == 'centre': + dim = self.geometry.dimension_labels.index('vertical') + centre_slice_pos = (self.geometry.shape[dim]-1) / 2. + ind0 = int(numpy.floor(centre_slice_pos)) + + w2 = centre_slice_pos - ind0 + out = DataContainer.get_slice(self, channel=channel, vertical=ind0, horizontal_x=horizontal_x, horizontal_y=horizontal_y) + + if w2 > 0: + out2 = DataContainer.get_slice(self, channel=channel, vertical=ind0 + 1, horizontal_x=horizontal_x, horizontal_y=horizontal_y) + out = out * (1 - w2) + out2 * w2 + else: + out = DataContainer.get_slice(self, channel=channel, vertical=vertical, horizontal_x=horizontal_x, horizontal_y=horizontal_y) + + if len(out.shape) == 1 or geometry_new is None: + return out + else: + return ImageData(out.array, deep_copy=False, geometry=geometry_new, suppress_warning=True) + + + def apply_circular_mask(self, radius=0.99, in_place=True): + """ + + Apply a circular mask to the horizontal_x and horizontal_y slices. Values outside this mask will be set to zero. + + This will most commonly be used to mask edge artefacts from standard CT reconstructions with FBP. + + Parameters + ---------- + radius : float, default 0.99 + radius of mask by percentage of size of horizontal_x or horizontal_y, whichever is greater + + in_place : boolean, default True + If `True` masks the current data, if `False` returns a new `ImageData` object. + + + Returns + ------- + ImageData + If `in_place = False` returns a new ImageData object with the masked data + + """ + ig = self.geometry + + # grid + y_range = (ig.voxel_num_y-1)/2 + x_range = (ig.voxel_num_x-1)/2 + + Y, X = numpy.ogrid[-y_range:y_range+1,-x_range:x_range+1] + + # use centre from geometry in units distance to account for aspect ratio of pixels + dist_from_center = numpy.sqrt((X*ig.voxel_size_x+ ig.center_x)**2 + (Y*ig.voxel_size_y+ig.center_y)**2) + + size_x = ig.voxel_num_x * ig.voxel_size_x + size_y = ig.voxel_num_y * ig.voxel_size_y + + if size_x > size_y: + radius_applied =radius * size_x/2 + else: + radius_applied =radius * size_y/2 + + # approximate the voxel as a circle and get the radius + # ie voxel area = 1, circle of area=1 has r = 0.56 + r=((ig.voxel_size_x * ig.voxel_size_y )/numpy.pi)**(1/2) + + # we have the voxel centre distance to mask. voxels with distance greater than |r| are fully inside or outside. + # values on the border region between -r and r are preserved + mask =(radius_applied-dist_from_center).clip(-r,r) + + # rescale to -pi/2->+pi/2 + mask *= (0.5*numpy.pi)/r + + # the sin of the linear distance gives us an approximation of area of the circle to include in the mask + numpy.sin(mask, out = mask) + + # rescale the data 0 - 1 + mask = 0.5 + mask * 0.5 + + # reorder dataset so 'horizontal_y' and 'horizontal_x' are the final dimensions + labels_orig = self.dimension_labels + labels = list(labels_orig) + + labels.remove('horizontal_y') + labels.remove('horizontal_x') + labels.append('horizontal_y') + labels.append('horizontal_x') + + + if in_place == True: + self.reorder(labels) + numpy.multiply(self.array, mask, out=self.array) + self.reorder(labels_orig) + + else: + image_data_out = self.copy() + image_data_out.reorder(labels) + numpy.multiply(image_data_out.array, mask, out=image_data_out.array) + image_data_out.reorder(labels_orig) + + return image_data_out diff --git a/Wrappers/Python/cil/framework/image_geometry.py b/Wrappers/Python/cil/framework/image_geometry.py new file mode 100644 index 0000000000..f16da77b1d --- /dev/null +++ b/Wrappers/Python/cil/framework/image_geometry.py @@ -0,0 +1,322 @@ +# Copyright 2018 United Kingdom Research and Innovation +# Copyright 2018 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +import copy +import warnings +from numbers import Number + +import numpy + +from .image_data import ImageData +from .label import data_order, image_labels + + +class ImageGeometry: + @property + def CHANNEL(self): + warnings.warn("use image_labels['CHANNEL'] instead", DeprecationWarning, stacklevel=2) + return image_labels['CHANNEL'] + + @property + def HORIZONTAL_X(self): + warnings.warn("use image_labels['HORIZONTAL_X'] instead", DeprecationWarning, stacklevel=2) + return image_labels['HORIZONTAL_X'] + + @property + def HORIZONTAL_Y(self): + warnings.warn("use image_labels['HORIZONTAL_Y'] instead", DeprecationWarning, stacklevel=2) + return image_labels['HORIZONTAL_Y'] + + @property + def RANDOM(self): + warnings.warn("use image_labels['RANDOM'] instead", DeprecationWarning, stacklevel=2) + return image_labels['RANDOM'] + + @property + def RANDOM_INT(self): + warnings.warn("use image_labels['RANDOM_INT'] instead", DeprecationWarning, stacklevel=2) + return image_labels['RANDOM_INT'] + + @property + def VERTICAL(self): + warnings.warn("use image_labels['VERTICAL'] instead", DeprecationWarning, stacklevel=2) + return image_labels['VERTICAL'] + + @property + def shape(self): + + shape_dict = {image_labels["CHANNEL"]: self.channels, + image_labels["VERTICAL"]: self.voxel_num_z, + image_labels["HORIZONTAL_Y"]: self.voxel_num_y, + image_labels["HORIZONTAL_X"]: self.voxel_num_x} + + shape = [] + for label in self.dimension_labels: + shape.append(shape_dict[label]) + + return tuple(shape) + + @shape.setter + def shape(self, val): + print("Deprecated - shape will be set automatically") + + @property + def spacing(self): + + spacing_dict = {image_labels["CHANNEL"]: self.channel_spacing, + image_labels["VERTICAL"]: self.voxel_size_z, + image_labels["HORIZONTAL_Y"]: self.voxel_size_y, + image_labels["HORIZONTAL_X"]: self.voxel_size_x} + + spacing = [] + for label in self.dimension_labels: + spacing.append(spacing_dict[label]) + + return tuple(spacing) + + @property + def length(self): + return len(self.dimension_labels) + + @property + def ndim(self): + return len(self.dimension_labels) + + @property + def dimension_labels(self): + + labels_default = data_order["CIL_IG_LABELS"] + + shape_default = [ self.channels, + self.voxel_num_z, + self.voxel_num_y, + self.voxel_num_x] + + try: + labels = list(self._dimension_labels) + except AttributeError: + labels = labels_default.copy() + + for i, x in enumerate(shape_default): + if x == 0 or x==1: + try: + labels.remove(labels_default[i]) + except ValueError: + pass #if not in custom list carry on + return tuple(labels) + + @dimension_labels.setter + def dimension_labels(self, val): + self.set_labels(val) + + def set_labels(self, labels): + labels_default = data_order["CIL_IG_LABELS"] + + #check input and store. This value is not used directly + if labels is not None: + for x in labels: + if x not in labels_default: + raise ValueError('Requested axis are not possible. Accepted label names {},\ngot {}'\ + .format(labels_default,labels)) + + self._dimension_labels = tuple(labels) + + def __eq__(self, other): + + if not isinstance(other, self.__class__): + return False + + if self.voxel_num_x == other.voxel_num_x \ + and self.voxel_num_y == other.voxel_num_y \ + and self.voxel_num_z == other.voxel_num_z \ + and self.voxel_size_x == other.voxel_size_x \ + and self.voxel_size_y == other.voxel_size_y \ + and self.voxel_size_z == other.voxel_size_z \ + and self.center_x == other.center_x \ + and self.center_y == other.center_y \ + and self.center_z == other.center_z \ + and self.channels == other.channels \ + and self.channel_spacing == other.channel_spacing \ + and self.dimension_labels == other.dimension_labels: + + return True + + return False + + @property + def dtype(self): + return self._dtype + + @dtype.setter + def dtype(self, val): + self._dtype = val + + def __init__(self, + voxel_num_x=0, + voxel_num_y=0, + voxel_num_z=0, + voxel_size_x=1, + voxel_size_y=1, + voxel_size_z=1, + center_x=0, + center_y=0, + center_z=0, + channels=1, + **kwargs): + + self.voxel_num_x = int(voxel_num_x) + self.voxel_num_y = int(voxel_num_y) + self.voxel_num_z = int(voxel_num_z) + self.voxel_size_x = float(voxel_size_x) + self.voxel_size_y = float(voxel_size_y) + self.voxel_size_z = float(voxel_size_z) + self.center_x = center_x + self.center_y = center_y + self.center_z = center_z + self.channels = channels + self.channel_labels = None + self.channel_spacing = 1.0 + self.dimension_labels = kwargs.get('dimension_labels', None) + self.dtype = kwargs.get('dtype', numpy.float32) + + + def get_slice(self,channel=None, vertical=None, horizontal_x=None, horizontal_y=None): + ''' + Returns a new ImageGeometry of a single slice of in the requested direction. + ''' + geometry_new = self.copy() + if channel is not None: + geometry_new.channels = 1 + + try: + geometry_new.channel_labels = [self.channel_labels[channel]] + except: + geometry_new.channel_labels = None + + if vertical is not None: + geometry_new.voxel_num_z = 0 + + if horizontal_y is not None: + geometry_new.voxel_num_y = 0 + + if horizontal_x is not None: + geometry_new.voxel_num_x = 0 + + return geometry_new + + def get_order_by_label(self, dimension_labels, default_dimension_labels): + order = [] + for i, el in enumerate(default_dimension_labels): + for j, ek in enumerate(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 + + def get_max_x(self): + return self.center_x + 0.5*self.voxel_num_x*self.voxel_size_x + + def get_min_y(self): + return self.center_y - 0.5*self.voxel_num_y*self.voxel_size_y + + def get_max_y(self): + return self.center_y + 0.5*self.voxel_num_y*self.voxel_size_y + + def get_min_z(self): + if not self.voxel_num_z == 0: + return self.center_z - 0.5*self.voxel_num_z*self.voxel_size_z + else: + return 0 + + def get_max_z(self): + if not self.voxel_num_z == 0: + return self.center_z + 0.5*self.voxel_num_z*self.voxel_size_z + else: + return 0 + + def clone(self): + '''returns a copy of the ImageGeometry''' + return copy.deepcopy(self) + + def copy(self): + '''alias of clone''' + return self.clone() + + def __str__ (self): + repres = "" + repres += "Number of channels: {0}\n".format(self.channels) + repres += "channel_spacing: {0}\n".format(self.channel_spacing) + + if self.voxel_num_z > 0: + repres += "voxel_num : x{0},y{1},z{2}\n".format(self.voxel_num_x, self.voxel_num_y, self.voxel_num_z) + 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) + else: + repres += "voxel_num : x{0},y{1}\n".format(self.voxel_num_x, self.voxel_num_y) + repres += "voxel_size : x{0},y{1}\n".format(self.voxel_size_x, self.voxel_size_y) + repres += "center : x{0},y{1}\n".format(self.center_x, self.center_y) + + return repres + def allocate(self, value=0, **kwargs): + '''allocates an ImageData according to the size expressed in the instance + + :param value: accepts numbers to allocate an uniform array, or a string as 'random' or 'random_int' to create a random array or None. + :type value: number or string, default None allocates empty memory block, default 0 + :param dtype: numerical type to allocate + :type dtype: numpy type, default numpy.float32 + ''' + + dtype = kwargs.get('dtype', self.dtype) + + if kwargs.get('dimension_labels', None) is not None: + raise ValueError("Deprecated: 'dimension_labels' cannot be set with 'allocate()'. Use 'geometry.set_labels()' to modify the geometry before using allocate.") + + out = ImageData(geometry=self.copy(), + dtype=dtype, + suppress_warning=True) + + if isinstance(value, Number): + # it's created empty, so we make it 0 + out.array.fill(value) + else: + if value == image_labels["RANDOM"]: + seed = kwargs.get('seed', None) + if seed is not None: + numpy.random.seed(seed) + if numpy.iscomplexobj(out.array): + r = numpy.random.random_sample(self.shape) + 1j * numpy.random.random_sample(self.shape) + out.fill(r) + else: + out.fill(numpy.random.random_sample(self.shape)) + elif value == image_labels["RANDOM_INT"]: + seed = kwargs.get('seed', None) + if seed is not None: + numpy.random.seed(seed) + max_value = kwargs.get('max_value', 100) + if numpy.iscomplexobj(out.array): + out.fill(numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) + 1.j*numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32)) + else: + out.fill(numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32)) + elif value is None: + pass + else: + raise ValueError('Value {} unknown'.format(value)) + + return out diff --git a/Wrappers/Python/cil/framework/vector_data.py b/Wrappers/Python/cil/framework/vector_data.py new file mode 100644 index 0000000000..b7923f93aa --- /dev/null +++ b/Wrappers/Python/cil/framework/vector_data.py @@ -0,0 +1,74 @@ +# Copyright 2018 United Kingdom Research and Innovation +# Copyright 2018 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +import numpy + +from .data_container import DataContainer + + +class VectorData(DataContainer): + '''DataContainer to contain 1D array''' + + @property + def geometry(self): + return self._geometry + + @geometry.setter + def geometry(self, val): + self._geometry = val + + @property + def dimension_labels(self): + if hasattr(self, 'geometry'): + return self.geometry.dimension_labels + return self._dimension_labels + + @dimension_labels.setter + def dimension_labels(self, val): + if hasattr(self,'geometry'): + self.geometry.dimension_labels = val + + self._dimension_labels = val + + def __init__(self, array=None, **kwargs): + self.geometry = kwargs.get('geometry', None) + + 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: + from .vector_geometry import VectorGeometry + if len(array.shape) > 1: + raise ValueError('Incompatible size: expected 1D got {}'.format(array.shape)) + out = array + self.geometry = VectorGeometry(array.shape[0], **kwargs) + self.length = self.geometry.length + else: + self.length = self.geometry.length + + if array is None: + out = numpy.zeros((self.length,), dtype=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 + # need to pass the geometry, othewise None + super(VectorData, self).__init__(out, deep_copy, self.geometry.dimension_labels, geometry = self.geometry) diff --git a/Wrappers/Python/cil/framework/vector_geometry.py b/Wrappers/Python/cil/framework/vector_geometry.py new file mode 100644 index 0000000000..64c677be15 --- /dev/null +++ b/Wrappers/Python/cil/framework/vector_geometry.py @@ -0,0 +1,113 @@ +# Copyright 2018 United Kingdom Research and Innovation +# Copyright 2018 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +import copy +from numbers import Number + +import numpy + + +class VectorGeometry: + '''Geometry describing VectorData to contain 1D array''' + RANDOM = 'random' + RANDOM_INT = 'random_int' + + @property + def dtype(self): + return self._dtype + + @dtype.setter + def dtype(self, val): + self._dtype = val + + def __init__(self, + length, **kwargs): + + self.length = int(length) + self.shape = (length, ) + self.dtype = kwargs.get('dtype', numpy.float32) + self.dimension_labels = kwargs.get('dimension_labels', None) + + def clone(self): + '''returns a copy of VectorGeometry''' + return copy.deepcopy(self) + + def copy(self): + '''alias of clone''' + return self.clone() + + def __eq__(self, other): + + if not isinstance(other, self.__class__): + return False + + if self.length == other.length \ + and self.shape == other.shape \ + and self.dimension_labels == other.dimension_labels: + return True + return False + + def __str__ (self): + repres = "" + repres += "Length: {0}\n".format(self.length) + repres += "Shape: {0}\n".format(self.shape) + repres += "Dimension_labels: {0}\n".format(self.dimension_labels) + + return repres + + def allocate(self, value=0, **kwargs): + '''allocates an VectorData according to the size expressed in the instance + + :param value: accepts numbers to allocate an uniform array, or a string as 'random' or 'random_int' to create a random array or None. + :type value: number or string, default None allocates empty memory block + :param dtype: numerical type to allocate + :type dtype: numpy type, default numpy.float32 + :param seed: seed for the random number generator + :type seed: int, default None + :param max_value: max value of the random int array + :type max_value: int, default 100''' + from .vector_data import VectorData + + dtype = kwargs.get('dtype', self.dtype) + # self.dtype = kwargs.get('dtype', numpy.float32) + out = VectorData(geometry=self.copy(), dtype=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) + if numpy.iscomplexobj(out.array): + out.fill(numpy.random.random_sample(self.shape) + 1.j*numpy.random.random_sample(self.shape)) + else: + 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) + if numpy.iscomplexobj(out.array): + out.fill(numpy.random.randint(max_value, size=self.shape, dtype=numpy.int32) + 1.j*numpy.random.randint(max_value, size=self.shape, dtype=numpy.int32)) + else: + out.fill(numpy.random.randint(max_value, size=self.shape, dtype=numpy.int32)) + elif value is None: + pass + else: + raise ValueError('Value {} unknown'.format(value)) + return out From 18e260902631bd6ce3a7411f614eec6c36185ffc Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 14 Aug 2024 08:46:55 +0100 Subject: [PATCH 35/72] add geometry_labels --- Wrappers/Python/cil/framework/block.py | 24 +++++++++++++------ Wrappers/Python/cil/framework/label.py | 16 +++++++++---- .../Python/cil/framework/vector_geometry.py | 17 +++++++++---- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/Wrappers/Python/cil/framework/block.py b/Wrappers/Python/cil/framework/block.py index 320dade464..0d6259f94d 100644 --- a/Wrappers/Python/cil/framework/block.py +++ b/Wrappers/Python/cil/framework/block.py @@ -15,16 +15,26 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +import functools +import warnings +from numbers import Number import numpy -from numbers import Number -import functools -from cil.utilities.multiprocessing import NUM_THREADS + +from ..utilities.multiprocessing import NUM_THREADS +from .label import geometry_labels + class BlockGeometry(object): + @property + def RANDOM(self): + warnings.warn("use geometry_labels['RANDOM'] instead", DeprecationWarning, stacklevel=2) + return geometry_labels['RANDOM'] - RANDOM = 'random' - RANDOM_INT = 'random_int' + @property + def RANDOM_INT(self): + warnings.warn("use geometry_labels['RANDOM_INT'] instead", DeprecationWarning, stacklevel=2) + return geometry_labels['RANDOM_INT'] @property def dtype(self): @@ -106,7 +116,7 @@ def __next__(self): else: self.index = 0 raise StopIteration - + def __eq__(self, value: object) -> bool: if len(self.geometries) != len(value.geometries): return False @@ -710,7 +720,7 @@ def dot(self, other): def __len__(self): return self.shape[0] - + @property def geometry(self): try: diff --git a/Wrappers/Python/cil/framework/label.py b/Wrappers/Python/cil/framework/label.py index 1fcb3d4786..8aa1e06331 100644 --- a/Wrappers/Python/cil/framework/label.py +++ b/Wrappers/Python/cil/framework/label.py @@ -82,6 +82,11 @@ def get_order_for_engine(engine, geometry): return dimensions +class GeometryLabels(TypedDict): + RANDOM: str + RANDOM_INT: str + + image_labels: ImageLabels = {"RANDOM": "random", "RANDOM_INT": "random_int", "CHANNEL": "channel", @@ -89,14 +94,14 @@ def get_order_for_engine(engine, geometry): "HORIZONTAL_X": "horizontal_x", "HORIZONTAL_Y": "horizontal_y"} -acquisition_labels: AcquisitionLabels = {"RANDOM": "random", - "RANDOM_INT": "random_int", +acquisition_labels: AcquisitionLabels = {"RANDOM": image_labels["RANDOM"], + "RANDOM_INT": image_labels["RANDOM_INT"], "ANGLE_UNIT": "angle_unit", "DEGREE": "degree", "RADIAN": "radian", - "CHANNEL": "channel", + "CHANNEL": image_labels["CHANNEL"], "ANGLE": "angle", - "VERTICAL": "vertical", + "VERTICAL": image_labels["VERTICAL"], "HORIZONTAL": "horizontal", "PARALLEL": "parallel", "CONE": "cone", @@ -114,6 +119,9 @@ def get_order_for_engine(engine, geometry): "TOMOPHANTOM_IG_LABELS": [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] } +geometry_labels: GeometryLabels = {"RANDOM": image_labels["RANDOM"], "RANDOM_INT": image_labels["RANDOM_INT"]} + + get_order_for_engine = DataOrder.get_order_for_engine # type: ignore[attr-defined] diff --git a/Wrappers/Python/cil/framework/vector_geometry.py b/Wrappers/Python/cil/framework/vector_geometry.py index 64c677be15..0abec8fccc 100644 --- a/Wrappers/Python/cil/framework/vector_geometry.py +++ b/Wrappers/Python/cil/framework/vector_geometry.py @@ -17,14 +17,23 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt import copy from numbers import Number +import warnings import numpy +from .label import geometry_labels class VectorGeometry: '''Geometry describing VectorData to contain 1D array''' - RANDOM = 'random' - RANDOM_INT = 'random_int' + @property + def RANDOM(self): + warnings.warn("use geometry_labels['RANDOM'] instead", DeprecationWarning, stacklevel=2) + return geometry_labels['RANDOM'] + + @property + def RANDOM_INT(self): + warnings.warn("use geometry_labels['RANDOM_INT'] instead", DeprecationWarning, stacklevel=2) + return geometry_labels['RANDOM_INT'] @property def dtype(self): @@ -89,7 +98,7 @@ def allocate(self, value=0, **kwargs): if value != 0: out += value else: - if value == VectorGeometry.RANDOM: + if value == geometry_labels["RANDOM"]: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) @@ -97,7 +106,7 @@ def allocate(self, value=0, **kwargs): out.fill(numpy.random.random_sample(self.shape) + 1.j*numpy.random.random_sample(self.shape)) else: out.fill(numpy.random.random_sample(self.shape)) - elif value == VectorGeometry.RANDOM_INT: + elif value == geometry_labels["RANDOM_INT"]: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) From 477bfe876468f122d57e23882d4c462b243bdf49 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 14 Aug 2024 09:00:54 +0100 Subject: [PATCH 36/72] misc review comments --- Wrappers/Python/cil/framework/__init__.py | 4 +-- .../Python/cil/framework/acquisition_data.py | 4 +-- .../Python/cil/framework/data_container.py | 26 +++++-------------- Wrappers/Python/cil/framework/processors.py | 5 ---- 4 files changed, 9 insertions(+), 30 deletions(-) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index a436e589a5..de07f5d790 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -20,12 +20,12 @@ from .acquisition_data import AcquisitionData from .acquisition_geometry import AcquisitionGeometry from .system_configuration import SystemConfiguration -from .data_container import message, DataContainer +from .data_container import DataContainer from .image_data import ImageData from .image_geometry import ImageGeometry from .vector_data import VectorData from .vector_geometry import VectorGeometry -from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer, find_key +from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer from .block import BlockDataContainer, BlockGeometry from .partitioner import Partitioner from .label import acquisition_labels, image_labels, data_order, get_order_for_engine, check_order_for_engine diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index 479715649d..3ec9cdf9ab 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -72,9 +72,7 @@ def __init__(self, def get_slice(self,channel=None, angle=None, vertical=None, horizontal=None, force=False): - ''' - Returns a new dataset of a single slice of in the requested direction. \ - ''' + '''Returns a new dataset of a single slice in the requested direction.''' try: geometry_new = self.geometry.get_slice(channel=channel, angle=angle, vertical=vertical, horizontal=horizontal) except ValueError: diff --git a/Wrappers/Python/cil/framework/data_container.py b/Wrappers/Python/cil/framework/data_container.py index 8f55b59397..f0ab0170d7 100644 --- a/Wrappers/Python/cil/framework/data_container.py +++ b/Wrappers/Python/cil/framework/data_container.py @@ -28,16 +28,6 @@ from cil.utilities.multiprocessing import NUM_THREADS -def message(cls, msg, *args): - msg = "{0}: " + msg - for i in range(len(args)): - msg += " {%d}" %(i+1) - args = list(args) - args.insert(0, cls.__name__ ) - - return msg.format(*args ) - - class DataContainer(object): '''Generic class to hold data @@ -454,21 +444,17 @@ def pixel_wise_binary(self, pwop, x2, *args, **kwargs): # dimension_labels=self.dimension_labels, # geometry=self.geometry) return out - else: - raise ValueError(message(type(self),"Wrong size for data memory: out {} x2 {} expected {}".format( out.shape,x2.shape ,self.shape))) + raise ValueError(f"Wrong size for data memory: out {out.shape} x2 {x2.shape} expected {self.shape}") elif issubclass(type(out), DataContainer) and \ isinstance(x2, (Number, numpy.ndarray)): if self.check_dimensions(out): if isinstance(x2, numpy.ndarray) and\ not (x2.shape == self.shape and x2.dtype == self.dtype): - raise ValueError(message(type(self), - "Wrong size for data memory: out {} x2 {} expected {}"\ - .format( out.shape,x2.shape ,self.shape))) + raise ValueError(f"Wrong size for data memory: out {out.shape} x2 {x2.shape} expected {self.shape}") 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)) + raise ValueError(f"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: kwargs['out'] = out @@ -478,7 +464,7 @@ def pixel_wise_binary(self, pwop, x2, *args, **kwargs): # dimension_labels=self.dimension_labels, # geometry=self.geometry) else: - raise ValueError (message(type(self), "incompatible class:" , pwop.__name__, type(out))) + raise ValueError(f"incompatible class: {pwop.__name__} {type(out)}") def add(self, other, *args, **kwargs): if hasattr(other, '__container_priority__') and \ @@ -679,13 +665,13 @@ def pixel_wise_unary(self, pwop, *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)) + raise ValueError(f"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: kwargs['out'] = out pwop(self.as_array(), *args, **kwargs) else: - raise ValueError (message(type(self), "incompatible class:" , pwop.__name__, type(out))) + raise ValueError("incompatible class: {pwop.__name__} {type(out)}") def abs(self, *args, **kwargs): return self.pixel_wise_unary(numpy.abs, *args, **kwargs) diff --git a/Wrappers/Python/cil/framework/processors.py b/Wrappers/Python/cil/framework/processors.py index 2a16b6eaef..84a6efdaf7 100644 --- a/Wrappers/Python/cil/framework/processors.py +++ b/Wrappers/Python/cil/framework/processors.py @@ -21,11 +21,6 @@ from .data_container import DataContainer -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 Processor(object): '''Defines a generic DataContainer processor From 545783007def48d5037241c4a34e684159f87906 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 14 Aug 2024 09:01:13 +0100 Subject: [PATCH 37/72] fix merge --- Wrappers/Python/cil/plugins/astra/processors/FBP.py | 12 ++---------- Wrappers/Python/cil/plugins/tigre/FBP.py | 12 ++---------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/Wrappers/Python/cil/plugins/astra/processors/FBP.py b/Wrappers/Python/cil/plugins/astra/processors/FBP.py index 77b4a98b2e..70aa8baebb 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/FBP.py +++ b/Wrappers/Python/cil/plugins/astra/processors/FBP.py @@ -58,19 +58,11 @@ class FBP(DataProcessor): This uses the ram-lak filter only. """ - def __init__(self, image_geometry=None, acquisition_geometry=None, device='gpu', **kwargs): - sinogram_geometry = kwargs.get('sinogram_geometry', None) - if sinogram_geometry is not None: - acquisition_geometry = sinogram_geometry - warnings.warn("Use acquisition_geometry instead of sinogram_geometry", DeprecationWarning, stacklevel=2) - volume_geometry = kwargs.get('volume_geometry', None) - if volume_geometry is not None: - image_geometry = volume_geometry - warnings.warn("Use image_geometry instead of volume_geometry", DeprecationWarning, stacklevel=2) + + def __init__(self, image_geometry=None, acquisition_geometry=None, device='gpu'): if acquisition_geometry is None: raise TypeError("Please specify an acquisition_geometry to configure this processor") - if image_geometry is None: image_geometry = acquisition_geometry.get_ImageGeometry() diff --git a/Wrappers/Python/cil/plugins/tigre/FBP.py b/Wrappers/Python/cil/plugins/tigre/FBP.py index 6dde2dff83..9738f8dcab 100644 --- a/Wrappers/Python/cil/plugins/tigre/FBP.py +++ b/Wrappers/Python/cil/plugins/tigre/FBP.py @@ -53,18 +53,10 @@ class FBP(DataProcessor): >>> reconstruction = fbp.get_output() ''' - def __init__(self, image_geometry=None, acquisition_geometry=None, **kwargs): - sinogram_geometry = kwargs.get('sinogram_geometry', None) - if sinogram_geometry is not None: - acquisition_geometry = sinogram_geometry - warnings.warn("Use acquisition_geometry instead of sinogram_geometry", DeprecationWarning, stacklevel=2) - volume_geometry = kwargs.get('volume_geometry', None) - if volume_geometry is not None: - image_geometry = volume_geometry - warnings.warn("Use image_geometry instead of volume_geometry", DeprecationWarning, stacklevel=2) + + def __init__(self, image_geometry=None, acquisition_geometry=None, **kwargs): if acquisition_geometry is None: raise TypeError("Please specify an acquisition_geometry to configure this processor") - if image_geometry is None: image_geometry = acquisition_geometry.get_ImageGeometry() From f6bafe6be1eb50c914805d5ace5df1dd886082c9 Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Wed, 14 Aug 2024 18:04:34 +0000 Subject: [PATCH 38/72] Update label to use enums and separate functionality --- Wrappers/Python/cil/framework/__init__.py | 5 +- .../Python/cil/framework/acquisition_data.py | 17 + .../cil/framework/acquisition_geometry.py | 403 +++++++++++++++--- Wrappers/Python/cil/framework/base.py | 21 - Wrappers/Python/cil/framework/block.py | 10 +- .../Python/cil/framework/data_container.py | 6 +- Wrappers/Python/cil/framework/image_data.py | 19 +- .../Python/cil/framework/image_geometry.py | 74 ++-- Wrappers/Python/cil/framework/label.py | 271 +++++++----- .../cil/framework/system_configuration.py | 326 -------------- .../Python/cil/framework/vector_geometry.py | 19 +- Wrappers/Python/cil/io/ZEISSDataReader.py | 10 +- Wrappers/Python/cil/plugins/TomoPhantom.py | 8 +- .../astra/operators/ProjectionOperator.py | 6 +- .../astra/processors/AstraBackProjector3D.py | 6 +- .../processors/AstraForwardProjector3D.py | 6 +- .../cil/plugins/astra/processors/FBP.py | 6 +- .../utilities/convert_geometry_to_astra.py | 4 +- .../convert_geometry_to_astra_vec_2D.py | 4 +- .../convert_geometry_to_astra_vec_3D.py | 4 +- .../functions/regularisers.py | 4 +- Wrappers/Python/cil/plugins/tigre/FBP.py | 10 +- Wrappers/Python/cil/plugins/tigre/Geometry.py | 4 +- .../cil/plugins/tigre/ProjectionOperator.py | 8 +- .../cil/processors/CofR_image_sharpness.py | 4 +- .../Python/cil/processors/PaganinProcessor.py | 4 +- Wrappers/Python/cil/recon/FBP.py | 6 +- Wrappers/Python/cil/recon/Reconstructor.py | 4 +- Wrappers/Python/cil/utilities/dataexample.py | 12 +- Wrappers/Python/test/test_BlockOperator.py | 8 +- Wrappers/Python/test/test_DataContainer.py | 72 ++-- Wrappers/Python/test/test_Operator.py | 10 +- .../Python/test/test_PluginsTomoPhantom.py | 6 +- Wrappers/Python/test/test_algorithms.py | 10 +- Wrappers/Python/test/test_functions.py | 14 +- Wrappers/Python/test/test_io.py | 4 +- Wrappers/Python/test/test_ring_processor.py | 4 +- Wrappers/Python/test/test_subset.py | 72 ++-- Wrappers/Python/test/utils_projectors.py | 30 +- 39 files changed, 763 insertions(+), 748 deletions(-) delete mode 100644 Wrappers/Python/cil/framework/base.py delete mode 100644 Wrappers/Python/cil/framework/system_configuration.py diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index de07f5d790..bca270c943 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -18,8 +18,7 @@ from .cilacc import cilacc from .acquisition_data import AcquisitionData -from .acquisition_geometry import AcquisitionGeometry -from .system_configuration import SystemConfiguration +from .acquisition_geometry import AcquisitionGeometry, SystemConfiguration from .data_container import DataContainer from .image_data import ImageData from .image_geometry import ImageGeometry @@ -28,4 +27,4 @@ from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer from .block import BlockDataContainer, BlockGeometry from .partitioner import Partitioner -from .label import acquisition_labels, image_labels, data_order, get_order_for_engine, check_order_for_engine +from .label import DimensionLabelsAcquisition, DimensionLabelsImage, FillTypes, UnitsAngles, AcquisitionType, AcquisitionDimension diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index 3ec9cdf9ab..8add45e916 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -17,6 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt import numpy +from .label import DimensionLabelsAcquisition, Backends from .data_container import DataContainer from .partitioner import Partitioner @@ -101,3 +102,19 @@ def get_slice(self,channel=None, angle=None, vertical=None, horizontal=None, for return out else: return AcquisitionData(out.array, deep_copy=False, geometry=geometry_new, suppress_warning=True) + + def reorder(self, order=None): + ''' + reorders the data in memory as requested. + + :param order: ordered list of labels from self.dimension_labels, or order for engine 'astra' or 'tigre' + :type order: list, sting + ''' + + try: + Backends.validate(order) + order = DimensionLabelsAcquisition.get_order_for_engine(order, self.geometry) + except ValueError: + pass + + super(AcquisitionData, self).reorder(order) \ No newline at end of file diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index dabd380b3d..6d1846e2b4 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -22,11 +22,309 @@ import numpy -from .label import acquisition_labels, data_order +from .label import DimensionLabelsAcquisition, UnitsAngles, AcquisitionType, FillTypes, AcquisitionDimension from .acquisition_data import AcquisitionData -from .base import BaseAcquisitionGeometry from .image_geometry import ImageGeometry -from .system_configuration import ComponentDescription, PositionVector, PositionDirectionVector, SystemConfiguration + +class ComponentDescription(object): + r'''This class enables the creation of vectors and unit vectors used to describe the components of a tomography system + ''' + def __init__ (self, dof): + self._dof = dof + + @staticmethod + def create_vector(val): + try: + vec = numpy.array(val, dtype=numpy.float64).reshape(len(val)) + except: + raise ValueError("Can't convert to numpy array") + + return vec + + @staticmethod + def create_unit_vector(val): + vec = ComponentDescription.create_vector(val) + dot_product = vec.dot(vec) + if abs(dot_product)>1e-8: + vec = (vec/numpy.sqrt(dot_product)) + else: + raise ValueError("Can't return a unit vector of a zero magnitude vector") + return vec + + def length_check(self, val): + try: + val_length = len(val) + except: + raise ValueError("Vectors for {0}D geometries must have length = {0}. Got {1}".format(self._dof,val)) + + if val_length != self._dof: + raise ValueError("Vectors for {0}D geometries must have length = {0}. Got {1}".format(self._dof,val)) + + @staticmethod + def test_perpendicular(vector1, vector2): + dor_prod = vector1.dot(vector2) + if abs(dor_prod) <1e-10: + return True + return False + + @staticmethod + def test_parallel(vector1, vector2): + '''For unit vectors only. Returns true if directions are opposite''' + dor_prod = vector1.dot(vector2) + if 1- abs(dor_prod) <1e-10: + return True + return False + + +class PositionVector(ComponentDescription): + r'''This class creates a component of a tomography system with a position attribute + ''' + @property + def position(self): + try: + return self._position + except: + raise AttributeError + + @position.setter + def position(self, val): + self.length_check(val) + self._position = ComponentDescription.create_vector(val) + + +class DirectionVector(ComponentDescription): + r'''This class creates a component of a tomography system with a direction attribute + ''' + @property + def direction(self): + try: + return self._direction + except: + raise AttributeError + + @direction.setter + def direction(self, val): + self.length_check(val) + self._direction = ComponentDescription.create_unit_vector(val) + + +class PositionDirectionVector(PositionVector, DirectionVector): + r'''This class creates a component of a tomography system with position and direction attributes + ''' + pass + + +class Detector1D(PositionVector): + r'''This class creates a component of a tomography system with position and direction_x attributes used for 1D panels + ''' + @property + def direction_x(self): + try: + return self._direction_x + except: + raise AttributeError + + @direction_x.setter + def direction_x(self, val): + self.length_check(val) + self._direction_x = ComponentDescription.create_unit_vector(val) + + @property + def normal(self): + try: + return ComponentDescription.create_unit_vector([self._direction_x[1], -self._direction_x[0]]) + except: + raise AttributeError + + +class Detector2D(PositionVector): + r'''This class creates a component of a tomography system with position, direction_x and direction_y attributes used for 2D panels + ''' + @property + def direction_x(self): + try: + return self._direction_x + except: + raise AttributeError + + @property + def direction_y(self): + try: + return self._direction_y + except: + raise AttributeError + + @property + def normal(self): + try: + return numpy.cross(self._direction_x, self._direction_y) + except: + raise AttributeError + + def set_direction(self, x, y): + self.length_check(x) + x = ComponentDescription.create_unit_vector(x) + + self.length_check(y) + y = ComponentDescription.create_unit_vector(y) + + dot_product = x.dot(y) + if not numpy.isclose(dot_product, 0): + raise ValueError("vectors detector.direction_x and detector.direction_y must be orthogonal") + + self._direction_y = y + self._direction_x = x + + +class SystemConfiguration(object): + r'''This is a generic class to hold the description of a tomography system + ''' + + SYSTEM_SIMPLE = 'simple' + SYSTEM_OFFSET = 'offset' + SYSTEM_ADVANCED = 'advanced' + + @property + def dimension(self): + if self._dimension == 2: + return AcquisitionDimension.DIM2.value + else: + return AcquisitionDimension.DIM3.value + + @dimension.setter + def dimension(self,val): + if val != 2 and val != 3: + raise ValueError('Can set up 2D and 3D systems only. got {0}D'.format(val)) + else: + self._dimension = val + + @property + def geometry(self): + return self._geometry + + @geometry.setter + def geometry(self,val): + AcquisitionType.validate(val) + self._geometry = AcquisitionType.get_enum_member(val) + + def __init__(self, dof, geometry, units='units'): + """Initialises the system component attributes for the acquisition type + """ + self.dimension = dof + self.geometry = geometry + self.units = units + + if self.geometry == AcquisitionType.PARALLEL: + self.ray = DirectionVector(dof) + else: + self.source = PositionVector(dof) + + if dof == 2: + self.detector = Detector1D(dof) + self.rotation_axis = PositionVector(dof) + else: + self.detector = Detector2D(dof) + self.rotation_axis = PositionDirectionVector(dof) + + def __str__(self): + """Implements the string representation of the system configuration + """ + raise NotImplementedError + + def __eq__(self, other): + """Implements the equality check of the system configuration + """ + raise NotImplementedError + + @staticmethod + def rotation_vec_to_y(vec): + ''' returns a rotation matrix that will rotate the projection of vec on the x-y plane to the +y direction [0,1, Z]''' + + vec = ComponentDescription.create_unit_vector(vec) + + axis_rotation = numpy.eye(len(vec)) + + if numpy.allclose(vec[:2],[0,1]): + pass + elif numpy.allclose(vec[:2],[0,-1]): + axis_rotation[0][0] = -1 + axis_rotation[1][1] = -1 + else: + theta = math.atan2(vec[0],vec[1]) + axis_rotation[0][0] = axis_rotation[1][1] = math.cos(theta) + axis_rotation[0][1] = -math.sin(theta) + axis_rotation[1][0] = math.sin(theta) + + return axis_rotation + + @staticmethod + def rotation_vec_to_z(vec): + ''' returns a rotation matrix that will align vec with the z-direction [0,0,1]''' + + vec = ComponentDescription.create_unit_vector(vec) + + if len(vec) == 2: + return numpy.array([[1, 0],[0, 1]]) + + elif len(vec) == 3: + axis_rotation = numpy.eye(3) + + if numpy.allclose(vec,[0,0,1]): + pass + elif numpy.allclose(vec,[0,0,-1]): + axis_rotation = numpy.eye(3) + axis_rotation[1][1] = -1 + axis_rotation[2][2] = -1 + else: + vx = numpy.array([[0, 0, -vec[0]], [0, 0, -vec[1]], [vec[0], vec[1], 0]]) + axis_rotation = numpy.eye(3) + vx + vx.dot(vx) * 1 / (1 + vec[2]) + + else: + raise ValueError("Vec must have length 3, got {}".format(len(vec))) + + return axis_rotation + + def update_reference_frame(self): + r'''Transforms the system origin to the rotation_axis position + ''' + self.set_origin(self.rotation_axis.position) + + + def set_origin(self, origin): + r'''Transforms the system origin to the input origin + ''' + translation = origin.copy() + if hasattr(self,'source'): + self.source.position -= translation + + self.detector.position -= translation + self.rotation_axis.position -= translation + + + def get_centre_slice(self): + """Returns the 2D system configuration corresponding to the centre slice + """ + raise NotImplementedError + + def calculate_magnification(self): + r'''Calculates the magnification of the system using the source to rotate axis, + and source to detector distance along the direction. + + :return: returns [dist_source_center, dist_center_detector, magnification], [0] distance from the source to the rotate axis, [1] distance from the rotate axis to the detector, [2] magnification of the system + :rtype: list + ''' + raise NotImplementedError + + def system_description(self): + r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, + \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets + \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets + ''' + raise NotImplementedError + + def copy(self): + '''returns a copy of SystemConfiguration''' + return copy.deepcopy(self) class Parallel2D(SystemConfiguration): @@ -47,7 +345,7 @@ class Parallel2D(SystemConfiguration): def __init__ (self, ray_direction, detector_pos, detector_direction_x, rotation_axis_pos, units='units'): """Constructor method """ - super(Parallel2D, self).__init__(dof=2, geometry = 'parallel', units=units) + super(Parallel2D, self).__init__(dof=2, geometry = AcquisitionType.PARALLEL, units=units) #source self.ray.direction = ray_direction @@ -221,7 +519,7 @@ class Parallel3D(SystemConfiguration): def __init__ (self, ray_direction, detector_pos, detector_direction_x, detector_direction_y, rotation_axis_pos, rotation_axis_direction, units='units'): """Constructor method """ - super(Parallel3D, self).__init__(dof=3, geometry = 'parallel', units=units) + super(Parallel3D, self).__init__(dof=3, geometry = AcquisitionType.PARALLEL, units=units) #source self.ray.direction = ray_direction @@ -506,7 +804,7 @@ class Cone2D(SystemConfiguration): def __init__ (self, source_pos, detector_pos, detector_direction_x, rotation_axis_pos, units='units'): """Constructor method """ - super(Cone2D, self).__init__(dof=2, geometry = 'cone', units=units) + super(Cone2D, self).__init__(dof=2, geometry = AcquisitionType.CONE, units=units) #source self.source.position = source_pos @@ -685,7 +983,7 @@ class Cone3D(SystemConfiguration): def __init__ (self, source_pos, detector_pos, detector_direction_x, detector_direction_y, rotation_axis_pos, rotation_axis_direction, units='units'): """Constructor method """ - super(Cone3D, self).__init__(dof=3, geometry = 'cone', units=units) + super(Cone3D, self).__init__(dof=3, geometry = AcquisitionType.CONE, units=units) #source self.source.position = source_pos @@ -1171,10 +1469,8 @@ def angle_unit(self): @angle_unit.setter def angle_unit(self,val): - if val != acquisition_labels["DEGREE"] and val != acquisition_labels["RADIAN"]: - raise ValueError('angle_unit = {} not recognised please specify \'degree\' or \'radian\''.format(val)) - else: - self._angle_unit = val + UnitsAngles.validate(val) + self._angle_unit = UnitsAngles.get_enum_value(val) def __str__(self): repres = "Acquisition description:\n" @@ -1310,7 +1606,7 @@ def __eq__(self, other): return False -class AcquisitionGeometry(BaseAcquisitionGeometry): +class AcquisitionGeometry(object): """This class holds the AcquisitionGeometry of the system. Please initialise the AcquisitionGeometry using the using the static methods: @@ -1328,33 +1624,33 @@ class AcquisitionGeometry(BaseAcquisitionGeometry): #for backwards compatibility @property def ANGLE(self): - warnings.warn("use acquisition_labels['ANGLE'] instead", DeprecationWarning, stacklevel=2) - return acquisition_labels['ANGLE'] + warnings.warn("use DimensionLabelsAcquisition.Angle instead", DeprecationWarning, stacklevel=2) + return DimensionLabelsAcquisition.ANGLE @property def CHANNEL(self): - warnings.warn("use acquisition_labels['CHANNEL'] instead", DeprecationWarning, stacklevel=2) - return acquisition_labels['CHANNEL'] + warnings.warn("use DimensionLabelsAcquisition.Channel instead", DeprecationWarning, stacklevel=2) + return DimensionLabelsAcquisition.CHANNEL @property def DEGREE(self): - warnings.warn("use acquisition_labels['DEGREE'] instead", DeprecationWarning, stacklevel=2) - return acquisition_labels['DEGREE'] + warnings.warn("use UnitsAngles.DEGREE", DeprecationWarning, stacklevel=2) + return UnitsAngles.DEGREE @property def HORIZONTAL(self): - warnings.warn("use acquisition_labels['HORIZONTAL'] instead", DeprecationWarning, stacklevel=2) - return acquisition_labels['HORIZONTAL'] + warnings.warn("use DimensionLabelsAcquisition.HORIZONTAL instead", DeprecationWarning, stacklevel=2) + return DimensionLabelsAcquisition.HORIZONTAL @property def RADIAN(self): - warnings.warn("use acquisition_labels['RADIAN'] instead", DeprecationWarning, stacklevel=2) - return acquisition_labels['RADIAN'] + warnings.warn("use UnitsAngles.RADIAN instead", DeprecationWarning, stacklevel=2) + return UnitsAngles.RADIAN @property def VERTICAL(self): - warnings.warn("use acquisition_labels['VERTICAL'] instead", DeprecationWarning, stacklevel=2) - return acquisition_labels['VERTICAL'] + warnings.warn("use DimensionLabelsAcquisition.VERTICAL instead", DeprecationWarning, stacklevel=2) + return DimensionLabelsAcquisition.VERTICAL @property def geom_type(self): @@ -1426,10 +1722,10 @@ def dimension(self): @property def shape(self): - shape_dict = {acquisition_labels["CHANNEL"]: self.config.channels.num_channels, - acquisition_labels["ANGLE"]: self.config.angles.num_positions, - acquisition_labels["VERTICAL"]: self.config.panel.num_pixels[1], - acquisition_labels["HORIZONTAL"]: self.config.panel.num_pixels[0]} + shape_dict = {DimensionLabelsAcquisition.CHANNEL.value: self.config.channels.num_channels, + DimensionLabelsAcquisition.ANGLE.value: self.config.angles.num_positions, + DimensionLabelsAcquisition.VERTICAL.value: self.config.panel.num_pixels[1], + DimensionLabelsAcquisition.HORIZONTAL.value: self.config.panel.num_pixels[0]} shape = [] for label in self.dimension_labels: shape.append(shape_dict[label]) @@ -1438,7 +1734,7 @@ def shape(self): @property def dimension_labels(self): - labels_default = data_order["CIL_AG_LABELS"] + labels_default = DimensionLabelsAcquisition.get_default_order_for_engine("CIL") shape_default = [self.config.channels.num_channels, self.config.angles.num_positions, @@ -1465,15 +1761,14 @@ def dimension_labels(self): @dimension_labels.setter def dimension_labels(self, val): - labels_default = data_order["CIL_AG_LABELS"] - #check input and store. This value is not used directly if val is not None: + label_new=[] for x in val: - if x not in labels_default: - raise ValueError('Requested axis are not possible. Accepted label names {},\ngot {}'.format(labels_default,val)) + if DimensionLabelsAcquisition.validate(x): + label_new.append(DimensionLabelsAcquisition.get_enum_value(x)) - self._dimension_labels = tuple(val) + self._dimension_labels = tuple(label_new) @property def ndim(self): @@ -1544,16 +1839,14 @@ def get_centre_of_rotation(self, distance_units='default', angle_units='radian') else: raise ValueError("`distance_units` is not recognised. Must be 'default' or 'pixels'. Got {}".format(distance_units)) - if angle_units == 'radian': - angle = angle_rad - ang_units = 'radian' - elif angle_units == 'degree': + UnitsAngles.validate(angle_units) + angle_units = UnitsAngles.get_enum_member(angle_units) + + angle = angle_rad + if angle_units == UnitsAngles.DEGREE: angle = numpy.degrees(angle_rad) - ang_units = 'degree' - else: - raise ValueError("`angle_units` is not recognised. Must be 'radian' or 'degree'. Got {}".format(angle_units)) - return {'offset': (offset, offset_units), 'angle': (angle, ang_units)} + return {'offset': (offset, offset_units), 'angle': (angle, angle_units.value)} def set_centre_of_rotation(self, offset=0.0, distance_units='default', angle=0.0, angle_units='radian'): @@ -1590,12 +1883,13 @@ def set_centre_of_rotation(self, offset=0.0, distance_units='default', angle=0.0 if not hasattr(self.config.system, 'set_centre_of_rotation'): raise NotImplementedError() - if angle_units == 'radian': - angle_rad = angle - elif angle_units == 'degree': + + UnitsAngles.validate(angle_units) + angle_units = UnitsAngles.get_enum_member(angle_units) + + angle_rad = angle + if angle_units == UnitsAngles.DEGREE: angle_rad = numpy.radians(angle) - else: - raise ValueError("`angle_units` is not recognised. Must be 'radian' or 'degree'. Got {}".format(angle_units)) if distance_units =='default': offset_distance = offset @@ -1881,7 +2175,7 @@ def get_slice(self, channel=None, angle=None, vertical=None, horizontal=None): geometry_new.config.angles.angle_data = geometry_new.config.angles.angle_data[angle] if vertical is not None: - if geometry_new.geom_type == acquisition_labels["PARALLEL"] or vertical == 'centre' or abs(geometry_new.pixel_num_v/2 - vertical) < 1e-6: + if geometry_new.geom_type == AcquisitionType.PARALLEL or vertical == 'centre' or abs(geometry_new.pixel_num_v/2 - vertical) < 1e-6: geometry_new = geometry_new.get_centre_slice() else: raise ValueError("Can only subset centre slice geometry on cone-beam data. Expected vertical = 'centre'. Got vertical = {0}".format(vertical)) @@ -1911,8 +2205,11 @@ def allocate(self, value=0, **kwargs): if isinstance(value, Number): # it's created empty, so we make it 0 out.array.fill(value) - else: - if value == acquisition_labels["RANDOM"]: + elif value is not None: + FillTypes.validate(value) + value = FillTypes.get_enum_member(value) + + if value == FillTypes.RANDOM: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) @@ -1921,7 +2218,7 @@ def allocate(self, value=0, **kwargs): out.fill(r) else: out.fill(numpy.random.random_sample(self.shape)) - elif value == acquisition_labels["RANDOM_INT"]: + elif value == FillTypes.RANDOM_INT: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) @@ -1931,9 +2228,5 @@ def allocate(self, value=0, **kwargs): else: r = numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) out.fill(numpy.asarray(r, dtype=dtype)) - elif value is None: - pass - else: - raise ValueError('Value {} unknown'.format(value)) return out diff --git a/Wrappers/Python/cil/framework/base.py b/Wrappers/Python/cil/framework/base.py deleted file mode 100644 index 5f738b0a84..0000000000 --- a/Wrappers/Python/cil/framework/base.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2018 United Kingdom Research and Innovation -# Copyright 2018 The University of Manchester -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Authors: -# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -class BaseAcquisitionGeometry: - """This class only exists to give get_order_for_engine something to typecheck for. At some point separate interface - stuff into here or refactor get_order_for_engine to not need it.""" - pass diff --git a/Wrappers/Python/cil/framework/block.py b/Wrappers/Python/cil/framework/block.py index 0d6259f94d..5fd849db1a 100644 --- a/Wrappers/Python/cil/framework/block.py +++ b/Wrappers/Python/cil/framework/block.py @@ -22,19 +22,19 @@ import numpy from ..utilities.multiprocessing import NUM_THREADS -from .label import geometry_labels +from .label import FillTypes class BlockGeometry(object): @property def RANDOM(self): - warnings.warn("use geometry_labels['RANDOM'] instead", DeprecationWarning, stacklevel=2) - return geometry_labels['RANDOM'] + warnings.warn("use FillTypes.RANDOM instead", DeprecationWarning, stacklevel=2) + return FillTypes.RANDOM @property def RANDOM_INT(self): - warnings.warn("use geometry_labels['RANDOM_INT'] instead", DeprecationWarning, stacklevel=2) - return geometry_labels['RANDOM_INT'] + warnings.warn("use FillTypes.RANDOM_INT instead", DeprecationWarning, stacklevel=2) + return FillTypes.RANDOM_INT @property def dtype(self): diff --git a/Wrappers/Python/cil/framework/data_container.py b/Wrappers/Python/cil/framework/data_container.py index f0ab0170d7..327dc86280 100644 --- a/Wrappers/Python/cil/framework/data_container.py +++ b/Wrappers/Python/cil/framework/data_container.py @@ -23,7 +23,6 @@ import numpy -from .label import data_order, get_order_for_engine from .cilacc import cilacc from cil.utilities.multiprocessing import NUM_THREADS @@ -190,12 +189,10 @@ def reorder(self, order=None): ''' reorders the data in memory as requested. - :param order: ordered list of labels from self.dimension_labels, or order for engine 'astra' or 'tigre' + :param order: ordered list of labels from self.dimension_labels :type order: list, sting ''' - if order in data_order["ENGINES"]: - order = get_order_for_engine(order, self.geometry) try: if len(order) != len(self.shape): @@ -223,6 +220,7 @@ def reorder(self, order=None): else: self.geometry.set_labels(dimension_labels_new) + def fill(self, array, **dimension): '''fills the internal data array with the DataContainer, numpy array or number provided diff --git a/Wrappers/Python/cil/framework/image_data.py b/Wrappers/Python/cil/framework/image_data.py index b579b06fd7..578a5850c9 100644 --- a/Wrappers/Python/cil/framework/image_data.py +++ b/Wrappers/Python/cil/framework/image_data.py @@ -18,7 +18,7 @@ import numpy from .data_container import DataContainer - +from .label import DimensionLabelsImage, Backends class ImageData(DataContainer): '''DataContainer for holding 2D or 3D DataContainer''' @@ -189,3 +189,20 @@ def apply_circular_mask(self, radius=0.99, in_place=True): image_data_out.reorder(labels_orig) return image_data_out + + + def reorder(self, order=None): + ''' + reorders the data in memory as requested. + + :param order: ordered list of labels from self.dimension_labels, or order for engine 'astra' or 'tigre' + :type order: list, sting + ''' + + try: + Backends.validate(order) + order = DimensionLabelsImage.get_order_for_engine(order, self.geometry) + except ValueError: + pass + + super(ImageData, self).reorder(order) diff --git a/Wrappers/Python/cil/framework/image_geometry.py b/Wrappers/Python/cil/framework/image_geometry.py index f16da77b1d..dc08a774a4 100644 --- a/Wrappers/Python/cil/framework/image_geometry.py +++ b/Wrappers/Python/cil/framework/image_geometry.py @@ -22,47 +22,45 @@ import numpy from .image_data import ImageData -from .label import data_order, image_labels +from .label import DimensionLabelsImage, FillTypes class ImageGeometry: @property def CHANNEL(self): - warnings.warn("use image_labels['CHANNEL'] instead", DeprecationWarning, stacklevel=2) - return image_labels['CHANNEL'] + warnings.warn("use DimensionLabelsImage.CHANNEL instead", DeprecationWarning, stacklevel=2) + return DimensionLabelsImage.CHANNEL @property def HORIZONTAL_X(self): - warnings.warn("use image_labels['HORIZONTAL_X'] instead", DeprecationWarning, stacklevel=2) - return image_labels['HORIZONTAL_X'] + warnings.warn("use DimensionLabelsImage.HORIZONTAL_X instead", DeprecationWarning, stacklevel=2) + return DimensionLabelsImage.HORIZONTAL_X @property def HORIZONTAL_Y(self): - warnings.warn("use image_labels['HORIZONTAL_Y'] instead", DeprecationWarning, stacklevel=2) - return image_labels['HORIZONTAL_Y'] + warnings.warn("use DimensionLabelsImage.HORIZONTAL_Y instead", DeprecationWarning, stacklevel=2) + return DimensionLabelsImage.HORIZONTAL_Y @property def RANDOM(self): - warnings.warn("use image_labels['RANDOM'] instead", DeprecationWarning, stacklevel=2) - return image_labels['RANDOM'] - + warnings.warn("use FillTypes.RANDOM instead", DeprecationWarning, stacklevel=2) + return FillTypes.RANDOM @property def RANDOM_INT(self): - warnings.warn("use image_labels['RANDOM_INT'] instead", DeprecationWarning, stacklevel=2) - return image_labels['RANDOM_INT'] + warnings.warn("use FillTypes.RANDOM_INT instead", DeprecationWarning, stacklevel=2) + return FillTypes.RANDOM_INT @property def VERTICAL(self): - warnings.warn("use image_labels['VERTICAL'] instead", DeprecationWarning, stacklevel=2) - return image_labels['VERTICAL'] + warnings.warn("use DimensionLabelsImage.VERTICAL instead", DeprecationWarning, stacklevel=2) + return DimensionLabelsImage.VERTICAL @property - def shape(self): - - shape_dict = {image_labels["CHANNEL"]: self.channels, - image_labels["VERTICAL"]: self.voxel_num_z, - image_labels["HORIZONTAL_Y"]: self.voxel_num_y, - image_labels["HORIZONTAL_X"]: self.voxel_num_x} + def shape(self): + shape_dict = {DimensionLabelsImage.CHANNEL.value: self.channels, + DimensionLabelsImage.VERTICAL.value: self.voxel_num_z, + DimensionLabelsImage.HORIZONTAL_Y.value: self.voxel_num_y, + DimensionLabelsImage.HORIZONTAL_X.value: self.voxel_num_x} shape = [] for label in self.dimension_labels: @@ -77,10 +75,10 @@ def shape(self, val): @property def spacing(self): - spacing_dict = {image_labels["CHANNEL"]: self.channel_spacing, - image_labels["VERTICAL"]: self.voxel_size_z, - image_labels["HORIZONTAL_Y"]: self.voxel_size_y, - image_labels["HORIZONTAL_X"]: self.voxel_size_x} + spacing_dict = {DimensionLabelsImage.CHANNEL.value: self.channel_spacing, + DimensionLabelsImage.VERTICAL.value: self.voxel_size_z, + DimensionLabelsImage.HORIZONTAL_Y.value: self.voxel_size_y, + DimensionLabelsImage.HORIZONTAL_X.value: self.voxel_size_x} spacing = [] for label in self.dimension_labels: @@ -99,7 +97,7 @@ def ndim(self): @property def dimension_labels(self): - labels_default = data_order["CIL_IG_LABELS"] + labels_default = DimensionLabelsImage.get_default_order_for_engine("CIL") shape_default = [ self.channels, self.voxel_num_z, @@ -124,16 +122,13 @@ def dimension_labels(self, val): self.set_labels(val) def set_labels(self, labels): - labels_default = data_order["CIL_IG_LABELS"] - - #check input and store. This value is not used directly if labels is not None: + label_new=[] for x in labels: - if x not in labels_default: - raise ValueError('Requested axis are not possible. Accepted label names {},\ngot {}'\ - .format(labels_default,labels)) + if DimensionLabelsImage.validate(x): + label_new.append(DimensionLabelsImage.get_enum_value(x)) - self._dimension_labels = tuple(labels) + self._dimension_labels = tuple(label_new) def __eq__(self, other): @@ -295,8 +290,11 @@ def allocate(self, value=0, **kwargs): if isinstance(value, Number): # it's created empty, so we make it 0 out.array.fill(value) - else: - if value == image_labels["RANDOM"]: + elif value is not None: + FillTypes.validate(value) + value = FillTypes.get_enum_member(value) + + if value == FillTypes.RANDOM: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) @@ -305,7 +303,8 @@ def allocate(self, value=0, **kwargs): out.fill(r) else: out.fill(numpy.random.random_sample(self.shape)) - elif value == image_labels["RANDOM_INT"]: + + elif value == FillTypes.RANDOM_INT: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) @@ -314,9 +313,4 @@ def allocate(self, value=0, **kwargs): out.fill(numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) + 1.j*numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32)) else: out.fill(numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32)) - elif value is None: - pass - else: - raise ValueError('Value {} unknown'.format(value)) - return out diff --git a/Wrappers/Python/cil/framework/label.py b/Wrappers/Python/cil/framework/label.py index 8aa1e06331..753e4dcf53 100644 --- a/Wrappers/Python/cil/framework/label.py +++ b/Wrappers/Python/cil/framework/label.py @@ -1,5 +1,5 @@ -# Copyright 2018 United Kingdom Research and Innovation -# Copyright 2018 The University of Manchester +# Copyright 2024 United Kingdom Research and Innovation +# Copyright 2024 The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,122 +15,161 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from typing import TypedDict, List - -from .base import BaseAcquisitionGeometry - - -class ImageLabels(TypedDict): - RANDOM: str - RANDOM_INT: str - CHANNEL: str - VERTICAL: str - HORIZONTAL_X: str - HORIZONTAL_Y: str - - -class AcquisitionLabels(TypedDict): - RANDOM: str - RANDOM_INT: str - ANGLE_UNIT: str - DEGREE: str - RADIAN: str - CHANNEL: str - ANGLE: str - VERTICAL: str - HORIZONTAL: str - PARALLEL: str - CONE: str - DIM2: str - DIM3: str - - -class DataOrder(TypedDict): - ENGINES: List[str] - ASTRA_IG_LABELS: List[str] - TIGRE_IG_LABELS: List[str] - ASTRA_AG_LABELS: List[str] - TIGRE_AG_LABELS: List[str] - CIL_IG_LABELS: List[str] - CIL_AG_LABELS: List[str] - TOMOPHANTOM_IG_LABELS: List[str] - - @staticmethod # type: ignore[misc] - def get_order_for_engine(engine, geometry): - if engine == 'astra': - if isinstance(geometry, BaseAcquisitionGeometry): - dim_order = data_order["ASTRA_AG_LABELS"] - else: - dim_order = data_order["ASTRA_IG_LABELS"] - elif engine == 'tigre': - if isinstance(geometry, BaseAcquisitionGeometry): - dim_order = data_order["TIGRE_AG_LABELS"] - else: - dim_order = data_order["TIGRE_IG_LABELS"] - elif engine == 'cil': - if isinstance(geometry, BaseAcquisitionGeometry): - dim_order = data_order["CIL_AG_LABELS"] - else: - dim_order = data_order["CIL_IG_LABELS"] + +from enum import Enum + +class LabelsBase(Enum): + + @classmethod + def validate(cls, label): + """ + Validate if the given label is present in the class or its values. + Parameters: + label (str): The label to validate. + Returns: + bool: True if the label is present in the class or its values + Raises: + ValueError: If the label is not present in the class or its values. + """ + if isinstance(label, cls): + return True + elif label in [e.name for e in cls]: + return True + elif label in [e.value for e in cls]: + return True + else: + raise ValueError(f"Expected one of {[e.value for e in cls]}, got {label}") + + @classmethod + def member_from_value(cls, label): + if isinstance(label, str): + label = label.lower() + + for member in cls: + if member.value == label: + return member + raise ValueError(f"{label} is not a valid {cls.__name__}") + + @classmethod + def member_from_key(cls, label): + for member in cls: + if member.name == label: + return member + raise ValueError(f"{label} is not a valid {cls.__name__}") + + @classmethod + def get_enum_member(cls, label): + if isinstance(label, cls): + return label + elif label in [e.name for e in cls]: + return cls.member_from_key(label) + elif label in [e.value for e in cls]: + return cls.member_from_value(label) else: - raise ValueError("Unknown engine expected one of {0} got {1}".format(data_order["ENGINES"], engine)) + raise ValueError(f"{label} is not a valid {cls.__name__}") + + @classmethod + def get_enum_value(cls, label): + return cls.get_enum_member(label).value + + def __eq__(self, other): + if isinstance(other, str): + return self.value == other or self.name == other + return super().__eq__(other) + +class Backends(LabelsBase): + ASTRA = "astra" + TIGRE = "tigre" + CIL = "cil" + +class DimensionLabelsImage(LabelsBase): + CHANNEL = "channel" + VERTICAL = "vertical" + HORIZONTAL_X = "horizontal_x" + HORIZONTAL_Y = "horizontal_y" + + @classmethod + def get_default_order_for_engine(cls, engine): + engine_orders = { + Backends.ASTRA.value: [cls.CHANNEL.value, cls.VERTICAL.value, cls.HORIZONTAL_Y.value, cls.HORIZONTAL_X.value], + Backends.TIGRE.value: [cls.CHANNEL.value, cls.VERTICAL.value, cls.HORIZONTAL_Y.value, cls.HORIZONTAL_X.value], + Backends.CIL.value: [cls.CHANNEL.value, cls.VERTICAL.value, cls.HORIZONTAL_Y.value, cls.HORIZONTAL_X.value] + } + Backends.validate(engine) + engine = Backends.get_enum_value(engine) + + return engine_orders[engine] + + @classmethod + def get_order_for_engine(cls, engine, geometry): + + dim_order = cls.get_default_order_for_engine(engine) + dimensions = [label for label in dim_order if label in geometry.dimension_labels ] + + return dimensions - dimensions = [] - for label in dim_order: - if label in geometry.dimension_labels: - dimensions.append(label) + @classmethod + def check_order_for_engine(cls, engine, geometry): + order_requested = cls.get_order_for_engine(engine, geometry) + + if order_requested == list(geometry.dimension_labels): + return True + else: + raise ValueError( + "Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" + .format(order_requested, list(geometry.dimension_labels), engine)) + +class DimensionLabelsAcquisition(LabelsBase): + CHANNEL = "channel" + ANGLE = "angle" + VERTICAL = "vertical" + HORIZONTAL = "horizontal" + + @classmethod + def get_default_order_for_engine(cls, engine): + engine_orders = { + Backends.ASTRA.value: [cls.CHANNEL.value, cls.VERTICAL.value, cls.ANGLE.value, cls.HORIZONTAL.value], + Backends.TIGRE.value: [cls.CHANNEL.value, cls.ANGLE.value, cls.VERTICAL.value, cls.HORIZONTAL.value], + Backends.CIL.value: [cls.CHANNEL.value, cls.ANGLE.value, cls.VERTICAL.value, cls.HORIZONTAL.value] + } + Backends.validate(engine) + engine = Backends.get_enum_value(engine) + + return engine_orders[engine] + + @classmethod + def get_order_for_engine(cls, engine, geometry): + + dim_order = cls.get_default_order_for_engine(engine) + dimensions = [label for label in dim_order if label in geometry.dimension_labels ] return dimensions -class GeometryLabels(TypedDict): - RANDOM: str - RANDOM_INT: str - - -image_labels: ImageLabels = {"RANDOM": "random", - "RANDOM_INT": "random_int", - "CHANNEL": "channel", - "VERTICAL": "vertical", - "HORIZONTAL_X": "horizontal_x", - "HORIZONTAL_Y": "horizontal_y"} - -acquisition_labels: AcquisitionLabels = {"RANDOM": image_labels["RANDOM"], - "RANDOM_INT": image_labels["RANDOM_INT"], - "ANGLE_UNIT": "angle_unit", - "DEGREE": "degree", - "RADIAN": "radian", - "CHANNEL": image_labels["CHANNEL"], - "ANGLE": "angle", - "VERTICAL": image_labels["VERTICAL"], - "HORIZONTAL": "horizontal", - "PARALLEL": "parallel", - "CONE": "cone", - "DIM2": "2D", - "DIM3": "3D"} - -data_order: DataOrder = \ - {"ENGINES": ["astra", "tigre", "cil"], - "ASTRA_IG_LABELS": [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]], - "TIGRE_IG_LABELS": [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]], - "ASTRA_AG_LABELS": [acquisition_labels["CHANNEL"], acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"], acquisition_labels["HORIZONTAL"]], - "TIGRE_AG_LABELS": [acquisition_labels["CHANNEL"], acquisition_labels["ANGLE"], acquisition_labels["VERTICAL"], acquisition_labels["HORIZONTAL"]], - "CIL_IG_LABELS": [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]], - "CIL_AG_LABELS": [acquisition_labels["CHANNEL"], acquisition_labels["ANGLE"], acquisition_labels["VERTICAL"], acquisition_labels["HORIZONTAL"]], - "TOMOPHANTOM_IG_LABELS": [image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] - } - -geometry_labels: GeometryLabels = {"RANDOM": image_labels["RANDOM"], "RANDOM_INT": image_labels["RANDOM_INT"]} - - -get_order_for_engine = DataOrder.get_order_for_engine # type: ignore[attr-defined] - - -def check_order_for_engine(engine, geometry): - order_requested = get_order_for_engine(engine, geometry) - - if order_requested == list(geometry.dimension_labels): - return True - else: - raise ValueError( - "Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" - .format(order_requested, list(geometry.dimension_labels), engine)) + @classmethod + def check_order_for_engine(cls, engine, geometry): + order_requested = cls.get_order_for_engine(engine, geometry) + + if order_requested == list(geometry.dimension_labels): + return True + else: + raise ValueError( + "Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" + .format(order_requested, list(geometry.dimension_labels), engine)) + +class FillTypes(LabelsBase): + RANDOM = "random" + RANDOM_INT = "random_int" + +class UnitsAngles(LabelsBase): + DEGREE = "degree" + RADIAN = "radian" + + +class AcquisitionType(LabelsBase): + PARALLEL = "parallel" + CONE = "cone" + +class AcquisitionDimension(LabelsBase): + DIM2 = "2D" + DIM3 = "3D" + diff --git a/Wrappers/Python/cil/framework/system_configuration.py b/Wrappers/Python/cil/framework/system_configuration.py deleted file mode 100644 index ffab2913d2..0000000000 --- a/Wrappers/Python/cil/framework/system_configuration.py +++ /dev/null @@ -1,326 +0,0 @@ -# Copyright 2018 United Kingdom Research and Innovation -# Copyright 2018 The University of Manchester -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Authors: -# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -import copy -import math - -import numpy - -from .label import acquisition_labels - - -class ComponentDescription(object): - r'''This class enables the creation of vectors and unit vectors used to describe the components of a tomography system - ''' - def __init__ (self, dof): - self._dof = dof - - @staticmethod - def create_vector(val): - try: - vec = numpy.array(val, dtype=numpy.float64).reshape(len(val)) - except: - raise ValueError("Can't convert to numpy array") - - return vec - - @staticmethod - def create_unit_vector(val): - vec = ComponentDescription.create_vector(val) - dot_product = vec.dot(vec) - if abs(dot_product)>1e-8: - vec = (vec/numpy.sqrt(dot_product)) - else: - raise ValueError("Can't return a unit vector of a zero magnitude vector") - return vec - - def length_check(self, val): - try: - val_length = len(val) - except: - raise ValueError("Vectors for {0}D geometries must have length = {0}. Got {1}".format(self._dof,val)) - - if val_length != self._dof: - raise ValueError("Vectors for {0}D geometries must have length = {0}. Got {1}".format(self._dof,val)) - - @staticmethod - def test_perpendicular(vector1, vector2): - dor_prod = vector1.dot(vector2) - if abs(dor_prod) <1e-10: - return True - return False - - @staticmethod - def test_parallel(vector1, vector2): - '''For unit vectors only. Returns true if directions are opposite''' - dor_prod = vector1.dot(vector2) - if 1- abs(dor_prod) <1e-10: - return True - return False - - -class PositionVector(ComponentDescription): - r'''This class creates a component of a tomography system with a position attribute - ''' - @property - def position(self): - try: - return self._position - except: - raise AttributeError - - @position.setter - def position(self, val): - self.length_check(val) - self._position = ComponentDescription.create_vector(val) - - -class DirectionVector(ComponentDescription): - r'''This class creates a component of a tomography system with a direction attribute - ''' - @property - def direction(self): - try: - return self._direction - except: - raise AttributeError - - @direction.setter - def direction(self, val): - self.length_check(val) - self._direction = ComponentDescription.create_unit_vector(val) - - -class PositionDirectionVector(PositionVector, DirectionVector): - r'''This class creates a component of a tomography system with position and direction attributes - ''' - pass - - -class Detector1D(PositionVector): - r'''This class creates a component of a tomography system with position and direction_x attributes used for 1D panels - ''' - @property - def direction_x(self): - try: - return self._direction_x - except: - raise AttributeError - - @direction_x.setter - def direction_x(self, val): - self.length_check(val) - self._direction_x = ComponentDescription.create_unit_vector(val) - - @property - def normal(self): - try: - return ComponentDescription.create_unit_vector([self._direction_x[1], -self._direction_x[0]]) - except: - raise AttributeError - - -class Detector2D(PositionVector): - r'''This class creates a component of a tomography system with position, direction_x and direction_y attributes used for 2D panels - ''' - @property - def direction_x(self): - try: - return self._direction_x - except: - raise AttributeError - - @property - def direction_y(self): - try: - return self._direction_y - except: - raise AttributeError - - @property - def normal(self): - try: - return numpy.cross(self._direction_x, self._direction_y) - except: - raise AttributeError - - def set_direction(self, x, y): - self.length_check(x) - x = ComponentDescription.create_unit_vector(x) - - self.length_check(y) - y = ComponentDescription.create_unit_vector(y) - - dot_product = x.dot(y) - if not numpy.isclose(dot_product, 0): - raise ValueError("vectors detector.direction_x and detector.direction_y must be orthogonal") - - self._direction_y = y - self._direction_x = x - - -class SystemConfiguration(object): - r'''This is a generic class to hold the description of a tomography system - ''' - - SYSTEM_SIMPLE = 'simple' - SYSTEM_OFFSET = 'offset' - SYSTEM_ADVANCED = 'advanced' - - @property - def dimension(self): - if self._dimension == 2: - return '2D' - else: - return '3D' - - @dimension.setter - def dimension(self,val): - if val != 2 and val != 3: - raise ValueError('Can set up 2D and 3D systems only. got {0}D'.format(val)) - else: - self._dimension = val - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self,val): - if val != acquisition_labels["CONE"] and val != acquisition_labels["PARALLEL"]: - raise ValueError('geom_type = {} not recognised please specify \'cone\' or \'parallel\''.format(val)) - else: - self._geometry = val - - def __init__(self, dof, geometry, units='units'): - """Initialises the system component attributes for the acquisition type - """ - self.dimension = dof - self.geometry = geometry - self.units = units - - if geometry == acquisition_labels["PARALLEL"]: - self.ray = DirectionVector(dof) - else: - self.source = PositionVector(dof) - - if dof == 2: - self.detector = Detector1D(dof) - self.rotation_axis = PositionVector(dof) - else: - self.detector = Detector2D(dof) - self.rotation_axis = PositionDirectionVector(dof) - - def __str__(self): - """Implements the string representation of the system configuration - """ - raise NotImplementedError - - def __eq__(self, other): - """Implements the equality check of the system configuration - """ - raise NotImplementedError - - @staticmethod - def rotation_vec_to_y(vec): - ''' returns a rotation matrix that will rotate the projection of vec on the x-y plane to the +y direction [0,1, Z]''' - - vec = ComponentDescription.create_unit_vector(vec) - - axis_rotation = numpy.eye(len(vec)) - - if numpy.allclose(vec[:2],[0,1]): - pass - elif numpy.allclose(vec[:2],[0,-1]): - axis_rotation[0][0] = -1 - axis_rotation[1][1] = -1 - else: - theta = math.atan2(vec[0],vec[1]) - axis_rotation[0][0] = axis_rotation[1][1] = math.cos(theta) - axis_rotation[0][1] = -math.sin(theta) - axis_rotation[1][0] = math.sin(theta) - - return axis_rotation - - @staticmethod - def rotation_vec_to_z(vec): - ''' returns a rotation matrix that will align vec with the z-direction [0,0,1]''' - - vec = ComponentDescription.create_unit_vector(vec) - - if len(vec) == 2: - return numpy.array([[1, 0],[0, 1]]) - - elif len(vec) == 3: - axis_rotation = numpy.eye(3) - - if numpy.allclose(vec,[0,0,1]): - pass - elif numpy.allclose(vec,[0,0,-1]): - axis_rotation = numpy.eye(3) - axis_rotation[1][1] = -1 - axis_rotation[2][2] = -1 - else: - vx = numpy.array([[0, 0, -vec[0]], [0, 0, -vec[1]], [vec[0], vec[1], 0]]) - axis_rotation = numpy.eye(3) + vx + vx.dot(vx) * 1 / (1 + vec[2]) - - else: - raise ValueError("Vec must have length 3, got {}".format(len(vec))) - - return axis_rotation - - def update_reference_frame(self): - r'''Transforms the system origin to the rotation_axis position - ''' - self.set_origin(self.rotation_axis.position) - - - def set_origin(self, origin): - r'''Transforms the system origin to the input origin - ''' - translation = origin.copy() - if hasattr(self,'source'): - self.source.position -= translation - - self.detector.position -= translation - self.rotation_axis.position -= translation - - - def get_centre_slice(self): - """Returns the 2D system configuration corresponding to the centre slice - """ - raise NotImplementedError - - def calculate_magnification(self): - r'''Calculates the magnification of the system using the source to rotate axis, - and source to detector distance along the direction. - - :return: returns [dist_source_center, dist_center_detector, magnification], [0] distance from the source to the rotate axis, [1] distance from the rotate axis to the detector, [2] magnification of the system - :rtype: list - ''' - raise NotImplementedError - - def system_description(self): - r'''Returns `simple` if the the geometry matches the default definitions with no offsets or rotations, - \nReturns `offset` if the the geometry matches the default definitions with centre-of-rotation or detector offsets - \nReturns `advanced` if the the geometry has rotated or tilted rotation axis or detector, can also have offsets - ''' - raise NotImplementedError - - def copy(self): - '''returns a copy of SystemConfiguration''' - return copy.deepcopy(self) diff --git a/Wrappers/Python/cil/framework/vector_geometry.py b/Wrappers/Python/cil/framework/vector_geometry.py index 0abec8fccc..e84a15bcae 100644 --- a/Wrappers/Python/cil/framework/vector_geometry.py +++ b/Wrappers/Python/cil/framework/vector_geometry.py @@ -21,19 +21,19 @@ import numpy -from .label import geometry_labels +from .label import FillTypes class VectorGeometry: '''Geometry describing VectorData to contain 1D array''' @property def RANDOM(self): - warnings.warn("use geometry_labels['RANDOM'] instead", DeprecationWarning, stacklevel=2) - return geometry_labels['RANDOM'] + warnings.warn("use FillTypes.RANDOM instead", DeprecationWarning, stacklevel=2) + return FillTypes.RANDOM @property def RANDOM_INT(self): - warnings.warn("use geometry_labels['RANDOM_INT'] instead", DeprecationWarning, stacklevel=2) - return geometry_labels['RANDOM_INT'] + warnings.warn("use FillTypes.RANDOM_INT instead", DeprecationWarning, stacklevel=2) + return FillTypes.RANDOM_INT @property def dtype(self): @@ -97,8 +97,11 @@ def allocate(self, value=0, **kwargs): if isinstance(value, Number): if value != 0: out += value - else: - if value == geometry_labels["RANDOM"]: + elif value is not None: + FillTypes.validate(value) + value = FillTypes.get_enum_member(value) + + if value == FillTypes.RANDOM: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) @@ -106,7 +109,7 @@ def allocate(self, value=0, **kwargs): out.fill(numpy.random.random_sample(self.shape) + 1.j*numpy.random.random_sample(self.shape)) else: out.fill(numpy.random.random_sample(self.shape)) - elif value == geometry_labels["RANDOM_INT"]: + elif value == FillTypes.RANDOM_INT: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) diff --git a/Wrappers/Python/cil/io/ZEISSDataReader.py b/Wrappers/Python/cil/io/ZEISSDataReader.py index cfac5aeb72..8f01950337 100644 --- a/Wrappers/Python/cil/io/ZEISSDataReader.py +++ b/Wrappers/Python/cil/io/ZEISSDataReader.py @@ -18,7 +18,7 @@ # Andrew Shartis (UES, Inc.) -from cil.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry, acquisition_labels, data_order +from cil.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry, UnitsAngles, DimensionLabelsAcquisition, DimensionLabelsImage import numpy as np import os import olefile @@ -127,10 +127,10 @@ def set_up(self, if roi is not None: if metadata['data geometry'] == 'acquisition': - allowed_labels = data_order["CIL_AG_LABELS"] + allowed_labels = [item.value for item in DimensionLabelsAcquisition] zeiss_data_order = {'angle':0, 'vertical':1, 'horizontal':2} else: - allowed_labels = data_order["CIL_IG_LABELS"] + allowed_labels = [item.value for item in DimensionLabelsImage] zeiss_data_order = {'vertical':0, 'horizontal_y':1, 'horizontal_x':2} # check roi labels and create tuple for slicing @@ -232,11 +232,11 @@ def _setup_acq_geometry(self): ) \ .set_panel([self._metadata['image_width'], self._metadata['image_height']],\ pixel_size=[self._metadata['detector_pixel_size']/1000,self._metadata['detector_pixel_size']/1000])\ - .set_angles(self._metadata['thetas'],angle_unit=acquisition_labels["RADIAN"]) + .set_angles(self._metadata['thetas'],angle_unit=UnitsAngles.RADIAN) else: self._geometry = AcquisitionGeometry.create_Parallel3D()\ .set_panel([self._metadata['image_width'], self._metadata['image_height']])\ - .set_angles(self._metadata['thetas'],angle_unit=acquisition_labels["RADIAN"]) + .set_angles(self._metadata['thetas'],angle_unit=UnitsAngles.RADIAN) self._geometry.dimension_labels = ['angle', 'vertical', 'horizontal'] def _setup_image_geometry(self): diff --git a/Wrappers/Python/cil/plugins/TomoPhantom.py b/Wrappers/Python/cil/plugins/TomoPhantom.py index 55c3315b53..666c587754 100644 --- a/Wrappers/Python/cil/plugins/TomoPhantom.py +++ b/Wrappers/Python/cil/plugins/TomoPhantom.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import ImageData, image_labels, data_order +from cil.framework import ImageData, DimensionLabelsImage import tomophantom from tomophantom import TomoP2D, TomoP3D import os @@ -138,7 +138,7 @@ def get_ImageData(num_model, geometry): ag.set_panel((N,N-2)) ag.set_channels(channels) - ag.set_angles(angles, angle_unit=acquisition_labels["DEGREE"]) + ag.set_angles(angles, angle_unit=UnitsAngles.DEGREE) ig = ag.get_ImageGeometry() num_model = 1 @@ -147,10 +147,10 @@ def get_ImageData(num_model, geometry): ''' ig = geometry.copy() - ig.set_labels(data_order["TOMOPHANTOM_IG_LABELS"]) + ig.set_labels(DimensionLabelsImage.get_default_order_for_engine('cil')) num_dims = len(ig.dimension_labels) - if image_labels["CHANNEL"] in ig.dimension_labels: + if DimensionLabelsImage.CHANNEL.value in ig.dimension_labels: if not is_model_temporal(num_model): raise ValueError('Selected model {} is not a temporal model, please change your selection'.format(num_model)) if num_dims == 4: diff --git a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py index 55aff4f437..e76838abfb 100644 --- a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py @@ -18,7 +18,7 @@ import logging -from cil.framework import check_order_for_engine, BlockGeometry +from cil.framework import BlockGeometry, DimensionLabelsAcquisition, DimensionLabelsImage from cil.optimisation.operators import BlockOperator, LinearOperator, ChannelwiseOperator from cil.plugins.astra.operators import AstraProjector2D, AstraProjector3D @@ -115,8 +115,8 @@ def __init__(self, self).__init__(domain_geometry=image_geometry, range_geometry=acquisition_geometry) - check_order_for_engine('astra', image_geometry) - check_order_for_engine('astra', acquisition_geometry) + DimensionLabelsAcquisition.check_order_for_engine('astra',acquisition_geometry) + DimensionLabelsImage.check_order_for_engine('astra',image_geometry) self.volume_geometry = image_geometry self.sinogram_geometry = acquisition_geometry diff --git a/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py b/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py index 93090348d8..cec439de1a 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py +++ b/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import DataProcessor, ImageData, check_order_for_engine +from cil.framework import DataProcessor, ImageData, DimensionLabelsAcquisition, DimensionLabelsImage from cil.plugins.astra.utilities import convert_geometry_to_astra_vec_3D import astra from astra import astra_dict, algorithm, data3d @@ -66,7 +66,7 @@ def check_input(self, dataset): def set_ImageGeometry(self, volume_geometry): - check_order_for_engine('astra', volume_geometry) + DimensionLabelsImage.check_order_for_engine('astra', volume_geometry) if len(volume_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(volume_geometry.number_of_dimensions)) @@ -75,7 +75,7 @@ def set_ImageGeometry(self, volume_geometry): def set_AcquisitionGeometry(self, sinogram_geometry): - check_order_for_engine('astra', sinogram_geometry) + DimensionLabelsAcquisition.check_order_for_engine('astra', sinogram_geometry) if len(sinogram_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(sinogram_geometry.number_of_dimensions)) diff --git a/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py b/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py index 0e3becd9a7..18473bb566 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py +++ b/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import DataProcessor, AcquisitionData, check_order_for_engine +from cil.framework import DataProcessor, AcquisitionData, DimensionLabelsImage, DimensionLabelsAcquisition from cil.plugins.astra.utilities import convert_geometry_to_astra_vec_3D import astra from astra import astra_dict, algorithm, data3d @@ -64,7 +64,7 @@ def check_input(self, dataset): def set_ImageGeometry(self, volume_geometry): - check_order_for_engine('astra', volume_geometry) + DimensionLabelsImage.check_order_for_engine('astra', volume_geometry) if len(volume_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(volume_geometry.number_of_dimensions)) @@ -73,7 +73,7 @@ def set_ImageGeometry(self, volume_geometry): def set_AcquisitionGeometry(self, sinogram_geometry): - check_order_for_engine('astra', sinogram_geometry) + DimensionLabelsAcquisition.check_order_for_engine('astra', sinogram_geometry) if len(sinogram_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(sinogram_geometry.number_of_dimensions)) diff --git a/Wrappers/Python/cil/plugins/astra/processors/FBP.py b/Wrappers/Python/cil/plugins/astra/processors/FBP.py index 70aa8baebb..d0f0f781b0 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/FBP.py +++ b/Wrappers/Python/cil/plugins/astra/processors/FBP.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt import warnings -from cil.framework import DataProcessor, check_order_for_engine +from cil.framework import DataProcessor, DimensionLabelsImage, DimensionLabelsAcquisition from cil.plugins.astra.processors.FBP_Flexible import FBP_Flexible from cil.plugins.astra.processors.FDK_Flexible import FDK_Flexible from cil.plugins.astra.processors.FBP_Flexible import FBP_CPU @@ -66,8 +66,8 @@ def __init__(self, image_geometry=None, acquisition_geometry=None, device='gpu') if image_geometry is None: image_geometry = acquisition_geometry.get_ImageGeometry() - check_order_for_engine('astra', image_geometry) - check_order_for_engine('astra', acquisition_geometry) + DimensionLabelsAcquisition.check_order_for_engine('astra', acquisition_geometry) + DimensionLabelsImage.check_order_for_engine('astra', image_geometry) if device == 'gpu': if acquisition_geometry.geom_type == 'parallel': diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py index 1f4f2ffb7b..de69d780bb 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py @@ -19,7 +19,7 @@ import astra import numpy as np -from cil.framework import acquisition_labels +from cil.framework import UnitsAngles def convert_geometry_to_astra(volume_geometry, sinogram_geometry): """ @@ -49,7 +49,7 @@ def convert_geometry_to_astra(volume_geometry, sinogram_geometry): #get units - if sinogram_geometry.config.angles.angle_unit == acquisition_labels["DEGREE"]: + if UnitsAngles.get_enum_member(sinogram_geometry.config.angles.angle_unit) == UnitsAngles.DEGREE: angles_rad = sinogram_geometry.config.angles.angle_data * np.pi / 180.0 else: angles_rad = sinogram_geometry.config.angles.angle_data diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py index bc7b92d7a3..ce6738ed19 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py @@ -19,7 +19,7 @@ import astra import numpy as np -from cil.framework import acquisition_labels +from cil.framework import UnitsAngles def convert_geometry_to_astra_vec_2D(volume_geometry, sinogram_geometry_in): @@ -53,7 +53,7 @@ def convert_geometry_to_astra_vec_2D(volume_geometry, sinogram_geometry_in): panel = sinogram_geometry.config.panel #get units - degrees = angles.angle_unit == acquisition_labels["DEGREE"] + degrees = angles.angle_unit == UnitsAngles.DEGREE.value #create a 2D astra geom from 2D CIL geometry, 2D astra geometry has axis flipped compared to 3D volume_geometry_temp = volume_geometry.copy() diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py index 8279954cc4..ac33c127af 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py @@ -19,7 +19,7 @@ import astra import numpy as np -from cil.framework import acquisition_labels +from cil.framework import UnitsAngles def convert_geometry_to_astra_vec_3D(volume_geometry, sinogram_geometry_in): @@ -55,7 +55,7 @@ def convert_geometry_to_astra_vec_3D(volume_geometry, sinogram_geometry_in): panel = sinogram_geometry.config.panel #get units - degrees = angles.angle_unit == acquisition_labels["DEGREE"] + degrees = angles.angle_unit == UnitsAngles.DEGREE.value if sinogram_geometry.dimension == '2D': #create a 3D astra geom from 2D CIL geometry diff --git a/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py b/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py index 98bddc3bda..258aac5562 100644 --- a/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py +++ b/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py @@ -23,7 +23,7 @@ raise ImportError('Please `conda install "ccpi::ccpi-regulariser>=24.0.1"`') from exc -from cil.framework import check_order_for_engine, DataContainer +from cil.framework import DataContainer, DimensionLabelsImage from cil.optimisation.functions import Function import numpy as np import warnings @@ -494,7 +494,7 @@ def __rmul__(self, scalar): def check_input(self, input): '''TNV requires 2D+channel data with the first dimension as the channel dimension''' if isinstance(input, DataContainer): - check_order_for_engine('cil', input.geometry) + DimensionLabelsImage.check_order_for_engine('cil', input.geometry) if ( input.geometry.channels == 1 ) or ( not input.geometry.ndim == 3) : raise ValueError('TNV requires 2D+channel data. Got {}'.format(input.geometry.dimension_labels)) else: diff --git a/Wrappers/Python/cil/plugins/tigre/FBP.py b/Wrappers/Python/cil/plugins/tigre/FBP.py index 9738f8dcab..9cfb2daac4 100644 --- a/Wrappers/Python/cil/plugins/tigre/FBP.py +++ b/Wrappers/Python/cil/plugins/tigre/FBP.py @@ -22,7 +22,7 @@ import numpy as np -from cil.framework import DataProcessor, ImageData, check_order_for_engine +from cil.framework import DataProcessor, ImageData, DimensionLabelsAcquisition, DimensionLabelsImage from cil.plugins.tigre import CIL2TIGREGeometry try: @@ -64,8 +64,10 @@ def __init__(self, image_geometry=None, acquisition_geometry=None, **kwargs): if device != 'gpu': raise ValueError("TIGRE FBP is GPU only. Got device = {}".format(device)) - check_order_for_engine('tigre', image_geometry) - check_order_for_engine('tigre', acquisition_geometry) + + DimensionLabelsAcquisition.check_order_for_engine('tigre', acquisition_geometry) + DimensionLabelsImage.check_order_for_engine('tigre', image_geometry) + tigre_geom, tigre_angles = CIL2TIGREGeometry.getTIGREGeometry(image_geometry,acquisition_geometry) @@ -79,7 +81,7 @@ def check_input(self, dataset): raise ValueError("Expected input data to be single channel, got {0}"\ .format(self.acquisition_geometry.channels)) - check_order_for_engine('tigre', dataset.geometry) + DimensionLabelsAcquisition.check_order_for_engine('tigre', dataset.geometry) return True def process(self, out=None): diff --git a/Wrappers/Python/cil/plugins/tigre/Geometry.py b/Wrappers/Python/cil/plugins/tigre/Geometry.py index 509e4159f4..5c6f9140ce 100644 --- a/Wrappers/Python/cil/plugins/tigre/Geometry.py +++ b/Wrappers/Python/cil/plugins/tigre/Geometry.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import acquisition_labels +from cil.framework import UnitsAngles import numpy as np try: @@ -33,7 +33,7 @@ def getTIGREGeometry(ig, ag): #angles angles = ag.config.angles.angle_data + ag.config.angles.initial_angle - if ag.config.angles.angle_unit == acquisition_labels["DEGREE"]: + if UnitsAngles.get_enum_member(ag.config.angles.angle_unit) == UnitsAngles.DEGREE: angles *= (np.pi/180.) #convert CIL to TIGRE angles s diff --git a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py index c6c3606f2a..1b0c6ba26e 100644 --- a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import (ImageData, AcquisitionData, AcquisitionGeometry, check_order_for_engine, acquisition_labels, +from cil.framework import (ImageData, AcquisitionData, DimensionLabelsAcquisition, DimensionLabelsImage, BlockGeometry) from cil.optimisation.operators import BlockOperator, LinearOperator from cil.plugins.tigre import CIL2TIGREGeometry @@ -145,8 +145,8 @@ def __init__(self, "TIGRE projectors are GPU only. Got device = {}".format( device)) - check_order_for_engine('tigre', image_geometry) - check_order_for_engine('tigre', acquisition_geometry) + DimensionLabelsImage.check_order_for_engine('tigre', image_geometry) + DimensionLabelsAcquisition.check_order_for_engine('tigre', acquisition_geometry) super(ProjectionOperator,self).__init__(domain_geometry=image_geometry,\ range_geometry=acquisition_geometry) @@ -221,7 +221,7 @@ def adjoint(self, x, out=None): data = x.as_array() #if single angle projection add the dimension in for TIGRE - if x.dimension_labels[0] != acquisition_labels["ANGLE"]: + if x.dimension_labels[0] != DimensionLabelsAcquisition.ANGLE.value: data = np.expand_dims(data, axis=0) if self.tigre_geom.is2D: diff --git a/Wrappers/Python/cil/processors/CofR_image_sharpness.py b/Wrappers/Python/cil/processors/CofR_image_sharpness.py index 78837b32d2..5e7e8e3dc9 100644 --- a/Wrappers/Python/cil/processors/CofR_image_sharpness.py +++ b/Wrappers/Python/cil/processors/CofR_image_sharpness.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import Processor, AcquisitionData, check_order_for_engine +from cil.framework import Processor, AcquisitionData, DimensionLabelsAcquisition import matplotlib.pyplot as plt import scipy import numpy as np @@ -128,7 +128,7 @@ def check_input(self, data): else: test_geom = data.geometry - if not check_order_for_engine(self.backend, test_geom): + if not DimensionLabelsAcquisition.check_order_for_engine(self.backend, test_geom): raise ValueError("Input data must be reordered for use with selected backend. Use input.reorder{'{0}')".format(self.backend)) return True diff --git a/Wrappers/Python/cil/processors/PaganinProcessor.py b/Wrappers/Python/cil/processors/PaganinProcessor.py index acea978413..78f6e7e001 100644 --- a/Wrappers/Python/cil/processors/PaganinProcessor.py +++ b/Wrappers/Python/cil/processors/PaganinProcessor.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: # https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import Processor, AcquisitionData, get_order_for_engine +from cil.framework import Processor, AcquisitionData, DimensionLabelsAcquisition import numpy as np from scipy.fft import fft2 @@ -206,7 +206,7 @@ def check_input(self, data): def process(self, out=None): data = self.get_input() - cil_order = tuple(get_order_for_engine('cil', data.geometry)) + cil_order = tuple(DimensionLabelsAcquisition.get_order_for_engine('cil',data.geometry)) if data.dimension_labels != cil_order: log.warning(msg="This processor will work most efficiently using\ \nCIL data order, consider using `data.reorder('cil')`") diff --git a/Wrappers/Python/cil/recon/FBP.py b/Wrappers/Python/cil/recon/FBP.py index fe2ff890e8..d5b24edea9 100644 --- a/Wrappers/Python/cil/recon/FBP.py +++ b/Wrappers/Python/cil/recon/FBP.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import cilacc -from cil.framework import acquisition_labels +from cil.framework import AcquisitionType from cil.recon import Reconstructor from scipy.fft import fftfreq @@ -376,7 +376,7 @@ def __init__ (self, input, image_geometry=None, filter='ram-lak'): #call parent initialiser super().__init__(input, image_geometry, filter, backend='tigre') - if input.geometry.geom_type != acquisition_labels["CONE"]: + if AcquisitionType.get_enum_member(input.geometry.geom_type) != AcquisitionType.CONE: raise TypeError("This reconstructor is for cone-beam data only.") @@ -485,7 +485,7 @@ def __init__ (self, input, image_geometry=None, filter='ram-lak', backend='tigre super().__init__(input, image_geometry, filter, backend) self.set_split_processing(False) - if input.geometry.geom_type != acquisition_labels["PARALLEL"]: + if AcquisitionType.get_enum_member(input.geometry.geom_type) != AcquisitionType.PARALLEL: raise TypeError("This reconstructor is for parallel-beam data only.") diff --git a/Wrappers/Python/cil/recon/Reconstructor.py b/Wrappers/Python/cil/recon/Reconstructor.py index f02d8e4397..919a1032ca 100644 --- a/Wrappers/Python/cil/recon/Reconstructor.py +++ b/Wrappers/Python/cil/recon/Reconstructor.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import AcquisitionData, ImageGeometry, check_order_for_engine +from cil.framework import AcquisitionData, ImageGeometry, DimensionLabelsAcquisition import importlib import weakref @@ -105,7 +105,7 @@ def _configure_for_backend(self, backend='tigre'): if backend not in self.supported_backends: raise ValueError("Backend unsupported. Supported backends: {}".format(self.supported_backends)) - if not check_order_for_engine(backend, self.acquisition_geometry): + if not DimensionLabelsAcquisition.check_order_for_engine(backend, self.acquisition_geometry): raise ValueError("Input data must be reordered for use with selected backend. Use input.reorder{'{0}')".format(backend)) #set ProjectionOperator class from backend diff --git a/Wrappers/Python/cil/utilities/dataexample.py b/Wrappers/Python/cil/utilities/dataexample.py index 86192f469d..b21c000c20 100644 --- a/Wrappers/Python/cil/utilities/dataexample.py +++ b/Wrappers/Python/cil/utilities/dataexample.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import ImageData, ImageGeometry, image_labels +from cil.framework import ImageData, ImageGeometry, DimensionLabelsImage import numpy import numpy as np from PIL import Image @@ -354,7 +354,7 @@ def load(self, which, size=None, scale=(0,1), **kwargs): sdata = numpy.zeros((N, M)) sdata[int(round(N/4)):int(round(3*N/4)), int(round(M/4)):int(round(3*M/4))] = 0.5 sdata[int(round(N/8)):int(round(7*N/8)), int(round(3*M/8)):int(round(5*M/8))] = 1 - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y.value, DimensionLabelsImage.HORIZONTAL_X.value]) data = ig.allocate() data.fill(sdata) @@ -369,7 +369,7 @@ def load(self, which, size=None, scale=(0,1), **kwargs): N = size[0] M = size[1] - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y.value, DimensionLabelsImage.HORIZONTAL_X.value]) data = ig.allocate() tmp = numpy.array(f.convert('L').resize((M,N))) data.fill(tmp/numpy.max(tmp)) @@ -391,13 +391,13 @@ def load(self, which, size=None, scale=(0,1), **kwargs): bands = tmp.getbands() ig = ImageGeometry(voxel_num_x=M, voxel_num_y=N, channels=len(bands), - dimension_labels=[image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"],image_labels["CHANNEL"]]) + dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y.value, DimensionLabelsImage.HORIZONTAL_X.value,DimensionLabelsImage.CHANNEL.value]) data = ig.allocate() data.fill(numpy.array(tmp.resize((M,N)))) - data.reorder([image_labels["CHANNEL"],image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]]) + data.reorder([DimensionLabelsImage.CHANNEL.value,DimensionLabelsImage.HORIZONTAL_Y.value, DimensionLabelsImage.HORIZONTAL_X.value]) data.geometry.channel_labels = bands else: - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y.value, DimensionLabelsImage.HORIZONTAL_X.value]) data = ig.allocate() data.fill(numpy.array(tmp.resize((M,N)))) diff --git a/Wrappers/Python/test/test_BlockOperator.py b/Wrappers/Python/test/test_BlockOperator.py index 6c5aa41474..58e0a65d99 100644 --- a/Wrappers/Python/test/test_BlockOperator.py +++ b/Wrappers/Python/test/test_BlockOperator.py @@ -20,7 +20,7 @@ from utils import initialise_tests import logging from cil.optimisation.operators import BlockOperator, GradientOperator -from cil.framework import BlockDataContainer, BlockGeometry, ImageGeometry, image_labels +from cil.framework import BlockDataContainer, BlockGeometry, ImageGeometry, FillTypes from cil.optimisation.operators import IdentityOperator import numpy from cil.optimisation.operators import FiniteDifferenceOperator @@ -58,7 +58,7 @@ def test_BlockOperator(self): self.assertBlockDataContainerEqual(z1, res) - z1 = B.range_geometry().allocate(image_labels["RANDOM"]) + z1 = B.range_geometry().allocate(FillTypes["RANDOM"]) res1 = B.adjoint(z1) res2 = B.domain_geometry().allocate() @@ -159,7 +159,7 @@ def test_BlockOperator(self): ) B1 = BlockOperator(G, Id) - U = ig.allocate(image_labels["RANDOM"]) + U = ig.allocate(FillTypes["RANDOM"]) #U = BlockDataContainer(u,u) RES1 = B1.range_geometry().allocate() @@ -284,7 +284,7 @@ def test_BlockOperatorLinearValidity(self): B = BlockOperator(G, Id) # Nx1 case u = ig.allocate('random', seed=2) - w = B.range_geometry().allocate(image_labels["RANDOM"], seed=3) + w = B.range_geometry().allocate(FillTypes["RANDOM"], seed=3) w1 = B.direct(u) u1 = B.adjoint(w) self.assertAlmostEqual((w * w1).sum() , (u1*u).sum(), places=5) diff --git a/Wrappers/Python/test/test_DataContainer.py b/Wrappers/Python/test/test_DataContainer.py index 2b88369f0f..c7c27804f2 100644 --- a/Wrappers/Python/test/test_DataContainer.py +++ b/Wrappers/Python/test/test_DataContainer.py @@ -19,7 +19,7 @@ import sys import numpy from cil.framework import (DataContainer, ImageGeometry, ImageData, VectorGeometry, AcquisitionData, - AcquisitionGeometry, BlockGeometry, VectorData, acquisition_labels, image_labels) + AcquisitionGeometry, BlockGeometry, VectorData, DimensionLabelsImage, DimensionLabelsAcquisition) from timeit import default_timer as timer import logging from testclass import CCPiTestClass @@ -455,8 +455,8 @@ def test_ImageData(self): self.assertEqual(vol.number_of_dimensions, 3) ig2 = ImageGeometry (voxel_num_x=2,voxel_num_y=3,voxel_num_z=4, - dimension_labels=[image_labels["HORIZONTAL_X"], image_labels["HORIZONTAL_Y"], - image_labels["VERTICAL"]]) + dimension_labels=[DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["HORIZONTAL_Y"], + DimensionLabelsImage["VERTICAL"]]) data = ig2.allocate() self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ig2.shape)) self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape) @@ -501,8 +501,8 @@ def test_AcquisitionData(self): self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape) ag2 = AcquisitionGeometry.create_Parallel3D().set_angles(numpy.linspace(0, 180, num=10)).set_panel((2,3)).set_channels(4)\ - .set_labels([acquisition_labels["VERTICAL"] , - acquisition_labels["ANGLE"], acquisition_labels["HORIZONTAL"], acquisition_labels["CHANNEL"]]) + .set_labels([DimensionLabelsAcquisition["VERTICAL"] , + DimensionLabelsAcquisition["ANGLE"], DimensionLabelsAcquisition["HORIZONTAL"], DimensionLabelsAcquisition["CHANNEL"]]) data = ag2.allocate() self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ag2.shape)) @@ -739,27 +739,27 @@ def test_AcquisitionDataSubset(self): # expected dimension_labels - self.assertListEqual([acquisition_labels["CHANNEL"] , - acquisition_labels["ANGLE"] , acquisition_labels["VERTICAL"] , - acquisition_labels["HORIZONTAL"]], + self.assertListEqual([DimensionLabelsAcquisition["CHANNEL"] , + DimensionLabelsAcquisition["ANGLE"] , DimensionLabelsAcquisition["VERTICAL"] , + DimensionLabelsAcquisition["HORIZONTAL"]], list(sgeometry.dimension_labels)) sino = sgeometry.allocate() # test reshape - new_order = [acquisition_labels["HORIZONTAL"] , - acquisition_labels["CHANNEL"] , acquisition_labels["VERTICAL"] , - acquisition_labels["ANGLE"]] + new_order = [DimensionLabelsAcquisition["HORIZONTAL"] , + DimensionLabelsAcquisition["CHANNEL"] , DimensionLabelsAcquisition["VERTICAL"] , + DimensionLabelsAcquisition["ANGLE"]] sino.reorder(new_order) self.assertListEqual(new_order, list(sino.geometry.dimension_labels)) ss1 = sino.get_slice(vertical = 0) - self.assertListEqual([acquisition_labels["HORIZONTAL"] , - acquisition_labels["CHANNEL"] , - acquisition_labels["ANGLE"]], list(ss1.geometry.dimension_labels)) + self.assertListEqual([DimensionLabelsAcquisition["HORIZONTAL"] , + DimensionLabelsAcquisition["CHANNEL"] , + DimensionLabelsAcquisition["ANGLE"]], list(ss1.geometry.dimension_labels)) ss2 = sino.get_slice(vertical = 0, channel=0) - self.assertListEqual([acquisition_labels["HORIZONTAL"] , - acquisition_labels["ANGLE"]], list(ss2.geometry.dimension_labels)) + self.assertListEqual([DimensionLabelsAcquisition["HORIZONTAL"] , + DimensionLabelsAcquisition["ANGLE"]], list(ss2.geometry.dimension_labels)) def test_ImageDataSubset(self): @@ -783,42 +783,42 @@ def test_ImageDataSubset(self): self.assertListEqual(['channel', 'horizontal_y'], list(ss1.geometry.dimension_labels)) vg = ImageGeometry(3,4,5,channels=2) - self.assertListEqual([image_labels["CHANNEL"], image_labels["VERTICAL"], - image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]], + self.assertListEqual([DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["VERTICAL"], + DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["HORIZONTAL_X"]], list(vg.dimension_labels)) ss2 = vg.allocate() ss3 = vol.get_slice(channel=0) - self.assertListEqual([image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]], list(ss3.geometry.dimension_labels)) + self.assertListEqual([DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["HORIZONTAL_X"]], list(ss3.geometry.dimension_labels)) def test_DataContainerSubset(self): dc = DataContainer(numpy.ones((2,3,4,5))) - dc.dimension_labels =[acquisition_labels["CHANNEL"] , - acquisition_labels["ANGLE"] , acquisition_labels["VERTICAL"] , - acquisition_labels["HORIZONTAL"]] + dc.dimension_labels =[DimensionLabelsAcquisition["CHANNEL"] , + DimensionLabelsAcquisition["ANGLE"] , DimensionLabelsAcquisition["VERTICAL"] , + DimensionLabelsAcquisition["HORIZONTAL"]] # test reshape - new_order = [acquisition_labels["HORIZONTAL"] , - acquisition_labels["CHANNEL"] , acquisition_labels["VERTICAL"] , - acquisition_labels["ANGLE"]] + new_order = [DimensionLabelsAcquisition["HORIZONTAL"] , + DimensionLabelsAcquisition["CHANNEL"] , DimensionLabelsAcquisition["VERTICAL"] , + DimensionLabelsAcquisition["ANGLE"]] dc.reorder(new_order) self.assertListEqual(new_order, list(dc.dimension_labels)) ss1 = dc.get_slice(vertical=0) - self.assertListEqual([acquisition_labels["HORIZONTAL"] , - acquisition_labels["CHANNEL"] , - acquisition_labels["ANGLE"]], list(ss1.dimension_labels)) + self.assertListEqual([DimensionLabelsAcquisition["HORIZONTAL"] , + DimensionLabelsAcquisition["CHANNEL"] , + DimensionLabelsAcquisition["ANGLE"]], list(ss1.dimension_labels)) ss2 = dc.get_slice(vertical=0, channel=0) - self.assertListEqual([acquisition_labels["HORIZONTAL"] , - acquisition_labels["ANGLE"]], list(ss2.dimension_labels)) + self.assertListEqual([DimensionLabelsAcquisition["HORIZONTAL"] , + DimensionLabelsAcquisition["ANGLE"]], list(ss2.dimension_labels)) # Check we can get slice still even if force parameter is passed: ss3 = dc.get_slice(vertical=0, channel=0, force=True) - self.assertListEqual([acquisition_labels["HORIZONTAL"] , - acquisition_labels["ANGLE"]], list(ss3.dimension_labels)) + self.assertListEqual([DimensionLabelsAcquisition["HORIZONTAL"] , + DimensionLabelsAcquisition["ANGLE"]], list(ss3.dimension_labels)) def test_DataContainerChaining(self): @@ -965,7 +965,7 @@ def test_multiply_out(self): numpy.testing.assert_array_equal(a, u.as_array()) - #u = ig.allocate(image_labels["RANDOM_INT"], seed=1) + #u = ig.allocate(DimensionLabelsImage["RANDOM_INT"], seed=1) l = functools.reduce(lambda x,y: x*y, (10,11,12), 1) a = numpy.zeros((l, ), dtype=numpy.float32) @@ -1274,7 +1274,7 @@ def test_fill_dimension_ImageData(self): ig = ImageGeometry(2,3,4) u = ig.allocate(0) a = numpy.ones((4,2)) - # default_labels = [image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] + # default_labels = [DimensionLabelsImage["VERTICAL"], DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["HORIZONTAL_X"]] data = u.as_array() axis_number = u.get_dimension_axis('horizontal_y') @@ -1302,7 +1302,7 @@ def test_fill_dimension_AcquisitionData(self): ag.set_labels(('horizontal','angle','vertical','channel')) u = ag.allocate(0) a = numpy.ones((4,2)) - # default_labels = [image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] + # default_labels = [DimensionLabelsImage["VERTICAL"], DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["HORIZONTAL_X"]] data = u.as_array() axis_number = u.get_dimension_axis('horizontal_y') @@ -1336,7 +1336,7 @@ def test_fill_dimension_AcquisitionData(self): u = ag.allocate(0) # (2, 5, 3, 4) a = numpy.ones((2,5)) - # default_labels = [image_labels["VERTICAL"], image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] + # default_labels = [DimensionLabelsImage["VERTICAL"], DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["HORIZONTAL_X"]] b = u.get_slice(channel=0, vertical=0) data = u.as_array() diff --git a/Wrappers/Python/test/test_Operator.py b/Wrappers/Python/test/test_Operator.py index 97c65a33a2..b71c8395ed 100644 --- a/Wrappers/Python/test/test_Operator.py +++ b/Wrappers/Python/test/test_Operator.py @@ -19,7 +19,7 @@ import unittest from unittest.mock import Mock from utils import initialise_tests -from cil.framework import ImageGeometry, BlockGeometry, VectorGeometry, BlockDataContainer, DataContainer, image_labels +from cil.framework import ImageGeometry, BlockGeometry, VectorGeometry, BlockDataContainer, DataContainer, FillTypes from cil.optimisation.operators import BlockOperator,\ FiniteDifferenceOperator, SymmetrisedGradientOperator import numpy @@ -249,13 +249,13 @@ def test_FiniteDifference(self): FD = FiniteDifferenceOperator(ig, direction = 0, bnd_cond = 'Neumann') u = FD.domain_geometry().allocate('random') - res = FD.domain_geometry().allocate(image_labels["RANDOM"]) + res = FD.domain_geometry().allocate(FillTypes["RANDOM"]) FD.adjoint(u, out=res) w = FD.adjoint(u) self.assertNumpyArrayEqual(res.as_array(), w.as_array()) - res = Id.domain_geometry().allocate(image_labels["RANDOM"]) + res = Id.domain_geometry().allocate(FillTypes["RANDOM"]) Id.adjoint(u, out=res) w = Id.adjoint(u) @@ -264,14 +264,14 @@ def test_FiniteDifference(self): G = GradientOperator(ig) - u = G.range_geometry().allocate(image_labels["RANDOM"]) + u = G.range_geometry().allocate(FillTypes["RANDOM"]) 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(image_labels["RANDOM"]) + u = G.domain_geometry().allocate(FillTypes["RANDOM"]) res = G.range_geometry().allocate() G.direct(u, out=res) w = G.direct(u) diff --git a/Wrappers/Python/test/test_PluginsTomoPhantom.py b/Wrappers/Python/test/test_PluginsTomoPhantom.py index 82528352f8..ac5ef8e7ca 100644 --- a/Wrappers/Python/test/test_PluginsTomoPhantom.py +++ b/Wrappers/Python/test/test_PluginsTomoPhantom.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt import unittest -from cil.framework import AcquisitionGeometry, acquisition_labels +from cil.framework import AcquisitionGeometry, UnitsAngles import numpy as np from utils import has_tomophantom, initialise_tests @@ -36,7 +36,7 @@ def setUp(self): ag = AcquisitionGeometry.create_Cone2D((offset,-100), (offset,100)) ag.set_panel(N) - ag.set_angles(angles, angle_unit=acquisition_labels["DEGREE"]) + ag.set_angles(angles, angle_unit=UnitsAngles["DEGREE"]) ig = ag.get_ImageGeometry() self.ag = ag self.ig = ig @@ -103,7 +103,7 @@ def setUp(self): ag = AcquisitionGeometry.create_Cone3D((offset,-100,0), (offset,100,0)) ag.set_panel((N,N/2)) - ag.set_angles(angles, angle_unit=acquisition_labels["DEGREE"]) + ag.set_angles(angles, angle_unit=UnitsAngles["DEGREE"]) ig = ag.get_ImageGeometry() self.ag = ag self.ig = ig diff --git a/Wrappers/Python/test/test_algorithms.py b/Wrappers/Python/test/test_algorithms.py index 179cdb66f5..69beca0022 100644 --- a/Wrappers/Python/test/test_algorithms.py +++ b/Wrappers/Python/test/test_algorithms.py @@ -25,7 +25,7 @@ import logging from cil.framework import (ImageGeometry, ImageData, VectorGeometry, AcquisitionData, AcquisitionGeometry, - BlockDataContainer, BlockGeometry, VectorData, image_labels) + BlockDataContainer, BlockGeometry, VectorData, FillTypes) from cil.optimisation.utilities import ArmijoStepSizeRule, ConstantStepSize from cil.optimisation.operators import IdentityOperator @@ -250,7 +250,7 @@ def test_FISTA(self): b = initial.copy() # fill with random numbers b.fill(np.random.random(initial.shape)) - initial = ig.allocate(image_labels["RANDOM"]) + initial = ig.allocate(FillTypes["RANDOM"]) identity = IdentityOperator(ig) norm2sq = OperatorCompositionFunction(L2NormSquared(b=b), identity) @@ -353,9 +353,9 @@ def test_FISTA_update(self): def test_FISTA_Norm2Sq(self): ig = ImageGeometry(127, 139, 149) - b = ig.allocate(image_labels["RANDOM"]) + b = ig.allocate(FillTypes["RANDOM"]) # fill with random numbers - initial = ig.allocate(image_labels["RANDOM"]) + initial = ig.allocate(FillTypes["RANDOM"]) identity = IdentityOperator(ig) norm2sq = LeastSquares(identity, b) @@ -382,7 +382,7 @@ def test_FISTA_catch_Lipschitz(self): b = initial.copy() # fill with random numbers b.fill(np.random.random(initial.shape)) - initial = ig.allocate(image_labels["RANDOM"]) + initial = ig.allocate(FillTypes["RANDOM"]) identity = IdentityOperator(ig) norm2sq = LeastSquares(identity, b) diff --git a/Wrappers/Python/test/test_functions.py b/Wrappers/Python/test/test_functions.py index 53a7045efe..b0fd68115c 100644 --- a/Wrappers/Python/test/test_functions.py +++ b/Wrappers/Python/test/test_functions.py @@ -21,7 +21,7 @@ from cil.optimisation.functions.Function import ScaledFunction import numpy as np -from cil.framework import VectorGeometry, VectorData, BlockDataContainer, DataContainer, image_labels, ImageGeometry, \ +from cil.framework import VectorGeometry, VectorData, BlockDataContainer, DataContainer, FillTypes, ImageGeometry, \ AcquisitionGeometry from cil.optimisation.operators import IdentityOperator, MatrixOperator, CompositionOperator, DiagonalOperator, BlockOperator from cil.optimisation.functions import Function, KullbackLeibler, ConstantFunction, TranslateFunction, soft_shrinkage @@ -79,9 +79,9 @@ def test_Function(self): operator = BlockOperator(op1, op2, shape=(2, 1)) # Create functions - noisy_data = ag.allocate(image_labels["RANDOM"], dtype=numpy.float64) + noisy_data = ag.allocate(FillTypes["RANDOM"], dtype=numpy.float64) - d = ag.allocate(image_labels["RANDOM"], dtype=numpy.float64) + d = ag.allocate(FillTypes["RANDOM"], dtype=numpy.float64) alpha = 0.5 # scaled function @@ -111,8 +111,8 @@ def test_L2NormSquared(self): numpy.random.seed(1) M, N, K = 2, 3, 5 ig = ImageGeometry(voxel_num_x=M, voxel_num_y=N, voxel_num_z=K) - u = ig.allocate(image_labels["RANDOM"]) - b = ig.allocate(image_labels["RANDOM"]) + u = ig.allocate(FillTypes["RANDOM"]) + b = ig.allocate(FillTypes["RANDOM"]) # check grad/call no data f = L2NormSquared() @@ -222,8 +222,8 @@ def test_L2NormSquaredOut(self): M, N, K = 2, 3, 5 ig = ImageGeometry(voxel_num_x=M, voxel_num_y=N, voxel_num_z=K) - u = ig.allocate(image_labels["RANDOM"], seed=1) - b = ig.allocate(image_labels["RANDOM"], seed=2) + u = ig.allocate(FillTypes["RANDOM"], seed=1) + b = ig.allocate(FillTypes["RANDOM"], seed=2) # check grad/call no data f = L2NormSquared() diff --git a/Wrappers/Python/test/test_io.py b/Wrappers/Python/test/test_io.py index 0682477405..c914afdc6f 100644 --- a/Wrappers/Python/test/test_io.py +++ b/Wrappers/Python/test/test_io.py @@ -22,7 +22,7 @@ import numpy as np import os -from cil.framework import ImageGeometry, acquisition_labels +from cil.framework import ImageGeometry, UnitsAngles from cil.io import NEXUSDataReader, NikonDataReader, ZEISSDataReader from cil.io import TIFFWriter, TIFFStackReader from cil.io.utilities import HDF5_utilities @@ -87,7 +87,7 @@ class TestZeissDataReader(unittest.TestCase): def setUp(self): if has_file: self.reader = ZEISSDataReader() - angle_unit = acquisition_labels["RADIAN"] + angle_unit = UnitsAngles["RADIAN"] self.reader.set_up(file_name=filename, angle_unit=angle_unit) diff --git a/Wrappers/Python/test/test_ring_processor.py b/Wrappers/Python/test/test_ring_processor.py index 42f7aa8bba..2a2a059af8 100644 --- a/Wrappers/Python/test/test_ring_processor.py +++ b/Wrappers/Python/test/test_ring_processor.py @@ -18,7 +18,7 @@ import unittest from cil.processors import RingRemover, TransmissionAbsorptionConverter, Slicer -from cil.framework import ImageGeometry, AcquisitionGeometry, acquisition_labels +from cil.framework import ImageGeometry, AcquisitionGeometry, UnitsAngles from cil.utilities import dataexample from cil.utilities.quality_measures import mse @@ -61,7 +61,7 @@ def test_L1Norm_2D(self): angles = np.linspace(0, 180, 120, dtype=np.float32) ag = AcquisitionGeometry.create_Parallel2D()\ - .set_angles(angles, angle_unit=acquisition_labels["DEGREE"])\ + .set_angles(angles, angle_unit=UnitsAngles["DEGREE"])\ .set_panel(detectors) sin = ag.allocate(None) sino = TomoP2D.ModelSino(model, detectors, detectors, angles, path_library2D) diff --git a/Wrappers/Python/test/test_subset.py b/Wrappers/Python/test/test_subset.py index 42feeedfe9..0f9565a295 100644 --- a/Wrappers/Python/test/test_subset.py +++ b/Wrappers/Python/test/test_subset.py @@ -19,7 +19,7 @@ import unittest from utils import initialise_tests import numpy -from cil.framework import DataContainer, ImageGeometry, AcquisitionGeometry, acquisition_labels, image_labels +from cil.framework import DataContainer, ImageGeometry, AcquisitionGeometry, DimensionLabelsImage, DimensionLabelsAcquisition from timeit import default_timer as timer initialise_tests() @@ -208,8 +208,8 @@ def setUp(self): def test_ImageDataAllocate1a(self): data = self.ig.allocate() - default_dimension_labels = [image_labels["CHANNEL"], image_labels["VERTICAL"], - image_labels["HORIZONTAL_Y"], image_labels["HORIZONTAL_X"]] + default_dimension_labels = [DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["VERTICAL"], + DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["HORIZONTAL_X"]] self.assertTrue( default_dimension_labels == list(data.dimension_labels) ) def test_ImageDataAllocate1b(self): @@ -217,64 +217,64 @@ def test_ImageDataAllocate1b(self): self.assertTrue( data.shape == (5,4,3,2)) def test_ImageDataAllocate2a(self): - non_default_dimension_labels = [ image_labels["HORIZONTAL_X"], image_labels["VERTICAL"], - image_labels["HORIZONTAL_Y"], image_labels["CHANNEL"]] + non_default_dimension_labels = [ DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["VERTICAL"], + DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["CHANNEL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() self.assertTrue( non_default_dimension_labels == list(data.dimension_labels) ) def test_ImageDataAllocate2b(self): - non_default_dimension_labels = [ image_labels["HORIZONTAL_X"], image_labels["VERTICAL"], - image_labels["HORIZONTAL_Y"], image_labels["CHANNEL"]] + non_default_dimension_labels = [ DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["VERTICAL"], + DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["CHANNEL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() self.assertTrue( data.shape == (2,4,3,5)) def test_ImageDataSubset1a(self): - non_default_dimension_labels = [image_labels["HORIZONTAL_X"], image_labels["CHANNEL"], image_labels["HORIZONTAL_Y"], - image_labels["VERTICAL"]] + non_default_dimension_labels = [DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["HORIZONTAL_Y"], + DimensionLabelsImage["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(horizontal_y = 1) self.assertTrue( sub.shape == (2,5,4)) def test_ImageDataSubset2a(self): - non_default_dimension_labels = [image_labels["HORIZONTAL_X"], image_labels["CHANNEL"], image_labels["HORIZONTAL_Y"], - image_labels["VERTICAL"]] + non_default_dimension_labels = [DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["HORIZONTAL_Y"], + DimensionLabelsImage["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(horizontal_x = 1) self.assertTrue( sub.shape == (5,3,4)) def test_ImageDataSubset3a(self): - non_default_dimension_labels = [image_labels["HORIZONTAL_X"], image_labels["CHANNEL"], image_labels["HORIZONTAL_Y"], - image_labels["VERTICAL"]] + non_default_dimension_labels = [DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["HORIZONTAL_Y"], + DimensionLabelsImage["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(channel = 1) self.assertTrue( sub.shape == (2,3,4)) def test_ImageDataSubset4a(self): - non_default_dimension_labels = [image_labels["HORIZONTAL_X"], image_labels["CHANNEL"], image_labels["HORIZONTAL_Y"], - image_labels["VERTICAL"]] + non_default_dimension_labels = [DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["HORIZONTAL_Y"], + DimensionLabelsImage["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(vertical = 1) self.assertTrue( sub.shape == (2,5,3)) def test_ImageDataSubset5a(self): - non_default_dimension_labels = [image_labels["HORIZONTAL_X"], image_labels["HORIZONTAL_Y"]] + non_default_dimension_labels = [DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["HORIZONTAL_Y"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(horizontal_y = 1) self.assertTrue( sub.shape == (2,)) def test_ImageDataSubset1b(self): - non_default_dimension_labels = [image_labels["HORIZONTAL_X"], image_labels["CHANNEL"], image_labels["HORIZONTAL_Y"], - image_labels["VERTICAL"]] + non_default_dimension_labels = [DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["HORIZONTAL_Y"], + DimensionLabelsImage["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() - new_dimension_labels = [image_labels["HORIZONTAL_Y"], image_labels["CHANNEL"], image_labels["VERTICAL"], image_labels["HORIZONTAL_X"]] + new_dimension_labels = [DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["VERTICAL"], DimensionLabelsImage["HORIZONTAL_X"]] data.reorder(new_dimension_labels) self.assertTrue( data.shape == (3,5,4,2)) @@ -286,9 +286,9 @@ def test_ImageDataSubset1c(self): def test_AcquisitionDataAllocate1a(self): data = self.ag.allocate() - default_dimension_labels = [acquisition_labels["CHANNEL"] , - acquisition_labels["ANGLE"] , acquisition_labels["VERTICAL"] , - acquisition_labels["HORIZONTAL"]] + default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"] , + DimensionLabelsAcquisition["ANGLE"] , DimensionLabelsAcquisition["VERTICAL"] , + DimensionLabelsAcquisition["HORIZONTAL"]] self.assertTrue( default_dimension_labels == list(data.dimension_labels) ) def test_AcquisitionDataAllocate1b(self): @@ -296,8 +296,8 @@ def test_AcquisitionDataAllocate1b(self): self.assertTrue( data.shape == (4,3,2,20)) def test_AcquisitionDataAllocate2a(self): - non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], - acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] + non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], + DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() @@ -305,15 +305,15 @@ def test_AcquisitionDataAllocate2a(self): self.assertTrue( non_default_dimension_labels == list(data.dimension_labels) ) def test_AcquisitionDataAllocate2b(self): - non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], - acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] + non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], + DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() self.assertTrue( data.shape == (4,20,2,3)) def test_AcquisitionDataSubset1a(self): - non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], - acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] + non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], + DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) @@ -321,24 +321,24 @@ def test_AcquisitionDataSubset1a(self): self.assertTrue( sub.shape == (4,20,3)) def test_AcquisitionDataSubset1b(self): - non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], - acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] + non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], + DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) sub = data.get_slice(channel = 0) self.assertTrue( sub.shape == (20,2,3)) def test_AcquisitionDataSubset1c(self): - non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], - acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] + non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], + DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) sub = data.get_slice(horizontal = 0, force=True) self.assertTrue( sub.shape == (4,2,3)) def test_AcquisitionDataSubset1d(self): - non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], - acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] + non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], + DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) @@ -348,8 +348,8 @@ def test_AcquisitionDataSubset1d(self): self.assertTrue( sub.shape == (4,20,2) ) self.assertTrue( sub.geometry.angles[0] == data.geometry.angles[sliceme]) def test_AcquisitionDataSubset1e(self): - non_default_dimension_labels = [acquisition_labels["CHANNEL"], acquisition_labels["HORIZONTAL"], - acquisition_labels["VERTICAL"], acquisition_labels["ANGLE"]] + non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], + DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) diff --git a/Wrappers/Python/test/utils_projectors.py b/Wrappers/Python/test/utils_projectors.py index c1deb6c534..29173dabcb 100644 --- a/Wrappers/Python/test/utils_projectors.py +++ b/Wrappers/Python/test/utils_projectors.py @@ -19,7 +19,7 @@ import numpy as np from cil.optimisation.operators import LinearOperator from cil.utilities import dataexample -from cil.framework import AcquisitionGeometry, get_order_for_engine +from cil.framework import AcquisitionGeometry, DimensionLabelsAcquisition class SimData(object): @@ -138,7 +138,7 @@ def Cone3D(self): ag_test_1 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,0,0])\ .set_panel([16,16],[1,1])\ .set_angles([0]) - ag_test_1.set_labels(get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() @@ -149,7 +149,7 @@ def Cone3D(self): ag_test_2 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,0,0])\ .set_panel([16,16],[2,2])\ .set_angles([0]) - ag_test_2.set_labels(get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() norm_2 = 8 @@ -159,7 +159,7 @@ def Cone3D(self): ag_test_3 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,0,0])\ .set_panel([16,16],[0.5,0.5])\ .set_angles([0]) - ag_test_3.set_labels(get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() norm_3 = 2 @@ -169,7 +169,7 @@ def Cone3D(self): ag_test_4 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,1000,0])\ .set_panel([16,16],[0.5,0.5])\ .set_angles([0]) - ag_test_4.set_labels(get_order_for_engine(self.backend, ag_test_4)) + ag_test_4.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_4)) ig_test_4 = ag_test_4.get_ImageGeometry() norm_4 = 1 @@ -185,7 +185,7 @@ def Cone2D(self): ag_test_1 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,0])\ .set_panel(16,1)\ .set_angles([0]) - ag_test_1.set_labels(get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() @@ -196,7 +196,7 @@ def Cone2D(self): ag_test_2 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,0])\ .set_panel(16,2)\ .set_angles([0]) - ag_test_2.set_labels(get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() norm_2 = 8 @@ -206,7 +206,7 @@ def Cone2D(self): ag_test_3 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,0])\ .set_panel(16,0.5)\ .set_angles([0]) - ag_test_3.set_labels(get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() norm_3 = 2 @@ -216,7 +216,7 @@ def Cone2D(self): ag_test_4 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,1000])\ .set_panel(16,0.5)\ .set_angles([0]) - ag_test_4.set_labels(get_order_for_engine(self.backend, ag_test_4)) + ag_test_4.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_4)) ig_test_4 = ag_test_4.get_ImageGeometry() norm_4 = 1 @@ -232,7 +232,7 @@ def Parallel3D(self): ag_test_1 = AcquisitionGeometry.create_Parallel3D()\ .set_panel([16,16],[1,1])\ .set_angles([0]) - ag_test_1.set_labels(get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() @@ -242,7 +242,7 @@ def Parallel3D(self): ag_test_2 = AcquisitionGeometry.create_Parallel3D()\ .set_panel([16,16],[2,2])\ .set_angles([0]) - ag_test_2.set_labels(get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() @@ -253,7 +253,7 @@ def Parallel3D(self): ag_test_3 = AcquisitionGeometry.create_Parallel3D()\ .set_panel([16,16],[0.5,0.5])\ .set_angles([0]) - ag_test_3.set_labels(get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() @@ -271,7 +271,7 @@ def Parallel2D(self): .set_panel(16,1)\ .set_angles([0]) - ag_test_1.set_labels(get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() norm_1 = 4 @@ -280,7 +280,7 @@ def Parallel2D(self): ag_test_2 = AcquisitionGeometry.create_Parallel2D()\ .set_panel(16,2)\ .set_angles([0]) - ag_test_2.set_labels(get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() @@ -291,7 +291,7 @@ def Parallel2D(self): ag_test_3 = AcquisitionGeometry.create_Parallel2D()\ .set_panel(16,0.5)\ .set_angles([0]) - ag_test_3.set_labels(get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() norm_3 = 2 From 0a5e43c8a4f7ae79521a8df1f5f72f423f3d5a93 Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Thu, 15 Aug 2024 09:44:17 +0000 Subject: [PATCH 39/72] geometry property returns value --- Wrappers/Python/cil/framework/acquisition_geometry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 6d1846e2b4..16d240f999 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -200,7 +200,7 @@ def dimension(self,val): @property def geometry(self): - return self._geometry + return self._geometry.value @geometry.setter def geometry(self,val): From 74595afe06adf4c91cdac384930feedc336e8dbe Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Thu, 15 Aug 2024 11:00:40 +0000 Subject: [PATCH 40/72] angle property returns value --- Wrappers/Python/cil/framework/acquisition_geometry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 16d240f999..4c63b5bc9b 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -1465,12 +1465,12 @@ def initial_angle(self, val): @property def angle_unit(self): - return self._angle_unit + return self._angle_unit.value @angle_unit.setter def angle_unit(self,val): UnitsAngles.validate(val) - self._angle_unit = UnitsAngles.get_enum_value(val) + self._angle_unit = UnitsAngles.get_enum_member(val) def __str__(self): repres = "Acquisition description:\n" From 9382f1c9dc4c7e53ccdda4a8e8b95d50853af170 Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Thu, 15 Aug 2024 11:02:34 +0000 Subject: [PATCH 41/72] Remove redundant enum checks --- Wrappers/Python/cil/framework/acquisition_geometry.py | 1 - Wrappers/Python/cil/framework/image_geometry.py | 1 - Wrappers/Python/cil/framework/vector_geometry.py | 1 - Wrappers/Python/cil/plugins/TomoPhantom.py | 2 +- .../astra/utilities/convert_geometry_to_astra.py | 2 +- Wrappers/Python/cil/plugins/tigre/Geometry.py | 2 +- .../Python/cil/plugins/tigre/ProjectionOperator.py | 2 +- Wrappers/Python/cil/recon/FBP.py | 4 ++-- Wrappers/Python/cil/utilities/dataexample.py | 10 +++++----- 9 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 4c63b5bc9b..7fcc73edec 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -2207,7 +2207,6 @@ def allocate(self, value=0, **kwargs): out.array.fill(value) elif value is not None: FillTypes.validate(value) - value = FillTypes.get_enum_member(value) if value == FillTypes.RANDOM: seed = kwargs.get('seed', None) diff --git a/Wrappers/Python/cil/framework/image_geometry.py b/Wrappers/Python/cil/framework/image_geometry.py index dc08a774a4..1ca4083112 100644 --- a/Wrappers/Python/cil/framework/image_geometry.py +++ b/Wrappers/Python/cil/framework/image_geometry.py @@ -292,7 +292,6 @@ def allocate(self, value=0, **kwargs): out.array.fill(value) elif value is not None: FillTypes.validate(value) - value = FillTypes.get_enum_member(value) if value == FillTypes.RANDOM: seed = kwargs.get('seed', None) diff --git a/Wrappers/Python/cil/framework/vector_geometry.py b/Wrappers/Python/cil/framework/vector_geometry.py index e84a15bcae..23a482dea3 100644 --- a/Wrappers/Python/cil/framework/vector_geometry.py +++ b/Wrappers/Python/cil/framework/vector_geometry.py @@ -99,7 +99,6 @@ def allocate(self, value=0, **kwargs): out += value elif value is not None: FillTypes.validate(value) - value = FillTypes.get_enum_member(value) if value == FillTypes.RANDOM: seed = kwargs.get('seed', None) diff --git a/Wrappers/Python/cil/plugins/TomoPhantom.py b/Wrappers/Python/cil/plugins/TomoPhantom.py index 666c587754..4d99daa3ac 100644 --- a/Wrappers/Python/cil/plugins/TomoPhantom.py +++ b/Wrappers/Python/cil/plugins/TomoPhantom.py @@ -150,7 +150,7 @@ def get_ImageData(num_model, geometry): ig.set_labels(DimensionLabelsImage.get_default_order_for_engine('cil')) num_dims = len(ig.dimension_labels) - if DimensionLabelsImage.CHANNEL.value in ig.dimension_labels: + if DimensionLabelsImage.CHANNEL in ig.dimension_labels: if not is_model_temporal(num_model): raise ValueError('Selected model {} is not a temporal model, please change your selection'.format(num_model)) if num_dims == 4: diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py index de69d780bb..6a83ee0194 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py @@ -49,7 +49,7 @@ def convert_geometry_to_astra(volume_geometry, sinogram_geometry): #get units - if UnitsAngles.get_enum_member(sinogram_geometry.config.angles.angle_unit) == UnitsAngles.DEGREE: + if sinogram_geometry.config.angles.angle_unit == UnitsAngles.DEGREE: angles_rad = sinogram_geometry.config.angles.angle_data * np.pi / 180.0 else: angles_rad = sinogram_geometry.config.angles.angle_data diff --git a/Wrappers/Python/cil/plugins/tigre/Geometry.py b/Wrappers/Python/cil/plugins/tigre/Geometry.py index 5c6f9140ce..403b3822b3 100644 --- a/Wrappers/Python/cil/plugins/tigre/Geometry.py +++ b/Wrappers/Python/cil/plugins/tigre/Geometry.py @@ -33,7 +33,7 @@ def getTIGREGeometry(ig, ag): #angles angles = ag.config.angles.angle_data + ag.config.angles.initial_angle - if UnitsAngles.get_enum_member(ag.config.angles.angle_unit) == UnitsAngles.DEGREE: + if ag.config.angles.angle_unit == UnitsAngles.DEGREE: angles *= (np.pi/180.) #convert CIL to TIGRE angles s diff --git a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py index 1b0c6ba26e..54e2041291 100644 --- a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py @@ -221,7 +221,7 @@ def adjoint(self, x, out=None): data = x.as_array() #if single angle projection add the dimension in for TIGRE - if x.dimension_labels[0] != DimensionLabelsAcquisition.ANGLE.value: + if x.dimension_labels[0] != DimensionLabelsAcquisition.ANGLE: data = np.expand_dims(data, axis=0) if self.tigre_geom.is2D: diff --git a/Wrappers/Python/cil/recon/FBP.py b/Wrappers/Python/cil/recon/FBP.py index d5b24edea9..913b94acaa 100644 --- a/Wrappers/Python/cil/recon/FBP.py +++ b/Wrappers/Python/cil/recon/FBP.py @@ -376,7 +376,7 @@ def __init__ (self, input, image_geometry=None, filter='ram-lak'): #call parent initialiser super().__init__(input, image_geometry, filter, backend='tigre') - if AcquisitionType.get_enum_member(input.geometry.geom_type) != AcquisitionType.CONE: + if input.geometry.geom_type != AcquisitionType.CONE: raise TypeError("This reconstructor is for cone-beam data only.") @@ -485,7 +485,7 @@ def __init__ (self, input, image_geometry=None, filter='ram-lak', backend='tigre super().__init__(input, image_geometry, filter, backend) self.set_split_processing(False) - if AcquisitionType.get_enum_member(input.geometry.geom_type) != AcquisitionType.PARALLEL: + if input.geometry.geom_type != AcquisitionType.PARALLEL: raise TypeError("This reconstructor is for parallel-beam data only.") diff --git a/Wrappers/Python/cil/utilities/dataexample.py b/Wrappers/Python/cil/utilities/dataexample.py index b21c000c20..0890f1d40f 100644 --- a/Wrappers/Python/cil/utilities/dataexample.py +++ b/Wrappers/Python/cil/utilities/dataexample.py @@ -354,7 +354,7 @@ def load(self, which, size=None, scale=(0,1), **kwargs): sdata = numpy.zeros((N, M)) sdata[int(round(N/4)):int(round(3*N/4)), int(round(M/4)):int(round(3*M/4))] = 0.5 sdata[int(round(N/8)):int(round(7*N/8)), int(round(3*M/8)):int(round(5*M/8))] = 1 - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y.value, DimensionLabelsImage.HORIZONTAL_X.value]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y, DimensionLabelsImage.HORIZONTAL_X]) data = ig.allocate() data.fill(sdata) @@ -369,7 +369,7 @@ def load(self, which, size=None, scale=(0,1), **kwargs): N = size[0] M = size[1] - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y.value, DimensionLabelsImage.HORIZONTAL_X.value]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y, DimensionLabelsImage.HORIZONTAL_X]) data = ig.allocate() tmp = numpy.array(f.convert('L').resize((M,N))) data.fill(tmp/numpy.max(tmp)) @@ -391,13 +391,13 @@ def load(self, which, size=None, scale=(0,1), **kwargs): bands = tmp.getbands() ig = ImageGeometry(voxel_num_x=M, voxel_num_y=N, channels=len(bands), - dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y.value, DimensionLabelsImage.HORIZONTAL_X.value,DimensionLabelsImage.CHANNEL.value]) + dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y, DimensionLabelsImage.HORIZONTAL_X,DimensionLabelsImage.CHANNEL]) data = ig.allocate() data.fill(numpy.array(tmp.resize((M,N)))) - data.reorder([DimensionLabelsImage.CHANNEL.value,DimensionLabelsImage.HORIZONTAL_Y.value, DimensionLabelsImage.HORIZONTAL_X.value]) + data.reorder([DimensionLabelsImage.CHANNEL,DimensionLabelsImage.HORIZONTAL_Y, DimensionLabelsImage.HORIZONTAL_X]) data.geometry.channel_labels = bands else: - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y.value, DimensionLabelsImage.HORIZONTAL_X.value]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y, DimensionLabelsImage.HORIZONTAL_X]) data = ig.allocate() data.fill(numpy.array(tmp.resize((M,N)))) From fe6803eb770d47cd611fa32dbdf90fe91e77d46a Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Thu, 15 Aug 2024 11:09:29 +0000 Subject: [PATCH 42/72] rename label.py --- Wrappers/Python/cil/framework/__init__.py | 2 +- .../cil/framework/{label.py => labels.py} | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) rename Wrappers/Python/cil/framework/{label.py => labels.py} (95%) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index bca270c943..6ee3336c8f 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -27,4 +27,4 @@ from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer from .block import BlockDataContainer, BlockGeometry from .partitioner import Partitioner -from .label import DimensionLabelsAcquisition, DimensionLabelsImage, FillTypes, UnitsAngles, AcquisitionType, AcquisitionDimension +from .labels import DimensionLabelsAcquisition, DimensionLabelsImage, FillTypes, UnitsAngles, AcquisitionType, AcquisitionDimension diff --git a/Wrappers/Python/cil/framework/label.py b/Wrappers/Python/cil/framework/labels.py similarity index 95% rename from Wrappers/Python/cil/framework/label.py rename to Wrappers/Python/cil/framework/labels.py index 753e4dcf53..bd4c55d6fb 100644 --- a/Wrappers/Python/cil/framework/label.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -18,7 +18,7 @@ from enum import Enum -class LabelsBase(Enum): +class _LabelsBase(Enum): @classmethod def validate(cls, label): @@ -77,12 +77,12 @@ def __eq__(self, other): return self.value == other or self.name == other return super().__eq__(other) -class Backends(LabelsBase): +class Backends(_LabelsBase): ASTRA = "astra" TIGRE = "tigre" CIL = "cil" -class DimensionLabelsImage(LabelsBase): +class DimensionLabelsImage(_LabelsBase): CHANNEL = "channel" VERTICAL = "vertical" HORIZONTAL_X = "horizontal_x" @@ -119,7 +119,7 @@ def check_order_for_engine(cls, engine, geometry): "Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" .format(order_requested, list(geometry.dimension_labels), engine)) -class DimensionLabelsAcquisition(LabelsBase): +class DimensionLabelsAcquisition(_LabelsBase): CHANNEL = "channel" ANGLE = "angle" VERTICAL = "vertical" @@ -156,20 +156,19 @@ def check_order_for_engine(cls, engine, geometry): "Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" .format(order_requested, list(geometry.dimension_labels), engine)) -class FillTypes(LabelsBase): +class FillTypes(_LabelsBase): RANDOM = "random" RANDOM_INT = "random_int" -class UnitsAngles(LabelsBase): +class UnitsAngles(_LabelsBase): DEGREE = "degree" RADIAN = "radian" - -class AcquisitionType(LabelsBase): +class AcquisitionType(_LabelsBase): PARALLEL = "parallel" CONE = "cone" -class AcquisitionDimension(LabelsBase): +class AcquisitionDimension(_LabelsBase): DIM2 = "2D" DIM3 = "3D" From f8cb315bd5bf734b91189043ffc7808beb9767cf Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Thu, 15 Aug 2024 11:14:56 +0000 Subject: [PATCH 43/72] rename DimensionLabels classes for consistency --- Wrappers/Python/cil/framework/__init__.py | 2 +- .../Python/cil/framework/acquisition_data.py | 4 +- .../cil/framework/acquisition_geometry.py | 32 ++++----- Wrappers/Python/cil/framework/block.py | 2 +- Wrappers/Python/cil/framework/image_data.py | 4 +- .../Python/cil/framework/image_geometry.py | 40 +++++------ Wrappers/Python/cil/framework/labels.py | 4 +- .../Python/cil/framework/vector_geometry.py | 2 +- Wrappers/Python/cil/io/ZEISSDataReader.py | 6 +- Wrappers/Python/cil/plugins/TomoPhantom.py | 6 +- .../astra/operators/ProjectionOperator.py | 6 +- .../astra/processors/AstraBackProjector3D.py | 6 +- .../processors/AstraForwardProjector3D.py | 6 +- .../cil/plugins/astra/processors/FBP.py | 6 +- .../functions/regularisers.py | 4 +- Wrappers/Python/cil/plugins/tigre/FBP.py | 8 +-- .../cil/plugins/tigre/ProjectionOperator.py | 8 +-- .../cil/processors/CofR_image_sharpness.py | 4 +- .../Python/cil/processors/PaganinProcessor.py | 4 +- Wrappers/Python/cil/recon/Reconstructor.py | 4 +- Wrappers/Python/cil/utilities/dataexample.py | 12 ++-- Wrappers/Python/test/test_DataContainer.py | 72 +++++++++---------- Wrappers/Python/test/test_subset.py | 72 +++++++++---------- Wrappers/Python/test/utils_projectors.py | 30 ++++---- 24 files changed, 172 insertions(+), 172 deletions(-) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 6ee3336c8f..8388061b94 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -27,4 +27,4 @@ from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer from .block import BlockDataContainer, BlockGeometry from .partitioner import Partitioner -from .labels import DimensionLabelsAcquisition, DimensionLabelsImage, FillTypes, UnitsAngles, AcquisitionType, AcquisitionDimension +from .labels import AcquisitionDimensionLabels, ImageDimensionLabels, FillTypes, UnitsAngles, AcquisitionType, AcquisitionDimension diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index 8add45e916..54ad3bd873 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt import numpy -from .label import DimensionLabelsAcquisition, Backends +from .labels import AcquisitionDimensionLabels, Backends from .data_container import DataContainer from .partitioner import Partitioner @@ -113,7 +113,7 @@ def reorder(self, order=None): try: Backends.validate(order) - order = DimensionLabelsAcquisition.get_order_for_engine(order, self.geometry) + order = AcquisitionDimensionLabels.get_order_for_engine(order, self.geometry) except ValueError: pass diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 7fcc73edec..ed8f524939 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -22,7 +22,7 @@ import numpy -from .label import DimensionLabelsAcquisition, UnitsAngles, AcquisitionType, FillTypes, AcquisitionDimension +from .labels import AcquisitionDimensionLabels, UnitsAngles, AcquisitionType, FillTypes, AcquisitionDimension from .acquisition_data import AcquisitionData from .image_geometry import ImageGeometry @@ -1624,13 +1624,13 @@ class AcquisitionGeometry(object): #for backwards compatibility @property def ANGLE(self): - warnings.warn("use DimensionLabelsAcquisition.Angle instead", DeprecationWarning, stacklevel=2) - return DimensionLabelsAcquisition.ANGLE + warnings.warn("use AcquisitionDimensionLabels.Angle instead", DeprecationWarning, stacklevel=2) + return AcquisitionDimensionLabels.ANGLE @property def CHANNEL(self): - warnings.warn("use DimensionLabelsAcquisition.Channel instead", DeprecationWarning, stacklevel=2) - return DimensionLabelsAcquisition.CHANNEL + warnings.warn("use AcquisitionDimensionLabels.Channel instead", DeprecationWarning, stacklevel=2) + return AcquisitionDimensionLabels.CHANNEL @property def DEGREE(self): @@ -1639,8 +1639,8 @@ def DEGREE(self): @property def HORIZONTAL(self): - warnings.warn("use DimensionLabelsAcquisition.HORIZONTAL instead", DeprecationWarning, stacklevel=2) - return DimensionLabelsAcquisition.HORIZONTAL + warnings.warn("use AcquisitionDimensionLabels.HORIZONTAL instead", DeprecationWarning, stacklevel=2) + return AcquisitionDimensionLabels.HORIZONTAL @property def RADIAN(self): @@ -1649,8 +1649,8 @@ def RADIAN(self): @property def VERTICAL(self): - warnings.warn("use DimensionLabelsAcquisition.VERTICAL instead", DeprecationWarning, stacklevel=2) - return DimensionLabelsAcquisition.VERTICAL + warnings.warn("use AcquisitionDimensionLabels.VERTICAL instead", DeprecationWarning, stacklevel=2) + return AcquisitionDimensionLabels.VERTICAL @property def geom_type(self): @@ -1722,10 +1722,10 @@ def dimension(self): @property def shape(self): - shape_dict = {DimensionLabelsAcquisition.CHANNEL.value: self.config.channels.num_channels, - DimensionLabelsAcquisition.ANGLE.value: self.config.angles.num_positions, - DimensionLabelsAcquisition.VERTICAL.value: self.config.panel.num_pixels[1], - DimensionLabelsAcquisition.HORIZONTAL.value: self.config.panel.num_pixels[0]} + shape_dict = {AcquisitionDimensionLabels.CHANNEL.value: self.config.channels.num_channels, + AcquisitionDimensionLabels.ANGLE.value: self.config.angles.num_positions, + AcquisitionDimensionLabels.VERTICAL.value: self.config.panel.num_pixels[1], + AcquisitionDimensionLabels.HORIZONTAL.value: self.config.panel.num_pixels[0]} shape = [] for label in self.dimension_labels: shape.append(shape_dict[label]) @@ -1734,7 +1734,7 @@ def shape(self): @property def dimension_labels(self): - labels_default = DimensionLabelsAcquisition.get_default_order_for_engine("CIL") + labels_default = AcquisitionDimensionLabels.get_default_order_for_engine("CIL") shape_default = [self.config.channels.num_channels, self.config.angles.num_positions, @@ -1765,8 +1765,8 @@ def dimension_labels(self, val): if val is not None: label_new=[] for x in val: - if DimensionLabelsAcquisition.validate(x): - label_new.append(DimensionLabelsAcquisition.get_enum_value(x)) + if AcquisitionDimensionLabels.validate(x): + label_new.append(AcquisitionDimensionLabels.get_enum_value(x)) self._dimension_labels = tuple(label_new) diff --git a/Wrappers/Python/cil/framework/block.py b/Wrappers/Python/cil/framework/block.py index 5fd849db1a..f564de2609 100644 --- a/Wrappers/Python/cil/framework/block.py +++ b/Wrappers/Python/cil/framework/block.py @@ -22,7 +22,7 @@ import numpy from ..utilities.multiprocessing import NUM_THREADS -from .label import FillTypes +from .labels import FillTypes class BlockGeometry(object): diff --git a/Wrappers/Python/cil/framework/image_data.py b/Wrappers/Python/cil/framework/image_data.py index 578a5850c9..e918e474fb 100644 --- a/Wrappers/Python/cil/framework/image_data.py +++ b/Wrappers/Python/cil/framework/image_data.py @@ -18,7 +18,7 @@ import numpy from .data_container import DataContainer -from .label import DimensionLabelsImage, Backends +from .labels import ImageDimensionLabels, Backends class ImageData(DataContainer): '''DataContainer for holding 2D or 3D DataContainer''' @@ -201,7 +201,7 @@ def reorder(self, order=None): try: Backends.validate(order) - order = DimensionLabelsImage.get_order_for_engine(order, self.geometry) + order = ImageDimensionLabels.get_order_for_engine(order, self.geometry) except ValueError: pass diff --git a/Wrappers/Python/cil/framework/image_geometry.py b/Wrappers/Python/cil/framework/image_geometry.py index 1ca4083112..55cf9f7afc 100644 --- a/Wrappers/Python/cil/framework/image_geometry.py +++ b/Wrappers/Python/cil/framework/image_geometry.py @@ -22,24 +22,24 @@ import numpy from .image_data import ImageData -from .label import DimensionLabelsImage, FillTypes +from .labels import ImageDimensionLabels, FillTypes class ImageGeometry: @property def CHANNEL(self): - warnings.warn("use DimensionLabelsImage.CHANNEL instead", DeprecationWarning, stacklevel=2) - return DimensionLabelsImage.CHANNEL + warnings.warn("use ImageDimensionLabels.CHANNEL instead", DeprecationWarning, stacklevel=2) + return ImageDimensionLabels.CHANNEL @property def HORIZONTAL_X(self): - warnings.warn("use DimensionLabelsImage.HORIZONTAL_X instead", DeprecationWarning, stacklevel=2) - return DimensionLabelsImage.HORIZONTAL_X + warnings.warn("use ImageDimensionLabels.HORIZONTAL_X instead", DeprecationWarning, stacklevel=2) + return ImageDimensionLabels.HORIZONTAL_X @property def HORIZONTAL_Y(self): - warnings.warn("use DimensionLabelsImage.HORIZONTAL_Y instead", DeprecationWarning, stacklevel=2) - return DimensionLabelsImage.HORIZONTAL_Y + warnings.warn("use ImageDimensionLabels.HORIZONTAL_Y instead", DeprecationWarning, stacklevel=2) + return ImageDimensionLabels.HORIZONTAL_Y @property def RANDOM(self): @@ -52,15 +52,15 @@ def RANDOM_INT(self): @property def VERTICAL(self): - warnings.warn("use DimensionLabelsImage.VERTICAL instead", DeprecationWarning, stacklevel=2) - return DimensionLabelsImage.VERTICAL + warnings.warn("use ImageDimensionLabels.VERTICAL instead", DeprecationWarning, stacklevel=2) + return ImageDimensionLabels.VERTICAL @property def shape(self): - shape_dict = {DimensionLabelsImage.CHANNEL.value: self.channels, - DimensionLabelsImage.VERTICAL.value: self.voxel_num_z, - DimensionLabelsImage.HORIZONTAL_Y.value: self.voxel_num_y, - DimensionLabelsImage.HORIZONTAL_X.value: self.voxel_num_x} + shape_dict = {ImageDimensionLabels.CHANNEL.value: self.channels, + ImageDimensionLabels.VERTICAL.value: self.voxel_num_z, + ImageDimensionLabels.HORIZONTAL_Y.value: self.voxel_num_y, + ImageDimensionLabels.HORIZONTAL_X.value: self.voxel_num_x} shape = [] for label in self.dimension_labels: @@ -75,10 +75,10 @@ def shape(self, val): @property def spacing(self): - spacing_dict = {DimensionLabelsImage.CHANNEL.value: self.channel_spacing, - DimensionLabelsImage.VERTICAL.value: self.voxel_size_z, - DimensionLabelsImage.HORIZONTAL_Y.value: self.voxel_size_y, - DimensionLabelsImage.HORIZONTAL_X.value: self.voxel_size_x} + spacing_dict = {ImageDimensionLabels.CHANNEL.value: self.channel_spacing, + ImageDimensionLabels.VERTICAL.value: self.voxel_size_z, + ImageDimensionLabels.HORIZONTAL_Y.value: self.voxel_size_y, + ImageDimensionLabels.HORIZONTAL_X.value: self.voxel_size_x} spacing = [] for label in self.dimension_labels: @@ -97,7 +97,7 @@ def ndim(self): @property def dimension_labels(self): - labels_default = DimensionLabelsImage.get_default_order_for_engine("CIL") + labels_default = ImageDimensionLabels.get_default_order_for_engine("CIL") shape_default = [ self.channels, self.voxel_num_z, @@ -125,8 +125,8 @@ def set_labels(self, labels): if labels is not None: label_new=[] for x in labels: - if DimensionLabelsImage.validate(x): - label_new.append(DimensionLabelsImage.get_enum_value(x)) + if ImageDimensionLabels.validate(x): + label_new.append(ImageDimensionLabels.get_enum_value(x)) self._dimension_labels = tuple(label_new) diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index bd4c55d6fb..740ac0044f 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -82,7 +82,7 @@ class Backends(_LabelsBase): TIGRE = "tigre" CIL = "cil" -class DimensionLabelsImage(_LabelsBase): +class ImageDimensionLabels(_LabelsBase): CHANNEL = "channel" VERTICAL = "vertical" HORIZONTAL_X = "horizontal_x" @@ -119,7 +119,7 @@ def check_order_for_engine(cls, engine, geometry): "Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" .format(order_requested, list(geometry.dimension_labels), engine)) -class DimensionLabelsAcquisition(_LabelsBase): +class AcquisitionDimensionLabels(_LabelsBase): CHANNEL = "channel" ANGLE = "angle" VERTICAL = "vertical" diff --git a/Wrappers/Python/cil/framework/vector_geometry.py b/Wrappers/Python/cil/framework/vector_geometry.py index 23a482dea3..952bbdbe15 100644 --- a/Wrappers/Python/cil/framework/vector_geometry.py +++ b/Wrappers/Python/cil/framework/vector_geometry.py @@ -21,7 +21,7 @@ import numpy -from .label import FillTypes +from .labels import FillTypes class VectorGeometry: '''Geometry describing VectorData to contain 1D array''' diff --git a/Wrappers/Python/cil/io/ZEISSDataReader.py b/Wrappers/Python/cil/io/ZEISSDataReader.py index 8f01950337..41e7ab1908 100644 --- a/Wrappers/Python/cil/io/ZEISSDataReader.py +++ b/Wrappers/Python/cil/io/ZEISSDataReader.py @@ -18,7 +18,7 @@ # Andrew Shartis (UES, Inc.) -from cil.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry, UnitsAngles, DimensionLabelsAcquisition, DimensionLabelsImage +from cil.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry, UnitsAngles, AcquisitionDimensionLabels, ImageDimensionLabels import numpy as np import os import olefile @@ -127,10 +127,10 @@ def set_up(self, if roi is not None: if metadata['data geometry'] == 'acquisition': - allowed_labels = [item.value for item in DimensionLabelsAcquisition] + allowed_labels = [item.value for item in AcquisitionDimensionLabels] zeiss_data_order = {'angle':0, 'vertical':1, 'horizontal':2} else: - allowed_labels = [item.value for item in DimensionLabelsImage] + allowed_labels = [item.value for item in ImageDimensionLabels] zeiss_data_order = {'vertical':0, 'horizontal_y':1, 'horizontal_x':2} # check roi labels and create tuple for slicing diff --git a/Wrappers/Python/cil/plugins/TomoPhantom.py b/Wrappers/Python/cil/plugins/TomoPhantom.py index 4d99daa3ac..3f2d87f071 100644 --- a/Wrappers/Python/cil/plugins/TomoPhantom.py +++ b/Wrappers/Python/cil/plugins/TomoPhantom.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import ImageData, DimensionLabelsImage +from cil.framework import ImageData, ImageDimensionLabels import tomophantom from tomophantom import TomoP2D, TomoP3D import os @@ -147,10 +147,10 @@ def get_ImageData(num_model, geometry): ''' ig = geometry.copy() - ig.set_labels(DimensionLabelsImage.get_default_order_for_engine('cil')) + ig.set_labels(ImageDimensionLabels.get_default_order_for_engine('cil')) num_dims = len(ig.dimension_labels) - if DimensionLabelsImage.CHANNEL in ig.dimension_labels: + if ImageDimensionLabels.CHANNEL in ig.dimension_labels: if not is_model_temporal(num_model): raise ValueError('Selected model {} is not a temporal model, please change your selection'.format(num_model)) if num_dims == 4: diff --git a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py index e76838abfb..af7a70fa7d 100644 --- a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py @@ -18,7 +18,7 @@ import logging -from cil.framework import BlockGeometry, DimensionLabelsAcquisition, DimensionLabelsImage +from cil.framework import BlockGeometry, AcquisitionDimensionLabels, ImageDimensionLabels from cil.optimisation.operators import BlockOperator, LinearOperator, ChannelwiseOperator from cil.plugins.astra.operators import AstraProjector2D, AstraProjector3D @@ -115,8 +115,8 @@ def __init__(self, self).__init__(domain_geometry=image_geometry, range_geometry=acquisition_geometry) - DimensionLabelsAcquisition.check_order_for_engine('astra',acquisition_geometry) - DimensionLabelsImage.check_order_for_engine('astra',image_geometry) + AcquisitionDimensionLabels.check_order_for_engine('astra',acquisition_geometry) + ImageDimensionLabels.check_order_for_engine('astra',image_geometry) self.volume_geometry = image_geometry self.sinogram_geometry = acquisition_geometry diff --git a/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py b/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py index cec439de1a..4c21c85113 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py +++ b/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import DataProcessor, ImageData, DimensionLabelsAcquisition, DimensionLabelsImage +from cil.framework import DataProcessor, ImageData, AcquisitionDimensionLabels, ImageDimensionLabels from cil.plugins.astra.utilities import convert_geometry_to_astra_vec_3D import astra from astra import astra_dict, algorithm, data3d @@ -66,7 +66,7 @@ def check_input(self, dataset): def set_ImageGeometry(self, volume_geometry): - DimensionLabelsImage.check_order_for_engine('astra', volume_geometry) + ImageDimensionLabels.check_order_for_engine('astra', volume_geometry) if len(volume_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(volume_geometry.number_of_dimensions)) @@ -75,7 +75,7 @@ def set_ImageGeometry(self, volume_geometry): def set_AcquisitionGeometry(self, sinogram_geometry): - DimensionLabelsAcquisition.check_order_for_engine('astra', sinogram_geometry) + AcquisitionDimensionLabels.check_order_for_engine('astra', sinogram_geometry) if len(sinogram_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(sinogram_geometry.number_of_dimensions)) diff --git a/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py b/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py index 18473bb566..5a87d3e261 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py +++ b/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import DataProcessor, AcquisitionData, DimensionLabelsImage, DimensionLabelsAcquisition +from cil.framework import DataProcessor, AcquisitionData, ImageDimensionLabels, AcquisitionDimensionLabels from cil.plugins.astra.utilities import convert_geometry_to_astra_vec_3D import astra from astra import astra_dict, algorithm, data3d @@ -64,7 +64,7 @@ def check_input(self, dataset): def set_ImageGeometry(self, volume_geometry): - DimensionLabelsImage.check_order_for_engine('astra', volume_geometry) + ImageDimensionLabels.check_order_for_engine('astra', volume_geometry) if len(volume_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(volume_geometry.number_of_dimensions)) @@ -73,7 +73,7 @@ def set_ImageGeometry(self, volume_geometry): def set_AcquisitionGeometry(self, sinogram_geometry): - DimensionLabelsAcquisition.check_order_for_engine('astra', sinogram_geometry) + AcquisitionDimensionLabels.check_order_for_engine('astra', sinogram_geometry) if len(sinogram_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(sinogram_geometry.number_of_dimensions)) diff --git a/Wrappers/Python/cil/plugins/astra/processors/FBP.py b/Wrappers/Python/cil/plugins/astra/processors/FBP.py index d0f0f781b0..c52006eef9 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/FBP.py +++ b/Wrappers/Python/cil/plugins/astra/processors/FBP.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt import warnings -from cil.framework import DataProcessor, DimensionLabelsImage, DimensionLabelsAcquisition +from cil.framework import DataProcessor, ImageDimensionLabels, AcquisitionDimensionLabels from cil.plugins.astra.processors.FBP_Flexible import FBP_Flexible from cil.plugins.astra.processors.FDK_Flexible import FDK_Flexible from cil.plugins.astra.processors.FBP_Flexible import FBP_CPU @@ -66,8 +66,8 @@ def __init__(self, image_geometry=None, acquisition_geometry=None, device='gpu') if image_geometry is None: image_geometry = acquisition_geometry.get_ImageGeometry() - DimensionLabelsAcquisition.check_order_for_engine('astra', acquisition_geometry) - DimensionLabelsImage.check_order_for_engine('astra', image_geometry) + AcquisitionDimensionLabels.check_order_for_engine('astra', acquisition_geometry) + ImageDimensionLabels.check_order_for_engine('astra', image_geometry) if device == 'gpu': if acquisition_geometry.geom_type == 'parallel': diff --git a/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py b/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py index 258aac5562..516c203851 100644 --- a/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py +++ b/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py @@ -23,7 +23,7 @@ raise ImportError('Please `conda install "ccpi::ccpi-regulariser>=24.0.1"`') from exc -from cil.framework import DataContainer, DimensionLabelsImage +from cil.framework import DataContainer, ImageDimensionLabels from cil.optimisation.functions import Function import numpy as np import warnings @@ -494,7 +494,7 @@ def __rmul__(self, scalar): def check_input(self, input): '''TNV requires 2D+channel data with the first dimension as the channel dimension''' if isinstance(input, DataContainer): - DimensionLabelsImage.check_order_for_engine('cil', input.geometry) + ImageDimensionLabels.check_order_for_engine('cil', input.geometry) if ( input.geometry.channels == 1 ) or ( not input.geometry.ndim == 3) : raise ValueError('TNV requires 2D+channel data. Got {}'.format(input.geometry.dimension_labels)) else: diff --git a/Wrappers/Python/cil/plugins/tigre/FBP.py b/Wrappers/Python/cil/plugins/tigre/FBP.py index 9cfb2daac4..66fe2315a0 100644 --- a/Wrappers/Python/cil/plugins/tigre/FBP.py +++ b/Wrappers/Python/cil/plugins/tigre/FBP.py @@ -22,7 +22,7 @@ import numpy as np -from cil.framework import DataProcessor, ImageData, DimensionLabelsAcquisition, DimensionLabelsImage +from cil.framework import DataProcessor, ImageData, AcquisitionDimensionLabels, ImageDimensionLabels from cil.plugins.tigre import CIL2TIGREGeometry try: @@ -65,8 +65,8 @@ def __init__(self, image_geometry=None, acquisition_geometry=None, **kwargs): raise ValueError("TIGRE FBP is GPU only. Got device = {}".format(device)) - DimensionLabelsAcquisition.check_order_for_engine('tigre', acquisition_geometry) - DimensionLabelsImage.check_order_for_engine('tigre', image_geometry) + AcquisitionDimensionLabels.check_order_for_engine('tigre', acquisition_geometry) + ImageDimensionLabels.check_order_for_engine('tigre', image_geometry) tigre_geom, tigre_angles = CIL2TIGREGeometry.getTIGREGeometry(image_geometry,acquisition_geometry) @@ -81,7 +81,7 @@ def check_input(self, dataset): raise ValueError("Expected input data to be single channel, got {0}"\ .format(self.acquisition_geometry.channels)) - DimensionLabelsAcquisition.check_order_for_engine('tigre', dataset.geometry) + AcquisitionDimensionLabels.check_order_for_engine('tigre', dataset.geometry) return True def process(self, out=None): diff --git a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py index 54e2041291..f90bbce371 100644 --- a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import (ImageData, AcquisitionData, DimensionLabelsAcquisition, DimensionLabelsImage, +from cil.framework import (ImageData, AcquisitionData, AcquisitionDimensionLabels, ImageDimensionLabels, BlockGeometry) from cil.optimisation.operators import BlockOperator, LinearOperator from cil.plugins.tigre import CIL2TIGREGeometry @@ -145,8 +145,8 @@ def __init__(self, "TIGRE projectors are GPU only. Got device = {}".format( device)) - DimensionLabelsImage.check_order_for_engine('tigre', image_geometry) - DimensionLabelsAcquisition.check_order_for_engine('tigre', acquisition_geometry) + ImageDimensionLabels.check_order_for_engine('tigre', image_geometry) + AcquisitionDimensionLabels.check_order_for_engine('tigre', acquisition_geometry) super(ProjectionOperator,self).__init__(domain_geometry=image_geometry,\ range_geometry=acquisition_geometry) @@ -221,7 +221,7 @@ def adjoint(self, x, out=None): data = x.as_array() #if single angle projection add the dimension in for TIGRE - if x.dimension_labels[0] != DimensionLabelsAcquisition.ANGLE: + if x.dimension_labels[0] != AcquisitionDimensionLabels.ANGLE: data = np.expand_dims(data, axis=0) if self.tigre_geom.is2D: diff --git a/Wrappers/Python/cil/processors/CofR_image_sharpness.py b/Wrappers/Python/cil/processors/CofR_image_sharpness.py index 5e7e8e3dc9..10cca07da0 100644 --- a/Wrappers/Python/cil/processors/CofR_image_sharpness.py +++ b/Wrappers/Python/cil/processors/CofR_image_sharpness.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import Processor, AcquisitionData, DimensionLabelsAcquisition +from cil.framework import Processor, AcquisitionData, AcquisitionDimensionLabels import matplotlib.pyplot as plt import scipy import numpy as np @@ -128,7 +128,7 @@ def check_input(self, data): else: test_geom = data.geometry - if not DimensionLabelsAcquisition.check_order_for_engine(self.backend, test_geom): + if not AcquisitionDimensionLabels.check_order_for_engine(self.backend, test_geom): raise ValueError("Input data must be reordered for use with selected backend. Use input.reorder{'{0}')".format(self.backend)) return True diff --git a/Wrappers/Python/cil/processors/PaganinProcessor.py b/Wrappers/Python/cil/processors/PaganinProcessor.py index 78f6e7e001..ec101d03b8 100644 --- a/Wrappers/Python/cil/processors/PaganinProcessor.py +++ b/Wrappers/Python/cil/processors/PaganinProcessor.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: # https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import Processor, AcquisitionData, DimensionLabelsAcquisition +from cil.framework import Processor, AcquisitionData, AcquisitionDimensionLabels import numpy as np from scipy.fft import fft2 @@ -206,7 +206,7 @@ def check_input(self, data): def process(self, out=None): data = self.get_input() - cil_order = tuple(DimensionLabelsAcquisition.get_order_for_engine('cil',data.geometry)) + cil_order = tuple(AcquisitionDimensionLabels.get_order_for_engine('cil',data.geometry)) if data.dimension_labels != cil_order: log.warning(msg="This processor will work most efficiently using\ \nCIL data order, consider using `data.reorder('cil')`") diff --git a/Wrappers/Python/cil/recon/Reconstructor.py b/Wrappers/Python/cil/recon/Reconstructor.py index 919a1032ca..75b06b9bbd 100644 --- a/Wrappers/Python/cil/recon/Reconstructor.py +++ b/Wrappers/Python/cil/recon/Reconstructor.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import AcquisitionData, ImageGeometry, DimensionLabelsAcquisition +from cil.framework import AcquisitionData, ImageGeometry, AcquisitionDimensionLabels import importlib import weakref @@ -105,7 +105,7 @@ def _configure_for_backend(self, backend='tigre'): if backend not in self.supported_backends: raise ValueError("Backend unsupported. Supported backends: {}".format(self.supported_backends)) - if not DimensionLabelsAcquisition.check_order_for_engine(backend, self.acquisition_geometry): + if not AcquisitionDimensionLabels.check_order_for_engine(backend, self.acquisition_geometry): raise ValueError("Input data must be reordered for use with selected backend. Use input.reorder{'{0}')".format(backend)) #set ProjectionOperator class from backend diff --git a/Wrappers/Python/cil/utilities/dataexample.py b/Wrappers/Python/cil/utilities/dataexample.py index 0890f1d40f..4e3037fc88 100644 --- a/Wrappers/Python/cil/utilities/dataexample.py +++ b/Wrappers/Python/cil/utilities/dataexample.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import ImageData, ImageGeometry, DimensionLabelsImage +from cil.framework import ImageData, ImageGeometry, ImageDimensionLabels import numpy import numpy as np from PIL import Image @@ -354,7 +354,7 @@ def load(self, which, size=None, scale=(0,1), **kwargs): sdata = numpy.zeros((N, M)) sdata[int(round(N/4)):int(round(3*N/4)), int(round(M/4)):int(round(3*M/4))] = 0.5 sdata[int(round(N/8)):int(round(7*N/8)), int(round(3*M/8)):int(round(5*M/8))] = 1 - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y, DimensionLabelsImage.HORIZONTAL_X]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X]) data = ig.allocate() data.fill(sdata) @@ -369,7 +369,7 @@ def load(self, which, size=None, scale=(0,1), **kwargs): N = size[0] M = size[1] - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y, DimensionLabelsImage.HORIZONTAL_X]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X]) data = ig.allocate() tmp = numpy.array(f.convert('L').resize((M,N))) data.fill(tmp/numpy.max(tmp)) @@ -391,13 +391,13 @@ def load(self, which, size=None, scale=(0,1), **kwargs): bands = tmp.getbands() ig = ImageGeometry(voxel_num_x=M, voxel_num_y=N, channels=len(bands), - dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y, DimensionLabelsImage.HORIZONTAL_X,DimensionLabelsImage.CHANNEL]) + dimension_labels=[ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X,ImageDimensionLabels.CHANNEL]) data = ig.allocate() data.fill(numpy.array(tmp.resize((M,N)))) - data.reorder([DimensionLabelsImage.CHANNEL,DimensionLabelsImage.HORIZONTAL_Y, DimensionLabelsImage.HORIZONTAL_X]) + data.reorder([ImageDimensionLabels.CHANNEL,ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X]) data.geometry.channel_labels = bands else: - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[DimensionLabelsImage.HORIZONTAL_Y, DimensionLabelsImage.HORIZONTAL_X]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X]) data = ig.allocate() data.fill(numpy.array(tmp.resize((M,N)))) diff --git a/Wrappers/Python/test/test_DataContainer.py b/Wrappers/Python/test/test_DataContainer.py index c7c27804f2..a97d1a89ce 100644 --- a/Wrappers/Python/test/test_DataContainer.py +++ b/Wrappers/Python/test/test_DataContainer.py @@ -19,7 +19,7 @@ import sys import numpy from cil.framework import (DataContainer, ImageGeometry, ImageData, VectorGeometry, AcquisitionData, - AcquisitionGeometry, BlockGeometry, VectorData, DimensionLabelsImage, DimensionLabelsAcquisition) + AcquisitionGeometry, BlockGeometry, VectorData, ImageDimensionLabels, AcquisitionDimensionLabels) from timeit import default_timer as timer import logging from testclass import CCPiTestClass @@ -455,8 +455,8 @@ def test_ImageData(self): self.assertEqual(vol.number_of_dimensions, 3) ig2 = ImageGeometry (voxel_num_x=2,voxel_num_y=3,voxel_num_z=4, - dimension_labels=[DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["HORIZONTAL_Y"], - DimensionLabelsImage["VERTICAL"]]) + dimension_labels=[ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["HORIZONTAL_Y"], + ImageDimensionLabels["VERTICAL"]]) data = ig2.allocate() self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ig2.shape)) self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape) @@ -501,8 +501,8 @@ def test_AcquisitionData(self): self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape) ag2 = AcquisitionGeometry.create_Parallel3D().set_angles(numpy.linspace(0, 180, num=10)).set_panel((2,3)).set_channels(4)\ - .set_labels([DimensionLabelsAcquisition["VERTICAL"] , - DimensionLabelsAcquisition["ANGLE"], DimensionLabelsAcquisition["HORIZONTAL"], DimensionLabelsAcquisition["CHANNEL"]]) + .set_labels([AcquisitionDimensionLabels["VERTICAL"] , + AcquisitionDimensionLabels["ANGLE"], AcquisitionDimensionLabels["HORIZONTAL"], AcquisitionDimensionLabels["CHANNEL"]]) data = ag2.allocate() self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ag2.shape)) @@ -739,27 +739,27 @@ def test_AcquisitionDataSubset(self): # expected dimension_labels - self.assertListEqual([DimensionLabelsAcquisition["CHANNEL"] , - DimensionLabelsAcquisition["ANGLE"] , DimensionLabelsAcquisition["VERTICAL"] , - DimensionLabelsAcquisition["HORIZONTAL"]], + self.assertListEqual([AcquisitionDimensionLabels["CHANNEL"] , + AcquisitionDimensionLabels["ANGLE"] , AcquisitionDimensionLabels["VERTICAL"] , + AcquisitionDimensionLabels["HORIZONTAL"]], list(sgeometry.dimension_labels)) sino = sgeometry.allocate() # test reshape - new_order = [DimensionLabelsAcquisition["HORIZONTAL"] , - DimensionLabelsAcquisition["CHANNEL"] , DimensionLabelsAcquisition["VERTICAL"] , - DimensionLabelsAcquisition["ANGLE"]] + new_order = [AcquisitionDimensionLabels["HORIZONTAL"] , + AcquisitionDimensionLabels["CHANNEL"] , AcquisitionDimensionLabels["VERTICAL"] , + AcquisitionDimensionLabels["ANGLE"]] sino.reorder(new_order) self.assertListEqual(new_order, list(sino.geometry.dimension_labels)) ss1 = sino.get_slice(vertical = 0) - self.assertListEqual([DimensionLabelsAcquisition["HORIZONTAL"] , - DimensionLabelsAcquisition["CHANNEL"] , - DimensionLabelsAcquisition["ANGLE"]], list(ss1.geometry.dimension_labels)) + self.assertListEqual([AcquisitionDimensionLabels["HORIZONTAL"] , + AcquisitionDimensionLabels["CHANNEL"] , + AcquisitionDimensionLabels["ANGLE"]], list(ss1.geometry.dimension_labels)) ss2 = sino.get_slice(vertical = 0, channel=0) - self.assertListEqual([DimensionLabelsAcquisition["HORIZONTAL"] , - DimensionLabelsAcquisition["ANGLE"]], list(ss2.geometry.dimension_labels)) + self.assertListEqual([AcquisitionDimensionLabels["HORIZONTAL"] , + AcquisitionDimensionLabels["ANGLE"]], list(ss2.geometry.dimension_labels)) def test_ImageDataSubset(self): @@ -783,42 +783,42 @@ def test_ImageDataSubset(self): self.assertListEqual(['channel', 'horizontal_y'], list(ss1.geometry.dimension_labels)) vg = ImageGeometry(3,4,5,channels=2) - self.assertListEqual([DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["VERTICAL"], - DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["HORIZONTAL_X"]], + self.assertListEqual([ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["VERTICAL"], + ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["HORIZONTAL_X"]], list(vg.dimension_labels)) ss2 = vg.allocate() ss3 = vol.get_slice(channel=0) - self.assertListEqual([DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["HORIZONTAL_X"]], list(ss3.geometry.dimension_labels)) + self.assertListEqual([ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["HORIZONTAL_X"]], list(ss3.geometry.dimension_labels)) def test_DataContainerSubset(self): dc = DataContainer(numpy.ones((2,3,4,5))) - dc.dimension_labels =[DimensionLabelsAcquisition["CHANNEL"] , - DimensionLabelsAcquisition["ANGLE"] , DimensionLabelsAcquisition["VERTICAL"] , - DimensionLabelsAcquisition["HORIZONTAL"]] + dc.dimension_labels =[AcquisitionDimensionLabels["CHANNEL"] , + AcquisitionDimensionLabels["ANGLE"] , AcquisitionDimensionLabels["VERTICAL"] , + AcquisitionDimensionLabels["HORIZONTAL"]] # test reshape - new_order = [DimensionLabelsAcquisition["HORIZONTAL"] , - DimensionLabelsAcquisition["CHANNEL"] , DimensionLabelsAcquisition["VERTICAL"] , - DimensionLabelsAcquisition["ANGLE"]] + new_order = [AcquisitionDimensionLabels["HORIZONTAL"] , + AcquisitionDimensionLabels["CHANNEL"] , AcquisitionDimensionLabels["VERTICAL"] , + AcquisitionDimensionLabels["ANGLE"]] dc.reorder(new_order) self.assertListEqual(new_order, list(dc.dimension_labels)) ss1 = dc.get_slice(vertical=0) - self.assertListEqual([DimensionLabelsAcquisition["HORIZONTAL"] , - DimensionLabelsAcquisition["CHANNEL"] , - DimensionLabelsAcquisition["ANGLE"]], list(ss1.dimension_labels)) + self.assertListEqual([AcquisitionDimensionLabels["HORIZONTAL"] , + AcquisitionDimensionLabels["CHANNEL"] , + AcquisitionDimensionLabels["ANGLE"]], list(ss1.dimension_labels)) ss2 = dc.get_slice(vertical=0, channel=0) - self.assertListEqual([DimensionLabelsAcquisition["HORIZONTAL"] , - DimensionLabelsAcquisition["ANGLE"]], list(ss2.dimension_labels)) + self.assertListEqual([AcquisitionDimensionLabels["HORIZONTAL"] , + AcquisitionDimensionLabels["ANGLE"]], list(ss2.dimension_labels)) # Check we can get slice still even if force parameter is passed: ss3 = dc.get_slice(vertical=0, channel=0, force=True) - self.assertListEqual([DimensionLabelsAcquisition["HORIZONTAL"] , - DimensionLabelsAcquisition["ANGLE"]], list(ss3.dimension_labels)) + self.assertListEqual([AcquisitionDimensionLabels["HORIZONTAL"] , + AcquisitionDimensionLabels["ANGLE"]], list(ss3.dimension_labels)) def test_DataContainerChaining(self): @@ -965,7 +965,7 @@ def test_multiply_out(self): numpy.testing.assert_array_equal(a, u.as_array()) - #u = ig.allocate(DimensionLabelsImage["RANDOM_INT"], seed=1) + #u = ig.allocate(ImageDimensionLabels["RANDOM_INT"], seed=1) l = functools.reduce(lambda x,y: x*y, (10,11,12), 1) a = numpy.zeros((l, ), dtype=numpy.float32) @@ -1274,7 +1274,7 @@ def test_fill_dimension_ImageData(self): ig = ImageGeometry(2,3,4) u = ig.allocate(0) a = numpy.ones((4,2)) - # default_labels = [DimensionLabelsImage["VERTICAL"], DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["HORIZONTAL_X"]] + # default_labels = [ImageDimensionLabels["VERTICAL"], ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["HORIZONTAL_X"]] data = u.as_array() axis_number = u.get_dimension_axis('horizontal_y') @@ -1302,7 +1302,7 @@ def test_fill_dimension_AcquisitionData(self): ag.set_labels(('horizontal','angle','vertical','channel')) u = ag.allocate(0) a = numpy.ones((4,2)) - # default_labels = [DimensionLabelsImage["VERTICAL"], DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["HORIZONTAL_X"]] + # default_labels = [ImageDimensionLabels["VERTICAL"], ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["HORIZONTAL_X"]] data = u.as_array() axis_number = u.get_dimension_axis('horizontal_y') @@ -1336,7 +1336,7 @@ def test_fill_dimension_AcquisitionData(self): u = ag.allocate(0) # (2, 5, 3, 4) a = numpy.ones((2,5)) - # default_labels = [DimensionLabelsImage["VERTICAL"], DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["HORIZONTAL_X"]] + # default_labels = [ImageDimensionLabels["VERTICAL"], ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["HORIZONTAL_X"]] b = u.get_slice(channel=0, vertical=0) data = u.as_array() diff --git a/Wrappers/Python/test/test_subset.py b/Wrappers/Python/test/test_subset.py index 0f9565a295..b08790ee45 100644 --- a/Wrappers/Python/test/test_subset.py +++ b/Wrappers/Python/test/test_subset.py @@ -19,7 +19,7 @@ import unittest from utils import initialise_tests import numpy -from cil.framework import DataContainer, ImageGeometry, AcquisitionGeometry, DimensionLabelsImage, DimensionLabelsAcquisition +from cil.framework import DataContainer, ImageGeometry, AcquisitionGeometry, ImageDimensionLabels, AcquisitionDimensionLabels from timeit import default_timer as timer initialise_tests() @@ -208,8 +208,8 @@ def setUp(self): def test_ImageDataAllocate1a(self): data = self.ig.allocate() - default_dimension_labels = [DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["VERTICAL"], - DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["HORIZONTAL_X"]] + default_dimension_labels = [ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["VERTICAL"], + ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["HORIZONTAL_X"]] self.assertTrue( default_dimension_labels == list(data.dimension_labels) ) def test_ImageDataAllocate1b(self): @@ -217,64 +217,64 @@ def test_ImageDataAllocate1b(self): self.assertTrue( data.shape == (5,4,3,2)) def test_ImageDataAllocate2a(self): - non_default_dimension_labels = [ DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["VERTICAL"], - DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["CHANNEL"]] + non_default_dimension_labels = [ ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["VERTICAL"], + ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["CHANNEL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() self.assertTrue( non_default_dimension_labels == list(data.dimension_labels) ) def test_ImageDataAllocate2b(self): - non_default_dimension_labels = [ DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["VERTICAL"], - DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["CHANNEL"]] + non_default_dimension_labels = [ ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["VERTICAL"], + ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["CHANNEL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() self.assertTrue( data.shape == (2,4,3,5)) def test_ImageDataSubset1a(self): - non_default_dimension_labels = [DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["HORIZONTAL_Y"], - DimensionLabelsImage["VERTICAL"]] + non_default_dimension_labels = [ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["HORIZONTAL_Y"], + ImageDimensionLabels["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(horizontal_y = 1) self.assertTrue( sub.shape == (2,5,4)) def test_ImageDataSubset2a(self): - non_default_dimension_labels = [DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["HORIZONTAL_Y"], - DimensionLabelsImage["VERTICAL"]] + non_default_dimension_labels = [ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["HORIZONTAL_Y"], + ImageDimensionLabels["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(horizontal_x = 1) self.assertTrue( sub.shape == (5,3,4)) def test_ImageDataSubset3a(self): - non_default_dimension_labels = [DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["HORIZONTAL_Y"], - DimensionLabelsImage["VERTICAL"]] + non_default_dimension_labels = [ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["HORIZONTAL_Y"], + ImageDimensionLabels["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(channel = 1) self.assertTrue( sub.shape == (2,3,4)) def test_ImageDataSubset4a(self): - non_default_dimension_labels = [DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["HORIZONTAL_Y"], - DimensionLabelsImage["VERTICAL"]] + non_default_dimension_labels = [ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["HORIZONTAL_Y"], + ImageDimensionLabels["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(vertical = 1) self.assertTrue( sub.shape == (2,5,3)) def test_ImageDataSubset5a(self): - non_default_dimension_labels = [DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["HORIZONTAL_Y"]] + non_default_dimension_labels = [ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["HORIZONTAL_Y"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(horizontal_y = 1) self.assertTrue( sub.shape == (2,)) def test_ImageDataSubset1b(self): - non_default_dimension_labels = [DimensionLabelsImage["HORIZONTAL_X"], DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["HORIZONTAL_Y"], - DimensionLabelsImage["VERTICAL"]] + non_default_dimension_labels = [ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["HORIZONTAL_Y"], + ImageDimensionLabels["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() - new_dimension_labels = [DimensionLabelsImage["HORIZONTAL_Y"], DimensionLabelsImage["CHANNEL"], DimensionLabelsImage["VERTICAL"], DimensionLabelsImage["HORIZONTAL_X"]] + new_dimension_labels = [ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["VERTICAL"], ImageDimensionLabels["HORIZONTAL_X"]] data.reorder(new_dimension_labels) self.assertTrue( data.shape == (3,5,4,2)) @@ -286,9 +286,9 @@ def test_ImageDataSubset1c(self): def test_AcquisitionDataAllocate1a(self): data = self.ag.allocate() - default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"] , - DimensionLabelsAcquisition["ANGLE"] , DimensionLabelsAcquisition["VERTICAL"] , - DimensionLabelsAcquisition["HORIZONTAL"]] + default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"] , + AcquisitionDimensionLabels["ANGLE"] , AcquisitionDimensionLabels["VERTICAL"] , + AcquisitionDimensionLabels["HORIZONTAL"]] self.assertTrue( default_dimension_labels == list(data.dimension_labels) ) def test_AcquisitionDataAllocate1b(self): @@ -296,8 +296,8 @@ def test_AcquisitionDataAllocate1b(self): self.assertTrue( data.shape == (4,3,2,20)) def test_AcquisitionDataAllocate2a(self): - non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], - DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], + AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() @@ -305,15 +305,15 @@ def test_AcquisitionDataAllocate2a(self): self.assertTrue( non_default_dimension_labels == list(data.dimension_labels) ) def test_AcquisitionDataAllocate2b(self): - non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], - DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], + AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() self.assertTrue( data.shape == (4,20,2,3)) def test_AcquisitionDataSubset1a(self): - non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], - DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], + AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) @@ -321,24 +321,24 @@ def test_AcquisitionDataSubset1a(self): self.assertTrue( sub.shape == (4,20,3)) def test_AcquisitionDataSubset1b(self): - non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], - DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], + AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) sub = data.get_slice(channel = 0) self.assertTrue( sub.shape == (20,2,3)) def test_AcquisitionDataSubset1c(self): - non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], - DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], + AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) sub = data.get_slice(horizontal = 0, force=True) self.assertTrue( sub.shape == (4,2,3)) def test_AcquisitionDataSubset1d(self): - non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], - DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], + AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) @@ -348,8 +348,8 @@ def test_AcquisitionDataSubset1d(self): self.assertTrue( sub.shape == (4,20,2) ) self.assertTrue( sub.geometry.angles[0] == data.geometry.angles[sliceme]) def test_AcquisitionDataSubset1e(self): - non_default_dimension_labels = [DimensionLabelsAcquisition["CHANNEL"], DimensionLabelsAcquisition["HORIZONTAL"], - DimensionLabelsAcquisition["VERTICAL"], DimensionLabelsAcquisition["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], + AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) diff --git a/Wrappers/Python/test/utils_projectors.py b/Wrappers/Python/test/utils_projectors.py index 29173dabcb..2d2876d931 100644 --- a/Wrappers/Python/test/utils_projectors.py +++ b/Wrappers/Python/test/utils_projectors.py @@ -19,7 +19,7 @@ import numpy as np from cil.optimisation.operators import LinearOperator from cil.utilities import dataexample -from cil.framework import AcquisitionGeometry, DimensionLabelsAcquisition +from cil.framework import AcquisitionGeometry, AcquisitionDimensionLabels class SimData(object): @@ -138,7 +138,7 @@ def Cone3D(self): ag_test_1 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,0,0])\ .set_panel([16,16],[1,1])\ .set_angles([0]) - ag_test_1.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() @@ -149,7 +149,7 @@ def Cone3D(self): ag_test_2 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,0,0])\ .set_panel([16,16],[2,2])\ .set_angles([0]) - ag_test_2.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() norm_2 = 8 @@ -159,7 +159,7 @@ def Cone3D(self): ag_test_3 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,0,0])\ .set_panel([16,16],[0.5,0.5])\ .set_angles([0]) - ag_test_3.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() norm_3 = 2 @@ -169,7 +169,7 @@ def Cone3D(self): ag_test_4 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,1000,0])\ .set_panel([16,16],[0.5,0.5])\ .set_angles([0]) - ag_test_4.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_4)) + ag_test_4.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_4)) ig_test_4 = ag_test_4.get_ImageGeometry() norm_4 = 1 @@ -185,7 +185,7 @@ def Cone2D(self): ag_test_1 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,0])\ .set_panel(16,1)\ .set_angles([0]) - ag_test_1.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() @@ -196,7 +196,7 @@ def Cone2D(self): ag_test_2 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,0])\ .set_panel(16,2)\ .set_angles([0]) - ag_test_2.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() norm_2 = 8 @@ -206,7 +206,7 @@ def Cone2D(self): ag_test_3 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,0])\ .set_panel(16,0.5)\ .set_angles([0]) - ag_test_3.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() norm_3 = 2 @@ -216,7 +216,7 @@ def Cone2D(self): ag_test_4 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,1000])\ .set_panel(16,0.5)\ .set_angles([0]) - ag_test_4.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_4)) + ag_test_4.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_4)) ig_test_4 = ag_test_4.get_ImageGeometry() norm_4 = 1 @@ -232,7 +232,7 @@ def Parallel3D(self): ag_test_1 = AcquisitionGeometry.create_Parallel3D()\ .set_panel([16,16],[1,1])\ .set_angles([0]) - ag_test_1.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() @@ -242,7 +242,7 @@ def Parallel3D(self): ag_test_2 = AcquisitionGeometry.create_Parallel3D()\ .set_panel([16,16],[2,2])\ .set_angles([0]) - ag_test_2.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() @@ -253,7 +253,7 @@ def Parallel3D(self): ag_test_3 = AcquisitionGeometry.create_Parallel3D()\ .set_panel([16,16],[0.5,0.5])\ .set_angles([0]) - ag_test_3.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() @@ -271,7 +271,7 @@ def Parallel2D(self): .set_panel(16,1)\ .set_angles([0]) - ag_test_1.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() norm_1 = 4 @@ -280,7 +280,7 @@ def Parallel2D(self): ag_test_2 = AcquisitionGeometry.create_Parallel2D()\ .set_panel(16,2)\ .set_angles([0]) - ag_test_2.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() @@ -291,7 +291,7 @@ def Parallel2D(self): ag_test_3 = AcquisitionGeometry.create_Parallel2D()\ .set_panel(16,0.5)\ .set_angles([0]) - ag_test_3.set_labels(DimensionLabelsAcquisition.get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() norm_3 = 2 From e1977c276cf03af181992bade03147ea19272c32 Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Thu, 15 Aug 2024 16:09:12 +0000 Subject: [PATCH 44/72] simplified enum methods by overwriting default eq and contains --- Wrappers/Python/cil/framework/labels.py | 73 ++++++++++--------------- 1 file changed, 28 insertions(+), 45 deletions(-) diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index 740ac0044f..e87dbd081c 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -16,66 +16,49 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from enum import Enum +from enum import Enum, EnumMeta class _LabelsBase(Enum): @classmethod def validate(cls, label): - """ - Validate if the given label is present in the class or its values. - Parameters: - label (str): The label to validate. - Returns: - bool: True if the label is present in the class or its values - Raises: - ValueError: If the label is not present in the class or its values. - """ - if isinstance(label, cls): - return True - elif label in [e.name for e in cls]: - return True - elif label in [e.value for e in cls]: - return True - else: - raise ValueError(f"Expected one of {[e.value for e in cls]}, got {label}") + try: + for member in cls: + if member == label: + return True + except: + pass - @classmethod - def member_from_value(cls, label): - if isinstance(label, str): - label = label.lower() - - for member in cls: - if member.value == label: - return member - raise ValueError(f"{label} is not a valid {cls.__name__}") + raise ValueError(f"Expected one of {[e.value for e in cls]} from {cls.__name__}, got {label}") - @classmethod - def member_from_key(cls, label): - for member in cls: - if member.name == label: - return member - raise ValueError(f"{label} is not a valid {cls.__name__}") - @classmethod def get_enum_member(cls, label): - if isinstance(label, cls): - return label - elif label in [e.name for e in cls]: - return cls.member_from_key(label) - elif label in [e.value for e in cls]: - return cls.member_from_value(label) - else: - raise ValueError(f"{label} is not a valid {cls.__name__}") + try: + for member in cls: + if member == label: + return member + except: + pass + + raise ValueError(f"Expected one of {[e.value for e in cls]} from {cls.__name__}, got {label}") @classmethod def get_enum_value(cls, label): return cls.get_enum_member(label).value def __eq__(self, other): - if isinstance(other, str): - return self.value == other or self.name == other - return super().__eq__(other) + if self.value == other: + return True + return False + + def __contains__(cls, item): + for member in cls: + if member.value == item: + return True + return False + +# Needed for python < 3.12 +EnumMeta.__contains__ = _LabelsBase.__contains__ class Backends(_LabelsBase): ASTRA = "astra" From d8539d811f384b12f8afeb1804ec74406d214de3 Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Thu, 15 Aug 2024 16:09:31 +0000 Subject: [PATCH 45/72] fix case error --- Wrappers/Python/cil/framework/acquisition_geometry.py | 2 +- Wrappers/Python/cil/framework/image_geometry.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index ed8f524939..a6b2ef7cbb 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -1734,7 +1734,7 @@ def shape(self): @property def dimension_labels(self): - labels_default = AcquisitionDimensionLabels.get_default_order_for_engine("CIL") + labels_default = AcquisitionDimensionLabels.get_default_order_for_engine("cil") shape_default = [self.config.channels.num_channels, self.config.angles.num_positions, diff --git a/Wrappers/Python/cil/framework/image_geometry.py b/Wrappers/Python/cil/framework/image_geometry.py index 55cf9f7afc..f01ecb3073 100644 --- a/Wrappers/Python/cil/framework/image_geometry.py +++ b/Wrappers/Python/cil/framework/image_geometry.py @@ -97,7 +97,7 @@ def ndim(self): @property def dimension_labels(self): - labels_default = ImageDimensionLabels.get_default_order_for_engine("CIL") + labels_default = ImageDimensionLabels.get_default_order_for_engine("cil") shape_default = [ self.channels, self.voxel_num_z, From 32a5f2b4825c81c7594f7585949ad10d707cc8e6 Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Thu, 15 Aug 2024 16:09:49 +0000 Subject: [PATCH 46/72] added unit tests for labels.py --- Wrappers/Python/test/test_labels.py | 271 ++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 Wrappers/Python/test/test_labels.py diff --git a/Wrappers/Python/test/test_labels.py b/Wrappers/Python/test/test_labels.py new file mode 100644 index 0000000000..fcd5bf86f5 --- /dev/null +++ b/Wrappers/Python/test/test_labels.py @@ -0,0 +1,271 @@ +# Copyright 2024 United Kingdom Research and Innovation +# Copyright 2024 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt + +import numpy as np + +import unittest + +from cil.framework.labels import (_LabelsBase, + FillTypes, UnitsAngles, + AcquisitionType, AcquisitionDimension, + ImageDimensionLabels, AcquisitionDimensionLabels, Backends) + +from cil.framework import AcquisitionGeometry, ImageGeometry + +class Test_Lables(unittest.TestCase): + + def test_labels_validate(self): + + input_good = ["3D", AcquisitionDimension.DIM3] + input_bad = ["bad_str", "DIM3", UnitsAngles.DEGREE] + + for input in input_good: + self.assertTrue(AcquisitionDimension.validate(input)) + + for input in input_bad: + with self.assertRaises(ValueError): + AcquisitionDimension.validate(input) + + + def test_labels_get_enum_member(self): + out_gold = AcquisitionDimension.DIM3 + + input_good = ["3D", AcquisitionDimension.DIM3] + input_bad = ["bad_str", "DIM3", UnitsAngles.DEGREE] + + for input in input_good: + out = AcquisitionDimension.get_enum_member(input) + self.assertEqual(out, out_gold) + self.assertTrue(isinstance(out, AcquisitionDimension)) + + for input in input_bad: + with self.assertRaises(ValueError): + AcquisitionDimension.get_enum_member(input) + + + def test_labels_get_enum_value(self): + out_gold = "3D" + + input_good = ["3D", AcquisitionDimension.DIM3] + input_bad = ["bad_str", "DIM3", UnitsAngles.DEGREE] + + for input in input_good: + out = AcquisitionDimension.get_enum_value(input) + self.assertEqual(out, out_gold) + self.assertTrue(isinstance(out, str)) + + for input in input_bad: + with self.assertRaises(ValueError): + AcquisitionDimension.get_enum_value(input) + + + def test_labels_eq(self): + self.assertTrue(_LabelsBase.__eq__(AcquisitionDimension.DIM3, "3D")) + self.assertTrue(_LabelsBase.__eq__(AcquisitionDimension.DIM3, AcquisitionDimension.DIM3)) + + self.assertFalse(_LabelsBase.__eq__(AcquisitionDimension.DIM3, "DIM3")) + self.assertFalse(_LabelsBase.__eq__(AcquisitionDimension.DIM3, "2D")) + self.assertFalse(_LabelsBase.__eq__(AcquisitionDimension.DIM3, AcquisitionDimension.DIM2)) + self.assertFalse(_LabelsBase.__eq__(AcquisitionDimension.DIM3, AcquisitionDimension)) + + + def test_labels_contains(self): + self.assertTrue(_LabelsBase.__contains__(AcquisitionDimension, "3D")) + self.assertTrue(_LabelsBase.__contains__(AcquisitionDimension, AcquisitionDimension.DIM3)) + self.assertTrue(_LabelsBase.__contains__(AcquisitionDimension, AcquisitionDimension.DIM2)) + + self.assertFalse(_LabelsBase.__contains__(AcquisitionDimension, "DIM3")) + self.assertFalse(_LabelsBase.__contains__(AcquisitionDimension, AcquisitionDimension)) + + + def test_backends(self): + self.assertTrue('astra' in Backends) + self.assertTrue('cil' in Backends) + self.assertTrue('tigre' in Backends) + self.assertTrue(Backends.ASTRA in Backends) + self.assertTrue(Backends.CIL in Backends) + self.assertTrue(Backends.TIGRE in Backends) + + def test_fill_types(self): + self.assertTrue('random' in FillTypes) + self.assertTrue('random_int' in FillTypes) + self.assertTrue(FillTypes.RANDOM in FillTypes) + self.assertTrue(FillTypes.RANDOM_INT in FillTypes) + + def test_units_angles(self): + self.assertTrue('degree' in UnitsAngles) + self.assertTrue('radian' in UnitsAngles) + self.assertTrue(UnitsAngles.DEGREE in UnitsAngles) + self.assertTrue(UnitsAngles.RADIAN in UnitsAngles) + + def test_acquisition_type(self): + self.assertTrue('parallel' in AcquisitionType) + self.assertTrue('cone' in AcquisitionType) + self.assertTrue(AcquisitionType.PARALLEL in AcquisitionType) + self.assertTrue(AcquisitionType.CONE in AcquisitionType) + + def test_acquisition_dimension(self): + self.assertTrue('2D' in AcquisitionDimension) + self.assertTrue('3D' in AcquisitionDimension) + self.assertTrue(AcquisitionDimension.DIM2 in AcquisitionDimension) + self.assertTrue(AcquisitionDimension.DIM3 in AcquisitionDimension) + + def test_image_dimension_labels(self): + self.assertTrue('channel' in ImageDimensionLabels) + self.assertTrue('vertical' in ImageDimensionLabels) + self.assertTrue('horizontal_x' in ImageDimensionLabels) + self.assertTrue('horizontal_y' in ImageDimensionLabels) + self.assertTrue(ImageDimensionLabels.CHANNEL in ImageDimensionLabels) + self.assertTrue(ImageDimensionLabels.VERTICAL in ImageDimensionLabels) + self.assertTrue(ImageDimensionLabels.HORIZONTAL_X in ImageDimensionLabels) + self.assertTrue(ImageDimensionLabels.HORIZONTAL_Y in ImageDimensionLabels) + + def test_image_dimension_labels_default_order(self): + + order_gold = [ImageDimensionLabels.CHANNEL, ImageDimensionLabels.VERTICAL, ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X] + + order = ImageDimensionLabels.get_default_order_for_engine("cil") + self.assertEqual(order,order_gold ) + + order = ImageDimensionLabels.get_default_order_for_engine("tigre") + self.assertEqual(order,order_gold) + + order = ImageDimensionLabels.get_default_order_for_engine("astra") + self.assertEqual(order, order_gold) + + with self.assertRaises(ValueError): + order = AcquisitionDimensionLabels.get_default_order_for_engine("bad_engine") + + + def test_image_dimension_labels_get_order(self): + ig = ImageGeometry(4, 8, 1, channels=2) + ig.set_labels(['channel', 'horizontal_y', 'horizontal_x']) + + # for 2D all engines have the same order + order_gold = [ImageDimensionLabels.CHANNEL, ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X] + order = ImageDimensionLabels.get_order_for_engine("cil", ig) + self.assertEqual(order, order_gold) + + order = ImageDimensionLabels.get_order_for_engine("tigre", ig) + self.assertEqual(order, order_gold) + + order = ImageDimensionLabels.get_order_for_engine("astra", ig) + self.assertEqual(order, order_gold) + + def test_image_dimension_labels_check_order(self): + ig = ImageGeometry(4, 8, 1, channels=2) + ig.set_labels(['horizontal_x', 'horizontal_y', 'channel']) + + with self.assertRaises(ValueError): + ImageDimensionLabels.check_order_for_engine("cil", ig) + + with self.assertRaises(ValueError): + ImageDimensionLabels.check_order_for_engine("tigre", ig) + + with self.assertRaises(ValueError): + ImageDimensionLabels.check_order_for_engine("astra", ig) + + ig.set_labels(['channel', 'horizontal_y', 'horizontal_x']) + self.assertTrue( ImageDimensionLabels.check_order_for_engine("cil", ig)) + self.assertTrue( ImageDimensionLabels.check_order_for_engine("tigre", ig)) + self.assertTrue( ImageDimensionLabels.check_order_for_engine("astra", ig)) + + def test_acquisition_dimension_labels(self): + self.assertTrue('channel' in AcquisitionDimensionLabels) + self.assertTrue('angle' in AcquisitionDimensionLabels) + self.assertTrue('vertical' in AcquisitionDimensionLabels) + self.assertTrue('horizontal' in AcquisitionDimensionLabels) + self.assertTrue(AcquisitionDimensionLabels.CHANNEL in AcquisitionDimensionLabels) + self.assertTrue(AcquisitionDimensionLabels.ANGLE in AcquisitionDimensionLabels) + self.assertTrue(AcquisitionDimensionLabels.VERTICAL in AcquisitionDimensionLabels) + self.assertTrue(AcquisitionDimensionLabels.HORIZONTAL in AcquisitionDimensionLabels) + + def test_acquisition_dimension_labels_default_order(self): + order = AcquisitionDimensionLabels.get_default_order_for_engine("cil") + self.assertEqual(order, [AcquisitionDimensionLabels.CHANNEL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.VERTICAL, AcquisitionDimensionLabels.HORIZONTAL]) + + order = AcquisitionDimensionLabels.get_default_order_for_engine("tigre") + self.assertEqual(order, [AcquisitionDimensionLabels.CHANNEL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.VERTICAL, AcquisitionDimensionLabels.HORIZONTAL]) + + order = AcquisitionDimensionLabels.get_default_order_for_engine("astra") + self.assertEqual(order, [AcquisitionDimensionLabels.CHANNEL, AcquisitionDimensionLabels.VERTICAL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.HORIZONTAL]) + + with self.assertRaises(ValueError): + order = AcquisitionDimensionLabels.get_default_order_for_engine("bad_engine") + + def test_acquisition_dimension_labels_get_order(self): + + ag = AcquisitionGeometry.create_Parallel2D()\ + .set_angles(np.arange(0,16 , 1), angle_unit="degree")\ + .set_panel(4)\ + .set_channels(8)\ + .set_labels(['angle', 'horizontal', 'channel']) + + # for 2D all engines have the same order + order_gold = [AcquisitionDimensionLabels.CHANNEL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.HORIZONTAL] + order = AcquisitionDimensionLabels.get_order_for_engine("cil", ag) + self.assertEqual(order, order_gold) + + order = AcquisitionDimensionLabels.get_order_for_engine("tigre", ag) + self.assertEqual(order, order_gold) + + order = AcquisitionDimensionLabels.get_order_for_engine("astra", ag) + self.assertEqual(order, order_gold) + + + ag = AcquisitionGeometry.create_Parallel3D()\ + .set_angles(np.arange(0,16 , 1), angle_unit="degree")\ + .set_panel((4,2))\ + .set_labels(['angle', 'horizontal', 'vertical']) + + + order_gold = [AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.VERTICAL, AcquisitionDimensionLabels.HORIZONTAL] + order = AcquisitionDimensionLabels.get_order_for_engine("cil", ag) + self.assertEqual(order, order_gold) + + order = AcquisitionDimensionLabels.get_order_for_engine("tigre", ag) + self.assertEqual(order, order_gold) + + order_gold = [AcquisitionDimensionLabels.VERTICAL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.HORIZONTAL] + order = AcquisitionDimensionLabels.get_order_for_engine("astra", ag) + self.assertEqual(order, order_gold) + + + def test_acquisition_dimension_labels_check_order(self): + + ag = AcquisitionGeometry.create_Parallel3D()\ + .set_angles(np.arange(0,16 , 1), angle_unit="degree")\ + .set_panel((8,4))\ + .set_channels(2)\ + .set_labels(['angle', 'horizontal', 'channel', 'vertical']) + + with self.assertRaises(ValueError): + AcquisitionDimensionLabels.check_order_for_engine("cil", ag) + + with self.assertRaises(ValueError): + AcquisitionDimensionLabels.check_order_for_engine("tigre", ag) + + with self.assertRaises(ValueError): + AcquisitionDimensionLabels.check_order_for_engine("astra", ag) + + ag.set_labels(['channel', 'angle', 'vertical', 'horizontal']) + self.assertTrue( AcquisitionDimensionLabels.check_order_for_engine("cil", ag)) + self.assertTrue( AcquisitionDimensionLabels.check_order_for_engine("tigre", ag)) + + ag.set_labels(['channel', 'vertical', 'angle', 'horizontal']) + self.assertTrue( AcquisitionDimensionLabels.check_order_for_engine("astra", ag)) From e82d134f15a585f8e28d7ccd5b115f562595e911 Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Thu, 15 Aug 2024 16:14:33 +0000 Subject: [PATCH 47/72] renamed to AcquisitionTypes and AcquisitionDimensions for consistency --- Wrappers/Python/cil/framework/__init__.py | 2 +- .../cil/framework/acquisition_geometry.py | 22 +++---- Wrappers/Python/cil/framework/labels.py | 4 +- Wrappers/Python/cil/recon/FBP.py | 6 +- Wrappers/Python/test/test_labels.py | 62 +++++++++---------- 5 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 8388061b94..ab99ffced3 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -27,4 +27,4 @@ from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer from .block import BlockDataContainer, BlockGeometry from .partitioner import Partitioner -from .labels import AcquisitionDimensionLabels, ImageDimensionLabels, FillTypes, UnitsAngles, AcquisitionType, AcquisitionDimension +from .labels import AcquisitionDimensionLabels, ImageDimensionLabels, FillTypes, UnitsAngles, AcquisitionTypes, AcquisitionDimensions diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index a6b2ef7cbb..2a06c02510 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -22,7 +22,7 @@ import numpy -from .labels import AcquisitionDimensionLabels, UnitsAngles, AcquisitionType, FillTypes, AcquisitionDimension +from .labels import AcquisitionDimensionLabels, UnitsAngles, AcquisitionTypes, FillTypes, AcquisitionDimensions from .acquisition_data import AcquisitionData from .image_geometry import ImageGeometry @@ -187,9 +187,9 @@ class SystemConfiguration(object): @property def dimension(self): if self._dimension == 2: - return AcquisitionDimension.DIM2.value + return AcquisitionDimensions.DIM2.value else: - return AcquisitionDimension.DIM3.value + return AcquisitionDimensions.DIM3.value @dimension.setter def dimension(self,val): @@ -204,8 +204,8 @@ def geometry(self): @geometry.setter def geometry(self,val): - AcquisitionType.validate(val) - self._geometry = AcquisitionType.get_enum_member(val) + AcquisitionTypes.validate(val) + self._geometry = AcquisitionTypes.get_enum_member(val) def __init__(self, dof, geometry, units='units'): """Initialises the system component attributes for the acquisition type @@ -214,7 +214,7 @@ def __init__(self, dof, geometry, units='units'): self.geometry = geometry self.units = units - if self.geometry == AcquisitionType.PARALLEL: + if self.geometry == AcquisitionTypes.PARALLEL: self.ray = DirectionVector(dof) else: self.source = PositionVector(dof) @@ -345,7 +345,7 @@ class Parallel2D(SystemConfiguration): def __init__ (self, ray_direction, detector_pos, detector_direction_x, rotation_axis_pos, units='units'): """Constructor method """ - super(Parallel2D, self).__init__(dof=2, geometry = AcquisitionType.PARALLEL, units=units) + super(Parallel2D, self).__init__(dof=2, geometry = AcquisitionTypes.PARALLEL, units=units) #source self.ray.direction = ray_direction @@ -519,7 +519,7 @@ class Parallel3D(SystemConfiguration): def __init__ (self, ray_direction, detector_pos, detector_direction_x, detector_direction_y, rotation_axis_pos, rotation_axis_direction, units='units'): """Constructor method """ - super(Parallel3D, self).__init__(dof=3, geometry = AcquisitionType.PARALLEL, units=units) + super(Parallel3D, self).__init__(dof=3, geometry = AcquisitionTypes.PARALLEL, units=units) #source self.ray.direction = ray_direction @@ -804,7 +804,7 @@ class Cone2D(SystemConfiguration): def __init__ (self, source_pos, detector_pos, detector_direction_x, rotation_axis_pos, units='units'): """Constructor method """ - super(Cone2D, self).__init__(dof=2, geometry = AcquisitionType.CONE, units=units) + super(Cone2D, self).__init__(dof=2, geometry = AcquisitionTypes.CONE, units=units) #source self.source.position = source_pos @@ -983,7 +983,7 @@ class Cone3D(SystemConfiguration): def __init__ (self, source_pos, detector_pos, detector_direction_x, detector_direction_y, rotation_axis_pos, rotation_axis_direction, units='units'): """Constructor method """ - super(Cone3D, self).__init__(dof=3, geometry = AcquisitionType.CONE, units=units) + super(Cone3D, self).__init__(dof=3, geometry = AcquisitionTypes.CONE, units=units) #source self.source.position = source_pos @@ -2175,7 +2175,7 @@ def get_slice(self, channel=None, angle=None, vertical=None, horizontal=None): geometry_new.config.angles.angle_data = geometry_new.config.angles.angle_data[angle] if vertical is not None: - if geometry_new.geom_type == AcquisitionType.PARALLEL or vertical == 'centre' or abs(geometry_new.pixel_num_v/2 - vertical) < 1e-6: + if geometry_new.geom_type == AcquisitionTypes.PARALLEL or vertical == 'centre' or abs(geometry_new.pixel_num_v/2 - vertical) < 1e-6: geometry_new = geometry_new.get_centre_slice() else: raise ValueError("Can only subset centre slice geometry on cone-beam data. Expected vertical = 'centre'. Got vertical = {0}".format(vertical)) diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index e87dbd081c..0c4c5b9974 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -147,11 +147,11 @@ class UnitsAngles(_LabelsBase): DEGREE = "degree" RADIAN = "radian" -class AcquisitionType(_LabelsBase): +class AcquisitionTypes(_LabelsBase): PARALLEL = "parallel" CONE = "cone" -class AcquisitionDimension(_LabelsBase): +class AcquisitionDimensions(_LabelsBase): DIM2 = "2D" DIM3 = "3D" diff --git a/Wrappers/Python/cil/recon/FBP.py b/Wrappers/Python/cil/recon/FBP.py index 913b94acaa..c9392d7eba 100644 --- a/Wrappers/Python/cil/recon/FBP.py +++ b/Wrappers/Python/cil/recon/FBP.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import cilacc -from cil.framework import AcquisitionType +from cil.framework import AcquisitionTypes from cil.recon import Reconstructor from scipy.fft import fftfreq @@ -376,7 +376,7 @@ def __init__ (self, input, image_geometry=None, filter='ram-lak'): #call parent initialiser super().__init__(input, image_geometry, filter, backend='tigre') - if input.geometry.geom_type != AcquisitionType.CONE: + if input.geometry.geom_type != AcquisitionTypes.CONE: raise TypeError("This reconstructor is for cone-beam data only.") @@ -485,7 +485,7 @@ def __init__ (self, input, image_geometry=None, filter='ram-lak', backend='tigre super().__init__(input, image_geometry, filter, backend) self.set_split_processing(False) - if input.geometry.geom_type != AcquisitionType.PARALLEL: + if input.geometry.geom_type != AcquisitionTypes.PARALLEL: raise TypeError("This reconstructor is for parallel-beam data only.") diff --git a/Wrappers/Python/test/test_labels.py b/Wrappers/Python/test/test_labels.py index fcd5bf86f5..9506363ce1 100644 --- a/Wrappers/Python/test/test_labels.py +++ b/Wrappers/Python/test/test_labels.py @@ -22,7 +22,7 @@ from cil.framework.labels import (_LabelsBase, FillTypes, UnitsAngles, - AcquisitionType, AcquisitionDimension, + AcquisitionTypes, AcquisitionDimensions, ImageDimensionLabels, AcquisitionDimensionLabels, Backends) from cil.framework import AcquisitionGeometry, ImageGeometry @@ -31,66 +31,66 @@ class Test_Lables(unittest.TestCase): def test_labels_validate(self): - input_good = ["3D", AcquisitionDimension.DIM3] + input_good = ["3D", AcquisitionDimensions.DIM3] input_bad = ["bad_str", "DIM3", UnitsAngles.DEGREE] for input in input_good: - self.assertTrue(AcquisitionDimension.validate(input)) + self.assertTrue(AcquisitionDimensions.validate(input)) for input in input_bad: with self.assertRaises(ValueError): - AcquisitionDimension.validate(input) + AcquisitionDimensions.validate(input) def test_labels_get_enum_member(self): - out_gold = AcquisitionDimension.DIM3 + out_gold = AcquisitionDimensions.DIM3 - input_good = ["3D", AcquisitionDimension.DIM3] + input_good = ["3D", AcquisitionDimensions.DIM3] input_bad = ["bad_str", "DIM3", UnitsAngles.DEGREE] for input in input_good: - out = AcquisitionDimension.get_enum_member(input) + out = AcquisitionDimensions.get_enum_member(input) self.assertEqual(out, out_gold) - self.assertTrue(isinstance(out, AcquisitionDimension)) + self.assertTrue(isinstance(out, AcquisitionDimensions)) for input in input_bad: with self.assertRaises(ValueError): - AcquisitionDimension.get_enum_member(input) + AcquisitionDimensions.get_enum_member(input) def test_labels_get_enum_value(self): out_gold = "3D" - input_good = ["3D", AcquisitionDimension.DIM3] + input_good = ["3D", AcquisitionDimensions.DIM3] input_bad = ["bad_str", "DIM3", UnitsAngles.DEGREE] for input in input_good: - out = AcquisitionDimension.get_enum_value(input) + out = AcquisitionDimensions.get_enum_value(input) self.assertEqual(out, out_gold) self.assertTrue(isinstance(out, str)) for input in input_bad: with self.assertRaises(ValueError): - AcquisitionDimension.get_enum_value(input) + AcquisitionDimensions.get_enum_value(input) def test_labels_eq(self): - self.assertTrue(_LabelsBase.__eq__(AcquisitionDimension.DIM3, "3D")) - self.assertTrue(_LabelsBase.__eq__(AcquisitionDimension.DIM3, AcquisitionDimension.DIM3)) + self.assertTrue(_LabelsBase.__eq__(AcquisitionDimensions.DIM3, "3D")) + self.assertTrue(_LabelsBase.__eq__(AcquisitionDimensions.DIM3, AcquisitionDimensions.DIM3)) - self.assertFalse(_LabelsBase.__eq__(AcquisitionDimension.DIM3, "DIM3")) - self.assertFalse(_LabelsBase.__eq__(AcquisitionDimension.DIM3, "2D")) - self.assertFalse(_LabelsBase.__eq__(AcquisitionDimension.DIM3, AcquisitionDimension.DIM2)) - self.assertFalse(_LabelsBase.__eq__(AcquisitionDimension.DIM3, AcquisitionDimension)) + self.assertFalse(_LabelsBase.__eq__(AcquisitionDimensions.DIM3, "DIM3")) + self.assertFalse(_LabelsBase.__eq__(AcquisitionDimensions.DIM3, "2D")) + self.assertFalse(_LabelsBase.__eq__(AcquisitionDimensions.DIM3, AcquisitionDimensions.DIM2)) + self.assertFalse(_LabelsBase.__eq__(AcquisitionDimensions.DIM3, AcquisitionDimensions)) def test_labels_contains(self): - self.assertTrue(_LabelsBase.__contains__(AcquisitionDimension, "3D")) - self.assertTrue(_LabelsBase.__contains__(AcquisitionDimension, AcquisitionDimension.DIM3)) - self.assertTrue(_LabelsBase.__contains__(AcquisitionDimension, AcquisitionDimension.DIM2)) + self.assertTrue(_LabelsBase.__contains__(AcquisitionDimensions, "3D")) + self.assertTrue(_LabelsBase.__contains__(AcquisitionDimensions, AcquisitionDimensions.DIM3)) + self.assertTrue(_LabelsBase.__contains__(AcquisitionDimensions, AcquisitionDimensions.DIM2)) - self.assertFalse(_LabelsBase.__contains__(AcquisitionDimension, "DIM3")) - self.assertFalse(_LabelsBase.__contains__(AcquisitionDimension, AcquisitionDimension)) + self.assertFalse(_LabelsBase.__contains__(AcquisitionDimensions, "DIM3")) + self.assertFalse(_LabelsBase.__contains__(AcquisitionDimensions, AcquisitionDimensions)) def test_backends(self): @@ -114,16 +114,16 @@ def test_units_angles(self): self.assertTrue(UnitsAngles.RADIAN in UnitsAngles) def test_acquisition_type(self): - self.assertTrue('parallel' in AcquisitionType) - self.assertTrue('cone' in AcquisitionType) - self.assertTrue(AcquisitionType.PARALLEL in AcquisitionType) - self.assertTrue(AcquisitionType.CONE in AcquisitionType) + self.assertTrue('parallel' in AcquisitionTypes) + self.assertTrue('cone' in AcquisitionTypes) + self.assertTrue(AcquisitionTypes.PARALLEL in AcquisitionTypes) + self.assertTrue(AcquisitionTypes.CONE in AcquisitionTypes) def test_acquisition_dimension(self): - self.assertTrue('2D' in AcquisitionDimension) - self.assertTrue('3D' in AcquisitionDimension) - self.assertTrue(AcquisitionDimension.DIM2 in AcquisitionDimension) - self.assertTrue(AcquisitionDimension.DIM3 in AcquisitionDimension) + self.assertTrue('2D' in AcquisitionDimensions) + self.assertTrue('3D' in AcquisitionDimensions) + self.assertTrue(AcquisitionDimensions.DIM2 in AcquisitionDimensions) + self.assertTrue(AcquisitionDimensions.DIM3 in AcquisitionDimensions) def test_image_dimension_labels(self): self.assertTrue('channel' in ImageDimensionLabels) From 025c739f555753633d783fc0776791062c1cae53 Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Thu, 15 Aug 2024 16:48:10 +0000 Subject: [PATCH 48/72] added doc strings --- Wrappers/Python/cil/framework/labels.py | 329 ++++++++++++++++++++++-- 1 file changed, 303 insertions(+), 26 deletions(-) diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index 0c4c5b9974..c21d0fcef6 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -19,53 +19,155 @@ from enum import Enum, EnumMeta class _LabelsBase(Enum): + """ + Base class for labels enumeration. + + Methods: + -------- + - validate(label): Validates if the given label is a valid member of the enumeration. + - get_enum_member(label): Returns the enum member corresponding to the given label. + - get_enum_value(label): Returns the value of the enum member corresponding to the given label. + - __eq__(other): Checks if the enum value is equal to the given value. + - __contains__(item): Checks if the enum contains the given value. + """ @classmethod def validate(cls, label): + """ + Validates if the given label is a valid member of the specified class. + + Parameters: + ---------- + label : str, enum + The label to validate. + + Raises: + ------- + ValueError + If the label is not a valid member of the class. + + Returns: + -------- + bool + True if the label is a valid member of the class, False otherwise. + """ + try: for member in cls: if member == label: - return True - except: + return True + except AttributeError: pass - raise ValueError(f"Expected one of {[e.value for e in cls]} from {cls.__name__}, got {label}") + raise ValueError(f"Expected one of {[e.value for e in cls]} \ + from {cls.__name__}, got {label}") + @classmethod def get_enum_member(cls, label): + """ + Returns the enum member corresponding to the given label. + + Parameters: + ---------- + label : str + The label to get the corresponding enum member. + + Raises: + ------- + ValueError + If the label is not a valid member of the class. + + Returns: + -------- + Enum + The enum member corresponding to the given label. + """ + try: for member in cls: if member == label: - return member - except: + return member + except AttributeError: pass - - raise ValueError(f"Expected one of {[e.value for e in cls]} from {cls.__name__}, got {label}") - + + raise ValueError(f"Expected one of {[e.value for e in cls]} \ + from {cls.__name__}, got {label}") + + @classmethod def get_enum_value(cls, label): + """ + Returns the value of the enum member corresponding to the given label. + + Parameters: + ---------- + label : str + The enum member to get the corresponding enum value. + + Raises: + ------- + ValueError + If the label is not a valid member of the class. + + Returns: + -------- + str + The value of the enum member corresponding to the given label. + """ + return cls.get_enum_member(label).value - + + def __eq__(self, other): if self.value == other: return True return False - - def __contains__(cls, item): - for member in cls: + + def __contains__(self, item): + for member in self: if member.value == item: return True return False - + # Needed for python < 3.12 EnumMeta.__contains__ = _LabelsBase.__contains__ class Backends(_LabelsBase): + """ + Available backends for CIL. + + Attributes + ---------- + ASTRA ('astra'): The ASTRA toolbox. + TIGRE ('tigre'): The TIGRE toolbox. + CIL ('cil'): Native CIL implementation. + + Examples + -------- + FBP(data, backend=Backends.ASTRA) + FBP(data, backend="astra") + """ ASTRA = "astra" TIGRE = "tigre" CIL = "cil" class ImageDimensionLabels(_LabelsBase): + """ + Available dimension labels for image data. + + Attributes + ---------- + CHANNEL ('channel'): The channel dimension. + VERTICAL ('vertical'): The vertical dimension. + HORIZONTAL_X ('horizontal_x'): The horizontal dimension in x. + HORIZONTAL_Y ('horizontal_y'): The horizontal dimension in y. + + Examples + -------- + data.reorder([ImageDimensionLabels.HORIZONTAL_X, ImageDimensionLabels.VERTICAL]) + data.reorder(["horizontal_x", "vertical"]) + """ CHANNEL = "channel" VERTICAL = "vertical" HORIZONTAL_X = "horizontal_x" @@ -73,10 +175,30 @@ class ImageDimensionLabels(_LabelsBase): @classmethod def get_default_order_for_engine(cls, engine): + """ + Returns the default dimension order for the given engine. + + Parameters + ---------- + engine : str + The engine to get the default dimension order for. + + Returns + ------- + list + The default dimension order for the given engine. + + Raises + ------ + ValueError + If the engine is not a valid member of the Backends enumeration + """ + order = [cls.CHANNEL.value, cls.VERTICAL.value, \ + cls.HORIZONTAL_Y.value, cls.HORIZONTAL_X.value] engine_orders = { - Backends.ASTRA.value: [cls.CHANNEL.value, cls.VERTICAL.value, cls.HORIZONTAL_Y.value, cls.HORIZONTAL_X.value], - Backends.TIGRE.value: [cls.CHANNEL.value, cls.VERTICAL.value, cls.HORIZONTAL_Y.value, cls.HORIZONTAL_X.value], - Backends.CIL.value: [cls.CHANNEL.value, cls.VERTICAL.value, cls.HORIZONTAL_Y.value, cls.HORIZONTAL_X.value] + Backends.ASTRA.value: order, + Backends.TIGRE.value: order, + Backends.CIL.value: order } Backends.validate(engine) engine = Backends.get_enum_value(engine) @@ -85,6 +207,20 @@ def get_default_order_for_engine(cls, engine): @classmethod def get_order_for_engine(cls, engine, geometry): + """ + Returns the order of dimensions for a specific engine and geometry. + + Parameters: + ---------- + engine : str + The engine name. + geometry : ImageGeometry + + Returns: + -------- + list + The order of dimensions for the given engine and geometry. + """ dim_order = cls.get_default_order_for_engine(engine) dimensions = [label for label in dim_order if label in geometry.dimension_labels ] @@ -93,16 +229,55 @@ def get_order_for_engine(cls, engine, geometry): @classmethod def check_order_for_engine(cls, engine, geometry): + """ + Checks if the order of dimensions is correct for a specific engine and geometry. + + Parameters: + ---------- + engine : str + The engine name. + geometry : ImageGeometry + The geometry object. + + Returns: + -------- + bool + True if the order of dimensions is correct, False otherwise. + + Raises: + ------- + ValueError + If the order of dimensions is incorrect. + """ order_requested = cls.get_order_for_engine(engine, geometry) if order_requested == list(geometry.dimension_labels): return True else: raise ValueError( - "Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" - .format(order_requested, list(geometry.dimension_labels), engine)) - + f"Expected dimension_label order {order_requested}, \ + got {list(geometry.dimension_labels)}.\n\ + Try using `data.reorder('{engine}')` to permute for {engine}") + class AcquisitionDimensionLabels(_LabelsBase): + """ + Available dimension labels for acquisition data. + + Attributes + ---------- + CHANNEL ('channel'): The channel dimension. + ANGLE ('angle'): The angle dimension. + VERTICAL ('vertical'): The vertical dimension. + HORIZONTAL ('horizontal'): The horizontal dimension. + + Examples + -------- + data.reorder([AcquisitionDimensionLabels.CHANNEL, + AcquisitionDimensionLabels.ANGLE, + AcquisitionDimensionLabels.HORIZONTAL]) + data.reorder(["channel", "angle", "horizontal"]) + """ + CHANNEL = "channel" ANGLE = "angle" VERTICAL = "vertical" @@ -110,10 +285,32 @@ class AcquisitionDimensionLabels(_LabelsBase): @classmethod def get_default_order_for_engine(cls, engine): + """ + Returns the default dimension order for the given engine. + + Parameters + ---------- + engine : str + The engine to get the default dimension order for. + + Returns + ------- + list + The default dimension order for the given engine. + + Raises + ------ + ValueError + If the engine is not a valid member of the Backends enumeration + """ + engine_orders = { - Backends.ASTRA.value: [cls.CHANNEL.value, cls.VERTICAL.value, cls.ANGLE.value, cls.HORIZONTAL.value], - Backends.TIGRE.value: [cls.CHANNEL.value, cls.ANGLE.value, cls.VERTICAL.value, cls.HORIZONTAL.value], - Backends.CIL.value: [cls.CHANNEL.value, cls.ANGLE.value, cls.VERTICAL.value, cls.HORIZONTAL.value] + Backends.ASTRA.value: [cls.CHANNEL.value, cls.VERTICAL.value, \ + cls.ANGLE.value, cls.HORIZONTAL.value], + Backends.TIGRE.value: [cls.CHANNEL.value, cls.ANGLE.value, \ + cls.VERTICAL.value, cls.HORIZONTAL.value], + Backends.CIL.value: [cls.CHANNEL.value, cls.ANGLE.value, \ + cls.VERTICAL.value, cls.HORIZONTAL.value] } Backends.validate(engine) engine = Backends.get_enum_value(engine) @@ -122,6 +319,20 @@ def get_default_order_for_engine(cls, engine): @classmethod def get_order_for_engine(cls, engine, geometry): + """ + Returns the order of dimensions for a specific engine and geometry. + + Parameters: + ---------- + engine : str + The engine name. + geometry : AcquisitionGeometry + + Returns: + -------- + list + The order of dimensions for the given engine and geometry. + """ dim_order = cls.get_default_order_for_engine(engine) dimensions = [label for label in dim_order if label in geometry.dimension_labels ] @@ -130,28 +341,94 @@ def get_order_for_engine(cls, engine, geometry): @classmethod def check_order_for_engine(cls, engine, geometry): + """ + Checks if the order of dimensions is correct for a specific engine and geometry. + + Parameters: + ---------- + engine : str + The engine name. + geometry : AcquisitionGeometry + + Returns: + -------- + bool + True if the order of dimensions is correct, False otherwise. + + Raises: + ------- + ValueError + If the order of dimensions is incorrect. + """ + order_requested = cls.get_order_for_engine(engine, geometry) if order_requested == list(geometry.dimension_labels): return True else: raise ValueError( - "Expected dimension_label order {0}, got {1}.\nTry using `data.reorder('{2}')` to permute for {2}" - .format(order_requested, list(geometry.dimension_labels), engine)) - + f"Expected dimension_label order {order_requested}, \ + got {list(geometry.dimension_labels)}.\n\ + Try using `data.reorder('{engine}')` to permute for {engine}") + class FillTypes(_LabelsBase): + """ + Available fill types for image data. + + Attributes + ---------- + RANDOM ('random'): Fill with random values. + RANDOM_INT ('random_int'): Fill with random integers. + + Examples + -------- + data.fill(FillTypes.random) + data.fill("random") + """ + RANDOM = "random" RANDOM_INT = "random_int" class UnitsAngles(_LabelsBase): + """ + Available units for angles. + + Attributes + ---------- + DEGREE ('degree'): Degrees. + RADIAN ('radian'): Radians. + + Examples + -------- + data.geometry.set_unitangles(angle_data, angle_units=UnitsAngles.DEGREE) + data.geometry.set_unit(angle_data, angle_units="degree") + """ + DEGREE = "degree" RADIAN = "radian" class AcquisitionTypes(_LabelsBase): + """ + Available acquisition types. + + Attributes + ---------- + PARALLEL ('parallel'): Parallel beam. + CONE ('cone'): Cone beam. + """ + PARALLEL = "parallel" CONE = "cone" class AcquisitionDimensions(_LabelsBase): + """ + Available acquisition dimensions. + + Attributes + ---------- + DIM2 ('2D'): 2D acquisition. + DIM3 ('3D'): 3D acquisition. + """ + DIM2 = "2D" DIM3 = "3D" - From 35f2dff9f374dd21c4da6c23a272f6ef53963bcc Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Thu, 15 Aug 2024 16:59:11 +0000 Subject: [PATCH 49/72] updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c93df735..efc533cd71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ - New Features: - Added SAG and SAGA stochastic functions (#1624) - Allow `SumFunction` with 1 item (#1857) + - Added `labels` module with `ImageDimensionLabels`, `AcquisitionDimensionLabels`,`AcquisitionDimensions`, `AcquisitionTypes`, `UnitsAngles`, `FillTypes`. (#1692) - Enhancements: - Use ravel instead of flat in KullbackLeibler numba backend (#1874) - Upgrade Python wrapper (#1873, #1875) + - Internal refactor: Replaced string-based label checks with enum-based checks for improved type safety and consistency. (#1692) * 24.1.0 - New Features: From a6683a9cc835a5781f923b929503a4048d8fdd44 Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Fri, 16 Aug 2024 11:05:58 +0000 Subject: [PATCH 50/72] Simplification of enum useage --- .../Python/cil/framework/acquisition_data.py | 7 +- .../cil/framework/acquisition_geometry.py | 28 +-- Wrappers/Python/cil/framework/image_data.py | 7 +- .../Python/cil/framework/image_geometry.py | 14 +- Wrappers/Python/cil/framework/labels.py | 205 +++--------------- .../Python/cil/framework/vector_geometry.py | 12 +- 6 files changed, 61 insertions(+), 212 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index 54ad3bd873..2f67f931dc 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -111,10 +111,7 @@ def reorder(self, order=None): :type order: list, sting ''' - try: - Backends.validate(order) - order = AcquisitionDimensionLabels.get_order_for_engine(order, self.geometry) - except ValueError: - pass + if order in Backends : + order = AcquisitionDimensionLabels.get_order_for_engine(order, self.geometry) super(AcquisitionData, self).reorder(order) \ No newline at end of file diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 2a06c02510..0e8cbccceb 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -204,8 +204,7 @@ def geometry(self): @geometry.setter def geometry(self,val): - AcquisitionTypes.validate(val) - self._geometry = AcquisitionTypes.get_enum_member(val) + self._geometry = AcquisitionTypes(val) def __init__(self, dof, geometry, units='units'): """Initialises the system component attributes for the acquisition type @@ -1469,8 +1468,7 @@ def angle_unit(self): @angle_unit.setter def angle_unit(self,val): - UnitsAngles.validate(val) - self._angle_unit = UnitsAngles.get_enum_member(val) + self._angle_unit = UnitsAngles(val) def __str__(self): repres = "Acquisition description:\n" @@ -1761,13 +1759,8 @@ def dimension_labels(self): @dimension_labels.setter def dimension_labels(self, val): - if val is not None: - label_new=[] - for x in val: - if AcquisitionDimensionLabels.validate(x): - label_new.append(AcquisitionDimensionLabels.get_enum_value(x)) - + label_new=[AcquisitionDimensionLabels(x).value for x in val if x in AcquisitionDimensionLabels] self._dimension_labels = tuple(label_new) @property @@ -1839,8 +1832,7 @@ def get_centre_of_rotation(self, distance_units='default', angle_units='radian') else: raise ValueError("`distance_units` is not recognised. Must be 'default' or 'pixels'. Got {}".format(distance_units)) - UnitsAngles.validate(angle_units) - angle_units = UnitsAngles.get_enum_member(angle_units) + angle_units = UnitsAngles(angle_units) angle = angle_rad if angle_units == UnitsAngles.DEGREE: @@ -1884,8 +1876,7 @@ def set_centre_of_rotation(self, offset=0.0, distance_units='default', angle=0.0 raise NotImplementedError() - UnitsAngles.validate(angle_units) - angle_units = UnitsAngles.get_enum_member(angle_units) + angle_units = UnitsAngles(angle_units) angle_rad = angle if angle_units == UnitsAngles.DEGREE: @@ -2205,9 +2196,7 @@ def allocate(self, value=0, **kwargs): if isinstance(value, Number): # it's created empty, so we make it 0 out.array.fill(value) - elif value is not None: - FillTypes.validate(value) - + elif value in FillTypes: if value == FillTypes.RANDOM: seed = kwargs.get('seed', None) if seed is not None: @@ -2227,5 +2216,8 @@ def allocate(self, value=0, **kwargs): else: r = numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) out.fill(numpy.asarray(r, dtype=dtype)) - + elif value is None: + pass + else: + raise ValueError(f'Value {value} unknown') return out diff --git a/Wrappers/Python/cil/framework/image_data.py b/Wrappers/Python/cil/framework/image_data.py index e918e474fb..4d3fa98e41 100644 --- a/Wrappers/Python/cil/framework/image_data.py +++ b/Wrappers/Python/cil/framework/image_data.py @@ -199,10 +199,7 @@ def reorder(self, order=None): :type order: list, sting ''' - try: - Backends.validate(order) - order = ImageDimensionLabels.get_order_for_engine(order, self.geometry) - except ValueError: - pass + if order in Backends: + order = ImageDimensionLabels.get_order_for_engine(order, self.geometry) super(ImageData, self).reorder(order) diff --git a/Wrappers/Python/cil/framework/image_geometry.py b/Wrappers/Python/cil/framework/image_geometry.py index f01ecb3073..e174586821 100644 --- a/Wrappers/Python/cil/framework/image_geometry.py +++ b/Wrappers/Python/cil/framework/image_geometry.py @@ -123,11 +123,7 @@ def dimension_labels(self, val): def set_labels(self, labels): if labels is not None: - label_new=[] - for x in labels: - if ImageDimensionLabels.validate(x): - label_new.append(ImageDimensionLabels.get_enum_value(x)) - + label_new=[ImageDimensionLabels(x).value for x in labels if x in ImageDimensionLabels] self._dimension_labels = tuple(label_new) def __eq__(self, other): @@ -290,9 +286,7 @@ def allocate(self, value=0, **kwargs): if isinstance(value, Number): # it's created empty, so we make it 0 out.array.fill(value) - elif value is not None: - FillTypes.validate(value) - + elif value in FillTypes: if value == FillTypes.RANDOM: seed = kwargs.get('seed', None) if seed is not None: @@ -312,4 +306,8 @@ def allocate(self, value=0, **kwargs): out.fill(numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32) + 1.j*numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32)) else: out.fill(numpy.random.randint(max_value,size=self.shape, dtype=numpy.int32)) + elif value is None: + pass + else: + raise ValueError(f'Value {value} unknown') return out diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index c21d0fcef6..8a3bd70cb6 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -20,105 +20,14 @@ class _LabelsBase(Enum): """ - Base class for labels enumeration. + Base class for labels enumeration. These changes are needed for python < 3.12. Methods: -------- - - validate(label): Validates if the given label is a valid member of the enumeration. - - get_enum_member(label): Returns the enum member corresponding to the given label. - - get_enum_value(label): Returns the value of the enum member corresponding to the given label. - - __eq__(other): Checks if the enum value is equal to the given value. - - __contains__(item): Checks if the enum contains the given value. + - __eq__(other): Checks if the enum or enum values are equal + - __contains__(item): Checks if the enum contains the given value """ - @classmethod - def validate(cls, label): - """ - Validates if the given label is a valid member of the specified class. - - Parameters: - ---------- - label : str, enum - The label to validate. - - Raises: - ------- - ValueError - If the label is not a valid member of the class. - - Returns: - -------- - bool - True if the label is a valid member of the class, False otherwise. - """ - - try: - for member in cls: - if member == label: - return True - except AttributeError: - pass - - raise ValueError(f"Expected one of {[e.value for e in cls]} \ - from {cls.__name__}, got {label}") - - - @classmethod - def get_enum_member(cls, label): - """ - Returns the enum member corresponding to the given label. - - Parameters: - ---------- - label : str - The label to get the corresponding enum member. - - Raises: - ------- - ValueError - If the label is not a valid member of the class. - - Returns: - -------- - Enum - The enum member corresponding to the given label. - """ - - try: - for member in cls: - if member == label: - return member - except AttributeError: - pass - - raise ValueError(f"Expected one of {[e.value for e in cls]} \ - from {cls.__name__}, got {label}") - - - @classmethod - def get_enum_value(cls, label): - """ - Returns the value of the enum member corresponding to the given label. - - Parameters: - ---------- - label : str - The enum member to get the corresponding enum value. - - Raises: - ------- - ValueError - If the label is not a valid member of the class. - - Returns: - -------- - str - The value of the enum member corresponding to the given label. - """ - - return cls.get_enum_member(label).value - - def __eq__(self, other): if self.value == other: return True @@ -174,39 +83,7 @@ class ImageDimensionLabels(_LabelsBase): HORIZONTAL_Y = "horizontal_y" @classmethod - def get_default_order_for_engine(cls, engine): - """ - Returns the default dimension order for the given engine. - - Parameters - ---------- - engine : str - The engine to get the default dimension order for. - - Returns - ------- - list - The default dimension order for the given engine. - - Raises - ------ - ValueError - If the engine is not a valid member of the Backends enumeration - """ - order = [cls.CHANNEL.value, cls.VERTICAL.value, \ - cls.HORIZONTAL_Y.value, cls.HORIZONTAL_X.value] - engine_orders = { - Backends.ASTRA.value: order, - Backends.TIGRE.value: order, - Backends.CIL.value: order - } - Backends.validate(engine) - engine = Backends.get_enum_value(engine) - - return engine_orders[engine] - - @classmethod - def get_order_for_engine(cls, engine, geometry): + def get_order_for_engine(cls, engine, geometry=None): """ Returns the order of dimensions for a specific engine and geometry. @@ -214,18 +91,29 @@ def get_order_for_engine(cls, engine, geometry): ---------- engine : str The engine name. - geometry : ImageGeometry + geometry : ImageGeometry, optional + The geometry object. If None, the default order is returned. Returns: -------- list The order of dimensions for the given engine and geometry. """ + order = [cls.CHANNEL.value, cls.VERTICAL.value, \ + cls.HORIZONTAL_Y.value, cls.HORIZONTAL_X.value] + + engine_orders = { + Backends.ASTRA.value: order, + Backends.TIGRE.value: order, + Backends.CIL.value: order + } - dim_order = cls.get_default_order_for_engine(engine) - dimensions = [label for label in dim_order if label in geometry.dimension_labels ] + dim_order = engine_orders[Backends(engine).value] - return dimensions + if geometry is None: + return dim_order + else: + return [label for label in dim_order if label in geometry.dimension_labels ] @classmethod def check_order_for_engine(cls, engine, geometry): @@ -242,7 +130,7 @@ def check_order_for_engine(cls, engine, geometry): Returns: -------- bool - True if the order of dimensions is correct, False otherwise. + True if the order of dimensions is correct. Raises: ------- @@ -283,27 +171,24 @@ class AcquisitionDimensionLabels(_LabelsBase): VERTICAL = "vertical" HORIZONTAL = "horizontal" + @classmethod - def get_default_order_for_engine(cls, engine): + def get_order_for_engine(cls, engine, geometry=None): """ - Returns the default dimension order for the given engine. + Returns the order of dimensions for a specific engine and geometry. - Parameters + Parameters: ---------- engine : str - The engine to get the default dimension order for. + The engine name. + geometry : AcquisitionGeometry, optional + The geometry object. If None, the default order is returned. - Returns - ------- + Returns: + -------- list - The default dimension order for the given engine. - - Raises - ------ - ValueError - If the engine is not a valid member of the Backends enumeration + The order of dimensions for the given engine and geometry. """ - engine_orders = { Backends.ASTRA.value: [cls.CHANNEL.value, cls.VERTICAL.value, \ cls.ANGLE.value, cls.HORIZONTAL.value], @@ -312,32 +197,14 @@ def get_default_order_for_engine(cls, engine): Backends.CIL.value: [cls.CHANNEL.value, cls.ANGLE.value, \ cls.VERTICAL.value, cls.HORIZONTAL.value] } - Backends.validate(engine) - engine = Backends.get_enum_value(engine) - - return engine_orders[engine] - @classmethod - def get_order_for_engine(cls, engine, geometry): - """ - Returns the order of dimensions for a specific engine and geometry. + dim_order = engine_orders[Backends(engine).value] - Parameters: - ---------- - engine : str - The engine name. - geometry : AcquisitionGeometry - - Returns: - -------- - list - The order of dimensions for the given engine and geometry. - """ - - dim_order = cls.get_default_order_for_engine(engine) - dimensions = [label for label in dim_order if label in geometry.dimension_labels ] + if geometry is None: + return dim_order + else: + return [label for label in dim_order if label in geometry.dimension_labels ] - return dimensions @classmethod def check_order_for_engine(cls, engine, geometry): @@ -353,7 +220,7 @@ def check_order_for_engine(cls, engine, geometry): Returns: -------- bool - True if the order of dimensions is correct, False otherwise. + True if the order of dimensions is correct Raises: ------- diff --git a/Wrappers/Python/cil/framework/vector_geometry.py b/Wrappers/Python/cil/framework/vector_geometry.py index 952bbdbe15..a21c1bff68 100644 --- a/Wrappers/Python/cil/framework/vector_geometry.py +++ b/Wrappers/Python/cil/framework/vector_geometry.py @@ -97,9 +97,7 @@ def allocate(self, value=0, **kwargs): if isinstance(value, Number): if value != 0: out += value - elif value is not None: - FillTypes.validate(value) - + elif value in FillTypes: if value == FillTypes.RANDOM: seed = kwargs.get('seed', None) if seed is not None: @@ -117,8 +115,8 @@ def allocate(self, value=0, **kwargs): out.fill(numpy.random.randint(max_value, size=self.shape, dtype=numpy.int32) + 1.j*numpy.random.randint(max_value, size=self.shape, dtype=numpy.int32)) else: out.fill(numpy.random.randint(max_value, size=self.shape, dtype=numpy.int32)) - elif value is None: - pass - else: - raise ValueError('Value {} unknown'.format(value)) + elif value is None: + pass + else: + raise ValueError(f'Value {value} unknown') return out From 36fb87aad842249c7883b8963cefddbade6b1fb5 Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Fri, 16 Aug 2024 11:13:20 +0000 Subject: [PATCH 51/72] doc string updates on reorder --- Wrappers/Python/cil/framework/acquisition_data.py | 10 ++++++---- Wrappers/Python/cil/framework/image_data.py | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index 2f67f931dc..6ffdabd0fb 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -105,13 +105,15 @@ def get_slice(self,channel=None, angle=None, vertical=None, horizontal=None, for def reorder(self, order=None): ''' - reorders the data in memory as requested. + Reorders the data in memory as requested. This is an in-place operation. - :param order: ordered list of labels from self.dimension_labels, or order for engine 'astra' or 'tigre' - :type order: list, sting + Parameters + ---------- + order : list or str + Ordered list of labels from self.dimension_labels, or string 'astra' or 'tigre'. ''' if order in Backends : order = AcquisitionDimensionLabels.get_order_for_engine(order, self.geometry) - super(AcquisitionData, self).reorder(order) \ No newline at end of file + super(AcquisitionData, self).reorder(order) diff --git a/Wrappers/Python/cil/framework/image_data.py b/Wrappers/Python/cil/framework/image_data.py index 4d3fa98e41..0f1c15dc02 100644 --- a/Wrappers/Python/cil/framework/image_data.py +++ b/Wrappers/Python/cil/framework/image_data.py @@ -193,10 +193,12 @@ def apply_circular_mask(self, radius=0.99, in_place=True): def reorder(self, order=None): ''' - reorders the data in memory as requested. + Reorders the data in memory as requested. This is an in-place operation. - :param order: ordered list of labels from self.dimension_labels, or order for engine 'astra' or 'tigre' - :type order: list, sting + Parameters + ---------- + order : list or str + Ordered list of labels from self.dimension_labels, or string 'astra' or 'tigre'. ''' if order in Backends: From 894c36900566877c537aa8ab07cc200806bce0a5 Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Fri, 16 Aug 2024 11:17:06 +0000 Subject: [PATCH 52/72] super update --- Wrappers/Python/cil/framework/acquisition_data.py | 2 +- Wrappers/Python/cil/framework/image_data.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index 6ffdabd0fb..5d57b5afd9 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -116,4 +116,4 @@ def reorder(self, order=None): if order in Backends : order = AcquisitionDimensionLabels.get_order_for_engine(order, self.geometry) - super(AcquisitionData, self).reorder(order) + super().reorder(order) diff --git a/Wrappers/Python/cil/framework/image_data.py b/Wrappers/Python/cil/framework/image_data.py index 0f1c15dc02..5ac8eabdef 100644 --- a/Wrappers/Python/cil/framework/image_data.py +++ b/Wrappers/Python/cil/framework/image_data.py @@ -204,4 +204,4 @@ def reorder(self, order=None): if order in Backends: order = ImageDimensionLabels.get_order_for_engine(order, self.geometry) - super(ImageData, self).reorder(order) + super().reorder(order) From a5da5de07d00710fe47f033c324befff40d40bb8 Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Fri, 16 Aug 2024 11:17:57 +0000 Subject: [PATCH 53/72] data order simplification --- .../Python/cil/framework/acquisition_geometry.py | 2 +- Wrappers/Python/cil/framework/image_geometry.py | 2 +- Wrappers/Python/cil/plugins/TomoPhantom.py | 2 +- Wrappers/Python/test/test_labels.py | 16 ++++++++-------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 0e8cbccceb..c52278af32 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -1732,7 +1732,7 @@ def shape(self): @property def dimension_labels(self): - labels_default = AcquisitionDimensionLabels.get_default_order_for_engine("cil") + labels_default = AcquisitionDimensionLabels.get_order_for_engine("cil") shape_default = [self.config.channels.num_channels, self.config.angles.num_positions, diff --git a/Wrappers/Python/cil/framework/image_geometry.py b/Wrappers/Python/cil/framework/image_geometry.py index e174586821..b9c99a68ed 100644 --- a/Wrappers/Python/cil/framework/image_geometry.py +++ b/Wrappers/Python/cil/framework/image_geometry.py @@ -97,7 +97,7 @@ def ndim(self): @property def dimension_labels(self): - labels_default = ImageDimensionLabels.get_default_order_for_engine("cil") + labels_default = ImageDimensionLabels.get_order_for_engine("cil") shape_default = [ self.channels, self.voxel_num_z, diff --git a/Wrappers/Python/cil/plugins/TomoPhantom.py b/Wrappers/Python/cil/plugins/TomoPhantom.py index 3f2d87f071..f36ef29ad3 100644 --- a/Wrappers/Python/cil/plugins/TomoPhantom.py +++ b/Wrappers/Python/cil/plugins/TomoPhantom.py @@ -147,7 +147,7 @@ def get_ImageData(num_model, geometry): ''' ig = geometry.copy() - ig.set_labels(ImageDimensionLabels.get_default_order_for_engine('cil')) + ig.set_labels(ImageDimensionLabels.get_order_for_engine('cil')) num_dims = len(ig.dimension_labels) if ImageDimensionLabels.CHANNEL in ig.dimension_labels: diff --git a/Wrappers/Python/test/test_labels.py b/Wrappers/Python/test/test_labels.py index 9506363ce1..4792a32654 100644 --- a/Wrappers/Python/test/test_labels.py +++ b/Wrappers/Python/test/test_labels.py @@ -139,17 +139,17 @@ def test_image_dimension_labels_default_order(self): order_gold = [ImageDimensionLabels.CHANNEL, ImageDimensionLabels.VERTICAL, ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X] - order = ImageDimensionLabels.get_default_order_for_engine("cil") + order = ImageDimensionLabels.get_order_for_engine("cil") self.assertEqual(order,order_gold ) - order = ImageDimensionLabels.get_default_order_for_engine("tigre") + order = ImageDimensionLabels.get_order_for_engine("tigre") self.assertEqual(order,order_gold) - order = ImageDimensionLabels.get_default_order_for_engine("astra") + order = ImageDimensionLabels.get_order_for_engine("astra") self.assertEqual(order, order_gold) with self.assertRaises(ValueError): - order = AcquisitionDimensionLabels.get_default_order_for_engine("bad_engine") + order = AcquisitionDimensionLabels.get_order_for_engine("bad_engine") def test_image_dimension_labels_get_order(self): @@ -196,17 +196,17 @@ def test_acquisition_dimension_labels(self): self.assertTrue(AcquisitionDimensionLabels.HORIZONTAL in AcquisitionDimensionLabels) def test_acquisition_dimension_labels_default_order(self): - order = AcquisitionDimensionLabels.get_default_order_for_engine("cil") + order = AcquisitionDimensionLabels.get_order_for_engine("cil") self.assertEqual(order, [AcquisitionDimensionLabels.CHANNEL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.VERTICAL, AcquisitionDimensionLabels.HORIZONTAL]) - order = AcquisitionDimensionLabels.get_default_order_for_engine("tigre") + order = AcquisitionDimensionLabels.get_order_for_engine("tigre") self.assertEqual(order, [AcquisitionDimensionLabels.CHANNEL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.VERTICAL, AcquisitionDimensionLabels.HORIZONTAL]) - order = AcquisitionDimensionLabels.get_default_order_for_engine("astra") + order = AcquisitionDimensionLabels.get_order_for_engine("astra") self.assertEqual(order, [AcquisitionDimensionLabels.CHANNEL, AcquisitionDimensionLabels.VERTICAL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.HORIZONTAL]) with self.assertRaises(ValueError): - order = AcquisitionDimensionLabels.get_default_order_for_engine("bad_engine") + order = AcquisitionDimensionLabels.get_order_for_engine("bad_engine") def test_acquisition_dimension_labels_get_order(self): From 5c40579c1a4fdd47034fec4cfac96194d1bb441c Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Fri, 16 Aug 2024 11:26:32 +0000 Subject: [PATCH 54/72] update unittests --- Wrappers/Python/test/test_labels.py | 38 ++++------------------------- 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/Wrappers/Python/test/test_labels.py b/Wrappers/Python/test/test_labels.py index 4792a32654..e9c70e2fec 100644 --- a/Wrappers/Python/test/test_labels.py +++ b/Wrappers/Python/test/test_labels.py @@ -29,49 +29,21 @@ class Test_Lables(unittest.TestCase): - def test_labels_validate(self): + def test_base_labels(self): - input_good = ["3D", AcquisitionDimensions.DIM3] - input_bad = ["bad_str", "DIM3", UnitsAngles.DEGREE] - - for input in input_good: - self.assertTrue(AcquisitionDimensions.validate(input)) - - for input in input_bad: - with self.assertRaises(ValueError): - AcquisitionDimensions.validate(input) - - - def test_labels_get_enum_member(self): out_gold = AcquisitionDimensions.DIM3 input_good = ["3D", AcquisitionDimensions.DIM3] input_bad = ["bad_str", "DIM3", UnitsAngles.DEGREE] - for input in input_good: - out = AcquisitionDimensions.get_enum_member(input) + for item in input_good: + out = AcquisitionDimensions(item) self.assertEqual(out, out_gold) self.assertTrue(isinstance(out, AcquisitionDimensions)) - - for input in input_bad: - with self.assertRaises(ValueError): - AcquisitionDimensions.get_enum_member(input) - - - def test_labels_get_enum_value(self): - out_gold = "3D" - - input_good = ["3D", AcquisitionDimensions.DIM3] - input_bad = ["bad_str", "DIM3", UnitsAngles.DEGREE] - for input in input_good: - out = AcquisitionDimensions.get_enum_value(input) - self.assertEqual(out, out_gold) - self.assertTrue(isinstance(out, str)) - - for input in input_bad: + for item in input_bad: with self.assertRaises(ValueError): - AcquisitionDimensions.get_enum_value(input) + AcquisitionDimensions(item) def test_labels_eq(self): From 6300f6505640f581c1fea2455c903f373950dd25 Mon Sep 17 00:00:00 2001 From: Gemma Fardell <47746591+gfardell@users.noreply.github.com> Date: Fri, 16 Aug 2024 12:27:05 +0100 Subject: [PATCH 55/72] Update CHANGELOG.md Co-authored-by: Casper da Costa-Luis Signed-off-by: Gemma Fardell <47746591+gfardell@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efc533cd71..e19a69218a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Enhancements: - Use ravel instead of flat in KullbackLeibler numba backend (#1874) - Upgrade Python wrapper (#1873, #1875) - - Internal refactor: Replaced string-based label checks with enum-based checks for improved type safety and consistency. (#1692) + - Internal refactor: Replaced string-based label checks with enum-based checks for improved type safety and consistency (#1692) * 24.1.0 - New Features: From a534c6784d81c920ccf81fba861e04f10bba605a Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 19 Aug 2024 14:51:18 +0100 Subject: [PATCH 56/72] drop default reorder(order=None) --- Wrappers/Python/cil/framework/acquisition_data.py | 3 +-- Wrappers/Python/cil/framework/data_container.py | 2 +- Wrappers/Python/cil/framework/image_data.py | 5 ++--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index 5d57b5afd9..ced1985ebb 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -112,8 +112,7 @@ def reorder(self, order=None): order : list or str Ordered list of labels from self.dimension_labels, or string 'astra' or 'tigre'. ''' - - if order in Backends : + if order in Backends: order = AcquisitionDimensionLabels.get_order_for_engine(order, self.geometry) super().reorder(order) diff --git a/Wrappers/Python/cil/framework/data_container.py b/Wrappers/Python/cil/framework/data_container.py index 327dc86280..41cf50fcc2 100644 --- a/Wrappers/Python/cil/framework/data_container.py +++ b/Wrappers/Python/cil/framework/data_container.py @@ -185,7 +185,7 @@ def get_slice(self, **kw): from .vector_data import VectorData return VectorData(new_array, dimension_labels=dimension_labels_list) - def reorder(self, order=None): + def reorder(self, order): ''' reorders the data in memory as requested. diff --git a/Wrappers/Python/cil/framework/image_data.py b/Wrappers/Python/cil/framework/image_data.py index 5ac8eabdef..172b71765d 100644 --- a/Wrappers/Python/cil/framework/image_data.py +++ b/Wrappers/Python/cil/framework/image_data.py @@ -191,16 +191,15 @@ def apply_circular_mask(self, radius=0.99, in_place=True): return image_data_out - def reorder(self, order=None): + def reorder(self, order): ''' Reorders the data in memory as requested. This is an in-place operation. Parameters ---------- - order : list or str + order: list or str Ordered list of labels from self.dimension_labels, or string 'astra' or 'tigre'. ''' - if order in Backends: order = ImageDimensionLabels.get_order_for_engine(order, self.geometry) From 55dc29bd74eff8ce53d7bf9bf5306e0f01ada451 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 19 Aug 2024 15:10:28 +0100 Subject: [PATCH 57/72] add labels.StrEnum --- Wrappers/Python/cil/framework/labels.py | 290 ++++++++++-------------- 1 file changed, 121 insertions(+), 169 deletions(-) diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index 8a3bd70cb6..8fb7c27c0d 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -15,279 +15,232 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +from enum import Enum, auto, unique +try: + from enum import EnumType +except ImportError: # Python<3.11 + from enum import EnumMeta as EnumType + + +class _StrEnumMeta(EnumType): + """Python<3.12 requires this in a metaclass (rather than directly in StrEnum)""" + def __contains__(self, item: str) -> bool: + try: + key = item.upper() + except AttributeError: + return False + return key in self.__members__ or item in self.__members__.values() + + +@unique +class StrEnum(str, Enum, metaclass=_StrEnumMeta): + """Case-insensitive StrEnum""" + @classmethod + def _missing_(cls, value: str): + return cls.__members__.get(value.upper(), None) -from enum import Enum, EnumMeta + def __eq__(self, value: str) -> bool: + """Uses value.upper() for case-insensitivity""" + try: + return super().__eq__(self.__class__[value.upper()]) + except (KeyError, ValueError): + return False -class _LabelsBase(Enum): - """ - Base class for labels enumeration. These changes are needed for python < 3.12. + def __hash__(self) -> int: + """consistent hashing for dictionary keys""" + return hash(self.value) - Methods: - -------- - - __eq__(other): Checks if the enum or enum values are equal - - __contains__(item): Checks if the enum contains the given value - """ + # compatibility with Python>=3.11 `enum.StrEnum` + __str__ = str.__str__ + __format__ = str.__format__ - def __eq__(self, other): - if self.value == other: - return True - return False + @staticmethod + def _generate_next_value_(name: str, start, count, last_values) -> str: + return name.lower() - def __contains__(self, item): - for member in self: - if member.value == item: - return True - return False -# Needed for python < 3.12 -EnumMeta.__contains__ = _LabelsBase.__contains__ -class Backends(_LabelsBase): +class Backends(StrEnum): """ Available backends for CIL. - Attributes - ---------- - ASTRA ('astra'): The ASTRA toolbox. - TIGRE ('tigre'): The TIGRE toolbox. - CIL ('cil'): Native CIL implementation. - Examples -------- + ``` FBP(data, backend=Backends.ASTRA) FBP(data, backend="astra") + ``` """ - ASTRA = "astra" - TIGRE = "tigre" - CIL = "cil" + ASTRA = auto() + TIGRE = auto() + CIL = auto() -class ImageDimensionLabels(_LabelsBase): + +class ImageDimensionLabels(StrEnum): """ Available dimension labels for image data. - - Attributes - ---------- - CHANNEL ('channel'): The channel dimension. - VERTICAL ('vertical'): The vertical dimension. - HORIZONTAL_X ('horizontal_x'): The horizontal dimension in x. - HORIZONTAL_Y ('horizontal_y'): The horizontal dimension in y. Examples -------- + ``` data.reorder([ImageDimensionLabels.HORIZONTAL_X, ImageDimensionLabels.VERTICAL]) data.reorder(["horizontal_x", "vertical"]) + ``` """ - CHANNEL = "channel" - VERTICAL = "vertical" - HORIZONTAL_X = "horizontal_x" - HORIZONTAL_Y = "horizontal_y" + CHANNEL = auto() + VERTICAL = auto() + HORIZONTAL_X = auto() + HORIZONTAL_Y = auto() @classmethod - def get_order_for_engine(cls, engine, geometry=None): + def get_order_for_engine(cls, engine: str, geometry=None) -> list: """ Returns the order of dimensions for a specific engine and geometry. - Parameters: + Parameters ---------- - engine : str - The engine name. - geometry : ImageGeometry, optional - The geometry object. If None, the default order is returned. - - Returns: - -------- - list - The order of dimensions for the given engine and geometry. + geometry: ImageGeometry, optional + If unspecified, the default order is returned. """ - order = [cls.CHANNEL.value, cls.VERTICAL.value, \ - cls.HORIZONTAL_Y.value, cls.HORIZONTAL_X.value] - - engine_orders = { - Backends.ASTRA.value: order, - Backends.TIGRE.value: order, - Backends.CIL.value: order - } - - dim_order = engine_orders[Backends(engine).value] + order = [cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X] + engine_orders = {Backends.ASTRA: order, Backends.TIGRE: order, Backends.CIL: order} + dim_order = engine_orders[Backends[engine.upper()]] if geometry is None: return dim_order - else: - return [label for label in dim_order if label in geometry.dimension_labels ] + return [label for label in dim_order if label in geometry.dimension_labels] @classmethod - def check_order_for_engine(cls, engine, geometry): + def check_order_for_engine(cls, engine: str, geometry) -> bool: """ - Checks if the order of dimensions is correct for a specific engine and geometry. + Returns True iff the order of dimensions is correct for a specific engine and geometry. - Parameters: + Parameters ---------- - engine : str - The engine name. - geometry : ImageGeometry - The geometry object. - - Returns: - -------- - bool - True if the order of dimensions is correct. - - Raises: - ------- - ValueError - If the order of dimensions is incorrect. + geometry: ImageGeometry + + Raises + ------ + ValueError if the order of dimensions is incorrect. """ order_requested = cls.get_order_for_engine(engine, geometry) - if order_requested == list(geometry.dimension_labels): return True - else: - raise ValueError( - f"Expected dimension_label order {order_requested}, \ - got {list(geometry.dimension_labels)}.\n\ - Try using `data.reorder('{engine}')` to permute for {engine}") + raise ValueError( + f"Expected dimension_label order {order_requested}" + f" got {list(geometry.dimension_labels)}." + f" Try using `data.reorder('{engine}')` to permute for {engine}") + -class AcquisitionDimensionLabels(_LabelsBase): +class AcquisitionDimensionLabels(StrEnum): """ Available dimension labels for acquisition data. - - Attributes - ---------- - CHANNEL ('channel'): The channel dimension. - ANGLE ('angle'): The angle dimension. - VERTICAL ('vertical'): The vertical dimension. - HORIZONTAL ('horizontal'): The horizontal dimension. - + Examples -------- + ``` data.reorder([AcquisitionDimensionLabels.CHANNEL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.HORIZONTAL]) data.reorder(["channel", "angle", "horizontal"]) + ``` """ - - CHANNEL = "channel" - ANGLE = "angle" - VERTICAL = "vertical" - HORIZONTAL = "horizontal" - + CHANNEL = auto() + ANGLE = auto() + VERTICAL = auto() + HORIZONTAL = auto() @classmethod - def get_order_for_engine(cls, engine, geometry=None): + def get_order_for_engine(cls, engine: str, geometry=None) -> list: """ Returns the order of dimensions for a specific engine and geometry. - Parameters: + Parameters ---------- - engine : str - The engine name. geometry : AcquisitionGeometry, optional - The geometry object. If None, the default order is returned. - - Returns: - -------- - list - The order of dimensions for the given engine and geometry. + If unspecified, the default order is returned. """ engine_orders = { - Backends.ASTRA.value: [cls.CHANNEL.value, cls.VERTICAL.value, \ - cls.ANGLE.value, cls.HORIZONTAL.value], - Backends.TIGRE.value: [cls.CHANNEL.value, cls.ANGLE.value, \ - cls.VERTICAL.value, cls.HORIZONTAL.value], - Backends.CIL.value: [cls.CHANNEL.value, cls.ANGLE.value, \ - cls.VERTICAL.value, cls.HORIZONTAL.value] - } - - dim_order = engine_orders[Backends(engine).value] + Backends.ASTRA: [cls.CHANNEL, cls.VERTICAL, cls.ANGLE, cls.HORIZONTAL], + Backends.TIGRE: [cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL], + Backends.CIL: [cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL]} + dim_order = engine_orders[Backends[engine.upper()]] if geometry is None: return dim_order - else: - return [label for label in dim_order if label in geometry.dimension_labels ] - + return [label for label in dim_order if label in geometry.dimension_labels] @classmethod - def check_order_for_engine(cls, engine, geometry): + def check_order_for_engine(cls, engine: str, geometry) -> bool: """ - Checks if the order of dimensions is correct for a specific engine and geometry. + Returns True iff the order of dimensions is correct for a specific engine and geometry. - Parameters: + Parameters ---------- - engine : str - The engine name. - geometry : AcquisitionGeometry - - Returns: - -------- - bool - True if the order of dimensions is correct - - Raises: - ------- - ValueError - If the order of dimensions is incorrect. - """ + geometry: AcquisitionGeometry + Raises + ------ + ValueError if the order of dimensions is incorrect. + """ order_requested = cls.get_order_for_engine(engine, geometry) - if order_requested == list(geometry.dimension_labels): return True - else: - raise ValueError( - f"Expected dimension_label order {order_requested}, \ - got {list(geometry.dimension_labels)}.\n\ - Try using `data.reorder('{engine}')` to permute for {engine}") + raise ValueError( + f"Expected dimension_label order {order_requested}," + f" got {list(geometry.dimension_labels)}." + f" Try using `data.reorder('{engine}')` to permute for {engine}") + -class FillTypes(_LabelsBase): +class FillTypes(StrEnum): """ Available fill types for image data. Attributes ---------- - RANDOM ('random'): Fill with random values. - RANDOM_INT ('random_int'): Fill with random integers. + RANDOM: Fill with random values. + RANDOM_INT: Fill with random integers. Examples -------- - data.fill(FillTypes.random) + ``` + data.fill(FillTypes.RANDOM) data.fill("random") + ``` """ + RANDOM = auto() + RANDOM_INT = auto() - RANDOM = "random" - RANDOM_INT = "random_int" -class UnitsAngles(_LabelsBase): +class UnitsAngles(StrEnum): """ Available units for angles. - Attributes - ---------- - DEGREE ('degree'): Degrees. - RADIAN ('radian'): Radians. - Examples -------- + ``` data.geometry.set_unitangles(angle_data, angle_units=UnitsAngles.DEGREE) data.geometry.set_unit(angle_data, angle_units="degree") + ``` """ + DEGREE = auto() + RADIAN = auto() - DEGREE = "degree" - RADIAN = "radian" -class AcquisitionTypes(_LabelsBase): +class AcquisitionTypes(StrEnum): """ Available acquisition types. Attributes ---------- - PARALLEL ('parallel'): Parallel beam. - CONE ('cone'): Cone beam. + PARALLEL: Parallel beam. + CONE: Cone beam. """ + PARALLEL = auto() + CONE = auto() - PARALLEL = "parallel" - CONE = "cone" -class AcquisitionDimensions(_LabelsBase): +class AcquisitionDimensions(StrEnum): """ Available acquisition dimensions. @@ -296,6 +249,5 @@ class AcquisitionDimensions(_LabelsBase): DIM2 ('2D'): 2D acquisition. DIM3 ('3D'): 3D acquisition. """ - DIM2 = "2D" DIM3 = "3D" From 714434b24afff8173d930e0860566557007c7562 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 19 Aug 2024 20:39:01 +0100 Subject: [PATCH 58/72] fix tests --- .../cil/framework/acquisition_geometry.py | 20 +- .../Python/cil/framework/image_geometry.py | 36 +-- Wrappers/Python/cil/framework/labels.py | 13 +- Wrappers/Python/cil/io/NEXUSDataWriter.py | 12 +- Wrappers/Python/cil/io/ZEISSDataReader.py | 14 +- .../convert_geometry_to_astra_vec_2D.py | 2 +- .../convert_geometry_to_astra_vec_3D.py | 2 +- Wrappers/Python/test/test_DataContainer.py | 12 +- Wrappers/Python/test/test_labels.py | 240 +++++++----------- 9 files changed, 130 insertions(+), 221 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index c52278af32..c373614869 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -200,7 +200,7 @@ def dimension(self,val): @property def geometry(self): - return self._geometry.value + return self._geometry @geometry.setter def geometry(self,val): @@ -1720,15 +1720,11 @@ def dimension(self): @property def shape(self): - shape_dict = {AcquisitionDimensionLabels.CHANNEL.value: self.config.channels.num_channels, - AcquisitionDimensionLabels.ANGLE.value: self.config.angles.num_positions, - AcquisitionDimensionLabels.VERTICAL.value: self.config.panel.num_pixels[1], - AcquisitionDimensionLabels.HORIZONTAL.value: self.config.panel.num_pixels[0]} - shape = [] - for label in self.dimension_labels: - shape.append(shape_dict[label]) - - return tuple(shape) + shape_dict = {AcquisitionDimensionLabels.CHANNEL: self.config.channels.num_channels, + AcquisitionDimensionLabels.ANGLE: self.config.angles.num_positions, + AcquisitionDimensionLabels.VERTICAL: self.config.panel.num_pixels[1], + AcquisitionDimensionLabels.HORIZONTAL: self.config.panel.num_pixels[0]} + return tuple(shape_dict[label] for label in self.dimension_labels) @property def dimension_labels(self): @@ -1758,10 +1754,8 @@ def dimension_labels(self): @dimension_labels.setter def dimension_labels(self, val): - if val is not None: - label_new=[AcquisitionDimensionLabels(x).value for x in val if x in AcquisitionDimensionLabels] - self._dimension_labels = tuple(label_new) + self._dimension_labels = tuple(AcquisitionDimensionLabels(x) for x in val if x in AcquisitionDimensionLabels) @property def ndim(self): diff --git a/Wrappers/Python/cil/framework/image_geometry.py b/Wrappers/Python/cil/framework/image_geometry.py index b9c99a68ed..ab9049ac83 100644 --- a/Wrappers/Python/cil/framework/image_geometry.py +++ b/Wrappers/Python/cil/framework/image_geometry.py @@ -56,17 +56,12 @@ def VERTICAL(self): return ImageDimensionLabels.VERTICAL @property - def shape(self): - shape_dict = {ImageDimensionLabels.CHANNEL.value: self.channels, - ImageDimensionLabels.VERTICAL.value: self.voxel_num_z, - ImageDimensionLabels.HORIZONTAL_Y.value: self.voxel_num_y, - ImageDimensionLabels.HORIZONTAL_X.value: self.voxel_num_x} - - shape = [] - for label in self.dimension_labels: - shape.append(shape_dict[label]) - - return tuple(shape) + def shape(self): + shape_dict = {ImageDimensionLabels.CHANNEL: self.channels, + ImageDimensionLabels.VERTICAL: self.voxel_num_z, + ImageDimensionLabels.HORIZONTAL_Y: self.voxel_num_y, + ImageDimensionLabels.HORIZONTAL_X: self.voxel_num_x} + return tuple(shape_dict[label] for label in self.dimension_labels) @shape.setter def shape(self, val): @@ -74,17 +69,11 @@ def shape(self, val): @property def spacing(self): - - spacing_dict = {ImageDimensionLabels.CHANNEL.value: self.channel_spacing, - ImageDimensionLabels.VERTICAL.value: self.voxel_size_z, - ImageDimensionLabels.HORIZONTAL_Y.value: self.voxel_size_y, - ImageDimensionLabels.HORIZONTAL_X.value: self.voxel_size_x} - - spacing = [] - for label in self.dimension_labels: - spacing.append(spacing_dict[label]) - - return tuple(spacing) + spacing_dict = {ImageDimensionLabels.CHANNEL: self.channel_spacing, + ImageDimensionLabels.VERTICAL: self.voxel_size_z, + ImageDimensionLabels.HORIZONTAL_Y: self.voxel_size_y, + ImageDimensionLabels.HORIZONTAL_X: self.voxel_size_x} + return tuple(spacing_dict[label] for label in self.dimension_labels) @property def length(self): @@ -123,8 +112,7 @@ def dimension_labels(self, val): def set_labels(self, labels): if labels is not None: - label_new=[ImageDimensionLabels(x).value for x in labels if x in ImageDimensionLabels] - self._dimension_labels = tuple(label_new) + self._dimension_labels = tuple(ImageDimensionLabels(x) for x in labels if x in ImageDimensionLabels) def __eq__(self, other): diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index 8fb7c27c0d..9eac7530c6 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -27,7 +27,7 @@ class _StrEnumMeta(EnumType): def __contains__(self, item: str) -> bool: try: key = item.upper() - except AttributeError: + except (AttributeError, TypeError): return False return key in self.__members__ or item in self.__members__.values() @@ -42,9 +42,10 @@ def _missing_(cls, value: str): def __eq__(self, value: str) -> bool: """Uses value.upper() for case-insensitivity""" try: - return super().__eq__(self.__class__[value.upper()]) - except (KeyError, ValueError): - return False + value = self.__class__[value.upper()] + except (KeyError, ValueError, AttributeError): + pass + return super().__eq__(value) def __hash__(self) -> int: """consistent hashing for dictionary keys""" @@ -104,7 +105,7 @@ def get_order_for_engine(cls, engine: str, geometry=None) -> list: """ order = [cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X] engine_orders = {Backends.ASTRA: order, Backends.TIGRE: order, Backends.CIL: order} - dim_order = engine_orders[Backends[engine.upper()]] + dim_order = engine_orders[Backends(engine)] if geometry is None: return dim_order @@ -164,7 +165,7 @@ def get_order_for_engine(cls, engine: str, geometry=None) -> list: Backends.ASTRA: [cls.CHANNEL, cls.VERTICAL, cls.ANGLE, cls.HORIZONTAL], Backends.TIGRE: [cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL], Backends.CIL: [cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL]} - dim_order = engine_orders[Backends[engine.upper()]] + dim_order = engine_orders[Backends(engine)] if geometry is None: return dim_order diff --git a/Wrappers/Python/cil/io/NEXUSDataWriter.py b/Wrappers/Python/cil/io/NEXUSDataWriter.py index daa9c94927..a3839d702c 100644 --- a/Wrappers/Python/cil/io/NEXUSDataWriter.py +++ b/Wrappers/Python/cil/io/NEXUSDataWriter.py @@ -145,23 +145,19 @@ def write(self): ds_data.write_direct(self.data.array) # set up dataset attributes - if (isinstance(self.data, ImageData)): - ds_data.attrs['data_type'] = 'ImageData' - else: - ds_data.attrs['data_type'] = 'AcquisitionData' + ds_data.attrs['data_type'] = 'ImageData' if isinstance(self.data, ImageData) else 'AcquisitionData' for i in range(self.data.number_of_dimensions): - ds_data.attrs['dim{}'.format(i)] = self.data.dimension_labels[i] - - if (isinstance(self.data, AcquisitionData)): + ds_data.attrs[f'dim{i}'] = str(self.data.dimension_labels[i]) + if isinstance(self.data, AcquisitionData): # create group to store configuration f.create_group('entry1/tomo_entry/config') f.create_group('entry1/tomo_entry/config/source') f.create_group('entry1/tomo_entry/config/detector') f.create_group('entry1/tomo_entry/config/rotation_axis') - ds_data.attrs['geometry'] = self.data.geometry.config.system.geometry + ds_data.attrs['geometry'] = str(self.data.geometry.config.system.geometry) ds_data.attrs['dimension'] = self.data.geometry.config.system.dimension ds_data.attrs['num_channels'] = self.data.geometry.config.channels.num_channels diff --git a/Wrappers/Python/cil/io/ZEISSDataReader.py b/Wrappers/Python/cil/io/ZEISSDataReader.py index 41e7ab1908..3bce8b3d0d 100644 --- a/Wrappers/Python/cil/io/ZEISSDataReader.py +++ b/Wrappers/Python/cil/io/ZEISSDataReader.py @@ -127,17 +127,16 @@ def set_up(self, if roi is not None: if metadata['data geometry'] == 'acquisition': - allowed_labels = [item.value for item in AcquisitionDimensionLabels] - zeiss_data_order = {'angle':0, 'vertical':1, 'horizontal':2} + zeiss_data_order = {AcquisitionDimensionLabels.ANGLE: 0, + AcquisitionDimensionLabels.VERTICAL: 1, + AcquisitionDimensionLabels.HORIZONTAL: 2} else: - allowed_labels = [item.value for item in ImageDimensionLabels] - zeiss_data_order = {'vertical':0, 'horizontal_y':1, 'horizontal_x':2} + zeiss_data_order = {ImageDimensionLabels.VERTICAL: 0, + ImageDimensionLabels.HORIZONTAL_Y: 1, + ImageDimensionLabels.HORIZONTAL_X: 2} # check roi labels and create tuple for slicing for key in roi.keys(): - if key not in allowed_labels: - raise Exception("Wrong label, got {0}. Expected dimension labels in {1}, {2}, {3}".format(key,**allowed_labels)) - idx = zeiss_data_order[key] if roi[key] != -1: for i, x in enumerate(roi[key]): @@ -289,4 +288,3 @@ def get_geometry(self): def get_metadata(self): '''return the metadata of the file''' return self._metadata - diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py index ce6738ed19..105ce588c6 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py @@ -53,7 +53,7 @@ def convert_geometry_to_astra_vec_2D(volume_geometry, sinogram_geometry_in): panel = sinogram_geometry.config.panel #get units - degrees = angles.angle_unit == UnitsAngles.DEGREE.value + degrees = angles.angle_unit == UnitsAngles.DEGREE #create a 2D astra geom from 2D CIL geometry, 2D astra geometry has axis flipped compared to 3D volume_geometry_temp = volume_geometry.copy() diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py index ac33c127af..5a0e846b64 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py @@ -55,7 +55,7 @@ def convert_geometry_to_astra_vec_3D(volume_geometry, sinogram_geometry_in): panel = sinogram_geometry.config.panel #get units - degrees = angles.angle_unit == UnitsAngles.DEGREE.value + degrees = angles.angle_unit == UnitsAngles.DEGREE if sinogram_geometry.dimension == '2D': #create a 3D astra geom from 2D CIL geometry diff --git a/Wrappers/Python/test/test_DataContainer.py b/Wrappers/Python/test/test_DataContainer.py index a97d1a89ce..273bfb5bb6 100644 --- a/Wrappers/Python/test/test_DataContainer.py +++ b/Wrappers/Python/test/test_DataContainer.py @@ -865,19 +865,19 @@ def error_message(function_name, test_name): expected = expected_func(data.as_array(), axis=1) expected_dimension_labels = data.dimension_labels[0],data.dimension_labels[2] numpy.testing.assert_almost_equal(result.as_array(), expected, err_msg=error_message(function_name, "'with 1 axis'")) - numpy.testing.assert_equal(result.dimension_labels, expected_dimension_labels, err_msg=error_message(function_name, "'with 1 axis'")) + self.assertEqual(result.dimension_labels, expected_dimension_labels, f"{function_name} 'with 1 axis'") # test specifying axis with an int result = test_func(axis=1) numpy.testing.assert_almost_equal(result.as_array(), expected, err_msg=error_message(function_name, "'with 1 axis'")) - numpy.testing.assert_equal(result.dimension_labels,expected_dimension_labels, err_msg=error_message(function_name, "'with 1 axis'")) + self.assertEqual(result.dimension_labels,expected_dimension_labels, f"{function_name} 'with 1 axis'") # test specifying function in 2 axes result = test_func(axis=(data.dimension_labels[0],data.dimension_labels[1])) numpy.testing.assert_almost_equal(result.as_array(), expected_func(data.as_array(), axis=(0,1)), err_msg=error_message(function_name, "'with 2 axes'")) - numpy.testing.assert_equal(result.dimension_labels,(data.dimension_labels[2],), err_msg=error_message(function_name, "'with 2 axes'")) + self.assertEqual(result.dimension_labels, (data.dimension_labels[2],), f"{function_name} 'with 2 axes'") # test specifying function in 2 axes with an int result = test_func(axis=(0,1)) numpy.testing.assert_almost_equal(result.as_array(), expected_func(data.as_array(), axis=(0,1)), err_msg=error_message(function_name, "'with 2 axes'")) - numpy.testing.assert_equal(result.dimension_labels,(data.dimension_labels[2],), err_msg=error_message(function_name, "'with 2 axes'")) + self.assertEqual(result.dimension_labels, (data.dimension_labels[2],), f"{function_name} 'with 2 axes'") # test specifying function in 3 axes result = test_func(axis=(data.dimension_labels[0],data.dimension_labels[1],data.dimension_labels[2])) numpy.testing.assert_almost_equal(result, expected_func(data.as_array()), err_msg=error_message(function_name, "'with 3 axes'")) @@ -885,10 +885,10 @@ def error_message(function_name, test_name): expected_array = expected_func(data.as_array(), axis = 0) test_func(axis=0, out=out) numpy.testing.assert_almost_equal(out.as_array(), expected_array, err_msg=error_message(function_name, "'of out argument'")) - numpy.testing.assert_equal(out.dimension_labels, (data.dimension_labels[1],data.dimension_labels[2]), err_msg=error_message(function_name, "'of out argument'")) + self.assertEqual(out.dimension_labels, (data.dimension_labels[1],data.dimension_labels[2]), f"{function_name} 'of out argument'") test_func(axis=data.dimension_labels[0], out=out) numpy.testing.assert_almost_equal(out.as_array(), expected_array, err_msg=error_message(function_name, "'of out argument'")) - numpy.testing.assert_equal(out.dimension_labels, (data.dimension_labels[1],data.dimension_labels[2]), err_msg=error_message(function_name, "'of out argument'")) + self.assertEqual(out.dimension_labels, (data.dimension_labels[1],data.dimension_labels[2]), f"{function_name} 'of out argument'") # test providing a numpy array to out out = numpy.zeros((2,2), dtype=data.dtype) test_func(axis=0, out=out) diff --git a/Wrappers/Python/test/test_labels.py b/Wrappers/Python/test/test_labels.py index e9c70e2fec..bc3417ab14 100644 --- a/Wrappers/Python/test/test_labels.py +++ b/Wrappers/Python/test/test_labels.py @@ -15,229 +15,161 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +import unittest import numpy as np -import unittest - -from cil.framework.labels import (_LabelsBase, - FillTypes, UnitsAngles, - AcquisitionTypes, AcquisitionDimensions, +from cil.framework import AcquisitionGeometry, ImageGeometry +from cil.framework.labels import (StrEnum, + FillTypes, UnitsAngles, + AcquisitionTypes, AcquisitionDimensions, ImageDimensionLabels, AcquisitionDimensionLabels, Backends) -from cil.framework import AcquisitionGeometry, ImageGeometry class Test_Lables(unittest.TestCase): - - def test_base_labels(self): - - out_gold = AcquisitionDimensions.DIM3 - - input_good = ["3D", AcquisitionDimensions.DIM3] - input_bad = ["bad_str", "DIM3", UnitsAngles.DEGREE] - - for item in input_good: + def test_labels_strenum(self): + for item in ("3D", "DIM3", AcquisitionDimensions.DIM3): out = AcquisitionDimensions(item) - self.assertEqual(out, out_gold) + self.assertEqual(out, AcquisitionDimensions.DIM3) self.assertTrue(isinstance(out, AcquisitionDimensions)) - - for item in input_bad: + for item in ("bad_str", "4D", "DIM4", UnitsAngles.DEGREE): with self.assertRaises(ValueError): AcquisitionDimensions(item) - - def test_labels_eq(self): - self.assertTrue(_LabelsBase.__eq__(AcquisitionDimensions.DIM3, "3D")) - self.assertTrue(_LabelsBase.__eq__(AcquisitionDimensions.DIM3, AcquisitionDimensions.DIM3)) - - self.assertFalse(_LabelsBase.__eq__(AcquisitionDimensions.DIM3, "DIM3")) - self.assertFalse(_LabelsBase.__eq__(AcquisitionDimensions.DIM3, "2D")) - self.assertFalse(_LabelsBase.__eq__(AcquisitionDimensions.DIM3, AcquisitionDimensions.DIM2)) - self.assertFalse(_LabelsBase.__eq__(AcquisitionDimensions.DIM3, AcquisitionDimensions)) - + def test_labels_strenum_eq(self): + for i in ("3D", "DIM3", AcquisitionDimensions.DIM3): + self.assertEqual(AcquisitionDimensions.DIM3, i) + self.assertEqual(i, AcquisitionDimensions.DIM3) + for i in ("2D", "DIM2", AcquisitionDimensions.DIM2, AcquisitionDimensions): + self.assertNotEqual(AcquisitionDimensions.DIM3, i) def test_labels_contains(self): - self.assertTrue(_LabelsBase.__contains__(AcquisitionDimensions, "3D")) - self.assertTrue(_LabelsBase.__contains__(AcquisitionDimensions, AcquisitionDimensions.DIM3)) - self.assertTrue(_LabelsBase.__contains__(AcquisitionDimensions, AcquisitionDimensions.DIM2)) - - self.assertFalse(_LabelsBase.__contains__(AcquisitionDimensions, "DIM3")) - self.assertFalse(_LabelsBase.__contains__(AcquisitionDimensions, AcquisitionDimensions)) - + for i in ("3D", "DIM3", AcquisitionDimensions.DIM3, AcquisitionDimensions.DIM2): + self.assertIn(i, AcquisitionDimensions) + for i in ("4D", "DIM4", AcquisitionDimensions): + self.assertNotIn(i, AcquisitionDimensions) def test_backends(self): - self.assertTrue('astra' in Backends) - self.assertTrue('cil' in Backends) - self.assertTrue('tigre' in Backends) - self.assertTrue(Backends.ASTRA in Backends) - self.assertTrue(Backends.CIL in Backends) - self.assertTrue(Backends.TIGRE in Backends) + for i in ('ASTRA', 'CIL', 'TIGRE'): + self.assertIn(i, Backends) + self.assertIn(i.lower(), Backends) + self.assertIn(getattr(Backends, i), Backends) def test_fill_types(self): - self.assertTrue('random' in FillTypes) - self.assertTrue('random_int' in FillTypes) - self.assertTrue(FillTypes.RANDOM in FillTypes) - self.assertTrue(FillTypes.RANDOM_INT in FillTypes) - + for i in ('RANDOM', 'RANDOM_INT'): + self.assertIn(i, FillTypes) + self.assertIn(i.lower(), FillTypes) + self.assertIn(getattr(FillTypes, i), FillTypes) + def test_units_angles(self): - self.assertTrue('degree' in UnitsAngles) - self.assertTrue('radian' in UnitsAngles) - self.assertTrue(UnitsAngles.DEGREE in UnitsAngles) - self.assertTrue(UnitsAngles.RADIAN in UnitsAngles) + for i in ('DEGREE', 'RADIAN'): + self.assertIn(i, UnitsAngles) + self.assertIn(i.lower(), UnitsAngles) + self.assertIn(getattr(UnitsAngles, i), UnitsAngles) def test_acquisition_type(self): - self.assertTrue('parallel' in AcquisitionTypes) - self.assertTrue('cone' in AcquisitionTypes) - self.assertTrue(AcquisitionTypes.PARALLEL in AcquisitionTypes) - self.assertTrue(AcquisitionTypes.CONE in AcquisitionTypes) + for i in ('PARALLEL', 'CONE'): + self.assertIn(i, AcquisitionTypes) + self.assertIn(i.lower(), AcquisitionTypes) + self.assertIn(getattr(AcquisitionTypes, i), AcquisitionTypes) def test_acquisition_dimension(self): - self.assertTrue('2D' in AcquisitionDimensions) - self.assertTrue('3D' in AcquisitionDimensions) - self.assertTrue(AcquisitionDimensions.DIM2 in AcquisitionDimensions) - self.assertTrue(AcquisitionDimensions.DIM3 in AcquisitionDimensions) + for i in ('2D', '3D'): + self.assertIn(i, AcquisitionDimensions) + for i in ('DIM2', 'DIM3'): + self.assertIn(i, AcquisitionDimensions) + self.assertIn(i.lower(), AcquisitionDimensions) + self.assertIn(getattr(AcquisitionDimensions, i), AcquisitionDimensions) def test_image_dimension_labels(self): - self.assertTrue('channel' in ImageDimensionLabels) - self.assertTrue('vertical' in ImageDimensionLabels) - self.assertTrue('horizontal_x' in ImageDimensionLabels) - self.assertTrue('horizontal_y' in ImageDimensionLabels) - self.assertTrue(ImageDimensionLabels.CHANNEL in ImageDimensionLabels) - self.assertTrue(ImageDimensionLabels.VERTICAL in ImageDimensionLabels) - self.assertTrue(ImageDimensionLabels.HORIZONTAL_X in ImageDimensionLabels) - self.assertTrue(ImageDimensionLabels.HORIZONTAL_Y in ImageDimensionLabels) + for i in ('CHANNEL', 'VERTICAL', 'HORIZONTAL_X', 'HORIZONTAL_Y'): + self.assertIn(i, ImageDimensionLabels) + self.assertIn(i.lower(), ImageDimensionLabels) + self.assertIn(getattr(ImageDimensionLabels, i), ImageDimensionLabels) def test_image_dimension_labels_default_order(self): - - order_gold = [ImageDimensionLabels.CHANNEL, ImageDimensionLabels.VERTICAL, ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X] - - order = ImageDimensionLabels.get_order_for_engine("cil") - self.assertEqual(order,order_gold ) - - order = ImageDimensionLabels.get_order_for_engine("tigre") - self.assertEqual(order,order_gold) - - order = ImageDimensionLabels.get_order_for_engine("astra") - self.assertEqual(order, order_gold) - - with self.assertRaises(ValueError): - order = AcquisitionDimensionLabels.get_order_for_engine("bad_engine") + order_gold = [ImageDimensionLabels.CHANNEL, 'VERTICAL', 'horizontal_y', 'HORIZONTAL_X'] + for i in ('CIL', 'TIGRE', 'ASTRA'): + self.assertSequenceEqual(ImageDimensionLabels.get_order_for_engine(i), order_gold) + with self.assertRaises((KeyError, ValueError)): + AcquisitionDimensionLabels.get_order_for_engine("bad_engine") def test_image_dimension_labels_get_order(self): ig = ImageGeometry(4, 8, 1, channels=2) ig.set_labels(['channel', 'horizontal_y', 'horizontal_x']) # for 2D all engines have the same order - order_gold = [ImageDimensionLabels.CHANNEL, ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X] - order = ImageDimensionLabels.get_order_for_engine("cil", ig) - self.assertEqual(order, order_gold) - - order = ImageDimensionLabels.get_order_for_engine("tigre", ig) - self.assertEqual(order, order_gold) - - order = ImageDimensionLabels.get_order_for_engine("astra", ig) - self.assertEqual(order, order_gold) + order_gold = [ImageDimensionLabels.CHANNEL, 'HORIZONTAL_Y', 'horizontal_x'] + self.assertSequenceEqual(ImageDimensionLabels.get_order_for_engine('cil', ig), order_gold) + self.assertSequenceEqual(ImageDimensionLabels.get_order_for_engine('tigre', ig), order_gold) + self.assertSequenceEqual(ImageDimensionLabels.get_order_for_engine('astra', ig), order_gold) def test_image_dimension_labels_check_order(self): ig = ImageGeometry(4, 8, 1, channels=2) ig.set_labels(['horizontal_x', 'horizontal_y', 'channel']) - with self.assertRaises(ValueError): - ImageDimensionLabels.check_order_for_engine("cil", ig) - - with self.assertRaises(ValueError): - ImageDimensionLabels.check_order_for_engine("tigre", ig) - - with self.assertRaises(ValueError): - ImageDimensionLabels.check_order_for_engine("astra", ig) + for i in ('cil', 'tigre', 'astra'): + with self.assertRaises(ValueError): + ImageDimensionLabels.check_order_for_engine(i, ig) ig.set_labels(['channel', 'horizontal_y', 'horizontal_x']) - self.assertTrue( ImageDimensionLabels.check_order_for_engine("cil", ig)) - self.assertTrue( ImageDimensionLabels.check_order_for_engine("tigre", ig)) - self.assertTrue( ImageDimensionLabels.check_order_for_engine("astra", ig)) + self.assertTrue(ImageDimensionLabels.check_order_for_engine("cil", ig)) + self.assertTrue(ImageDimensionLabels.check_order_for_engine("tigre", ig)) + self.assertTrue(ImageDimensionLabels.check_order_for_engine("astra", ig)) def test_acquisition_dimension_labels(self): - self.assertTrue('channel' in AcquisitionDimensionLabels) - self.assertTrue('angle' in AcquisitionDimensionLabels) - self.assertTrue('vertical' in AcquisitionDimensionLabels) - self.assertTrue('horizontal' in AcquisitionDimensionLabels) - self.assertTrue(AcquisitionDimensionLabels.CHANNEL in AcquisitionDimensionLabels) - self.assertTrue(AcquisitionDimensionLabels.ANGLE in AcquisitionDimensionLabels) - self.assertTrue(AcquisitionDimensionLabels.VERTICAL in AcquisitionDimensionLabels) - self.assertTrue(AcquisitionDimensionLabels.HORIZONTAL in AcquisitionDimensionLabels) + for i in ('CHANNEL', 'ANGLE', 'VERTICAL', 'HORIZONTAL'): + self.assertIn(i, AcquisitionDimensionLabels) + self.assertIn(i.lower(), AcquisitionDimensionLabels) + self.assertIn(getattr(AcquisitionDimensionLabels, i), AcquisitionDimensionLabels) def test_acquisition_dimension_labels_default_order(self): - order = AcquisitionDimensionLabels.get_order_for_engine("cil") - self.assertEqual(order, [AcquisitionDimensionLabels.CHANNEL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.VERTICAL, AcquisitionDimensionLabels.HORIZONTAL]) + self.assertEqual(AcquisitionDimensionLabels.get_order_for_engine('CIL'), [AcquisitionDimensionLabels.CHANNEL, 'ANGLE', 'vertical', 'HORIZONTAL']) + self.assertEqual(AcquisitionDimensionLabels.get_order_for_engine(Backends.TIGRE), ['CHANNEL', 'ANGLE', 'VERTICAL', 'HORIZONTAL']) + self.assertEqual(AcquisitionDimensionLabels.get_order_for_engine('astra'), ['CHANNEL', 'VERTICAL', 'ANGLE', 'HORIZONTAL']) - order = AcquisitionDimensionLabels.get_order_for_engine("tigre") - self.assertEqual(order, [AcquisitionDimensionLabels.CHANNEL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.VERTICAL, AcquisitionDimensionLabels.HORIZONTAL]) - - order = AcquisitionDimensionLabels.get_order_for_engine("astra") - self.assertEqual(order, [AcquisitionDimensionLabels.CHANNEL, AcquisitionDimensionLabels.VERTICAL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.HORIZONTAL]) - - with self.assertRaises(ValueError): - order = AcquisitionDimensionLabels.get_order_for_engine("bad_engine") + with self.assertRaises((KeyError, ValueError)): + AcquisitionDimensionLabels.get_order_for_engine("bad_engine") def test_acquisition_dimension_labels_get_order(self): - ag = AcquisitionGeometry.create_Parallel2D()\ .set_angles(np.arange(0,16 , 1), angle_unit="degree")\ .set_panel(4)\ .set_channels(8)\ .set_labels(['angle', 'horizontal', 'channel']) - - # for 2D all engines have the same order - order_gold = [AcquisitionDimensionLabels.CHANNEL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.HORIZONTAL] - order = AcquisitionDimensionLabels.get_order_for_engine("cil", ag) - self.assertEqual(order, order_gold) - - order = AcquisitionDimensionLabels.get_order_for_engine("tigre", ag) - self.assertEqual(order, order_gold) - - order = AcquisitionDimensionLabels.get_order_for_engine("astra", ag) - self.assertEqual(order, order_gold) + # for 2D all engines have the same order + order_gold = [AcquisitionDimensionLabels.CHANNEL, 'ANGLE', 'horizontal'] + self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine('CIL', ag), order_gold) + self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine('TIGRE', ag), order_gold) + self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine('ASTRA', ag), order_gold) ag = AcquisitionGeometry.create_Parallel3D()\ .set_angles(np.arange(0,16 , 1), angle_unit="degree")\ .set_panel((4,2))\ .set_labels(['angle', 'horizontal', 'vertical']) - - - order_gold = [AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.VERTICAL, AcquisitionDimensionLabels.HORIZONTAL] - order = AcquisitionDimensionLabels.get_order_for_engine("cil", ag) - self.assertEqual(order, order_gold) - - order = AcquisitionDimensionLabels.get_order_for_engine("tigre", ag) - self.assertEqual(order, order_gold) - - order_gold = [AcquisitionDimensionLabels.VERTICAL, AcquisitionDimensionLabels.ANGLE, AcquisitionDimensionLabels.HORIZONTAL] - order = AcquisitionDimensionLabels.get_order_for_engine("astra", ag) - self.assertEqual(order, order_gold) + order_gold = [AcquisitionDimensionLabels.ANGLE, 'VERTICAL', 'horizontal'] + self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine("cil", ag), order_gold) + self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine("tigre", ag), order_gold) + order_gold = [AcquisitionDimensionLabels.VERTICAL, 'ANGLE', 'horizontal'] + self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine("astra", ag), order_gold) def test_acquisition_dimension_labels_check_order(self): - ag = AcquisitionGeometry.create_Parallel3D()\ .set_angles(np.arange(0,16 , 1), angle_unit="degree")\ .set_panel((8,4))\ .set_channels(2)\ .set_labels(['angle', 'horizontal', 'channel', 'vertical']) - - with self.assertRaises(ValueError): - AcquisitionDimensionLabels.check_order_for_engine("cil", ag) - with self.assertRaises(ValueError): - AcquisitionDimensionLabels.check_order_for_engine("tigre", ag) - - with self.assertRaises(ValueError): - AcquisitionDimensionLabels.check_order_for_engine("astra", ag) + for i in ('cil', 'tigre', 'astra'): + with self.assertRaises(ValueError): + AcquisitionDimensionLabels.check_order_for_engine(i, ag) ag.set_labels(['channel', 'angle', 'vertical', 'horizontal']) - self.assertTrue( AcquisitionDimensionLabels.check_order_for_engine("cil", ag)) - self.assertTrue( AcquisitionDimensionLabels.check_order_for_engine("tigre", ag)) + self.assertTrue(AcquisitionDimensionLabels.check_order_for_engine("cil", ag)) + self.assertTrue(AcquisitionDimensionLabels.check_order_for_engine("tigre", ag)) ag.set_labels(['channel', 'vertical', 'angle', 'horizontal']) - self.assertTrue( AcquisitionDimensionLabels.check_order_for_engine("astra", ag)) + self.assertTrue(AcquisitionDimensionLabels.check_order_for_engine("astra", ag)) From 73cbdfa7337a239ac7b488c442fb96ab692acc19 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 19 Aug 2024 21:44:43 +0100 Subject: [PATCH 59/72] use more tuples --- .../cil/framework/acquisition_geometry.py | 5 ++-- .../Python/cil/framework/data_container.py | 6 ++--- .../Python/cil/framework/image_geometry.py | 5 ++-- Wrappers/Python/cil/framework/labels.py | 24 +++++++++---------- Wrappers/Python/test/test_labels.py | 18 +++++++------- 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 9dd600f0a1..20e55bdae0 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -1737,9 +1737,10 @@ def dimension_labels(self): ] try: - labels = list(self._dimension_labels) + labels = self._dimension_labels except AttributeError: - labels = labels_default.copy() + labels = labels_default + labels = list(labels) #remove from list labels where len == 1 # diff --git a/Wrappers/Python/cil/framework/data_container.py b/Wrappers/Python/cil/framework/data_container.py index 4053eea411..4febd7a449 100644 --- a/Wrappers/Python/cil/framework/data_container.py +++ b/Wrappers/Python/cil/framework/data_container.py @@ -56,8 +56,8 @@ def dimension_labels(self): def dimension_labels(self, val): if val is None: self._dimension_labels = None - elif len(list(val))==self.number_of_dimensions: - self._dimension_labels = tuple(val) + elif len(val_tuple := tuple(val)) == self.number_of_dimensions: + self._dimension_labels = val_tuple else: raise ValueError("dimension_labels expected a list containing {0} strings got {1}".format(self.number_of_dimensions, val)) @@ -260,7 +260,7 @@ def fill(self, array, **dimension): else: axis = [':']* self.number_of_dimensions - dimension_labels = list(self.dimension_labels) + dimension_labels = tuple(self.dimension_labels) for k,v in dimension.items(): i = dimension_labels.index(k) axis[i] = v diff --git a/Wrappers/Python/cil/framework/image_geometry.py b/Wrappers/Python/cil/framework/image_geometry.py index ab9049ac83..e280638899 100644 --- a/Wrappers/Python/cil/framework/image_geometry.py +++ b/Wrappers/Python/cil/framework/image_geometry.py @@ -94,9 +94,10 @@ def dimension_labels(self): self.voxel_num_x] try: - labels = list(self._dimension_labels) + labels = self._dimension_labels except AttributeError: - labels = labels_default.copy() + labels = labels_default + labels = list(labels) for i, x in enumerate(shape_default): if x == 0 or x==1: diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index 9eac7530c6..420bf7136e 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -94,7 +94,7 @@ class ImageDimensionLabels(StrEnum): HORIZONTAL_Y = auto() @classmethod - def get_order_for_engine(cls, engine: str, geometry=None) -> list: + def get_order_for_engine(cls, engine: str, geometry=None) -> tuple: """ Returns the order of dimensions for a specific engine and geometry. @@ -103,13 +103,13 @@ def get_order_for_engine(cls, engine: str, geometry=None) -> list: geometry: ImageGeometry, optional If unspecified, the default order is returned. """ - order = [cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X] + order = cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X engine_orders = {Backends.ASTRA: order, Backends.TIGRE: order, Backends.CIL: order} dim_order = engine_orders[Backends(engine)] if geometry is None: return dim_order - return [label for label in dim_order if label in geometry.dimension_labels] + return tuple(label for label in dim_order if label in geometry.dimension_labels) @classmethod def check_order_for_engine(cls, engine: str, geometry) -> bool: @@ -125,11 +125,11 @@ def check_order_for_engine(cls, engine: str, geometry) -> bool: ValueError if the order of dimensions is incorrect. """ order_requested = cls.get_order_for_engine(engine, geometry) - if order_requested == list(geometry.dimension_labels): + if order_requested == tuple(geometry.dimension_labels): return True raise ValueError( f"Expected dimension_label order {order_requested}" - f" got {list(geometry.dimension_labels)}." + f" got {tuple(geometry.dimension_labels)}." f" Try using `data.reorder('{engine}')` to permute for {engine}") @@ -152,7 +152,7 @@ class AcquisitionDimensionLabels(StrEnum): HORIZONTAL = auto() @classmethod - def get_order_for_engine(cls, engine: str, geometry=None) -> list: + def get_order_for_engine(cls, engine: str, geometry=None) -> tuple: """ Returns the order of dimensions for a specific engine and geometry. @@ -162,14 +162,14 @@ def get_order_for_engine(cls, engine: str, geometry=None) -> list: If unspecified, the default order is returned. """ engine_orders = { - Backends.ASTRA: [cls.CHANNEL, cls.VERTICAL, cls.ANGLE, cls.HORIZONTAL], - Backends.TIGRE: [cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL], - Backends.CIL: [cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL]} + Backends.ASTRA: (cls.CHANNEL, cls.VERTICAL, cls.ANGLE, cls.HORIZONTAL), + Backends.TIGRE: (cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL), + Backends.CIL: (cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL)} dim_order = engine_orders[Backends(engine)] if geometry is None: return dim_order - return [label for label in dim_order if label in geometry.dimension_labels] + return tuple(label for label in dim_order if label in geometry.dimension_labels) @classmethod def check_order_for_engine(cls, engine: str, geometry) -> bool: @@ -185,11 +185,11 @@ def check_order_for_engine(cls, engine: str, geometry) -> bool: ValueError if the order of dimensions is incorrect. """ order_requested = cls.get_order_for_engine(engine, geometry) - if order_requested == list(geometry.dimension_labels): + if order_requested == tuple(geometry.dimension_labels): return True raise ValueError( f"Expected dimension_label order {order_requested}," - f" got {list(geometry.dimension_labels)}." + f" got {tuple(geometry.dimension_labels)}." f" Try using `data.reorder('{engine}')` to permute for {engine}") diff --git a/Wrappers/Python/test/test_labels.py b/Wrappers/Python/test/test_labels.py index bc3417ab14..3efdc57f65 100644 --- a/Wrappers/Python/test/test_labels.py +++ b/Wrappers/Python/test/test_labels.py @@ -88,7 +88,7 @@ def test_image_dimension_labels(self): self.assertIn(getattr(ImageDimensionLabels, i), ImageDimensionLabels) def test_image_dimension_labels_default_order(self): - order_gold = [ImageDimensionLabels.CHANNEL, 'VERTICAL', 'horizontal_y', 'HORIZONTAL_X'] + order_gold = ImageDimensionLabels.CHANNEL, 'VERTICAL', 'horizontal_y', 'HORIZONTAL_X' for i in ('CIL', 'TIGRE', 'ASTRA'): self.assertSequenceEqual(ImageDimensionLabels.get_order_for_engine(i), order_gold) @@ -100,7 +100,7 @@ def test_image_dimension_labels_get_order(self): ig.set_labels(['channel', 'horizontal_y', 'horizontal_x']) # for 2D all engines have the same order - order_gold = [ImageDimensionLabels.CHANNEL, 'HORIZONTAL_Y', 'horizontal_x'] + order_gold = ImageDimensionLabels.CHANNEL, 'HORIZONTAL_Y', 'horizontal_x' self.assertSequenceEqual(ImageDimensionLabels.get_order_for_engine('cil', ig), order_gold) self.assertSequenceEqual(ImageDimensionLabels.get_order_for_engine('tigre', ig), order_gold) self.assertSequenceEqual(ImageDimensionLabels.get_order_for_engine('astra', ig), order_gold) @@ -125,9 +125,11 @@ def test_acquisition_dimension_labels(self): self.assertIn(getattr(AcquisitionDimensionLabels, i), AcquisitionDimensionLabels) def test_acquisition_dimension_labels_default_order(self): - self.assertEqual(AcquisitionDimensionLabels.get_order_for_engine('CIL'), [AcquisitionDimensionLabels.CHANNEL, 'ANGLE', 'vertical', 'HORIZONTAL']) - self.assertEqual(AcquisitionDimensionLabels.get_order_for_engine(Backends.TIGRE), ['CHANNEL', 'ANGLE', 'VERTICAL', 'HORIZONTAL']) - self.assertEqual(AcquisitionDimensionLabels.get_order_for_engine('astra'), ['CHANNEL', 'VERTICAL', 'ANGLE', 'HORIZONTAL']) + gold = AcquisitionDimensionLabels.CHANNEL, 'ANGLE', 'vertical', 'HORIZONTAL' + self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine('CIL'), gold) + self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine(Backends.TIGRE), gold) + gold = 'CHANNEL', 'VERTICAL', 'ANGLE', 'HORIZONTAL' + self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine('astra'), gold) with self.assertRaises((KeyError, ValueError)): AcquisitionDimensionLabels.get_order_for_engine("bad_engine") @@ -140,7 +142,7 @@ def test_acquisition_dimension_labels_get_order(self): .set_labels(['angle', 'horizontal', 'channel']) # for 2D all engines have the same order - order_gold = [AcquisitionDimensionLabels.CHANNEL, 'ANGLE', 'horizontal'] + order_gold = AcquisitionDimensionLabels.CHANNEL, 'ANGLE', 'horizontal' self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine('CIL', ag), order_gold) self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine('TIGRE', ag), order_gold) self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine('ASTRA', ag), order_gold) @@ -150,10 +152,10 @@ def test_acquisition_dimension_labels_get_order(self): .set_panel((4,2))\ .set_labels(['angle', 'horizontal', 'vertical']) - order_gold = [AcquisitionDimensionLabels.ANGLE, 'VERTICAL', 'horizontal'] + order_gold = AcquisitionDimensionLabels.ANGLE, 'VERTICAL', 'horizontal' self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine("cil", ag), order_gold) self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine("tigre", ag), order_gold) - order_gold = [AcquisitionDimensionLabels.VERTICAL, 'ANGLE', 'horizontal'] + order_gold = AcquisitionDimensionLabels.VERTICAL, 'ANGLE', 'horizontal' self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine("astra", ag), order_gold) def test_acquisition_dimension_labels_check_order(self): From 96ec5ab14656f7897c90254a4791220915cc75ac Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 19 Aug 2024 22:50:35 +0100 Subject: [PATCH 60/72] combine Acquisition{Types,Dimensions}(StrEnum) => AcquisitionType(Flag) --- Wrappers/Python/cil/framework/__init__.py | 2 +- .../cil/framework/acquisition_geometry.py | 33 ++++++------ Wrappers/Python/cil/framework/labels.py | 51 ++++++++++++------- Wrappers/Python/cil/io/NEXUSDataWriter.py | 7 +-- Wrappers/Python/cil/io/NikonDataReader.py | 6 +-- .../astra/operators/ProjectionOperator.py | 4 +- .../cil/plugins/astra/processors/FBP.py | 6 +-- .../plugins/astra/processors/FBP_Flexible.py | 6 +-- .../utilities/convert_geometry_to_astra.py | 13 ++--- .../convert_geometry_to_astra_vec_3D.py | 4 +- Wrappers/Python/cil/plugins/tigre/Geometry.py | 4 +- .../cil/processors/CofR_image_sharpness.py | 19 ++----- .../cil/processors/CofR_xcorrelation.py | 13 ++--- Wrappers/Python/cil/recon/FBP.py | 18 +++---- Wrappers/Python/cil/utilities/display.py | 9 ++-- Wrappers/Python/test/test_labels.py | 51 ++++++++----------- Wrappers/Python/test/utils_projectors.py | 8 +-- 17 files changed, 116 insertions(+), 138 deletions(-) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index ab99ffced3..8a37b5767a 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -27,4 +27,4 @@ from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer from .block import BlockDataContainer, BlockGeometry from .partitioner import Partitioner -from .labels import AcquisitionDimensionLabels, ImageDimensionLabels, FillTypes, UnitsAngles, AcquisitionTypes, AcquisitionDimensions +from .labels import AcquisitionDimensionLabels, ImageDimensionLabels, FillTypes, UnitsAngles, AcquisitionType diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 20e55bdae0..36799f94a4 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -22,7 +22,7 @@ import numpy -from .labels import AcquisitionDimensionLabels, UnitsAngles, AcquisitionTypes, FillTypes, AcquisitionDimensions +from .labels import AcquisitionDimensionLabels, UnitsAngles, AcquisitionType, FillTypes from .acquisition_data import AcquisitionData from .image_geometry import ImageGeometry @@ -186,10 +186,7 @@ class SystemConfiguration(object): @property def dimension(self): - if self._dimension == 2: - return AcquisitionDimensions.DIM2.value - else: - return AcquisitionDimensions.DIM3.value + return AcquisitionType.DIM2 if self._dimension == 2 else AcquisitionType.DIM3 @dimension.setter def dimension(self,val): @@ -203,8 +200,8 @@ def geometry(self): return self._geometry @geometry.setter - def geometry(self,val): - self._geometry = AcquisitionTypes(val) + def geometry(self, val): + self._geometry = AcquisitionType(val) def __init__(self, dof, geometry, units='units'): """Initialises the system component attributes for the acquisition type @@ -213,7 +210,7 @@ def __init__(self, dof, geometry, units='units'): self.geometry = geometry self.units = units - if self.geometry == AcquisitionTypes.PARALLEL: + if AcquisitionType.PARALLEL & self.geometry: self.ray = DirectionVector(dof) else: self.source = PositionVector(dof) @@ -344,7 +341,7 @@ class Parallel2D(SystemConfiguration): def __init__ (self, ray_direction, detector_pos, detector_direction_x, rotation_axis_pos, units='units'): """Constructor method """ - super(Parallel2D, self).__init__(dof=2, geometry=AcquisitionTypes.PARALLEL, units=units) + super(Parallel2D, self).__init__(dof=2, geometry=AcquisitionType.PARALLEL, units=units) #source self.ray.direction = ray_direction @@ -518,7 +515,7 @@ class Parallel3D(SystemConfiguration): def __init__ (self, ray_direction, detector_pos, detector_direction_x, detector_direction_y, rotation_axis_pos, rotation_axis_direction, units='units'): """Constructor method """ - super(Parallel3D, self).__init__(dof=3, geometry=AcquisitionTypes.PARALLEL, units=units) + super(Parallel3D, self).__init__(dof=3, geometry=AcquisitionType.PARALLEL, units=units) #source self.ray.direction = ray_direction @@ -803,7 +800,7 @@ class Cone2D(SystemConfiguration): def __init__ (self, source_pos, detector_pos, detector_direction_x, rotation_axis_pos, units='units'): """Constructor method """ - super(Cone2D, self).__init__(dof=2, geometry=AcquisitionTypes.CONE, units=units) + super(Cone2D, self).__init__(dof=2, geometry=AcquisitionType.CONE, units=units) #source self.source.position = source_pos @@ -982,7 +979,7 @@ class Cone3D(SystemConfiguration): def __init__ (self, source_pos, detector_pos, detector_direction_x, detector_direction_y, rotation_axis_pos, rotation_axis_direction, units='units'): """Constructor method """ - super(Cone3D, self).__init__(dof=3, geometry=AcquisitionTypes.CONE, units=units) + super(Cone3D, self).__init__(dof=3, geometry=AcquisitionType.CONE, units=units) #source self.source.position = source_pos @@ -1819,7 +1816,7 @@ def get_centre_of_rotation(self, distance_units='default', angle_units='radian') offset = offset_distance/ self.config.panel.pixel_size[0] offset_units = 'pixels' - if self.dimension == '3D' and self.config.panel.pixel_size[0] != self.config.panel.pixel_size[1]: + if AcquisitionType.DIM3 & self.dimension and self.config.panel.pixel_size[0] != self.config.panel.pixel_size[1]: #if aspect ratio of pixels isn't 1:1 need to convert angle by new ratio y_pix = 1 /self.config.panel.pixel_size[1] x_pix = math.tan(angle_rad)/self.config.panel.pixel_size[0] @@ -1884,7 +1881,7 @@ def set_centre_of_rotation(self, offset=0.0, distance_units='default', angle=0.0 else: raise ValueError("`distance_units` is not recognised. Must be 'default' or 'pixels'. Got {}".format(distance_units)) - if self.dimension == '2D': + if AcquisitionType.DIM2 & self.dimension: self.config.system.set_centre_of_rotation(offset_distance) else: self.config.system.set_centre_of_rotation(offset_distance, angle_rad) @@ -1921,7 +1918,7 @@ def set_centre_of_rotation_by_slice(self, offset1, slice_index1=None, offset2=No if not hasattr(self.config.system, 'set_centre_of_rotation'): raise NotImplementedError() - if self.dimension == '2D': + if AcquisitionType.DIM2 & self.dimension: if offset2 is not None: warnings.warn("2D so offset2 is ingored", UserWarning, stacklevel=2) self.set_centre_of_rotation(offset1) @@ -2118,7 +2115,7 @@ def copy(self): def get_centre_slice(self): '''returns a 2D AcquisitionGeometry that corresponds to the centre slice of the input''' - if self.dimension == '2D': + if AcquisitionType.DIM2 & self.dimension: return self AG_2D = copy.deepcopy(self) @@ -2133,7 +2130,7 @@ def get_ImageGeometry(self, resolution=1.0): num_voxel_xy = int(numpy.ceil(self.config.panel.num_pixels[0] * resolution)) voxel_size_xy = self.config.panel.pixel_size[0] / (resolution * self.magnification) - if self.dimension == '3D': + if AcquisitionType.DIM3 & self.dimension: num_voxel_z = int(numpy.ceil(self.config.panel.num_pixels[1] * resolution)) voxel_size_z = self.config.panel.pixel_size[1] / (resolution * self.magnification) else: @@ -2161,7 +2158,7 @@ def get_slice(self, channel=None, angle=None, vertical=None, horizontal=None): geometry_new.config.angles.angle_data = geometry_new.config.angles.angle_data[angle] if vertical is not None: - if geometry_new.geom_type == AcquisitionTypes.PARALLEL or vertical == 'centre' or abs(geometry_new.pixel_num_v/2 - vertical) < 1e-6: + if AcquisitionType.PARALLEL & geometry_new.geom_type or vertical == 'centre' or abs(geometry_new.pixel_num_v/2 - vertical) < 1e-6: geometry_new = geometry_new.get_centre_slice() else: raise ValueError("Can only subset centre slice geometry on cone-beam data. Expected vertical = 'centre'. Got vertical = {0}".format(vertical)) diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index 420bf7136e..26f3947b46 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -15,7 +15,7 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from enum import Enum, auto, unique +from enum import Enum, Flag as _Flag, auto, unique try: from enum import EnumType except ImportError: # Python<3.11 @@ -40,7 +40,6 @@ def _missing_(cls, value: str): return cls.__members__.get(value.upper(), None) def __eq__(self, value: str) -> bool: - """Uses value.upper() for case-insensitivity""" try: value = self.__class__[value.upper()] except (KeyError, ValueError, AttributeError): @@ -60,7 +59,6 @@ def _generate_next_value_(name: str, start, count, last_values) -> str: return name.lower() - class Backends(StrEnum): """ Available backends for CIL. @@ -228,27 +226,46 @@ class UnitsAngles(StrEnum): RADIAN = auto() -class AcquisitionTypes(StrEnum): +class _FlagMeta(EnumType): + """Python<3.12 requires this in a metaclass (rather than directly in Flag)""" + def __contains__(self, item) -> bool: + return item.upper() in self.__members__ if isinstance(item, str) else super().__contains__(item) + + +@unique +class Flag(_Flag, metaclass=_FlagMeta): + """Case-insensitive Flag""" + @classmethod + def _missing_(cls, value): + return cls.__members__.get(value.upper(), None) if isinstance(value, str) else super()._missing_(value) + + def __eq__(self, value: str) -> bool: + return super().__eq__(self.__class__[value.upper()] if isinstance(value, str) else value) + + +class AcquisitionType(Flag): """ - Available acquisition types. + Available acquisition types & dimensions. Attributes ---------- PARALLEL: Parallel beam. CONE: Cone beam. + DIM2: 2D acquisition. + DIM3: 3D acquisition. """ PARALLEL = auto() CONE = auto() + DIM2 = auto() + DIM3 = auto() - -class AcquisitionDimensions(StrEnum): - """ - Available acquisition dimensions. - - Attributes - ---------- - DIM2 ('2D'): 2D acquisition. - DIM3 ('3D'): 3D acquisition. - """ - DIM2 = "2D" - DIM3 = "3D" + @classmethod + def _missing_(cls, value): + """2D/3D aliases""" + if isinstance(value, str): + value = {'2D': 'DIM2', '3D': 'DIM3'}.get(value.upper(), value) + return super()._missing_(value) + + def __str__(self) -> str: + """2D/3D special handling""" + return '2D' if self == self.DIM2 else '3D' if self == self.DIM3 else self.name diff --git a/Wrappers/Python/cil/io/NEXUSDataWriter.py b/Wrappers/Python/cil/io/NEXUSDataWriter.py index a3839d702c..e15f4c5025 100644 --- a/Wrappers/Python/cil/io/NEXUSDataWriter.py +++ b/Wrappers/Python/cil/io/NEXUSDataWriter.py @@ -18,7 +18,8 @@ import numpy as np import os -from cil.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry +from cil.framework import AcquisitionData, ImageData +from cil.framework.labels import AcquisitionType from cil.version import version import datetime from cil.io import utilities @@ -158,7 +159,7 @@ def write(self): f.create_group('entry1/tomo_entry/config/rotation_axis') ds_data.attrs['geometry'] = str(self.data.geometry.config.system.geometry) - ds_data.attrs['dimension'] = self.data.geometry.config.system.dimension + ds_data.attrs['dimension'] = str(self.data.geometry.config.system.dimension) ds_data.attrs['num_channels'] = self.data.geometry.config.channels.num_channels f.create_dataset('entry1/tomo_entry/config/detector/direction_x', @@ -192,7 +193,7 @@ def write(self): ds_data.attrs['pixel_size_h'] = self.data.geometry.config.panel.pixel_size[0] ds_data.attrs['panel_origin'] = self.data.geometry.config.panel.origin - if self.data.geometry.config.system.dimension == '3D': + if AcquisitionType.DIM3 & self.data.geometry.config.system.dimension: f.create_dataset('entry1/tomo_entry/config/detector/direction_y', (self.data.geometry.config.system.detector.direction_y.shape), dtype = 'float32', diff --git a/Wrappers/Python/cil/io/NikonDataReader.py b/Wrappers/Python/cil/io/NikonDataReader.py index ca7962d795..51b72ba847 100644 --- a/Wrappers/Python/cil/io/NikonDataReader.py +++ b/Wrappers/Python/cil/io/NikonDataReader.py @@ -16,9 +16,9 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import AcquisitionData, AcquisitionGeometry +from cil.framework import AcquisitionGeometry +from cil.framework.labels import AcquisitionType from cil.io.TIFF import TIFFStackReader -import warnings import numpy as np import os @@ -334,7 +334,7 @@ def get_geometry(self): def get_roi(self): '''returns the roi''' roi = self._roi_par[:] - if self._ag.dimension == '2D': + if AcquisitionType.DIM2 & self._ag.dimension: roi.pop(1) roidict = {} diff --git a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py index af7a70fa7d..d62d7e4281 100644 --- a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py @@ -18,7 +18,7 @@ import logging -from cil.framework import BlockGeometry, AcquisitionDimensionLabels, ImageDimensionLabels +from cil.framework import BlockGeometry, AcquisitionDimensionLabels, ImageDimensionLabels, AcquisitionType from cil.optimisation.operators import BlockOperator, LinearOperator, ChannelwiseOperator from cil.plugins.astra.operators import AstraProjector2D, AstraProjector3D @@ -127,7 +127,7 @@ def __init__(self, if device == 'gpu': operator = AstraProjector3D(volume_geometry_sc, sinogram_geometry_sc) - elif self.sinogram_geometry.dimension == '2D': + elif AcquisitionType.DIM2 & self.sinogram_geometry.dimension: operator = AstraProjector2D(volume_geometry_sc, sinogram_geometry_sc, device=device) diff --git a/Wrappers/Python/cil/plugins/astra/processors/FBP.py b/Wrappers/Python/cil/plugins/astra/processors/FBP.py index c52006eef9..b750412e3c 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/FBP.py +++ b/Wrappers/Python/cil/plugins/astra/processors/FBP.py @@ -15,9 +15,7 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -import warnings - -from cil.framework import DataProcessor, ImageDimensionLabels, AcquisitionDimensionLabels +from cil.framework import DataProcessor, ImageDimensionLabels, AcquisitionDimensionLabels, AcquisitionType from cil.plugins.astra.processors.FBP_Flexible import FBP_Flexible from cil.plugins.astra.processors.FDK_Flexible import FDK_Flexible from cil.plugins.astra.processors.FBP_Flexible import FBP_CPU @@ -81,7 +79,7 @@ def __init__(self, image_geometry=None, acquisition_geometry=None, device='gpu') if acquisition_geometry.geom_type == 'cone': raise NotImplementedError("Cannot process cone-beam data without a GPU") - if acquisition_geometry.dimension == '2D': + if AcquisitionType.DIM2 & acquisition_geometry.dimension: processor = FBP_CPU(image_geometry, acquisition_geometry) else: raise NotImplementedError("Cannot process 3D data without a GPU") diff --git a/Wrappers/Python/cil/plugins/astra/processors/FBP_Flexible.py b/Wrappers/Python/cil/plugins/astra/processors/FBP_Flexible.py index 138caa1d05..4bed64064b 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/FBP_Flexible.py +++ b/Wrappers/Python/cil/plugins/astra/processors/FBP_Flexible.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import AcquisitionGeometry, Processor, ImageData +from cil.framework import AcquisitionGeometry, Processor, ImageData, AcquisitionType from cil.plugins.astra.processors.FDK_Flexible import FDK_Flexible from cil.plugins.astra.utilities import convert_geometry_to_astra_vec_3D, convert_geometry_to_astra import logging @@ -76,7 +76,7 @@ def __init__(self, volume_geometry, detector_position = sino_geom_cone.config.system.detector.position detector_direction_x = sino_geom_cone.config.system.detector.direction_x - if sino_geom_cone.dimension == '2D': + if AcquisitionType.DIM2 & sino_geom_cone.dimension: tmp = AcquisitionGeometry.create_Cone2D(cone_source, detector_position, detector_direction_x) else: detector_direction_y = sino_geom_cone.config.system.detector.direction_y @@ -141,7 +141,7 @@ def check_input(self, dataset): raise ValueError("Expected input data to be parallel beam geometry , got {0}"\ .format(self.sinogram_geometry.geom_type)) - if self.sinogram_geometry.dimension != '2D': + if not AcquisitionType.DIM2 & self.sinogram_geometry.dimension: raise ValueError("Expected input data to be 2D , got {0}"\ .format(self.sinogram_geometry.dimension)) diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py index 6a83ee0194..09526b581e 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py @@ -19,7 +19,7 @@ import astra import numpy as np -from cil.framework import UnitsAngles +from cil.framework import AcquisitionType, UnitsAngles def convert_geometry_to_astra(volume_geometry, sinogram_geometry): """ @@ -39,13 +39,8 @@ def convert_geometry_to_astra(volume_geometry, sinogram_geometry): The ASTRA vol_geom and proj_geom """ - # determine if the geometry is 2D or 3D - - if sinogram_geometry.pixel_num_v > 1: - dimension = '3D' - else: - dimension = '2D' + dimension = AcquisitionType.DIM3 if sinogram_geometry.pixel_num_v > 1 else AcquisitionType.DIM2 #get units @@ -54,7 +49,7 @@ def convert_geometry_to_astra(volume_geometry, sinogram_geometry): else: angles_rad = sinogram_geometry.config.angles.angle_data - if dimension == '2D': + if AcquisitionType.DIM2 & dimension: vol_geom = astra.create_vol_geom(volume_geometry.voxel_num_y, volume_geometry.voxel_num_x, volume_geometry.get_min_x(), @@ -77,7 +72,7 @@ def convert_geometry_to_astra(volume_geometry, sinogram_geometry): else: NotImplemented - elif dimension == '3D': + elif AcquisitionType.DIM3 & dimension: vol_geom = astra.create_vol_geom(volume_geometry.voxel_num_y, volume_geometry.voxel_num_x, volume_geometry.voxel_num_z, diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py index 5a0e846b64..ca87c712a9 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py @@ -19,7 +19,7 @@ import astra import numpy as np -from cil.framework import UnitsAngles +from cil.framework import AcquisitionType, UnitsAngles def convert_geometry_to_astra_vec_3D(volume_geometry, sinogram_geometry_in): @@ -57,7 +57,7 @@ def convert_geometry_to_astra_vec_3D(volume_geometry, sinogram_geometry_in): #get units degrees = angles.angle_unit == UnitsAngles.DEGREE - if sinogram_geometry.dimension == '2D': + if AcquisitionType.DIM2 & sinogram_geometry.dimension: #create a 3D astra geom from 2D CIL geometry volume_geometry_temp = volume_geometry.copy() volume_geometry_temp.voxel_num_z = 1 diff --git a/Wrappers/Python/cil/plugins/tigre/Geometry.py b/Wrappers/Python/cil/plugins/tigre/Geometry.py index 403b3822b3..61c7a9cd3b 100644 --- a/Wrappers/Python/cil/plugins/tigre/Geometry.py +++ b/Wrappers/Python/cil/plugins/tigre/Geometry.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import UnitsAngles +from cil.framework import AcquisitionType, UnitsAngles import numpy as np try: @@ -109,7 +109,7 @@ def __init__(self, ig, ag): self.sDetector = self.dDetector * self.nDetector # total size of the detector (mm) - if ag_in.dimension == '2D': + if AcquisitionType.DIM2 & ag_in.dimension: self.is2D = True #fix IG to single slice in z diff --git a/Wrappers/Python/cil/processors/CofR_image_sharpness.py b/Wrappers/Python/cil/processors/CofR_image_sharpness.py index 10cca07da0..7906d74741 100644 --- a/Wrappers/Python/cil/processors/CofR_image_sharpness.py +++ b/Wrappers/Python/cil/processors/CofR_image_sharpness.py @@ -16,15 +16,13 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import Processor, AcquisitionData, AcquisitionDimensionLabels +from cil.framework import Processor, AcquisitionData, AcquisitionDimensionLabels, AcquisitionType import matplotlib.pyplot as plt import scipy import numpy as np -import inspect import logging import math import importlib -import warnings log = logging.getLogger(__name__) @@ -123,10 +121,7 @@ def check_input(self, data): raise ValueError('slice_index is out of range. Must be in range 0-{0}. Got {1}'.format(data.get_dimension_size('vertical'), self.slice_index)) #check order for single slice data - if data.geometry.dimension == '3D': - test_geom = data.geometry.get_slice(vertical='centre') - else: - test_geom = data.geometry + test_geom = data.geometry.get_slice(vertical='centre') if AcquisitionType.DIM3 & data.geometry.dimension else data.geometry if not AcquisitionDimensionLabels.check_order_for_engine(self.backend, test_geom): raise ValueError("Input data must be reordered for use with selected backend. Use input.reorder{'{0}')".format(self.backend)) @@ -250,16 +245,12 @@ def get_min(self, offsets, values, ind): w1 = ind_centre - ind0 return (1.0 - w1) * offsets[ind0] + w1 * offsets[ind0+1] - def process(self, out=None): - #get slice - data_full = self.get_input() + data = data_full = self.get_input() - if data_full.geometry.dimension == '3D': - data = data_full.get_slice(vertical=self.slice_index) - else: - data = data_full + if AcquisitionType.DIM3 & data_full.geometry.dimension: + data = data.get_slice(vertical=self.slice_index) data.geometry.config.system.align_reference_frame('cil') width = data.geometry.config.panel.num_pixels[0] diff --git a/Wrappers/Python/cil/processors/CofR_xcorrelation.py b/Wrappers/Python/cil/processors/CofR_xcorrelation.py index 62fe47f4a9..0feeaba2ce 100644 --- a/Wrappers/Python/cil/processors/CofR_xcorrelation.py +++ b/Wrappers/Python/cil/processors/CofR_xcorrelation.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import Processor, AcquisitionData +from cil.framework import Processor, AcquisitionData, AcquisitionType import numpy as np import logging @@ -138,14 +138,9 @@ def _return_180_index(angles_deg, initial_index): return np.abs(angles_deg - 180).argmin() def process(self, out=None): - - data_full = self.get_input() - - if data_full.geometry.dimension == '3D': - - data = data_full.get_slice(vertical=self.slice_index) - else: - data = data_full + data = data_full = self.get_input() + if AcquisitionType.DIM3 & data_full.geometry.dimension: + data = data.get_slice(vertical=self.slice_index) geometry = data.geometry diff --git a/Wrappers/Python/cil/recon/FBP.py b/Wrappers/Python/cil/recon/FBP.py index c9392d7eba..8e2f1dcf39 100644 --- a/Wrappers/Python/cil/recon/FBP.py +++ b/Wrappers/Python/cil/recon/FBP.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import cilacc -from cil.framework import AcquisitionTypes +from cil.framework import AcquisitionType from cil.recon import Reconstructor from scipy.fft import fftfreq @@ -254,13 +254,13 @@ def get_filter_array(self): elif self._filter == 'hann': filter_array = ramp * (0.5 + 0.5 * np.cos(freq*np.pi)) - return np.asarray(filter_array,dtype=np.float32).reshape(2**self.fft_order) - - + return np.asarray(filter_array,dtype=np.float32).reshape(2**self.fft_order) + + def plot_filter(self): """ Returns a plot of the filter array. - + Returns ------- matplotlib.pyplot @@ -376,7 +376,7 @@ def __init__ (self, input, image_geometry=None, filter='ram-lak'): #call parent initialiser super().__init__(input, image_geometry, filter, backend='tigre') - if input.geometry.geom_type != AcquisitionTypes.CONE: + if not AcquisitionType.CONE & input.geometry.geom_type: raise TypeError("This reconstructor is for cone-beam data only.") @@ -485,7 +485,7 @@ def __init__ (self, input, image_geometry=None, filter='ram-lak', backend='tigre super().__init__(input, image_geometry, filter, backend) self.set_split_processing(False) - if input.geometry.geom_type != AcquisitionTypes.PARALLEL: + if not AcquisitionType.PARALLEL & input.geometry.geom_type: raise TypeError("This reconstructor is for parallel-beam data only.") @@ -564,13 +564,11 @@ def run(self, out=None, verbose=1): ImageData The reconstructed volume. Suppressed if `out` is passed """ - if verbose: print(self) if self.slices_per_chunk: - - if self.acquisition_geometry.dimension == '2D': + if AcquisitionType.DIM2 & self.acquisition_geometry.dimension: raise ValueError("Only 3D datasets can be processed in chunks with `set_split_processing`") elif self.acquisition_geometry.system_description == 'advanced': raise ValueError("Only simple and offset geometries can be processed in chunks with `set_split_processing`") diff --git a/Wrappers/Python/cil/utilities/display.py b/Wrappers/Python/cil/utilities/display.py index cc7cc10fd4..7048028398 100644 --- a/Wrappers/Python/cil/utilities/display.py +++ b/Wrappers/Python/cil/utilities/display.py @@ -19,7 +19,7 @@ #%% -from cil.framework import AcquisitionGeometry, AcquisitionData, ImageData, DataContainer, BlockDataContainer +from cil.framework import AcquisitionGeometry, AcquisitionData, ImageData, DataContainer, BlockDataContainer, AcquisitionType import numpy as np import warnings @@ -700,10 +700,8 @@ def do_3d_projection(self, renderer=None): return np.min(zs) class _ShowGeometry(object): - def __init__(self, acquisition_geometry, image_geometry=None): - - if acquisition_geometry.dimension == "2D": + if AcquisitionType.DIM2 & acquisition_geometry.dimension: self.ndim = 2 sys = acquisition_geometry.config.system if acquisition_geometry.geom_type == 'cone': @@ -1051,8 +1049,7 @@ class show_geometry(show_base): def __init__(self,acquisition_geometry, image_geometry=None, elevation=20, azimuthal=-35, view_distance=10, grid=False, figsize=(10,10), fontsize=10): - - if acquisition_geometry.dimension == '2D': + if AcquisitionType.DIM2 & acquisition_geometry.dimension: elevation = 90 azimuthal = 0 diff --git a/Wrappers/Python/test/test_labels.py b/Wrappers/Python/test/test_labels.py index 3efdc57f65..0e5e34677f 100644 --- a/Wrappers/Python/test/test_labels.py +++ b/Wrappers/Python/test/test_labels.py @@ -20,34 +20,31 @@ import numpy as np from cil.framework import AcquisitionGeometry, ImageGeometry -from cil.framework.labels import (StrEnum, - FillTypes, UnitsAngles, - AcquisitionTypes, AcquisitionDimensions, - ImageDimensionLabels, AcquisitionDimensionLabels, Backends) +from cil.framework.labels import FillTypes, UnitsAngles, AcquisitionType, ImageDimensionLabels, AcquisitionDimensionLabels, Backends class Test_Lables(unittest.TestCase): def test_labels_strenum(self): - for item in ("3D", "DIM3", AcquisitionDimensions.DIM3): - out = AcquisitionDimensions(item) - self.assertEqual(out, AcquisitionDimensions.DIM3) - self.assertTrue(isinstance(out, AcquisitionDimensions)) - for item in ("bad_str", "4D", "DIM4", UnitsAngles.DEGREE): + for item in (UnitsAngles.DEGREE, "DEGREE", "degree"): + out = UnitsAngles(item) + self.assertEqual(out, UnitsAngles.DEGREE) + self.assertTrue(isinstance(out, UnitsAngles)) + for item in ("bad_str", FillTypes.RANDOM): with self.assertRaises(ValueError): - AcquisitionDimensions(item) + UnitsAngles(item) def test_labels_strenum_eq(self): - for i in ("3D", "DIM3", AcquisitionDimensions.DIM3): - self.assertEqual(AcquisitionDimensions.DIM3, i) - self.assertEqual(i, AcquisitionDimensions.DIM3) - for i in ("2D", "DIM2", AcquisitionDimensions.DIM2, AcquisitionDimensions): - self.assertNotEqual(AcquisitionDimensions.DIM3, i) + for i in (UnitsAngles.RADIAN, "RADIAN", "radian"): + self.assertEqual(UnitsAngles.RADIAN, i) + self.assertEqual(i, UnitsAngles.RADIAN) + for i in ("DEGREE", UnitsAngles.DEGREE, UnitsAngles): + self.assertNotEqual(UnitsAngles.RADIAN, i) def test_labels_contains(self): - for i in ("3D", "DIM3", AcquisitionDimensions.DIM3, AcquisitionDimensions.DIM2): - self.assertIn(i, AcquisitionDimensions) - for i in ("4D", "DIM4", AcquisitionDimensions): - self.assertNotIn(i, AcquisitionDimensions) + for i in ("RADIAN", "degree", UnitsAngles.RADIAN, UnitsAngles.DEGREE): + self.assertIn(i, UnitsAngles) + for i in ("bad_str", UnitsAngles): + self.assertNotIn(i, UnitsAngles) def test_backends(self): for i in ('ASTRA', 'CIL', 'TIGRE'): @@ -68,18 +65,10 @@ def test_units_angles(self): self.assertIn(getattr(UnitsAngles, i), UnitsAngles) def test_acquisition_type(self): - for i in ('PARALLEL', 'CONE'): - self.assertIn(i, AcquisitionTypes) - self.assertIn(i.lower(), AcquisitionTypes) - self.assertIn(getattr(AcquisitionTypes, i), AcquisitionTypes) - - def test_acquisition_dimension(self): - for i in ('2D', '3D'): - self.assertIn(i, AcquisitionDimensions) - for i in ('DIM2', 'DIM3'): - self.assertIn(i, AcquisitionDimensions) - self.assertIn(i.lower(), AcquisitionDimensions) - self.assertIn(getattr(AcquisitionDimensions, i), AcquisitionDimensions) + for i in ('PARALLEL', 'CONE', 'DIM2', 'DIM3'): + self.assertIn(i, AcquisitionType) + self.assertIn(i.lower(), AcquisitionType) + self.assertIn(getattr(AcquisitionType, i), AcquisitionType) def test_image_dimension_labels(self): for i in ('CHANNEL', 'VERTICAL', 'HORIZONTAL_X', 'HORIZONTAL_Y'): diff --git a/Wrappers/Python/test/utils_projectors.py b/Wrappers/Python/test/utils_projectors.py index 1ffe20a37c..c009c82e28 100644 --- a/Wrappers/Python/test/utils_projectors.py +++ b/Wrappers/Python/test/utils_projectors.py @@ -19,7 +19,7 @@ import numpy as np from cil.optimisation.operators import LinearOperator from cil.utilities import dataexample -from cil.framework import AcquisitionGeometry, AcquisitionDimensionLabels +from cil.framework import AcquisitionGeometry, AcquisitionDimensionLabels, AcquisitionType class SimData(object): @@ -375,7 +375,7 @@ def test_forward_projector(self): if (i + j)% 2 == 0: checker[j*4:(j+1)*4,i*4:(i+1)*4] = ones * 16 - if self.ag.dimension == '2D': + if AcquisitionType.DIM2 & self.ag.dimension: checker = checker[0] res = res[0] @@ -408,7 +408,7 @@ def test_backward_projector(self): if (i + k)% 2 == 0: res[k*4:(k+1)*4,:,i*4:(i+1)*4] = ones - if self.ag.dimension == '2D': + if AcquisitionType.DIM2 & self.ag.dimension: checker = checker[0] res = res[0] @@ -482,7 +482,7 @@ def test_FBP_roi(self): reco = FBP(self.acq_data) np.testing.assert_allclose(reco.as_array(), self.gold_roi, atol=self.tolerance_fbp_roi) - if self.ag.dimension == '3D': + if AcquisitionType.DIM3 & self.ag.dimension: FBP = self.FBP(self.ig_single_slice, self.ag, **self.FBP_args) reco = FBP(self.acq_data) np.testing.assert_allclose(reco.as_array(), self.gold_roi_single_slice, atol=self.tolerance_fbp_roi) From cdb4b9b7eda949f72abe673f49905f84688c6f9f Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 21 Aug 2024 12:32:11 +0100 Subject: [PATCH 61/72] missing reorder() --- Wrappers/Python/cil/framework/acquisition_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index b862bd1dbd..67b201d5cc 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -105,13 +105,13 @@ def get_slice(self,channel=None, angle=None, vertical=None, horizontal=None, for else: return AcquisitionData(out.array, deep_copy=False, geometry=geometry_new, suppress_warning=True) - def reorder(self, order=None): + def reorder(self, order): ''' Reorders the data in memory as requested. This is an in-place operation. Parameters ---------- - order : list or str + order: list or str Ordered list of labels from self.dimension_labels, or string 'astra' or 'tigre'. ''' if order in Backends: From 28255eccb661d227b2d52d7829272a26907dfac0 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 21 Aug 2024 12:50:27 +0100 Subject: [PATCH 62/72] explicit labels import --- Wrappers/Python/cil/framework/__init__.py | 1 - Wrappers/Python/cil/io/ZEISSDataReader.py | 3 +- Wrappers/Python/cil/plugins/TomoPhantom.py | 3 +- .../astra/operators/ProjectionOperator.py | 4 +-- .../astra/processors/AstraBackProjector3D.py | 5 ++- .../processors/AstraForwardProjector3D.py | 5 ++- .../cil/plugins/astra/processors/FBP.py | 3 +- .../plugins/astra/processors/FBP_Flexible.py | 3 +- .../utilities/convert_geometry_to_astra.py | 2 +- .../convert_geometry_to_astra_vec_2D.py | 2 +- .../convert_geometry_to_astra_vec_3D.py | 2 +- .../functions/regularisers.py | 13 +++---- Wrappers/Python/cil/plugins/tigre/FBP.py | 5 ++- Wrappers/Python/cil/plugins/tigre/Geometry.py | 2 +- .../cil/plugins/tigre/ProjectionOperator.py | 10 +++--- .../cil/processors/CofR_image_sharpness.py | 3 +- .../cil/processors/CofR_xcorrelation.py | 3 +- .../Python/cil/processors/PaganinProcessor.py | 3 +- Wrappers/Python/cil/recon/FBP.py | 2 +- Wrappers/Python/cil/recon/Reconstructor.py | 3 +- Wrappers/Python/cil/utilities/dataexample.py | 35 ++++++++++--------- Wrappers/Python/cil/utilities/display.py | 3 +- Wrappers/Python/test/test_BlockOperator.py | 3 +- Wrappers/Python/test/test_DataContainer.py | 21 +++++------ Wrappers/Python/test/test_Operator.py | 12 +++---- .../Python/test/test_PluginsTomoPhantom.py | 6 ++-- Wrappers/Python/test/test_algorithms.py | 5 +-- Wrappers/Python/test/test_functions.py | 33 ++++++++--------- Wrappers/Python/test/test_io.py | 3 +- Wrappers/Python/test/test_ring_processor.py | 3 +- Wrappers/Python/test/test_subset.py | 3 +- Wrappers/Python/test/utils_projectors.py | 3 +- 32 files changed, 112 insertions(+), 95 deletions(-) diff --git a/Wrappers/Python/cil/framework/__init__.py b/Wrappers/Python/cil/framework/__init__.py index 8a37b5767a..bda9fdb11e 100644 --- a/Wrappers/Python/cil/framework/__init__.py +++ b/Wrappers/Python/cil/framework/__init__.py @@ -27,4 +27,3 @@ from .processors import DataProcessor, Processor, AX, PixelByPixelDataProcessor, CastDataContainer from .block import BlockDataContainer, BlockGeometry from .partitioner import Partitioner -from .labels import AcquisitionDimensionLabels, ImageDimensionLabels, FillTypes, UnitsAngles, AcquisitionType diff --git a/Wrappers/Python/cil/io/ZEISSDataReader.py b/Wrappers/Python/cil/io/ZEISSDataReader.py index 3bce8b3d0d..07e7cde6af 100644 --- a/Wrappers/Python/cil/io/ZEISSDataReader.py +++ b/Wrappers/Python/cil/io/ZEISSDataReader.py @@ -18,7 +18,8 @@ # Andrew Shartis (UES, Inc.) -from cil.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry, UnitsAngles, AcquisitionDimensionLabels, ImageDimensionLabels +from cil.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry +from cil.framework.labels import UnitsAngles, AcquisitionDimensionLabels, ImageDimensionLabels import numpy as np import os import olefile diff --git a/Wrappers/Python/cil/plugins/TomoPhantom.py b/Wrappers/Python/cil/plugins/TomoPhantom.py index f36ef29ad3..f0933fa4c8 100644 --- a/Wrappers/Python/cil/plugins/TomoPhantom.py +++ b/Wrappers/Python/cil/plugins/TomoPhantom.py @@ -16,7 +16,8 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import ImageData, ImageDimensionLabels +from cil.framework import ImageData +from cil.framework.labels import ImageDimensionLabels import tomophantom from tomophantom import TomoP2D, TomoP3D import os diff --git a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py index d62d7e4281..fe276b55d0 100644 --- a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py @@ -15,10 +15,10 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt - import logging -from cil.framework import BlockGeometry, AcquisitionDimensionLabels, ImageDimensionLabels, AcquisitionType +from cil.framework import BlockGeometry +from cil.framework.labels import AcquisitionDimensionLabels, ImageDimensionLabels, AcquisitionType from cil.optimisation.operators import BlockOperator, LinearOperator, ChannelwiseOperator from cil.plugins.astra.operators import AstraProjector2D, AstraProjector3D diff --git a/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py b/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py index 4c21c85113..c6e47e00dc 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py +++ b/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py @@ -15,9 +15,8 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt - - -from cil.framework import DataProcessor, ImageData, AcquisitionDimensionLabels, ImageDimensionLabels +from cil.framework import DataProcessor, ImageData +from cil.framework.labels import AcquisitionDimensionLabels, ImageDimensionLabels from cil.plugins.astra.utilities import convert_geometry_to_astra_vec_3D import astra from astra import astra_dict, algorithm, data3d diff --git a/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py b/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py index 5a87d3e261..455e65959b 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py +++ b/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py @@ -15,9 +15,8 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt - - -from cil.framework import DataProcessor, AcquisitionData, ImageDimensionLabels, AcquisitionDimensionLabels +from cil.framework import DataProcessor, AcquisitionData +from cil.framework.labels import ImageDimensionLabels, AcquisitionDimensionLabels from cil.plugins.astra.utilities import convert_geometry_to_astra_vec_3D import astra from astra import astra_dict, algorithm, data3d diff --git a/Wrappers/Python/cil/plugins/astra/processors/FBP.py b/Wrappers/Python/cil/plugins/astra/processors/FBP.py index b750412e3c..0bca1d23b8 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/FBP.py +++ b/Wrappers/Python/cil/plugins/astra/processors/FBP.py @@ -15,7 +15,8 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import DataProcessor, ImageDimensionLabels, AcquisitionDimensionLabels, AcquisitionType +from cil.framework import DataProcessor +from cil.framework.labels import ImageDimensionLabels, AcquisitionDimensionLabels, AcquisitionType from cil.plugins.astra.processors.FBP_Flexible import FBP_Flexible from cil.plugins.astra.processors.FDK_Flexible import FDK_Flexible from cil.plugins.astra.processors.FBP_Flexible import FBP_CPU diff --git a/Wrappers/Python/cil/plugins/astra/processors/FBP_Flexible.py b/Wrappers/Python/cil/plugins/astra/processors/FBP_Flexible.py index 4bed64064b..3eb47048d1 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/FBP_Flexible.py +++ b/Wrappers/Python/cil/plugins/astra/processors/FBP_Flexible.py @@ -17,7 +17,8 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import AcquisitionGeometry, Processor, ImageData, AcquisitionType +from cil.framework import AcquisitionGeometry, Processor, ImageData +from cil.framework.labels import AcquisitionType from cil.plugins.astra.processors.FDK_Flexible import FDK_Flexible from cil.plugins.astra.utilities import convert_geometry_to_astra_vec_3D, convert_geometry_to_astra import logging diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py index 09526b581e..fdf232b3f7 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py @@ -19,7 +19,7 @@ import astra import numpy as np -from cil.framework import AcquisitionType, UnitsAngles +from cil.framework.labels import AcquisitionType, UnitsAngles def convert_geometry_to_astra(volume_geometry, sinogram_geometry): """ diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py index 105ce588c6..1e15bfd8e1 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py @@ -19,7 +19,7 @@ import astra import numpy as np -from cil.framework import UnitsAngles +from cil.framework.labels import UnitsAngles def convert_geometry_to_astra_vec_2D(volume_geometry, sinogram_geometry_in): diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py index ca87c712a9..0ea14f4e26 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py @@ -19,7 +19,7 @@ import astra import numpy as np -from cil.framework import AcquisitionType, UnitsAngles +from cil.framework.labels import AcquisitionType, UnitsAngles def convert_geometry_to_astra_vec_3D(volume_geometry, sinogram_geometry_in): diff --git a/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py b/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py index 516c203851..061813e70b 100644 --- a/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py +++ b/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py @@ -23,7 +23,8 @@ raise ImportError('Please `conda install "ccpi::ccpi-regulariser>=24.0.1"`') from exc -from cil.framework import DataContainer, ImageDimensionLabels +from cil.framework import DataContainer +from cil.framework.labels import ImageDimensionLabels from cil.optimisation.functions import Function import numpy as np import warnings @@ -139,7 +140,7 @@ class FGP_TV(TV_Base): Note ----- - In CIL Version 24.1.0 we change the default value of nonnegativity to False. This means non-negativity is not enforced by default. + In CIL Version 24.1.0 we change the default value of nonnegativity to False. This means non-negativity is not enforced by default. Parameters ---------- @@ -211,9 +212,9 @@ def __init__(self, alpha=1, max_iteration=100, tolerance=0, isotropic=True, nonn else: self.methodTV = 1 - if nonnegativity is None: # Deprecate this warning in future versions and allow nonnegativity to be default False in the init. + if nonnegativity is None: # Deprecate this warning in future versions and allow nonnegativity to be default False in the init. warnings.warn('Note that the default behaviour now sets the nonnegativity constraint to False ', UserWarning, stacklevel=2) - nonnegativity=False + nonnegativity=False if nonnegativity == True: self.nonnegativity = 1 else: @@ -412,7 +413,7 @@ def __call__(self,x): return np.nan def proximal_numpy(self, in_arr, tau): - + info = np.zeros((2,), dtype=np.float32) res = regularisers.FGP_dTV(\ @@ -470,7 +471,7 @@ def __call__(self,x): def proximal_numpy(self, in_arr, tau): # remove any dimension of size 1 in_arr = np.squeeze(in_arr) - + res = regularisers.TNV(in_arr, self.alpha * tau, self.max_iteration, diff --git a/Wrappers/Python/cil/plugins/tigre/FBP.py b/Wrappers/Python/cil/plugins/tigre/FBP.py index 66fe2315a0..1f7d01871c 100644 --- a/Wrappers/Python/cil/plugins/tigre/FBP.py +++ b/Wrappers/Python/cil/plugins/tigre/FBP.py @@ -15,14 +15,13 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt - import contextlib import io -import warnings import numpy as np -from cil.framework import DataProcessor, ImageData, AcquisitionDimensionLabels, ImageDimensionLabels +from cil.framework import DataProcessor, ImageData +from cil.framework.labels import AcquisitionDimensionLabels, ImageDimensionLabels from cil.plugins.tigre import CIL2TIGREGeometry try: diff --git a/Wrappers/Python/cil/plugins/tigre/Geometry.py b/Wrappers/Python/cil/plugins/tigre/Geometry.py index 61c7a9cd3b..14ce391d60 100644 --- a/Wrappers/Python/cil/plugins/tigre/Geometry.py +++ b/Wrappers/Python/cil/plugins/tigre/Geometry.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import AcquisitionType, UnitsAngles +from cil.framework.labels import AcquisitionType, UnitsAngles import numpy as np try: diff --git a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py index f90bbce371..9a9246e05b 100644 --- a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py @@ -15,14 +15,14 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt +import logging + +import numpy as np -from cil.framework import (ImageData, AcquisitionData, AcquisitionDimensionLabels, ImageDimensionLabels, - BlockGeometry) +from cil.framework import ImageData, AcquisitionData, BlockGeometry +from cil.framework.labels import AcquisitionDimensionLabels, ImageDimensionLabels from cil.optimisation.operators import BlockOperator, LinearOperator from cil.plugins.tigre import CIL2TIGREGeometry -import numpy as np -import logging -import warnings log = logging.getLogger(__name__) diff --git a/Wrappers/Python/cil/processors/CofR_image_sharpness.py b/Wrappers/Python/cil/processors/CofR_image_sharpness.py index 7906d74741..032307f678 100644 --- a/Wrappers/Python/cil/processors/CofR_image_sharpness.py +++ b/Wrappers/Python/cil/processors/CofR_image_sharpness.py @@ -16,7 +16,8 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import Processor, AcquisitionData, AcquisitionDimensionLabels, AcquisitionType +from cil.framework import Processor, AcquisitionData +from cil.framework.labels import AcquisitionDimensionLabels, AcquisitionType import matplotlib.pyplot as plt import scipy import numpy as np diff --git a/Wrappers/Python/cil/processors/CofR_xcorrelation.py b/Wrappers/Python/cil/processors/CofR_xcorrelation.py index 0feeaba2ce..1193f9ad48 100644 --- a/Wrappers/Python/cil/processors/CofR_xcorrelation.py +++ b/Wrappers/Python/cil/processors/CofR_xcorrelation.py @@ -16,7 +16,8 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import Processor, AcquisitionData, AcquisitionType +from cil.framework import Processor, AcquisitionData +from cil.framework.labels import AcquisitionType import numpy as np import logging diff --git a/Wrappers/Python/cil/processors/PaganinProcessor.py b/Wrappers/Python/cil/processors/PaganinProcessor.py index ec101d03b8..87ca5d2d60 100644 --- a/Wrappers/Python/cil/processors/PaganinProcessor.py +++ b/Wrappers/Python/cil/processors/PaganinProcessor.py @@ -17,7 +17,8 @@ # CIL Developers, listed at: # https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import Processor, AcquisitionData, AcquisitionDimensionLabels +from cil.framework import Processor, AcquisitionData +from cil.framework.labels import AcquisitionDimensionLabels import numpy as np from scipy.fft import fft2 diff --git a/Wrappers/Python/cil/recon/FBP.py b/Wrappers/Python/cil/recon/FBP.py index 8e2f1dcf39..b2c597ecaf 100644 --- a/Wrappers/Python/cil/recon/FBP.py +++ b/Wrappers/Python/cil/recon/FBP.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import cilacc -from cil.framework import AcquisitionType +from cil.framework.labels import AcquisitionType from cil.recon import Reconstructor from scipy.fft import fftfreq diff --git a/Wrappers/Python/cil/recon/Reconstructor.py b/Wrappers/Python/cil/recon/Reconstructor.py index 75b06b9bbd..4ddac2a33e 100644 --- a/Wrappers/Python/cil/recon/Reconstructor.py +++ b/Wrappers/Python/cil/recon/Reconstructor.py @@ -16,7 +16,8 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import AcquisitionData, ImageGeometry, AcquisitionDimensionLabels +from cil.framework import AcquisitionData, ImageGeometry +from cil.framework.labels import AcquisitionDimensionLabels import importlib import weakref diff --git a/Wrappers/Python/cil/utilities/dataexample.py b/Wrappers/Python/cil/utilities/dataexample.py index 4e3037fc88..faab6d1f47 100644 --- a/Wrappers/Python/cil/utilities/dataexample.py +++ b/Wrappers/Python/cil/utilities/dataexample.py @@ -16,7 +16,8 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework import ImageData, ImageGeometry, ImageDimensionLabels +from cil.framework import ImageGeometry +from cil.framework.labels import ImageDimensionLabels import numpy import numpy as np from PIL import Image @@ -33,7 +34,7 @@ class DATA(object): @classmethod def dfile(cls): return None - + class CILDATA(DATA): data_dir = os.path.abspath(os.path.join(sys.prefix, 'share','cil')) @classmethod @@ -41,9 +42,9 @@ def get(cls, size=None, scale=(0,1), **kwargs): ddir = kwargs.get('data_dir', CILDATA.data_dir) loader = TestData(data_dir=ddir) return loader.load(cls.dfile(), size, scale, **kwargs) - + class REMOTEDATA(DATA): - + FOLDER = '' URL = '' FILE_SIZE = '' @@ -56,7 +57,7 @@ def get(cls, data_dir): def _download_and_extract_from_url(cls, data_dir): with urlopen(cls.URL) as response: with BytesIO(response.read()) as bytes, ZipFile(bytes) as zipfile: - zipfile.extractall(path = data_dir) + zipfile.extractall(path = data_dir) @classmethod def download_data(cls, data_dir): @@ -72,8 +73,8 @@ def download_data(cls, data_dir): if os.path.isdir(os.path.join(data_dir, cls.FOLDER)): print("Dataset already exists in " + data_dir) else: - if input("Are you sure you want to download " + cls.FILE_SIZE + " dataset from " + cls.URL + " ? (y/n)") == "y": - print('Downloading dataset from ' + cls.URL) + if input("Are you sure you want to download " + cls.FILE_SIZE + " dataset from " + cls.URL + " ? (y/n)") == "y": + print('Downloading dataset from ' + cls.URL) cls._download_and_extract_from_url(os.path.join(data_dir,cls.FOLDER)) print('Download complete') else: @@ -185,15 +186,15 @@ def get(cls, **kwargs): ------- ImageData The simulated spheres volume - ''' + ''' ddir = kwargs.get('data_dir', CILDATA.data_dir) loader = NEXUSDataReader() loader.set_up(file_name=os.path.join(os.path.abspath(ddir), 'sim_volume.nxs')) return loader.read() - + class WALNUT(REMOTEDATA): ''' - A microcomputed tomography dataset of a walnut from https://zenodo.org/records/4822516 + A microcomputed tomography dataset of a walnut from https://zenodo.org/records/4822516 ''' FOLDER = 'walnut' URL = 'https://zenodo.org/record/4822516/files/walnut.zip' @@ -202,7 +203,7 @@ class WALNUT(REMOTEDATA): @classmethod def get(cls, data_dir): ''' - A microcomputed tomography dataset of a walnut from https://zenodo.org/records/4822516 + A microcomputed tomography dataset of a walnut from https://zenodo.org/records/4822516 This function returns the raw projection data from the .txrm file Parameters @@ -222,19 +223,19 @@ def get(cls, data_dir): except(FileNotFoundError): raise(FileNotFoundError("Dataset .txrm file not found in specifed data_dir: {} \n \ Specify a different data_dir or download data with dataexample.{}.download_data(data_dir)".format(filepath, cls.__name__))) - + class USB(REMOTEDATA): ''' - A microcomputed tomography dataset of a usb memory stick from https://zenodo.org/records/4822516 + A microcomputed tomography dataset of a usb memory stick from https://zenodo.org/records/4822516 ''' - FOLDER = 'USB' + FOLDER = 'USB' URL = 'https://zenodo.org/record/4822516/files/usb.zip' FILE_SIZE = '3.2 GB' @classmethod def get(cls, data_dir): ''' - A microcomputed tomography dataset of a usb memory stick from https://zenodo.org/records/4822516 + A microcomputed tomography dataset of a usb memory stick from https://zenodo.org/records/4822516 This function returns the raw projection data from the .txrm file Parameters @@ -254,7 +255,7 @@ def get(cls, data_dir): except(FileNotFoundError): raise(FileNotFoundError("Dataset .txrm file not found in: {} \n \ Specify a different data_dir or download data with dataexample.{}.download_data(data_dir)".format(filepath, cls.__name__))) - + class KORN(REMOTEDATA): ''' A microcomputed tomography dataset of a sunflower seeds in a box from https://zenodo.org/records/6874123 @@ -319,7 +320,7 @@ class TestData(object): def __init__(self, data_dir): self.data_dir = data_dir - + def load(self, which, size=None, scale=(0,1), **kwargs): ''' Return a test data of the requested image diff --git a/Wrappers/Python/cil/utilities/display.py b/Wrappers/Python/cil/utilities/display.py index 7048028398..4228e1b1af 100644 --- a/Wrappers/Python/cil/utilities/display.py +++ b/Wrappers/Python/cil/utilities/display.py @@ -19,7 +19,8 @@ #%% -from cil.framework import AcquisitionGeometry, AcquisitionData, ImageData, DataContainer, BlockDataContainer, AcquisitionType +from cil.framework import AcquisitionGeometry, AcquisitionData, ImageData, DataContainer, BlockDataContainer +from cil.framework.labels import AcquisitionType import numpy as np import warnings diff --git a/Wrappers/Python/test/test_BlockOperator.py b/Wrappers/Python/test/test_BlockOperator.py index 58e0a65d99..3ba51f688c 100644 --- a/Wrappers/Python/test/test_BlockOperator.py +++ b/Wrappers/Python/test/test_BlockOperator.py @@ -20,7 +20,8 @@ from utils import initialise_tests import logging from cil.optimisation.operators import BlockOperator, GradientOperator -from cil.framework import BlockDataContainer, BlockGeometry, ImageGeometry, FillTypes +from cil.framework import BlockDataContainer, BlockGeometry, ImageGeometry +from cil.framework.labels import FillTypes from cil.optimisation.operators import IdentityOperator import numpy from cil.optimisation.operators import FiniteDifferenceOperator diff --git a/Wrappers/Python/test/test_DataContainer.py b/Wrappers/Python/test/test_DataContainer.py index ffeea3f70f..b955d1e485 100644 --- a/Wrappers/Python/test/test_DataContainer.py +++ b/Wrappers/Python/test/test_DataContainer.py @@ -19,7 +19,8 @@ import sys import numpy from cil.framework import (DataContainer, ImageGeometry, ImageData, VectorGeometry, AcquisitionData, - AcquisitionGeometry, BlockGeometry, VectorData, ImageDimensionLabels, AcquisitionDimensionLabels) + AcquisitionGeometry, BlockGeometry, VectorData) +from cil.framework.labels import ImageDimensionLabels, AcquisitionDimensionLabels from timeit import default_timer as timer import logging from testclass import CCPiTestClass @@ -470,27 +471,27 @@ def test_ImageData_from_numpy(self): arr = numpy.arange(24, dtype=numpy.float32).reshape(2, 3, 4) ig = ImageGeometry(voxel_num_x=4, voxel_num_y=3, voxel_num_z=2) data = ImageData(arr, deep_copy=True, geometry=ig) - + # Check initial shape, geometry, and deep copy self.assertEqual(data.shape, (2, 3, 4)) self.assertEqual(data.geometry, ig) self.assertNotEqual(id(data.array), id(arr)) - + # Fill with zeros and check data.fill(0) numpy.testing.assert_array_equal(data.as_array(), numpy.zeros(ig.shape)) - + # Fill with original array and check data.fill(arr) self.assertEqual(data.shape, (2, 3, 4)) - self.assertEqual(data.geometry, ig) - + self.assertEqual(data.geometry, ig) + # Reshape array with singleton dimension and create new ImageData arr = arr.reshape(1, 2, 3, 4) data = ImageData(arr, geometry=ig) self.assertEqual(data.shape, (2, 3, 4)) self.assertEqual(data.geometry, ig) - + # Fill with zeros and reshaped array, then check data.fill(0) data.fill(arr) @@ -548,7 +549,7 @@ def test_AcquisitionData_from_numpy(self): arr = numpy.arange(24, dtype=numpy.float32).reshape(2, 3, 4) ag = AcquisitionGeometry.create_Parallel3D().set_angles([0, 1]).set_panel((4, 3)) data = AcquisitionData(arr, deep_copy=True, geometry=ag) - + # Check initial shape, geometry, and deep copy self.assertEqual(data.shape, (2, 3, 4)) self.assertEqual(data.geometry, ag) @@ -557,7 +558,7 @@ def test_AcquisitionData_from_numpy(self): # Fill with zeros and check data.fill(0) numpy.testing.assert_array_equal(data.as_array(), numpy.zeros(ag.shape)) - + # Fill with original array and check data.fill(arr) self.assertEqual(data.shape, (2, 3, 4)) @@ -574,7 +575,7 @@ def test_AcquisitionData_from_numpy(self): data.fill(0) data.fill(arr) self.assertEqual(data.shape, (2, 3, 4)) - self.assertEqual(data.geometry, ag) + self.assertEqual(data.geometry, ag) # Change dtype to float64, fill, and check arr = arr.astype(numpy.float64) diff --git a/Wrappers/Python/test/test_Operator.py b/Wrappers/Python/test/test_Operator.py index b71c8395ed..0411a1eb2c 100644 --- a/Wrappers/Python/test/test_Operator.py +++ b/Wrappers/Python/test/test_Operator.py @@ -19,9 +19,9 @@ import unittest from unittest.mock import Mock from utils import initialise_tests -from cil.framework import ImageGeometry, BlockGeometry, VectorGeometry, BlockDataContainer, DataContainer, FillTypes -from cil.optimisation.operators import BlockOperator,\ - FiniteDifferenceOperator, SymmetrisedGradientOperator +from cil.framework import ImageGeometry, BlockGeometry, VectorGeometry, DataContainer +from cil.framework.labels import FillTypes +from cil.optimisation.operators import FiniteDifferenceOperator, SymmetrisedGradientOperator import numpy from timeit import default_timer as timer from cil.optimisation.operators import GradientOperator, IdentityOperator,\ @@ -234,10 +234,10 @@ def test_IdentityOperator(self): y = Id.direct(img) numpy.testing.assert_array_equal(y.as_array(), img.as_array()) - + #Check is_linear self.assertTrue(Id.is_linear()) - + #Check is_orthogonal self.assertTrue(Id.is_orthogonal()) @@ -506,7 +506,7 @@ def test_ProjectionMap(self): res1 = bg.allocate(0) proj_map.adjoint(x, out=res1) - + res2=bg.allocate('random') proj_map.adjoint(x, out=res2) diff --git a/Wrappers/Python/test/test_PluginsTomoPhantom.py b/Wrappers/Python/test/test_PluginsTomoPhantom.py index ac5ef8e7ca..4357914abc 100644 --- a/Wrappers/Python/test/test_PluginsTomoPhantom.py +++ b/Wrappers/Python/test/test_PluginsTomoPhantom.py @@ -15,10 +15,12 @@ # # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt - import unittest -from cil.framework import AcquisitionGeometry, UnitsAngles + import numpy as np + +from cil.framework import AcquisitionGeometry +from cil.framework.labels import UnitsAngles from utils import has_tomophantom, initialise_tests initialise_tests() diff --git a/Wrappers/Python/test/test_algorithms.py b/Wrappers/Python/test/test_algorithms.py index 69beca0022..502fcb25be 100644 --- a/Wrappers/Python/test/test_algorithms.py +++ b/Wrappers/Python/test/test_algorithms.py @@ -24,8 +24,9 @@ import numpy as np import logging -from cil.framework import (ImageGeometry, ImageData, VectorGeometry, AcquisitionData, AcquisitionGeometry, - BlockDataContainer, BlockGeometry, VectorData, FillTypes) +from cil.framework import (ImageGeometry, ImageData, AcquisitionData, AcquisitionGeometry, + BlockDataContainer, BlockGeometry, VectorData) +from cil.framework.labels import FillTypes from cil.optimisation.utilities import ArmijoStepSizeRule, ConstantStepSize from cil.optimisation.operators import IdentityOperator diff --git a/Wrappers/Python/test/test_functions.py b/Wrappers/Python/test/test_functions.py index b0fd68115c..46eb442154 100644 --- a/Wrappers/Python/test/test_functions.py +++ b/Wrappers/Python/test/test_functions.py @@ -21,8 +21,9 @@ from cil.optimisation.functions.Function import ScaledFunction import numpy as np -from cil.framework import VectorGeometry, VectorData, BlockDataContainer, DataContainer, FillTypes, ImageGeometry, \ +from cil.framework import VectorGeometry, VectorData, BlockDataContainer, DataContainer, ImageGeometry, \ AcquisitionGeometry +from cil.framework.labels import FillTypes from cil.optimisation.operators import IdentityOperator, MatrixOperator, CompositionOperator, DiagonalOperator, BlockOperator from cil.optimisation.functions import Function, KullbackLeibler, ConstantFunction, TranslateFunction, soft_shrinkage from cil.optimisation.operators import GradientOperator @@ -96,7 +97,7 @@ def test_Function(self): a3 = 0.5 * d.squared_norm() + d.dot(noisy_data) self.assertAlmostEqual(a3, g.convex_conjugate(d), places=7) - #negative function + #negative function g = - L2NormSquared(b=noisy_data) # Compare call of g @@ -955,7 +956,7 @@ def proximal_conjugate_test(self, function, geom): class TestL1Norm (CCPiTestClass): - + def test_L1Norm_vs_WeightedL1Norm_noweight(self): f1 = L1Norm() f2 = L1Norm(weight=None) @@ -1003,7 +1004,7 @@ def test_L1Norm_vs_WeightedL1Norm(self): np.testing.assert_allclose(f1(x), float(N*M)) np.testing.assert_allclose(f2(x), w) np.testing.assert_allclose(f2(x), f1(weights)) - + np.random.seed(1) weights= geom.allocate('random').abs() w = weights.abs().sum() @@ -1014,7 +1015,7 @@ def test_L1Norm_vs_WeightedL1Norm(self): np.testing.assert_allclose(f1(x), float(N*M)) np.testing.assert_allclose(f2(x), w) - + np.random.seed(1) w = 1 @@ -1023,8 +1024,8 @@ def test_L1Norm_vs_WeightedL1Norm(self): f2 = L1Norm(weight=w) np.testing.assert_allclose(f1(x), f2(x)) - - + + with self.assertRaises(ValueError): a=3+4j f2 = L1Norm(weight=a) @@ -1041,18 +1042,18 @@ def test_L1Norm_vs_WeightedL1Norm(self): np.testing.assert_allclose(f1(x), float(N*M)) np.testing.assert_allclose(f2(x), w) - + x=geom.allocate(3j) b=geom.allocate(2j) f1 = L1Norm(b=b) np.testing.assert_allclose(f1(x), M*N) - + x=geom.allocate(1+1j) b=geom.allocate(1) f1 = L1Norm(b=b) np.testing.assert_allclose(f1(x), M*N) - - + + def test_ZeroWeights_L1Norm(self): weight = VectorData(np.array([0., 1.])) tau = 0.2 @@ -1142,7 +1143,7 @@ def soft_shrinkage_test(self, x): tau = -3j with self.assertRaises(ValueError): ret = soft_shrinkage(-0.5 *x, tau) - + # tau np.ndarray tau = 1. * np.ones_like(x.as_array()) ret = soft_shrinkage(x, tau) @@ -1188,7 +1189,7 @@ def soft_shrinkage_test(self, x): def test_L1_prox_init(self): pass - def test_L1Sparsity(self): + def test_L1Sparsity(self): from cil.optimisation.operators import WaveletOperator f1 = L1Norm() N, M = 2,3 @@ -1201,13 +1202,13 @@ def test_L1Sparsity(self): W = WaveletOperator(geom, level=0) # level=0 makes this the identity operator f2 = L1Sparsity(W, weight=weights) self.L1SparsityTest(f1, f2, x) - + f2 = L1Sparsity(W, b=b, weight=weights) self.L1SparsityTest(f3, f2, x) f2 = L1Sparsity(W) self.L1SparsityTest(f1, f2, x) - + f2 = L1Sparsity(W, b=b) self.L1SparsityTest(f3, f2, x) @@ -1218,7 +1219,7 @@ def L1SparsityTest(self, f1, f2, x): np.testing.assert_allclose(f1.proximal(x, tau).as_array(),\ f2.proximal(x, tau).as_array()) - + np.testing.assert_almost_equal(f1.convex_conjugate(x), f2.convex_conjugate(x)) diff --git a/Wrappers/Python/test/test_io.py b/Wrappers/Python/test/test_io.py index c914afdc6f..d7fa75e3c1 100644 --- a/Wrappers/Python/test/test_io.py +++ b/Wrappers/Python/test/test_io.py @@ -22,7 +22,8 @@ import numpy as np import os -from cil.framework import ImageGeometry, UnitsAngles +from cil.framework import ImageGeometry +from cil.framework.labels import UnitsAngles from cil.io import NEXUSDataReader, NikonDataReader, ZEISSDataReader from cil.io import TIFFWriter, TIFFStackReader from cil.io.utilities import HDF5_utilities diff --git a/Wrappers/Python/test/test_ring_processor.py b/Wrappers/Python/test/test_ring_processor.py index 2a2a059af8..5e1dea1ebf 100644 --- a/Wrappers/Python/test/test_ring_processor.py +++ b/Wrappers/Python/test/test_ring_processor.py @@ -18,7 +18,8 @@ import unittest from cil.processors import RingRemover, TransmissionAbsorptionConverter, Slicer -from cil.framework import ImageGeometry, AcquisitionGeometry, UnitsAngles +from cil.framework import ImageGeometry, AcquisitionGeometry +from cil.framework.labels import UnitsAngles from cil.utilities import dataexample from cil.utilities.quality_measures import mse diff --git a/Wrappers/Python/test/test_subset.py b/Wrappers/Python/test/test_subset.py index b08790ee45..9a084affb5 100644 --- a/Wrappers/Python/test/test_subset.py +++ b/Wrappers/Python/test/test_subset.py @@ -19,7 +19,8 @@ import unittest from utils import initialise_tests import numpy -from cil.framework import DataContainer, ImageGeometry, AcquisitionGeometry, ImageDimensionLabels, AcquisitionDimensionLabels +from cil.framework import DataContainer, ImageGeometry, AcquisitionGeometry +from cil.framework.labels import ImageDimensionLabels, AcquisitionDimensionLabels from timeit import default_timer as timer initialise_tests() diff --git a/Wrappers/Python/test/utils_projectors.py b/Wrappers/Python/test/utils_projectors.py index c009c82e28..9ba0670d46 100644 --- a/Wrappers/Python/test/utils_projectors.py +++ b/Wrappers/Python/test/utils_projectors.py @@ -19,7 +19,8 @@ import numpy as np from cil.optimisation.operators import LinearOperator from cil.utilities import dataexample -from cil.framework import AcquisitionGeometry, AcquisitionDimensionLabels, AcquisitionType +from cil.framework import AcquisitionGeometry +from cil.framework.labels import AcquisitionDimensionLabels, AcquisitionType class SimData(object): From d8ff5288c983b9e656a7a814c67b4ce5b43b3da2 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 21 Aug 2024 12:54:53 +0100 Subject: [PATCH 63/72] fix docstring --- Wrappers/Python/cil/framework/labels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index 26f3947b46..c767383b83 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -218,8 +218,8 @@ class UnitsAngles(StrEnum): Examples -------- ``` - data.geometry.set_unitangles(angle_data, angle_units=UnitsAngles.DEGREE) - data.geometry.set_unit(angle_data, angle_units="degree") + data.geometry.set_angles(angle_data, angle_units=UnitsAngles.DEGREE) + data.geometry.set_angles(angle_data, angle_units="degree") ``` """ DEGREE = auto() From e885f2d1b626c467af81f9acb99f9e45543be16f Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 21 Aug 2024 12:56:28 +0100 Subject: [PATCH 64/72] better OOP --- Wrappers/Python/cil/framework/labels.py | 102 ++++++++++-------------- 1 file changed, 41 insertions(+), 61 deletions(-) diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index c767383b83..6146ce8c66 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -75,21 +75,10 @@ class Backends(StrEnum): CIL = auto() -class ImageDimensionLabels(StrEnum): - """ - Available dimension labels for image data. - - Examples - -------- - ``` - data.reorder([ImageDimensionLabels.HORIZONTAL_X, ImageDimensionLabels.VERTICAL]) - data.reorder(["horizontal_x", "vertical"]) - ``` - """ - CHANNEL = auto() - VERTICAL = auto() - HORIZONTAL_X = auto() - HORIZONTAL_Y = auto() +class _DimensionBase: + @classmethod + def _default_order(cls, engine: str) -> tuple: + raise NotImplementedError @classmethod def get_order_for_engine(cls, engine: str, geometry=None) -> tuple: @@ -98,16 +87,13 @@ def get_order_for_engine(cls, engine: str, geometry=None) -> tuple: Parameters ---------- - geometry: ImageGeometry, optional + geometry: ImageGeometry | AcquisitionGeometry If unspecified, the default order is returned. """ - order = cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X - engine_orders = {Backends.ASTRA: order, Backends.TIGRE: order, Backends.CIL: order} - dim_order = engine_orders[Backends(engine)] - + order = cls._default_order(engine) if geometry is None: - return dim_order - return tuple(label for label in dim_order if label in geometry.dimension_labels) + return order + return tuple(label for label in order if label in geometry.dimension_labels) @classmethod def check_order_for_engine(cls, engine: str, geometry) -> bool: @@ -116,7 +102,7 @@ def check_order_for_engine(cls, engine: str, geometry) -> bool: Parameters ---------- - geometry: ImageGeometry + geometry: ImageGeometry | AcquisitionGeometry Raises ------ @@ -126,12 +112,38 @@ def check_order_for_engine(cls, engine: str, geometry) -> bool: if order_requested == tuple(geometry.dimension_labels): return True raise ValueError( - f"Expected dimension_label order {order_requested}" + f"Expected dimension_label order {order_requested}," f" got {tuple(geometry.dimension_labels)}." f" Try using `data.reorder('{engine}')` to permute for {engine}") -class AcquisitionDimensionLabels(StrEnum): +class ImageDimensionLabels(_DimensionBase, StrEnum): + """ + Available dimension labels for image data. + + Examples + -------- + ``` + data.reorder([ImageDimensionLabels.HORIZONTAL_X, ImageDimensionLabels.VERTICAL]) + data.reorder(["horizontal_x", "vertical"]) + ``` + """ + CHANNEL = auto() + VERTICAL = auto() + HORIZONTAL_X = auto() + HORIZONTAL_Y = auto() + + @classmethod + def _default_order(cls, engine: str) -> tuple: + engine = Backends(engine) + orders = { + Backends.ASTRA: (cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X), + Backends.TIGRE: (cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X), + Backends.CIL: (cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X)} + return orders[engine] + + +class AcquisitionDimensionLabels(_DimensionBase, StrEnum): """ Available dimension labels for acquisition data. @@ -150,45 +162,13 @@ class AcquisitionDimensionLabels(StrEnum): HORIZONTAL = auto() @classmethod - def get_order_for_engine(cls, engine: str, geometry=None) -> tuple: - """ - Returns the order of dimensions for a specific engine and geometry. - - Parameters - ---------- - geometry : AcquisitionGeometry, optional - If unspecified, the default order is returned. - """ - engine_orders = { + def _default_order(cls, engine: str) -> tuple: + engine = Backends(engine) + orders = { Backends.ASTRA: (cls.CHANNEL, cls.VERTICAL, cls.ANGLE, cls.HORIZONTAL), Backends.TIGRE: (cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL), Backends.CIL: (cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL)} - dim_order = engine_orders[Backends(engine)] - - if geometry is None: - return dim_order - return tuple(label for label in dim_order if label in geometry.dimension_labels) - - @classmethod - def check_order_for_engine(cls, engine: str, geometry) -> bool: - """ - Returns True iff the order of dimensions is correct for a specific engine and geometry. - - Parameters - ---------- - geometry: AcquisitionGeometry - - Raises - ------ - ValueError if the order of dimensions is incorrect. - """ - order_requested = cls.get_order_for_engine(engine, geometry) - if order_requested == tuple(geometry.dimension_labels): - return True - raise ValueError( - f"Expected dimension_label order {order_requested}," - f" got {tuple(geometry.dimension_labels)}." - f" Try using `data.reorder('{engine}')` to permute for {engine}") + return orders[engine] class FillTypes(StrEnum): From 11ab7482c626f0079f630f89973188ce112328b0 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 21 Aug 2024 12:58:55 +0100 Subject: [PATCH 65/72] AcquisitionType.validate --- .../cil/framework/acquisition_geometry.py | 39 ++++++++----------- Wrappers/Python/cil/framework/labels.py | 23 ++++++++++- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 36799f94a4..5c168f0b22 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -176,38 +176,30 @@ def set_direction(self, x, y): self._direction_x = x -class SystemConfiguration(object): - r'''This is a generic class to hold the description of a tomography system - ''' - +class SystemConfiguration: + '''This is a generic class to hold the description of a tomography system''' SYSTEM_SIMPLE = 'simple' SYSTEM_OFFSET = 'offset' SYSTEM_ADVANCED = 'advanced' @property def dimension(self): - return AcquisitionType.DIM2 if self._dimension == 2 else AcquisitionType.DIM3 - - @dimension.setter - def dimension(self,val): - if val != 2 and val != 3: - raise ValueError('Can set up 2D and 3D systems only. got {0}D'.format(val)) - else: - self._dimension = val + return self.acquisition_type.dimension @property def geometry(self): - return self._geometry + return self.acquisition_type.geometry - @geometry.setter - def geometry(self, val): - self._geometry = AcquisitionType(val) + @property + def acquisition_type(self): + return self._acquisition_type - def __init__(self, dof, geometry, units='units'): - """Initialises the system component attributes for the acquisition type - """ - self.dimension = dof - self.geometry = geometry + @acquisition_type.setter + def acquisition_type(self, val): + self._acquisition_type = AcquisitionType(val).validate() + + def __init__(self, dof: int, geometry, units='units'): + self.acquisition_type = AcquisitionType(f"{dof}D") | AcquisitionType(geometry) self.units = units if AcquisitionType.PARALLEL & self.geometry: @@ -215,7 +207,7 @@ def __init__(self, dof, geometry, units='units'): else: self.source = PositionVector(dof) - if dof == 2: + if AcquisitionType.DIM2 & self.dimension: self.detector = Detector1D(dof) self.rotation_axis = PositionVector(dof) else: @@ -1964,7 +1956,8 @@ def set_panel(self, num_pixels, pixel_size=(1,1), origin='bottom-left'): :return: returns a configured AcquisitionGeometry object :rtype: AcquisitionGeometry ''' - self.config.panel = Panel(num_pixels, pixel_size, origin, self.config.system._dimension) + dof = {AcquisitionType.DIM2: 2, AcquisitionType.DIM3: 3}[self.config.system.dimension] + self.config.panel = Panel(num_pixels, pixel_size, origin, dof) return self def set_channels(self, num_channels=1, channel_labels=None): diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index 6146ce8c66..c8cff9cff8 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -239,6 +239,19 @@ class AcquisitionType(Flag): DIM2 = auto() DIM3 = auto() + def validate(self): + assert len(self.dimension) < 2, f"{self} must be 2D xor 3D" + assert len(self.geometry) < 2, f"{self} must be parallel xor cone beam" + return self + + @property + def dimension(self): + return self & (self.DIM2 | self.DIM3) + + @property + def geometry(self): + return self & (self.PARALLEL | self.CONE) + @classmethod def _missing_(cls, value): """2D/3D aliases""" @@ -248,4 +261,12 @@ def _missing_(cls, value): def __str__(self) -> str: """2D/3D special handling""" - return '2D' if self == self.DIM2 else '3D' if self == self.DIM3 else self.name + return '2D' if self == self.DIM2 else '3D' if self == self.DIM3 else (self.name or super().__str__()) + + def __hash__(self) -> int: + """consistent hashing for dictionary keys""" + return hash(self.value) + + # compatibility with Python>=3.11 `enum.Flag` + def __len__(self) -> int: + return bin(self.value).count('1') From 4320645297cb2d0877536f4371f6ed6884dc4b58 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 21 Aug 2024 13:03:45 +0100 Subject: [PATCH 66/72] raise error on invalid dimensions --- Wrappers/Python/cil/framework/acquisition_geometry.py | 2 +- Wrappers/Python/cil/framework/image_geometry.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 5c168f0b22..55f769d688 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -1745,7 +1745,7 @@ def dimension_labels(self): @dimension_labels.setter def dimension_labels(self, val): if val is not None: - self._dimension_labels = tuple(AcquisitionDimensionLabels(x) for x in val if x in AcquisitionDimensionLabels) + self._dimension_labels = tuple(map(AcquisitionDimensionLabels, val)) @property def ndim(self): diff --git a/Wrappers/Python/cil/framework/image_geometry.py b/Wrappers/Python/cil/framework/image_geometry.py index e280638899..1be594b93d 100644 --- a/Wrappers/Python/cil/framework/image_geometry.py +++ b/Wrappers/Python/cil/framework/image_geometry.py @@ -113,10 +113,9 @@ def dimension_labels(self, val): def set_labels(self, labels): if labels is not None: - self._dimension_labels = tuple(ImageDimensionLabels(x) for x in labels if x in ImageDimensionLabels) + self._dimension_labels = tuple(map(ImageDimensionLabels, labels)) def __eq__(self, other): - if not isinstance(other, self.__class__): return False From 8bb8821092e071e0a6ba9fb9df9f10c5cfb7ed69 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 21 Aug 2024 13:04:31 +0100 Subject: [PATCH 67/72] rename labels --- .../Python/cil/framework/acquisition_data.py | 6 +- .../cil/framework/acquisition_geometry.py | 42 +++--- Wrappers/Python/cil/framework/block.py | 6 +- Wrappers/Python/cil/framework/image_data.py | 6 +- .../Python/cil/framework/image_geometry.py | 40 +++--- Wrappers/Python/cil/framework/labels.py | 34 ++--- .../Python/cil/framework/vector_geometry.py | 12 +- Wrappers/Python/cil/io/ZEISSDataReader.py | 18 +-- Wrappers/Python/cil/plugins/TomoPhantom.py | 6 +- .../astra/operators/ProjectionOperator.py | 6 +- .../astra/processors/AstraBackProjector3D.py | 6 +- .../processors/AstraForwardProjector3D.py | 6 +- .../cil/plugins/astra/processors/FBP.py | 6 +- .../utilities/convert_geometry_to_astra.py | 4 +- .../convert_geometry_to_astra_vec_2D.py | 4 +- .../convert_geometry_to_astra_vec_3D.py | 4 +- .../functions/regularisers.py | 4 +- Wrappers/Python/cil/plugins/tigre/FBP.py | 8 +- Wrappers/Python/cil/plugins/tigre/Geometry.py | 4 +- .../cil/plugins/tigre/ProjectionOperator.py | 8 +- .../cil/processors/CofR_image_sharpness.py | 4 +- .../Python/cil/processors/PaganinProcessor.py | 4 +- Wrappers/Python/cil/recon/Reconstructor.py | 4 +- Wrappers/Python/cil/utilities/dataexample.py | 12 +- Wrappers/Python/test/test_BlockOperator.py | 8 +- Wrappers/Python/test/test_DataContainer.py | 64 +++++----- Wrappers/Python/test/test_Operator.py | 10 +- .../Python/test/test_PluginsTomoPhantom.py | 6 +- Wrappers/Python/test/test_algorithms.py | 10 +- Wrappers/Python/test/test_functions.py | 14 +- Wrappers/Python/test/test_io.py | 4 +- Wrappers/Python/test/test_labels.py | 120 +++++++++--------- Wrappers/Python/test/test_ring_processor.py | 4 +- Wrappers/Python/test/test_subset.py | 72 +++++------ Wrappers/Python/test/utils_projectors.py | 30 ++--- 35 files changed, 298 insertions(+), 298 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_data.py b/Wrappers/Python/cil/framework/acquisition_data.py index 67b201d5cc..ece19f1946 100644 --- a/Wrappers/Python/cil/framework/acquisition_data.py +++ b/Wrappers/Python/cil/framework/acquisition_data.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt import numpy -from .labels import AcquisitionDimensionLabels, Backends +from .labels import AcquisitionDimension, Backend from .data_container import DataContainer from .partitioner import Partitioner @@ -114,7 +114,7 @@ def reorder(self, order): order: list or str Ordered list of labels from self.dimension_labels, or string 'astra' or 'tigre'. ''' - if order in Backends: - order = AcquisitionDimensionLabels.get_order_for_engine(order, self.geometry) + if order in Backend: + order = AcquisitionDimension.get_order_for_engine(order, self.geometry) super().reorder(order) diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 55f769d688..09bb759587 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -22,7 +22,7 @@ import numpy -from .labels import AcquisitionDimensionLabels, UnitsAngles, AcquisitionType, FillTypes +from .labels import AcquisitionDimension, AngleUnit, AcquisitionType, FillType from .acquisition_data import AcquisitionData from .image_geometry import ImageGeometry @@ -1457,7 +1457,7 @@ def angle_unit(self): @angle_unit.setter def angle_unit(self,val): - self._angle_unit = UnitsAngles(val) + self._angle_unit = AngleUnit(val) def __str__(self): repres = "Acquisition description:\n" @@ -1612,32 +1612,32 @@ class AcquisitionGeometry(object): @property def ANGLE(self): warnings.warn("use AcquisitionDimensionLabels.Angle instead", DeprecationWarning, stacklevel=2) - return AcquisitionDimensionLabels.ANGLE + return AcquisitionDimension.ANGLE @property def CHANNEL(self): warnings.warn("use AcquisitionDimensionLabels.Channel instead", DeprecationWarning, stacklevel=2) - return AcquisitionDimensionLabels.CHANNEL + return AcquisitionDimension.CHANNEL @property def DEGREE(self): warnings.warn("use UnitsAngles.DEGREE", DeprecationWarning, stacklevel=2) - return UnitsAngles.DEGREE + return AngleUnit.DEGREE @property def HORIZONTAL(self): warnings.warn("use AcquisitionDimensionLabels.HORIZONTAL instead", DeprecationWarning, stacklevel=2) - return AcquisitionDimensionLabels.HORIZONTAL + return AcquisitionDimension.HORIZONTAL @property def RADIAN(self): warnings.warn("use UnitsAngles.RADIAN instead", DeprecationWarning, stacklevel=2) - return UnitsAngles.RADIAN + return AngleUnit.RADIAN @property def VERTICAL(self): warnings.warn("use AcquisitionDimensionLabels.VERTICAL instead", DeprecationWarning, stacklevel=2) - return AcquisitionDimensionLabels.VERTICAL + return AcquisitionDimension.VERTICAL @property def geom_type(self): @@ -1709,15 +1709,15 @@ def dimension(self): @property def shape(self): - shape_dict = {AcquisitionDimensionLabels.CHANNEL: self.config.channels.num_channels, - AcquisitionDimensionLabels.ANGLE: self.config.angles.num_positions, - AcquisitionDimensionLabels.VERTICAL: self.config.panel.num_pixels[1], - AcquisitionDimensionLabels.HORIZONTAL: self.config.panel.num_pixels[0]} + shape_dict = {AcquisitionDimension.CHANNEL: self.config.channels.num_channels, + AcquisitionDimension.ANGLE: self.config.angles.num_positions, + AcquisitionDimension.VERTICAL: self.config.panel.num_pixels[1], + AcquisitionDimension.HORIZONTAL: self.config.panel.num_pixels[0]} return tuple(shape_dict[label] for label in self.dimension_labels) @property def dimension_labels(self): - labels_default = AcquisitionDimensionLabels.get_order_for_engine("cil") + labels_default = AcquisitionDimension.get_order_for_engine("cil") shape_default = [self.config.channels.num_channels, self.config.angles.num_positions, @@ -1745,7 +1745,7 @@ def dimension_labels(self): @dimension_labels.setter def dimension_labels(self, val): if val is not None: - self._dimension_labels = tuple(map(AcquisitionDimensionLabels, val)) + self._dimension_labels = tuple(map(AcquisitionDimension, val)) @property def ndim(self): @@ -1816,10 +1816,10 @@ def get_centre_of_rotation(self, distance_units='default', angle_units='radian') else: raise ValueError("`distance_units` is not recognised. Must be 'default' or 'pixels'. Got {}".format(distance_units)) - angle_units = UnitsAngles(angle_units) + angle_units = AngleUnit(angle_units) angle = angle_rad - if angle_units == UnitsAngles.DEGREE: + if angle_units == AngleUnit.DEGREE: angle = numpy.degrees(angle_rad) return {'offset': (offset, offset_units), 'angle': (angle, angle_units.value)} @@ -1860,10 +1860,10 @@ def set_centre_of_rotation(self, offset=0.0, distance_units='default', angle=0.0 raise NotImplementedError() - angle_units = UnitsAngles(angle_units) + angle_units = AngleUnit(angle_units) angle_rad = angle - if angle_units == UnitsAngles.DEGREE: + if angle_units == AngleUnit.DEGREE: angle_rad = numpy.radians(angle) if distance_units =='default': @@ -2181,8 +2181,8 @@ def allocate(self, value=0, **kwargs): if isinstance(value, Number): # it's created empty, so we make it 0 out.array.fill(value) - elif value in FillTypes: - if value == FillTypes.RANDOM: + elif value in FillType: + if value == FillType.RANDOM: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) @@ -2191,7 +2191,7 @@ def allocate(self, value=0, **kwargs): out.fill(r) else: out.fill(numpy.random.random_sample(self.shape)) - elif value == FillTypes.RANDOM_INT: + elif value == FillType.RANDOM_INT: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) diff --git a/Wrappers/Python/cil/framework/block.py b/Wrappers/Python/cil/framework/block.py index f564de2609..72af8bb7de 100644 --- a/Wrappers/Python/cil/framework/block.py +++ b/Wrappers/Python/cil/framework/block.py @@ -22,19 +22,19 @@ import numpy from ..utilities.multiprocessing import NUM_THREADS -from .labels import FillTypes +from .labels import FillType class BlockGeometry(object): @property def RANDOM(self): warnings.warn("use FillTypes.RANDOM instead", DeprecationWarning, stacklevel=2) - return FillTypes.RANDOM + return FillType.RANDOM @property def RANDOM_INT(self): warnings.warn("use FillTypes.RANDOM_INT instead", DeprecationWarning, stacklevel=2) - return FillTypes.RANDOM_INT + return FillType.RANDOM_INT @property def dtype(self): diff --git a/Wrappers/Python/cil/framework/image_data.py b/Wrappers/Python/cil/framework/image_data.py index 994b632015..daca1f1ee5 100644 --- a/Wrappers/Python/cil/framework/image_data.py +++ b/Wrappers/Python/cil/framework/image_data.py @@ -18,7 +18,7 @@ import numpy from .data_container import DataContainer -from .labels import ImageDimensionLabels, Backends +from .labels import ImageDimension, Backend class ImageData(DataContainer): '''DataContainer for holding 2D or 3D DataContainer''' @@ -200,7 +200,7 @@ def reorder(self, order): order: list or str Ordered list of labels from self.dimension_labels, or string 'astra' or 'tigre'. ''' - if order in Backends: - order = ImageDimensionLabels.get_order_for_engine(order, self.geometry) + if order in Backend: + order = ImageDimension.get_order_for_engine(order, self.geometry) super().reorder(order) diff --git a/Wrappers/Python/cil/framework/image_geometry.py b/Wrappers/Python/cil/framework/image_geometry.py index 1be594b93d..f2d2745871 100644 --- a/Wrappers/Python/cil/framework/image_geometry.py +++ b/Wrappers/Python/cil/framework/image_geometry.py @@ -22,45 +22,45 @@ import numpy from .image_data import ImageData -from .labels import ImageDimensionLabels, FillTypes +from .labels import ImageDimension, FillType class ImageGeometry: @property def CHANNEL(self): warnings.warn("use ImageDimensionLabels.CHANNEL instead", DeprecationWarning, stacklevel=2) - return ImageDimensionLabels.CHANNEL + return ImageDimension.CHANNEL @property def HORIZONTAL_X(self): warnings.warn("use ImageDimensionLabels.HORIZONTAL_X instead", DeprecationWarning, stacklevel=2) - return ImageDimensionLabels.HORIZONTAL_X + return ImageDimension.HORIZONTAL_X @property def HORIZONTAL_Y(self): warnings.warn("use ImageDimensionLabels.HORIZONTAL_Y instead", DeprecationWarning, stacklevel=2) - return ImageDimensionLabels.HORIZONTAL_Y + return ImageDimension.HORIZONTAL_Y @property def RANDOM(self): warnings.warn("use FillTypes.RANDOM instead", DeprecationWarning, stacklevel=2) - return FillTypes.RANDOM + return FillType.RANDOM @property def RANDOM_INT(self): warnings.warn("use FillTypes.RANDOM_INT instead", DeprecationWarning, stacklevel=2) - return FillTypes.RANDOM_INT + return FillType.RANDOM_INT @property def VERTICAL(self): warnings.warn("use ImageDimensionLabels.VERTICAL instead", DeprecationWarning, stacklevel=2) - return ImageDimensionLabels.VERTICAL + return ImageDimension.VERTICAL @property def shape(self): - shape_dict = {ImageDimensionLabels.CHANNEL: self.channels, - ImageDimensionLabels.VERTICAL: self.voxel_num_z, - ImageDimensionLabels.HORIZONTAL_Y: self.voxel_num_y, - ImageDimensionLabels.HORIZONTAL_X: self.voxel_num_x} + shape_dict = {ImageDimension.CHANNEL: self.channels, + ImageDimension.VERTICAL: self.voxel_num_z, + ImageDimension.HORIZONTAL_Y: self.voxel_num_y, + ImageDimension.HORIZONTAL_X: self.voxel_num_x} return tuple(shape_dict[label] for label in self.dimension_labels) @shape.setter @@ -69,10 +69,10 @@ def shape(self, val): @property def spacing(self): - spacing_dict = {ImageDimensionLabels.CHANNEL: self.channel_spacing, - ImageDimensionLabels.VERTICAL: self.voxel_size_z, - ImageDimensionLabels.HORIZONTAL_Y: self.voxel_size_y, - ImageDimensionLabels.HORIZONTAL_X: self.voxel_size_x} + spacing_dict = {ImageDimension.CHANNEL: self.channel_spacing, + ImageDimension.VERTICAL: self.voxel_size_z, + ImageDimension.HORIZONTAL_Y: self.voxel_size_y, + ImageDimension.HORIZONTAL_X: self.voxel_size_x} return tuple(spacing_dict[label] for label in self.dimension_labels) @property @@ -86,7 +86,7 @@ def ndim(self): @property def dimension_labels(self): - labels_default = ImageDimensionLabels.get_order_for_engine("cil") + labels_default = ImageDimension.get_order_for_engine("cil") shape_default = [ self.channels, self.voxel_num_z, @@ -113,7 +113,7 @@ def dimension_labels(self, val): def set_labels(self, labels): if labels is not None: - self._dimension_labels = tuple(map(ImageDimensionLabels, labels)) + self._dimension_labels = tuple(map(ImageDimension, labels)) def __eq__(self, other): if not isinstance(other, self.__class__): @@ -274,8 +274,8 @@ def allocate(self, value=0, **kwargs): if isinstance(value, Number): # it's created empty, so we make it 0 out.array.fill(value) - elif value in FillTypes: - if value == FillTypes.RANDOM: + elif value in FillType: + if value == FillType.RANDOM: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) @@ -285,7 +285,7 @@ def allocate(self, value=0, **kwargs): else: out.fill(numpy.random.random_sample(self.shape)) - elif value == FillTypes.RANDOM_INT: + elif value == FillType.RANDOM_INT: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index c8cff9cff8..a6aa4adf2f 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -59,7 +59,7 @@ def _generate_next_value_(name: str, start, count, last_values) -> str: return name.lower() -class Backends(StrEnum): +class Backend(StrEnum): """ Available backends for CIL. @@ -117,14 +117,14 @@ def check_order_for_engine(cls, engine: str, geometry) -> bool: f" Try using `data.reorder('{engine}')` to permute for {engine}") -class ImageDimensionLabels(_DimensionBase, StrEnum): +class ImageDimension(_DimensionBase, StrEnum): """ Available dimension labels for image data. Examples -------- ``` - data.reorder([ImageDimensionLabels.HORIZONTAL_X, ImageDimensionLabels.VERTICAL]) + data.reorder([ImageDimension.HORIZONTAL_X, ImageDimension.VERTICAL]) data.reorder(["horizontal_x", "vertical"]) ``` """ @@ -135,24 +135,24 @@ class ImageDimensionLabels(_DimensionBase, StrEnum): @classmethod def _default_order(cls, engine: str) -> tuple: - engine = Backends(engine) + engine = Backend(engine) orders = { - Backends.ASTRA: (cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X), - Backends.TIGRE: (cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X), - Backends.CIL: (cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X)} + Backend.ASTRA: (cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X), + Backend.TIGRE: (cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X), + Backend.CIL: (cls.CHANNEL, cls.VERTICAL, cls.HORIZONTAL_Y, cls.HORIZONTAL_X)} return orders[engine] -class AcquisitionDimensionLabels(_DimensionBase, StrEnum): +class AcquisitionDimension(_DimensionBase, StrEnum): """ Available dimension labels for acquisition data. Examples -------- ``` - data.reorder([AcquisitionDimensionLabels.CHANNEL, - AcquisitionDimensionLabels.ANGLE, - AcquisitionDimensionLabels.HORIZONTAL]) + data.reorder([AcquisitionDimension.CHANNEL, + AcquisitionDimension.ANGLE, + AcquisitionDimension.HORIZONTAL]) data.reorder(["channel", "angle", "horizontal"]) ``` """ @@ -163,15 +163,15 @@ class AcquisitionDimensionLabels(_DimensionBase, StrEnum): @classmethod def _default_order(cls, engine: str) -> tuple: - engine = Backends(engine) + engine = Backend(engine) orders = { - Backends.ASTRA: (cls.CHANNEL, cls.VERTICAL, cls.ANGLE, cls.HORIZONTAL), - Backends.TIGRE: (cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL), - Backends.CIL: (cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL)} + Backend.ASTRA: (cls.CHANNEL, cls.VERTICAL, cls.ANGLE, cls.HORIZONTAL), + Backend.TIGRE: (cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL), + Backend.CIL: (cls.CHANNEL, cls.ANGLE, cls.VERTICAL, cls.HORIZONTAL)} return orders[engine] -class FillTypes(StrEnum): +class FillType(StrEnum): """ Available fill types for image data. @@ -191,7 +191,7 @@ class FillTypes(StrEnum): RANDOM_INT = auto() -class UnitsAngles(StrEnum): +class AngleUnit(StrEnum): """ Available units for angles. diff --git a/Wrappers/Python/cil/framework/vector_geometry.py b/Wrappers/Python/cil/framework/vector_geometry.py index 5066e87c90..4f0d62b769 100644 --- a/Wrappers/Python/cil/framework/vector_geometry.py +++ b/Wrappers/Python/cil/framework/vector_geometry.py @@ -21,7 +21,7 @@ import numpy -from .labels import FillTypes +from .labels import FillType class VectorGeometry: @@ -29,12 +29,12 @@ class VectorGeometry: @property def RANDOM(self): warnings.warn("use FillTypes.RANDOM instead", DeprecationWarning, stacklevel=2) - return FillTypes.RANDOM + return FillType.RANDOM @property def RANDOM_INT(self): warnings.warn("use FillTypes.RANDOM_INT instead", DeprecationWarning, stacklevel=2) - return FillTypes.RANDOM_INT + return FillType.RANDOM_INT @property def dtype(self): @@ -98,8 +98,8 @@ def allocate(self, value=0, **kwargs): if isinstance(value, Number): if value != 0: out += value - elif value in FillTypes: - if value == FillTypes.RANDOM: + elif value in FillType: + if value == FillType.RANDOM: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) @@ -107,7 +107,7 @@ def allocate(self, value=0, **kwargs): out.fill(numpy.random.random_sample(self.shape) + 1.j*numpy.random.random_sample(self.shape)) else: out.fill(numpy.random.random_sample(self.shape)) - elif value == FillTypes.RANDOM_INT: + elif value == FillType.RANDOM_INT: seed = kwargs.get('seed', None) if seed is not None: numpy.random.seed(seed) diff --git a/Wrappers/Python/cil/io/ZEISSDataReader.py b/Wrappers/Python/cil/io/ZEISSDataReader.py index 07e7cde6af..0e3bf86deb 100644 --- a/Wrappers/Python/cil/io/ZEISSDataReader.py +++ b/Wrappers/Python/cil/io/ZEISSDataReader.py @@ -19,7 +19,7 @@ from cil.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry -from cil.framework.labels import UnitsAngles, AcquisitionDimensionLabels, ImageDimensionLabels +from cil.framework.labels import AngleUnit, AcquisitionDimension, ImageDimension import numpy as np import os import olefile @@ -128,13 +128,13 @@ def set_up(self, if roi is not None: if metadata['data geometry'] == 'acquisition': - zeiss_data_order = {AcquisitionDimensionLabels.ANGLE: 0, - AcquisitionDimensionLabels.VERTICAL: 1, - AcquisitionDimensionLabels.HORIZONTAL: 2} + zeiss_data_order = {AcquisitionDimension.ANGLE: 0, + AcquisitionDimension.VERTICAL: 1, + AcquisitionDimension.HORIZONTAL: 2} else: - zeiss_data_order = {ImageDimensionLabels.VERTICAL: 0, - ImageDimensionLabels.HORIZONTAL_Y: 1, - ImageDimensionLabels.HORIZONTAL_X: 2} + zeiss_data_order = {ImageDimension.VERTICAL: 0, + ImageDimension.HORIZONTAL_Y: 1, + ImageDimension.HORIZONTAL_X: 2} # check roi labels and create tuple for slicing for key in roi.keys(): @@ -232,11 +232,11 @@ def _setup_acq_geometry(self): ) \ .set_panel([self._metadata['image_width'], self._metadata['image_height']],\ pixel_size=[self._metadata['detector_pixel_size']/1000,self._metadata['detector_pixel_size']/1000])\ - .set_angles(self._metadata['thetas'],angle_unit=UnitsAngles.RADIAN) + .set_angles(self._metadata['thetas'],angle_unit=AngleUnit.RADIAN) else: self._geometry = AcquisitionGeometry.create_Parallel3D()\ .set_panel([self._metadata['image_width'], self._metadata['image_height']])\ - .set_angles(self._metadata['thetas'],angle_unit=UnitsAngles.RADIAN) + .set_angles(self._metadata['thetas'],angle_unit=AngleUnit.RADIAN) self._geometry.dimension_labels = ['angle', 'vertical', 'horizontal'] def _setup_image_geometry(self): diff --git a/Wrappers/Python/cil/plugins/TomoPhantom.py b/Wrappers/Python/cil/plugins/TomoPhantom.py index f0933fa4c8..ce6e6ac14b 100644 --- a/Wrappers/Python/cil/plugins/TomoPhantom.py +++ b/Wrappers/Python/cil/plugins/TomoPhantom.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import ImageData -from cil.framework.labels import ImageDimensionLabels +from cil.framework.labels import ImageDimension import tomophantom from tomophantom import TomoP2D, TomoP3D import os @@ -148,10 +148,10 @@ def get_ImageData(num_model, geometry): ''' ig = geometry.copy() - ig.set_labels(ImageDimensionLabels.get_order_for_engine('cil')) + ig.set_labels(ImageDimension.get_order_for_engine('cil')) num_dims = len(ig.dimension_labels) - if ImageDimensionLabels.CHANNEL in ig.dimension_labels: + if ImageDimension.CHANNEL in ig.dimension_labels: if not is_model_temporal(num_model): raise ValueError('Selected model {} is not a temporal model, please change your selection'.format(num_model)) if num_dims == 4: diff --git a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py index fe276b55d0..e1d4cb63cd 100644 --- a/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/astra/operators/ProjectionOperator.py @@ -18,7 +18,7 @@ import logging from cil.framework import BlockGeometry -from cil.framework.labels import AcquisitionDimensionLabels, ImageDimensionLabels, AcquisitionType +from cil.framework.labels import AcquisitionDimension, ImageDimension, AcquisitionType from cil.optimisation.operators import BlockOperator, LinearOperator, ChannelwiseOperator from cil.plugins.astra.operators import AstraProjector2D, AstraProjector3D @@ -115,8 +115,8 @@ def __init__(self, self).__init__(domain_geometry=image_geometry, range_geometry=acquisition_geometry) - AcquisitionDimensionLabels.check_order_for_engine('astra',acquisition_geometry) - ImageDimensionLabels.check_order_for_engine('astra',image_geometry) + AcquisitionDimension.check_order_for_engine('astra',acquisition_geometry) + ImageDimension.check_order_for_engine('astra',image_geometry) self.volume_geometry = image_geometry self.sinogram_geometry = acquisition_geometry diff --git a/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py b/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py index c6e47e00dc..ffacce0267 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py +++ b/Wrappers/Python/cil/plugins/astra/processors/AstraBackProjector3D.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import DataProcessor, ImageData -from cil.framework.labels import AcquisitionDimensionLabels, ImageDimensionLabels +from cil.framework.labels import AcquisitionDimension, ImageDimension from cil.plugins.astra.utilities import convert_geometry_to_astra_vec_3D import astra from astra import astra_dict, algorithm, data3d @@ -65,7 +65,7 @@ def check_input(self, dataset): def set_ImageGeometry(self, volume_geometry): - ImageDimensionLabels.check_order_for_engine('astra', volume_geometry) + ImageDimension.check_order_for_engine('astra', volume_geometry) if len(volume_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(volume_geometry.number_of_dimensions)) @@ -74,7 +74,7 @@ def set_ImageGeometry(self, volume_geometry): def set_AcquisitionGeometry(self, sinogram_geometry): - AcquisitionDimensionLabels.check_order_for_engine('astra', sinogram_geometry) + AcquisitionDimension.check_order_for_engine('astra', sinogram_geometry) if len(sinogram_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(sinogram_geometry.number_of_dimensions)) diff --git a/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py b/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py index 455e65959b..b90d88c5dd 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py +++ b/Wrappers/Python/cil/plugins/astra/processors/AstraForwardProjector3D.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import DataProcessor, AcquisitionData -from cil.framework.labels import ImageDimensionLabels, AcquisitionDimensionLabels +from cil.framework.labels import ImageDimension, AcquisitionDimension from cil.plugins.astra.utilities import convert_geometry_to_astra_vec_3D import astra from astra import astra_dict, algorithm, data3d @@ -63,7 +63,7 @@ def check_input(self, dataset): def set_ImageGeometry(self, volume_geometry): - ImageDimensionLabels.check_order_for_engine('astra', volume_geometry) + ImageDimension.check_order_for_engine('astra', volume_geometry) if len(volume_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(volume_geometry.number_of_dimensions)) @@ -72,7 +72,7 @@ def set_ImageGeometry(self, volume_geometry): def set_AcquisitionGeometry(self, sinogram_geometry): - AcquisitionDimensionLabels.check_order_for_engine('astra', sinogram_geometry) + AcquisitionDimension.check_order_for_engine('astra', sinogram_geometry) if len(sinogram_geometry.dimension_labels) > 3: raise ValueError("Supports 2D and 3D data only, got {0}".format(sinogram_geometry.number_of_dimensions)) diff --git a/Wrappers/Python/cil/plugins/astra/processors/FBP.py b/Wrappers/Python/cil/plugins/astra/processors/FBP.py index 0bca1d23b8..2da98ec6aa 100644 --- a/Wrappers/Python/cil/plugins/astra/processors/FBP.py +++ b/Wrappers/Python/cil/plugins/astra/processors/FBP.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import DataProcessor -from cil.framework.labels import ImageDimensionLabels, AcquisitionDimensionLabels, AcquisitionType +from cil.framework.labels import ImageDimension, AcquisitionDimension, AcquisitionType from cil.plugins.astra.processors.FBP_Flexible import FBP_Flexible from cil.plugins.astra.processors.FDK_Flexible import FDK_Flexible from cil.plugins.astra.processors.FBP_Flexible import FBP_CPU @@ -65,8 +65,8 @@ def __init__(self, image_geometry=None, acquisition_geometry=None, device='gpu') if image_geometry is None: image_geometry = acquisition_geometry.get_ImageGeometry() - AcquisitionDimensionLabels.check_order_for_engine('astra', acquisition_geometry) - ImageDimensionLabels.check_order_for_engine('astra', image_geometry) + AcquisitionDimension.check_order_for_engine('astra', acquisition_geometry) + ImageDimension.check_order_for_engine('astra', image_geometry) if device == 'gpu': if acquisition_geometry.geom_type == 'parallel': diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py index fdf232b3f7..15fbd49761 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra.py @@ -19,7 +19,7 @@ import astra import numpy as np -from cil.framework.labels import AcquisitionType, UnitsAngles +from cil.framework.labels import AcquisitionType, AngleUnit def convert_geometry_to_astra(volume_geometry, sinogram_geometry): """ @@ -44,7 +44,7 @@ def convert_geometry_to_astra(volume_geometry, sinogram_geometry): #get units - if sinogram_geometry.config.angles.angle_unit == UnitsAngles.DEGREE: + if sinogram_geometry.config.angles.angle_unit == AngleUnit.DEGREE: angles_rad = sinogram_geometry.config.angles.angle_data * np.pi / 180.0 else: angles_rad = sinogram_geometry.config.angles.angle_data diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py index 1e15bfd8e1..75a9ef8fa1 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_2D.py @@ -19,7 +19,7 @@ import astra import numpy as np -from cil.framework.labels import UnitsAngles +from cil.framework.labels import AngleUnit def convert_geometry_to_astra_vec_2D(volume_geometry, sinogram_geometry_in): @@ -53,7 +53,7 @@ def convert_geometry_to_astra_vec_2D(volume_geometry, sinogram_geometry_in): panel = sinogram_geometry.config.panel #get units - degrees = angles.angle_unit == UnitsAngles.DEGREE + degrees = angles.angle_unit == AngleUnit.DEGREE #create a 2D astra geom from 2D CIL geometry, 2D astra geometry has axis flipped compared to 3D volume_geometry_temp = volume_geometry.copy() diff --git a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py index 0ea14f4e26..d03322df74 100644 --- a/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py +++ b/Wrappers/Python/cil/plugins/astra/utilities/convert_geometry_to_astra_vec_3D.py @@ -19,7 +19,7 @@ import astra import numpy as np -from cil.framework.labels import AcquisitionType, UnitsAngles +from cil.framework.labels import AcquisitionType, AngleUnit def convert_geometry_to_astra_vec_3D(volume_geometry, sinogram_geometry_in): @@ -55,7 +55,7 @@ def convert_geometry_to_astra_vec_3D(volume_geometry, sinogram_geometry_in): panel = sinogram_geometry.config.panel #get units - degrees = angles.angle_unit == UnitsAngles.DEGREE + degrees = angles.angle_unit == AngleUnit.DEGREE if AcquisitionType.DIM2 & sinogram_geometry.dimension: #create a 3D astra geom from 2D CIL geometry diff --git a/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py b/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py index 061813e70b..c9e46af6d3 100644 --- a/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py +++ b/Wrappers/Python/cil/plugins/ccpi_regularisation/functions/regularisers.py @@ -24,7 +24,7 @@ from cil.framework import DataContainer -from cil.framework.labels import ImageDimensionLabels +from cil.framework.labels import ImageDimension from cil.optimisation.functions import Function import numpy as np import warnings @@ -495,7 +495,7 @@ def __rmul__(self, scalar): def check_input(self, input): '''TNV requires 2D+channel data with the first dimension as the channel dimension''' if isinstance(input, DataContainer): - ImageDimensionLabels.check_order_for_engine('cil', input.geometry) + ImageDimension.check_order_for_engine('cil', input.geometry) if ( input.geometry.channels == 1 ) or ( not input.geometry.ndim == 3) : raise ValueError('TNV requires 2D+channel data. Got {}'.format(input.geometry.dimension_labels)) else: diff --git a/Wrappers/Python/cil/plugins/tigre/FBP.py b/Wrappers/Python/cil/plugins/tigre/FBP.py index 1f7d01871c..b27de5bb52 100644 --- a/Wrappers/Python/cil/plugins/tigre/FBP.py +++ b/Wrappers/Python/cil/plugins/tigre/FBP.py @@ -21,7 +21,7 @@ import numpy as np from cil.framework import DataProcessor, ImageData -from cil.framework.labels import AcquisitionDimensionLabels, ImageDimensionLabels +from cil.framework.labels import AcquisitionDimension, ImageDimension from cil.plugins.tigre import CIL2TIGREGeometry try: @@ -64,8 +64,8 @@ def __init__(self, image_geometry=None, acquisition_geometry=None, **kwargs): raise ValueError("TIGRE FBP is GPU only. Got device = {}".format(device)) - AcquisitionDimensionLabels.check_order_for_engine('tigre', acquisition_geometry) - ImageDimensionLabels.check_order_for_engine('tigre', image_geometry) + AcquisitionDimension.check_order_for_engine('tigre', acquisition_geometry) + ImageDimension.check_order_for_engine('tigre', image_geometry) tigre_geom, tigre_angles = CIL2TIGREGeometry.getTIGREGeometry(image_geometry,acquisition_geometry) @@ -80,7 +80,7 @@ def check_input(self, dataset): raise ValueError("Expected input data to be single channel, got {0}"\ .format(self.acquisition_geometry.channels)) - AcquisitionDimensionLabels.check_order_for_engine('tigre', dataset.geometry) + AcquisitionDimension.check_order_for_engine('tigre', dataset.geometry) return True def process(self, out=None): diff --git a/Wrappers/Python/cil/plugins/tigre/Geometry.py b/Wrappers/Python/cil/plugins/tigre/Geometry.py index 14ce391d60..358b5bca0e 100644 --- a/Wrappers/Python/cil/plugins/tigre/Geometry.py +++ b/Wrappers/Python/cil/plugins/tigre/Geometry.py @@ -16,7 +16,7 @@ # Authors: # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt -from cil.framework.labels import AcquisitionType, UnitsAngles +from cil.framework.labels import AcquisitionType, AngleUnit import numpy as np try: @@ -33,7 +33,7 @@ def getTIGREGeometry(ig, ag): #angles angles = ag.config.angles.angle_data + ag.config.angles.initial_angle - if ag.config.angles.angle_unit == UnitsAngles.DEGREE: + if ag.config.angles.angle_unit == AngleUnit.DEGREE: angles *= (np.pi/180.) #convert CIL to TIGRE angles s diff --git a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py index 9a9246e05b..e6248b6863 100644 --- a/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py +++ b/Wrappers/Python/cil/plugins/tigre/ProjectionOperator.py @@ -20,7 +20,7 @@ import numpy as np from cil.framework import ImageData, AcquisitionData, BlockGeometry -from cil.framework.labels import AcquisitionDimensionLabels, ImageDimensionLabels +from cil.framework.labels import AcquisitionDimension, ImageDimension from cil.optimisation.operators import BlockOperator, LinearOperator from cil.plugins.tigre import CIL2TIGREGeometry @@ -145,8 +145,8 @@ def __init__(self, "TIGRE projectors are GPU only. Got device = {}".format( device)) - ImageDimensionLabels.check_order_for_engine('tigre', image_geometry) - AcquisitionDimensionLabels.check_order_for_engine('tigre', acquisition_geometry) + ImageDimension.check_order_for_engine('tigre', image_geometry) + AcquisitionDimension.check_order_for_engine('tigre', acquisition_geometry) super(ProjectionOperator,self).__init__(domain_geometry=image_geometry,\ range_geometry=acquisition_geometry) @@ -221,7 +221,7 @@ def adjoint(self, x, out=None): data = x.as_array() #if single angle projection add the dimension in for TIGRE - if x.dimension_labels[0] != AcquisitionDimensionLabels.ANGLE: + if x.dimension_labels[0] != AcquisitionDimension.ANGLE: data = np.expand_dims(data, axis=0) if self.tigre_geom.is2D: diff --git a/Wrappers/Python/cil/processors/CofR_image_sharpness.py b/Wrappers/Python/cil/processors/CofR_image_sharpness.py index 032307f678..1be84e54f1 100644 --- a/Wrappers/Python/cil/processors/CofR_image_sharpness.py +++ b/Wrappers/Python/cil/processors/CofR_image_sharpness.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import Processor, AcquisitionData -from cil.framework.labels import AcquisitionDimensionLabels, AcquisitionType +from cil.framework.labels import AcquisitionDimension, AcquisitionType import matplotlib.pyplot as plt import scipy import numpy as np @@ -124,7 +124,7 @@ def check_input(self, data): #check order for single slice data test_geom = data.geometry.get_slice(vertical='centre') if AcquisitionType.DIM3 & data.geometry.dimension else data.geometry - if not AcquisitionDimensionLabels.check_order_for_engine(self.backend, test_geom): + if not AcquisitionDimension.check_order_for_engine(self.backend, test_geom): raise ValueError("Input data must be reordered for use with selected backend. Use input.reorder{'{0}')".format(self.backend)) return True diff --git a/Wrappers/Python/cil/processors/PaganinProcessor.py b/Wrappers/Python/cil/processors/PaganinProcessor.py index 87ca5d2d60..4d5b3d07f3 100644 --- a/Wrappers/Python/cil/processors/PaganinProcessor.py +++ b/Wrappers/Python/cil/processors/PaganinProcessor.py @@ -18,7 +18,7 @@ # https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import Processor, AcquisitionData -from cil.framework.labels import AcquisitionDimensionLabels +from cil.framework.labels import AcquisitionDimension import numpy as np from scipy.fft import fft2 @@ -207,7 +207,7 @@ def check_input(self, data): def process(self, out=None): data = self.get_input() - cil_order = tuple(AcquisitionDimensionLabels.get_order_for_engine('cil',data.geometry)) + cil_order = tuple(AcquisitionDimension.get_order_for_engine('cil',data.geometry)) if data.dimension_labels != cil_order: log.warning(msg="This processor will work most efficiently using\ \nCIL data order, consider using `data.reorder('cil')`") diff --git a/Wrappers/Python/cil/recon/Reconstructor.py b/Wrappers/Python/cil/recon/Reconstructor.py index 4ddac2a33e..b2b6972664 100644 --- a/Wrappers/Python/cil/recon/Reconstructor.py +++ b/Wrappers/Python/cil/recon/Reconstructor.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import AcquisitionData, ImageGeometry -from cil.framework.labels import AcquisitionDimensionLabels +from cil.framework.labels import AcquisitionDimension import importlib import weakref @@ -106,7 +106,7 @@ def _configure_for_backend(self, backend='tigre'): if backend not in self.supported_backends: raise ValueError("Backend unsupported. Supported backends: {}".format(self.supported_backends)) - if not AcquisitionDimensionLabels.check_order_for_engine(backend, self.acquisition_geometry): + if not AcquisitionDimension.check_order_for_engine(backend, self.acquisition_geometry): raise ValueError("Input data must be reordered for use with selected backend. Use input.reorder{'{0}')".format(backend)) #set ProjectionOperator class from backend diff --git a/Wrappers/Python/cil/utilities/dataexample.py b/Wrappers/Python/cil/utilities/dataexample.py index faab6d1f47..cd0fb93de3 100644 --- a/Wrappers/Python/cil/utilities/dataexample.py +++ b/Wrappers/Python/cil/utilities/dataexample.py @@ -17,7 +17,7 @@ # CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt from cil.framework import ImageGeometry -from cil.framework.labels import ImageDimensionLabels +from cil.framework.labels import ImageDimension import numpy import numpy as np from PIL import Image @@ -355,7 +355,7 @@ def load(self, which, size=None, scale=(0,1), **kwargs): sdata = numpy.zeros((N, M)) sdata[int(round(N/4)):int(round(3*N/4)), int(round(M/4)):int(round(3*M/4))] = 0.5 sdata[int(round(N/8)):int(round(7*N/8)), int(round(3*M/8)):int(round(5*M/8))] = 1 - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[ImageDimension.HORIZONTAL_Y, ImageDimension.HORIZONTAL_X]) data = ig.allocate() data.fill(sdata) @@ -370,7 +370,7 @@ def load(self, which, size=None, scale=(0,1), **kwargs): N = size[0] M = size[1] - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[ImageDimension.HORIZONTAL_Y, ImageDimension.HORIZONTAL_X]) data = ig.allocate() tmp = numpy.array(f.convert('L').resize((M,N))) data.fill(tmp/numpy.max(tmp)) @@ -392,13 +392,13 @@ def load(self, which, size=None, scale=(0,1), **kwargs): bands = tmp.getbands() ig = ImageGeometry(voxel_num_x=M, voxel_num_y=N, channels=len(bands), - dimension_labels=[ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X,ImageDimensionLabels.CHANNEL]) + dimension_labels=[ImageDimension.HORIZONTAL_Y, ImageDimension.HORIZONTAL_X,ImageDimension.CHANNEL]) data = ig.allocate() data.fill(numpy.array(tmp.resize((M,N)))) - data.reorder([ImageDimensionLabels.CHANNEL,ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X]) + data.reorder([ImageDimension.CHANNEL,ImageDimension.HORIZONTAL_Y, ImageDimension.HORIZONTAL_X]) data.geometry.channel_labels = bands else: - ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[ImageDimensionLabels.HORIZONTAL_Y, ImageDimensionLabels.HORIZONTAL_X]) + ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N, dimension_labels=[ImageDimension.HORIZONTAL_Y, ImageDimension.HORIZONTAL_X]) data = ig.allocate() data.fill(numpy.array(tmp.resize((M,N)))) diff --git a/Wrappers/Python/test/test_BlockOperator.py b/Wrappers/Python/test/test_BlockOperator.py index 3ba51f688c..910a711fda 100644 --- a/Wrappers/Python/test/test_BlockOperator.py +++ b/Wrappers/Python/test/test_BlockOperator.py @@ -21,7 +21,7 @@ import logging from cil.optimisation.operators import BlockOperator, GradientOperator from cil.framework import BlockDataContainer, BlockGeometry, ImageGeometry -from cil.framework.labels import FillTypes +from cil.framework.labels import FillType from cil.optimisation.operators import IdentityOperator import numpy from cil.optimisation.operators import FiniteDifferenceOperator @@ -59,7 +59,7 @@ def test_BlockOperator(self): self.assertBlockDataContainerEqual(z1, res) - z1 = B.range_geometry().allocate(FillTypes["RANDOM"]) + z1 = B.range_geometry().allocate(FillType["RANDOM"]) res1 = B.adjoint(z1) res2 = B.domain_geometry().allocate() @@ -160,7 +160,7 @@ def test_BlockOperator(self): ) B1 = BlockOperator(G, Id) - U = ig.allocate(FillTypes["RANDOM"]) + U = ig.allocate(FillType["RANDOM"]) #U = BlockDataContainer(u,u) RES1 = B1.range_geometry().allocate() @@ -285,7 +285,7 @@ def test_BlockOperatorLinearValidity(self): B = BlockOperator(G, Id) # Nx1 case u = ig.allocate('random', seed=2) - w = B.range_geometry().allocate(FillTypes["RANDOM"], seed=3) + w = B.range_geometry().allocate(FillType["RANDOM"], seed=3) w1 = B.direct(u) u1 = B.adjoint(w) self.assertAlmostEqual((w * w1).sum() , (u1*u).sum(), places=5) diff --git a/Wrappers/Python/test/test_DataContainer.py b/Wrappers/Python/test/test_DataContainer.py index b955d1e485..780ebdcd7d 100644 --- a/Wrappers/Python/test/test_DataContainer.py +++ b/Wrappers/Python/test/test_DataContainer.py @@ -20,7 +20,7 @@ import numpy from cil.framework import (DataContainer, ImageGeometry, ImageData, VectorGeometry, AcquisitionData, AcquisitionGeometry, BlockGeometry, VectorData) -from cil.framework.labels import ImageDimensionLabels, AcquisitionDimensionLabels +from cil.framework.labels import ImageDimension, AcquisitionDimension from timeit import default_timer as timer import logging from testclass import CCPiTestClass @@ -456,8 +456,8 @@ def test_ImageData(self): self.assertEqual(vol.number_of_dimensions, 3) ig2 = ImageGeometry (voxel_num_x=2,voxel_num_y=3,voxel_num_z=4, - dimension_labels=[ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["HORIZONTAL_Y"], - ImageDimensionLabels["VERTICAL"]]) + dimension_labels=[ImageDimension["HORIZONTAL_X"], ImageDimension["HORIZONTAL_Y"], + ImageDimension["VERTICAL"]]) data = ig2.allocate() self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ig2.shape)) self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape) @@ -535,8 +535,8 @@ def test_AcquisitionData(self): self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape) ag2 = AcquisitionGeometry.create_Parallel3D().set_angles(numpy.linspace(0, 180, num=10)).set_panel((2,3)).set_channels(4)\ - .set_labels([AcquisitionDimensionLabels["VERTICAL"] , - AcquisitionDimensionLabels["ANGLE"], AcquisitionDimensionLabels["HORIZONTAL"], AcquisitionDimensionLabels["CHANNEL"]]) + .set_labels([AcquisitionDimension["VERTICAL"] , + AcquisitionDimension["ANGLE"], AcquisitionDimension["HORIZONTAL"], AcquisitionDimension["CHANNEL"]]) data = ag2.allocate() self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ag2.shape)) @@ -815,27 +815,27 @@ def test_AcquisitionDataSubset(self): # expected dimension_labels - self.assertListEqual([AcquisitionDimensionLabels["CHANNEL"] , - AcquisitionDimensionLabels["ANGLE"] , AcquisitionDimensionLabels["VERTICAL"] , - AcquisitionDimensionLabels["HORIZONTAL"]], + self.assertListEqual([AcquisitionDimension["CHANNEL"] , + AcquisitionDimension["ANGLE"] , AcquisitionDimension["VERTICAL"] , + AcquisitionDimension["HORIZONTAL"]], list(sgeometry.dimension_labels)) sino = sgeometry.allocate() # test reshape - new_order = [AcquisitionDimensionLabels["HORIZONTAL"] , - AcquisitionDimensionLabels["CHANNEL"] , AcquisitionDimensionLabels["VERTICAL"] , - AcquisitionDimensionLabels["ANGLE"]] + new_order = [AcquisitionDimension["HORIZONTAL"] , + AcquisitionDimension["CHANNEL"] , AcquisitionDimension["VERTICAL"] , + AcquisitionDimension["ANGLE"]] sino.reorder(new_order) self.assertListEqual(new_order, list(sino.geometry.dimension_labels)) ss1 = sino.get_slice(vertical = 0) - self.assertListEqual([AcquisitionDimensionLabels["HORIZONTAL"] , - AcquisitionDimensionLabels["CHANNEL"] , - AcquisitionDimensionLabels["ANGLE"]], list(ss1.geometry.dimension_labels)) + self.assertListEqual([AcquisitionDimension["HORIZONTAL"] , + AcquisitionDimension["CHANNEL"] , + AcquisitionDimension["ANGLE"]], list(ss1.geometry.dimension_labels)) ss2 = sino.get_slice(vertical = 0, channel=0) - self.assertListEqual([AcquisitionDimensionLabels["HORIZONTAL"] , - AcquisitionDimensionLabels["ANGLE"]], list(ss2.geometry.dimension_labels)) + self.assertListEqual([AcquisitionDimension["HORIZONTAL"] , + AcquisitionDimension["ANGLE"]], list(ss2.geometry.dimension_labels)) def test_ImageDataSubset(self): @@ -859,42 +859,42 @@ def test_ImageDataSubset(self): self.assertListEqual(['channel', 'horizontal_y'], list(ss1.geometry.dimension_labels)) vg = ImageGeometry(3,4,5,channels=2) - self.assertListEqual([ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["VERTICAL"], - ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["HORIZONTAL_X"]], + self.assertListEqual([ImageDimension["CHANNEL"], ImageDimension["VERTICAL"], + ImageDimension["HORIZONTAL_Y"], ImageDimension["HORIZONTAL_X"]], list(vg.dimension_labels)) ss2 = vg.allocate() ss3 = vol.get_slice(channel=0) - self.assertListEqual([ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["HORIZONTAL_X"]], list(ss3.geometry.dimension_labels)) + self.assertListEqual([ImageDimension["HORIZONTAL_Y"], ImageDimension["HORIZONTAL_X"]], list(ss3.geometry.dimension_labels)) def test_DataContainerSubset(self): dc = DataContainer(numpy.ones((2,3,4,5))) - dc.dimension_labels =[AcquisitionDimensionLabels["CHANNEL"] , - AcquisitionDimensionLabels["ANGLE"] , AcquisitionDimensionLabels["VERTICAL"] , - AcquisitionDimensionLabels["HORIZONTAL"]] + dc.dimension_labels =[AcquisitionDimension["CHANNEL"] , + AcquisitionDimension["ANGLE"] , AcquisitionDimension["VERTICAL"] , + AcquisitionDimension["HORIZONTAL"]] # test reshape - new_order = [AcquisitionDimensionLabels["HORIZONTAL"] , - AcquisitionDimensionLabels["CHANNEL"] , AcquisitionDimensionLabels["VERTICAL"] , - AcquisitionDimensionLabels["ANGLE"]] + new_order = [AcquisitionDimension["HORIZONTAL"] , + AcquisitionDimension["CHANNEL"] , AcquisitionDimension["VERTICAL"] , + AcquisitionDimension["ANGLE"]] dc.reorder(new_order) self.assertListEqual(new_order, list(dc.dimension_labels)) ss1 = dc.get_slice(vertical=0) - self.assertListEqual([AcquisitionDimensionLabels["HORIZONTAL"] , - AcquisitionDimensionLabels["CHANNEL"] , - AcquisitionDimensionLabels["ANGLE"]], list(ss1.dimension_labels)) + self.assertListEqual([AcquisitionDimension["HORIZONTAL"] , + AcquisitionDimension["CHANNEL"] , + AcquisitionDimension["ANGLE"]], list(ss1.dimension_labels)) ss2 = dc.get_slice(vertical=0, channel=0) - self.assertListEqual([AcquisitionDimensionLabels["HORIZONTAL"] , - AcquisitionDimensionLabels["ANGLE"]], list(ss2.dimension_labels)) + self.assertListEqual([AcquisitionDimension["HORIZONTAL"] , + AcquisitionDimension["ANGLE"]], list(ss2.dimension_labels)) # Check we can get slice still even if force parameter is passed: ss3 = dc.get_slice(vertical=0, channel=0, force=True) - self.assertListEqual([AcquisitionDimensionLabels["HORIZONTAL"] , - AcquisitionDimensionLabels["ANGLE"]], list(ss3.dimension_labels)) + self.assertListEqual([AcquisitionDimension["HORIZONTAL"] , + AcquisitionDimension["ANGLE"]], list(ss3.dimension_labels)) def test_DataContainerChaining(self): diff --git a/Wrappers/Python/test/test_Operator.py b/Wrappers/Python/test/test_Operator.py index 0411a1eb2c..a90d41dd84 100644 --- a/Wrappers/Python/test/test_Operator.py +++ b/Wrappers/Python/test/test_Operator.py @@ -20,7 +20,7 @@ from unittest.mock import Mock from utils import initialise_tests from cil.framework import ImageGeometry, BlockGeometry, VectorGeometry, DataContainer -from cil.framework.labels import FillTypes +from cil.framework.labels import FillType from cil.optimisation.operators import FiniteDifferenceOperator, SymmetrisedGradientOperator import numpy from timeit import default_timer as timer @@ -249,13 +249,13 @@ def test_FiniteDifference(self): FD = FiniteDifferenceOperator(ig, direction = 0, bnd_cond = 'Neumann') u = FD.domain_geometry().allocate('random') - res = FD.domain_geometry().allocate(FillTypes["RANDOM"]) + res = FD.domain_geometry().allocate(FillType["RANDOM"]) FD.adjoint(u, out=res) w = FD.adjoint(u) self.assertNumpyArrayEqual(res.as_array(), w.as_array()) - res = Id.domain_geometry().allocate(FillTypes["RANDOM"]) + res = Id.domain_geometry().allocate(FillType["RANDOM"]) Id.adjoint(u, out=res) w = Id.adjoint(u) @@ -264,14 +264,14 @@ def test_FiniteDifference(self): G = GradientOperator(ig) - u = G.range_geometry().allocate(FillTypes["RANDOM"]) + u = G.range_geometry().allocate(FillType["RANDOM"]) 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(FillTypes["RANDOM"]) + u = G.domain_geometry().allocate(FillType["RANDOM"]) res = G.range_geometry().allocate() G.direct(u, out=res) w = G.direct(u) diff --git a/Wrappers/Python/test/test_PluginsTomoPhantom.py b/Wrappers/Python/test/test_PluginsTomoPhantom.py index 4357914abc..287634c2de 100644 --- a/Wrappers/Python/test/test_PluginsTomoPhantom.py +++ b/Wrappers/Python/test/test_PluginsTomoPhantom.py @@ -20,7 +20,7 @@ import numpy as np from cil.framework import AcquisitionGeometry -from cil.framework.labels import UnitsAngles +from cil.framework.labels import AngleUnit from utils import has_tomophantom, initialise_tests initialise_tests() @@ -38,7 +38,7 @@ def setUp(self): ag = AcquisitionGeometry.create_Cone2D((offset,-100), (offset,100)) ag.set_panel(N) - ag.set_angles(angles, angle_unit=UnitsAngles["DEGREE"]) + ag.set_angles(angles, angle_unit=AngleUnit["DEGREE"]) ig = ag.get_ImageGeometry() self.ag = ag self.ig = ig @@ -105,7 +105,7 @@ def setUp(self): ag = AcquisitionGeometry.create_Cone3D((offset,-100,0), (offset,100,0)) ag.set_panel((N,N/2)) - ag.set_angles(angles, angle_unit=UnitsAngles["DEGREE"]) + ag.set_angles(angles, angle_unit=AngleUnit["DEGREE"]) ig = ag.get_ImageGeometry() self.ag = ag self.ig = ig diff --git a/Wrappers/Python/test/test_algorithms.py b/Wrappers/Python/test/test_algorithms.py index 502fcb25be..ba7ed47798 100644 --- a/Wrappers/Python/test/test_algorithms.py +++ b/Wrappers/Python/test/test_algorithms.py @@ -26,7 +26,7 @@ from cil.framework import (ImageGeometry, ImageData, AcquisitionData, AcquisitionGeometry, BlockDataContainer, BlockGeometry, VectorData) -from cil.framework.labels import FillTypes +from cil.framework.labels import FillType from cil.optimisation.utilities import ArmijoStepSizeRule, ConstantStepSize from cil.optimisation.operators import IdentityOperator @@ -251,7 +251,7 @@ def test_FISTA(self): b = initial.copy() # fill with random numbers b.fill(np.random.random(initial.shape)) - initial = ig.allocate(FillTypes["RANDOM"]) + initial = ig.allocate(FillType["RANDOM"]) identity = IdentityOperator(ig) norm2sq = OperatorCompositionFunction(L2NormSquared(b=b), identity) @@ -354,9 +354,9 @@ def test_FISTA_update(self): def test_FISTA_Norm2Sq(self): ig = ImageGeometry(127, 139, 149) - b = ig.allocate(FillTypes["RANDOM"]) + b = ig.allocate(FillType["RANDOM"]) # fill with random numbers - initial = ig.allocate(FillTypes["RANDOM"]) + initial = ig.allocate(FillType["RANDOM"]) identity = IdentityOperator(ig) norm2sq = LeastSquares(identity, b) @@ -383,7 +383,7 @@ def test_FISTA_catch_Lipschitz(self): b = initial.copy() # fill with random numbers b.fill(np.random.random(initial.shape)) - initial = ig.allocate(FillTypes["RANDOM"]) + initial = ig.allocate(FillType["RANDOM"]) identity = IdentityOperator(ig) norm2sq = LeastSquares(identity, b) diff --git a/Wrappers/Python/test/test_functions.py b/Wrappers/Python/test/test_functions.py index 46eb442154..8fa9c1558a 100644 --- a/Wrappers/Python/test/test_functions.py +++ b/Wrappers/Python/test/test_functions.py @@ -23,7 +23,7 @@ from cil.framework import VectorGeometry, VectorData, BlockDataContainer, DataContainer, ImageGeometry, \ AcquisitionGeometry -from cil.framework.labels import FillTypes +from cil.framework.labels import FillType from cil.optimisation.operators import IdentityOperator, MatrixOperator, CompositionOperator, DiagonalOperator, BlockOperator from cil.optimisation.functions import Function, KullbackLeibler, ConstantFunction, TranslateFunction, soft_shrinkage from cil.optimisation.operators import GradientOperator @@ -80,9 +80,9 @@ def test_Function(self): operator = BlockOperator(op1, op2, shape=(2, 1)) # Create functions - noisy_data = ag.allocate(FillTypes["RANDOM"], dtype=numpy.float64) + noisy_data = ag.allocate(FillType["RANDOM"], dtype=numpy.float64) - d = ag.allocate(FillTypes["RANDOM"], dtype=numpy.float64) + d = ag.allocate(FillType["RANDOM"], dtype=numpy.float64) alpha = 0.5 # scaled function @@ -112,8 +112,8 @@ def test_L2NormSquared(self): numpy.random.seed(1) M, N, K = 2, 3, 5 ig = ImageGeometry(voxel_num_x=M, voxel_num_y=N, voxel_num_z=K) - u = ig.allocate(FillTypes["RANDOM"]) - b = ig.allocate(FillTypes["RANDOM"]) + u = ig.allocate(FillType["RANDOM"]) + b = ig.allocate(FillType["RANDOM"]) # check grad/call no data f = L2NormSquared() @@ -223,8 +223,8 @@ def test_L2NormSquaredOut(self): M, N, K = 2, 3, 5 ig = ImageGeometry(voxel_num_x=M, voxel_num_y=N, voxel_num_z=K) - u = ig.allocate(FillTypes["RANDOM"], seed=1) - b = ig.allocate(FillTypes["RANDOM"], seed=2) + u = ig.allocate(FillType["RANDOM"], seed=1) + b = ig.allocate(FillType["RANDOM"], seed=2) # check grad/call no data f = L2NormSquared() diff --git a/Wrappers/Python/test/test_io.py b/Wrappers/Python/test/test_io.py index d7fa75e3c1..bf82d72d7e 100644 --- a/Wrappers/Python/test/test_io.py +++ b/Wrappers/Python/test/test_io.py @@ -23,7 +23,7 @@ import numpy as np import os from cil.framework import ImageGeometry -from cil.framework.labels import UnitsAngles +from cil.framework.labels import AngleUnit from cil.io import NEXUSDataReader, NikonDataReader, ZEISSDataReader from cil.io import TIFFWriter, TIFFStackReader from cil.io.utilities import HDF5_utilities @@ -88,7 +88,7 @@ class TestZeissDataReader(unittest.TestCase): def setUp(self): if has_file: self.reader = ZEISSDataReader() - angle_unit = UnitsAngles["RADIAN"] + angle_unit = AngleUnit["RADIAN"] self.reader.set_up(file_name=filename, angle_unit=angle_unit) diff --git a/Wrappers/Python/test/test_labels.py b/Wrappers/Python/test/test_labels.py index 0e5e34677f..21703ebbe9 100644 --- a/Wrappers/Python/test/test_labels.py +++ b/Wrappers/Python/test/test_labels.py @@ -20,49 +20,49 @@ import numpy as np from cil.framework import AcquisitionGeometry, ImageGeometry -from cil.framework.labels import FillTypes, UnitsAngles, AcquisitionType, ImageDimensionLabels, AcquisitionDimensionLabels, Backends +from cil.framework.labels import FillType, AngleUnit, AcquisitionType, ImageDimension, AcquisitionDimension, Backend class Test_Lables(unittest.TestCase): def test_labels_strenum(self): - for item in (UnitsAngles.DEGREE, "DEGREE", "degree"): - out = UnitsAngles(item) - self.assertEqual(out, UnitsAngles.DEGREE) - self.assertTrue(isinstance(out, UnitsAngles)) - for item in ("bad_str", FillTypes.RANDOM): + for item in (AngleUnit.DEGREE, "DEGREE", "degree"): + out = AngleUnit(item) + self.assertEqual(out, AngleUnit.DEGREE) + self.assertTrue(isinstance(out, AngleUnit)) + for item in ("bad_str", FillType.RANDOM): with self.assertRaises(ValueError): - UnitsAngles(item) + AngleUnit(item) def test_labels_strenum_eq(self): - for i in (UnitsAngles.RADIAN, "RADIAN", "radian"): - self.assertEqual(UnitsAngles.RADIAN, i) - self.assertEqual(i, UnitsAngles.RADIAN) - for i in ("DEGREE", UnitsAngles.DEGREE, UnitsAngles): - self.assertNotEqual(UnitsAngles.RADIAN, i) + for i in (AngleUnit.RADIAN, "RADIAN", "radian"): + self.assertEqual(AngleUnit.RADIAN, i) + self.assertEqual(i, AngleUnit.RADIAN) + for i in ("DEGREE", AngleUnit.DEGREE, AngleUnit): + self.assertNotEqual(AngleUnit.RADIAN, i) def test_labels_contains(self): - for i in ("RADIAN", "degree", UnitsAngles.RADIAN, UnitsAngles.DEGREE): - self.assertIn(i, UnitsAngles) - for i in ("bad_str", UnitsAngles): - self.assertNotIn(i, UnitsAngles) + for i in ("RADIAN", "degree", AngleUnit.RADIAN, AngleUnit.DEGREE): + self.assertIn(i, AngleUnit) + for i in ("bad_str", AngleUnit): + self.assertNotIn(i, AngleUnit) def test_backends(self): for i in ('ASTRA', 'CIL', 'TIGRE'): - self.assertIn(i, Backends) - self.assertIn(i.lower(), Backends) - self.assertIn(getattr(Backends, i), Backends) + self.assertIn(i, Backend) + self.assertIn(i.lower(), Backend) + self.assertIn(getattr(Backend, i), Backend) def test_fill_types(self): for i in ('RANDOM', 'RANDOM_INT'): - self.assertIn(i, FillTypes) - self.assertIn(i.lower(), FillTypes) - self.assertIn(getattr(FillTypes, i), FillTypes) + self.assertIn(i, FillType) + self.assertIn(i.lower(), FillType) + self.assertIn(getattr(FillType, i), FillType) def test_units_angles(self): for i in ('DEGREE', 'RADIAN'): - self.assertIn(i, UnitsAngles) - self.assertIn(i.lower(), UnitsAngles) - self.assertIn(getattr(UnitsAngles, i), UnitsAngles) + self.assertIn(i, AngleUnit) + self.assertIn(i.lower(), AngleUnit) + self.assertIn(getattr(AngleUnit, i), AngleUnit) def test_acquisition_type(self): for i in ('PARALLEL', 'CONE', 'DIM2', 'DIM3'): @@ -72,27 +72,27 @@ def test_acquisition_type(self): def test_image_dimension_labels(self): for i in ('CHANNEL', 'VERTICAL', 'HORIZONTAL_X', 'HORIZONTAL_Y'): - self.assertIn(i, ImageDimensionLabels) - self.assertIn(i.lower(), ImageDimensionLabels) - self.assertIn(getattr(ImageDimensionLabels, i), ImageDimensionLabels) + self.assertIn(i, ImageDimension) + self.assertIn(i.lower(), ImageDimension) + self.assertIn(getattr(ImageDimension, i), ImageDimension) def test_image_dimension_labels_default_order(self): - order_gold = ImageDimensionLabels.CHANNEL, 'VERTICAL', 'horizontal_y', 'HORIZONTAL_X' + order_gold = ImageDimension.CHANNEL, 'VERTICAL', 'horizontal_y', 'HORIZONTAL_X' for i in ('CIL', 'TIGRE', 'ASTRA'): - self.assertSequenceEqual(ImageDimensionLabels.get_order_for_engine(i), order_gold) + self.assertSequenceEqual(ImageDimension.get_order_for_engine(i), order_gold) with self.assertRaises((KeyError, ValueError)): - AcquisitionDimensionLabels.get_order_for_engine("bad_engine") + AcquisitionDimension.get_order_for_engine("bad_engine") def test_image_dimension_labels_get_order(self): ig = ImageGeometry(4, 8, 1, channels=2) ig.set_labels(['channel', 'horizontal_y', 'horizontal_x']) # for 2D all engines have the same order - order_gold = ImageDimensionLabels.CHANNEL, 'HORIZONTAL_Y', 'horizontal_x' - self.assertSequenceEqual(ImageDimensionLabels.get_order_for_engine('cil', ig), order_gold) - self.assertSequenceEqual(ImageDimensionLabels.get_order_for_engine('tigre', ig), order_gold) - self.assertSequenceEqual(ImageDimensionLabels.get_order_for_engine('astra', ig), order_gold) + order_gold = ImageDimension.CHANNEL, 'HORIZONTAL_Y', 'horizontal_x' + self.assertSequenceEqual(ImageDimension.get_order_for_engine('cil', ig), order_gold) + self.assertSequenceEqual(ImageDimension.get_order_for_engine('tigre', ig), order_gold) + self.assertSequenceEqual(ImageDimension.get_order_for_engine('astra', ig), order_gold) def test_image_dimension_labels_check_order(self): ig = ImageGeometry(4, 8, 1, channels=2) @@ -100,28 +100,28 @@ def test_image_dimension_labels_check_order(self): for i in ('cil', 'tigre', 'astra'): with self.assertRaises(ValueError): - ImageDimensionLabels.check_order_for_engine(i, ig) + ImageDimension.check_order_for_engine(i, ig) ig.set_labels(['channel', 'horizontal_y', 'horizontal_x']) - self.assertTrue(ImageDimensionLabels.check_order_for_engine("cil", ig)) - self.assertTrue(ImageDimensionLabels.check_order_for_engine("tigre", ig)) - self.assertTrue(ImageDimensionLabels.check_order_for_engine("astra", ig)) + self.assertTrue(ImageDimension.check_order_for_engine("cil", ig)) + self.assertTrue(ImageDimension.check_order_for_engine("tigre", ig)) + self.assertTrue(ImageDimension.check_order_for_engine("astra", ig)) def test_acquisition_dimension_labels(self): for i in ('CHANNEL', 'ANGLE', 'VERTICAL', 'HORIZONTAL'): - self.assertIn(i, AcquisitionDimensionLabels) - self.assertIn(i.lower(), AcquisitionDimensionLabels) - self.assertIn(getattr(AcquisitionDimensionLabels, i), AcquisitionDimensionLabels) + self.assertIn(i, AcquisitionDimension) + self.assertIn(i.lower(), AcquisitionDimension) + self.assertIn(getattr(AcquisitionDimension, i), AcquisitionDimension) def test_acquisition_dimension_labels_default_order(self): - gold = AcquisitionDimensionLabels.CHANNEL, 'ANGLE', 'vertical', 'HORIZONTAL' - self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine('CIL'), gold) - self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine(Backends.TIGRE), gold) + gold = AcquisitionDimension.CHANNEL, 'ANGLE', 'vertical', 'HORIZONTAL' + self.assertSequenceEqual(AcquisitionDimension.get_order_for_engine('CIL'), gold) + self.assertSequenceEqual(AcquisitionDimension.get_order_for_engine(Backend.TIGRE), gold) gold = 'CHANNEL', 'VERTICAL', 'ANGLE', 'HORIZONTAL' - self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine('astra'), gold) + self.assertSequenceEqual(AcquisitionDimension.get_order_for_engine('astra'), gold) with self.assertRaises((KeyError, ValueError)): - AcquisitionDimensionLabels.get_order_for_engine("bad_engine") + AcquisitionDimension.get_order_for_engine("bad_engine") def test_acquisition_dimension_labels_get_order(self): ag = AcquisitionGeometry.create_Parallel2D()\ @@ -131,21 +131,21 @@ def test_acquisition_dimension_labels_get_order(self): .set_labels(['angle', 'horizontal', 'channel']) # for 2D all engines have the same order - order_gold = AcquisitionDimensionLabels.CHANNEL, 'ANGLE', 'horizontal' - self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine('CIL', ag), order_gold) - self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine('TIGRE', ag), order_gold) - self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine('ASTRA', ag), order_gold) + order_gold = AcquisitionDimension.CHANNEL, 'ANGLE', 'horizontal' + self.assertSequenceEqual(AcquisitionDimension.get_order_for_engine('CIL', ag), order_gold) + self.assertSequenceEqual(AcquisitionDimension.get_order_for_engine('TIGRE', ag), order_gold) + self.assertSequenceEqual(AcquisitionDimension.get_order_for_engine('ASTRA', ag), order_gold) ag = AcquisitionGeometry.create_Parallel3D()\ .set_angles(np.arange(0,16 , 1), angle_unit="degree")\ .set_panel((4,2))\ .set_labels(['angle', 'horizontal', 'vertical']) - order_gold = AcquisitionDimensionLabels.ANGLE, 'VERTICAL', 'horizontal' - self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine("cil", ag), order_gold) - self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine("tigre", ag), order_gold) - order_gold = AcquisitionDimensionLabels.VERTICAL, 'ANGLE', 'horizontal' - self.assertSequenceEqual(AcquisitionDimensionLabels.get_order_for_engine("astra", ag), order_gold) + order_gold = AcquisitionDimension.ANGLE, 'VERTICAL', 'horizontal' + self.assertSequenceEqual(AcquisitionDimension.get_order_for_engine("cil", ag), order_gold) + self.assertSequenceEqual(AcquisitionDimension.get_order_for_engine("tigre", ag), order_gold) + order_gold = AcquisitionDimension.VERTICAL, 'ANGLE', 'horizontal' + self.assertSequenceEqual(AcquisitionDimension.get_order_for_engine("astra", ag), order_gold) def test_acquisition_dimension_labels_check_order(self): ag = AcquisitionGeometry.create_Parallel3D()\ @@ -156,11 +156,11 @@ def test_acquisition_dimension_labels_check_order(self): for i in ('cil', 'tigre', 'astra'): with self.assertRaises(ValueError): - AcquisitionDimensionLabels.check_order_for_engine(i, ag) + AcquisitionDimension.check_order_for_engine(i, ag) ag.set_labels(['channel', 'angle', 'vertical', 'horizontal']) - self.assertTrue(AcquisitionDimensionLabels.check_order_for_engine("cil", ag)) - self.assertTrue(AcquisitionDimensionLabels.check_order_for_engine("tigre", ag)) + self.assertTrue(AcquisitionDimension.check_order_for_engine("cil", ag)) + self.assertTrue(AcquisitionDimension.check_order_for_engine("tigre", ag)) ag.set_labels(['channel', 'vertical', 'angle', 'horizontal']) - self.assertTrue(AcquisitionDimensionLabels.check_order_for_engine("astra", ag)) + self.assertTrue(AcquisitionDimension.check_order_for_engine("astra", ag)) diff --git a/Wrappers/Python/test/test_ring_processor.py b/Wrappers/Python/test/test_ring_processor.py index 5e1dea1ebf..6330780a5b 100644 --- a/Wrappers/Python/test/test_ring_processor.py +++ b/Wrappers/Python/test/test_ring_processor.py @@ -19,7 +19,7 @@ import unittest from cil.processors import RingRemover, TransmissionAbsorptionConverter, Slicer from cil.framework import ImageGeometry, AcquisitionGeometry -from cil.framework.labels import UnitsAngles +from cil.framework.labels import AngleUnit from cil.utilities import dataexample from cil.utilities.quality_measures import mse @@ -62,7 +62,7 @@ def test_L1Norm_2D(self): angles = np.linspace(0, 180, 120, dtype=np.float32) ag = AcquisitionGeometry.create_Parallel2D()\ - .set_angles(angles, angle_unit=UnitsAngles["DEGREE"])\ + .set_angles(angles, angle_unit=AngleUnit["DEGREE"])\ .set_panel(detectors) sin = ag.allocate(None) sino = TomoP2D.ModelSino(model, detectors, detectors, angles, path_library2D) diff --git a/Wrappers/Python/test/test_subset.py b/Wrappers/Python/test/test_subset.py index 9a084affb5..87dbba2adc 100644 --- a/Wrappers/Python/test/test_subset.py +++ b/Wrappers/Python/test/test_subset.py @@ -20,7 +20,7 @@ from utils import initialise_tests import numpy from cil.framework import DataContainer, ImageGeometry, AcquisitionGeometry -from cil.framework.labels import ImageDimensionLabels, AcquisitionDimensionLabels +from cil.framework.labels import ImageDimension, AcquisitionDimension from timeit import default_timer as timer initialise_tests() @@ -209,8 +209,8 @@ def setUp(self): def test_ImageDataAllocate1a(self): data = self.ig.allocate() - default_dimension_labels = [ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["VERTICAL"], - ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["HORIZONTAL_X"]] + default_dimension_labels = [ImageDimension["CHANNEL"], ImageDimension["VERTICAL"], + ImageDimension["HORIZONTAL_Y"], ImageDimension["HORIZONTAL_X"]] self.assertTrue( default_dimension_labels == list(data.dimension_labels) ) def test_ImageDataAllocate1b(self): @@ -218,64 +218,64 @@ def test_ImageDataAllocate1b(self): self.assertTrue( data.shape == (5,4,3,2)) def test_ImageDataAllocate2a(self): - non_default_dimension_labels = [ ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["VERTICAL"], - ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["CHANNEL"]] + non_default_dimension_labels = [ ImageDimension["HORIZONTAL_X"], ImageDimension["VERTICAL"], + ImageDimension["HORIZONTAL_Y"], ImageDimension["CHANNEL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() self.assertTrue( non_default_dimension_labels == list(data.dimension_labels) ) def test_ImageDataAllocate2b(self): - non_default_dimension_labels = [ ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["VERTICAL"], - ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["CHANNEL"]] + non_default_dimension_labels = [ ImageDimension["HORIZONTAL_X"], ImageDimension["VERTICAL"], + ImageDimension["HORIZONTAL_Y"], ImageDimension["CHANNEL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() self.assertTrue( data.shape == (2,4,3,5)) def test_ImageDataSubset1a(self): - non_default_dimension_labels = [ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["HORIZONTAL_Y"], - ImageDimensionLabels["VERTICAL"]] + non_default_dimension_labels = [ImageDimension["HORIZONTAL_X"], ImageDimension["CHANNEL"], ImageDimension["HORIZONTAL_Y"], + ImageDimension["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(horizontal_y = 1) self.assertTrue( sub.shape == (2,5,4)) def test_ImageDataSubset2a(self): - non_default_dimension_labels = [ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["HORIZONTAL_Y"], - ImageDimensionLabels["VERTICAL"]] + non_default_dimension_labels = [ImageDimension["HORIZONTAL_X"], ImageDimension["CHANNEL"], ImageDimension["HORIZONTAL_Y"], + ImageDimension["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(horizontal_x = 1) self.assertTrue( sub.shape == (5,3,4)) def test_ImageDataSubset3a(self): - non_default_dimension_labels = [ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["HORIZONTAL_Y"], - ImageDimensionLabels["VERTICAL"]] + non_default_dimension_labels = [ImageDimension["HORIZONTAL_X"], ImageDimension["CHANNEL"], ImageDimension["HORIZONTAL_Y"], + ImageDimension["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(channel = 1) self.assertTrue( sub.shape == (2,3,4)) def test_ImageDataSubset4a(self): - non_default_dimension_labels = [ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["HORIZONTAL_Y"], - ImageDimensionLabels["VERTICAL"]] + non_default_dimension_labels = [ImageDimension["HORIZONTAL_X"], ImageDimension["CHANNEL"], ImageDimension["HORIZONTAL_Y"], + ImageDimension["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(vertical = 1) self.assertTrue( sub.shape == (2,5,3)) def test_ImageDataSubset5a(self): - non_default_dimension_labels = [ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["HORIZONTAL_Y"]] + non_default_dimension_labels = [ImageDimension["HORIZONTAL_X"], ImageDimension["HORIZONTAL_Y"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() sub = data.get_slice(horizontal_y = 1) self.assertTrue( sub.shape == (2,)) def test_ImageDataSubset1b(self): - non_default_dimension_labels = [ImageDimensionLabels["HORIZONTAL_X"], ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["HORIZONTAL_Y"], - ImageDimensionLabels["VERTICAL"]] + non_default_dimension_labels = [ImageDimension["HORIZONTAL_X"], ImageDimension["CHANNEL"], ImageDimension["HORIZONTAL_Y"], + ImageDimension["VERTICAL"]] self.ig.set_labels(non_default_dimension_labels) data = self.ig.allocate() - new_dimension_labels = [ImageDimensionLabels["HORIZONTAL_Y"], ImageDimensionLabels["CHANNEL"], ImageDimensionLabels["VERTICAL"], ImageDimensionLabels["HORIZONTAL_X"]] + new_dimension_labels = [ImageDimension["HORIZONTAL_Y"], ImageDimension["CHANNEL"], ImageDimension["VERTICAL"], ImageDimension["HORIZONTAL_X"]] data.reorder(new_dimension_labels) self.assertTrue( data.shape == (3,5,4,2)) @@ -287,9 +287,9 @@ def test_ImageDataSubset1c(self): def test_AcquisitionDataAllocate1a(self): data = self.ag.allocate() - default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"] , - AcquisitionDimensionLabels["ANGLE"] , AcquisitionDimensionLabels["VERTICAL"] , - AcquisitionDimensionLabels["HORIZONTAL"]] + default_dimension_labels = [AcquisitionDimension["CHANNEL"] , + AcquisitionDimension["ANGLE"] , AcquisitionDimension["VERTICAL"] , + AcquisitionDimension["HORIZONTAL"]] self.assertTrue( default_dimension_labels == list(data.dimension_labels) ) def test_AcquisitionDataAllocate1b(self): @@ -297,8 +297,8 @@ def test_AcquisitionDataAllocate1b(self): self.assertTrue( data.shape == (4,3,2,20)) def test_AcquisitionDataAllocate2a(self): - non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], - AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimension["CHANNEL"], AcquisitionDimension["HORIZONTAL"], + AcquisitionDimension["VERTICAL"], AcquisitionDimension["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() @@ -306,15 +306,15 @@ def test_AcquisitionDataAllocate2a(self): self.assertTrue( non_default_dimension_labels == list(data.dimension_labels) ) def test_AcquisitionDataAllocate2b(self): - non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], - AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimension["CHANNEL"], AcquisitionDimension["HORIZONTAL"], + AcquisitionDimension["VERTICAL"], AcquisitionDimension["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() self.assertTrue( data.shape == (4,20,2,3)) def test_AcquisitionDataSubset1a(self): - non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], - AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimension["CHANNEL"], AcquisitionDimension["HORIZONTAL"], + AcquisitionDimension["VERTICAL"], AcquisitionDimension["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) @@ -322,24 +322,24 @@ def test_AcquisitionDataSubset1a(self): self.assertTrue( sub.shape == (4,20,3)) def test_AcquisitionDataSubset1b(self): - non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], - AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimension["CHANNEL"], AcquisitionDimension["HORIZONTAL"], + AcquisitionDimension["VERTICAL"], AcquisitionDimension["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) sub = data.get_slice(channel = 0) self.assertTrue( sub.shape == (20,2,3)) def test_AcquisitionDataSubset1c(self): - non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], - AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimension["CHANNEL"], AcquisitionDimension["HORIZONTAL"], + AcquisitionDimension["VERTICAL"], AcquisitionDimension["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) sub = data.get_slice(horizontal = 0, force=True) self.assertTrue( sub.shape == (4,2,3)) def test_AcquisitionDataSubset1d(self): - non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], - AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimension["CHANNEL"], AcquisitionDimension["HORIZONTAL"], + AcquisitionDimension["VERTICAL"], AcquisitionDimension["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) @@ -349,8 +349,8 @@ def test_AcquisitionDataSubset1d(self): self.assertTrue( sub.shape == (4,20,2) ) self.assertTrue( sub.geometry.angles[0] == data.geometry.angles[sliceme]) def test_AcquisitionDataSubset1e(self): - non_default_dimension_labels = [AcquisitionDimensionLabels["CHANNEL"], AcquisitionDimensionLabels["HORIZONTAL"], - AcquisitionDimensionLabels["VERTICAL"], AcquisitionDimensionLabels["ANGLE"]] + non_default_dimension_labels = [AcquisitionDimension["CHANNEL"], AcquisitionDimension["HORIZONTAL"], + AcquisitionDimension["VERTICAL"], AcquisitionDimension["ANGLE"]] self.ag.set_labels(non_default_dimension_labels) data = self.ag.allocate() #self.assertTrue( data.shape == (4,20,2,3)) diff --git a/Wrappers/Python/test/utils_projectors.py b/Wrappers/Python/test/utils_projectors.py index 9ba0670d46..795951edb7 100644 --- a/Wrappers/Python/test/utils_projectors.py +++ b/Wrappers/Python/test/utils_projectors.py @@ -20,7 +20,7 @@ from cil.optimisation.operators import LinearOperator from cil.utilities import dataexample from cil.framework import AcquisitionGeometry -from cil.framework.labels import AcquisitionDimensionLabels, AcquisitionType +from cil.framework.labels import AcquisitionDimension, AcquisitionType class SimData(object): @@ -148,7 +148,7 @@ def Cone3D(self): ag_test_1 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,0,0])\ .set_panel([16,16],[1,1])\ .set_angles([0]) - ag_test_1.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() @@ -159,7 +159,7 @@ def Cone3D(self): ag_test_2 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,0,0])\ .set_panel([16,16],[2,2])\ .set_angles([0]) - ag_test_2.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() norm_2 = 8 @@ -169,7 +169,7 @@ def Cone3D(self): ag_test_3 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,0,0])\ .set_panel([16,16],[0.5,0.5])\ .set_angles([0]) - ag_test_3.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() norm_3 = 2 @@ -179,7 +179,7 @@ def Cone3D(self): ag_test_4 = AcquisitionGeometry.create_Cone3D(source_position=[0,-1000,0],detector_position=[0,1000,0])\ .set_panel([16,16],[0.5,0.5])\ .set_angles([0]) - ag_test_4.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_4)) + ag_test_4.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_4)) ig_test_4 = ag_test_4.get_ImageGeometry() norm_4 = 1 @@ -195,7 +195,7 @@ def Cone2D(self): ag_test_1 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,0])\ .set_panel(16,1)\ .set_angles([0]) - ag_test_1.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() @@ -206,7 +206,7 @@ def Cone2D(self): ag_test_2 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,0])\ .set_panel(16,2)\ .set_angles([0]) - ag_test_2.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() norm_2 = 8 @@ -216,7 +216,7 @@ def Cone2D(self): ag_test_3 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,0])\ .set_panel(16,0.5)\ .set_angles([0]) - ag_test_3.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() norm_3 = 2 @@ -226,7 +226,7 @@ def Cone2D(self): ag_test_4 = AcquisitionGeometry.create_Cone2D(source_position=[0,-1000],detector_position=[0,1000])\ .set_panel(16,0.5)\ .set_angles([0]) - ag_test_4.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_4)) + ag_test_4.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_4)) ig_test_4 = ag_test_4.get_ImageGeometry() norm_4 = 1 @@ -242,7 +242,7 @@ def Parallel3D(self): ag_test_1 = AcquisitionGeometry.create_Parallel3D()\ .set_panel([16,16],[1,1])\ .set_angles([0]) - ag_test_1.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() @@ -252,7 +252,7 @@ def Parallel3D(self): ag_test_2 = AcquisitionGeometry.create_Parallel3D()\ .set_panel([16,16],[2,2])\ .set_angles([0]) - ag_test_2.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() @@ -263,7 +263,7 @@ def Parallel3D(self): ag_test_3 = AcquisitionGeometry.create_Parallel3D()\ .set_panel([16,16],[0.5,0.5])\ .set_angles([0]) - ag_test_3.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() @@ -281,7 +281,7 @@ def Parallel2D(self): .set_panel(16,1)\ .set_angles([0]) - ag_test_1.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_1)) + ag_test_1.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_1)) ig_test_1 = ag_test_1.get_ImageGeometry() norm_1 = 4 @@ -290,7 +290,7 @@ def Parallel2D(self): ag_test_2 = AcquisitionGeometry.create_Parallel2D()\ .set_panel(16,2)\ .set_angles([0]) - ag_test_2.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_2)) + ag_test_2.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_2)) ig_test_2 = ag_test_2.get_ImageGeometry() @@ -301,7 +301,7 @@ def Parallel2D(self): ag_test_3 = AcquisitionGeometry.create_Parallel2D()\ .set_panel(16,0.5)\ .set_angles([0]) - ag_test_3.set_labels(AcquisitionDimensionLabels.get_order_for_engine(self.backend, ag_test_3)) + ag_test_3.set_labels(AcquisitionDimension.get_order_for_engine(self.backend, ag_test_3)) ig_test_3 = ag_test_3.get_ImageGeometry() norm_3 = 2 From 96e3429194e247b6cede82f1dd7322f12cfd89ad Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Wed, 21 Aug 2024 13:40:29 +0100 Subject: [PATCH 68/72] update docstrings --- .../Python/cil/framework/acquisition_geometry.py | 12 ++++++------ Wrappers/Python/cil/framework/block.py | 4 ++-- Wrappers/Python/cil/framework/image_geometry.py | 12 ++++++------ Wrappers/Python/cil/framework/labels.py | 6 +++--- Wrappers/Python/cil/framework/vector_geometry.py | 4 ++-- Wrappers/Python/cil/plugins/TomoPhantom.py | 2 +- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Wrappers/Python/cil/framework/acquisition_geometry.py b/Wrappers/Python/cil/framework/acquisition_geometry.py index 09bb759587..d4e805f0c3 100644 --- a/Wrappers/Python/cil/framework/acquisition_geometry.py +++ b/Wrappers/Python/cil/framework/acquisition_geometry.py @@ -1611,32 +1611,32 @@ class AcquisitionGeometry(object): #for backwards compatibility @property def ANGLE(self): - warnings.warn("use AcquisitionDimensionLabels.Angle instead", DeprecationWarning, stacklevel=2) + warnings.warn("use AcquisitionDimension.Angle instead", DeprecationWarning, stacklevel=2) return AcquisitionDimension.ANGLE @property def CHANNEL(self): - warnings.warn("use AcquisitionDimensionLabels.Channel instead", DeprecationWarning, stacklevel=2) + warnings.warn("use AcquisitionDimension.Channel instead", DeprecationWarning, stacklevel=2) return AcquisitionDimension.CHANNEL @property def DEGREE(self): - warnings.warn("use UnitsAngles.DEGREE", DeprecationWarning, stacklevel=2) + warnings.warn("use AngleUnit.DEGREE", DeprecationWarning, stacklevel=2) return AngleUnit.DEGREE @property def HORIZONTAL(self): - warnings.warn("use AcquisitionDimensionLabels.HORIZONTAL instead", DeprecationWarning, stacklevel=2) + warnings.warn("use AcquisitionDimension.HORIZONTAL instead", DeprecationWarning, stacklevel=2) return AcquisitionDimension.HORIZONTAL @property def RADIAN(self): - warnings.warn("use UnitsAngles.RADIAN instead", DeprecationWarning, stacklevel=2) + warnings.warn("use AngleUnit.RADIAN instead", DeprecationWarning, stacklevel=2) return AngleUnit.RADIAN @property def VERTICAL(self): - warnings.warn("use AcquisitionDimensionLabels.VERTICAL instead", DeprecationWarning, stacklevel=2) + warnings.warn("use AcquisitionDimension.VERTICAL instead", DeprecationWarning, stacklevel=2) return AcquisitionDimension.VERTICAL @property diff --git a/Wrappers/Python/cil/framework/block.py b/Wrappers/Python/cil/framework/block.py index 72af8bb7de..fbe2c9c3cf 100644 --- a/Wrappers/Python/cil/framework/block.py +++ b/Wrappers/Python/cil/framework/block.py @@ -28,12 +28,12 @@ class BlockGeometry(object): @property def RANDOM(self): - warnings.warn("use FillTypes.RANDOM instead", DeprecationWarning, stacklevel=2) + warnings.warn("use FillType.RANDOM instead", DeprecationWarning, stacklevel=2) return FillType.RANDOM @property def RANDOM_INT(self): - warnings.warn("use FillTypes.RANDOM_INT instead", DeprecationWarning, stacklevel=2) + warnings.warn("use FillType.RANDOM_INT instead", DeprecationWarning, stacklevel=2) return FillType.RANDOM_INT @property diff --git a/Wrappers/Python/cil/framework/image_geometry.py b/Wrappers/Python/cil/framework/image_geometry.py index f2d2745871..b0a190cbe0 100644 --- a/Wrappers/Python/cil/framework/image_geometry.py +++ b/Wrappers/Python/cil/framework/image_geometry.py @@ -28,31 +28,31 @@ class ImageGeometry: @property def CHANNEL(self): - warnings.warn("use ImageDimensionLabels.CHANNEL instead", DeprecationWarning, stacklevel=2) + warnings.warn("use ImageDimension.CHANNEL instead", DeprecationWarning, stacklevel=2) return ImageDimension.CHANNEL @property def HORIZONTAL_X(self): - warnings.warn("use ImageDimensionLabels.HORIZONTAL_X instead", DeprecationWarning, stacklevel=2) + warnings.warn("use ImageDimension.HORIZONTAL_X instead", DeprecationWarning, stacklevel=2) return ImageDimension.HORIZONTAL_X @property def HORIZONTAL_Y(self): - warnings.warn("use ImageDimensionLabels.HORIZONTAL_Y instead", DeprecationWarning, stacklevel=2) + warnings.warn("use ImageDimension.HORIZONTAL_Y instead", DeprecationWarning, stacklevel=2) return ImageDimension.HORIZONTAL_Y @property def RANDOM(self): - warnings.warn("use FillTypes.RANDOM instead", DeprecationWarning, stacklevel=2) + warnings.warn("use FillType.RANDOM instead", DeprecationWarning, stacklevel=2) return FillType.RANDOM @property def RANDOM_INT(self): - warnings.warn("use FillTypes.RANDOM_INT instead", DeprecationWarning, stacklevel=2) + warnings.warn("use FillType.RANDOM_INT instead", DeprecationWarning, stacklevel=2) return FillType.RANDOM_INT @property def VERTICAL(self): - warnings.warn("use ImageDimensionLabels.VERTICAL instead", DeprecationWarning, stacklevel=2) + warnings.warn("use ImageDimension.VERTICAL instead", DeprecationWarning, stacklevel=2) return ImageDimension.VERTICAL @property diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index a6aa4adf2f..642da62d1d 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -66,7 +66,7 @@ class Backend(StrEnum): Examples -------- ``` - FBP(data, backend=Backends.ASTRA) + FBP(data, backend=Backend.ASTRA) FBP(data, backend="astra") ``` """ @@ -183,7 +183,7 @@ class FillType(StrEnum): Examples -------- ``` - data.fill(FillTypes.RANDOM) + data.fill(FillType.RANDOM) data.fill("random") ``` """ @@ -198,7 +198,7 @@ class AngleUnit(StrEnum): Examples -------- ``` - data.geometry.set_angles(angle_data, angle_units=UnitsAngles.DEGREE) + data.geometry.set_angles(angle_data, angle_units=AngleUnit.DEGREE) data.geometry.set_angles(angle_data, angle_units="degree") ``` """ diff --git a/Wrappers/Python/cil/framework/vector_geometry.py b/Wrappers/Python/cil/framework/vector_geometry.py index 4f0d62b769..d51204539a 100644 --- a/Wrappers/Python/cil/framework/vector_geometry.py +++ b/Wrappers/Python/cil/framework/vector_geometry.py @@ -28,12 +28,12 @@ class VectorGeometry: '''Geometry describing VectorData to contain 1D array''' @property def RANDOM(self): - warnings.warn("use FillTypes.RANDOM instead", DeprecationWarning, stacklevel=2) + warnings.warn("use FillType.RANDOM instead", DeprecationWarning, stacklevel=2) return FillType.RANDOM @property def RANDOM_INT(self): - warnings.warn("use FillTypes.RANDOM_INT instead", DeprecationWarning, stacklevel=2) + warnings.warn("use FillType.RANDOM_INT instead", DeprecationWarning, stacklevel=2) return FillType.RANDOM_INT @property diff --git a/Wrappers/Python/cil/plugins/TomoPhantom.py b/Wrappers/Python/cil/plugins/TomoPhantom.py index ce6e6ac14b..b55376a56f 100644 --- a/Wrappers/Python/cil/plugins/TomoPhantom.py +++ b/Wrappers/Python/cil/plugins/TomoPhantom.py @@ -139,7 +139,7 @@ def get_ImageData(num_model, geometry): ag.set_panel((N,N-2)) ag.set_channels(channels) - ag.set_angles(angles, angle_unit=UnitsAngles.DEGREE) + ag.set_angles(angles, angle_unit=AngleUnit.DEGREE) ig = ag.get_ImageGeometry() num_model = 1 From 2a5c0f7de6687f36b56e1a252a34fe3c9a98a338 Mon Sep 17 00:00:00 2001 From: hrobarts Date: Wed, 21 Aug 2024 14:48:49 +0000 Subject: [PATCH 69/72] Update labels docs --- Wrappers/Python/cil/framework/labels.py | 54 +++++++++++++++++-------- docs/source/framework.rst | 20 +++++++-- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index 642da62d1d..2a263cb7c5 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -121,12 +121,18 @@ class ImageDimension(_DimensionBase, StrEnum): """ Available dimension labels for image data. + Attributes + ---------- + CHANNEL + VERTICAL + HORIZONTAL_X + HORIZONTAL_Y + Examples -------- - ``` - data.reorder([ImageDimension.HORIZONTAL_X, ImageDimension.VERTICAL]) - data.reorder(["horizontal_x", "vertical"]) - ``` + >>> data.reorder([ImageDimension.HORIZONTAL_X, ImageDimension.VERTICAL]) + >>> data.reorder(["horizontal_x", "vertical"]) + """ CHANNEL = auto() VERTICAL = auto() @@ -147,14 +153,19 @@ class AcquisitionDimension(_DimensionBase, StrEnum): """ Available dimension labels for acquisition data. + Attributes + ---------- + CHANNEL + ANGLE + VERTICAL + HORIZONTAL + Examples -------- - ``` - data.reorder([AcquisitionDimension.CHANNEL, + >>> data.reorder([AcquisitionDimension.CHANNEL, AcquisitionDimension.ANGLE, AcquisitionDimension.HORIZONTAL]) - data.reorder(["channel", "angle", "horizontal"]) - ``` + >>> data.reorder(["channel", "angle", "horizontal"]) """ CHANNEL = auto() ANGLE = auto() @@ -182,10 +193,8 @@ class FillType(StrEnum): Examples -------- - ``` - data.fill(FillType.RANDOM) - data.fill("random") - ``` + >>> data.fill(FillType.RANDOM) + >>> data.fill("random") """ RANDOM = auto() RANDOM_INT = auto() @@ -194,13 +203,17 @@ class FillType(StrEnum): class AngleUnit(StrEnum): """ Available units for angles. + + Attributes + ---------- + DEGREE + RADIAN Examples -------- - ``` - data.geometry.set_angles(angle_data, angle_units=AngleUnit.DEGREE) - data.geometry.set_angles(angle_data, angle_units="degree") - ``` + >>> data.geometry.set_angles(angle_data, angle_units=AngleUnit.DEGREE) + >>> data.geometry.set_angles(angle_data, angle_units="degree") + """ DEGREE = auto() RADIAN = auto() @@ -240,16 +253,25 @@ class AcquisitionType(Flag): DIM3 = auto() def validate(self): + """ + Check if the geometry and dimension types are allowed + """ assert len(self.dimension) < 2, f"{self} must be 2D xor 3D" assert len(self.geometry) < 2, f"{self} must be parallel xor cone beam" return self @property def dimension(self): + """ + Returns the label for the dimension type + """ return self & (self.DIM2 | self.DIM3) @property def geometry(self): + """ + Returns the label for the geometry type + """ return self & (self.PARALLEL | self.CONE) @classmethod diff --git a/docs/source/framework.rst b/docs/source/framework.rst index 27f2a45e31..c0924f1321 100644 --- a/docs/source/framework.rst +++ b/docs/source/framework.rst @@ -232,11 +232,25 @@ The :code:`partition` method is defined as part of: :members: -DataOrder +Labels ========= -.. autoclass:: cil.framework.DataOrder +Classes which define the accepted labels + +.. autoclass:: cil.framework.labels.ImageDimension :members: - :inherited-members: + +.. autoclass:: cil.framework.labels.AcquisitionDimension + :members: + +.. autoclass:: cil.framework.labels.FillType + :members: + +.. autoclass:: cil.framework.labels.AngleUnit + :members: + +.. autoclass:: cil.framework.labels.AcquisitionType + :members: + DataProcessor ============= From edb7953512365dd61234a4c1900240f35da6e13b Mon Sep 17 00:00:00 2001 From: hrobarts Date: Wed, 21 Aug 2024 14:57:51 +0000 Subject: [PATCH 70/72] Update changelog, add contributors to notice --- CHANGELOG.md | 3 ++- NOTICE.txt | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e93db985ff..500c3e504e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,12 @@ - Added SVRG and LSVRG stochastic functions (#1625) - Added SAG and SAGA stochastic functions (#1624) - Allow `SumFunction` with 1 item (#1857) - - Added `labels` module with `ImageDimensionLabels`, `AcquisitionDimensionLabels`,`AcquisitionDimensions`, `AcquisitionTypes`, `UnitsAngles`, `FillTypes` (#1692) + - Added `labels` module with `ImageDimension`, `AcquisitionDimension`, `AcquisitionType`, `AngleUnit`, `FillType` (#1692) - Enhancements: - Use ravel instead of flat in KullbackLeibler numba backend (#1874) - Upgrade Python wrapper (#1873, #1875) - Internal refactor: Replaced string-based label checks with enum-based checks for improved type safety and consistency (#1692) + - Internal refactor: Separate framework into multiple files (#1692) - Bug fixes: - `ImageData` removes dimensions of size 1 from the input array. This fixes an issue where single slice reconstructions from 3D data would fail due to shape mismatches (#1885) - Make Binner accept accelerated=False (#1887) diff --git a/NOTICE.txt b/NOTICE.txt index 2de157bb77..e5887d3b80 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -65,6 +65,8 @@ Ashley Gillman (2024) -12 Zeljko Kereta (2024) - 5 Evgueni Ovtchinnikov (2024) -1 Georg Schramm (2024) - 13 +Joshua Hellier (2024) - 3 +Nicholas Whyatt (2024) - 1 CIL Advisory Board: Llion Evans - 9 From 56f897977a787e39dcfa058d0516aa5f928dbea2 Mon Sep 17 00:00:00 2001 From: hrobarts Date: Thu, 22 Aug 2024 07:55:35 +0000 Subject: [PATCH 71/72] Update docs --- Wrappers/Python/cil/framework/labels.py | 37 ++++++++----------------- docs/source/framework.rst | 3 ++ 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index 2a263cb7c5..ab68c3a84f 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -121,13 +121,6 @@ class ImageDimension(_DimensionBase, StrEnum): """ Available dimension labels for image data. - Attributes - ---------- - CHANNEL - VERTICAL - HORIZONTAL_X - HORIZONTAL_Y - Examples -------- >>> data.reorder([ImageDimension.HORIZONTAL_X, ImageDimension.VERTICAL]) @@ -153,13 +146,6 @@ class AcquisitionDimension(_DimensionBase, StrEnum): """ Available dimension labels for acquisition data. - Attributes - ---------- - CHANNEL - ANGLE - VERTICAL - HORIZONTAL - Examples -------- >>> data.reorder([AcquisitionDimension.CHANNEL, @@ -188,8 +174,10 @@ class FillType(StrEnum): Attributes ---------- - RANDOM: Fill with random values. - RANDOM_INT: Fill with random integers. + RANDOM: + Fill with random values. + RANDOM_INT: + Fill with random integers. Examples -------- @@ -203,11 +191,6 @@ class FillType(StrEnum): class AngleUnit(StrEnum): """ Available units for angles. - - Attributes - ---------- - DEGREE - RADIAN Examples -------- @@ -242,10 +225,14 @@ class AcquisitionType(Flag): Attributes ---------- - PARALLEL: Parallel beam. - CONE: Cone beam. - DIM2: 2D acquisition. - DIM3: 3D acquisition. + PARALLEL: + Parallel beam. + CONE: + Cone beam. + DIM2: + 2D acquisition. + DIM3: + 3D acquisition. """ PARALLEL = auto() CONE = auto() diff --git a/docs/source/framework.rst b/docs/source/framework.rst index c0924f1321..5d20634b9e 100644 --- a/docs/source/framework.rst +++ b/docs/source/framework.rst @@ -238,15 +238,18 @@ Classes which define the accepted labels .. autoclass:: cil.framework.labels.ImageDimension :members: + :undoc-members: .. autoclass:: cil.framework.labels.AcquisitionDimension :members: + :undoc-members: .. autoclass:: cil.framework.labels.FillType :members: .. autoclass:: cil.framework.labels.AngleUnit :members: + :undoc-members: .. autoclass:: cil.framework.labels.AcquisitionType :members: From 23e59b6a85faba6b3a8ece01bea5191326b7a5d9 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Thu, 22 Aug 2024 12:31:36 +0100 Subject: [PATCH 72/72] fix AcquisitionType.dimension comparison to '2D'/'3D' --- Wrappers/Python/cil/framework/labels.py | 19 +++++++++++-------- Wrappers/Python/test/test_labels.py | 9 +++++++++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Wrappers/Python/cil/framework/labels.py b/Wrappers/Python/cil/framework/labels.py index ab68c3a84f..c1bb23dfb1 100644 --- a/Wrappers/Python/cil/framework/labels.py +++ b/Wrappers/Python/cil/framework/labels.py @@ -125,7 +125,7 @@ class ImageDimension(_DimensionBase, StrEnum): -------- >>> data.reorder([ImageDimension.HORIZONTAL_X, ImageDimension.VERTICAL]) >>> data.reorder(["horizontal_x", "vertical"]) - + """ CHANNEL = auto() VERTICAL = auto() @@ -174,9 +174,9 @@ class FillType(StrEnum): Attributes ---------- - RANDOM: + RANDOM: Fill with random values. - RANDOM_INT: + RANDOM_INT: Fill with random integers. Examples @@ -216,22 +216,25 @@ def _missing_(cls, value): return cls.__members__.get(value.upper(), None) if isinstance(value, str) else super()._missing_(value) def __eq__(self, value: str) -> bool: - return super().__eq__(self.__class__[value.upper()] if isinstance(value, str) else value) + return super().__eq__(self.__class__(value.upper()) if isinstance(value, str) else value) class AcquisitionType(Flag): """ Available acquisition types & dimensions. + WARNING: It's best to use strings rather than integers to initialise. + >>> AcquisitionType(3) == AcquisitionType(2 | 1) == AcquisitionType.CONE|PARALLEL != AcquisitionType('3D') + Attributes ---------- - PARALLEL: + PARALLEL: Parallel beam. - CONE: + CONE: Cone beam. - DIM2: + DIM2: 2D acquisition. - DIM3: + DIM3: 3D acquisition. """ PARALLEL = auto() diff --git a/Wrappers/Python/test/test_labels.py b/Wrappers/Python/test/test_labels.py index 21703ebbe9..e97b52d2bc 100644 --- a/Wrappers/Python/test/test_labels.py +++ b/Wrappers/Python/test/test_labels.py @@ -69,6 +69,15 @@ def test_acquisition_type(self): self.assertIn(i, AcquisitionType) self.assertIn(i.lower(), AcquisitionType) self.assertIn(getattr(AcquisitionType, i), AcquisitionType) + combo = AcquisitionType.DIM2 | AcquisitionType.CONE + for i in (AcquisitionType.DIM2, AcquisitionType.CONE): + self.assertIn(i, combo) + for i in (AcquisitionType.DIM3, AcquisitionType.PARALLEL): + self.assertNotIn(i, combo) + for i in ('2D', 'DIM2', AcquisitionType.DIM2): + self.assertEqual(combo.dimension, i) + for i in ('CONE', 'cone', AcquisitionType.CONE): + self.assertEqual(combo.geometry, i) def test_image_dimension_labels(self): for i in ('CHANNEL', 'VERTICAL', 'HORIZONTAL_X', 'HORIZONTAL_Y'):