From 5a6ec104e24f8c4c60a0fe3d142e44c12be4d2a5 Mon Sep 17 00:00:00 2001 From: Jan Eglinger Date: Fri, 12 Apr 2024 10:29:03 +0200 Subject: [PATCH 1/2] filter: add dilate and feature --- src/faim_wako_searchfirst/filter.py | 27 +++++++++++++++++++ tests/test_filter.py | 42 +++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 tests/test_filter.py diff --git a/src/faim_wako_searchfirst/filter.py b/src/faim_wako_searchfirst/filter.py index 71ec09d..dde027f 100644 --- a/src/faim_wako_searchfirst/filter.py +++ b/src/faim_wako_searchfirst/filter.py @@ -15,6 +15,7 @@ from numpy import ndarray from skimage.measure import regionprops +from skimage.segmentation import expand_labels from tifffile import imread @@ -40,6 +41,23 @@ def area( labels[labels == region.label] = 0 +def feature( + tif_file: Path, + labels: ndarray, + feature: str, + min_value: float, + max_value: float, +): + """Filter objects in 'labels' by specified feature value range.""" + regions = regionprops(labels) + if hasattr(regions[0], feature): + for region in regions: + if not min_value <= getattr(region, feature) <= max_value: + labels[labels == region.label] = 0 + else: + raise AttributeError(f"'regionprops' object has no attribute '{feature}'") + + def solidity( tif_file: Path, labels: ndarray, @@ -53,6 +71,15 @@ def solidity( labels[labels == region.label] = 0 +def dilate( + tif_file: Path, + labels: ndarray, + pixel_distance: float = 10.0, +): + """Dilate objects by specified amount.""" + labels[:] = expand_labels(label_image=labels, distance=pixel_distance) + + def intensity( tif_file: Path, labels: ndarray, diff --git a/tests/test_filter.py b/tests/test_filter.py new file mode 100644 index 0000000..de99c60 --- /dev/null +++ b/tests/test_filter.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2024 Friedrich Miescher Institute for Biomedical Research (FMI), Basel (Switzerland) +# +# SPDX-License-Identifier: MIT + +"""Test faim_wako_searchfirst.filter module.""" + +from pathlib import Path + +import numpy as np +import pytest +from faim_wako_searchfirst.filter import dilate, feature +from skimage.io import imread + + +@pytest.fixture +def _label_image(): + return imread(Path("tests") / "resources" / "simple_labels.tif") + + +def test_feature(_label_image: np.ndarray): + """Test generic feature filter.""" + labels = _label_image.copy() + feature( + tif_file=None, + labels=labels, + feature="solidity", + min_value=0.9, + max_value=1.0, + ) + assert np.unique(labels).tolist() == [0, 2, 3, 4] + + +def test_dilate(_label_image: np.ndarray): + """Test oject dilation.""" + labels = _label_image.copy() + assert np.sum(labels[labels == 1]) == 1353 + dilate( + tif_file=None, + labels=labels, + pixel_distance=5.0, + ) + assert np.sum(labels[labels == 1]) == 2803 From 39d01e7007c6cf0fe8423a3d254d40be0282054e Mon Sep 17 00:00:00 2001 From: Jan Eglinger Date: Mon, 15 Apr 2024 10:33:46 +0200 Subject: [PATCH 2/2] Update config.yml and README with new options --- README.md | 32 +++++++++++++++++++++++--------- config.yml | 8 +++++++- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e75ea44..46f76ad 100644 --- a/README.md +++ b/README.md @@ -33,21 +33,29 @@ Configuration is managed in a `config.yml` file: ```yaml # Required -file_selection: # criteria for file selection in case of multiple channels/slices per position + +# criteria for file selection in case of multiple channels/slices per position +file_selection: channel: C01 -process: # choose method how to segment, filter, and sample the objects - segment: threshold # choices: threshold, cellpose - filter: [bounding_box, area, solidity, intensity] - sample: centers # choices: centers, grid_overlap, dense_grid, object_centered_grid, region_centered_grid -# Each subsequent section provides arguments to one of the methods defined in 'process'. +# choose method how to segment, filter, and sample the objects +process: + # segment methods: threshold, cellpose + segment: threshold + # filter methods: bounding_box, area, solidity, intensity + filter: [bounding_box, area, solidity, feature, dilate, intensity] + # sample methods: centers, grid_overlap, dense_grid, + # object_centered_grid, region_centered_grid + sample: centers + +# Each section below provides arguments to one of the methods set in 'process'. # Config sections for methods not selected above will be ignored. # segment threshold: threshold: 128 - include_holes: yes - gaussian_sigma: 2.0 # default: 0.0 + include_holes: true + gaussian_sigma: 0.0 # default: 0.0 # filter bounding_box: @@ -61,13 +69,19 @@ area: solidity: min_solidity: 0.9 max_solidity: 1.0 +feature: + feature: eccentricity + min_value: 0.0 + max_value: 0.99 +dilate: + pixel_distance: 1.0 intensity: target_channel: C03 min_intensity: 128 # sample dense_grid: - binning_factor: 20 # default: 50 + binning_factor: 50 # default: 50 grid_overlap: mag_first_pass: 4 mag_second_pass: 60 diff --git a/config.yml b/config.yml index d56ad6d..e219b8b 100644 --- a/config.yml +++ b/config.yml @@ -14,7 +14,7 @@ process: # segment methods: threshold, cellpose segment: threshold # filter methods: bounding_box, area, solidity, intensity - filter: [bounding_box, area, solidity, intensity] + filter: [bounding_box, area, solidity, feature, intensity, dilate] # sample methods: centers, grid_overlap, dense_grid, # object_centered_grid, region_centered_grid sample: centers @@ -40,6 +40,12 @@ area: solidity: min_solidity: 0.9 max_solidity: 1.0 +feature: + feature: eccentricity + min_value: 0.0 + max_value: 0.99 +dilate: + pixel_distance: 1.0 intensity: target_channel: C03 min_intensity: 128