From 38bbd0709dce37f14ff61d63b442b6dd03d72613 Mon Sep 17 00:00:00 2001 From: Jan Eglinger Date: Wed, 22 Nov 2023 11:10:19 +0100 Subject: [PATCH] Add cellpose segmentation Also update supported Python versions (cellpose needs 3.8+). --- .github/workflows/test.yml | 2 +- config_cellpose.yml | 18 ++++++++++++++ pyproject.toml | 6 ++--- src/faim_wako_searchfirst/segment.py | 36 ++++++++++++++++++++++++---- tests/test_cellpose.py | 36 ++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 config_cellpose.yml create mode 100644 tests/test_cellpose.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8840397..c7b9dc3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11.0-beta.5 - 3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 diff --git a/config_cellpose.yml b/config_cellpose.yml new file mode 100644 index 0000000..b6d23b7 --- /dev/null +++ b/config_cellpose.yml @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2023 Friedrich Miescher Institute for Biomedical Research (FMI), Basel (Switzerland) +# +# SPDX-License-Identifier: MIT + +# Required +file_selection: # criteria for file selection in case of multiple channels/slices per position + channel: C01 +process: # choose method how to segment, filter, and sample the objects + segment: cellpose + filter: [] + sample: centers + +# Each subsequent section provides arguments to one of the methods defined in 'process' +cellpose: + diameter: 10.0 + pretrained_model: cyto2 + cellprob_threshold: 0.0 + flow_threshold: 0.4 diff --git a/pyproject.toml b/pyproject.toml index b503228..ed5d696 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "hatchling.build" name = "faim-wako-searchfirst" description = '' readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" license = "MIT" keywords = [] authors = [ @@ -19,7 +19,6 @@ authors = [ classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -28,6 +27,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ + "cellpose", "confuse", "rich", "scikit-image", @@ -55,7 +55,7 @@ cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=faim_w no-cov = "cov --no-cov {args}" [[tool.hatch.envs.test.matrix]] -python = ["37", "38", "39", "310", "311"] +python = ["38", "39", "310", "311"] [tool.coverage.run] branch = true diff --git a/src/faim_wako_searchfirst/segment.py b/src/faim_wako_searchfirst/segment.py index 738fe2d..48eeaf4 100644 --- a/src/faim_wako_searchfirst/segment.py +++ b/src/faim_wako_searchfirst/segment.py @@ -9,8 +9,11 @@ """ import logging +from pathlib import Path +from typing import Union import numpy as np +from cellpose import models from scipy.ndimage import binary_fill_holes from skimage.measure import label, regionprops @@ -26,10 +29,6 @@ def threshold( :param img: input image :param threshold: global threshold :param include_holes: if true, holes will be filled - :param min_size: minimum object size - :param max_size: maximum object size - :param min_eccentricity: minimum eccentricity of object - :param max_eccentricity: maximum eccentricity of object :param logger: :return: a label image representing the detected objects @@ -41,3 +40,32 @@ def threshold( regions = regionprops(labeled_image) logger.info(f"Found {len(regions)} connected components.") return labeled_image + + +def cellpose( + img, + diameter: float, + pretrained_model: Union[str, Path] = "cyto2", + logger=logging, + **kwargs, +): + """Segment a given image by global thresholding. + + :param img: input image + :param diameter: expected object diameter + :param pretrained_model: name of cellpose model, or path to pretrained model + :param logger: + + :return: a label image representing the detected objects + """ + logger.info(f"Load cellpose model: {pretrained_model}") + model: models.CellposeModel = models.CellposeModel( + pretrained_model=pretrained_model, + ) + mask, _, _ = model.eval( + img, + channels=[0, 0], + diameter=diameter, + **kwargs, + ) + return mask diff --git a/tests/test_cellpose.py b/tests/test_cellpose.py new file mode 100644 index 0000000..3e32787 --- /dev/null +++ b/tests/test_cellpose.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2023 Friedrich Miescher Institute for Biomedical Research (FMI), Basel (Switzerland) +# +# SPDX-License-Identifier: MIT + +"""Test faim_wako_searchfirst module.""" +import csv +import shutil +from pathlib import Path + +import pytest +from faim_wako_searchfirst.main import run + + +@pytest.fixture +def _data_path(tmp_path): + # copy test set into tmp_path, return resulting Path + testset_path = Path("tests/resources/TestSet") + assert testset_path.exists() + return shutil.copytree(testset_path, tmp_path / "TestSet") + + +def test_cellpose(_data_path): + """Test run with parameters defined in the sample config_cellpose.yml file.""" + run(_data_path, configfile="config_cellpose.yml") + csv_path = _data_path / "TestSet_D07_T0001F002L01A02Z01C01.csv" + assert csv_path.exists() + with open(csv_path, "r") as csv_file: + reader = csv.reader(csv_file, quoting=csv.QUOTE_NONNUMERIC) + entries = list(reader) + assert len(entries) == 3, "Incorrect number of objects detected." + assert entries[0] == pytest.approx([1, 40.5, 30.5]) + assert entries[1] == pytest.approx([2, 158.5, 58.5]) + assert entries[2] == pytest.approx([3, 79.5287, 146.4483]) + + segmentation_folder = _data_path.parent / (_data_path.name + "_segmentation") + assert sum(1 for _ in segmentation_folder.glob("*")) == 1