diff --git a/httomo/method_wrappers/__init__.py b/httomo/method_wrappers/__init__.py index 890fefac5..4f22f4208 100644 --- a/httomo/method_wrappers/__init__.py +++ b/httomo/method_wrappers/__init__.py @@ -7,9 +7,11 @@ # import all other wrappers to make sure they are available to the factory function # (add imports here when createing new wrappers) import httomo.method_wrappers.datareducer +import httomo.method_wrappers.sino360_to_180 import httomo.method_wrappers.dezinging import httomo.method_wrappers.distortion_correction import httomo.method_wrappers.seam_blender +import httomo.method_wrappers.average_frames import httomo.method_wrappers.images import httomo.method_wrappers.reconstruction import httomo.method_wrappers.rotation diff --git a/httomo/method_wrappers/average_frames.py b/httomo/method_wrappers/average_frames.py new file mode 100644 index 000000000..9e4ee4692 --- /dev/null +++ b/httomo/method_wrappers/average_frames.py @@ -0,0 +1,37 @@ +from httomo.method_wrappers.generic import GenericMethodWrapper +from httomo.block_interfaces import T +import numpy as np + + +class AverageFramesWrapper(GenericMethodWrapper): + """ + Wrapper for frames/projection averaging. + """ + + @classmethod + def should_select_this_class(cls, module_path: str, method_name: str) -> bool: + return "average_projection_frames" in method_name + + def _preprocess_data(self, block: T) -> T: + # when the angular preview is getting changed by averaging the angles should be changed accordingly + config_params = self._config_params + k = config_params["projection_averaging_factor"] + + n_proj = block.data.shape[0] # original data angular size + n_full = n_proj // k + remainder = n_proj % k + + n_out = n_full + (remainder > 0) + + averaged_angles = np.empty(n_out, dtype=block.angles.dtype) + + if n_full: + averaged_angles[:n_full] = ( + block.angles_radians[: n_full * k].reshape(n_full, k).mean(axis=1) + ) + + if remainder: + averaged_angles[-1] = block.angles_radians[n_full * k :].mean() + + block.angles_radians = averaged_angles + return block diff --git a/httomo/method_wrappers/reconstruction.py b/httomo/method_wrappers/reconstruction.py index 10fb85292..782268e95 100644 --- a/httomo/method_wrappers/reconstruction.py +++ b/httomo/method_wrappers/reconstruction.py @@ -12,24 +12,17 @@ class ReconstructionWrapper(GenericMethodWrapper): - """Wraps reconstruction functions, limiting the length of the angles array - before calling the method.""" + """Wraps reconstruction functions.""" @classmethod def should_select_this_class(cls, module_path: str, method_name: str) -> bool: return module_path.endswith(".algorithm") def _preprocess_data(self, block: T) -> T: - # this is essential for the angles cutting below to be valid assert ( self.pattern == Pattern.sinogram ), "reconstruction methods must be sinogram" - - # for 360 degrees data the angular dimension will be truncated while angles are not. - # Truncating angles if the angular dimension has got a different size - datashape0 = block.data.shape[0] - if datashape0 != len(block.angles_radians): - block.angles_radians = block.angles_radians[0:datashape0] + assert len(block.angles_radians) == block.data.shape[0] self._input_shape = block.data.shape return super()._preprocess_data(block) diff --git a/httomo/method_wrappers/sino360_to_180.py b/httomo/method_wrappers/sino360_to_180.py new file mode 100644 index 000000000..7b902850f --- /dev/null +++ b/httomo/method_wrappers/sino360_to_180.py @@ -0,0 +1,18 @@ +from httomo.method_wrappers.generic import GenericMethodWrapper +from httomo.block_interfaces import T + + +class Sino360to180Wrapper(GenericMethodWrapper): + """ + Wrapper to perform extended FoV (360degrees) data conversion to a standard 180 degrees data. + The wrapper is responsible for changing the angles after the data has changed. + """ + + @classmethod + def should_select_this_class(cls, module_path: str, method_name: str) -> bool: + return "sino_360_to_180" in method_name + + def _postprocess_data(self, block: T) -> T: + # for 360 degrees data the angular dimension is truncated so the angles should be changed in a similar fashion. + block.angles_radians = block.angles_radians[0 : block.data.shape[0]] + return block diff --git a/httomo/runner/method_wrapper.py b/httomo/runner/method_wrapper.py index 763299f08..dd74a473b 100644 --- a/httomo/runner/method_wrapper.py +++ b/httomo/runner/method_wrapper.py @@ -194,6 +194,7 @@ def calculate_max_slices( data_dtype: np.dtype, slicing_dim: int, non_slice_dims_shape: Tuple[int, int], + angles: np.ndarray, available_memory: int, ) -> Tuple[int, int]: """If it runs on GPU, determine the maximum number of slices that can fit in the diff --git a/tests/conftest.py b/tests/conftest.py index dbeca7356..300f68650 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -228,6 +228,10 @@ def FBP2d_astra(): def FBP3d_tomobar_denoising(): return "docs/source/pipelines_full/FBP3d_tomobar_denoising.yaml" +@pytest.fixture +def angles_averaging(): + return "docs/source/pipelines_full/angles_averaging.yaml" + @pytest.fixture def FISTA3d_tomobar(): diff --git a/tests/method_wrappers/test_360_to_180.py b/tests/method_wrappers/test_360_to_180.py new file mode 100644 index 000000000..194799324 --- /dev/null +++ b/tests/method_wrappers/test_360_to_180.py @@ -0,0 +1,48 @@ +from httomo.method_wrappers import make_method_wrapper +from httomo.method_wrappers.sino360_to_180 import Sino360to180Wrapper +from httomo.runner.auxiliary_data import AuxiliaryData +from httomo.runner.dataset import DataSetBlock +from ..testing_utils import make_mock_preview_config, make_mock_repo +from httomo_backends.methods_database.query import Pattern + +import numpy as np +from mpi4py import MPI +from pytest_mock import MockerFixture + + +def test_sino_360_to_180(mocker: MockerFixture): + GLOBAL_SHAPE = (10, 20, 30) + GLOBAL_SHAPE_MOD = (5, 20, 30) + + class FakeModule: + def sino_360_to_180_tester(data): + np.testing.assert_array_equal(data, 1) + return data + + mocker.patch( + "httomo.method_wrappers.generic.import_module", return_value=FakeModule + ) + wrp = make_method_wrapper( + make_mock_repo(mocker, pattern=Pattern.sinogram), + "mocked_module_path.morph", + "sino_360_to_180_tester", + MPI.COMM_WORLD, + make_mock_preview_config(mocker), + ) + assert isinstance(wrp, Sino360to180Wrapper) + + aux_data = AuxiliaryData(angles=2.0 * np.ones(GLOBAL_SHAPE[0], dtype=np.float32)) + data = np.ones( + GLOBAL_SHAPE_MOD, dtype=np.float32 + ) # assuming the data already averaged here by factor of 2 + input = DataSetBlock( + data[:, 0:3, :], + slicing_dim=1, + aux_data=aux_data, + chunk_shape=GLOBAL_SHAPE_MOD, + global_shape=GLOBAL_SHAPE_MOD, + ) + + wrp.execute(input) + + assert aux_data.get_angles().shape[0] == 5 diff --git a/tests/method_wrappers/test_angle_average.py b/tests/method_wrappers/test_angle_average.py new file mode 100644 index 000000000..18503b765 --- /dev/null +++ b/tests/method_wrappers/test_angle_average.py @@ -0,0 +1,50 @@ +from httomo.method_wrappers import make_method_wrapper +from httomo.method_wrappers.average_frames import AverageFramesWrapper +from httomo.runner.auxiliary_data import AuxiliaryData +from httomo.runner.dataset import DataSetBlock +from ..testing_utils import make_mock_preview_config, make_mock_repo +from httomo_backends.methods_database.query import Pattern + +import numpy as np +from mpi4py import MPI +from pytest_mock import MockerFixture + + +def test_angle_averaging(mocker: MockerFixture): + GLOBAL_SHAPE = (10, 20, 30) + + class FakeModule: + def average_projection_frames_tester(data, projection_averaging_factor): + np.testing.assert_array_equal(data, 1) + return data + + mocker.patch( + "httomo.method_wrappers.generic.import_module", return_value=FakeModule + ) + wrp = make_method_wrapper( + make_mock_repo(mocker, pattern=Pattern.sinogram), + "mocked_module_path.morph", + "average_projection_frames_tester", + MPI.COMM_WORLD, + make_mock_preview_config(mocker), + projection_averaging_factor=2, + ) + assert isinstance(wrp, AverageFramesWrapper) + + aux_data = AuxiliaryData( + angles=2.0 * np.ones(GLOBAL_SHAPE[0] + 10, dtype=np.float32) + ) + data = np.ones( + GLOBAL_SHAPE, dtype=np.float32 + ) # assuming the data already averaged here by factor of 2 + input = DataSetBlock( + data[:, 0:3, :], + slicing_dim=1, + aux_data=aux_data, + chunk_shape=GLOBAL_SHAPE, + global_shape=GLOBAL_SHAPE, + ) + + wrp.execute(input) + + assert aux_data.get_angles().shape[0] == 5 diff --git a/tests/method_wrappers/test_reconstruction.py b/tests/method_wrappers/test_reconstruction.py index 89675f9ce..a3f27e196 100644 --- a/tests/method_wrappers/test_reconstruction.py +++ b/tests/method_wrappers/test_reconstruction.py @@ -1,57 +1,13 @@ -import math from httomo.method_wrappers import make_method_wrapper -from httomo.method_wrappers.reconstruction import ReconstructionWrapper from httomo.runner.auxiliary_data import AuxiliaryData from httomo.runner.dataset import DataSetBlock from ..testing_utils import make_mock_preview_config, make_mock_repo -from httomo_backends.methods_database.query import Pattern - import numpy as np from mpi4py import MPI from pytest_mock import MockerFixture -def test_recon_handles_reconstruction_angle_reshape(mocker: MockerFixture): - GLOBAL_SHAPE = (10, 20, 30) - - class FakeModule: - # we give the angles a different name on purpose - def recon_tester(data, theta): - np.testing.assert_array_equal(data, 1) - np.testing.assert_array_equal(theta, 2) - assert data.shape[0] == len(theta) - return data - - mocker.patch( - "httomo.method_wrappers.generic.import_module", return_value=FakeModule - ) - wrp = make_method_wrapper( - make_mock_repo(mocker, pattern=Pattern.sinogram), - "mocked_module_path.algorithm", - "recon_tester", - MPI.COMM_WORLD, - make_mock_preview_config(mocker), - ) - assert isinstance(wrp, ReconstructionWrapper) - - aux_data = AuxiliaryData( - angles=2.0 * np.ones(GLOBAL_SHAPE[0] + 10, dtype=np.float32) - ) - data = np.ones(GLOBAL_SHAPE, dtype=np.float32) - input = DataSetBlock( - data[:, 0:3, :], - slicing_dim=1, - aux_data=aux_data, - chunk_shape=GLOBAL_SHAPE, - global_shape=GLOBAL_SHAPE, - ) - - wrp.execute(input) - - assert aux_data.get_angles().shape[0] == GLOBAL_SHAPE[0] - - def test_recon_handles_reconstruction_axisswap(mocker: MockerFixture): class FakeModule: def recon_tester(data, theta): diff --git a/tests/test_parallel_pipeline_big.py b/tests/test_parallel_pipeline_big.py index 39563d98d..bdced7546 100644 --- a/tests/test_parallel_pipeline_big.py +++ b/tests/test_parallel_pipeline_big.py @@ -176,6 +176,85 @@ def test_pipe_parallel_FBP3d_tomobar_k11_38730_in_memory_preview( assert res_norm < 1e-6 +# ######################################################################## +@pytest.mark.full_data_parallel +def test_angles_averaging_LPRec_i12_119647_preview( + get_files: Callable, + cmd_mpirun, + i12_119647, + angles_averaging, + FBP3d_tomobar_k11_38730_npz, + output_folder, +): + + change_value_parameters_method_pipeline( + angles_averaging, + method=[ + "standard_tomo", + "standard_tomo", + "standard_tomo", + "standard_tomo", + "average_projection_frames", + ], + key=[ + "data_path", + "image_key_path", + "rotation_angles", + "preview", + "projection_averaging_factor", + ], + value=[ + "/entry/imaging/data", + "/entry/instrument/imaging/image_key", + {"data_path": "/entry/imaging_sum/gts_cs_theta"}, + {"detector_y": {"start": 800, "stop": 1200}}, + 12, + ], + ) + + cmd_mpirun.insert(9, i12_119647) + cmd_mpirun.insert(10, angles_averaging) + cmd_mpirun.insert(11, output_folder) + + process = Popen( + cmd_mpirun, env=os.environ, shell=False, stdin=PIPE, stdout=PIPE, stderr=PIPE + ) + output, error = process.communicate() + print(output) + + files = get_files(output_folder) + + # #: check the generated reconstruction (hdf5 file) + # h5_files = list(filter(lambda x: ".h5" in x, files)) + # assert len(h5_files) == 1 + + # # load the pre-saved numpy array for comparison bellow + # data_gt = FBP3d_tomobar_k11_38730_npz["data"] + # axis_slice = FBP3d_tomobar_k11_38730_npz["axis_slice"] + # slices, sizeX, sizeY = np.shape(data_gt) + + # step = axis_slice // (slices + 2) + # # store for the result + # data_result = np.zeros((slices, sizeX, sizeY), dtype=np.float32) + + # path_to_data = "data/" + # h5_file_name = "FBP3d_tomobar" + # for file_to_open in h5_files: + # if h5_file_name in file_to_open: + # h5f = h5py.File(file_to_open, "r") + # index_prog = step + # for i in range(slices): + # data_result[i, :, :] = h5f[path_to_data][:, index_prog, :] + # index_prog += step + # h5f.close() + # else: + # message_str = f"File name with {h5_file_name} string cannot be found." + # raise FileNotFoundError(message_str) + + # residual_im = data_gt - data_result + # res_norm = np.linalg.norm(residual_im.flatten()).astype("float32") + # assert res_norm < 1e-6 + # ########################################################################