From 604728434a1e4d96bac177d9fe0bc95eca5a6014 Mon Sep 17 00:00:00 2001 From: jluethi Date: Wed, 31 Jan 2024 22:02:32 +0100 Subject: [PATCH 01/25] Add Cellpose options --- .../tasks/cellpose_segmentation.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/fractal_tasks_core/tasks/cellpose_segmentation.py b/fractal_tasks_core/tasks/cellpose_segmentation.py index 562e1bc57..d1d0a0051 100644 --- a/fractal_tasks_core/tasks/cellpose_segmentation.py +++ b/fractal_tasks_core/tasks/cellpose_segmentation.py @@ -70,10 +70,18 @@ def segment_ROI( diameter: float = 30.0, cellprob_threshold: float = 0.0, flow_threshold: float = 0.4, + normalize: bool = True, label_dtype: Optional[np.dtype] = None, augment: bool = False, net_avg: bool = False, min_size: int = 15, + batch_size: int = 8, + invert: bool = False, + tile: bool = True, + tile_overlap: float = 0.1, + resample: bool = True, + interp: bool = True, + stitch_threshold: float = 0.0, ) -> np.ndarray: """ Internal function that runs Cellpose segmentation for a single ROI. @@ -92,6 +100,10 @@ def segment_ROI( diameter: Expected object diameter in pixels for cellpose. cellprob_threshold: Cellpose model parameter. flow_threshold: Cellpose model parameter. + normalize: normalize data so 0.0=1st percentile and 1.0=99th + percentile of image intensities in each channel. This automatic + normalization can lead to issues when the image to be segmented + is very sparse. label_dtype: Label images are cast into this `np.dtype`. augment: Whether to use cellpose augmentation to tile images with overlap. @@ -99,6 +111,18 @@ def segment_ROI( networks (useful for `nuclei`, `cyto` and `cyto2`, not sure it works for the others). min_size: Minimum size of the segmented objects. + batch_size: number of 224x224 patches to run simultaneously on the GPU + (can make smaller or bigger depending on GPU memory usage) + invert: invert image pixel intensity before running network (if True, + image is also normalized) + tile: tiles image to ensure GPU/CPU memory usage limited (recommended) + tile_overlap: fraction of overlap of tiles when computing flows + resample: run dynamics at original image size (will be slower but + create more accurate boundaries) + interp: interpolate during 2D dynamics (not available in 3D) + (in previous versions it was False, now it defaults to True) + stitch_threshold: if stitch_threshold>0.0 and not do_3D and equal + image sizes, masks are stitched in 3D to return volume segmentation """ # Write some debugging info @@ -123,7 +147,15 @@ def segment_ROI( anisotropy=anisotropy, cellprob_threshold=cellprob_threshold, flow_threshold=flow_threshold, + normalize=normalize, min_size=min_size, + batch_size=batch_size, + invert=invert, + tile=tile, + tile_overlap=tile_overlap, + resample=resample, + interp=interp, + stitch_threshold=stitch_threshold, ) if mask.ndim == 2: @@ -169,11 +201,20 @@ def cellpose_segmentation( pretrained_model: Optional[str] = None, cellprob_threshold: float = 0.0, flow_threshold: float = 0.4, + normalize: bool = True, anisotropy: Optional[float] = None, min_size: int = 15, augment: bool = False, net_avg: bool = False, use_gpu: bool = True, + batch_size: int = 8, + invert: bool = False, + tile: bool = True, + tile_overlap: float = 0.1, + resample: bool = True, + interp: bool = True, + stitch_threshold: float = 0.0, + # Overwrite option overwrite: bool = True, ) -> dict[str, Any]: """ @@ -235,6 +276,10 @@ def cellpose_segmentation( this threshold if cellpose is not returning as many ROIs as you’d expect. Similarly, decrease this threshold if cellpose is returning too many ill-shaped ROIs." + normalize: normalize data so 0.0=1st percentile and 1.0=99th + percentile of image intensities in each channel. This automatic + normalization can lead to issues when the image to be segmented + is very sparse. anisotropy: Ratio of the pixel sizes along Z and XY axis (ignored if the image is not three-dimensional). If `None`, it is inferred from the OME-NGFF metadata. @@ -249,6 +294,18 @@ def cellpose_segmentation( use_gpu: If `False`, always use the CPU; if `True`, use the GPU if possible (as defined in `cellpose.core.use_gpu()`) and fall-back to the CPU otherwise. + batch_size: number of 224x224 patches to run simultaneously on the GPU + (can make smaller or bigger depending on GPU memory usage) + invert: invert image pixel intensity before running network (if True, + image is also normalized) + tile: tiles image to ensure GPU/CPU memory usage limited (recommended) + tile_overlap: fraction of overlap of tiles when computing flows + resample: run dynamics at original image size (will be slower but + create more accurate boundaries) + interp: interpolate during 2D dynamics (not available in 3D) + (in previous versions it was False, now it defaults to True) + stitch_threshold: if stitch_threshold>0.0 and not do_3D and equal + image sizes, masks are stitched in 3D to return volume segmentation overwrite: If `True`, overwrite the task output. """ @@ -529,9 +586,17 @@ def cellpose_segmentation( diameter=diameter_level0 / coarsening_xy**level, cellprob_threshold=cellprob_threshold, flow_threshold=flow_threshold, + normalize=normalize, min_size=min_size, augment=augment, net_avg=net_avg, + batch_size=batch_size, + invert=invert, + tile=tile, + tile_overlap=tile_overlap, + resample=resample, + interp=interp, + stitch_threshold=stitch_threshold, ) # Prepare keyword arguments for preprocessing function From 3ce46eb501a9ebe3fdcf9fc940887c5d094fe25e Mon Sep 17 00:00:00 2001 From: jluethi Date: Wed, 31 Jan 2024 22:26:49 +0100 Subject: [PATCH 02/25] Switch to using Enums for model_type in Cellpose task --- fractal_tasks_core/tasks/cellpose_segmentation.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/fractal_tasks_core/tasks/cellpose_segmentation.py b/fractal_tasks_core/tasks/cellpose_segmentation.py index d1d0a0051..132d69660 100644 --- a/fractal_tasks_core/tasks/cellpose_segmentation.py +++ b/fractal_tasks_core/tasks/cellpose_segmentation.py @@ -18,6 +18,7 @@ import time from pathlib import Path from typing import Any +from typing import Literal from typing import Optional from typing import Sequence @@ -197,7 +198,8 @@ def cellpose_segmentation( relabeling: bool = True, # Cellpose-related arguments diameter_level0: float = 30.0, - model_type: str = "cyto2", + # https://github.com/fractal-analytics-platform/fractal-tasks-core/issues/401 # noqa E501 + model_type: Literal[tuple(models.MODEL_NAMES)] = "cyto2", pretrained_model: Optional[str] = None, cellprob_threshold: float = 0.0, flow_threshold: float = 0.4, @@ -317,12 +319,8 @@ def cellpose_segmentation( logger.info(f"{zarrurl=}") # Preliminary checks on Cellpose model - if pretrained_model is None: - if model_type not in models.MODEL_NAMES: - raise ValueError(f"ERROR model_type={model_type} is not allowed.") - else: - if not os.path.exists(pretrained_model): - raise ValueError(f"{pretrained_model=} does not exist.") + if not os.path.exists(pretrained_model): + raise ValueError(f"{pretrained_model=} does not exist.") # Read attributes from NGFF metadata ngff_image_meta = load_NgffImageMeta(zarrurl) From 1e05b8fad4ce2faaf7318ecce6ac8c6540f167c6 Mon Sep 17 00:00:00 2001 From: jluethi Date: Wed, 31 Jan 2024 22:27:54 +0100 Subject: [PATCH 03/25] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ad3b69c7..b5b237516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ **Note**: Numbers like (\#123) point to closed Pull Requests on the fractal-tasks-core repository. +# 0.14.2 +* Add more options to the Cellpose task to control model behavior (#650) +* For Cellpose task, switch to using Enums for model_type (see issue #401) + # 0.14.1 * Fix bug in `cellpose_segmentation` upon using masked loading and setting `channel2` (\#639). Thanks [@FranziskaMoos-FMI](https://github.com/FranziskaMoos-FMI) and [@enricotagliavini](https://github.com/enricotagliavini). From 12064acb8cf7e31e9fbf076f6a3368f55043d850 Mon Sep 17 00:00:00 2001 From: jluethi Date: Wed, 31 Jan 2024 22:34:04 +0100 Subject: [PATCH 04/25] Update manifest --- fractal_tasks_core/__FRACTAL_MANIFEST__.json | 65 ++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json index 6428adb2f..b7eb00ffe 100644 --- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json +++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json @@ -439,6 +439,23 @@ "model_type": { "title": "Model Type", "default": "cyto2", + "enum": [ + "cyto", + "nuclei", + "tissuenet", + "livecell", + "cyto2", + "general", + "CP", + "CPx", + "TN1", + "TN2", + "TN3", + "LC1", + "LC2", + "LC3", + "LC4" + ], "type": "string", "description": "Parameter of `CellposeModel` class. Defines which model should be used. Typical choices are `nuclei`, `cyto`, `cyto2`, etc." }, @@ -459,6 +476,12 @@ "type": "number", "description": "Parameter of `CellposeModel.eval` method. Valid values between 0.0 and 1.0. From Cellpose documentation: \"Increase this threshold if cellpose is not returning as many ROIs as you\u2019d expect. Similarly, decrease this threshold if cellpose is returning too many ill-shaped ROIs.\"" }, + "normalize": { + "title": "Normalize", + "default": true, + "type": "boolean", + "description": "normalize data so 0.0=1st percentile and 1.0=99th percentile of image intensities in each channel. This automatic normalization can lead to issues when the image to be segmented is very sparse." + }, "anisotropy": { "title": "Anisotropy", "type": "number", @@ -488,6 +511,48 @@ "type": "boolean", "description": "If `False`, always use the CPU; if `True`, use the GPU if possible (as defined in `cellpose.core.use_gpu()`) and fall-back to the CPU otherwise." }, + "batch_size": { + "title": "Batch Size", + "default": 8, + "type": "integer", + "description": "number of 224x224 patches to run simultaneously on the GPU (can make smaller or bigger depending on GPU memory usage)" + }, + "invert": { + "title": "Invert", + "default": false, + "type": "boolean", + "description": "invert image pixel intensity before running network (if True, image is also normalized)" + }, + "tile": { + "title": "Tile", + "default": true, + "type": "boolean", + "description": "tiles image to ensure GPU/CPU memory usage limited (recommended)" + }, + "tile_overlap": { + "title": "Tile Overlap", + "default": 0.1, + "type": "number", + "description": "fraction of overlap of tiles when computing flows" + }, + "resample": { + "title": "Resample", + "default": true, + "type": "boolean", + "description": "run dynamics at original image size (will be slower but create more accurate boundaries)" + }, + "interp": { + "title": "Interp", + "default": true, + "type": "boolean", + "description": "interpolate during 2D dynamics (not available in 3D) (in previous versions it was False, now it defaults to True)" + }, + "stitch_threshold": { + "title": "Stitch Threshold", + "default": 0.0, + "type": "number", + "description": "if stitch_threshold>0.0 and not do_3D and equal image sizes, masks are stitched in 3D to return volume segmentation" + }, "overwrite": { "title": "Overwrite", "default": true, From 281f9b2f02d5da63b4aaf7319a92330cd87b1a84 Mon Sep 17 00:00:00 2001 From: jluethi Date: Wed, 31 Jan 2024 22:36:48 +0100 Subject: [PATCH 05/25] bump version to 0.14.2a0 --- fractal_tasks_core/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fractal_tasks_core/__init__.py b/fractal_tasks_core/__init__.py index 07951b71c..19125a3c8 100644 --- a/fractal_tasks_core/__init__.py +++ b/fractal_tasks_core/__init__.py @@ -6,6 +6,6 @@ ) -__VERSION__ = "0.14.1" +__VERSION__ = "0.14.2a0" __OME_NGFF_VERSION__ = "0.4" __FRACTAL_TABLE_VERSION__ = "1" diff --git a/pyproject.toml b/pyproject.toml index dc34dadea..446310a9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fractal-tasks-core" -version = "0.14.1" +version = "0.14.2a0" description = "Core bioimage-analysis library and tasks of the Fractal analytics platform" authors = [ "Joel Lüthi ", @@ -89,7 +89,7 @@ source = ["fractal_tasks_core"] omit = ["tests/*", "examples/*", "fractal_tasks_core/dev/*", "fractal_tasks_core/tasks/compress_tif.py"] [tool.bumpver] -current_version = "0.14.1" +current_version = "0.14.2a0" version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]" commit_message = "bump version {old_version} -> {new_version}" commit = true From 33e04727a3b74b8a523a726508e59f205e2e8b5b Mon Sep 17 00:00:00 2001 From: jluethi Date: Wed, 31 Jan 2024 22:42:24 +0100 Subject: [PATCH 06/25] Fix check in cellpose task pretrained_model path validation --- fractal_tasks_core/tasks/cellpose_segmentation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fractal_tasks_core/tasks/cellpose_segmentation.py b/fractal_tasks_core/tasks/cellpose_segmentation.py index 132d69660..26b8384ff 100644 --- a/fractal_tasks_core/tasks/cellpose_segmentation.py +++ b/fractal_tasks_core/tasks/cellpose_segmentation.py @@ -319,8 +319,9 @@ def cellpose_segmentation( logger.info(f"{zarrurl=}") # Preliminary checks on Cellpose model - if not os.path.exists(pretrained_model): - raise ValueError(f"{pretrained_model=} does not exist.") + if pretrained_model: + if not os.path.exists(pretrained_model): + raise ValueError(f"{pretrained_model=} does not exist.") # Read attributes from NGFF metadata ngff_image_meta = load_NgffImageMeta(zarrurl) From 38338dab957f770da886c8ce56ebdd5d9f0109a0 Mon Sep 17 00:00:00 2001 From: jluethi Date: Wed, 31 Jan 2024 22:59:41 +0100 Subject: [PATCH 07/25] Update tests for cellpose task with new model_type input validation --- tests/tasks/test_workflows_cellpose_segmentation.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/tasks/test_workflows_cellpose_segmentation.py b/tests/tasks/test_workflows_cellpose_segmentation.py index 10aedf45a..6f7145c08 100644 --- a/tests/tasks/test_workflows_cellpose_segmentation.py +++ b/tests/tasks/test_workflows_cellpose_segmentation.py @@ -690,7 +690,15 @@ def test_workflow_with_per_FOV_labeling_via_script( res = subprocess.run(shlex.split(command), **run_options) # type: ignore print(res.stdout) print(res.stderr) - error_msg = f"ERROR model_type={INVALID_MODEL_TYPE} is not allowed" + error_msg = ( + "unexpected value; permitted: 'cyto', 'nuclei', " + "'tissuenet', 'livecell', 'cyto2', 'general', 'CP', 'CPx', " + "'TN1', 'TN2', 'TN3', 'LC1', 'LC2', 'LC3', 'LC4' " + f"(type=value_error.const; given={INVALID_MODEL_TYPE}; " + "permitted=('cyto', 'nuclei', 'tissuenet', 'livecell', " + "'cyto2', 'general', 'CP', 'CPx', 'TN1', 'TN2', 'TN3', " + "'LC1', 'LC2', 'LC3', 'LC4'))" + ) assert error_msg in res.stderr assert "urllib.error.HTTPError" not in res.stdout assert "urllib.error.HTTPError" not in res.stderr From e6c0b650a5af146c9912008127d3438d2515e3b0 Mon Sep 17 00:00:00 2001 From: jluethi Date: Wed, 31 Jan 2024 23:01:56 +0100 Subject: [PATCH 08/25] Add a comment on potential test failure due to cellpose updates to cellpose tests --- tests/tasks/test_workflows_cellpose_segmentation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/tasks/test_workflows_cellpose_segmentation.py b/tests/tasks/test_workflows_cellpose_segmentation.py index 6f7145c08..48daba1ab 100644 --- a/tests/tasks/test_workflows_cellpose_segmentation.py +++ b/tests/tasks/test_workflows_cellpose_segmentation.py @@ -690,6 +690,9 @@ def test_workflow_with_per_FOV_labeling_via_script( res = subprocess.run(shlex.split(command), **run_options) # type: ignore print(res.stdout) print(res.stderr) + # If this check fails after updating the cellpose version, you'll likely + # need to update the manifest to include a changed set of available models + # See https://github.com/fractal-analytics-platform/fractal-tasks-core/issues/401 # noqa E501 error_msg = ( "unexpected value; permitted: 'cyto', 'nuclei', " "'tissuenet', 'livecell', 'cyto2', 'general', 'CP', 'CPx', " From 7ebfd8591137f4c9ac0d6f198c981f1d2584ad6d Mon Sep 17 00:00:00 2001 From: jluethi Date: Wed, 31 Jan 2024 23:08:41 +0100 Subject: [PATCH 09/25] Bump version to 0.14.2a1 --- fractal_tasks_core/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fractal_tasks_core/__init__.py b/fractal_tasks_core/__init__.py index 19125a3c8..ca48d312f 100644 --- a/fractal_tasks_core/__init__.py +++ b/fractal_tasks_core/__init__.py @@ -6,6 +6,6 @@ ) -__VERSION__ = "0.14.2a0" +__VERSION__ = "0.14.2a1" __OME_NGFF_VERSION__ = "0.4" __FRACTAL_TABLE_VERSION__ = "1" diff --git a/pyproject.toml b/pyproject.toml index 446310a9b..fb67a9c4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fractal-tasks-core" -version = "0.14.2a0" +version = "0.14.2a1" description = "Core bioimage-analysis library and tasks of the Fractal analytics platform" authors = [ "Joel Lüthi ", @@ -89,7 +89,7 @@ source = ["fractal_tasks_core"] omit = ["tests/*", "examples/*", "fractal_tasks_core/dev/*", "fractal_tasks_core/tasks/compress_tif.py"] [tool.bumpver] -current_version = "0.14.2a0" +current_version = "0.14.2a1" version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]" commit_message = "bump version {old_version} -> {new_version}" commit = true From ec12e90da3a16ea63f7b9c82f8213b8c20003d43 Mon Sep 17 00:00:00 2001 From: jluethi Date: Tue, 6 Feb 2024 21:05:43 +0100 Subject: [PATCH 10/25] Add a CellposeCustomNormalizer model and implement custom image normalization for the Cellpose task --- .../fetch_test_data_from_zenodo.sh | 5 + .../run_example_cellpose_normalization.py | 139 ++++++++++++ fractal_tasks_core/__FRACTAL_MANIFEST__.json | 44 +++- .../tasks/cellpose_segmentation.py | 35 ++- .../tasks/cellpose_transforms.py | 209 ++++++++++++++++++ 5 files changed, 422 insertions(+), 10 deletions(-) create mode 100755 examples/06_cellpose_dev_example/fetch_test_data_from_zenodo.sh create mode 100644 examples/06_cellpose_dev_example/run_example_cellpose_normalization.py create mode 100644 fractal_tasks_core/tasks/cellpose_transforms.py diff --git a/examples/06_cellpose_dev_example/fetch_test_data_from_zenodo.sh b/examples/06_cellpose_dev_example/fetch_test_data_from_zenodo.sh new file mode 100755 index 000000000..fcda5db76 --- /dev/null +++ b/examples/06_cellpose_dev_example/fetch_test_data_from_zenodo.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +DOI="10.5281/zenodo.7059515" +CLEAN_DOI=${DOI/\//_} +zenodo_get $DOI -o ../images/${CLEAN_DOI} diff --git a/examples/06_cellpose_dev_example/run_example_cellpose_normalization.py b/examples/06_cellpose_dev_example/run_example_cellpose_normalization.py new file mode 100644 index 000000000..092f48e6e --- /dev/null +++ b/examples/06_cellpose_dev_example/run_example_cellpose_normalization.py @@ -0,0 +1,139 @@ +""" +Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and +University of Zurich + +Original authors: +Marco Franzon +Tommaso Comparin + +This file is part of Fractal and was originally developed by eXact lab S.r.l. + under contract with Liberali Lab from the Friedrich Miescher +Institute for Biomedical Research and Pelkmans Lab from the University of +Zurich. +""" +import os + +from devtools import debug + +from fractal_tasks_core.channels import OmeroChannel +from fractal_tasks_core.channels import Window +from fractal_tasks_core.tasks.cellpose_segmentation import ( + cellpose_segmentation, +) +from fractal_tasks_core.tasks.cellpose_transforms import ( + CellposeCustomNormalizer, +) +from fractal_tasks_core.tasks.copy_ome_zarr import copy_ome_zarr +from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr +from fractal_tasks_core.tasks.maximum_intensity_projection import ( + maximum_intensity_projection, +) +from fractal_tasks_core.tasks.yokogawa_to_ome_zarr import yokogawa_to_ome_zarr + +allowed_channels = [ + OmeroChannel( + label="DAPI", + wavelength_id="A01_C01", + color="00FFFF", + window=Window(start=110, end=700), + ) +] + + +num_levels = 6 +coarsening_xy = 2 + + +# Init +img_path = "../images/10.5281_zenodo.7059515/" +if not os.path.isdir(img_path): + raise FileNotFoundError( + f"{img_path} is missing," + " try running ./fetch_test_data_from_zenodo.sh" + ) +zarr_path = "tmp_out_normalize/" +metadata: dict = {} + +# Create zarr structure +metadata_update = create_ome_zarr( + input_paths=[img_path], + output_path=zarr_path, + metadata=metadata, + image_extension="png", + allowed_channels=allowed_channels, + num_levels=num_levels, + coarsening_xy=coarsening_xy, + overwrite=True, +) +metadata.update(metadata_update) +debug(metadata) + +# Yokogawa to zarr +for component in metadata["image"]: + yokogawa_to_ome_zarr( + input_paths=[zarr_path], + output_path=zarr_path, + metadata=metadata, + component=component, + ) +debug(metadata) + + +# Copy zarr structure +metadata_update = copy_ome_zarr( + input_paths=[zarr_path], + output_path=zarr_path, + metadata=metadata, + suffix="mip", + overwrite=True, +) +metadata.update(metadata_update) + +# Make MIP +for component in metadata["image"]: + metadata_update = maximum_intensity_projection( + input_paths=[zarr_path], + output_path=zarr_path, + metadata=metadata, + component=component, + ) + metadata.update(metadata_update) + + +normalize = CellposeCustomNormalizer(default_normalize=True) +normalize = CellposeCustomNormalizer( + default_normalize=False, lower_percentile=1.0, upper_percentile=99.0 +) +# normalize=CellposeCustomNormalizer( +# default_normalize=True, +# lower_percentile=1.0, +# upper_percentile=99.0 +# ) # ValueError +# normalize=CellposeCustomNormalizer( +# default_normalize=False, +# lower_percentile=1.0, +# upper_percentile=99.0, +# lower_bound=100, +# upper_bound=500 +# ) # ValueError +normalize = CellposeCustomNormalizer( + default_normalize=False, lower_bound=100, upper_bound=500 +) + + +# Per-FOV labeling +for component in metadata["image"]: + cellpose_segmentation( + input_paths=[zarr_path], + output_path=zarr_path, + metadata=metadata, + component=component, + channel=dict(wavelength_id="A01_C01"), + level=1, + relabeling=True, + diameter_level0=40.0, + model_type="cyto2", + normalize=normalize, + ) + metadata.update(metadata_update) +debug(metadata) diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json index b7eb00ffe..6860c8b9c 100644 --- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json +++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json @@ -478,9 +478,19 @@ }, "normalize": { "title": "Normalize", - "default": true, - "type": "boolean", - "description": "normalize data so 0.0=1st percentile and 1.0=99th percentile of image intensities in each channel. This automatic normalization can lead to issues when the image to be segmented is very sparse." + "default": { + "default_normalize": true, + "lower_percentile": null, + "upper_percentile": null, + "lower_bound": null, + "upper_bound": null + }, + "allOf": [ + { + "$ref": "#/definitions/CellposeCustomNormalizer" + } + ], + "description": "By default, data is normalized so 0.0=1st percentile and 1.0=99th percentile of image intensities in each channel. This automatic normalization can lead to issues when the image to be segmented is very sparse. You can turn off the automated rescaling and either provide your own rescaling percentiles or fixed rescaling upper and lower bound integers" }, "anisotropy": { "title": "Anisotropy", @@ -586,6 +596,34 @@ "description": "Name of the channel." } } + }, + "CellposeCustomNormalizer": { + "title": "CellposeCustomNormalizer", + "description": "Validator to handle different normalization scenarios for Cellpose models", + "type": "object", + "properties": { + "default_normalize": { + "title": "Default Normalize", + "default": true, + "type": "boolean" + }, + "lower_percentile": { + "title": "Lower Percentile", + "type": "number" + }, + "upper_percentile": { + "title": "Upper Percentile", + "type": "number" + }, + "lower_bound": { + "title": "Lower Bound", + "type": "integer" + }, + "upper_bound": { + "title": "Upper Bound", + "type": "integer" + } + } } } }, diff --git a/fractal_tasks_core/tasks/cellpose_segmentation.py b/fractal_tasks_core/tasks/cellpose_segmentation.py index 26b8384ff..20a114cdb 100644 --- a/fractal_tasks_core/tasks/cellpose_segmentation.py +++ b/fractal_tasks_core/tasks/cellpose_segmentation.py @@ -55,6 +55,10 @@ from fractal_tasks_core.roi import is_ROI_table_valid from fractal_tasks_core.roi import load_region from fractal_tasks_core.tables import write_table +from fractal_tasks_core.tasks.cellpose_transforms import ( + CellposeCustomNormalizer, +) +from fractal_tasks_core.tasks.cellpose_transforms import normalized_img from fractal_tasks_core.utils import rescale_datasets logger = logging.getLogger(__name__) @@ -71,7 +75,9 @@ def segment_ROI( diameter: float = 30.0, cellprob_threshold: float = 0.0, flow_threshold: float = 0.4, - normalize: bool = True, + normalize: CellposeCustomNormalizer = CellposeCustomNormalizer( + default_normalize=True + ), label_dtype: Optional[np.dtype] = None, augment: bool = False, net_avg: bool = False, @@ -133,9 +139,20 @@ def segment_ROI( f" {do_3D=} |" f" {model.diam_mean=} |" f" {diameter=} |" - f" {flow_threshold=}" + f" {flow_threshold=} |" + f" {normalize.default_normalize=}" ) + # Optionally perform custom normalization + if not normalize.default_normalize: + x = normalized_img( + x, + lower_p=normalize.lower_percentile, + upper_p=normalize.upper_percentile, + lower_bound=normalize.lower_bound, + upper_bound=normalize.upper_bound, + ) + # Actual labeling t0 = time.perf_counter() mask, _, _ = model.eval( @@ -203,7 +220,9 @@ def cellpose_segmentation( pretrained_model: Optional[str] = None, cellprob_threshold: float = 0.0, flow_threshold: float = 0.4, - normalize: bool = True, + normalize: CellposeCustomNormalizer = CellposeCustomNormalizer( + default_normalize=True + ), anisotropy: Optional[float] = None, min_size: int = 15, augment: bool = False, @@ -278,10 +297,12 @@ def cellpose_segmentation( this threshold if cellpose is not returning as many ROIs as you’d expect. Similarly, decrease this threshold if cellpose is returning too many ill-shaped ROIs." - normalize: normalize data so 0.0=1st percentile and 1.0=99th - percentile of image intensities in each channel. This automatic - normalization can lead to issues when the image to be segmented - is very sparse. + normalize: By default, data is normalized so 0.0=1st percentile and + 1.0=99th percentile of image intensities in each channel. + This automatic normalization can lead to issues when the image to + be segmented is very sparse. You can turn off the automated + rescaling and either provide your own rescaling percentiles or + fixed rescaling upper and lower bound integers anisotropy: Ratio of the pixel sizes along Z and XY axis (ignored if the image is not three-dimensional). If `None`, it is inferred from the OME-NGFF metadata. diff --git a/fractal_tasks_core/tasks/cellpose_transforms.py b/fractal_tasks_core/tasks/cellpose_transforms.py new file mode 100644 index 000000000..d088f980b --- /dev/null +++ b/fractal_tasks_core/tasks/cellpose_transforms.py @@ -0,0 +1,209 @@ +# Copyright 2023 (C) Friedrich Miescher Institute for Biomedical Research and +# University of Zurich +# +# Original authors: +# Joel Lüthi +# +# This file is part of Fractal and was originally developed by eXact lab S.r.l. +# under contract with Liberali Lab from the Friedrich Miescher +# Institute for Biomedical Research and Pelkmans Lab from the University of +# Zurich. +""" +Helper functions for image normalization in +""" +import logging +from typing import Optional + +import numpy as np +from pydantic import BaseModel +from pydantic import root_validator + + +logger = logging.getLogger(__name__) + + +class CellposeCustomNormalizer(BaseModel): + """ + Validator to handle different normalization scenarios for Cellpose models + + If default normalization is to be used, no other parameters can be + specified. Alternatively, when default normalization is not applied, + either percentiles or explicit integer bounds can be applied. + + Attributes: + default_normalize: Whether to use the default Cellpose normalization + approach (rescaling the image between the 1st and 99th percentile) + lower_percentile: Specify a custom lower-bound percentile for rescaling + as a float value between 0 and 100. Set to 1 to run the same as + default). You can only specify percentils or bounds, not both. + upper_percentile: Specify a custom upper-bound percentile for rescaling + as a float value between 0 and 100. Set to 99 to run the same as + default, set to e.g. 99.99 if the default rescaling was too harsh. + You can only specify percentils or bounds, not both. + lower_bound: Explicit lower bound value to rescale the image at. + Needs to be an integer, e.g. 100. + You can only specify percentils or bounds, not both. + upper_bound: Explicit upper bound value to rescale the image at. + Needs to be an integer, e.g. 2000 + You can only specify percentils or bounds, not both. + + """ + + default_normalize: bool = True + lower_percentile: Optional[float] = None + upper_percentile: Optional[float] = None + lower_bound: Optional[int] = None + upper_bound: Optional[int] = None + + # In the future, add an option to allow using precomputed percentiles + # that are stored in OME-Zarr histograms and use this pydantic model that + # those histograms actually exist + + @root_validator + def validate_conditions(cls, values): + default_normalize = values.get("default_normalize") + lower_percentile = values.get("lower_percentile") + upper_percentile = values.get("upper_percentile") + lower_bound = values.get("lower_bound") + upper_bound = values.get("upper_bound") + + # If default_normalize is True, check that all other fields are None + if default_normalize: + if any( + v is not None + for v in [ + lower_percentile, + upper_percentile, + lower_bound, + upper_bound, + ] + ): + raise ValueError( + "When default_normalize is True, no percentile or " + "bounds can be specified" + ) + + # Check for lower_percentile and upper_percentile condition + if lower_percentile is not None or upper_percentile is not None: + if lower_percentile is None or upper_percentile is None: + raise ValueError( + "Both lower_percentile and upper_percentile must be set " + "together" + ) + if lower_bound is not None or upper_bound is not None: + raise ValueError( + "If a percentile is specified, no hard set lower_bound " + "or upper_bound can be specified" + ) + + # If lower_bound or upper_bound is set, the other must also be set, + # and lower_percentile & upper_percentile must be None + if lower_bound is not None or upper_bound is not None: + if lower_bound is None or upper_bound is None: + raise ValueError( + "Both lower_bound and upper_bound must be set together" + ) + if lower_percentile is not None or upper_percentile is not None: + raise ValueError( + "If explicit lower and upper bounds are set, percentiles " + "cannot be specified" + ) + + return values + + +def normalized_img( + img, + axis=-1, + invert=False, + lower_p: float = 1.0, + upper_p: float = 99.0, + lower_bound=None, + upper_bound=None, +): + """normalize each channel of the image so that so that 0.0=1st percentile + and 1.0=99th percentile of image intensities + + Based on https://github.com/MouseLand/cellpose/blob/4f5661983c3787efa443bbbd3f60256f4fd8bf53/cellpose/transforms.py#L375 # noqa E501 + + optional inversion + + Parameters + ------------ + + img: ND-array (at least 3 dimensions) + + axis: channel axis to loop over for normalization + + invert: invert image (useful if cells are dark instead of bright) + + lower_p: Lower percentile for rescaling + + upper_p: Upper percentile for rescaling + + lower_bound: Lower fixed-value used for rescaling + + upper_bound: Upper fixed-value used for rescaling + + Returns + --------------- + + img: ND-array, float32 + normalized image of same size + + """ + if img.ndim < 3: + error_message = "Image needs to have at least 3 dimensions" + logger.critical(error_message) + raise ValueError(error_message) + + img = img.astype(np.float32) + img = np.moveaxis(img, axis, 0) + for k in range(img.shape[0]): + if lower_p is not None: + # ptp can still give nan's with weird images + i99 = np.percentile(img[k], upper_p) + i1 = np.percentile(img[k], lower_p) + if i99 - i1 > +1e-3: # np.ptp(img[k]) > 1e-3: + img[k] = normalize_percentile( + img[k], lower=lower_p, upper=upper_p + ) + if invert: + img[k] = -1 * img[k] + 1 + else: + img[k] = 0 + elif lower_bound is not None: + if upper_bound - lower_bound > +1e-3: + img[k] = normalize_bounds( + img[k], lower=lower_bound, upper=upper_bound + ) + if invert: + img[k] = -1 * img[k] + 1 + else: + img[k] = 0 + else: + raise ValueError("No normalization method specified") + img = np.moveaxis(img, 0, axis) + return img + + +def normalize_percentile(Y, lower: int = 1, upper: int = 99): + """normalize image so 0.0 is lower percentile and 1.0 is upper percentile + Percentiles are passed as integers + """ + X = Y.copy() + x01 = np.percentile(X, lower) + x99 = np.percentile(X, upper) + X = (X - x01) / (x99 - x01) + return X + + +def normalize_bounds(Y, lower: int = 0, upper: int = 65535): + """normalize image so 0.0 is lower percentile and 1.0 is upper percentile + Percentiles are passed as integers + """ + X = Y.copy() + X = (X - lower) / (upper - lower) + # Avoid negative values (if lower > pixel value) + X[X < 0] = 0 + return X From 15407e2f6bb7d0caa0bea1da9cc69305961d73a9 Mon Sep 17 00:00:00 2001 From: jluethi Date: Tue, 6 Feb 2024 21:07:18 +0100 Subject: [PATCH 11/25] Update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5b237516..b368ba0c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ **Note**: Numbers like (\#123) point to closed Pull Requests on the fractal-tasks-core repository. # 0.14.2 +* Add custom normalization options to the Cellpose task (#650) * Add more options to the Cellpose task to control model behavior (#650) * For Cellpose task, switch to using Enums for model_type (see issue #401) From bc8c923f6f5cb5423ca85a5328e2e627bc105b71 Mon Sep 17 00:00:00 2001 From: jluethi Date: Wed, 7 Feb 2024 17:30:01 +0100 Subject: [PATCH 12/25] Update manifest creation to include CellposeCustomNormalizer model --- fractal_tasks_core/__FRACTAL_MANIFEST__.json | 15 ++++++++++----- fractal_tasks_core/dev/lib_args_schemas.py | 5 +++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json index 6860c8b9c..fc156db09 100644 --- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json +++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json @@ -605,23 +605,28 @@ "default_normalize": { "title": "Default Normalize", "default": true, - "type": "boolean" + "type": "boolean", + "description": "Whether to use the default Cellpose normalization approach (rescaling the image between the 1st and 99th percentile)" }, "lower_percentile": { "title": "Lower Percentile", - "type": "number" + "type": "number", + "description": "Specify a custom lower-bound percentile for rescaling as a float value between 0 and 100. Set to 1 to run the same as default). You can only specify percentils or bounds, not both." }, "upper_percentile": { "title": "Upper Percentile", - "type": "number" + "type": "number", + "description": "Specify a custom upper-bound percentile for rescaling as a float value between 0 and 100. Set to 99 to run the same as default, set to e.g. 99.99 if the default rescaling was too harsh. You can only specify percentils or bounds, not both." }, "lower_bound": { "title": "Lower Bound", - "type": "integer" + "type": "integer", + "description": "Explicit lower bound value to rescale the image at. Needs to be an integer, e.g. 100. You can only specify percentils or bounds, not both." }, "upper_bound": { "title": "Upper Bound", - "type": "integer" + "type": "integer", + "description": "Explicit upper bound value to rescale the image at. Needs to be an integer, e.g. 2000 You can only specify percentils or bounds, not both." } } } diff --git a/fractal_tasks_core/dev/lib_args_schemas.py b/fractal_tasks_core/dev/lib_args_schemas.py index bad4b4231..3b98aa539 100644 --- a/fractal_tasks_core/dev/lib_args_schemas.py +++ b/fractal_tasks_core/dev/lib_args_schemas.py @@ -60,6 +60,11 @@ "tasks/napari_workflows_wrapper_models.py", "NapariWorkflowsOutput", ), + ( + "fractal_tasks_core", + "tasks/cellpose_transforms.py", + "CellposeCustomNormalizer", + ), ] From a9185ba9cbda4529260408015eb7da30d55890b8 Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 8 Feb 2024 09:51:20 +0100 Subject: [PATCH 13/25] Do not construct `CellposeCustomNormalizer` from within function-argument default value --- fractal_tasks_core/__FRACTAL_MANIFEST__.json | 13 +------------ fractal_tasks_core/tasks/cellpose_segmentation.py | 8 +++++--- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json index fc156db09..a5bc6000c 100644 --- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json +++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json @@ -477,19 +477,8 @@ "description": "Parameter of `CellposeModel.eval` method. Valid values between 0.0 and 1.0. From Cellpose documentation: \"Increase this threshold if cellpose is not returning as many ROIs as you\u2019d expect. Similarly, decrease this threshold if cellpose is returning too many ill-shaped ROIs.\"" }, "normalize": { + "$ref": "#/definitions/CellposeCustomNormalizer", "title": "Normalize", - "default": { - "default_normalize": true, - "lower_percentile": null, - "upper_percentile": null, - "lower_bound": null, - "upper_bound": null - }, - "allOf": [ - { - "$ref": "#/definitions/CellposeCustomNormalizer" - } - ], "description": "By default, data is normalized so 0.0=1st percentile and 1.0=99th percentile of image intensities in each channel. This automatic normalization can lead to issues when the image to be segmented is very sparse. You can turn off the automated rescaling and either provide your own rescaling percentiles or fixed rescaling upper and lower bound integers" }, "anisotropy": { diff --git a/fractal_tasks_core/tasks/cellpose_segmentation.py b/fractal_tasks_core/tasks/cellpose_segmentation.py index 20a114cdb..d45ac8b86 100644 --- a/fractal_tasks_core/tasks/cellpose_segmentation.py +++ b/fractal_tasks_core/tasks/cellpose_segmentation.py @@ -220,9 +220,7 @@ def cellpose_segmentation( pretrained_model: Optional[str] = None, cellprob_threshold: float = 0.0, flow_threshold: float = 0.4, - normalize: CellposeCustomNormalizer = CellposeCustomNormalizer( - default_normalize=True - ), + normalize: Optional[CellposeCustomNormalizer] = None, anisotropy: Optional[float] = None, min_size: int = 15, augment: bool = False, @@ -402,6 +400,10 @@ def cellpose_segmentation( except (KeyError, IndexError): output_label_name = f"label_{ind_channel}" + # Set default normalizer + if normalize is None: + normalize = CellposeCustomNormalizer(default_normalize=True) + # Load ZYX data data_zyx = da.from_zarr(f"{zarrurl}/{level}")[ind_channel] logger.info(f"{data_zyx.shape=}") From 4d6099ffcb8dfaa36d15f3ea98ca836bec99ae2b Mon Sep 17 00:00:00 2001 From: jluethi Date: Thu, 8 Feb 2024 11:05:44 +0100 Subject: [PATCH 14/25] Fix using the correct normalize call to model.eval --- fractal_tasks_core/tasks/cellpose_segmentation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fractal_tasks_core/tasks/cellpose_segmentation.py b/fractal_tasks_core/tasks/cellpose_segmentation.py index 20a114cdb..4261ddaac 100644 --- a/fractal_tasks_core/tasks/cellpose_segmentation.py +++ b/fractal_tasks_core/tasks/cellpose_segmentation.py @@ -165,7 +165,7 @@ def segment_ROI( anisotropy=anisotropy, cellprob_threshold=cellprob_threshold, flow_threshold=flow_threshold, - normalize=normalize, + normalize=normalize.default_normalize, min_size=min_size, batch_size=batch_size, invert=invert, From f3b38ffce0bf87d6667e3a2dcb3f4b8dc894a07f Mon Sep 17 00:00:00 2001 From: jluethi Date: Thu, 8 Feb 2024 11:58:01 +0100 Subject: [PATCH 15/25] Add constraints for percentiles --- fractal_tasks_core/__FRACTAL_MANIFEST__.json | 4 ++++ fractal_tasks_core/tasks/cellpose_transforms.py | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json index a5bc6000c..3ac7fdf6a 100644 --- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json +++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json @@ -599,11 +599,15 @@ }, "lower_percentile": { "title": "Lower Percentile", + "minimum": 0, + "maximum": 100, "type": "number", "description": "Specify a custom lower-bound percentile for rescaling as a float value between 0 and 100. Set to 1 to run the same as default). You can only specify percentils or bounds, not both." }, "upper_percentile": { "title": "Upper Percentile", + "minimum": 0, + "maximum": 100, "type": "number", "description": "Specify a custom upper-bound percentile for rescaling as a float value between 0 and 100. Set to 99 to run the same as default, set to e.g. 99.99 if the default rescaling was too harsh. You can only specify percentils or bounds, not both." }, diff --git a/fractal_tasks_core/tasks/cellpose_transforms.py b/fractal_tasks_core/tasks/cellpose_transforms.py index d088f980b..4c7647f53 100644 --- a/fractal_tasks_core/tasks/cellpose_transforms.py +++ b/fractal_tasks_core/tasks/cellpose_transforms.py @@ -16,6 +16,7 @@ import numpy as np from pydantic import BaseModel +from pydantic import Field from pydantic import root_validator @@ -50,8 +51,8 @@ class CellposeCustomNormalizer(BaseModel): """ default_normalize: bool = True - lower_percentile: Optional[float] = None - upper_percentile: Optional[float] = None + lower_percentile: Optional[float] = Field(None, ge=0, le=100) + upper_percentile: Optional[float] = Field(None, ge=0, le=100) lower_bound: Optional[int] = None upper_bound: Optional[int] = None From 58f974c5dfc08972181a99ffaf4e309f759343a6 Mon Sep 17 00:00:00 2001 From: jluethi Date: Thu, 8 Feb 2024 11:58:57 +0100 Subject: [PATCH 16/25] Add test for CellposeCustomNormalizer --- tests/tasks/test_unit_cellpose_transforms.py | 58 ++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/tasks/test_unit_cellpose_transforms.py diff --git a/tests/tasks/test_unit_cellpose_transforms.py b/tests/tasks/test_unit_cellpose_transforms.py new file mode 100644 index 000000000..4b520fce6 --- /dev/null +++ b/tests/tasks/test_unit_cellpose_transforms.py @@ -0,0 +1,58 @@ +import pytest + +from fractal_tasks_core.tasks.cellpose_transforms import ( + CellposeCustomNormalizer, +) + + +@pytest.mark.parametrize( + "default_normalize, lower_percentile, upper_percentile, lower_bound, " + "upper_bound, expected_value_error", + [ + (True, None, None, None, None, False), + (True, 1, 99, None, None, True), + (True, 1, None, None, None, True), + (True, None, 99, None, None, True), + (True, 1, 99, 0, 100, True), + (True, None, None, 0, 100, True), + (True, None, None, None, 100, True), + (True, None, None, 0, None, True), + (False, None, None, None, None, False), + (False, 1, 99, None, None, False), + (False, 1, None, None, None, True), + (False, None, 99, None, None, True), + (False, 1, 99, 0, 100, True), + (False, None, None, 0, 100, False), + (False, None, None, None, 100, True), + (False, None, None, 0, None, True), + ], +) +def test_CellposeCustomNormalizer( + default_normalize, + lower_percentile, + upper_percentile, + lower_bound, + upper_bound, + expected_value_error, +): + if expected_value_error: + pass + with pytest.raises(ValueError): + CellposeCustomNormalizer( + default_normalize=default_normalize, + lower_percentile=lower_percentile, + upper_percentile=upper_percentile, + lower_bound=lower_bound, + upper_bound=upper_bound, + ) + else: + assert ( + CellposeCustomNormalizer( + default_normalize=default_normalize, + lower_percentile=lower_percentile, + upper_percentile=upper_percentile, + lower_bound=lower_bound, + upper_bound=upper_bound, + ).default_normalize + == default_normalize + ) From 061c5cec66cf0bdb8f4270ec510e7e632039d788 Mon Sep 17 00:00:00 2001 From: jluethi Date: Thu, 8 Feb 2024 13:17:06 +0100 Subject: [PATCH 17/25] Increase version to 0.14.2a2 --- fractal_tasks_core/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fractal_tasks_core/__init__.py b/fractal_tasks_core/__init__.py index ca48d312f..c1f508bfe 100644 --- a/fractal_tasks_core/__init__.py +++ b/fractal_tasks_core/__init__.py @@ -6,6 +6,6 @@ ) -__VERSION__ = "0.14.2a1" +__VERSION__ = "0.14.2a2" __OME_NGFF_VERSION__ = "0.4" __FRACTAL_TABLE_VERSION__ = "1" diff --git a/pyproject.toml b/pyproject.toml index fb67a9c4b..ca6ad3e7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fractal-tasks-core" -version = "0.14.2a1" +version = "0.14.2a2" description = "Core bioimage-analysis library and tasks of the Fractal analytics platform" authors = [ "Joel Lüthi ", @@ -89,7 +89,7 @@ source = ["fractal_tasks_core"] omit = ["tests/*", "examples/*", "fractal_tasks_core/dev/*", "fractal_tasks_core/tasks/compress_tif.py"] [tool.bumpver] -current_version = "0.14.2a1" +current_version = "0.14.2a2" version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]" commit_message = "bump version {old_version} -> {new_version}" commit = true From 46bfa0b34d5ea1e8f96556e04661e69f8f08bbbe Mon Sep 17 00:00:00 2001 From: jluethi Date: Thu, 8 Feb 2024 13:41:24 +0100 Subject: [PATCH 18/25] Remove clipping from normalization with bounds --- fractal_tasks_core/tasks/cellpose_transforms.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fractal_tasks_core/tasks/cellpose_transforms.py b/fractal_tasks_core/tasks/cellpose_transforms.py index 4c7647f53..5a97f4761 100644 --- a/fractal_tasks_core/tasks/cellpose_transforms.py +++ b/fractal_tasks_core/tasks/cellpose_transforms.py @@ -122,8 +122,10 @@ def normalized_img( lower_bound=None, upper_bound=None, ): - """normalize each channel of the image so that so that 0.0=1st percentile - and 1.0=99th percentile of image intensities + """normalize each channel of the image so that so that 0.0=lower percentile + or lower bound and 1.0=upper percentile or upper bound of image intensities. + + The normalization can result in values < 0 or > 1 (no clipping). Based on https://github.com/MouseLand/cellpose/blob/4f5661983c3787efa443bbbd3f60256f4fd8bf53/cellpose/transforms.py#L375 # noqa E501 @@ -205,6 +207,4 @@ def normalize_bounds(Y, lower: int = 0, upper: int = 65535): """ X = Y.copy() X = (X - lower) / (upper - lower) - # Avoid negative values (if lower > pixel value) - X[X < 0] = 0 return X From 25aecf9ea88c15ec68a29bdc74046d5c71f98811 Mon Sep 17 00:00:00 2001 From: jluethi Date: Thu, 8 Feb 2024 13:41:45 +0100 Subject: [PATCH 19/25] Add tests for normalized_img --- tests/tasks/test_unit_cellpose_transforms.py | 66 ++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/tasks/test_unit_cellpose_transforms.py b/tests/tasks/test_unit_cellpose_transforms.py index 4b520fce6..9d858dffb 100644 --- a/tests/tasks/test_unit_cellpose_transforms.py +++ b/tests/tasks/test_unit_cellpose_transforms.py @@ -1,8 +1,12 @@ +import numpy as np import pytest from fractal_tasks_core.tasks.cellpose_transforms import ( CellposeCustomNormalizer, ) +from fractal_tasks_core.tasks.cellpose_transforms import ( + normalized_img, +) @pytest.mark.parametrize( @@ -56,3 +60,65 @@ def test_CellposeCustomNormalizer( ).default_normalize == default_normalize ) + + +def test_normalized_img_percentile(): + # Create a 4D numpy array with values evenly distributed from 0 to 1000 + img = np.linspace(1, 1000, num=1000).reshape((1, 10, 10, 10)) + + # Normalize the image + normalized = normalized_img(img, axis=0, lower_p=1.0, upper_p=99.0) + + # Check dimensions are unchanged + assert ( + img.shape == normalized.shape + ), "Normalized image should have the same shape as input" + + # Check type is float32 as per the function's specification + assert ( + normalized.dtype == np.float32 + ), "Normalized image should be of type float32" + + # Check that the normalization results in the expected clipping + assert np.sum(np.sum(np.sum(np.sum(normalized <= 0)))) == 10 + assert np.sum(np.sum(np.sum(np.sum(normalized >= 1)))) == 10 + + +@pytest.mark.parametrize( + "lower_bound, upper_bound, lower_than_0, higher_than_1", + [ + (0, 901, 0, 100), + (100, 901, 100, 100), + (10, 991, 10, 10), + (1, 999, 1, 2), + ], +) +def test_normalized_img_bounds( + lower_bound, upper_bound, lower_than_0, higher_than_1 +): + # Create a 4D numpy array with values evenly distributed from 0 to 1000 + img = np.linspace(1, 1000, num=1000).reshape((1, 10, 10, 10)) + + # Normalize the image + normalized = normalized_img( + img, + axis=0, + lower_p=None, + upper_p=None, + lower_bound=lower_bound, + upper_bound=upper_bound, + ) + + # Check dimensions are unchanged + assert ( + img.shape == normalized.shape + ), "Normalized image should have the same shape as input" + + # Check type is float32 as per the function's specification + assert ( + normalized.dtype == np.float32 + ), "Normalized image should be of type float32" + + # Check that the normalization results in the expected clipping + assert np.sum(np.sum(np.sum(np.sum(normalized <= 0)))) == lower_than_0 + assert np.sum(np.sum(np.sum(np.sum(normalized >= 1)))) == higher_than_1 From 66a5218cf0bbe2bb9314af5e963fec151bf64a8a Mon Sep 17 00:00:00 2001 From: jluethi Date: Thu, 15 Feb 2024 13:15:24 +0100 Subject: [PATCH 20/25] Update Cellpose normalization pydantic model --- fractal_tasks_core/__FRACTAL_MANIFEST__.json | 29 +++-- .../tasks/cellpose_segmentation.py | 13 +- .../tasks/cellpose_transforms.py | 112 +++++++++++------- tests/tasks/test_unit_cellpose_transforms.py | 59 +++++---- 4 files changed, 125 insertions(+), 88 deletions(-) diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json index 3ac7fdf6a..1aa42f699 100644 --- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json +++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json @@ -479,7 +479,7 @@ "normalize": { "$ref": "#/definitions/CellposeCustomNormalizer", "title": "Normalize", - "description": "By default, data is normalized so 0.0=1st percentile and 1.0=99th percentile of image intensities in each channel. This automatic normalization can lead to issues when the image to be segmented is very sparse. You can turn off the automated rescaling and either provide your own rescaling percentiles or fixed rescaling upper and lower bound integers" + "description": "By default, data is normalized so 0.0=1st percentile and 1.0=99th percentile of image intensities in each channel. This automatic normalization can lead to issues when the image to be segmented is very sparse. You can turn off the default rescaling. With the \"custom\" option, you can either provide your own rescaling percentiles or fixed rescaling upper and lower bound integers." }, "anisotropy": { "title": "Anisotropy", @@ -591,37 +591,44 @@ "description": "Validator to handle different normalization scenarios for Cellpose models", "type": "object", "properties": { - "default_normalize": { - "title": "Default Normalize", - "default": true, - "type": "boolean", - "description": "Whether to use the default Cellpose normalization approach (rescaling the image between the 1st and 99th percentile)" + "type": { + "title": "Type", + "enum": [ + "default", + "custom", + "no_normalization" + ], + "type": "string", + "description": "One of `default` (Cellpose default normalization), `custom` (using the other custom parameters) or `no_normalization`." }, "lower_percentile": { "title": "Lower Percentile", "minimum": 0, "maximum": 100, "type": "number", - "description": "Specify a custom lower-bound percentile for rescaling as a float value between 0 and 100. Set to 1 to run the same as default). You can only specify percentils or bounds, not both." + "description": "Specify a custom lower-bound percentile for rescaling as a float value between 0 and 100. Set to 1 to run the same as default). You can only specify percentiles or bounds, not both." }, "upper_percentile": { "title": "Upper Percentile", "minimum": 0, "maximum": 100, "type": "number", - "description": "Specify a custom upper-bound percentile for rescaling as a float value between 0 and 100. Set to 99 to run the same as default, set to e.g. 99.99 if the default rescaling was too harsh. You can only specify percentils or bounds, not both." + "description": "Specify a custom upper-bound percentile for rescaling as a float value between 0 and 100. Set to 99 to run the same as default, set to e.g. 99.99 if the default rescaling was too harsh. You can only specify percentiles or bounds, not both." }, "lower_bound": { "title": "Lower Bound", "type": "integer", - "description": "Explicit lower bound value to rescale the image at. Needs to be an integer, e.g. 100. You can only specify percentils or bounds, not both." + "description": "Explicit lower bound value to rescale the image at. Needs to be an integer, e.g. 100. You can only specify percentiles or bounds, not both." }, "upper_bound": { "title": "Upper Bound", "type": "integer", - "description": "Explicit upper bound value to rescale the image at. Needs to be an integer, e.g. 2000 You can only specify percentils or bounds, not both." + "description": "Explicit upper bound value to rescale the image at. Needs to be an integer, e.g. 2000. You can only specify percentiles or bounds, not both." } - } + }, + "exclude": [ + "cellpose_normalize" + ] } } }, diff --git a/fractal_tasks_core/tasks/cellpose_segmentation.py b/fractal_tasks_core/tasks/cellpose_segmentation.py index d059b1855..b1dc9b061 100644 --- a/fractal_tasks_core/tasks/cellpose_segmentation.py +++ b/fractal_tasks_core/tasks/cellpose_segmentation.py @@ -140,11 +140,11 @@ def segment_ROI( f" {model.diam_mean=} |" f" {diameter=} |" f" {flow_threshold=} |" - f" {normalize.default_normalize=}" + f" {normalize.type=}" ) # Optionally perform custom normalization - if not normalize.default_normalize: + if normalize.type == "custom": x = normalized_img( x, lower_p=normalize.lower_percentile, @@ -165,7 +165,7 @@ def segment_ROI( anisotropy=anisotropy, cellprob_threshold=cellprob_threshold, flow_threshold=flow_threshold, - normalize=normalize.default_normalize, + normalize=normalize.get_cellpose_normalize(), min_size=min_size, batch_size=batch_size, invert=invert, @@ -298,9 +298,10 @@ def cellpose_segmentation( normalize: By default, data is normalized so 0.0=1st percentile and 1.0=99th percentile of image intensities in each channel. This automatic normalization can lead to issues when the image to - be segmented is very sparse. You can turn off the automated - rescaling and either provide your own rescaling percentiles or - fixed rescaling upper and lower bound integers + be segmented is very sparse. You can turn off the default + rescaling. With the "custom" option, you can either provide your + own rescaling percentiles or fixed rescaling upper and lower + bound integers. anisotropy: Ratio of the pixel sizes along Z and XY axis (ignored if the image is not three-dimensional). If `None`, it is inferred from the OME-NGFF metadata. diff --git a/fractal_tasks_core/tasks/cellpose_transforms.py b/fractal_tasks_core/tasks/cellpose_transforms.py index 5a97f4761..9694d63da 100644 --- a/fractal_tasks_core/tasks/cellpose_transforms.py +++ b/fractal_tasks_core/tasks/cellpose_transforms.py @@ -12,6 +12,7 @@ Helper functions for image normalization in """ import logging +from typing import Literal from typing import Optional import numpy as np @@ -27,30 +28,33 @@ class CellposeCustomNormalizer(BaseModel): """ Validator to handle different normalization scenarios for Cellpose models - If default normalization is to be used, no other parameters can be - specified. Alternatively, when default normalization is not applied, - either percentiles or explicit integer bounds can be applied. + If `type="default"`, then Cellpose default normalization is + used and no other parameters can be specified. + If `type="no_normalization"`, then no normalization is used and no + other parameters can be specified. + If `type="custom"`, then either percentiles or explicit integer + bounds can be applied. Attributes: - default_normalize: Whether to use the default Cellpose normalization - approach (rescaling the image between the 1st and 99th percentile) + type: + One of `default` (Cellpose default normalization), `custom` + (using the other custom parameters) or `no_normalization`. lower_percentile: Specify a custom lower-bound percentile for rescaling as a float value between 0 and 100. Set to 1 to run the same as - default). You can only specify percentils or bounds, not both. + default). You can only specify percentiles or bounds, not both. upper_percentile: Specify a custom upper-bound percentile for rescaling as a float value between 0 and 100. Set to 99 to run the same as default, set to e.g. 99.99 if the default rescaling was too harsh. - You can only specify percentils or bounds, not both. + You can only specify percentiles or bounds, not both. lower_bound: Explicit lower bound value to rescale the image at. Needs to be an integer, e.g. 100. - You can only specify percentils or bounds, not both. + You can only specify percentiles or bounds, not both. upper_bound: Explicit upper bound value to rescale the image at. - Needs to be an integer, e.g. 2000 - You can only specify percentils or bounds, not both. - + Needs to be an integer, e.g. 2000. + You can only specify percentiles or bounds, not both. """ - default_normalize: bool = True + type: Optional[Literal["default", "custom", "no_normalization"]] = None lower_percentile: Optional[float] = Field(None, ge=0, le=100) upper_percentile: Optional[float] = Field(None, ge=0, le=100) lower_bound: Optional[int] = None @@ -62,56 +66,72 @@ class CellposeCustomNormalizer(BaseModel): @root_validator def validate_conditions(cls, values): - default_normalize = values.get("default_normalize") + # Extract values + type = values.get("type") lower_percentile = values.get("lower_percentile") upper_percentile = values.get("upper_percentile") lower_bound = values.get("lower_bound") upper_bound = values.get("upper_bound") - # If default_normalize is True, check that all other fields are None - if default_normalize: - if any( - v is not None - for v in [ - lower_percentile, - upper_percentile, - lower_bound, - upper_bound, - ] - ): - raise ValueError( - "When default_normalize is True, no percentile or " - "bounds can be specified" - ) - - # Check for lower_percentile and upper_percentile condition - if lower_percentile is not None or upper_percentile is not None: - if lower_percentile is None or upper_percentile is None: + # Verify that custom parameters are only provided when type="custom" + if type != "custom": + if lower_percentile is not None: raise ValueError( - "Both lower_percentile and upper_percentile must be set " - "together" + f"Type='{type}' but {lower_percentile=}. " + "Hint: set type='custom'." ) - if lower_bound is not None or upper_bound is not None: + if upper_percentile is not None: raise ValueError( - "If a percentile is specified, no hard set lower_bound " - "or upper_bound can be specified" + f"Type='{type}' but {upper_percentile=}. " + "Hint: set type='custom'." ) - - # If lower_bound or upper_bound is set, the other must also be set, - # and lower_percentile & upper_percentile must be None - if lower_bound is not None or upper_bound is not None: - if lower_bound is None or upper_bound is None: + if lower_bound is not None: raise ValueError( - "Both lower_bound and upper_bound must be set together" + f"Type='{type}' but {lower_bound=}. " + "Hint: set type='custom'." ) - if lower_percentile is not None or upper_percentile is not None: + if upper_bound is not None: raise ValueError( - "If explicit lower and upper bounds are set, percentiles " - "cannot be specified" + f"Type='{type}' but {upper_bound=}. " + "Hint: set type='custom'." ) + # The only valid options are: + # 1. Both percentiles are set and both bounds are unset + # 2. Both bounds are set and both percentiles are unset + are_percentiles_set = ( + lower_percentile is not None, + upper_percentile is not None, + ) + are_bounds_set = ( + lower_bound is not None, + upper_bound is not None, + ) + if len(set(are_percentiles_set)) != 1: + raise ValueError( + "Both lower_percentile and upper_percentile must be set " + "together." + ) + if len(set(are_bounds_set)) != 1: + raise ValueError( + "Both lower_bound and upper_bound must be set together" + ) + if lower_percentile is not None and lower_bound is not None: + raise ValueError( + "You cannot set both explicit bounds and percentile bounds " + "at the same time. Hint: use only one of the two options." + ) + return values + def get_cellpose_normalize(self): + # Set a variable for whether cellpose internal normalization is applied + cellpose_normalize = True + if self.type == "custom" or self.type == "no_normalization": + cellpose_normalize = False + + return cellpose_normalize + def normalized_img( img, diff --git a/tests/tasks/test_unit_cellpose_transforms.py b/tests/tasks/test_unit_cellpose_transforms.py index 9d858dffb..e830364cf 100644 --- a/tests/tasks/test_unit_cellpose_transforms.py +++ b/tests/tasks/test_unit_cellpose_transforms.py @@ -10,29 +10,30 @@ @pytest.mark.parametrize( - "default_normalize, lower_percentile, upper_percentile, lower_bound, " + "type, lower_percentile, upper_percentile, lower_bound, " "upper_bound, expected_value_error", [ - (True, None, None, None, None, False), - (True, 1, 99, None, None, True), - (True, 1, None, None, None, True), - (True, None, 99, None, None, True), - (True, 1, 99, 0, 100, True), - (True, None, None, 0, 100, True), - (True, None, None, None, 100, True), - (True, None, None, 0, None, True), - (False, None, None, None, None, False), - (False, 1, 99, None, None, False), - (False, 1, None, None, None, True), - (False, None, 99, None, None, True), - (False, 1, 99, 0, 100, True), - (False, None, None, 0, 100, False), - (False, None, None, None, 100, True), - (False, None, None, 0, None, True), + ("default", None, None, None, None, False), + ("default", 1, 99, None, None, True), + ("default", 1, None, None, None, True), + ("default", None, 99, None, None, True), + ("default", 1, 99, 0, 100, True), + ("default", None, None, 0, 100, True), + ("default", None, None, None, 100, True), + ("default", None, None, 0, None, True), + ("no_normalization", None, None, None, None, False), + ("custom", 1, 99, None, None, False), + ("custom", 1, None, None, None, True), + ("custom", None, 99, None, None, True), + ("custom", 1, 99, 0, 100, True), + ("custom", None, None, 0, 100, False), + ("custom", None, None, None, 100, True), + ("custom", None, None, 0, None, True), + ("wrong_type", None, None, None, None, True), ], ) def test_CellposeCustomNormalizer( - default_normalize, + type, lower_percentile, upper_percentile, lower_bound, @@ -43,23 +44,31 @@ def test_CellposeCustomNormalizer( pass with pytest.raises(ValueError): CellposeCustomNormalizer( - default_normalize=default_normalize, + type=type, lower_percentile=lower_percentile, upper_percentile=upper_percentile, lower_bound=lower_bound, upper_bound=upper_bound, ) else: - assert ( - CellposeCustomNormalizer( - default_normalize=default_normalize, + if type == "default": + assert CellposeCustomNormalizer( + type=type, lower_percentile=lower_percentile, upper_percentile=upper_percentile, lower_bound=lower_bound, upper_bound=upper_bound, - ).default_normalize - == default_normalize - ) + ).get_cellpose_normalize() + else: + assert not ( + CellposeCustomNormalizer( + type=type, + lower_percentile=lower_percentile, + upper_percentile=upper_percentile, + lower_bound=lower_bound, + upper_bound=upper_bound, + ).get_cellpose_normalize() + ) def test_normalized_img_percentile(): From e62c7fe8f32afc48f26ffabf0bb45641c1012bf0 Mon Sep 17 00:00:00 2001 From: jluethi Date: Thu, 15 Feb 2024 13:18:35 +0100 Subject: [PATCH 21/25] Switch to Option 3: Provide CellposeCustomNormalizer() as default value for normalize --- fractal_tasks_core/__FRACTAL_MANIFEST__.json | 18 +++++++++++++----- .../tasks/cellpose_segmentation.py | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json index 1aa42f699..c37392054 100644 --- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json +++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json @@ -477,8 +477,19 @@ "description": "Parameter of `CellposeModel.eval` method. Valid values between 0.0 and 1.0. From Cellpose documentation: \"Increase this threshold if cellpose is not returning as many ROIs as you\u2019d expect. Similarly, decrease this threshold if cellpose is returning too many ill-shaped ROIs.\"" }, "normalize": { - "$ref": "#/definitions/CellposeCustomNormalizer", "title": "Normalize", + "default": { + "type": null, + "lower_percentile": null, + "upper_percentile": null, + "lower_bound": null, + "upper_bound": null + }, + "allOf": [ + { + "$ref": "#/definitions/CellposeCustomNormalizer" + } + ], "description": "By default, data is normalized so 0.0=1st percentile and 1.0=99th percentile of image intensities in each channel. This automatic normalization can lead to issues when the image to be segmented is very sparse. You can turn off the default rescaling. With the \"custom\" option, you can either provide your own rescaling percentiles or fixed rescaling upper and lower bound integers." }, "anisotropy": { @@ -625,10 +636,7 @@ "type": "integer", "description": "Explicit upper bound value to rescale the image at. Needs to be an integer, e.g. 2000. You can only specify percentiles or bounds, not both." } - }, - "exclude": [ - "cellpose_normalize" - ] + } } } }, diff --git a/fractal_tasks_core/tasks/cellpose_segmentation.py b/fractal_tasks_core/tasks/cellpose_segmentation.py index b1dc9b061..434635a5d 100644 --- a/fractal_tasks_core/tasks/cellpose_segmentation.py +++ b/fractal_tasks_core/tasks/cellpose_segmentation.py @@ -220,7 +220,7 @@ def cellpose_segmentation( pretrained_model: Optional[str] = None, cellprob_threshold: float = 0.0, flow_threshold: float = 0.4, - normalize: Optional[CellposeCustomNormalizer] = None, + normalize: CellposeCustomNormalizer = CellposeCustomNormalizer(), anisotropy: Optional[float] = None, min_size: int = 15, augment: bool = False, From cd6f134fbca361524f925c59a225eab28a56abf3 Mon Sep 17 00:00:00 2001 From: jluethi Date: Thu, 15 Feb 2024 13:21:54 +0100 Subject: [PATCH 22/25] Improve CellposeCustomNormalizer: make type required --- fractal_tasks_core/__FRACTAL_MANIFEST__.json | 3 ++- fractal_tasks_core/tasks/cellpose_transforms.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json index c37392054..e18d8518e 100644 --- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json +++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json @@ -479,7 +479,7 @@ "normalize": { "title": "Normalize", "default": { - "type": null, + "type": "default", "lower_percentile": null, "upper_percentile": null, "lower_bound": null, @@ -604,6 +604,7 @@ "properties": { "type": { "title": "Type", + "default": "default", "enum": [ "default", "custom", diff --git a/fractal_tasks_core/tasks/cellpose_transforms.py b/fractal_tasks_core/tasks/cellpose_transforms.py index 9694d63da..4dc6b0120 100644 --- a/fractal_tasks_core/tasks/cellpose_transforms.py +++ b/fractal_tasks_core/tasks/cellpose_transforms.py @@ -54,7 +54,7 @@ class CellposeCustomNormalizer(BaseModel): You can only specify percentiles or bounds, not both. """ - type: Optional[Literal["default", "custom", "no_normalization"]] = None + type: Literal["default", "custom", "no_normalization"] = "default" lower_percentile: Optional[float] = Field(None, ge=0, le=100) upper_percentile: Optional[float] = Field(None, ge=0, le=100) lower_bound: Optional[int] = None From 15a089476d6158c3434c0dd8e127beff02716833 Mon Sep 17 00:00:00 2001 From: jluethi Date: Thu, 15 Feb 2024 13:40:51 +0100 Subject: [PATCH 23/25] Docstrings & cleanups --- .../tasks/cellpose_segmentation.py | 10 +---- .../tasks/cellpose_transforms.py | 43 ++++++++++++------- tests/tasks/test_unit_cellpose_transforms.py | 4 +- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/fractal_tasks_core/tasks/cellpose_segmentation.py b/fractal_tasks_core/tasks/cellpose_segmentation.py index 434635a5d..1974a5b13 100644 --- a/fractal_tasks_core/tasks/cellpose_segmentation.py +++ b/fractal_tasks_core/tasks/cellpose_segmentation.py @@ -75,9 +75,7 @@ def segment_ROI( diameter: float = 30.0, cellprob_threshold: float = 0.0, flow_threshold: float = 0.4, - normalize: CellposeCustomNormalizer = CellposeCustomNormalizer( - default_normalize=True - ), + normalize: CellposeCustomNormalizer = CellposeCustomNormalizer(), label_dtype: Optional[np.dtype] = None, augment: bool = False, net_avg: bool = False, @@ -165,7 +163,7 @@ def segment_ROI( anisotropy=anisotropy, cellprob_threshold=cellprob_threshold, flow_threshold=flow_threshold, - normalize=normalize.get_cellpose_normalize(), + normalize=normalize.cellpose_normalize, min_size=min_size, batch_size=batch_size, invert=invert, @@ -401,10 +399,6 @@ def cellpose_segmentation( except (KeyError, IndexError): output_label_name = f"label_{ind_channel}" - # Set default normalizer - if normalize is None: - normalize = CellposeCustomNormalizer(default_normalize=True) - # Load ZYX data data_zyx = da.from_zarr(f"{zarrurl}/{level}")[ind_channel] logger.info(f"{data_zyx.shape=}") diff --git a/fractal_tasks_core/tasks/cellpose_transforms.py b/fractal_tasks_core/tasks/cellpose_transforms.py index 4dc6b0120..414e077fe 100644 --- a/fractal_tasks_core/tasks/cellpose_transforms.py +++ b/fractal_tasks_core/tasks/cellpose_transforms.py @@ -124,23 +124,25 @@ def validate_conditions(cls, values): return values - def get_cellpose_normalize(self): - # Set a variable for whether cellpose internal normalization is applied - cellpose_normalize = True - if self.type == "custom" or self.type == "no_normalization": - cellpose_normalize = False + @property + def cellpose_normalize(self) -> bool: + """ + Determine whether cellpose should apply its internal normalization. - return cellpose_normalize + If type is set to `custom` or `no_normalization`, don't apply cellpose + internal normalization + """ + return self.type == "default" def normalized_img( - img, - axis=-1, - invert=False, + img: np.ndarray, + axis: int = -1, + invert: bool = False, lower_p: float = 1.0, upper_p: float = 99.0, - lower_bound=None, - upper_bound=None, + lower_bound: Optional[int] = None, + upper_bound: Optional[int] = None, ): """normalize each channel of the image so that so that 0.0=lower percentile or lower bound and 1.0=upper percentile or upper bound of image intensities. @@ -210,9 +212,15 @@ def normalized_img( return img -def normalize_percentile(Y, lower: int = 1, upper: int = 99): +def normalize_percentile(Y: np.ndarray, lower: float = 1, upper: float = 99): """normalize image so 0.0 is lower percentile and 1.0 is upper percentile - Percentiles are passed as integers + Percentiles are passed as floats (must be between 0 and 100) + + Args: + Y: The image to be normalized + lower: Lower percentile + upper: Upper percentile + """ X = Y.copy() x01 = np.percentile(X, lower) @@ -222,8 +230,13 @@ def normalize_percentile(Y, lower: int = 1, upper: int = 99): def normalize_bounds(Y, lower: int = 0, upper: int = 65535): - """normalize image so 0.0 is lower percentile and 1.0 is upper percentile - Percentiles are passed as integers + """normalize image so 0.0 is lower value and 1.0 is upper value + + Args: + Y: The image to be normalized + lower: Lower normalization value + upper: Upper normalization value + """ X = Y.copy() X = (X - lower) / (upper - lower) diff --git a/tests/tasks/test_unit_cellpose_transforms.py b/tests/tasks/test_unit_cellpose_transforms.py index e830364cf..2f03d7430 100644 --- a/tests/tasks/test_unit_cellpose_transforms.py +++ b/tests/tasks/test_unit_cellpose_transforms.py @@ -58,7 +58,7 @@ def test_CellposeCustomNormalizer( upper_percentile=upper_percentile, lower_bound=lower_bound, upper_bound=upper_bound, - ).get_cellpose_normalize() + ).cellpose_normalize else: assert not ( CellposeCustomNormalizer( @@ -67,7 +67,7 @@ def test_CellposeCustomNormalizer( upper_percentile=upper_percentile, lower_bound=lower_bound, upper_bound=upper_bound, - ).get_cellpose_normalize() + ).cellpose_normalize ) From 0085a13fc2edce5dbe03e402307264ff4b4fc19e Mon Sep 17 00:00:00 2001 From: jluethi Date: Thu, 15 Feb 2024 13:42:37 +0100 Subject: [PATCH 24/25] Revert manual version updates --- fractal_tasks_core/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fractal_tasks_core/__init__.py b/fractal_tasks_core/__init__.py index c1f508bfe..07951b71c 100644 --- a/fractal_tasks_core/__init__.py +++ b/fractal_tasks_core/__init__.py @@ -6,6 +6,6 @@ ) -__VERSION__ = "0.14.2a2" +__VERSION__ = "0.14.1" __OME_NGFF_VERSION__ = "0.4" __FRACTAL_TABLE_VERSION__ = "1" diff --git a/pyproject.toml b/pyproject.toml index ca6ad3e7b..dc34dadea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fractal-tasks-core" -version = "0.14.2a2" +version = "0.14.1" description = "Core bioimage-analysis library and tasks of the Fractal analytics platform" authors = [ "Joel Lüthi ", @@ -89,7 +89,7 @@ source = ["fractal_tasks_core"] omit = ["tests/*", "examples/*", "fractal_tasks_core/dev/*", "fractal_tasks_core/tasks/compress_tif.py"] [tool.bumpver] -current_version = "0.14.2a2" +current_version = "0.14.1" version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]" commit_message = "bump version {old_version} -> {new_version}" commit = true From e7c1d74ee4885ef9d6a30c05d230826f6dc647fe Mon Sep 17 00:00:00 2001 From: jluethi Date: Thu, 15 Feb 2024 13:44:26 +0100 Subject: [PATCH 25/25] Add type hint of helper function --- fractal_tasks_core/tasks/cellpose_transforms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fractal_tasks_core/tasks/cellpose_transforms.py b/fractal_tasks_core/tasks/cellpose_transforms.py index 414e077fe..f74ed0ef7 100644 --- a/fractal_tasks_core/tasks/cellpose_transforms.py +++ b/fractal_tasks_core/tasks/cellpose_transforms.py @@ -229,7 +229,7 @@ def normalize_percentile(Y: np.ndarray, lower: float = 1, upper: float = 99): return X -def normalize_bounds(Y, lower: int = 0, upper: int = 65535): +def normalize_bounds(Y: np.ndarray, lower: int = 0, upper: int = 65535): """normalize image so 0.0 is lower value and 1.0 is upper value Args: