From e1e6f877980f1cd36502caa020cac3e4e282e317 Mon Sep 17 00:00:00 2001 From: Laura Engelhardt Date: Sat, 26 Jul 2025 15:29:35 +0200 Subject: [PATCH 1/7] Pass only required data to the image reader and not the entire config --- .../core/image_reader_classes/image_reader.py | 4 ++-- .../core/image_reader_classes/png_reader.py | 6 +++--- src/i2pp/core/import_image.py | 17 ++++++----------- src/i2pp/core/run.py | 16 +++++++++++++++- tests/i2pp/core/test_import_image.py | 5 ++--- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/i2pp/core/image_reader_classes/image_reader.py b/src/i2pp/core/image_reader_classes/image_reader.py index fa0bbd9..c2a3579 100644 --- a/src/i2pp/core/image_reader_classes/image_reader.py +++ b/src/i2pp/core/image_reader_classes/image_reader.py @@ -173,9 +173,9 @@ class ImageReader(ABC): images (PNG). """ - def __init__(self, config: dict, bounding_box: BoundingBox): + def __init__(self, options: dict, bounding_box: BoundingBox): """Init ImageReader.""" - self.config = config + self.options = options self.bounding_box = bounding_box def _get_slice_orientation( diff --git a/src/i2pp/core/image_reader_classes/png_reader.py b/src/i2pp/core/image_reader_classes/png_reader.py index 66f7668..c60a361 100644 --- a/src/i2pp/core/image_reader_classes/png_reader.py +++ b/src/i2pp/core/image_reader_classes/png_reader.py @@ -99,7 +99,7 @@ def load_image(self, folder_path: Path) -> list[np.ndarray]: """Loads and processes PNG image data from a specified directory. This function reads all PNG files in the given folder, verifies - the format of the image_metadata in the configuration, and + the format of the image_metadata dictionary, and converts the images to RGB format. The 2-dimensional PNG images together represent a 3D image. @@ -118,7 +118,7 @@ def load_image(self, folder_path: Path) -> list[np.ndarray]: logging.info("Load image data!") - self._verify_image_metadata(self.config["image_metadata"]) + self._verify_image_metadata(self.options["image_metadata"]) raw_png = [] @@ -152,7 +152,7 @@ def convert_to_image_data(self, raw_pngs: list[np.ndarray]) -> ImageData: grid coordinates, orientation, and metadata. """ - image_metadata = self.config["image_metadata"] + image_metadata = self.options["image_metadata"] row_direction = np.array( image_metadata.get("row_direction") or [0, -1, 0] diff --git a/src/i2pp/core/import_image.py b/src/i2pp/core/import_image.py index 7c2d9d8..d202a2f 100644 --- a/src/i2pp/core/import_image.py +++ b/src/i2pp/core/import_image.py @@ -3,7 +3,7 @@ import logging from enum import Enum from pathlib import Path -from typing import Type, cast +from typing import Type import numpy as np import pydicom @@ -161,7 +161,7 @@ def determine_image_format(folder_path: Path) -> ImageFormat: def verify_and_load_imagedata( - config: dict, bounding_box: BoundingBox + folder_path: Path, options: dict, bounding_box: BoundingBox ) -> ImageData: """Validates input data format and loads 3D image data. @@ -172,8 +172,9 @@ def verify_and_load_imagedata( the pixel intensity range based on the pixel type. Arguments: - config (dict): User configuration containing the directory for input - data and other settings. + folder_path (Path): Path to the image-data folder. + options (dict): Options for loading image data (e.g., metadata + settings). bounding_box (BoundingBox): The spatial region defining the area of interest for image processing. @@ -185,17 +186,11 @@ def verify_and_load_imagedata( RuntimeError: If the input data folder is invalid or contains unsupported data formats. """ - relative_path = Path(config["input informations"]["image_folder_path"]) - - folder_path = Path.cwd() / relative_path - _detect_and_append_suffixes(folder_path) image_format = determine_image_format(folder_path) - image_reader = cast( - ImageReader, image_format.get_reader()(config, bounding_box) - ) + image_reader = image_format.get_reader()(options, bounding_box) raw_image = image_reader.load_image(folder_path) diff --git a/src/i2pp/core/run.py b/src/i2pp/core/run.py index 7a93d0d..2188505 100644 --- a/src/i2pp/core/run.py +++ b/src/i2pp/core/run.py @@ -2,6 +2,7 @@ import copy import time +from pathlib import Path from i2pp.core.discretization_helpers import verify_and_load_discretization from i2pp.core.export_data import export_data @@ -36,7 +37,20 @@ def run_i2pp(config_i2pp): dis = verify_and_load_discretization(config_i2pp) - image_data = verify_and_load_imagedata(config_i2pp, dis.bounding_box) + # Retrieve information from configuration for loading image data + try: + relative_path = Path(config_i2pp["image"]["path"]) + except KeyError as e: + raise ValueError(f"Missing required configuration key: {e}") from e + image_path = Path.cwd() / relative_path + + image_options = dict() + image_options["image_metadata"] = config_i2pp["image"].get("metadata", {}) + + # Load the image data + image_data = verify_and_load_imagedata( + image_path, image_options, dis.bounding_box + ) processing_options: dict = config_i2pp["processing options"] smoothing_bool = processing_options.get("smoothing", False) diff --git a/tests/i2pp/core/test_import_image.py b/tests/i2pp/core/test_import_image.py index a44bd5d..a5d4a75 100644 --- a/tests/i2pp/core/test_import_image.py +++ b/tests/i2pp/core/test_import_image.py @@ -162,11 +162,10 @@ def test_verify_and_load_imagedata(tmp_path: Path): ds.save_as(dicom_file_path1, enforce_file_format=False) ds.save_as(dicom_file_path2, enforce_file_format=False) input_path = tmp_path - - config = {"input informations": {"image_folder_path": input_path}} + options: dict = {} image_data = verify_and_load_imagedata( - config, BoundingBox(max=[0, 0, 1000], min=[1, 1, -1000]) + input_path, options, BoundingBox(max=[0, 0, 1000], min=[1, 1, -1000]) ) assert np.array_equal(image_data.pixel_data.shape, (2, 128, 128)) From 243e454f385e88a90694c468a29145f9094577f0 Mon Sep 17 00:00:00 2001 From: Laura Engelhardt Date: Sat, 26 Jul 2025 15:34:33 +0200 Subject: [PATCH 2/7] Pass only required data to the discret. readers --- src/i2pp/core/discretization_helpers.py | 30 +++++++------------ .../discretization_reader.py | 6 ++-- .../fourc_yaml_reader.py | 11 +++---- .../mesh_reader.py | 6 ++-- src/i2pp/core/run.py | 9 +++++- .../i2pp/core/test_discretization_helpers.py | 14 ++++----- 6 files changed, 36 insertions(+), 40 deletions(-) diff --git a/src/i2pp/core/discretization_helpers.py b/src/i2pp/core/discretization_helpers.py index f4f2047..26ea674 100644 --- a/src/i2pp/core/discretization_helpers.py +++ b/src/i2pp/core/discretization_helpers.py @@ -79,36 +79,28 @@ def determine_discretization_format(file_path: Path) -> DiscretizationFormat: ) -def verify_and_load_discretization(config: dict) -> Discretization: - """Loads and processes mesh data based on the user configuration. +def verify_and_load_discretization( + discretization_path: Path, options: dict +) -> Discretization: + """Loads and processes mesh data. - This function verifies the input file, selects the appropriate reader - (MeshReader or FourCYamlReader), and loads the discretization data. + This function selects the appropriate reader (MeshReader or + FourCYamlReader), and loads the discretization data. Finally, it determines the discretization's bounding box. Arguments: - config (dict): User configuration containing paths and processing - options. + discretization_path (Path): Path to the discretization file. + options (dict): Options for loading the discretization that are passed + to the reader classes. Returns: DiscretizationData: The loaded and processed mesh data. - - Raises: - RuntimeError: If the mesh file is not valid or in the wrong format. """ - relative_path = Path( - config["input informations"]["discretization_file_path"] - ) - - file_path = Path.cwd() / relative_path - - dis_format = determine_discretization_format(file_path) + dis_format = determine_discretization_format(discretization_path) dis_reader = cast(DiscretizationReader, dis_format.get_reader()()) - dis = dis_reader.load_discretization( - file_path, config["processing options"] - ) + dis = dis_reader.load_discretization(discretization_path, options) bounding_box = find_mins_maxs(points=dis.nodes.coords, enlargement=2) diff --git a/src/i2pp/core/discretization_reader_classes/discretization_reader.py b/src/i2pp/core/discretization_reader_classes/discretization_reader.py index 788b7e9..2b94c49 100644 --- a/src/i2pp/core/discretization_reader_classes/discretization_reader.py +++ b/src/i2pp/core/discretization_reader_classes/discretization_reader.py @@ -102,7 +102,7 @@ def __init__(self): @abstractmethod def load_discretization( - self, file_path: Path, config: dict + self, file_path: Path, options: dict ) -> Discretization: """Abstract method to load discretization data from a file path. @@ -114,8 +114,8 @@ def load_discretization( Arguments: file_path (Path): Path to the discretization file. - config (dict): A dictionary containing configuration options for - loading the discretization. + options (dict): A dictionary containing options for loading the + discretization. Returns: Discretization: An instance of Discretization containing diff --git a/src/i2pp/core/discretization_reader_classes/fourc_yaml_reader.py b/src/i2pp/core/discretization_reader_classes/fourc_yaml_reader.py index 69d6222..f4b7927 100644 --- a/src/i2pp/core/discretization_reader_classes/fourc_yaml_reader.py +++ b/src/i2pp/core/discretization_reader_classes/fourc_yaml_reader.py @@ -70,7 +70,7 @@ def _filter_discretization( return dis def load_discretization( - self, file_path: Path, config: dict + self, file_path: Path, options: dict ) -> Discretization: """Loads and processes a finite element discretization from a .4C.yaml file. @@ -81,7 +81,9 @@ def load_discretization( Arguments: file_path (Path): Path to the .4C.yaml file. - config (dict): User configuration containing material ID filters. + options (dict): Options for loading the discretization. + Filtering for material ids can be enabled by specifying + `material_ids` in the options dictionary. Returns: Discretization: The finite element discretization including nodes @@ -94,10 +96,9 @@ def load_discretization( raw_dis.compute_ids(zero_based=True) - if config["material_ids"] is not None: - + if options["material_ids"] is not None: raw_dis = self._filter_discretization( - raw_dis, np.array(config["material_ids"]) + raw_dis, np.array(options["material_ids"]) ) nodes_coords = [] diff --git a/src/i2pp/core/discretization_reader_classes/mesh_reader.py b/src/i2pp/core/discretization_reader_classes/mesh_reader.py index 4b7a4b6..71aee81 100644 --- a/src/i2pp/core/discretization_reader_classes/mesh_reader.py +++ b/src/i2pp/core/discretization_reader_classes/mesh_reader.py @@ -33,7 +33,7 @@ def _filter_discretization(self) -> None: raise RuntimeError("This function is not implemented yet.") def load_discretization( - self, file_path: Path, config: dict + self, file_path: Path, options: dict ) -> Discretization: """Loads and processes a finite element model from a .mesh file. @@ -43,6 +43,7 @@ def load_discretization( Arguments: file_path (Path): Path to the .mesh file. + options (dict): Options for loading the discretization. Returns: Discretization: A structured representation of the finite element @@ -53,8 +54,7 @@ def load_discretization( raw_dis = trimesh.load(file_path) - if config["material_ids"] is not None: - + if options["material_ids"] is not None: self._filter_discretization() nodes = Nodes( diff --git a/src/i2pp/core/run.py b/src/i2pp/core/run.py index 2188505..9cb89ac 100644 --- a/src/i2pp/core/run.py +++ b/src/i2pp/core/run.py @@ -35,7 +35,14 @@ def run_i2pp(config_i2pp): start_time = time.time() - dis = verify_and_load_discretization(config_i2pp) + relative_path = Path(config_i2pp["discretization"]["path"]) + discretization_path = Path.cwd() / relative_path + + options = dict() + options["material_ids"] = config_i2pp["processing options"].get( + "material_ids", None + ) + dis = verify_and_load_discretization(discretization_path, options) # Retrieve information from configuration for loading image data try: diff --git a/tests/i2pp/core/test_discretization_helpers.py b/tests/i2pp/core/test_discretization_helpers.py index 8e02d91..d040480 100644 --- a/tests/i2pp/core/test_discretization_helpers.py +++ b/tests/i2pp/core/test_discretization_helpers.py @@ -73,14 +73,8 @@ def test_determine_discretization_format_mesh(tmp_path: Path) -> None: def test_verify_and_load_discretization(): """Test verify_and_load_discretization.""" - test_config = { - "input informations": { - "discretization_file_path": "test_path.4C.yaml", - }, - "processing options": "options", - } - absolut_path = Path.cwd() / "test_path.4C.yaml" + options_dict = {} mock_bounding = tuple([[0, 0, 0], [1, 1, 1]]) nodes = Nodes([0, 0, 0], 0) mock_dis = Discretization(nodes, []) @@ -96,13 +90,15 @@ def test_verify_and_load_discretization(): "i2pp.core.discretization_helpers.find_mins_maxs", return_value=mock_bounding, ) as mock_find_mins_maxs: - dis = verify_and_load_discretization(test_config) + dis = verify_and_load_discretization( + absolut_path, options_dict + ) mock_determine_discretization_format.assert_called_once_with( absolut_path ) mock_load_discretization.assert_called_once_with( - absolut_path, "options" + absolut_path, options_dict ) mock_find_mins_maxs.assert_called_once_with( points=[0, 0, 0], enlargement=2 From c847b777757c7f75cc08951ac51261cedd84d1bb Mon Sep 17 00:00:00 2001 From: Laura Engelhardt Date: Sat, 26 Jul 2025 15:41:30 +0200 Subject: [PATCH 3/7] Do not pass config to the visualizer as unused --- src/i2pp/core/run.py | 2 +- .../visualization_classes/discretization_visualization.py | 4 ---- src/i2pp/core/visualize_results.py | 5 +---- tests/i2pp/core/test_visualize_results.py | 2 +- .../test_discretization_visualization.py | 6 +----- 5 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/i2pp/core/run.py b/src/i2pp/core/run.py index 9cb89ac..8e7874a 100644 --- a/src/i2pp/core/run.py +++ b/src/i2pp/core/run.py @@ -106,4 +106,4 @@ def run_i2pp(config_i2pp): visualization_options: dict = config_i2pp["visualization_options"] if bool(visualization_options["plot_results"]): - visualize_results(config_i2pp, elements, image_data, dis) + visualize_results(elements, image_data, dis) diff --git a/src/i2pp/core/visualization_classes/discretization_visualization.py b/src/i2pp/core/visualization_classes/discretization_visualization.py index 61a0f2b..0fc4116 100644 --- a/src/i2pp/core/visualization_classes/discretization_visualization.py +++ b/src/i2pp/core/visualization_classes/discretization_visualization.py @@ -20,7 +20,6 @@ class DiscretizationVisualizer(Visualizer): def compute_grid( self, - config: dict, elements_with_values: list[Element], dis: Discretization, ) -> None: @@ -42,8 +41,6 @@ def compute_grid( later use. Arguments: - config (dict): Configuration dictionary containing processing - options. elements_with_values (list[Element]): List of elements with assigned values. dis (Discretization): The discretization object containing nodes @@ -52,7 +49,6 @@ def compute_grid( None """ - config["processing options"]["material_ids"] = None unstructured_grid, ele_has_value = initialize_unstructured_grid( elements_with_values, self.pixel_type, dis ) diff --git a/src/i2pp/core/visualize_results.py b/src/i2pp/core/visualize_results.py index ce02aee..fa595a5 100644 --- a/src/i2pp/core/visualize_results.py +++ b/src/i2pp/core/visualize_results.py @@ -39,7 +39,6 @@ def _run_processes_safely(*processes: Process) -> None: def visualize_results( - config: dict, elements_with_values: list[Element], image_data: ImageData, dis: Discretization, @@ -51,8 +50,6 @@ def visualize_results( - One for visualizing the image data as a structured grid. Arguments: - config (dict): Configuration dictionary containing visualization - options. elements_with_values (list[Element]): List of elements with their assigned values. image_data (ImageData): Image data containing pixel values and grid @@ -77,7 +74,7 @@ def plot_discretization(): title="Mesh Visualization", ) - visualizer_dis.compute_grid(config, elements_with_values, dis) + visualizer_dis.compute_grid(elements_with_values, dis) visualizer_dis.plot_grid() diff --git a/tests/i2pp/core/test_visualize_results.py b/tests/i2pp/core/test_visualize_results.py index eba5f67..faf2bec 100644 --- a/tests/i2pp/core/test_visualize_results.py +++ b/tests/i2pp/core/test_visualize_results.py @@ -52,7 +52,7 @@ def test_visualize_results( """Test that visualize_results starts two processes without actually running them.""" - visualize_results({}, mock_elements, mock_image_data, mock_discretization) + visualize_results(mock_elements, mock_image_data, mock_discretization) assert mock_process.call_count == 2 diff --git a/tests/i2pp/core/visualization_classes/test_discretization_visualization.py b/tests/i2pp/core/visualization_classes/test_discretization_visualization.py index 25b0b5d..c50dbf8 100644 --- a/tests/i2pp/core/visualization_classes/test_discretization_visualization.py +++ b/tests/i2pp/core/visualization_classes/test_discretization_visualization.py @@ -18,8 +18,6 @@ def test_create_vtk_from_unfiltered_discretization(): element2 = Element([0, 1, 2, 3], 1, data=20) element3 = Element([0, 1, 2, 3], 2, data=np.nan) - test_config = {"processing options": {}} - mock_unstructured_grid = MagicMock() mock_unstructured_grid.extract_cells = MagicMock() @@ -31,9 +29,7 @@ def test_create_vtk_from_unfiltered_discretization(): return_value=(mock_unstructured_grid, np.array([True, True, False])), ) as mock_initialize_unstructured_grid: dis_mock = MagicMock() - visualizer.compute_grid( - test_config, [element1, element2, element3], dis=dis_mock - ) + visualizer.compute_grid([element1, element2, element3], dis=dis_mock) mock_initialize_unstructured_grid.assert_called_once_with( [element1, element2, element3], PixelValueType.CT, dis_mock From 48f0567588b3b7ded90dac67fd2ee02083396c81 Mon Sep 17 00:00:00 2001 From: Laura Engelhardt Date: Sat, 26 Jul 2025 15:48:51 +0200 Subject: [PATCH 4/7] Pass only required data to the Interpolator --- src/i2pp/core/interpolate_element_data.py | 21 +++++++++++---------- src/i2pp/core/run.py | 7 ++++++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/i2pp/core/interpolate_element_data.py b/src/i2pp/core/interpolate_element_data.py index da679a4..5cb532a 100644 --- a/src/i2pp/core/interpolate_element_data.py +++ b/src/i2pp/core/interpolate_element_data.py @@ -18,7 +18,7 @@ from i2pp.core.interpolator_classes.interpolator_nodes import InterpolatorNodes -class CalculationType(Enum): +class InterpolationType(Enum): """Enum representing different calculation types for element value determination. @@ -58,9 +58,9 @@ def get_interpolator(self) -> Type[Interpolator]: """ interpolator_map = { - CalculationType.NODES: InterpolatorNodes, - CalculationType.ALLVOXELS: InterpolatorAllVoxel, - CalculationType.CENTER: InterpolatorCenter, + InterpolationType.NODES: InterpolatorNodes, + InterpolationType.ALLVOXELS: InterpolatorAllVoxel, + InterpolationType.CENTER: InterpolatorCenter, } if self not in interpolator_map: @@ -70,7 +70,7 @@ def get_interpolator(self) -> Type[Interpolator]: def interpolate_image_to_discretization( - dis: Discretization, image_data: ImageData, config: dict + dis: Discretization, image_data: ImageData, interpolation_method: str ) -> list[Element]: """Performs interpolation of image data onto the FEM Discretization based on the specified calculation type. @@ -91,16 +91,17 @@ def interpolate_image_to_discretization( elements and node coordinates. image_data (ImageData): A structured representation containing 3D pixel data, grid coordinates, orientation, and metadata. - config (dict): User-defined configuration settings. + interpolation_method (str): The type of interpolation to perform for + assigning pixel values to the elements. This should match one of + the `InterpolationType` enum values (e.g., "nodes", + "elementcenter", "allvoxels"). Returns: list[Element]: A list of FEM elements with interpolated pixel data. """ - calculation_type = CalculationType( - config["processing options"]["calculation_type"] - ) + enum_interpolation_method = InterpolationType(interpolation_method) - interpolator = calculation_type.get_interpolator()() + interpolator = enum_interpolation_method.get_interpolator()() return interpolator.compute_element_data(dis, image_data) diff --git a/src/i2pp/core/run.py b/src/i2pp/core/run.py index 8e7874a..0eba5d1 100644 --- a/src/i2pp/core/run.py +++ b/src/i2pp/core/run.py @@ -87,8 +87,13 @@ def run_i2pp(config_i2pp): time_after_smoothing - time_pre_smoothing ) + # Retrieve the interpolation method from the configuration + interpolation_method = ( + config_i2pp["processing options"]["interpolation_method"], + ) + # Interpolate the image data onto the mesh elements elements = interpolate_image_to_discretization( - dis, image_data, config_i2pp + dis, image_data, interpolation_method=interpolation_method ) export_data( From bbed2a0232708adfb69714a6743a1c23a2c226c6 Mon Sep 17 00:00:00 2001 From: Laura Engelhardt Date: Sat, 26 Jul 2025 20:52:41 +0200 Subject: [PATCH 5/7] Pass only required data to the export function --- src/i2pp/core/export_data.py | 131 +++++++++++++++------------- src/i2pp/core/run.py | 43 +++++++-- tests/i2pp/core/test_export_data.py | 127 ++++++++++++++------------- tests/i2pp/core/test_run.py | 30 ++++++- 4 files changed, 202 insertions(+), 129 deletions(-) diff --git a/src/i2pp/core/export_data.py b/src/i2pp/core/export_data.py index 49e176b..fa6eed7 100644 --- a/src/i2pp/core/export_data.py +++ b/src/i2pp/core/export_data.py @@ -3,7 +3,6 @@ import importlib.util import json import logging -import os from enum import Enum from pathlib import Path from typing import Any, Callable @@ -14,9 +13,7 @@ Discretization, Element, ) -from i2pp.core.image_reader_classes.image_reader import ( - PixelValueType, -) +from i2pp.core.image_reader_classes.image_reader import PixelValueType from i2pp.core.utilities import make_json_serializable, normalize_values @@ -46,7 +43,7 @@ def __init__(self): pass def load_user_function( - self, script_path: str, function_name: str + self, script_path: Path, function_name: str ) -> Callable[..., Any]: """Dynamically loads a user-defined function from a script file. @@ -94,7 +91,7 @@ def load_user_function( return user_function - def parse_export_format(self, config: dict): + def parse_export_format(self, export_format: str): """Parses the export format from the user configuration. This function retrieves the export format from the user configuration @@ -102,29 +99,24 @@ def parse_export_format(self, config: dict): specified or is invalid, it raises an error. Arguments: - config (dict): User configuration containing export settings. + export_format (str): The export format as a string. Raises: RuntimeError: If the export format is not supported or not specified. """ - user_config: dict = config["output options"] - export_format_str = user_config.get("export_format", "Not specified") - try: - self.export_format = ExportFormat(export_format_str) + self.export_format = ExportFormat(export_format) except ValueError: supported_formats = ", ".join(fmt.value for fmt in ExportFormat) raise RuntimeError( ( - f"Export format '{export_format_str}' is not supported! " + f"Export format '{export_format}' is not supported! " f"Supported formats are: {supported_formats}." ) ) def write_data( - self, - data: Any, - config: dict, + self, data: Any, output_file: Path, name_of_output_property: str = "" ) -> dict: """Writes the provided data to a file based on user configuration. @@ -138,11 +130,7 @@ def write_data( string for TXT format or a numpy array for JSON format. element_ids (np.ndarray): Array of element IDs corresponding to the data. - config (dict): User configuration containing output settings. - - File location and name are determined by `config["Output options"]`: - - "Output path": Directory to save the file (defaults to CWD). - - "Output name": Filename (defaults to "Output"). + output_file (Path): Path to the output file. Returns: dict: A dictionary containing the exported data. For JSON @@ -151,17 +139,20 @@ def write_data( empty dictionary. """ - user_config: dict = config["output options"] - - directory = Path(user_config.get("output_path") or Path.cwd()) - output_name = str(user_config.get("output_name") or "i2pp_output") - - path = os.path.join( - directory, f"{output_name}.{self.export_format.value}" - ) - - directory.mkdir(parents=False, exist_ok=True) - logging.info(f"Writing data to {path}") + if not output_file.suffix: + logging.warning( + "Output file has no suffix. Appending the export format " + "suffix." + ) + output_file = output_file.with_suffix( + f".{self.export_format.value}" + ) + if not output_file.parent.exists(): + logging.info( + f"Creating directory {output_file.parent} for output file." + ) + output_file.parent.mkdir(parents=True, exist_ok=True) + logging.info(f"Writing data to {output_file}") if self.export_format == ExportFormat.JSON: assert isinstance(data, np.ndarray), ( @@ -186,7 +177,7 @@ def write_data( "The structured numpy array must have at least one additional " "field. Adapt the user function." ) - if not ("name_of_output_property" in user_config): + if name_of_output_property == "": raise RuntimeError( "You specified a JSON export format. In this case, you " "must also specify the 'name_of_output_property' in the " @@ -197,7 +188,7 @@ def write_data( # Convert the structured numpy array to a dictionary json_dump_data = { - config["output options"]["name_of_output_property"]: { + name_of_output_property: { str(entry[field_names[0]]): ( make_json_serializable(entry[field_names[1]]) if len(field_names) == 2 @@ -210,7 +201,7 @@ def write_data( } } - with open(path, "w") as json_file: + with open(output_file, "w") as json_file: try: json.dump(json_dump_data, json_file, indent=4) except TypeError as e: @@ -220,7 +211,7 @@ def write_data( "JSON serializable." ) - return {config["output options"]["name_of_output_property"]: data} + return {name_of_output_property: data} elif self.export_format == ExportFormat.TXT: err_msg = ( @@ -228,7 +219,7 @@ def write_data( "function must return a string." ) assert isinstance(data, str), err_msg - with open(path, "w") as txt_file: + with open(output_file, "w") as txt_file: txt_file.write(data) return {} else: @@ -238,7 +229,7 @@ def write_data( def export_vtk( self, - config: dict, + output_file: Path, elements: list[Element], pixel_type: PixelValueType, exported_data: dict, @@ -248,7 +239,7 @@ def export_vtk( the verification of the i2pp output. Arguments: - config (dict): User configuration containing output settings. + output_file (Path): The path to the output vtk file. elements (list[Element]): List of elements with IDs and data. pixel_type (PixelValueType): Type of pixel values used in the discretization. @@ -273,17 +264,27 @@ def export_vtk( # only add if the data is transferable to a VTK file unstructured_grid.cell_data[f"{key}_{name}"] = value[name] - output_path = os.path.join( - config["output options"]["output_path"] or Path.cwd(), - f"{config['output options']['output_name'] or 'i2pp_output'}.vtu", - ) - unstructured_grid.save(output_path) + # Ensure the output file ends with the correct suffix + if not output_file.suffix: + logging.warning( + "Output file has no suffix. Appending the export format " + "suffix." + ) + output_file = output_file.with_suffix(".vtu") + + unstructured_grid.save(output_file) def export_data( elements: list[Element], dis: Discretization, - config: dict, + user_script_path: Path, + user_function_name: str, + export_format: str, + property_output_file: Path, + name_of_output_property: str, + normalize: bool, + vtk_output_file: Path, pxl_range: np.ndarray, pixel_type: PixelValueType, ) -> None: @@ -295,9 +296,8 @@ def export_data( Workflow: - Extracts element values and IDs. - - Loads the user function from `config["Processing options"]`. - - If normalization is enabled in `config["Processing options"]`, - it normalizes the element values. + - Loads the user function. + - If normalization is enabled, normalizes the element values. - Calls the user function to generate the export string. - Writes the export string to a file using `Exporter.write_data()`. - Depending on the export format, it also exports the data to a VTK @@ -307,35 +307,46 @@ def export_data( elements (List[Element]): List of elements with IDs and data. dis (Discretization): The discretization object containing nodes and elements. - config (dict): User configuration containing export settings. + user_script_path (Path): Path to the user script containing the + user-defined function. + user_function_name (str): Name of the user-defined function to call + for exporting data. + export_format (str): The format in which the data will be + exported (e.g., "json", "txt"). + property_output_file (Path): Path to the output file where the + exported data will be written. + name_of_output_property (str): Name of the property to be exported. + normalize (bool): Whether to normalize the element values + before exporting. + vtk_output_file (Path): Path to the output file for VTK export. pxl_range (np.ndarray): Pixel range for normalization if enabled. pixel_type (PixelValueType): Type of pixel values. Raises: RuntimeError: If the user function cannot be loaded. """ - - logging.info("Export File!") + logging.info("Exporting file.") element_ids = np.array([ele.id + 1 for ele in elements]) element_data = np.array([ele.data for ele in elements]) - processing_options = config["processing options"] - - script_path = processing_options["user_script"] - function_name = processing_options["user_function"] - exporter = Exporter() - exporter.parse_export_format(config) + exporter.parse_export_format(export_format=export_format) - if processing_options["normalize_values"]: + if normalize: element_data = normalize_values(element_data, pxl_range) - user_function = exporter.load_user_function(script_path, function_name) + user_function = exporter.load_user_function( + user_script_path, user_function_name + ) result = user_function(element_ids, element_data) - exported_data = exporter.write_data(result, config) + exported_data = exporter.write_data( + result, property_output_file, name_of_output_property + ) if exporter.export_format == ExportFormat.JSON: - exporter.export_vtk(config, elements, pixel_type, exported_data, dis) + exporter.export_vtk( + vtk_output_file, elements, pixel_type, exported_data, dis + ) diff --git a/src/i2pp/core/run.py b/src/i2pp/core/run.py index 0eba5d1..b37a14e 100644 --- a/src/i2pp/core/run.py +++ b/src/i2pp/core/run.py @@ -1,6 +1,7 @@ """Runner which executes the main routine of img2physiprop.""" import copy +import os import time from pathlib import Path @@ -96,12 +97,44 @@ def run_i2pp(config_i2pp): dis, image_data, interpolation_method=interpolation_method ) + # Retrieve export/output options from the configuration + export_format = config_i2pp["output options"].get( + "export_format", "Not specified" + ) + directory = Path( + config_i2pp["output options"].get("output_path") or Path.cwd() + ) + output_name = str( + config_i2pp["output options"].get("output_name") or "i2pp_output" + ) + property_output_path = os.path.join( + directory, f"{output_name}.{export_format}" + ) + name_of_output_property = config_i2pp["output options"].get( + "name_of_output_property", None + ) + vtk_output_path = os.path.join( + config_i2pp["output options"]["output_path"] or Path.cwd(), + f"{config_i2pp['output options']['output_name'] or 'i2pp_output'}.vtu", + ) + processing_options = config_i2pp["processing options"] + script_path = processing_options["user_script"] + function_name = processing_options["user_function"] + normalize = processing_options.get("normalize_values", False) + + # Export the data export_data( - elements, - dis, - config_i2pp, - image_data.pixel_range, - image_data.pixel_type, + elements=elements, + dis=dis, + user_script_path=Path(script_path), + user_function_name=function_name, + export_format=export_format, + property_output_file=Path(property_output_path), + name_of_output_property=name_of_output_property, + normalize=normalize, + vtk_output_file=Path(vtk_output_path), + pxl_range=image_data.pixel_range, + pixel_type=image_data.pixel_type, ) end_time = time.time() diff --git a/tests/i2pp/core/test_export_data.py b/tests/i2pp/core/test_export_data.py index c325a73..d4084cd 100644 --- a/tests/i2pp/core/test_export_data.py +++ b/tests/i2pp/core/test_export_data.py @@ -18,7 +18,7 @@ def test_load_user_function_not_exist(): """_load_user_function if Path not found.""" - path = "not_exisitng_path.py" + path = Path("not_exisitng_path.py") function_name = "function_name" exporter = Exporter() @@ -34,7 +34,7 @@ def test_load_user_function_exist(): test_script.write(b"def test_function(data): return 2*data+1\n") test_script.close() - script_path = test_script.name + script_path = Path(test_script.name) exporter = Exporter() loaded_function = exporter.load_user_function(script_path, "test_function") @@ -50,7 +50,7 @@ def test_load_user_function_not_callable(): test_script = tempfile.NamedTemporaryFile(delete=False, suffix=".py") test_script.close() - script_path = test_script.name + script_path = Path(test_script.name) exporter = Exporter() @@ -62,20 +62,18 @@ def test_load_user_function_not_callable(): def test_parse_export_format_invalid_format(): """Test parse_export_format raises error for invalid format.""" - test_config = {"output options": {"export_format": "invalid_format"}} exporter = Exporter() with pytest.raises( RuntimeError, match="Export format 'invalid_format' is not supported!" ): - exporter.parse_export_format(test_config) + exporter.parse_export_format("invalid_format") def test_parse_export_format_valid_format(): """Test parse_export_format sets the correct export format.""" - test_config = {"output options": {"export_format": "json"}} exporter = Exporter() - exporter.parse_export_format(test_config) + exporter.parse_export_format("json") assert exporter.export_format == ExportFormat.JSON @@ -91,12 +89,6 @@ def test_write_data_creates_json_file(): ("property3", "i4"), ], ) - test_config = { - "output options": { - "output_name": "test_output", - "name_of_output_property": "MUE", - } - } expected_output = { "MUE": { "1": [[2.0, 3.0], "string1", 5], @@ -107,12 +99,11 @@ def test_write_data_creates_json_file(): exporter.export_format = ExportFormat.JSON with tempfile.TemporaryDirectory() as temp_dir: - test_config["output options"]["output_path"] = temp_dir - exporter.write_data(data, test_config) + output_file = Path(os.path.join(temp_dir, "test_output.json")) + property_name = "MUE" + exporter.write_data(data, output_file, property_name) - with open( - os.path.join(temp_dir, "test_output.json"), "r" - ) as json_file: + with open(output_file, "r") as json_file: written_data = json.load(json_file) assert written_data == expected_output @@ -126,7 +117,11 @@ def test_write_data_creates_json_file(): "can be of any type, but must be JSON serializable." ), ): - exporter.write_data("not a numpy array", test_config) + exporter.write_data( + "not a numpy array", + output_file, + property_name, + ) with pytest.raises( AssertionError, match=( @@ -134,7 +129,11 @@ def test_write_data_creates_json_file(): "Adapt the user function." ), ): - exporter.write_data(np.array([1, 2], dtype=int), test_config) + exporter.write_data( + np.array([1, 2], dtype=int), + output_file, + property_name, + ) with pytest.raises( AssertionError, match=( @@ -147,7 +146,8 @@ def test_write_data_creates_json_file(): [(2.3, [1.1, 2.5])], dtype=[("index", "f8"), ("property1", "f8", 2)], ), - test_config, + output_file, + property_name, ) with pytest.raises( @@ -162,7 +162,8 @@ def test_write_data_creates_json_file(): [(1, [1.1, 2.5])], dtype=[("not_index", "i4"), ("property1", "f8", 2)], ), - test_config, + output_file, + property_name, ) with pytest.raises( AssertionError, @@ -172,7 +173,9 @@ def test_write_data_creates_json_file(): ), ): exporter.write_data( - np.array([1], dtype=[("index", "i4")]), test_config + np.array([1], dtype=[("index", "i4")]), + output_file, + property_name, ) with pytest.raises( RuntimeError, @@ -189,7 +192,11 @@ def test_write_data_creates_json_file(): ("property2", "O"), ], ) - exporter.write_data(arr, test_config) + exporter.write_data( + arr, + output_file, + property_name, + ) with pytest.raises( RuntimeError, match=( @@ -198,8 +205,7 @@ def test_write_data_creates_json_file(): "configuration." ), ): - test_config["output options"].pop("name_of_output_property") - exporter.write_data(arr, test_config) + exporter.write_data(arr, output_file) def test_write_data_creates_txt_file(): @@ -207,15 +213,13 @@ def test_write_data_creates_txt_file(): export_string = "test." test_config = { - "output options": { - "output_path": str(Path.cwd() / "test_directory"), - "output_name": "test_output", - } + "output_path": str(Path.cwd() / "test_directory"), + "output_name": "test_output", } exporter = Exporter() exporter.export_format = ExportFormat.TXT file_path = os.path.join( - Path(test_config["output options"]["output_path"]), "test_output.txt" + Path(test_config["output_path"]), "test_output.txt" ) err_string = ( @@ -223,23 +227,20 @@ def test_write_data_creates_txt_file(): " function must return a string." ) with mock.patch("builtins.open", mock.mock_open()) as mocked_file: - exporter.write_data(export_string, test_config) - mocked_file.assert_called_once_with(file_path, "w") + exporter.write_data(export_string, Path(file_path)) + mocked_file.assert_called_once_with(Path(file_path), "w") mocked_file().write.assert_called_once_with(export_string) with pytest.raises(AssertionError, match=err_string): - exporter.write_data(0, test_config) + exporter.write_data(0, Path(file_path)) def test_export_vtk_adds_cell_data_and_saves_file(): """Test export_vtk adds cell data to unstructured grid and saves the file.""" - test_config = { - "output options": { - "output_path": str(Path.cwd() / "test_directory"), - "output_name": "test_output", - } - } + vtk_output_path = Path( + str(Path.cwd() / "test_directory" / "test_output.vtu"), + ) elements = [Element([0, 1], 0, data=10), Element([0, 1], 1, data=20)] pixel_type = PixelValueType.CT exported_data = { @@ -261,7 +262,7 @@ def test_export_vtk_adds_cell_data_and_saves_file(): ) as mock_init_grid: with patch.object(mock_grid, "save") as mock_save: exporter.export_vtk( - test_config, + vtk_output_path, elements, pixel_type, exported_data, @@ -278,11 +279,7 @@ def test_export_vtk_adds_cell_data_and_saves_file(): np.array([[1.0, 2.0], [2.0, 3.0]]), ) - expected_file_path = os.path.join( - test_config["output options"]["output_path"], - f"{test_config['output options']['output_name']}.vtu", - ) - mock_save.assert_called_once_with(expected_file_path) + mock_save.assert_called_once_with(vtk_output_path) @pytest.fixture @@ -296,7 +293,8 @@ def exporter_mocks(): patch.object( Exporter, "parse_export_format", - side_effect=lambda config: setattr( + return_value=ExportFormat.JSON, + side_effect=lambda export_format: setattr( Exporter, "export_format", ExportFormat.JSON ), ) as mock_parse_export_format, @@ -323,13 +321,10 @@ def exporter_mocks(): def test_export_data(exporter_mocks): """Test export_data.""" - test_config = { - "processing options": { - "user_script": "mock_script.py", - "user_function": "mock_function", - "normalize_values": True, - } - } + + user_script = Path("mock_script.py") + user_function = "mock_function" + normalize = True pixel_range = np.array([0, 20]) element1 = Element([0, 1], 0, data=10) element2 = Element([0, 1], 1, data=20) @@ -340,25 +335,35 @@ def test_export_data(exporter_mocks): export_data( elements, mock_discretization, - test_config, - pixel_range, - PixelValueType.CT, + user_script_path=user_script, + user_function_name=user_function, + export_format="json", + property_output_file=Path("output.json"), + name_of_output_property="property_name", + normalize=normalize, + vtk_output_file=Path("output.vtu"), + pxl_range=pixel_range, + pixel_type=PixelValueType.CT, ) exporter_mocks["mock_parse_export_format"].assert_called_once_with( - test_config + export_format="json" ) args, _ = exporter_mocks["mock_normalize_values"].call_args np.testing.assert_array_equal(args[0], np.array([10, 20])) np.testing.assert_array_equal(args[1], np.array([0, 20])) exporter_mocks["mock_load_user_function"].assert_called_once_with( - "mock_script.py", "mock_function" + Path("mock_script.py"), "mock_function" ) args, _ = exporter_mocks["mock_write_data"].call_args np.testing.assert_array_equal(args[0], expected_result) - assert args[1] == test_config + assert args[1] == Path("output.json") exporter_mocks["mock_export_vtk"].assert_called_once_with( - test_config, elements, PixelValueType.CT, None, mock_discretization + Path("output.vtu"), + elements, + PixelValueType.CT, + None, + mock_discretization, ) diff --git a/tests/i2pp/core/test_run.py b/tests/i2pp/core/test_run.py index 30cd57d..43f5f0b 100644 --- a/tests/i2pp/core/test_run.py +++ b/tests/i2pp/core/test_run.py @@ -1,5 +1,6 @@ """Test run routine.""" +from pathlib import Path from unittest import mock import pytest @@ -21,12 +22,19 @@ def minimal_valid_config(tmp_path): "smoothing": True, "smoothing_area": 3, "interpolation_method": "nodes", + "user_script": "tests/testdata/user_script.py", + "user_function": "process_image_data", + "normalize_values": False, }, "visualization_options": { "plot_smoothing": False, "plot_results": False, }, - "export": {"path": tmp_path / "output.pattern", "format": "pattern"}, + "output options": { + "output_path": tmp_path, + "output_name": "output", + "export_format": "pattern", + }, } @@ -71,8 +79,24 @@ def test_run_i2pp_runs_successfully( mock_load_image.assert_called_once() mock_interpolate.assert_called_once() mock_export.assert_called_once_with( - mock_elements, mock_dis, minimal_valid_config, (0, 255), "dummy" + elements=mock_elements, + dis=mock_dis, + user_script_path=Path("tests/testdata/user_script.py"), + user_function_name="process_image_data", + export_format="pattern", + property_output_file=Path( + minimal_valid_config["output options"]["output_path"] + ) + / "output.pattern", + name_of_output_property=None, + normalize=False, + vtk_output_file=Path( + minimal_valid_config["output options"]["output_path"] + ) + / "output.vtu", + pxl_range=(0, 255), + pixel_type="dummy", ) - mock_smooth_data.assert_called_once() + mock_smooth_data.assert_called_once_with([[0]], 3) mock_vis_results.assert_not_called() mock_vis_smoothing.assert_not_called() From 2ec29ce90ad58b0e82f3a5585874196c444522e3 Mon Sep 17 00:00:00 2001 From: Laura Engelhardt Date: Sat, 26 Jul 2025 21:06:05 +0200 Subject: [PATCH 6/7] Rename pxl_range to pixel_range --- src/i2pp/core/export_data.py | 6 +- .../core/image_reader_classes/image_reader.py | 4 +- src/i2pp/core/import_image.py | 2 +- src/i2pp/core/run.py | 2 +- src/i2pp/core/utilities.py | 8 +- .../image_reader_classes/test_image_reader.py | 12 ++- tests/i2pp/core/test_export_data.py | 2 +- tests/i2pp/core/test_run.py | 102 +++++++++++++++++- tests/i2pp/core/test_utilities.py | 8 +- 9 files changed, 124 insertions(+), 22 deletions(-) diff --git a/src/i2pp/core/export_data.py b/src/i2pp/core/export_data.py index fa6eed7..472c4a0 100644 --- a/src/i2pp/core/export_data.py +++ b/src/i2pp/core/export_data.py @@ -285,7 +285,7 @@ def export_data( name_of_output_property: str, normalize: bool, vtk_output_file: Path, - pxl_range: np.ndarray, + pixel_range: np.ndarray, pixel_type: PixelValueType, ) -> None: """Exports element data to a file using a user-defined function. @@ -319,7 +319,7 @@ def export_data( normalize (bool): Whether to normalize the element values before exporting. vtk_output_file (Path): Path to the output file for VTK export. - pxl_range (np.ndarray): Pixel range for normalization if enabled. + pixel_range (np.ndarray): Pixel range for normalization if enabled. pixel_type (PixelValueType): Type of pixel values. Raises: @@ -334,7 +334,7 @@ def export_data( exporter.parse_export_format(export_format=export_format) if normalize: - element_data = normalize_values(element_data, pxl_range) + element_data = normalize_values(element_data, pixel_range) user_function = exporter.load_user_function( user_script_path, user_function_name diff --git a/src/i2pp/core/image_reader_classes/image_reader.py b/src/i2pp/core/image_reader_classes/image_reader.py index c2a3579..0401973 100644 --- a/src/i2pp/core/image_reader_classes/image_reader.py +++ b/src/i2pp/core/image_reader_classes/image_reader.py @@ -25,7 +25,7 @@ class PixelValueType(Enum): MRT: Magnetic resonance tomography pixel values. Properties: - pxl_range: Returns the default pixel range for each pixel value type. + pixel_range: Returns the default pixel range for each pixel value type. - For CT: The range is between -1024 and 3071. - For RGB: The range is between 0 and 255. - For MRT: The pixel range of MRT can vary and must be calculated @@ -37,7 +37,7 @@ class PixelValueType(Enum): MRT = "MR" @property - def pxl_range(self) -> np.ndarray: + def pixel_range(self) -> np.ndarray: """Returns the default pixel range for each pixel value type.""" if self == PixelValueType.CT: return np.array([-1024, 3071]) diff --git a/src/i2pp/core/import_image.py b/src/i2pp/core/import_image.py index d202a2f..d79c4be 100644 --- a/src/i2pp/core/import_image.py +++ b/src/i2pp/core/import_image.py @@ -202,6 +202,6 @@ def verify_and_load_imagedata( [image_data.pixel_data.min(), image_data.pixel_data.max()] ) else: - image_data.pixel_range = image_data.pixel_type.pxl_range + image_data.pixel_range = image_data.pixel_type.pixel_range return image_data diff --git a/src/i2pp/core/run.py b/src/i2pp/core/run.py index b37a14e..7e45669 100644 --- a/src/i2pp/core/run.py +++ b/src/i2pp/core/run.py @@ -133,7 +133,7 @@ def run_i2pp(config_i2pp): name_of_output_property=name_of_output_property, normalize=normalize, vtk_output_file=Path(vtk_output_path), - pxl_range=image_data.pixel_range, + pixel_range=image_data.pixel_range, pixel_type=image_data.pixel_type, ) diff --git a/src/i2pp/core/utilities.py b/src/i2pp/core/utilities.py index 51b5570..f4690f3 100644 --- a/src/i2pp/core/utilities.py +++ b/src/i2pp/core/utilities.py @@ -35,7 +35,7 @@ def find_mins_maxs( return min_coords, max_coords -def normalize_values(data: np.ndarray, pxl_range: np.ndarray) -> np.ndarray: +def normalize_values(data: np.ndarray, pixel_range: np.ndarray) -> np.ndarray: """Normalizes data to a range between 0 and 1 based on the provided pixel range. @@ -45,21 +45,21 @@ def normalize_values(data: np.ndarray, pxl_range: np.ndarray) -> np.ndarray: Arguments: data (np.ndarray): The array of data values to normalize. - pxl_range (np.ndarray): A NumPy array containing the minimum and + pixel_range (np.ndarray): A NumPy array containing the minimum and maximum pixel range values [min, max] used for normalization. Returns: np.ndarray: The normalized data with values scaled between 0 and 1. """ # Validate supplied pixel range - range_diff = pxl_range[1] - pxl_range[0] + range_diff = pixel_range[1] - pixel_range[0] if range_diff == 0: logging.error("Pixel range difference is zero.") elif range_diff < 0: logging.error("Pixel range is inverted (max < min).") # Normalize the data - normalized_data = (data - pxl_range[0]) / range_diff + normalized_data = (data - pixel_range[0]) / range_diff return normalized_data diff --git a/tests/i2pp/core/image_reader_classes/test_image_reader.py b/tests/i2pp/core/image_reader_classes/test_image_reader.py index 3f606ea..2f3c663 100644 --- a/tests/i2pp/core/image_reader_classes/test_image_reader.py +++ b/tests/i2pp/core/image_reader_classes/test_image_reader.py @@ -11,11 +11,13 @@ ) -def test_pxl_range(): - """Test pxl_range of enum PixelValueType.""" - assert np.array_equal(PixelValueType.RGB.pxl_range, np.array([0, 255])) - assert np.array_equal(PixelValueType.CT.pxl_range, np.array([-1024, 3071])) - assert PixelValueType.MRT.pxl_range is None +def test_pixel_range(): + """Test pixel_range of enum PixelValueType.""" + assert np.array_equal(PixelValueType.RGB.pixel_range, np.array([0, 255])) + assert np.array_equal( + PixelValueType.CT.pixel_range, np.array([-1024, 3071]) + ) + assert PixelValueType.MRT.pixel_range is None def test_get_slice_orientation_planes(): diff --git a/tests/i2pp/core/test_export_data.py b/tests/i2pp/core/test_export_data.py index d4084cd..b1b12a5 100644 --- a/tests/i2pp/core/test_export_data.py +++ b/tests/i2pp/core/test_export_data.py @@ -342,7 +342,7 @@ def test_export_data(exporter_mocks): name_of_output_property="property_name", normalize=normalize, vtk_output_file=Path("output.vtu"), - pxl_range=pixel_range, + pixel_range=pixel_range, pixel_type=PixelValueType.CT, ) diff --git a/tests/i2pp/core/test_run.py b/tests/i2pp/core/test_run.py index 43f5f0b..666559a 100644 --- a/tests/i2pp/core/test_run.py +++ b/tests/i2pp/core/test_run.py @@ -38,6 +38,41 @@ def minimal_valid_config(tmp_path): } +@pytest.fixture +def large_valid_config(tmp_path): + """Fixture to provide a larger, more complex configuration for testing i2pp + run.""" + return { + "discretization": { + "path": "tests/testdata/discretization_large.mesh", + "type": "mesh", + }, + "image": { + "path": "tests/testdata/image_large.dcm", + "type": "dicom", + "metadata": {"spacing": [0.5, 0.5, 1.0]}, + }, + "processing options": { + "smoothing": True, + "smoothing_area": 5, + "interpolation_method": "elements", + "material_ids": [1, 2, 3], + "user_script": "tests/testdata/user_script.py", + "user_function": "process_image_data", + "normalize_values": True, + }, + "visualization_options": { + "plot_smoothing": True, + "plot_results": True, + }, + "output options": { + "output_path": tmp_path, + "output_name": "output_large", + "export_format": "json", + }, + } + + @mock.patch("i2pp.core.run.verify_and_load_discretization") @mock.patch("i2pp.core.run.verify_and_load_imagedata") @mock.patch("i2pp.core.run.interpolate_image_to_discretization") @@ -94,9 +129,74 @@ def test_run_i2pp_runs_successfully( minimal_valid_config["output options"]["output_path"] ) / "output.vtu", - pxl_range=(0, 255), + pixel_range=(0, 255), pixel_type="dummy", ) mock_smooth_data.assert_called_once_with([[0]], 3) mock_vis_results.assert_not_called() mock_vis_smoothing.assert_not_called() + + +@mock.patch("i2pp.core.run.verify_and_load_discretization") +@mock.patch("i2pp.core.run.verify_and_load_imagedata") +@mock.patch("i2pp.core.run.interpolate_image_to_discretization") +@mock.patch("i2pp.core.run.export_data") +@mock.patch("i2pp.core.run.smooth_data") +@mock.patch("i2pp.core.run.visualize_results") +@mock.patch("i2pp.core.run.visualize_smoothing") +def test_run_i2pp_with_large_config( + mock_vis_smoothing, + mock_vis_results, + mock_smooth_data, + mock_export, + mock_interpolate, + mock_load_image, + mock_load_dis, + large_valid_config, +): + """Test that run_i2pp executes successfully with a larger, more complex + configuration.""" + + mock_dis = mock.Mock() + mock_dis.bounding_box = ((0, 0, 0), (10, 10, 10)) + mock_load_dis.return_value = mock_dis + + mock_image = mock.Mock() + mock_image.pixel_data = [[0] * 100] * 100 + mock_image.pixel_range = (0, 255) + mock_image.pixel_type = "dummy" + mock_load_image.return_value = mock_image + + mock_smooth_data.return_value = mock_image + + mock_elements = [{"id": 1, "value": 123}] + mock_interpolate.return_value = mock_elements + + run_i2pp(large_valid_config) + + assert len(mock_load_dis.call_args_list) == 1 + assert len(mock_load_image.call_args_list) == 1 + assert len(mock_interpolate.call_args_list) == 1 + assert len(mock_export.call_args_list) == 1 + mock_export.assert_called_once_with( + elements=mock_elements, + dis=mock_dis, + user_script_path=Path("tests/testdata/user_script.py"), + user_function_name="process_image_data", + export_format="json", + property_output_file=Path( + large_valid_config["output options"]["output_path"] + ) + / "output_large.json", + name_of_output_property=None, + normalize=True, + vtk_output_file=Path( + large_valid_config["output options"]["output_path"] + ) + / "output_large.vtu", + pixel_range=(0, 255), + pixel_type="dummy", + ) + mock_smooth_data.assert_called_once_with([[0] * 100] * 100, 5) + mock_vis_results.assert_called_once() + mock_vis_smoothing.assert_called_once() diff --git a/tests/i2pp/core/test_utilities.py b/tests/i2pp/core/test_utilities.py index c8e7901..bdd8446 100644 --- a/tests/i2pp/core/test_utilities.py +++ b/tests/i2pp/core/test_utilities.py @@ -44,21 +44,21 @@ def test_find_mins_maxs_enlargement_2(): def test_norm_values_RGB(): """Test normalize_values for RGB.""" data = np.array([[0, 255, 255], [255, 255, 0], [0, 255, 0]]) - pxl_range = np.array([0, 255]) + pixel_range = np.array([0, 255]) assert np.array_equal( - normalize_values(data, pxl_range), + normalize_values(data, pixel_range), np.array([[0, 1, 1], [1, 1, 0], [0, 1, 0]]), ) def test_norm_values_Gray(): """Test normalize_values for Float-values.""" - pxl_range = np.array([-100, 100]) + pixel_range = np.array([-100, 100]) data = np.array([100, 50, 0, -50, -100]) assert np.array_equal( - normalize_values(data, pxl_range), + normalize_values(data, pixel_range), np.array([1.0, 0.75, 0.5, 0.25, 0.0]), ) From 6d7cddb00119faa1f2ac60a274cf3dabec945cd5 Mon Sep 17 00:00:00 2001 From: Laura Engelhardt Date: Mon, 28 Jul 2025 22:30:04 +0200 Subject: [PATCH 7/7] Fix typo in png reader tests --- tests/i2pp/core/image_reader_classes/test_png_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/i2pp/core/image_reader_classes/test_png_reader.py b/tests/i2pp/core/image_reader_classes/test_png_reader.py index 0c6e582..758e10e 100644 --- a/tests/i2pp/core/image_reader_classes/test_png_reader.py +++ b/tests/i2pp/core/image_reader_classes/test_png_reader.py @@ -71,7 +71,7 @@ def test_verify_image_metadata_wrong_shape(): test_class._verify_image_metadata(test_config["image_metadata"]) -def test__extract_number(): +def test_extract_number(): """Test _extract_number method.""" test_path = Path("test_123.png")