From 4d141c8edcb9d68e9ce2f1c69794f09950e916bc Mon Sep 17 00:00:00 2001 From: Qin Yu Date: Tue, 3 Dec 2024 04:09:39 +0100 Subject: [PATCH] feat(math): task and widget for image pair operations --- .../functionals/dataprocessing/__init__.py | 2 + .../dataprocessing/dataprocessing.py | 5 +- plantseg/tasks/dataprocessing_tasks.py | 34 ++++++++++ plantseg/viewer_napari/containers.py | 2 + plantseg/viewer_napari/widgets/__init__.py | 2 + .../viewer_napari/widgets/dataprocessing.py | 65 ++++++++++++++++++- 6 files changed, 108 insertions(+), 2 deletions(-) diff --git a/plantseg/functionals/dataprocessing/__init__.py b/plantseg/functionals/dataprocessing/__init__.py index 8ec14a48..87210978 100644 --- a/plantseg/functionals/dataprocessing/__init__.py +++ b/plantseg/functionals/dataprocessing/__init__.py @@ -3,6 +3,7 @@ remove_false_positives_by_foreground_probability, ) from plantseg.functionals.dataprocessing.dataprocessing import ( + ImagePairOperation, add_images, compute_scaling_factor, compute_scaling_voxelsize, @@ -52,6 +53,7 @@ "fix_layout_to_YX", "fix_layout", # simple image operations + "ImagePairOperation", "process_images", "max_images", "add_images", diff --git a/plantseg/functionals/dataprocessing/dataprocessing.py b/plantseg/functionals/dataprocessing/dataprocessing.py index d461f593..a012faec 100644 --- a/plantseg/functionals/dataprocessing/dataprocessing.py +++ b/plantseg/functionals/dataprocessing/dataprocessing.py @@ -355,10 +355,13 @@ def normalize_01_channel_wise(data: np.ndarray, channel_axis: int = 0, eps=1e-12 return np.moveaxis(normalized_channels, 0, channel_axis) +ImagePairOperation = Literal["add", "multiply", "subtract", "divide", "max"] + + def process_images( image1: np.ndarray, image2: np.ndarray, - operation: str, + operation: ImagePairOperation, normalize_input: bool = False, clip_output: bool = False, normalize_output: bool = True, diff --git a/plantseg/tasks/dataprocessing_tasks.py b/plantseg/tasks/dataprocessing_tasks.py index e710305f..d65c7130 100644 --- a/plantseg/tasks/dataprocessing_tasks.py +++ b/plantseg/tasks/dataprocessing_tasks.py @@ -2,9 +2,11 @@ from plantseg.core.image import ImageDimensionality, ImageLayout, PlantSegImage, SemanticType from plantseg.functionals.dataprocessing import ( + ImagePairOperation, fix_over_under_segmentation_from_nuclei, image_gaussian_smoothing, image_rescale, + process_images, relabel_segmentation, remove_false_positives_by_foreground_probability, set_biggest_instance_to_zero, @@ -306,3 +308,35 @@ def relabel_segmentation_task(image: PlantSegImage, background: int | None = Non new_data = relabel_segmentation(data, background=background) new_image = image.derive_new(new_data, name=f"{image.name}_relabeled") return new_image + + +@task_tracker +def image_pair_operation_task( + image1: PlantSegImage, + image2: PlantSegImage, + operation: ImagePairOperation, + normalize_input: bool = False, + clip_output: bool = False, + normalize_output: bool = False, +) -> PlantSegImage: + """ + Task to perform an operation on two images. + + Args: + image1 (PlantSegImage): First image to process. + Image2 (PlantSegImage): Second image to process. + operation (str): Operation to perform on the images. + + Returns: + PlantSegImage: New image resulting from the operation. + """ + result = process_images( + image1.get_data(), + image2.get_data(), + operation=operation, + normalize_input=normalize_input, + clip_output=clip_output, + normalize_output=normalize_output, + ) + new_image = image1.derive_new(result, name=f"{image1.name}_{operation}_{image2.name}") + return new_image diff --git a/plantseg/viewer_napari/containers.py b/plantseg/viewer_napari/containers.py index 97b55bde..bc342236 100644 --- a/plantseg/viewer_napari/containers.py +++ b/plantseg/viewer_napari/containers.py @@ -13,6 +13,7 @@ widget_filter_segmentation, widget_fix_over_under_segmentation_from_nuclei, widget_gaussian_smoothing, + widget_image_pair_operations, widget_infos, widget_open_file, widget_proofreading_initialisation, @@ -55,6 +56,7 @@ def get_preprocessing_tab(): widget_gaussian_smoothing, widget_rescaling, widget_cropping, + widget_image_pair_operations, ], labels=False, ) diff --git a/plantseg/viewer_napari/widgets/__init__.py b/plantseg/viewer_napari/widgets/__init__.py index 95a888c3..4b1c4cd3 100644 --- a/plantseg/viewer_napari/widgets/__init__.py +++ b/plantseg/viewer_napari/widgets/__init__.py @@ -2,6 +2,7 @@ widget_cropping, widget_fix_over_under_segmentation_from_nuclei, widget_gaussian_smoothing, + widget_image_pair_operations, widget_relabel, widget_remove_false_positives_by_foreground, widget_rescaling, @@ -40,6 +41,7 @@ "widget_gaussian_smoothing", "widget_rescaling", "widget_cropping", + "widget_image_pair_operations", # IO "widget_open_file", "widget_export_image", diff --git a/plantseg/viewer_napari/widgets/dataprocessing.py b/plantseg/viewer_napari/widgets/dataprocessing.py index 1d6eba14..473c1403 100644 --- a/plantseg/viewer_napari/widgets/dataprocessing.py +++ b/plantseg/viewer_napari/widgets/dataprocessing.py @@ -7,9 +7,11 @@ from plantseg.core.zoo import model_zoo from plantseg.io.voxelsize import VoxelSize from plantseg.tasks.dataprocessing_tasks import ( + ImagePairOperation, fix_over_under_segmentation_from_nuclei_task, gaussian_smoothing_task, image_cropping_task, + image_pair_operation_task, image_rescale_to_shape_task, image_rescale_to_voxel_size_task, relabel_segmentation_task, @@ -114,7 +116,7 @@ def widget_cropping( ps_image = PlantSegImage.from_napari_layer(image) - widgets_to_update = [] + widgets_to_update = None return schedule_task( image_cropping_task, @@ -632,3 +634,64 @@ def widget_set_biggest_instance_to_zero( }, widgets_to_update=widgets_to_update, ) + + +######################################################################################################################## +# # +# Image Pair Operation Widget # +# # +######################################################################################################################## + + +@magicgui( + call_button="Run Operation", + image1={ + "label": "Image 1", + "tooltip": "First image to apply the operation.", + }, + image2={ + "label": "Image 2", + "tooltip": "Second image to apply the operation.", + }, + operation={ + "label": "Operation", + "choices": ImagePairOperation, + }, + normalize_input={ + "label": "Normalize input", + "tooltip": "Normalize the input images to the range [0, 1].", + }, + clip_output={ + "label": "Clip output", + "tooltip": "Clip the output to the range [0, 1].", + }, + normalize_output={ + "label": "Normalize output", + "tooltip": "Normalize the output image to the range [0, 1].", + }, +) +def widget_image_pair_operations( + image1: Image, + image2: Image, + operation: ImagePairOperation = "add", + normalize_input: bool = False, + clip_output: bool = False, + normalize_output: bool = False, +) -> None: + """Apply an operation to two image layers.""" + + ps_image1 = PlantSegImage.from_napari_layer(image1) + ps_image2 = PlantSegImage.from_napari_layer(image2) + + return schedule_task( + image_pair_operation_task, + task_kwargs={ + "image1": ps_image1, + "image2": ps_image2, + "operation": operation, + "normalize_input": normalize_input, + "clip_output": clip_output, + "normalize_output": normalize_output, + }, + widgets_to_update=[], + )