From b4cfee2fcb0aa25029a3a79a6e2c51d251ce8bdc Mon Sep 17 00:00:00 2001 From: Robert Haase Date: Wed, 28 Feb 2024 11:29:18 +0100 Subject: [PATCH 01/12] make dependency flexible --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f59fd94..4206378b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,7 @@ option(BUILD_SHARED_LIBS OFF) ## CLIc dependency -set(CLIC_REPO_TAG 0.8.1) # branch name for dev +set(CLIC_REPO_TAG master) # branch name for dev set(CLIC_REPO_URL https://github.com/clEsperanto/CLIc.git) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/clic EXCLUDE_FROM_ALL) From 6661d001c544bfb74e922cebf15d0d5f403f42d8 Mon Sep 17 00:00:00 2001 From: Robert Haase Date: Wed, 28 Feb 2024 11:30:59 +0100 Subject: [PATCH 02/12] bump version in meta data --- pyclesperanto/_version.py | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyclesperanto/_version.py b/pyclesperanto/_version.py index c10b9197..45542b89 100644 --- a/pyclesperanto/_version.py +++ b/pyclesperanto/_version.py @@ -1,10 +1,10 @@ # pyclesperanto version -VERSION_CODE = 0, 8, 1 +VERSION_CODE = 0, 8, 2 VERSION_STATUS = "" VERSION = ".".join(str(x) for x in VERSION_CODE) + VERSION_STATUS # clic version -CLIC_VERSION_CODE = 0, 8, 1 +CLIC_VERSION_CODE = 0, 8, 2 CLIC_VERSION_STATUS = "" CLIC_VERSION = ".".join(str(x) for x in CLIC_VERSION_CODE) + CLIC_VERSION_STATUS diff --git a/pyproject.toml b/pyproject.toml index 8547ceeb..3d25b3d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ description = "GPU-accelerated image processing in python using OpenCL" name = "pyclesperanto" readme = "README.md" requires-python = ">=3.7" -version = "0.8.1" +version = "0.8.2" [project.urls] Documentation = "https://clesperanto.github.io/pyclesperanto/" From d94f6361659294ebe190f2f41c4c2ee84626d63d Mon Sep 17 00:00:00 2001 From: Robert Haase Date: Wed, 28 Feb 2024 13:26:47 +0100 Subject: [PATCH 03/12] regenerate API from CLIc --- pyclesperanto/_tier1.py | 76 ---------------------- pyclesperanto/_tier2.py | 135 ++++++++++++++++++++++++++++++++++++++++ pyclesperanto/_tier7.py | 67 ++++++++++++-------- pyclesperanto/_tier8.py | 46 ++++++++++++++ src/wrapper/tier1_.cpp | 10 --- src/wrapper/tier2_.cpp | 15 +++++ src/wrapper/tier8_.cpp | 18 ++++++ 7 files changed, 254 insertions(+), 113 deletions(-) create mode 100644 pyclesperanto/_tier8.py create mode 100644 src/wrapper/tier8_.cpp diff --git a/pyclesperanto/_tier1.py b/pyclesperanto/_tier1.py index 6d3a002f..37299e29 100644 --- a/pyclesperanto/_tier1.py +++ b/pyclesperanto/_tier1.py @@ -760,82 +760,6 @@ def detect_label_edges( -@plugin_function(category=['binarize', 'in assistant']) -def detect_maxima_box( - input_image: Image = 0, - output_image: Image = 0, - device: Device = None -) -> Image: - """Detects local maxima in a given square/cubic neighborhood. Pixels in the - resulting image are set to 1 if there is no other pixel in a given radius which - has a higher intensity, and to 0 otherwise. - - Parameters - ---------- - input_image: Image = 0 - Input image to process. - output_image: Image = 0 - Output result image. - device: Device = None - Device to perform the operation on. - - Returns - ------- - Image - - References - ---------- - [1] https://clij.github.io/clij2-docs/reference_detectMaximaBox - """ - - from ._pyclesperanto import _detect_maxima_box as op - - return op( - device=device, - src=input_image, - dst=output_image - ) - - - -@plugin_function(category=['binarize', 'in assistant']) -def detect_minima_box( - input_image: Image = 0, - output_image: Image = 0, - device: Device = None -) -> Image: - """Detects local maxima in a given square/cubic neighborhood. Pixels in the - resulting image are set to 1 if there is no other pixel in a given radius which - has a lower intensity, and to 0 otherwise. - - Parameters - ---------- - input_image: Image = 0 - Input image to process. - output_image: Image = 0 - Output result image. - device: Device = None - Device to perform the operation on. - - Returns - ------- - Image - - References - ---------- - [1] https://clij.github.io/clij2-docs/reference_detectMaximaBox - """ - - from ._pyclesperanto import _detect_minima_box as op - - return op( - device=device, - src=input_image, - dst=output_image - ) - - - @plugin_function(category=['binary processing']) def dilate_box( input_image: Image, diff --git a/pyclesperanto/_tier2.py b/pyclesperanto/_tier2.py index 4c2ac5d5..d111ab75 100644 --- a/pyclesperanto/_tier2.py +++ b/pyclesperanto/_tier2.py @@ -572,6 +572,106 @@ def degrees_to_radians( +@plugin_function(category=['binarize', 'in assistant']) +def detect_maxima_box( + input_image: Image = 0, + output_image: Image = 0, + radius_x: int = 0, + radius_y: int = 0, + radius_z: int = 0, + device: Device = None +) -> Image: + """Detects local maxima in a given square/cubic neighborhood. Pixels in the + resulting image are set to 1 if there is no other pixel in a given radius which + has a higher intensity, and to 0 otherwise. + + Parameters + ---------- + input_image: Image = 0 + Input image to process. + output_image: Image = 0 + Output result image. + radius_x: int = 0 + Radius of the region in X. + radius_y: int = 0 + Radius of the region in Y. + radius_z: int = 0 + Radius of the region in Z. + device: Device = None + Device to perform the operation on. + + Returns + ------- + Image + + References + ---------- + [1] https://clij.github.io/clij2-docs/reference_detectMaximaBox + """ + + from ._pyclesperanto import _detect_maxima_box as op + + return op( + device=device, + src=input_image, + dst=output_image, + radius_x=int(radius_x), + radius_y=int(radius_y), + radius_z=int(radius_z) + ) + + + +@plugin_function(category=['binarize', 'in assistant']) +def detect_minima_box( + input_image: Image = 0, + output_image: Image = 0, + radius_x: int = 0, + radius_y: int = 0, + radius_z: int = 0, + device: Device = None +) -> Image: + """Detects local maxima in a given square/cubic neighborhood. Pixels in the + resulting image are set to 1 if there is no other pixel in a given radius which + has a lower intensity, and to 0 otherwise. + + Parameters + ---------- + input_image: Image = 0 + Input image to process. + output_image: Image = 0 + Output result image. + radius_x: int = 0 + Radius of the region in X. + radius_y: int = 0 + Radius of the region in Y. + radius_z: int = 0 + Radius of the region in Z. + device: Device = None + Device to perform the operation on. + + Returns + ------- + Image + + References + ---------- + [1] https://clij.github.io/clij2-docs/reference_detectMaximaBox + """ + + from ._pyclesperanto import _detect_minima_box as op + + return op( + device=device, + src=input_image, + dst=output_image, + radius_x=int(radius_x), + radius_y=int(radius_y), + radius_z=int(radius_z) + ) + + + @plugin_function(category=['filter', 'background removal', 'bia-bob-suggestion']) def difference_of_gaussian( input_image: Image, @@ -985,6 +1085,41 @@ def radians_to_degrees( +@plugin_function(category=['label processing', 'in assistant', 'bia-bob-suggestion']) +def reduce_labels_to_label_edges_func( + input_image: Image, + output_image: Image = None, + device: Device = None +) -> Image: + """Takes a label map and reduces all labels to their edges. Label IDs stay and + background will be zero. + + Parameters + ---------- + input_image: Image + output_image: Image = None + device: Device = None + Device to perform the operation on. + + Returns + ------- + Image + + References + ---------- + [1] https://clij.github.io/clij2-docs/reference_reduceLabelsToLabelEdges + """ + + from ._pyclesperanto import _reduce_labels_to_label_edges_func as op + + return op( + device=device, + src=input_image, + dst=output_image + ) + + + @plugin_function(category=['filter', 'in assistant'], priority=-1) def small_hessian_eigenvalue( input_image: Image, diff --git a/pyclesperanto/_tier7.py b/pyclesperanto/_tier7.py index 84482b18..24f59f16 100644 --- a/pyclesperanto/_tier7.py +++ b/pyclesperanto/_tier7.py @@ -31,24 +31,27 @@ def rigid_transform( Parameters ---------- input_image: Image - target image + Image to be transformed output_image: Image = None - translation along x axis in pixels + Output image translate_x: float = 0 - translation along y axis in pixels + Translation along x axis in pixels translate_y: float = 0 - translation along z axis in pixels + Translation along y axis in pixels translate_z: float = 0 - rotation around x axis in radians + Translation along z axis in pixels angle_x: float = 0 - rotation around y axis in radians + Rotation around x axis in radians angle_y: float = 0 - rotation around z axis in radians + Rotation around y axis in radians angle_z: float = 0 + Rotation around z axis in radians centered: bool = True + If true, rotate image around center, else around the origin interpolate: bool = False - resize: bool = False If true, bi/trilinear interpolation will be applied, if hardware allows. + resize: bool = False + Automatically determines the size of the output depending on the rotation angles. device: Device = None Device to perform the operation on. @@ -96,18 +99,21 @@ def rotate( Parameters ---------- input_image: Image - target image + Image to be rotated output_image: Image = None - rotation around x axis in degrees + Output image angle_x: float = 0 - rotation around y axis in degrees + Rotation around x axis in degrees angle_y: float = 0 - rotation around z axis in degrees + Rotation around y axis in degrees angle_z: float = 0 + Rotation around z axis in degrees centered: bool = True + If true, rotate image around center, else around the origin interpolate: bool = False + If true, bi/trilinear interpolation will be applied, if hardware allows. resize: bool = False - If true, bi/trilinear interpolation will be applied, if hardware supports it. + Automatically determines the size of the output depending on the rotation angles. device: Device = None Device to perform the operation on. @@ -150,20 +156,21 @@ def scale( Parameters ---------- input_image: Image - target image + Image to be scaleded output_image: Image = None - scaling along x + Output image factor_x: float = 1 - scaling along y + Scaling along x factor_y: float = 1 - scaling along z + Scaling along y factor_z: float = 1 - If true, the image will be scaled to the center of the image. + Scaling along z centered: bool = True - If true, bi/trilinear interplation will be applied. + If true, the image will be scaled to the center of the image. interpolate: bool = False - Automatically determines output size image. + If true, bi/trilinear interplation will be applied. resize: bool = False + Automatically determines output size image. device: Device = None Device to perform the operation on. @@ -204,15 +211,15 @@ def translate( Parameters ---------- input_image: Image - image to be translated + Image to be translated output_image: Image = None - target image + Output image translate_x: float = 0 - translation along x axis in pixels + Translation along x axis in pixels translate_y: float = 0 - translation along y axis in pixels + Translation along y axis in pixels translate_z: float = 0 - translation along z axis in pixels + Translation along z axis in pixels interpolate: bool = False If true, bi/trilinear interplation will be applied. device: Device = None @@ -254,8 +261,11 @@ def closing_labels( Parameters ---------- input_image: Image + Input label image output_image: Image = None + Output label image radius: int = 0 + Radius size for the closing device: Device = None Device to perform the operation on. @@ -292,8 +302,11 @@ def opening_labels( Parameters ---------- input_image: Image + Input label image output_image: Image = None + Output label image radius: int = 0 + Radius size for the opening device: Device = None Device to perform the operation on. @@ -336,9 +349,9 @@ def voronoi_otsu_labeling( output_image: Image = None Output image spot_sigma: float = 2 - controls how close detected cells can be + Controls how close detected cells can be outline_sigma: float = 2 - controls how precise segmented objects are outlined. + Controls how precise segmented objects are outlined. device: Device = None Device to perform the operation on. diff --git a/pyclesperanto/_tier8.py b/pyclesperanto/_tier8.py new file mode 100644 index 00000000..5005896b --- /dev/null +++ b/pyclesperanto/_tier8.py @@ -0,0 +1,46 @@ +# +# This code is auto-generated from 'tier8.hpp' file, using 'gencle' script. +# Do not edit manually. +# + +from ._core import Device +from ._array import Image +from ._decorators import plugin_function +import numpy as np + + +@plugin_function(category=['label processing', 'in assistant', 'bia-bob-suggestion']) +def smooth_labels( + input_image: Image, + output_image: Image = None, + radius: int = 0, + device: Device = None +) -> Image: + """Apply a morphological opening operation to a label image and afterwards fills + gaps between the labels using voronoi-labeling. Finally, the result label + image is masked so that all background pixels remain background pixels. Note: + It is recommended to process isotropic label images. + + Parameters + ---------- + input_image: Image + output_image: Image = None + radius: int = 0 + device: Device = None + Device to perform the operation on. + + Returns + ------- + Image + + """ + + from ._pyclesperanto import _smooth_labels as op + + return op( + device=device, + src=input_image, + dst=output_image, + radius=int(radius) + ) + diff --git a/src/wrapper/tier1_.cpp b/src/wrapper/tier1_.cpp index 048de157..ff293f01 100644 --- a/src/wrapper/tier1_.cpp +++ b/src/wrapper/tier1_.cpp @@ -100,16 +100,6 @@ m.def("_detect_label_edges", &cle::tier1::detect_label_edges_func, "Call detect_ py::arg("device"), py::arg("src"), py::arg("dst")); -m.def("_detect_maxima_box", &cle::tier1::detect_maxima_box_func, "Call detect_maxima_box from C++.", - py::return_value_policy::take_ownership, - py::arg("device"), py::arg("src"), py::arg("dst")); - - -m.def("_detect_minima_box", &cle::tier1::detect_minima_box_func, "Call detect_minima_box from C++.", - py::return_value_policy::take_ownership, - py::arg("device"), py::arg("src"), py::arg("dst")); - - m.def("_dilate_box", &cle::tier1::dilate_box_func, "Call dilate_box from C++.", py::return_value_policy::take_ownership, py::arg("device"), py::arg("src"), py::arg("dst")); diff --git a/src/wrapper/tier2_.cpp b/src/wrapper/tier2_.cpp index d9b3ac06..e62426e2 100644 --- a/src/wrapper/tier2_.cpp +++ b/src/wrapper/tier2_.cpp @@ -80,6 +80,16 @@ m.def("_degrees_to_radians", &cle::tier2::degrees_to_radians_func, "Call degrees py::arg("device"), py::arg("src"), py::arg("dst")); +m.def("_detect_maxima_box", &cle::tier2::detect_maxima_box_func, "Call detect_maxima_box from C++.", + py::return_value_policy::take_ownership, + py::arg("device"), py::arg("src"), py::arg("dst"), py::arg("radius_x"), py::arg("radius_y"), py::arg("radius_z")); + + +m.def("_detect_minima_box", &cle::tier2::detect_minima_box_func, "Call detect_minima_box from C++.", + py::return_value_policy::take_ownership, + py::arg("device"), py::arg("src"), py::arg("dst"), py::arg("radius_x"), py::arg("radius_y"), py::arg("radius_z")); + + m.def("_difference_of_gaussian", &cle::tier2::difference_of_gaussian_func, "Call difference_of_gaussian from C++.", py::return_value_policy::take_ownership, py::arg("device"), py::arg("src"), py::arg("dst"), py::arg("sigma1_x"), py::arg("sigma1_y"), py::arg("sigma1_z"), py::arg("sigma2_x"), py::arg("sigma2_y"), py::arg("sigma2_z")); @@ -135,6 +145,11 @@ m.def("_radians_to_degrees", &cle::tier2::radians_to_degrees_func, "Call radians py::arg("device"), py::arg("src"), py::arg("dst")); +m.def("_reduce_labels_to_label_edges_func", &cle::tier2::reduce_labels_to_label_edges_func_func, "Call reduce_labels_to_label_edges_func from C++.", + py::return_value_policy::take_ownership, + py::arg("device"), py::arg("src"), py::arg("dst")); + + m.def("_small_hessian_eigenvalue", &cle::tier2::small_hessian_eigenvalue_func, "Call small_hessian_eigenvalue from C++.", py::return_value_policy::take_ownership, py::arg("device"), py::arg("src"), py::arg("dst")); diff --git a/src/wrapper/tier8_.cpp b/src/wrapper/tier8_.cpp new file mode 100644 index 00000000..92e8ea3f --- /dev/null +++ b/src/wrapper/tier8_.cpp @@ -0,0 +1,18 @@ + +// this code is auto-generated by the script 'pyclesperanto_autogen_tier_script.ipynb'. +// Do not edit manually. Instead, edit the script and run it again. + +#include "pycle_wrapper.hpp" +#include "tier8.hpp" + +namespace py = pybind11; + +auto tier8_(py::module &m) -> void { + + +m.def("_smooth_labels", &cle::tier8::smooth_labels_func, "Call smooth_labels from C++.", + py::return_value_policy::take_ownership, + py::arg("device"), py::arg("src"), py::arg("dst"), py::arg("radius")); + + +} From 96d92fee4aaced34a14f48043b724ecd11b1ab30 Mon Sep 17 00:00:00 2001 From: Robert Haase Date: Wed, 28 Feb 2024 13:35:38 +0100 Subject: [PATCH 04/12] corrected function name --- pyclesperanto/_tier2.py | 4 ++-- src/wrapper/tier2_.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyclesperanto/_tier2.py b/pyclesperanto/_tier2.py index d111ab75..75e7b4f4 100644 --- a/pyclesperanto/_tier2.py +++ b/pyclesperanto/_tier2.py @@ -1086,7 +1086,7 @@ def radians_to_degrees( @plugin_function(category=['label processing', 'in assistant', 'bia-bob-suggestion']) -def reduce_labels_to_label_edges_func( +def reduce_labels_to_label_edges( input_image: Image, output_image: Image = None, device: Device = None @@ -1110,7 +1110,7 @@ def reduce_labels_to_label_edges_func( [1] https://clij.github.io/clij2-docs/reference_reduceLabelsToLabelEdges """ - from ._pyclesperanto import _reduce_labels_to_label_edges_func as op + from ._pyclesperanto import _reduce_labels_to_label_edges as op return op( device=device, diff --git a/src/wrapper/tier2_.cpp b/src/wrapper/tier2_.cpp index e62426e2..ae36f1d0 100644 --- a/src/wrapper/tier2_.cpp +++ b/src/wrapper/tier2_.cpp @@ -145,7 +145,7 @@ m.def("_radians_to_degrees", &cle::tier2::radians_to_degrees_func, "Call radians py::arg("device"), py::arg("src"), py::arg("dst")); -m.def("_reduce_labels_to_label_edges_func", &cle::tier2::reduce_labels_to_label_edges_func_func, "Call reduce_labels_to_label_edges_func from C++.", +m.def("_reduce_labels_to_label_edges", &cle::tier2::reduce_labels_to_label_edges_func, "Call reduce_labels_to_label_edges from C++.", py::return_value_policy::take_ownership, py::arg("device"), py::arg("src"), py::arg("dst")); From 4585dcf876f51af49f1635d846f9d9d14f40ecc7 Mon Sep 17 00:00:00 2001 From: Robert Haase Date: Wed, 28 Feb 2024 13:48:54 +0100 Subject: [PATCH 05/12] added missing link to tier8 --- pyclesperanto/__init__.py | 1 + src/wrapper/pycle_wrapper.cpp | 1 + src/wrapper/pycle_wrapper.hpp | 1 + 3 files changed, 3 insertions(+) diff --git a/pyclesperanto/__init__.py b/pyclesperanto/__init__.py index 6d5d9ffc..b06d13c1 100644 --- a/pyclesperanto/__init__.py +++ b/pyclesperanto/__init__.py @@ -20,6 +20,7 @@ from ._tier5 import * from ._tier6 import * from ._tier7 import * +from ._tier8 import * from ._interroperability import * from ._version import VERSION as __version__ diff --git a/src/wrapper/pycle_wrapper.cpp b/src/wrapper/pycle_wrapper.cpp index edfee0d9..b2d58f1a 100644 --- a/src/wrapper/pycle_wrapper.cpp +++ b/src/wrapper/pycle_wrapper.cpp @@ -16,4 +16,5 @@ PYBIND11_MODULE(_pyclesperanto, m) tier5_(m); tier6_(m); tier7_(m); + tier8_(m); } diff --git a/src/wrapper/pycle_wrapper.hpp b/src/wrapper/pycle_wrapper.hpp index 7862f27f..83e68873 100644 --- a/src/wrapper/pycle_wrapper.hpp +++ b/src/wrapper/pycle_wrapper.hpp @@ -17,5 +17,6 @@ auto tier4_(pybind11::module_ &module) -> void; auto tier5_(pybind11::module_ &module) -> void; auto tier6_(pybind11::module_ &module) -> void; auto tier7_(pybind11::module_ &module) -> void; +auto tier8_(pybind11::module_ &module) -> void; #endif // __WRAPPER_PYCLESPERANTO_HPP From 4af777e024d7913f964b498b6e41991b12e06a84 Mon Sep 17 00:00:00 2001 From: Robert Haase Date: Wed, 28 Feb 2024 13:49:04 +0100 Subject: [PATCH 06/12] added tests --- tests/test_reduce_labels_to_label_edges.py | 35 +++++++++++++++++++ tests/test_smooth_labels.py | 39 ++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 tests/test_reduce_labels_to_label_edges.py create mode 100644 tests/test_smooth_labels.py diff --git a/tests/test_reduce_labels_to_label_edges.py b/tests/test_reduce_labels_to_label_edges.py new file mode 100644 index 00000000..b79b0fdf --- /dev/null +++ b/tests/test_reduce_labels_to_label_edges.py @@ -0,0 +1,35 @@ +import pyclesperanto as cle +import numpy as np + +def test_reduce_labels_to_label_edges(): + test = np.asarray([ + [0, 0, 0, 0, 0, 0, 0, 0], + [0,0,0,0, 1,1,1,0], + [0,0,2,0, 1,1,1,0], + [0,0,0,0, 1,1,1,0], + [0,3,3,3, 4,4,4,0], + [0,3,3,3, 4,4,4,0], + [0,3,3,3, 4,4,4,0], + [0,0,0,0, 0,0,0,0], + ]) + + reference = np.asarray([ + [0, 0, 0, 0, 0, 0, 0, 0], + [0,0,0,0, 1,1,1,0], + [0,0,2,0, 1,0,1,0], + [0,0,0,0, 1,1,1,0], + [0,3,3,3, 4,4,4,0], + [0,3,0,3, 4,0,4,0], + [0,3,3,3, 4,4,4,0], + [0,0,0,0, 0,0,0,0] + ]) + + result = cle.reduce_labels_to_label_edges(test) + + print(result) + print(reference) + + assert np.allclose(reference, result) + + + diff --git a/tests/test_smooth_labels.py b/tests/test_smooth_labels.py new file mode 100644 index 00000000..8742ba34 --- /dev/null +++ b/tests/test_smooth_labels.py @@ -0,0 +1,39 @@ +import pyclesperanto as cle +import numpy as np + +def test_smooth_labels_2d(): + + gpu_input = cle.push(np.asarray([ + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 2, 2, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 1, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + ])) + + gpu_reference = cle.push(np.asarray([ + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 2, 2, 2, 2], + ])) + + gpu_output = cle.smooth_labels(gpu_input, radius=3) + + a = cle.pull(gpu_output) + b = cle.pull(gpu_reference) + + print(a) + print(b) + + assert (np.array_equal(a, b)) + From 06bda524f375f3940e64a5094b81ff8efe9a4cfa Mon Sep 17 00:00:00 2001 From: Stephane Rigaud Date: Wed, 28 Feb 2024 14:52:36 +0100 Subject: [PATCH 07/12] fix: device name property --- src/wrapper/core_.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wrapper/core_.cpp b/src/wrapper/core_.cpp index 805b8ddd..0b64d75b 100644 --- a/src/wrapper/core_.cpp +++ b/src/wrapper/core_.cpp @@ -21,7 +21,9 @@ auto core_(pybind11::module_ &m) -> void .value("CUDA", cle::Device::Type::CUDA); py::class_>(m, "_Device") - .def_property_readonly("name", &cle::Device::getName) + // .def_property_readonly("name", &cle::Device::getName, py::arg("lowercase")) + .def_property_readonly("name", [](const cle::Device &device) + { return device.getName(false); }) .def_property_readonly("info", &cle::Device::getInfo) .def_property_readonly("type", &cle::Device::getType) .def("set_wait_to_finish", &cle::Device::setWaitToFinish) From 9f79efb10ddbe47458b2226f56f55e34564096d8 Mon Sep 17 00:00:00 2001 From: Stephane Rigaud Date: Wed, 28 Feb 2024 14:54:46 +0100 Subject: [PATCH 08/12] silencing wind build CI because #154 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 14a77783..edef8fc6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: python-version: ["3.9"] - platform: [ubuntu-latest, macos-latest, windows-latest] + platform: [ubuntu-latest, macos-latest] #, windows-latest] defaults: run: shell: "bash -l {0}" From c08066d311468c5922f2a2210ce81bae4c082a51 Mon Sep 17 00:00:00 2001 From: Robert Haase Date: Wed, 28 Feb 2024 15:13:51 +0100 Subject: [PATCH 09/12] add eroded_otsu- and gauss_otsu-labeling --- pyclesperanto/_tier6.py | 45 +++++++++++++++++++++++++ pyclesperanto/_tier7.py | 53 ++++++++++++++++++++++++++++++ src/wrapper/tier6_.cpp | 5 +++ src/wrapper/tier7_.cpp | 5 +++ tests/test_eroded_otsu_labeling.py | 39 ++++++++++++++++++++++ tests/test_gauss_otsu_labeling.py | 39 ++++++++++++++++++++++ 6 files changed, 186 insertions(+) create mode 100644 tests/test_eroded_otsu_labeling.py create mode 100644 tests/test_gauss_otsu_labeling.py diff --git a/pyclesperanto/_tier6.py b/pyclesperanto/_tier6.py index 4c9010a1..d94b977e 100644 --- a/pyclesperanto/_tier6.py +++ b/pyclesperanto/_tier6.py @@ -89,6 +89,51 @@ def erode_labels( +@plugin_function(category=['label', 'in assistant', 'bia-bob-suggestion']) +def gauss_otsu_labeling( + input_image0: Image, + output_image: Image = None, + outline_sigma: float = 0, + device: Device = None +) -> Image: + """Labels objects directly from grey-value images. The outline_sigma parameter + allows tuning the segmentation result. Under the hood, this filter applies a + Gaussian blur, Otsu-thresholding [1] and connected component labeling [2]. The + thresholded binary image is flooded using the Voronoi tesselation approach + starting from the found local maxima. + + Parameters + ---------- + input_image0: Image + intensity image to add labels + output_image: Image = None + Output label image. + outline_sigma: float = 0 + Gaussian blur sigma along all axes + device: Device = None + Device to perform the operation on. + + Returns + ------- + Image + + References + ---------- + [1] https://ieeexplore.ieee.org/document/4310076 + [2] https://en.wikipedia.org/wiki/Connected-component_labeling + """ + + from ._pyclesperanto import _gauss_otsu_labeling as op + + return op( + device=device, + src0=input_image0, + dst=output_image, + outline_sigma=float(outline_sigma) + ) + + + @plugin_function(category=['label', 'bia-bob-suggestion']) def masked_voronoi_labeling( input_image: Image, diff --git a/pyclesperanto/_tier7.py b/pyclesperanto/_tier7.py index 24f59f16..280b4460 100644 --- a/pyclesperanto/_tier7.py +++ b/pyclesperanto/_tier7.py @@ -9,6 +9,59 @@ import numpy as np +@plugin_function(category=['label', 'in assistant', 'bia-bob-suggestion']) +def eroded_otsu_labeling( + input_image: Image, + output_image: Image = None, + number_of_erosions: int = 5, + outline_sigma: float = 2, + device: Device = None +) -> Image: + """Segments and labels an image using blurring, Otsu-thresholding, binary erosion + and masked Voronoi-labeling. After bluring and Otsu-thresholding the image, + iterative binary erosion is applied. Objects in the eroded image are labeled + and the labels are extended to fit again into the initial binary image using + masked-Voronoi labeling. This function is similar to voronoi_otsu_labeling. It + is intended to deal better in case labels of objects swapping into each other + if objects are dense. Like when using Voronoi-Otsu-labeling, small objects may + disappear when applying this operation. This function is inspired by a similar + implementation in Java by Jan Brocher (Biovoxxel) [0] [1] + + Parameters + ---------- + input_image: Image + Image to be transformed + output_image: Image = None + Output image + number_of_erosions: int = 5 + Number of iteration of erosion + outline_sigma: float = 2 + Gaussian blur sigma applied before Otsu thresholding + device: Device = None + Device to perform the operation on. + + Returns + ------- + Image + + References + ---------- + [1] [0] https://github.com/biovoxxel/bv3dbox (BV_LabelSplitter.java#L83) + [2] [1] https://zenodo.org/badge/latestdoi/434949702 + """ + + from ._pyclesperanto import _eroded_otsu_labeling as op + + return op( + device=device, + src=input_image, + dst=output_image, + number_of_erosions=int(number_of_erosions), + outline_sigma=float(outline_sigma) + ) + + + @plugin_function(category=['transform', 'in assistant', 'bia-bob-suggestion']) def rigid_transform( input_image: Image, diff --git a/src/wrapper/tier6_.cpp b/src/wrapper/tier6_.cpp index 0d1549b2..ef412952 100644 --- a/src/wrapper/tier6_.cpp +++ b/src/wrapper/tier6_.cpp @@ -20,6 +20,11 @@ m.def("_erode_labels", &cle::tier6::erode_labels_func, "Call erode_labels from C py::arg("device"), py::arg("src"), py::arg("dst"), py::arg("radius"), py::arg("relabel")); +m.def("_gauss_otsu_labeling", &cle::tier6::gauss_otsu_labeling_func, "Call gauss_otsu_labeling from C++.", + py::return_value_policy::take_ownership, + py::arg("device"), py::arg("src0"), py::arg("dst"), py::arg("outline_sigma")); + + m.def("_masked_voronoi_labeling", &cle::tier6::masked_voronoi_labeling_func, "Call masked_voronoi_labeling from C++.", py::return_value_policy::take_ownership, py::arg("device"), py::arg("src"), py::arg("mask"), py::arg("dst")); diff --git a/src/wrapper/tier7_.cpp b/src/wrapper/tier7_.cpp index 7b7e57e1..5041b223 100644 --- a/src/wrapper/tier7_.cpp +++ b/src/wrapper/tier7_.cpp @@ -10,6 +10,11 @@ namespace py = pybind11; auto tier7_(py::module &m) -> void { +m.def("_eroded_otsu_labeling", &cle::tier7::eroded_otsu_labeling_func, "Call eroded_otsu_labeling from C++.", + py::return_value_policy::take_ownership, + py::arg("device"), py::arg("src"), py::arg("dst"), py::arg("number_of_erosions"), py::arg("outline_sigma")); + + m.def("_rigid_transform", &cle::tier7::rigid_transform_func, "Call rigid_transform from C++.", py::return_value_policy::take_ownership, py::arg("device"), py::arg("src"), py::arg("dst"), py::arg("translate_x"), py::arg("translate_y"), py::arg("translate_z"), py::arg("angle_x"), py::arg("angle_y"), py::arg("angle_z"), py::arg("centered"), py::arg("interpolate"), py::arg("resize")); diff --git a/tests/test_eroded_otsu_labeling.py b/tests/test_eroded_otsu_labeling.py new file mode 100644 index 00000000..bb6772e9 --- /dev/null +++ b/tests/test_eroded_otsu_labeling.py @@ -0,0 +1,39 @@ +import pyclesperanto as cle +import numpy as np + +def test_eroded_otsu_labeling(): + + gpu_input = cle.push(np.asarray([ + + [0, 0, 5, 5, 0, 0], + [0, 5, 8, 9, 1, 0], + [0, 5, 7, 6, 1, 1], + [0, 0, 0, 5, 5, 1], + [0, 0, 0, 5, 5, 1], + [0, 0, 5, 5, 5, 1], + [0, 0, 5, 5, 5, 0], + + ])) + + + gpu_reference = cle.push(np.asarray([ + + [0, 0, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 0], + [0, 0, 0, 2, 2, 0], + [0, 0, 2, 2, 2, 0], + [0, 0, 2, 2, 2, 0], + + ])) + + gpu_output = cle.eroded_otsu_labeling(gpu_input, number_of_erosions=1, outline_sigma=1) + + a = cle.pull(gpu_output) + b = cle.pull(gpu_reference) + + print(a) + print(b) + + assert (np.array_equal(a, b)) diff --git a/tests/test_gauss_otsu_labeling.py b/tests/test_gauss_otsu_labeling.py new file mode 100644 index 00000000..d72d427b --- /dev/null +++ b/tests/test_gauss_otsu_labeling.py @@ -0,0 +1,39 @@ +import pyclesperanto as cle +import numpy as np + +def test_gauss_otsu_labeling(): + + gpu_input = cle.push(np.asarray([ + + [0, 0, 1, 1, 0, 0], + [0, 1, 8, 9, 1, 0], + [0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 0], + [0, 0, 1, 8, 7, 1], + [0, 0, 1, 1, 1, 0], + + ])) + + + gpu_reference = cle.push(np.asarray([ + + [0, 0, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 2, 2, 0], + [0, 0, 2, 2, 2, 2], + [0, 0, 2, 2, 2, 0], + + ])) + + gpu_output = cle.gauss_otsu_labeling(gpu_input, outline_sigma=1) + + a = cle.pull(gpu_output) + b = cle.pull(gpu_reference) + + print(a) + print(b) + + assert (np.array_equal(a, b)) From b5487310713cc6ed5d6ac6d73e4a1cc6df3a6636 Mon Sep 17 00:00:00 2001 From: Robert Haase Date: Wed, 28 Feb 2024 15:25:41 +0100 Subject: [PATCH 10/12] bump version of CLIc dependency --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4206378b..08845562 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,7 @@ option(BUILD_SHARED_LIBS OFF) ## CLIc dependency -set(CLIC_REPO_TAG master) # branch name for dev +set(CLIC_REPO_TAG 0.8.2) # branch name for dev set(CLIC_REPO_URL https://github.com/clEsperanto/CLIc.git) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/clic EXCLUDE_FROM_ALL) From 11b326b0626b06980eb0535012a8a43a2bea9201 Mon Sep 17 00:00:00 2001 From: Robert Haase Date: Wed, 28 Feb 2024 15:26:49 +0100 Subject: [PATCH 11/12] update email and insitution --- LICENSE | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 4b733547..d6bfff0d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ -Copyright 2020 Stéphane Rigaud, Robert Haase, -Institut Pasteur Paris, Max Planck Institute for Molecular Cell Biology and Genetics Dresden +Copyright 2024 Stéphane Rigaud, Robert Haase, +Institut Pasteur Paris, Max Planck Institute for Molecular Cell Biology and Genetics Dresden, ScaDS.AI, Leipzig University Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/pyproject.toml b/pyproject.toml index 3d25b3d3..7e7ec58c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ exclude = [".flake8", ".git*", "*.yaml", "*.ipynb", "*.yml", "docs"] [project] authors = [ { name = "Stephane Rigaud", email = "stephane.rigaud@pasteur.fr" }, - { name = "Robert Haase", email = "robert.haase@tu-dresden.de" }, + { name = "Robert Haase", email = "robert.haase@uni-leipzig.de" }, ] classifiers = [ "Development Status :: 3 - Alpha", From 2a6ec8c1022ceb59a8294c4efceaca2ef6a9d96a Mon Sep 17 00:00:00 2001 From: Robert Haase Date: Wed, 28 Feb 2024 15:27:33 +0100 Subject: [PATCH 12/12] minor --- LICENSE | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index d6bfff0d..dbbf8015 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,4 @@ -Copyright 2024 Stéphane Rigaud, Robert Haase, -Institut Pasteur Paris, Max Planck Institute for Molecular Cell Biology and Genetics Dresden, ScaDS.AI, Leipzig University +Copyright 2024 Stéphane Rigaud, Robert Haase, Institut Pasteur Paris, Max Planck Institute for Molecular Cell Biology and Genetics Dresden, ScaDS.AI, Leipzig University Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: