From 6558e261610f63cf4827fec73c4c65b5da9cf63e Mon Sep 17 00:00:00 2001 From: ashmeigh Date: Mon, 26 Feb 2024 08:48:07 +0000 Subject: [PATCH 1/3] chnaging operations to compute function --- .../operations/circular_mask/circular_mask.py | 32 +++++---- .../operations/clip_values/clip_values.py | 43 +++++++----- .../operations/crop_coords/crop_coords.py | 54 +++++---------- .../core/operations/divide/divide.py | 30 +++++---- .../operations/nan_removal/nan_removal.py | 67 +++++++------------ mantidimaging/core/operations/rebin/rebin.py | 58 +++++----------- 6 files changed, 121 insertions(+), 163 deletions(-) diff --git a/mantidimaging/core/operations/circular_mask/circular_mask.py b/mantidimaging/core/operations/circular_mask/circular_mask.py index 0b62b466aa8..f42b4c1fa8a 100644 --- a/mantidimaging/core/operations/circular_mask/circular_mask.py +++ b/mantidimaging/core/operations/circular_mask/circular_mask.py @@ -3,12 +3,13 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Dict +import numpy as np import tomopy +from mantidimaging.core.parallel import shared as ps from mantidimaging.core.operations.base_filter import BaseFilter -from mantidimaging.core.utility.progress_reporting import Progress from mantidimaging.gui.utility.qt_helpers import Type if TYPE_CHECKING: @@ -29,28 +30,31 @@ class CircularMaskFilter(BaseFilter): link_histograms = True @staticmethod - def filter_func(data: ImageStack, circular_mask_ratio=0.95, circular_mask_value=0., progress=None) -> ImageStack: + def filter_func(cls, + data: ImageStack, + circular_mask_ratio=0.95, + circular_mask_value=0., + progress=None) -> ImageStack: """ :param data: Input data as a 3D numpy.ndarray :param circular_mask_ratio: The ratio to the full image. - The ratio must be 0 < ratio < 1 - :param circular_mask_value: The value that all pixels in the mask - will be set to. - + @@ -39,20 +41,21 @@ def filter_func(data: ImageStack, circular_mask_ratio=0.95, circular_mask_value= :return: The processed 3D numpy.ndarray """ - if not circular_mask_ratio or not circular_mask_ratio < 1: - raise ValueError(f'circular_mask_ratio must be > 0 and < 1. Value provided was {circular_mask_ratio}') - - progress = Progress.ensure_instance(progress, num_steps=1, task_name='Circular Mask') + if not 0 < circular_mask_ratio < 1: + raise ValueError( + f"Circular mask ratio must be greater than 0 and less than 1, but value was {circular_mask_ratio}") - with progress: - progress.update(msg="Applying circular mask") + params = {'circular_mask_ratio': circular_mask_ratio, 'circular_mask_value': circular_mask_value} - tomopy.circ_mask(arr=data.data, axis=0, ratio=circular_mask_ratio, val=circular_mask_value) + ps.run_compute_func(cls.compute_function, data.data.shape[0], [data.shared_array], params, progress) return data + @staticmethod + def compute_function(i: int, arrays: List[np.ndarray], params: Dict[str, any]): + tomopy.circ_mask(arrays[0][i], axis=0, ratio=params['circular_mask_ratio'], val=params['circular_mask_value']) + @staticmethod def register_gui(form, on_change, view): from mantidimaging.gui.utility import add_property_to_form diff --git a/mantidimaging/core/operations/clip_values/clip_values.py b/mantidimaging/core/operations/clip_values/clip_values.py index 174388a8bd4..9a3c307ee32 100644 --- a/mantidimaging/core/operations/clip_values/clip_values.py +++ b/mantidimaging/core/operations/clip_values/clip_values.py @@ -3,10 +3,12 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Dict + +import numpy as np from mantidimaging.core.operations.base_filter import BaseFilter -from mantidimaging.core.utility.progress_reporting import Progress +from mantidimaging.core.parallel import shared as ps if TYPE_CHECKING: from mantidimaging.core.data import ImageStack @@ -26,7 +28,8 @@ class ClipValuesFilter(BaseFilter): link_histograms = True @staticmethod - def filter_func(data, + def filter_func(cls, + data, clip_min=None, clip_max=None, clip_min_new_value=None, @@ -54,27 +57,31 @@ def filter_func(data, """ # We're using is None because 0.0 is a valid value if clip_min is None and clip_max is None: - raise ValueError('At least one of clip_min or clip_max must be supplied') + raise ValueError("At least one of clip_min or clip_max must be supplied") - progress = Progress.ensure_instance(progress, num_steps=2, task_name='Clipping Values.') - with progress: - sample = data.data - progress.update(msg="Determining clip min and clip max") - clip_min = clip_min if clip_min is not None else sample.min() - clip_max = clip_max if clip_max is not None else sample.max() + params = { + 'clip_min': clip_min, + 'clip_max': clip_max, + 'clip_min_new_value': clip_min_new_value, + 'clip_max_new_value': clip_max_new_value + } - clip_min_new_value = clip_min_new_value if clip_min_new_value is not None else clip_min + ps.run_compute_func(cls.compute_function, data.data.shape[0], [data.shared_array], params, progress) - clip_max_new_value = clip_max_new_value if clip_max_new_value is not None else clip_max + return data - progress.update(msg=f"Clipping data with values min {clip_min} and max {clip_max}") + @staticmethod + def compute_function(i: int, arrays: List[np.ndarray], params: Dict[str, any]): + array = arrays[0][i] - # this is the fastest way to clip the values, np.clip does not do - # the clipping in place and ends up copying the data - sample[sample < clip_min] = clip_min_new_value - sample[sample > clip_max] = clip_max_new_value + clip_min = params.get('clip_min', np.min(array)) + clip_max = params.get('clip_max', np.max(array)) + clip_min_new_value = params.get('clip_min_new_value', clip_min) + clip_max_new_value = params.get('clip_max_new_value', clip_max) - return data + np.clip(array, clip_min, clip_max, out=array) + array[array < clip_min] = clip_min_new_value + array[array > clip_max] = clip_max_new_value @staticmethod def register_gui(form, on_change, view): diff --git a/mantidimaging/core/operations/crop_coords/crop_coords.py b/mantidimaging/core/operations/crop_coords/crop_coords.py index 555b83d4fd0..ae9b0d8e72c 100644 --- a/mantidimaging/core/operations/crop_coords/crop_coords.py +++ b/mantidimaging/core/operations/crop_coords/crop_coords.py @@ -5,10 +5,10 @@ from functools import partial from typing import Union, Optional, List, TYPE_CHECKING -from mantidimaging import helper as h +import numpy as np + +from mantidimaging.core.parallel import shared as ps from mantidimaging.core.operations.base_filter import BaseFilter, FilterGroup -from mantidimaging.core.parallel import utility as pu -from mantidimaging.core.utility.progress_reporting import Progress from mantidimaging.core.utility.sensible_roi import SensibleROI from mantidimaging.gui.utility.qt_helpers import Type @@ -32,7 +32,8 @@ class CropCoordinatesFilter(BaseFilter): link_histograms = True @staticmethod - def filter_func(images: ImageStack, + def filter_func(cls, + images: ImageStack, region_of_interest: Optional[Union[List[int], List[float], SensibleROI]] = None, progress=None) -> ImageStack: """Execute the Crop Coordinates by Region of Interest filter. This does @@ -54,25 +55,24 @@ def filter_func(images: ImageStack, """ if region_of_interest is None: - region_of_interest = SensibleROI.from_list([0, 0, 50, 50]) + region_of_interest = [0, 0, 50, 50] # Default ROI + # ROI is correct (SensibleROI or list of coords) if isinstance(region_of_interest, list): - region_of_interest = SensibleROI.from_list(region_of_interest) - - assert isinstance(region_of_interest, SensibleROI) - - h.check_data_stack(images) + roi = region_of_interest + else: + roi = [region_of_interest.left, region_of_interest.top, region_of_interest.right, region_of_interest.bottom] - sample = images.data - shape = (sample.shape[0], region_of_interest.height, region_of_interest.width) - if any((s < 0 for s in shape)): - raise ValueError("It seems the Region of Interest is outside of the current image dimensions.\n" - "This can happen on the image preview right after a previous Crop Coordinates.") + params = {'roi': roi} + ps.run_compute_func(cls.compute_function, images.data.shape[0], images.shared_array, params, progress) - output = pu.create_array(shape, images.dtype) - execute_single(sample, region_of_interest, progress, out=output.array) - images.shared_array = output return images + @staticmethod + def compute_function(i: int, array: np.ndarray, params: dict): + roi = params['roi'] + # Crop ROI + array[i] = array[i, roi[1]:roi[3], roi[0]:roi[2]] + @staticmethod def register_gui(form, on_change, view): from mantidimaging.gui.utility import add_property_to_form @@ -97,21 +97,3 @@ def execute_wrapper(roi_field: QLineEdit) -> partial: @staticmethod def group_name() -> FilterGroup: return FilterGroup.Basic - - -def execute_single(data, roi, progress=None, out=None): - progress = Progress.ensure_instance(progress, task_name='Crop Coords') - - if roi: - progress.add_estimated_steps(1) - - with progress: - assert all(isinstance(region, int) for - region in roi), \ - "The region of interest coordinates are not integers!" - - progress.update(msg="Cropping with coordinates: {0}".format(roi)) - - output = out[:] if out is not None else data[:] - output[:] = data[:, roi.top:roi.bottom, roi.left:roi.right] - return output diff --git a/mantidimaging/core/operations/divide/divide.py b/mantidimaging/core/operations/divide/divide.py index bc65689da93..5e6b2e7c3be 100644 --- a/mantidimaging/core/operations/divide/divide.py +++ b/mantidimaging/core/operations/divide/divide.py @@ -5,7 +5,9 @@ from functools import partial from typing import Union, Callable, Dict, Any, TYPE_CHECKING -from mantidimaging import helper as h +import numpy as np + +from mantidimaging.core.parallel import shared as ps from mantidimaging.core.operations.base_filter import BaseFilter from mantidimaging.gui.utility.qt_helpers import Type @@ -29,23 +31,31 @@ class DivideFilter(BaseFilter): link_histograms = True @staticmethod - def filter_func(images: ImageStack, value: Union[int, float] = 0, unit="micron", progress=None) -> ImageStack: + def filter_func(cls, images: ImageStack, value: Union[int, float] = 0, unit="micron", progress=None) -> ImageStack: """ :param value: The division value. :param unit: The unit of the divisor. :return: The ImageStack object which has been divided by a value. """ - h.check_data_stack(images) - if not value: - raise ValueError('value parameter must not equal 0 or None') + if value == 0: + raise ValueError('value parameter must not equal 0') + # Convert microns to cm if necessary if unit == "micron": - value *= 1e-4 + conversion_factor = 1e-4 # Example conversion factor + value *= conversion_factor + + params = {'value': value} + ps.run_compute_func(cls.compute_function, images.data.shape[0], images.shared_array, params, progress) - images.data /= value return images + @staticmethod + def compute_function(i: int, array: np.ndarray, params: dict): + value = params['value'] + array[i] /= value + @staticmethod def register_gui(form: 'QFormLayout', on_change: Callable, view: 'BasePresenter') -> Dict[str, Any]: from mantidimaging.gui.utility import add_property_to_form @@ -75,9 +85,3 @@ def execute_wrapper( # type: ignore value = value_widget.value() unit = unit_widget.currentText() return partial(DivideFilter.filter_func, value=value, unit=unit) - - @staticmethod - def validate_execute_kwargs(kwargs: Dict[str, Any]) -> bool: - if 'value_widget' not in kwargs: - return False - return True diff --git a/mantidimaging/core/operations/nan_removal/nan_removal.py b/mantidimaging/core/operations/nan_removal/nan_removal.py index 9556b73d31f..fff5c3e4a7c 100644 --- a/mantidimaging/core/operations/nan_removal/nan_removal.py +++ b/mantidimaging/core/operations/nan_removal/nan_removal.py @@ -3,15 +3,13 @@ from __future__ import annotations from functools import partial -from logging import getLogger from typing import Dict, TYPE_CHECKING import numpy as np -import scipy.ndimage as scipy_ndimage +from tomopy import median_filter from mantidimaging.core.operations.base_filter import BaseFilter from mantidimaging.core.parallel import shared as ps -from mantidimaging.core.utility.progress_reporting import Progress from mantidimaging.gui.utility.qt_helpers import Type if TYPE_CHECKING: @@ -41,8 +39,12 @@ class NaNRemovalFilter(BaseFilter): MODES = ["Constant", "Median"] - @staticmethod - def filter_func(data, replace_value=None, mode_value="Constant", progress=None) -> ImageStack: + @classmethod + def filter_func(cls, + images: ImageStack, + replace_value: float = 0.0, + mode_value: str = "Constant", + progress=None) -> ImageStack: """ :param data: The input data. :param mode_value: Values to replace NaNs with. One of ["Constant", "Median"] @@ -51,16 +53,28 @@ def filter_func(data, replace_value=None, mode_value="Constant", progress=None) :return: The ImageStack object with the NaNs replaced. """ + params = {'replace_value': replace_value, 'mode_value': mode_value} + ps.run_compute_func(cls.compute_function, images.data.shape[0], images.shared_array, params, progress) + + return images + + @staticmethod + def compute_function(i: int, array: np.ndarray, params: dict): + mode_value = params['mode_value'] + replace_value = params['replace_value'] if mode_value == "Constant": - sample = data.data - nan_idxs = np.isnan(sample) - sample[nan_idxs] = replace_value + nan_idxs = np.isnan(array[i]) + array[i][nan_idxs] = replace_value elif mode_value == "Median": - _execute(data, 3, "reflect", progress) + nans = np.isnan(array[i]) + if np.any(nans): + median_data = np.where(nans, -np.inf, array[i]) + median_data = median_filter(median_data, size=3, mode='reflect') + array[i] = np.where(nans, median_data, array[i]) + # Convert infs back to NaNs + array[i] = np.where(np.logical_and(nans, array[i] == -np.inf), np.nan, array[i]) else: - raise ValueError(f"Unknown mode: '{mode_value}'\nShould be one of {NaNRemovalFilter.MODES}") - - return data + raise ValueError(f"Unknown mode: '{mode_value}'. Should be one of {NaNRemovalFilter.MODES}") @staticmethod def register_gui(form: 'QFormLayout', on_change: Callable, view: 'BaseMainWindowView') -> Dict[str, 'QWidget']: @@ -92,32 +106,3 @@ def execute_wrapper(mode_field=None, replace_value_field=None): mode_value = mode_field.currentText() replace_value = replace_value_field.value() return partial(NaNRemovalFilter.filter_func, replace_value=replace_value, mode_value=mode_value) - - -def _nan_to_median(data: np.ndarray, size: int, edgemode: str): - nans = np.isnan(data) - if np.any(nans): - median_data = np.where(nans, -np.inf, data) - median_data = scipy_ndimage.median_filter(median_data, size=size, mode=edgemode) - data = np.where(nans, median_data, data) - - if np.any(data == -np.inf): - # Convert any left over -infs back to NaNs - data = np.where(np.logical_and(nans, data == -np.inf), np.nan, data) - - return data - - -def _execute(images: ImageStack, size, edgemode, progress=None): - log = getLogger(__name__) - progress = Progress.ensure_instance(progress, task_name='NaN Removal') - - # create the partial function to forward the parameters - f = ps.create_partial(_nan_to_median, ps.return_to_self, size=size, edgemode=edgemode) - - with progress: - log.info("PARALLEL NaN Removal filter, with pixel data type: {0}".format(images.dtype)) - - ps.execute(f, [images.shared_array], images.data.shape[0], progress, msg="NaN Removal") - - return images diff --git a/mantidimaging/core/operations/rebin/rebin.py b/mantidimaging/core/operations/rebin/rebin.py index 3ce5c73d937..5945bb13da9 100644 --- a/mantidimaging/core/operations/rebin/rebin.py +++ b/mantidimaging/core/operations/rebin/rebin.py @@ -5,12 +5,11 @@ from functools import partial from typing import TYPE_CHECKING -import skimage.transform +import numpy as np +from skimage.transform import resize -from mantidimaging import helper as h from mantidimaging.core.operations.base_filter import BaseFilter from mantidimaging.core.parallel import shared as ps -from mantidimaging.core.parallel import utility as pu from mantidimaging.gui.utility import add_property_to_form from mantidimaging.gui.utility.qt_helpers import Type @@ -32,7 +31,7 @@ class RebinFilter(BaseFilter): link_histograms = True @staticmethod - def filter_func(images: ImageStack, rebin_param=0.5, mode=None, progress=None) -> ImageStack: + def filter_func(cls, images: ImageStack, rebin_param=0.5, mode=None, progress=None) -> ImageStack: """ :param images: Sample data which is to be processed. Expects radiograms :param rebin_param: int, float or tuple @@ -44,31 +43,26 @@ def filter_func(images: ImageStack, rebin_param=0.5, mode=None, progress=None) - :return: The processed 3D numpy.ndarray """ - h.check_data_stack(images) - + # Validate rebin_param if isinstance(rebin_param, tuple): - param_valid = rebin_param[0] > 0 and rebin_param[1] > 0 + new_shape = rebin_param + elif isinstance(rebin_param, (int, float)): + current_shape = images.data.shape[1:] + new_shape = (int(current_shape[0] * rebin_param), int(current_shape[1] * rebin_param)) else: - param_valid = rebin_param > 0 - - if not param_valid: - raise ValueError('Rebin parameter must be greater than 0') - - empty_resized_data = _create_reshaped_array(images, rebin_param) + raise ValueError("Invalid type for rebin_param") - f = ps.create_partial(skimage.transform.resize, - ps.return_to_second_at_i, - mode=mode, - output_shape=empty_resized_data.array.shape[1:]) - ps.execute(partial_func=f, - arrays=[images.shared_array, empty_resized_data], - num_operations=images.data.shape[0], - msg="Applying Rebin", - progress=progress) - images.shared_array = empty_resized_data + params = {'new_shape': new_shape, 'mode': mode} + ps.run_compute_func(cls.compute_function, images.data.shape[0], images.shared_array, params, progress) return images + @staticmethod + def compute_function(i: int, array: np.ndarray, params: dict): + new_shape = params['new_shape'] + mode = params['mode'] + array[i] = resize(array[i], output_shape=new_shape, mode=mode, preserve_range=True) + @staticmethod def register_gui(form, on_change, view): # Rebin by uniform factor options @@ -150,21 +144,3 @@ def execute_wrapper(rebin_to_dimensions_radio=None, def modes(): return ["constant", "edge", "wrap", "reflect", "symmetric"] - - -def _create_reshaped_array(images, rebin_param): - old_shape = images.data.shape - num_images = old_shape[0] - - # use SciPy's calculation to find the expected dimensions - # int to avoid visible deprecation warning - if isinstance(rebin_param, tuple): - expected_dimy = int(rebin_param[0]) - expected_dimx = int(rebin_param[1]) - else: - expected_dimy = int(rebin_param * old_shape[1]) - expected_dimx = int(rebin_param * old_shape[2]) - - # allocate memory for images with new dimensions - shape = (num_images, expected_dimy, expected_dimx) - return pu.create_array(shape, images.dtype) From 88fe6ad86c54c9009a6a86908af88e03a712d520 Mon Sep 17 00:00:00 2001 From: ashmeigh Date: Tue, 27 Feb 2024 14:50:03 +0000 Subject: [PATCH 2/3] changing operations to compute function --- .../core/operations/circular_mask/circular_mask.py | 6 +++--- mantidimaging/core/operations/clip_values/clip_values.py | 6 +++--- mantidimaging/core/operations/crop_coords/__init__.py | 2 +- mantidimaging/core/operations/crop_coords/crop_coords.py | 2 +- mantidimaging/core/operations/divide/divide.py | 2 +- mantidimaging/core/operations/rebin/rebin.py | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/mantidimaging/core/operations/circular_mask/circular_mask.py b/mantidimaging/core/operations/circular_mask/circular_mask.py index f42b4c1fa8a..e45f1863249 100644 --- a/mantidimaging/core/operations/circular_mask/circular_mask.py +++ b/mantidimaging/core/operations/circular_mask/circular_mask.py @@ -3,7 +3,7 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING, List, Dict +from typing import TYPE_CHECKING, List, Dict, Any import numpy as np import tomopy @@ -29,7 +29,7 @@ class CircularMaskFilter(BaseFilter): filter_name = "Circular Mask" link_histograms = True - @staticmethod + @classmethod def filter_func(cls, data: ImageStack, circular_mask_ratio=0.95, @@ -52,7 +52,7 @@ def filter_func(cls, return data @staticmethod - def compute_function(i: int, arrays: List[np.ndarray], params: Dict[str, any]): + def compute_function(i: int, arrays: List[np.ndarray], params: Dict[str, Any]): tomopy.circ_mask(arrays[0][i], axis=0, ratio=params['circular_mask_ratio'], val=params['circular_mask_value']) @staticmethod diff --git a/mantidimaging/core/operations/clip_values/clip_values.py b/mantidimaging/core/operations/clip_values/clip_values.py index 9a3c307ee32..26854e475d2 100644 --- a/mantidimaging/core/operations/clip_values/clip_values.py +++ b/mantidimaging/core/operations/clip_values/clip_values.py @@ -3,7 +3,7 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING, List, Dict +from typing import TYPE_CHECKING, List, Dict, Any import numpy as np @@ -27,7 +27,7 @@ class ClipValuesFilter(BaseFilter): filter_name = "Clip Values" link_histograms = True - @staticmethod + @classmethod def filter_func(cls, data, clip_min=None, @@ -71,7 +71,7 @@ def filter_func(cls, return data @staticmethod - def compute_function(i: int, arrays: List[np.ndarray], params: Dict[str, any]): + def compute_function(i: int, arrays: List[np.ndarray], params: Dict[str, Any]): array = arrays[0][i] clip_min = params.get('clip_min', np.min(array)) diff --git a/mantidimaging/core/operations/crop_coords/__init__.py b/mantidimaging/core/operations/crop_coords/__init__.py index 8223b80bb94..2e7ff11b982 100644 --- a/mantidimaging/core/operations/crop_coords/__init__.py +++ b/mantidimaging/core/operations/crop_coords/__init__.py @@ -2,6 +2,6 @@ # SPDX - License - Identifier: GPL-3.0-or-later from __future__ import annotations -from .crop_coords import CropCoordinatesFilter, execute_single # noqa:F401 +from .crop_coords import CropCoordinatesFilter # noqa:F401 FILTER_CLASS = CropCoordinatesFilter diff --git a/mantidimaging/core/operations/crop_coords/crop_coords.py b/mantidimaging/core/operations/crop_coords/crop_coords.py index ae9b0d8e72c..84a958a86eb 100644 --- a/mantidimaging/core/operations/crop_coords/crop_coords.py +++ b/mantidimaging/core/operations/crop_coords/crop_coords.py @@ -31,7 +31,7 @@ class CropCoordinatesFilter(BaseFilter): filter_name = "Crop Coordinates" link_histograms = True - @staticmethod + @classmethod def filter_func(cls, images: ImageStack, region_of_interest: Optional[Union[List[int], List[float], SensibleROI]] = None, diff --git a/mantidimaging/core/operations/divide/divide.py b/mantidimaging/core/operations/divide/divide.py index 5e6b2e7c3be..831f0b8d099 100644 --- a/mantidimaging/core/operations/divide/divide.py +++ b/mantidimaging/core/operations/divide/divide.py @@ -30,7 +30,7 @@ class DivideFilter(BaseFilter): filter_name = "Divide" link_histograms = True - @staticmethod + @classmethod def filter_func(cls, images: ImageStack, value: Union[int, float] = 0, unit="micron", progress=None) -> ImageStack: """ :param value: The division value. diff --git a/mantidimaging/core/operations/rebin/rebin.py b/mantidimaging/core/operations/rebin/rebin.py index 5945bb13da9..c8a22ed652e 100644 --- a/mantidimaging/core/operations/rebin/rebin.py +++ b/mantidimaging/core/operations/rebin/rebin.py @@ -30,7 +30,7 @@ class RebinFilter(BaseFilter): filter_name = "Rebin" link_histograms = True - @staticmethod + @classmethod def filter_func(cls, images: ImageStack, rebin_param=0.5, mode=None, progress=None) -> ImageStack: """ :param images: Sample data which is to be processed. Expects radiograms From af1bdce8b08fcfe1783f5530d8228a3bc6000b15 Mon Sep 17 00:00:00 2001 From: ashmeigh Date: Thu, 29 Feb 2024 18:16:23 +0000 Subject: [PATCH 3/3] testing compute operations --- .../operations/circular_mask/circular_mask.py | 11 ++--- .../operations/crop_coords/crop_coords.py | 46 ++++++++++++------- .../core/operations/divide/divide.py | 6 +-- .../operations/nan_removal/nan_removal.py | 12 ++--- mantidimaging/core/operations/rebin/rebin.py | 39 ++++++++++++---- scripts/operations_tests/operations_tests.py | 4 +- scripts/operations_tests/test_cases.json | 45 ++++++++++++++++-- 7 files changed, 115 insertions(+), 48 deletions(-) diff --git a/mantidimaging/core/operations/circular_mask/circular_mask.py b/mantidimaging/core/operations/circular_mask/circular_mask.py index e45f1863249..c42a99d341a 100644 --- a/mantidimaging/core/operations/circular_mask/circular_mask.py +++ b/mantidimaging/core/operations/circular_mask/circular_mask.py @@ -29,12 +29,8 @@ class CircularMaskFilter(BaseFilter): filter_name = "Circular Mask" link_histograms = True - @classmethod - def filter_func(cls, - data: ImageStack, - circular_mask_ratio=0.95, - circular_mask_value=0., - progress=None) -> ImageStack: + @staticmethod + def filter_func(data: ImageStack, circular_mask_ratio=0.95, circular_mask_value=0., progress=None) -> ImageStack: """ :param data: Input data as a 3D numpy.ndarray :param circular_mask_ratio: The ratio to the full image. @@ -47,7 +43,8 @@ def filter_func(cls, params = {'circular_mask_ratio': circular_mask_ratio, 'circular_mask_value': circular_mask_value} - ps.run_compute_func(cls.compute_function, data.data.shape[0], [data.shared_array], params, progress) + ps.run_compute_func(CircularMaskFilter.compute_function, data.data.shape[0], [data.shared_array], params, + progress) return data diff --git a/mantidimaging/core/operations/crop_coords/crop_coords.py b/mantidimaging/core/operations/crop_coords/crop_coords.py index 84a958a86eb..f31408c4b37 100644 --- a/mantidimaging/core/operations/crop_coords/crop_coords.py +++ b/mantidimaging/core/operations/crop_coords/crop_coords.py @@ -3,11 +3,12 @@ from __future__ import annotations from functools import partial -from typing import Union, Optional, List, TYPE_CHECKING +from typing import Union, Optional, List, TYPE_CHECKING, Dict, Any import numpy as np -from mantidimaging.core.parallel import shared as ps +from mantidimaging import helper as h +from mantidimaging.core.parallel import utility as pu, shared as ps from mantidimaging.core.operations.base_filter import BaseFilter, FilterGroup from mantidimaging.core.utility.sensible_roi import SensibleROI from mantidimaging.gui.utility.qt_helpers import Type @@ -31,9 +32,8 @@ class CropCoordinatesFilter(BaseFilter): filter_name = "Crop Coordinates" link_histograms = True - @classmethod - def filter_func(cls, - images: ImageStack, + @staticmethod + def filter_func(images: ImageStack, region_of_interest: Optional[Union[List[int], List[float], SensibleROI]] = None, progress=None) -> ImageStack: """Execute the Crop Coordinates by Region of Interest filter. This does @@ -55,25 +55,39 @@ def filter_func(cls, """ if region_of_interest is None: - region_of_interest = [0, 0, 50, 50] # Default ROI - # ROI is correct (SensibleROI or list of coords) + region_of_interest = SensibleROI.from_list([0, 0, 50, 50]) if isinstance(region_of_interest, list): - roi = region_of_interest - else: - roi = [region_of_interest.left, region_of_interest.top, region_of_interest.right, region_of_interest.bottom] + region_of_interest = SensibleROI.from_list(region_of_interest) + + assert isinstance(region_of_interest, SensibleROI) - params = {'roi': roi} - ps.run_compute_func(cls.compute_function, images.data.shape[0], images.shared_array, params, progress) + h.check_data_stack(images) + sample = images.data + shape = (sample.shape[0], region_of_interest.height, region_of_interest.width) + if any((s < 0 for s in shape)): + raise ValueError("It seems the Region of Interest is outside of the current image dimensions.\n" + "This can happen on the image preview right after a previous Crop Coordinates.") + + output = pu.create_array(shape, images.dtype) + params = {'sample': sample, 'roi': region_of_interest, 'output': output.array} + ps.run_compute_func(CropCoordinatesFilter.compute_function, sample.shape[0], images.shared_array, params, + progress) + images.shared_array = output return images @staticmethod - def compute_function(i: int, array: np.ndarray, params: dict): + def compute_function(i: int, array: np.ndarray, params: Dict[str, Any]): + _ = array + sample = params['sample'] roi = params['roi'] - # Crop ROI - array[i] = array[i, roi[1]:roi[3], roi[0]:roi[2]] + output = params['output'] + if isinstance(roi, SensibleROI): + left, top, right, bottom = roi.left, roi.top, roi.right, roi.bottom + else: + left, top, right, bottom = roi[0], roi[1], roi[2], roi[3] + output[i] = sample[i, top:bottom, left:right] - @staticmethod def register_gui(form, on_change, view): from mantidimaging.gui.utility import add_property_to_form label, roi_field = add_property_to_form("ROI", diff --git a/mantidimaging/core/operations/divide/divide.py b/mantidimaging/core/operations/divide/divide.py index 831f0b8d099..39f5a21d636 100644 --- a/mantidimaging/core/operations/divide/divide.py +++ b/mantidimaging/core/operations/divide/divide.py @@ -30,8 +30,8 @@ class DivideFilter(BaseFilter): filter_name = "Divide" link_histograms = True - @classmethod - def filter_func(cls, images: ImageStack, value: Union[int, float] = 0, unit="micron", progress=None) -> ImageStack: + @staticmethod + def filter_func(images: ImageStack, value: Union[int, float] = 0, unit="micron", progress=None) -> ImageStack: """ :param value: The division value. :param unit: The unit of the divisor. @@ -47,7 +47,7 @@ def filter_func(cls, images: ImageStack, value: Union[int, float] = 0, unit="mic value *= conversion_factor params = {'value': value} - ps.run_compute_func(cls.compute_function, images.data.shape[0], images.shared_array, params, progress) + ps.run_compute_func(DivideFilter.compute_function, images.data.shape[0], images.shared_array, params, progress) return images diff --git a/mantidimaging/core/operations/nan_removal/nan_removal.py b/mantidimaging/core/operations/nan_removal/nan_removal.py index fff5c3e4a7c..04801cb24ff 100644 --- a/mantidimaging/core/operations/nan_removal/nan_removal.py +++ b/mantidimaging/core/operations/nan_removal/nan_removal.py @@ -39,12 +39,8 @@ class NaNRemovalFilter(BaseFilter): MODES = ["Constant", "Median"] - @classmethod - def filter_func(cls, - images: ImageStack, - replace_value: float = 0.0, - mode_value: str = "Constant", - progress=None) -> ImageStack: + @staticmethod + def filter_func(data, replace_value=None, mode_value="Constant", progress=None) -> ImageStack: """ :param data: The input data. :param mode_value: Values to replace NaNs with. One of ["Constant", "Median"] @@ -54,9 +50,9 @@ def filter_func(cls, """ params = {'replace_value': replace_value, 'mode_value': mode_value} - ps.run_compute_func(cls.compute_function, images.data.shape[0], images.shared_array, params, progress) + ps.run_compute_func(NaNRemovalFilter.compute_function, data.data.shape[0], data.shared_array, params, progress) - return images + return data @staticmethod def compute_function(i: int, array: np.ndarray, params: dict): diff --git a/mantidimaging/core/operations/rebin/rebin.py b/mantidimaging/core/operations/rebin/rebin.py index c8a22ed652e..0f279fb18db 100644 --- a/mantidimaging/core/operations/rebin/rebin.py +++ b/mantidimaging/core/operations/rebin/rebin.py @@ -3,13 +3,13 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List import numpy as np from skimage.transform import resize from mantidimaging.core.operations.base_filter import BaseFilter -from mantidimaging.core.parallel import shared as ps +from mantidimaging.core.parallel import utility as pu, shared as ps from mantidimaging.gui.utility import add_property_to_form from mantidimaging.gui.utility.qt_helpers import Type @@ -30,8 +30,8 @@ class RebinFilter(BaseFilter): filter_name = "Rebin" link_histograms = True - @classmethod - def filter_func(cls, images: ImageStack, rebin_param=0.5, mode=None, progress=None) -> ImageStack: + @staticmethod + def filter_func(images: ImageStack, rebin_param=0.5, mode=None, progress=None) -> ImageStack: """ :param images: Sample data which is to be processed. Expects radiograms :param rebin_param: int, float or tuple @@ -43,7 +43,6 @@ def filter_func(cls, images: ImageStack, rebin_param=0.5, mode=None, progress=No :return: The processed 3D numpy.ndarray """ - # Validate rebin_param if isinstance(rebin_param, tuple): new_shape = rebin_param elif isinstance(rebin_param, (int, float)): @@ -52,16 +51,21 @@ def filter_func(cls, images: ImageStack, rebin_param=0.5, mode=None, progress=No else: raise ValueError("Invalid type for rebin_param") - params = {'new_shape': new_shape, 'mode': mode} - ps.run_compute_func(cls.compute_function, images.data.shape[0], images.shared_array, params, progress) + output = _create_reshaped_array(images, rebin_param) + params = {'new_shape': new_shape, 'mode': mode} + ps.run_compute_func(RebinFilter.compute_function, images.data.shape[0], [images.shared_array, output], params, + progress) + images.shared_array = output return images @staticmethod - def compute_function(i: int, array: np.ndarray, params: dict): + def compute_function(i: int, arrays: List[np.ndarray], params: dict): + array = arrays[0] + output = arrays[1] new_shape = params['new_shape'] mode = params['mode'] - array[i] = resize(array[i], output_shape=new_shape, mode=mode, preserve_range=True) + output[i] = resize(array[i], output_shape=new_shape, mode=mode, preserve_range=True) @staticmethod def register_gui(form, on_change, view): @@ -142,5 +146,22 @@ def execute_wrapper(rebin_to_dimensions_radio=None, return partial(RebinFilter.filter_func, mode=mode_field.currentText(), rebin_param=params) +def _create_reshaped_array(images, rebin_param): + old_shape = images.data.shape + num_images = old_shape[0] + + # use SciPy's calculation to find the expected dimensions + # int to avoid visible deprecation warning + if isinstance(rebin_param, tuple): + expected_dimy = int(rebin_param[0]) + expected_dimx = int(rebin_param[1]) + else: + expected_dimy = int(rebin_param * old_shape[1]) + expected_dimx = int(rebin_param * old_shape[2]) + + shape = (num_images, expected_dimy, expected_dimx) + return pu.create_array(shape, images.dtype) + + def modes(): return ["constant", "edge", "wrap", "reflect", "symmetric"] diff --git a/scripts/operations_tests/operations_tests.py b/scripts/operations_tests/operations_tests.py index ad247994ab4..4547f837ff3 100644 --- a/scripts/operations_tests/operations_tests.py +++ b/scripts/operations_tests/operations_tests.py @@ -31,8 +31,8 @@ from mantidimaging.core.io.loader import loader # noqa: E402 from mantidimaging.core.operations.loader import load_filter_packages # noqa: E402 -LOAD_SAMPLE = (Path.home() / "mantidimaging-data" / "ISIS" / "IMAT" / "IMAT00010675" / "Tomo" / - "IMAT_Flower_Tomo_000000.tif") +LOAD_SAMPLE = ("C:/Users/44770/Downloads/mantidimaging-data-small " + "(2)/mantidimaging-data-small/ISIS/IMAT/IMAT00010675/Tomo/IMAT_Flower_Tomo_000000.tif") if path := os.getenv("MANTIDIMAGING_APPROVAL_TESTS_DIR"): SAVE_DIR = Path(path) diff --git a/scripts/operations_tests/test_cases.json b/scripts/operations_tests/test_cases.json index 53ba438d64c..e7eec4e9f00 100644 --- a/scripts/operations_tests/test_cases.json +++ b/scripts/operations_tests/test_cases.json @@ -220,8 +220,10 @@ } ] }, - "Ring Removal": { - "params": {"center_mode": "image center"}, + "Ring Removal": { + "params": { + "center_mode": "image center" + }, "source_data": "flower128", "cases": [ { @@ -235,5 +237,42 @@ } } ] + }, + "Rebin": { + "params": { + "rebin_param": 0.5, + "mode": "reflect" + }, + "source_data": "flower128", + "cases": [ + { + "test_name": "rebin_by_factor_0.5", + "params": { + "rebin_param": 0.5 + } + }, + { + "test_name": "rebin_to_dimensions_100x100", + "params": { + "rebin_param": [ + 100, + 100 + ] + } + } + ] + }, + "Divide": { + "params": {}, + "source_data": "flower128", + "cases": [ + { + "test_name": "divide_by_non_zero_value", + "params": { + "value": 1.5, + "unit": "micron" + } + } + ] } -} +} \ No newline at end of file