From 9477daa28b5d1c429078aae47bf483af704ef98a Mon Sep 17 00:00:00 2001 From: Gregory Lee Date: Thu, 16 Jan 2025 08:04:28 -0500 Subject: [PATCH] Incorporate upstream changes from scikit-image 0.25 (#806) This MR updates for consistency with scikit-image v0.25 (released on Dec 13, 2024). The main API change is deprecating footprint functions `square`, `rectangle` and `cube` in favor of a general `footprint_rectangle` method. The supported version range of various dependencies have also been bumped. Authors: - Gregory Lee (https://github.com/grlee77) Approvers: - Gigon Bae (https://github.com/gigony) - Bradley Dice (https://github.com/bdice) - https://github.com/jakirkham URL: https://github.com/rapidsai/cucim/pull/806 --- .../all_cuda-118_arch-x86_64.yaml | 14 +- .../all_cuda-125_arch-x86_64.yaml | 14 +- dependencies.yaml | 18 +- python/cucim/pyproject.toml | 16 +- python/cucim/src/cucim/__init__.py | 4 +- python/cucim/src/cucim/skimage/__init__.py | 40 ++- python/cucim/src/cucim/skimage/__init__.pyi | 2 - .../src/cucim/skimage/_shared/filters.py | 11 +- .../src/cucim/skimage/_shared/testing.py | 11 +- .../cucim/skimage/_shared/tests/test_utils.py | 18 +- .../cucim/src/cucim/skimage/_shared/utils.py | 17 +- .../cucim/src/cucim/skimage/color/__init__.py | 2 + .../cucim/src/cucim/skimage/data/__init__.py | 4 +- .../src/cucim/skimage/exposure/__init__.py | 6 +- .../src/cucim/skimage/feature/__init__.py | 6 +- .../src/cucim/skimage/filters/__init__.py | 6 +- .../cucim/src/cucim/skimage/filters/_gabor.py | 27 +- .../skimage/filters/tests/test_gaussian.py | 11 - .../filters/tests/test_thresholding.py | 18 -- .../src/cucim/skimage/measure/__init__.py | 6 +- .../src/cucim/skimage/measure/_regionprops.py | 18 +- .../src/cucim/skimage/metrics/__init__.py | 6 +- .../skimage/metrics/_contingency_table.py | 15 +- .../src/cucim/skimage/morphology/__init__.py | 15 +- .../cucim/skimage/morphology/_skeletonize.py | 19 +- .../cucim/skimage/morphology/footprints.py | 232 +++++++++++------- .../src/cucim/skimage/morphology/gray.py | 24 +- .../skimage/morphology/tests/test_binary.py | 57 ++--- .../morphology/tests/test_footprints.py | 86 ++++--- .../skimage/morphology/tests/test_gray.py | 60 ++--- .../morphology/tests/test_skeletonize.py | 56 +++-- .../cucim/skimage/registration/__init__.py | 6 +- .../skimage/registration/_optical_flow.py | 4 +- .../registration/_optical_flow_utils.py | 3 +- .../src/cucim/skimage/restoration/__init__.py | 6 +- .../skimage/restoration/deconvolution.py | 7 +- .../cucim/skimage/segmentation/__init__.py | 3 +- .../src/cucim/skimage/segmentation/_join.py | 2 +- .../cucim/skimage/segmentation/boundaries.py | 4 +- .../src/cucim/skimage/transform/__init__.py | 6 +- .../src/cucim/skimage/transform/_geometric.py | 58 +---- .../cucim/src/cucim/skimage/util/__init__.py | 5 + .../src/cucim/skimage/util/_map_array.py | 2 +- 43 files changed, 492 insertions(+), 453 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 66b66b94f..c66bda1c6 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -16,12 +16,12 @@ dependencies: - gcc_linux-64=11.* - imagecodecs>=2021.6.8 - ipython -- lazy_loader>=0.1 +- lazy-loader>=0.4 - libcufile-dev=1.4.0.31 - libcufile=1.4.0.31 - libnvjpeg-dev=11.6.0.55 - libnvjpeg=11.6.0.55 -- matplotlib-base +- matplotlib-base>=3.7 - nbsphinx - ninja - numpy>=1.23.4,<3.0a0 @@ -36,15 +36,15 @@ dependencies: - pytest-cov>=2.12.1 - pytest-lazy-fixtures>=1.0.0 - pytest-xdist -- pytest>=6.2.4,<8.0.0a0 +- pytest>=7.0.0,<9.0.0a0 - python>=3.10,<3.13 -- pywavelets>=1.0 +- pywavelets>=1.6 - recommonmark -- scikit-image>=0.19.0,<0.25.0a0 -- scipy>=1.6.0 +- scikit-image>=0.19.0,<0.26.0a0 +- scipy>=1.11.2 - sphinx<6 - sysroot_linux-64==2.17 -- tifffile>=2022.7.28 +- tifffile>=2022.8.12 - yasm - pip: - opencv-python-headless>=4.6 diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml index 392850e85..dedab4a64 100644 --- a/conda/environments/all_cuda-125_arch-x86_64.yaml +++ b/conda/environments/all_cuda-125_arch-x86_64.yaml @@ -17,11 +17,11 @@ dependencies: - gcc_linux-64=11.* - imagecodecs>=2021.6.8 - ipython -- lazy_loader>=0.1 +- lazy-loader>=0.4 - libcufile-dev - libnvjpeg-dev - libnvjpeg-static -- matplotlib-base +- matplotlib-base>=3.7 - nbsphinx - ninja - numpy>=1.23.4,<3.0a0 @@ -35,15 +35,15 @@ dependencies: - pytest-cov>=2.12.1 - pytest-lazy-fixtures>=1.0.0 - pytest-xdist -- pytest>=6.2.4,<8.0.0a0 +- pytest>=7.0.0,<9.0.0a0 - python>=3.10,<3.13 -- pywavelets>=1.0 +- pywavelets>=1.6 - recommonmark -- scikit-image>=0.19.0,<0.25.0a0 -- scipy>=1.6.0 +- scikit-image>=0.19.0,<0.26.0a0 +- scipy>=1.11.2 - sphinx<6 - sysroot_linux-64==2.17 -- tifffile>=2022.7.28 +- tifffile>=2022.8.12 - yasm - pip: - opencv-python-headless>=4.6 diff --git a/dependencies.yaml b/dependencies.yaml index 5ffab20b2..dd6858e55 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -258,10 +258,10 @@ dependencies: - output_types: [conda, requirements, pyproject] packages: - click - - lazy_loader>=0.1 + - lazy-loader>=0.4 - numpy>=1.23.4,<3.0a0 - - scikit-image>=0.19.0,<0.25.0a0 - - scipy>=1.6.0 + - scikit-image>=0.19.0,<0.26.0a0 + - scipy>=1.11.2 - output_types: conda packages: - cupy>=12.0.0 @@ -285,17 +285,17 @@ dependencies: - output_types: [conda, requirements, pyproject] packages: - psutil>=5.8.0 - - pytest>=6.2.4,<8.0.0a0 + - pytest>=7.0.0,<9.0.0a0 - pytest-cov>=2.12.1 - pytest-lazy-fixtures>=1.0.0 - pytest-xdist - - tifffile>=2022.7.28 + - tifffile>=2022.8.12 - pooch>=1.6.0 # needed to download scikit-image sample data - - pywavelets>=1.0 + - pywavelets>=1.6 - output_types: [conda] packages: - imagecodecs>=2021.6.8 - - matplotlib-base + - matplotlib-base>=3.7 - openslide-python>=1.3.0 - pip - pip: @@ -305,9 +305,9 @@ dependencies: # skip packages on arm64 that don't provide a wheel - imagecodecs>=2021.6.8; platform_machine=='x86_64' - openslide-python>=1.3.0; platform_machine=='x86_64' - - matplotlib + - matplotlib>=3.7 - opencv-python-headless>=4.6 - output_types: [pyproject] packages: # Already added to requirements via docs. This is for tests. - - numpydoc>=1.5 + - numpydoc>=1.7 diff --git a/python/cucim/pyproject.toml b/python/cucim/pyproject.toml index 8b54160a5..d59f852fa 100644 --- a/python/cucim/pyproject.toml +++ b/python/cucim/pyproject.toml @@ -26,10 +26,10 @@ requires-python = ">=3.10" dependencies = [ "click", "cupy-cuda11x>=12.0.0", - "lazy_loader>=0.1", + "lazy-loader>=0.4", "numpy>=1.23.4,<3.0a0", - "scikit-image>=0.19.0,<0.25.0a0", - "scipy>=1.6.0", + "scikit-image>=0.19.0,<0.26.0a0", + "scipy>=1.11.2", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ "Development Status :: 4 - Beta", @@ -58,8 +58,8 @@ Tracker = "https://github.com/rapidsai/cucim/issues" [project.optional-dependencies] test = [ "imagecodecs>=2021.6.8; platform_machine=='x86_64'", - "matplotlib", - "numpydoc>=1.5", + "matplotlib>=3.7", + "numpydoc>=1.7", "opencv-python-headless>=4.6", "openslide-python>=1.3.0; platform_machine=='x86_64'", "pooch>=1.6.0", @@ -67,9 +67,9 @@ test = [ "pytest-cov>=2.12.1", "pytest-lazy-fixtures>=1.0.0", "pytest-xdist", - "pytest>=6.2.4,<8.0.0a0", - "pywavelets>=1.0", - "tifffile>=2022.7.28", + "pytest>=7.0.0,<9.0.0a0", + "pywavelets>=1.6", + "tifffile>=2022.8.12", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. developer = [ "black", diff --git a/python/cucim/src/cucim/__init__.py b/python/cucim/src/cucim/__init__.py index b655754be..67be564d0 100644 --- a/python/cucim/src/cucim/__init__.py +++ b/python/cucim/src/cucim/__init__.py @@ -59,9 +59,9 @@ except ImportError: pass -import lazy_loader as lazy +import lazy_loader as _lazy -__getattr__, __lazy_dir__, _ = lazy.attach_stub(__name__, __file__) +__getattr__, __lazy_dir__, _ = _lazy.attach_stub(__name__, __file__) def __dir__(): diff --git a/python/cucim/src/cucim/skimage/__init__.py b/python/cucim/src/cucim/skimage/__init__.py index 6bed31765..3027d09ce 100644 --- a/python/cucim/src/cucim/skimage/__init__.py +++ b/python/cucim/src/cucim/skimage/__init__.py @@ -17,21 +17,24 @@ exposure Image intensity adjustment, e.g., histogram equalization, etc. feature - Feature detection and extraction, e.g., texture analysis corners, etc. + Feature detection and extraction, e.g., texture analysis, corners, etc. filters Sharpening, edge finding, rank filters, thresholding, etc. measure - Measurement of image properties, e.g., region properties and contours. + Measurement of image properties, e.g., region properties, moments. metrics - Metrics corresponding to images, e.g. distance metrics, similarity, etc. + Metrics corresponding to images, e.g., distance metrics, similarity, etc. morphology - Morphological operations, e.g., opening or skeletonization. + Morphological algorithms, e.g., closing, opening, skeletonization. +registration + Image registration algorithms, e.g., optical flow or phase cross + correlation. restoration Restoration algorithms, e.g., deconvolution algorithms, denoising, etc. segmentation - Partitioning an image into multiple regions. + Algorithms to partition images into meaningful regions or boundaries. transform - Geometric and other transforms, e.g., rotation or the Radon transform. + Geometric and other transformations, e.g., rotations, warp. util Generic utilities. @@ -60,13 +63,32 @@ """ -import lazy_loader as lazy +import lazy_loader as _lazy -__getattr__, __lazy_dir__, _ = lazy.attach_stub(__name__, __file__) +__getattr__, __lazy_dir__, _ = _lazy.attach_stub(__name__, __file__) + +# Don't use the `__all__` and `__dir__` returned by `attach_stub` since that +# one would expose utility functions we don't want to advertise in our +# top-level module anymore. +__all__ = [ + "color", + "data", + "exposure", + "feature", + "filters", + "measure", + "metrics", + "morphology", + "registration", + "restoration", + "segmentation", + "transform", + "util", +] def __dir__(): - return __lazy_dir__() + return __all__.copy() # Legacy imports into the root namespace; not advertised in __all__ diff --git a/python/cucim/src/cucim/skimage/__init__.pyi b/python/cucim/src/cucim/skimage/__init__.pyi index 8b608db5b..a3666b737 100644 --- a/python/cucim/src/cucim/skimage/__init__.pyi +++ b/python/cucim/src/cucim/skimage/__init__.pyi @@ -1,5 +1,3 @@ -import lazy_loader as lazy # noqa: F401 - submodules = [ "color", "data", diff --git a/python/cucim/src/cucim/skimage/_shared/filters.py b/python/cucim/src/cucim/skimage/_shared/filters.py index 3eeb72774..0960fe291 100644 --- a/python/cucim/src/cucim/skimage/_shared/filters.py +++ b/python/cucim/src/cucim/skimage/_shared/filters.py @@ -10,21 +10,12 @@ import cucim.skimage._vendored.ndimage as ndi -from .._shared.utils import ( - DEPRECATED, - _supported_float_type, - convert_to_float, - deprecate_parameter, -) +from .._shared.utils import _supported_float_type, convert_to_float -@deprecate_parameter( - "output", new_name="out", start_version="24.06", stop_version="25.02" -) def gaussian( image, sigma=1, - output=DEPRECATED, mode="nearest", cval=0, preserve_range=False, diff --git a/python/cucim/src/cucim/skimage/_shared/testing.py b/python/cucim/src/cucim/skimage/_shared/testing.py index 911d9cda1..0d25c54cc 100644 --- a/python/cucim/src/cucim/skimage/_shared/testing.py +++ b/python/cucim/src/cucim/skimage/_shared/testing.py @@ -50,7 +50,7 @@ def assert_stacklevel(warnings, *, offset=-1): When cucim.skimage raises warnings, the stacklevel should ideally be set so that the origin of the warnings will point to the public function that was called by the user and not necessarily the very place where the - warnings were emitted (which may be inside of some internal function). + warnings were emitted (which may be inside some internal function). This utility function helps with checking that the stacklevel was set correctly on warnings captured by `pytest.warns`. @@ -84,6 +84,8 @@ def assert_stacklevel(warnings, *, offset=-1): ... ) ... assert_stacklevel(record, offset=-3) """ + __tracebackhide__ = True # Hide traceback for py.test + frame = inspect.stack()[1].frame # 0 is current frame, 1 is outer frame line_number = frame.f_lineno + offset filename = frame.f_code.co_filename @@ -91,3 +93,10 @@ def assert_stacklevel(warnings, *, offset=-1): for warning in warnings: actual = f"{warning.filename}:{warning.lineno}" assert actual == expected, f"{actual} != {expected}" + msg = ( + "Warning with wrong stacklevel:\n" + f" Expected: {expected}\n" + f" Actual: {actual}\n" + f" {warning.category.__name__}: {warning.message}" + ) + assert actual == expected, msg diff --git a/python/cucim/src/cucim/skimage/_shared/tests/test_utils.py b/python/cucim/src/cucim/skimage/_shared/tests/test_utils.py index 8ae5ad9bd..9d1669612 100644 --- a/python/cucim/src/cucim/skimage/_shared/tests/test_utils.py +++ b/python/cucim/src/cucim/skimage/_shared/tests/test_utils.py @@ -319,23 +319,13 @@ def test_warning_removed_param(self): r".*_func_deprecated_params`." ) with pytest.warns(FutureWarning, match=match): - assert _func_deprecated_params(1, 2) == ( - 1, - DEPRECATED, - DEPRECATED, - None, - ) + assert _func_deprecated_params(1, 2) == (1, 2, DEPRECATED, None) with pytest.warns(FutureWarning, match=match): - assert _func_deprecated_params(1, 2, 3) == ( - 1, - DEPRECATED, - DEPRECATED, - None, - ) + assert _func_deprecated_params(1, 2, 3) == (1, 2, 3, None) with pytest.warns(FutureWarning, match=match): assert _func_deprecated_params(1, old0=2) == ( 1, - DEPRECATED, + 2, DEPRECATED, None, ) @@ -343,7 +333,7 @@ def test_warning_removed_param(self): assert _func_deprecated_params(1, old1=2) == ( 1, DEPRECATED, - DEPRECATED, + 2, None, ) diff --git a/python/cucim/src/cucim/skimage/_shared/utils.py b/python/cucim/src/cucim/skimage/_shared/utils.py index d5da200b6..7312eaf4c 100644 --- a/python/cucim/src/cucim/skimage/_shared/utils.py +++ b/python/cucim/src/cucim/skimage/_shared/utils.py @@ -294,14 +294,19 @@ def fixed_func(*args, **kwargs): # Extract value of deprecated parameter if len(args) > deprecated_idx: deprecated_value = args[deprecated_idx] - args = ( - args[:deprecated_idx] - + (DEPRECATED,) - + args[deprecated_idx + 1 :] - ) + # Overwrite old with DEPRECATED if replacement exists + if self.new_name is not None: + args = ( + args[:deprecated_idx] + + (DEPRECATED,) + + args[deprecated_idx + 1 :] + ) if self.deprecated_name in kwargs.keys(): deprecated_value = kwargs[self.deprecated_name] - kwargs[self.deprecated_name] = DEPRECATED + # Overwrite old with DEPRECATED if replacement exists + if self.new_name is not None: + kwargs[self.deprecated_name] = DEPRECATED + # Extract value of new parameter (if present) if new_idx is not False and len(args) > new_idx: new_value = args[new_idx] diff --git a/python/cucim/src/cucim/skimage/color/__init__.py b/python/cucim/src/cucim/skimage/color/__init__.py index b3d396559..1f041af07 100644 --- a/python/cucim/src/cucim/skimage/color/__init__.py +++ b/python/cucim/src/cucim/skimage/color/__init__.py @@ -1,3 +1,5 @@ +"""Color space conversion.""" + from .colorconv import ( ahx_from_rgb, bex_from_rgb, diff --git a/python/cucim/src/cucim/skimage/data/__init__.py b/python/cucim/src/cucim/skimage/data/__init__.py index 62d86f776..5f4fdf9cb 100644 --- a/python/cucim/src/cucim/skimage/data/__init__.py +++ b/python/cucim/src/cucim/skimage/data/__init__.py @@ -1,3 +1,3 @@ -import lazy_loader as lazy +import lazy_loader as _lazy -__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/python/cucim/src/cucim/skimage/exposure/__init__.py b/python/cucim/src/cucim/skimage/exposure/__init__.py index 62d86f776..45d6ef21e 100644 --- a/python/cucim/src/cucim/skimage/exposure/__init__.py +++ b/python/cucim/src/cucim/skimage/exposure/__init__.py @@ -1,3 +1,5 @@ -import lazy_loader as lazy +"""Image intensity adjustment, e.g., histogram equalization, etc.""" -__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/python/cucim/src/cucim/skimage/feature/__init__.py b/python/cucim/src/cucim/skimage/feature/__init__.py index 62d86f776..e94d63785 100644 --- a/python/cucim/src/cucim/skimage/feature/__init__.py +++ b/python/cucim/src/cucim/skimage/feature/__init__.py @@ -1,3 +1,5 @@ -import lazy_loader as lazy +"""Feature detection and extraction, e.g., blobs, corners, etc.""" -__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/python/cucim/src/cucim/skimage/filters/__init__.py b/python/cucim/src/cucim/skimage/filters/__init__.py index 62d86f776..b91afc7bf 100644 --- a/python/cucim/src/cucim/skimage/filters/__init__.py +++ b/python/cucim/src/cucim/skimage/filters/__init__.py @@ -1,3 +1,5 @@ -import lazy_loader as lazy +"""Sharpening, edge finding, thresholding, etc.""" -__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/python/cucim/src/cucim/skimage/filters/_gabor.py b/python/cucim/src/cucim/skimage/filters/_gabor.py index 209e67eeb..8e25c5d6b 100644 --- a/python/cucim/src/cucim/skimage/filters/_gabor.py +++ b/python/cucim/src/cucim/skimage/filters/_gabor.py @@ -79,20 +79,19 @@ def gabor_kernel( -------- >>> import cupy as cp >>> from cucim.skimage.filters import gabor_kernel - >>> from skimage import io >>> from matplotlib import pyplot as plt # doctest: +SKIP >>> gk = gabor_kernel(frequency=0.2) - >>> plt.figure() # doctest: +SKIP - >>> io.imshow(cp.asnumpy(gk.real)) # doctest: +SKIP - >>> io.show() # doctest: +SKIP + >>> fig, ax = plt.subplots() # doctest: +SKIP + >>> ax.imshow(cp.asnumpy(gk.real)) # doctest: +SKIP + >>> plt.show() # doctest: +SKIP >>> # more ripples (equivalent to increasing the size of the >>> # Gaussian spread) >>> gk = gabor_kernel(frequency=0.2, bandwidth=0.1) - >>> plt.figure() # doctest: +SKIP - >>> io.imshow(cp.asnumpy(gk.real)) # doctest: +SKIP - >>> io.show() # doctest: +SKIP + >>> fig, ax = plt.subplots() # doctest: +SKIP + >>> ax.imshow(cp.asnumpy(gk.real)) # doctest: +SKIP + >>> plt.show() # doctest: +SKIP """ # noqa if sigma_x is None: sigma_x = _sigma_prefactor(bandwidth) / frequency @@ -209,21 +208,21 @@ def gabor( -------- >>> import cupy as cp >>> from cucim.skimage.filters import gabor - >>> from skimage import data, io + >>> from skimage import data >>> from matplotlib import pyplot as plt # doctest: +SKIP >>> image = cp.array(data.coins()) >>> # detecting edges in a coin image >>> filt_real, filt_imag = gabor(image, frequency=0.6) - >>> plt.figure() # doctest: +SKIP - >>> io.imshow(cp.asnumpy(filt_real)) # doctest: +SKIP - >>> io.show() # doctest: +SKIP + >>> fig, ax = plt.subplots() # doctest: +SKIP + >>> ax.imshow(cp.asnumpy(filt_real)) # doctest: +SKIP + >>> plt.show() # doctest: +SKIP >>> # less sensitivity to finer details with the lower frequency kernel >>> filt_real, filt_imag = gabor(image, frequency=0.1) - >>> plt.figure() # doctest: +SKIP - >>> io.imshow(cp.asnumpy(filt_real) # doctest: +SKIP - >>> io.show() # doctest: +SKIP + >>> fig, ax = plt.subplots() # doctest: +SKIP + >>> ax.imshow(cp.asnumpy(filt_real)) # doctest: +SKIP + >>> plt.show() # doctest: +SKIP """ # noqa check_nD(image, 2) # do not cast integer types to float! diff --git a/python/cucim/src/cucim/skimage/filters/tests/test_gaussian.py b/python/cucim/src/cucim/skimage/filters/tests/test_gaussian.py index c3d7a60bd..1723287b4 100644 --- a/python/cucim/src/cucim/skimage/filters/tests/test_gaussian.py +++ b/python/cucim/src/cucim/skimage/filters/tests/test_gaussian.py @@ -2,7 +2,6 @@ import numpy as np import pytest -from cucim.skimage._shared.testing import assert_stacklevel from cucim.skimage.filters._gaussian import difference_of_gaussians, gaussian @@ -175,16 +174,6 @@ def test_dog_invalid_sigma2(): difference_of_gaussians(image, (1, 5), (2, 4)) -def test_deprecated_gaussian_output(): - image = cp.array([0, 1, 0], dtype=float) - desired = cp.array([0.24197145, 0.39894347, 0.24197145]) - with pytest.warns(FutureWarning, match="Parameter `output` is") as record: - gaussian(image, output=image) - assert_stacklevel(record) - assert len(record) == 1 - cp.testing.assert_array_almost_equal(desired, image) - - @pytest.mark.parametrize( "dtype", [cp.uint8, cp.float16, cp.float32, cp.float64] ) diff --git a/python/cucim/src/cucim/skimage/filters/tests/test_thresholding.py b/python/cucim/src/cucim/skimage/filters/tests/test_thresholding.py index 3825dcb7e..db4af41c5 100644 --- a/python/cucim/src/cucim/skimage/filters/tests/test_thresholding.py +++ b/python/cucim/src/cucim/skimage/filters/tests/test_thresholding.py @@ -831,24 +831,6 @@ def test_multiotsu_more_classes_then_values(): threshold_multiotsu(img, classes=4) -# @pytest.mark.parametrize( -# "thresholding, lower, upper", -# [ -# (threshold_otsu, 86, 88), -# (threshold_yen, 197, 199), -# (threshold_isodata, 86, 88), -# (threshold_mean, 117, 119), -# (threshold_triangle, 21, 23), -# (threshold_minimum, 75, 77), -# ], -# ) -# def test_thresholds_dask_compatibility(thresholding, lower, upper): -# pytest.importorskip('dask', reason="dask python library is not installed") -# import dask.array as da -# dask_camera = da.from_array(camera, chunks=(256, 256)) -# assert lower < float(thresholding(dask_camera)) < upper - - @pytest.mark.skip("_get_multiotsu_thresh_indices functions not implemented yet") def test_multiotsu_lut(): for classes in [2, 3, 4]: diff --git a/python/cucim/src/cucim/skimage/measure/__init__.py b/python/cucim/src/cucim/skimage/measure/__init__.py index 62d86f776..39f158f13 100644 --- a/python/cucim/src/cucim/skimage/measure/__init__.py +++ b/python/cucim/src/cucim/skimage/measure/__init__.py @@ -1,3 +1,5 @@ -import lazy_loader as lazy +"""Measurement of image properties, e.g., region properties, moments.""" -__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/python/cucim/src/cucim/skimage/measure/_regionprops.py b/python/cucim/src/cucim/skimage/measure/_regionprops.py index 84badd7a0..8274b2c33 100644 --- a/python/cucim/src/cucim/skimage/measure/_regionprops.py +++ b/python/cucim/src/cucim/skimage/measure/_regionprops.py @@ -1,5 +1,6 @@ import inspect import math +import sys from functools import wraps from math import pi as PI from warnings import warn @@ -364,6 +365,15 @@ def __init__( } def __getattr__(self, attr): + if attr == "__setstate__": + # When deserializing this object with pickle, `__setstate__` + # is accessed before any other attributes like + # `self._intensity_image` are available which leads to a + # RecursionError when trying to access them later on in this + # function. So guard against this by provoking the default + # AttributeError (gh-6465). + return self.__getattribute__(attr) + if self._intensity_image is None and attr in _require_intensity_image: raise AttributeError( f"Attribute '{attr}' unavailable when `intensity_image` " @@ -1451,9 +1461,11 @@ def _parse_docs(): import textwrap doc = regionprops.__doc__ or "" - matches = re.finditer( - r"\*\*(\w+)\*\* \:.*?\n(.*?)(?=\n [\*\S]+)", doc, flags=re.DOTALL - ) + arg_regex = r"\*\*(\w+)\*\* \:.*?\n(.*?)(?=\n [\*\S]+)" + if sys.version_info >= (3, 13): + arg_regex = r"\*\*(\w+)\*\* \:.*?\n(.*?)(?=\n[\*\S]+)" + + matches = re.finditer(arg_regex, doc, flags=re.DOTALL) prop_doc = {m.group(1): textwrap.dedent(m.group(2)) for m in matches} return prop_doc diff --git a/python/cucim/src/cucim/skimage/metrics/__init__.py b/python/cucim/src/cucim/skimage/metrics/__init__.py index 62d86f776..0ea6126c7 100644 --- a/python/cucim/src/cucim/skimage/metrics/__init__.py +++ b/python/cucim/src/cucim/skimage/metrics/__init__.py @@ -1,3 +1,5 @@ -import lazy_loader as lazy +"""Metrics corresponding to images, e.g., distance metrics, similarity, etc.""" -__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/python/cucim/src/cucim/skimage/metrics/_contingency_table.py b/python/cucim/src/cucim/skimage/metrics/_contingency_table.py index 45923b478..d412d553e 100644 --- a/python/cucim/src/cucim/skimage/metrics/_contingency_table.py +++ b/python/cucim/src/cucim/skimage/metrics/_contingency_table.py @@ -4,7 +4,14 @@ __all__ = ["contingency_table"] -def contingency_table(im_true, im_test, *, ignore_labels=None, normalize=False): +def contingency_table( + im_true, + im_test, + *, + ignore_labels=None, + normalize=False, + sparse_type="matrix", +): """ Return the contingency table for all regions in matched segmentations. @@ -19,6 +26,10 @@ def contingency_table(im_true, im_test, *, ignore_labels=None, normalize=False): values will not be counted in the score. normalize : bool Determines if the contingency table is normalized by pixel count. + sparse_type : {"matrix"}, optional + scikit-image supports both "matrix" and "array" for this argument. + CuPy does not yet have csr_array support, so only "matrix" + (`cupy.scipy.sparse.csr_matrix`) is supported by cuCIM. Returns ------- @@ -26,6 +37,8 @@ def contingency_table(im_true, im_test, *, ignore_labels=None, normalize=False): A contingency table. `cont[i, j]` will equal the number of voxels labeled `i` in `im_true` and `j` in `im_test`. """ + if sparse_type != "matrix": + raise ValueError("only `sparse_type=matrix` is currently supported.") im_test_r = im_test.reshape(-1) im_true_r = im_true.reshape(-1) diff --git a/python/cucim/src/cucim/skimage/morphology/__init__.py b/python/cucim/src/cucim/skimage/morphology/__init__.py index eedc0bc77..867086ff9 100644 --- a/python/cucim/src/cucim/skimage/morphology/__init__.py +++ b/python/cucim/src/cucim/skimage/morphology/__init__.py @@ -1,10 +1,4 @@ -"""Utilities that operate on shapes in images. - -These operations are particularly suited for binary images, -although some may be useful for images of other types as well. - -Basic morphological operations include dilation and erosion. -""" +"""Morphological algorithms, e.g., closing, opening, skeletonization.""" from ._skeletonize import medial_axis, thin from .binary import ( @@ -18,6 +12,8 @@ cube, diamond, disk, + footprint_from_sequence, + footprint_rectangle, octagon, octahedron, rectangle, @@ -56,11 +52,10 @@ "closing", "white_tophat", "black_tophat", - "square", - "rectangle", + "footprint_from_sequence", + "footprint_rectangle", "diamond", "disk", - "cube", "octahedron", "ball", "octagon", diff --git a/python/cucim/src/cucim/skimage/morphology/_skeletonize.py b/python/cucim/src/cucim/skimage/morphology/_skeletonize.py index 53d55b6f4..81daf1ddf 100644 --- a/python/cucim/src/cucim/skimage/morphology/_skeletonize.py +++ b/python/cucim/src/cucim/skimage/morphology/_skeletonize.py @@ -120,7 +120,7 @@ def thin(image, max_num_iter=None): check_nD(image, 2) # convert image to uint8 with values in {0, 1} - skel = cp.asarray(image, dtype=bool).view(cp.uint8) + skel = cp.asarray(image, dtype=bool).copy().view(cp.uint8) # neighborhood mask mask = cp.asarray( @@ -252,13 +252,16 @@ def medial_axis(image, mask=None, return_distance=False, *, rng=None): """ try: - from skimage.morphology._skeletonize_cy import _skeletonize_loop - except ImportError as e: - warnings.warn( - "Could not find required private skimage Cython function:\n" - "\tskimage.morphology._skeletonize_cy._skeletonize_loop\n" - ) - raise e + from skimage.morphology._skeletonize import _skeletonize_loop + except ImportError: + try: + from skimage.morphology._skeletonize_cy import _skeletonize_loop + except ImportError as e: + warnings.warn( + "Could not find required private skimage Cython function:\n" + "\tskimage.morphology._skeletonize_cy._skeletonize_loop\n" + ) + raise e if mask is None: # masked_image is modified in-place later so make a copy of the input diff --git a/python/cucim/src/cucim/skimage/morphology/footprints.py b/python/cucim/src/cucim/skimage/morphology/footprints.py index c19574475..58620d546 100644 --- a/python/cucim/src/cucim/skimage/morphology/footprints.py +++ b/python/cucim/src/cucim/skimage/morphology/footprints.py @@ -1,4 +1,5 @@ import os +import warnings from collections.abc import Sequence from numbers import Integral @@ -6,6 +7,7 @@ import numpy as np from cucim.skimage import morphology +from cucim.skimage._shared.utils import deprecate_func # Precomputed ball and disk decompositions were saved as 2D arrays where the # radius of the desired decomposition is used to index into the first axis of @@ -116,6 +118,112 @@ def footprint_from_sequence(footprints): return morphology.binary_dilation(imag, footprints) +def footprint_rectangle(shape, *, dtype=cp.uint8, decomposition=None): + """Generate a rectangular or hyper-rectangular footprint. + + Generates, depending on the length and dimensions requested with `shape`, + a square, rectangle, cube, cuboid, or even higher-dimensional versions + of these shapes. + + Parameters + ---------- + shape : tuple[int, ...] + The length of the footprint in each dimension. The length of the + sequence determines the number of dimensions of the footprint. + dtype : data-type, optional + The data type of the footprint. + decomposition : {None, 'separable', 'sequence'}, optional + If None, a single array is returned. For 'sequence', a tuple of smaller + footprints is returned. Applying this series of smaller footprints will + give an identical result to a single, larger footprint, but often with + better computational performance. See Notes for more details. + With 'separable', this function uses separable 1D footprints for each + axis. Whether 'sequence' or 'separable' is computationally faster may + be architecture-dependent. + + Returns + ------- + footprint : array or tuple[tuple[ndarray, int], ...] + A footprint consisting only of ones, i.e. every pixel belongs to the + neighborhood. When `decomposition` is None, this is just an array. + Otherwise, this will be a tuple whose length is equal to the number of + unique structuring elements to apply (see Examples for more detail). + + Examples + -------- + >>> import cucim.skimage as ski + >>> ski.morphology.footprint_rectangle((3, 5)) + array([[1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]], dtype=uint8) + + Decomposition will return multiple footprints that combine into a simple + footprint of the requested shape. + + >>> ski.morphology.footprint_rectangle((9, 9), decomposition="sequence") + ((array([[1, 1, 1], + [1, 1, 1], + [1, 1, 1]], dtype=uint8), + 4),) + + `"sequence"` makes sure that the decomposition only returns 1D footprints. + + >>> ski.morphology.footprint_rectangle((3, 5), decomposition="separable") + ((array([[1], + [1], + [1]], dtype=uint8), + 1), + (array([[1, 1, 1, 1, 1]], dtype=uint8), 1)) + + Generate a 5-dimensional hypercube with 3 samples in each dimension + + >>> ski.morphology.footprint_rectangle((3,) * 5).shape + (3, 3, 3, 3, 3) + """ + has_even_width = any(width % 2 == 0 for width in shape) + if decomposition == "sequence" and has_even_width: + warnings.warn( + "decomposition='sequence' is only supported for uneven footprints, " + "falling back to decomposition='separable'", + stacklevel=2, + ) + decomposition = "sequence_fallback" + + def partial_footprint(dim, width): + shape_ = (1,) * dim + (width,) + (1,) * (len(shape) - dim - 1) + fp = (cp.ones(shape_, dtype=dtype), 1) + return fp + + if decomposition is None: + footprint = cp.ones(shape, dtype=dtype) + + elif decomposition in ("separable", "sequence_fallback"): + footprint = tuple( + partial_footprint(dim, width) for dim, width in enumerate(shape) + ) + + elif decomposition == "sequence": + min_width = min(shape) + sq_reps = _decompose_size(min_width, 3) + footprint = [(cp.ones((3,) * len(shape), dtype=dtype), sq_reps)] + for dim, width in enumerate(shape): + if width > min_width: + nextra = width - min_width + 1 + component = partial_footprint(dim, nextra) + footprint.append(component) + footprint = tuple(footprint) + + else: + raise ValueError(f"Unrecognized decomposition: {decomposition}") + + return footprint + + +@deprecate_func( + deprecated_version="25.02", + removed_version="25.08", + hint="Use `cucim.skimage.morphology.footprint_rectangle` instead.", +) def square(width, dtype=None, *, decomposition=None): """Generates a flat, square-shaped footprint. @@ -170,31 +278,10 @@ def square(width, dtype=None, *, decomposition=None): mode. """ - if decomposition is None: - if dtype is None: - return (width, width) - else: - return cp.ones((width, width), dtype=dtype) - - if decomposition == "separable" or width % 2 == 0: - if dtype is None: - sequence = (((width, 1), 1), ((1, width), 1)) - else: - sequence = ( - (cp.ones((width, 1), dtype=dtype), 1), - (cp.ones((1, width), dtype=dtype), 1), - ) - elif decomposition == "sequence": - # only handles odd widths - if dtype is None: - sequence = (((3, 3), _decompose_size(width, 3)),) - else: - sequence = ( - (cp.ones((3, 3), dtype=dtype), _decompose_size(width, 3)), - ) - else: - raise ValueError(f"Unrecognized decomposition: {decomposition}") - return sequence + footprint = footprint_rectangle( + shape=(width, width), dtype=dtype, decomposition=decomposition + ) + return footprint def _decompose_size(size, kernel_size=3): @@ -210,6 +297,11 @@ def _decompose_size(size, kernel_size=3): return 1 + (size - kernel_size) // (kernel_size - 1) +@deprecate_func( + deprecated_version="25.02", + removed_version="25.08", + hint="Use `cucim.skimage.morphology.footprint_rectangle` instead.", +) def rectangle(nrows, ncols, dtype=None, *, decomposition=None): """Generates a flat, rectangular-shaped footprint. @@ -268,45 +360,10 @@ def rectangle(nrows, ncols, dtype=None, *, decomposition=None): - The use of ``width`` and ``height`` has been deprecated in version 0.18.0. Use ``nrows`` and ``ncols`` instead. """ - if decomposition is None: # TODO: check optimal width setting here - if dtype is None: - return (nrows, ncols) - else: - return cp.ones((nrows, ncols), dtype=dtype) - - even_rows = nrows % 2 == 0 - even_cols = ncols % 2 == 0 - if decomposition == "separable" or even_rows or even_cols: - if dtype is None: - sequence = [((nrows, 1), 1), ((1, ncols), 1)] - else: - sequence = [ - (cp.ones((nrows, 1), dtype=dtype), 1), - (cp.ones((1, ncols), dtype=dtype), 1), - ] - elif decomposition == "sequence": - # this branch only support odd nrows, ncols - sq_size = 3 - sq_reps = _decompose_size(min(nrows, ncols), sq_size) - if dtype is None: - sequence = [((3, 3), sq_reps)] - else: - sequence = [(cp.ones((3, 3), dtype=dtype), sq_reps)] - if nrows > ncols: - nextra = nrows - ncols - if dtype is None: - sequence.append(((nextra + 1, 1), 1)) - else: - sequence.append((cp.ones((nextra + 1, 1), dtype=dtype), 1)) - elif ncols > nrows: - nextra = ncols - nrows - if dtype is None: - sequence.append(((1, nextra + 1), 1)) - else: - sequence.append((cp.ones((1, nextra + 1), dtype=dtype), 1)) - else: - raise ValueError(f"Unrecognized decomposition: {decomposition}") - return tuple(sequence) + footprint = footprint_rectangle( + shape=(nrows, ncols), dtype=dtype, decomposition=decomposition + ) + return footprint def diamond(radius, dtype=cp.uint8, *, decomposition=None): @@ -707,6 +764,11 @@ def ellipse(width, height, dtype=cp.uint8, *, decomposition=None): return sequence +@deprecate_func( + deprecated_version="25.02", + removed_version="25.08", + hint="Use `cucim.skimage.morphology.footprint_rectangle` instead.", +) def cube(width, dtype=None, *, decomposition=None): """Generates a cube-shaped footprint. @@ -757,35 +819,10 @@ def cube(width, dtype=None, *, decomposition=None): `width` is even, the sequence used will be identical to the 'separable' mode. """ - if decomposition is None: - if dtype is None: - return (width, width, width) - else: - return cp.ones((width, width, width), dtype=dtype) - if decomposition == "separable" or width % 2 == 0: - if dtype is None: - sequence = ( - ((width, 1, 1), 1), - ((1, width, 1), 1), - ((1, 1, width), 1), - ) - else: - sequence = ( - (cp.ones((width, 1, 1), dtype=dtype), 1), - (cp.ones((1, width, 1), dtype=dtype), 1), - (cp.ones((1, 1, width), dtype=dtype), 1), - ) - elif decomposition == "sequence": - # only handles odd widths - if dtype is None: - sequence = (((3, 3, 3), _decompose_size(width, 3)),) - else: - sequence = ( - (cp.ones((3, 3, 3), dtype=dtype), _decompose_size(width, 3)), - ) - else: - raise ValueError(f"Unrecognized decomposition: {decomposition}") - return sequence + footprint = footprint_rectangle( + shape=(width, width, width), dtype=dtype, decomposition=decomposition + ) + return footprint def octahedron(radius, dtype=cp.uint8, *, decomposition=None): @@ -858,7 +895,7 @@ def ball(radius, dtype=cp.uint8, *, strict_radius=True, decomposition=None): Parameters ---------- - radius : int + radius : float The radius of the ball-shaped footprint. Other Parameters @@ -996,7 +1033,12 @@ def octagon(m, n, dtype=cp.uint8, *, decomposition=None): n -= 1 sequence = [] if m > 1: - sequence += list(square(m, dtype=dtype, decomposition="sequence")) + decomposition = "separable" if m % 2 == 0 else "sequence" + sequence += list( + footprint_rectangle( + (m, m), dtype=dtype, decomposition="separable" + ) + ) if n > 0: sequence += [(diamond(1, dtype=dtype, decomposition=None), n)] footprint = tuple(sequence) diff --git a/python/cucim/src/cucim/skimage/morphology/gray.py b/python/cucim/src/cucim/skimage/morphology/gray.py index afc2e8cd9..a02d9452f 100644 --- a/python/cucim/src/cucim/skimage/morphology/gray.py +++ b/python/cucim/src/cucim/skimage/morphology/gray.py @@ -236,13 +236,13 @@ def erosion( -------- >>> # Erosion shrinks bright regions >>> import cupy as cp - >>> from cucim.skimage.morphology import square + >>> from cucim.skimage.morphology import footprint_rectangle >>> bright_square = cp.asarray([[0, 0, 0, 0, 0], ... [0, 1, 1, 1, 0], ... [0, 1, 1, 1, 0], ... [0, 1, 1, 1, 0], ... [0, 0, 0, 0, 0]], dtype=cp.uint8) - >>> erosion(bright_square, square(3)) + >>> erosion(bright_square, footprint_rectangle((3, 3))) array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 1, 0, 0], @@ -369,13 +369,13 @@ def dilation( -------- >>> # Dilation enlarges bright regions >>> import cupy as cp - >>> from cucim.skimage.morphology import square + >>> from cucim.skimage.morphology import footprint_rectangle >>> bright_pixel = cp.asarray([[0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0], ... [0, 0, 1, 0, 0], ... [0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0]], dtype=cp.uint8) - >>> dilation(bright_pixel, square(3)) + >>> dilation(bright_pixel, footprint_rectangle((3, 3))) array([[0, 0, 0, 0, 0], [0, 1, 1, 1, 0], [0, 1, 1, 1, 0], @@ -481,13 +481,13 @@ def opening(image, footprint=None, out=None, *, mode="reflect", cval=0.0): -------- >>> # Open up gap between two bright regions (but also shrink regions) >>> import cupy as cp - >>> from cucim.skimage.morphology import square + >>> from cucim.skimage.morphology import footprint_rectangle >>> bad_connection = cp.asarray([[1, 0, 0, 0, 1], ... [1, 1, 0, 1, 1], ... [1, 1, 1, 1, 1], ... [1, 1, 0, 1, 1], ... [1, 0, 0, 0, 1]], dtype=cp.uint8) - >>> opening(bad_connection, square(3)) + >>> opening(bad_connection, footprint_rectangle((3, 3))) array([[0, 0, 0, 0, 0], [1, 1, 0, 1, 1], [1, 1, 0, 1, 1], @@ -563,13 +563,13 @@ def closing(image, footprint=None, out=None, *, mode="reflect", cval=0.0): -------- >>> # Close a gap between two bright lines >>> import cupy as cp - >>> from cucim.skimage.morphology import square + >>> from cucim.skimage.morphology import footprint_rectangle >>> broken_line = cp.asarray([[0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0], ... [1, 1, 0, 1, 1], ... [0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0]], dtype=cp.uint8) - >>> closing(broken_line, square(3)) + >>> closing(broken_line, footprint_rectangle((3, 3))) array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [1, 1, 1, 1, 1], @@ -669,13 +669,13 @@ def white_tophat(image, footprint=None, out=None, *, mode="reflect", cval=0.0): -------- >>> # Subtract grey background from bright peak >>> import cupy as cp - >>> from cucim.skimage.morphology import square + >>> from cucim.skimage.morphology import footprint_rectangle >>> bright_on_grey = cp.asarray([[2, 3, 3, 3, 2], ... [3, 4, 5, 4, 3], ... [3, 5, 9, 5, 3], ... [3, 4, 5, 4, 3], ... [2, 3, 3, 3, 2]], dtype=cp.uint8) - >>> white_tophat(bright_on_grey, square(3)) + >>> white_tophat(bright_on_grey, footprint_rectangle((3, 3))) array([[0, 0, 0, 0, 0], [0, 0, 1, 0, 0], [0, 1, 5, 1, 0], @@ -764,13 +764,13 @@ def black_tophat(image, footprint=None, out=None, *, mode="reflect", cval=0.0): -------- >>> # Change dark peak to bright peak and subtract background >>> import cupy as cp - >>> from cucim.skimage.morphology import square + >>> from cucim.skimage.morphology import footprint_rectangle >>> dark_on_grey = cp.asarray([[7, 6, 6, 6, 7], ... [6, 5, 4, 5, 6], ... [6, 4, 0, 4, 6], ... [6, 5, 4, 5, 6], ... [7, 6, 6, 6, 7]], dtype=cp.uint8) - >>> black_tophat(dark_on_grey, square(3)) + >>> black_tophat(dark_on_grey, footprint_rectangle((3, 3))) array([[0, 0, 0, 0, 0], [0, 0, 1, 0, 0], [0, 1, 5, 1, 0], diff --git a/python/cucim/src/cucim/skimage/morphology/tests/test_binary.py b/python/cucim/src/cucim/skimage/morphology/tests/test_binary.py index aee555cab..b1b2cc78e 100755 --- a/python/cucim/src/cucim/skimage/morphology/tests/test_binary.py +++ b/python/cucim/src/cucim/skimage/morphology/tests/test_binary.py @@ -6,6 +6,7 @@ from skimage import data from cucim.skimage import color, morphology +from cucim.skimage.morphology import footprint_rectangle from cucim.skimage.util import img_as_bool img = color.rgb2gray(cp.array(data.astronaut())) @@ -13,28 +14,28 @@ def test_non_square_image(): - footprint = morphology.square(3) + footprint = footprint_rectangle((3, 3)) binary_res = morphology.binary_erosion(bw_img[:100, :200], footprint) gray_res = img_as_bool(morphology.erosion(bw_img[:100, :200], footprint)) testing.assert_array_equal(binary_res, gray_res) def test_binary_erosion(): - footprint = morphology.square(3) + footprint = footprint_rectangle((3, 3)) binary_res = morphology.binary_erosion(bw_img, footprint) gray_res = img_as_bool(morphology.erosion(bw_img, footprint)) testing.assert_array_equal(binary_res, gray_res) def test_binary_dilation(): - footprint = morphology.square(3) + footprint = footprint_rectangle((3, 3)) binary_res = morphology.binary_dilation(bw_img, footprint) gray_res = img_as_bool(morphology.dilation(bw_img, footprint)) testing.assert_array_equal(binary_res, gray_res) def test_binary_closing(): - footprint = morphology.square(3) + footprint = footprint_rectangle((3, 3)) binary_res = morphology.binary_closing(bw_img, footprint) gray_res = img_as_bool(morphology.closing(bw_img, footprint)) testing.assert_array_equal(binary_res, gray_res) @@ -52,7 +53,7 @@ def test_binary_closing_extensive(): def test_binary_opening(): - footprint = morphology.square(3) + footprint = footprint_rectangle((3, 3)) binary_res = morphology.binary_opening(bw_img, footprint) gray_res = img_as_bool(morphology.opening(bw_img, footprint)) testing.assert_array_equal(binary_res, gray_res) @@ -87,36 +88,16 @@ def _get_decomp_test_data(function, ndim=2): "function", ["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"], ) -@pytest.mark.parametrize("size", (3, 4, 11)) -@pytest.mark.parametrize("decomposition", ["separable", "sequence"]) -def test_square_decomposition(function, size, decomposition): - """Validate footprint decomposition for various shapes. - - comparison is made to the case without decomposition. - """ - footprint_ndarray = morphology.square(size, decomposition=None) - footprint = morphology.square(size, decomposition=decomposition) - img = _get_decomp_test_data(function) - func = getattr(morphology, function) - expected = func(img, footprint=footprint_ndarray) - out = func(img, footprint=footprint) - testing.assert_array_equal(expected, out) - - -@pytest.mark.parametrize( - "function", - ["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"], -) -@pytest.mark.parametrize("nrows", (3, 4, 11)) -@pytest.mark.parametrize("ncols", (3, 4, 11)) +@pytest.mark.parametrize("nrows", (3, 7, 11)) +@pytest.mark.parametrize("ncols", (3, 7, 11)) @pytest.mark.parametrize("decomposition", ["separable", "sequence"]) def test_rectangle_decomposition(function, nrows, ncols, decomposition): """Validate footprint decomposition for various shapes. comparison is made to the case without decomposition. """ - footprint_ndarray = morphology.rectangle(nrows, ncols, decomposition=None) - footprint = morphology.rectangle(nrows, ncols, decomposition=decomposition) + footprint_ndarray = footprint_rectangle((nrows, ncols), decomposition=None) + footprint = footprint_rectangle((nrows, ncols), decomposition=decomposition) img = _get_decomp_test_data(function) func = getattr(morphology, function) expected = func(img, footprint=footprint_ndarray) @@ -131,6 +112,9 @@ def test_rectangle_decomposition(function, nrows, ncols, decomposition): @pytest.mark.parametrize("m", (0, 1, 2, 3, 4, 5)) @pytest.mark.parametrize("n", (0, 1, 2, 3, 4, 5)) @pytest.mark.parametrize("decomposition", ["sequence"]) +@pytest.mark.filterwarnings( + "ignore:.*falling back to decomposition='separable':UserWarning:skimage" +) def test_octagon_decomposition(function, m, n, decomposition): """Validate footprint decomposition for various shapes. @@ -173,15 +157,19 @@ def test_diamond_decomposition(function, radius, decomposition): "function", ["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"], ) -@pytest.mark.parametrize("size", (3, 4, 5)) +@pytest.mark.parametrize("shape", [(3, 3, 3), (3, 4, 5)]) @pytest.mark.parametrize("decomposition", ["separable", "sequence"]) -def test_cube_decomposition(function, size, decomposition): +def test_cube_decomposition(function, shape, decomposition): """Validate footprint decomposition for various shapes. comparison is made to the case without decomposition. """ - footprint_ndarray = morphology.cube(size, decomposition=None) - footprint = morphology.cube(size, decomposition=decomposition) + footprint_ndarray = footprint_rectangle(shape, decomposition=None) + if any(s % 2 == 0 for s in shape) and decomposition == "sequence": + with pytest.warns(UserWarning, match="only supported for uneven"): + footprint = footprint_rectangle(shape, decomposition=decomposition) + else: + footprint = footprint_rectangle(shape, decomposition=decomposition) img = _get_decomp_test_data(function, ndim=3) func = getattr(morphology, function) expected = func(img, footprint=footprint_ndarray) @@ -195,6 +183,9 @@ def test_cube_decomposition(function, size, decomposition): ) @pytest.mark.parametrize("radius", (1, 2, 3)) @pytest.mark.parametrize("decomposition", ["sequence"]) +@pytest.mark.filterwarnings( + "ignore:.*falling back to decomposition='separable':UserWarning:skimage" +) def test_octahedron_decomposition(function, radius, decomposition): """Validate footprint decomposition for various shapes. diff --git a/python/cucim/src/cucim/skimage/morphology/tests/test_footprints.py b/python/cucim/src/cucim/skimage/morphology/tests/test_footprints.py index 1c46d970b..b2b339a78 100644 --- a/python/cucim/src/cucim/skimage/morphology/tests/test_footprints.py +++ b/python/cucim/src/cucim/skimage/morphology/tests/test_footprints.py @@ -11,33 +11,15 @@ import pytest from cupy.testing import assert_array_equal -from cucim.skimage._shared.testing import fetch -from cucim.skimage.morphology import footprints +from cucim.skimage._shared.testing import assert_stacklevel, fetch +from cucim.skimage.morphology import ( + footprint_from_sequence, + footprint_rectangle, + footprints, +) class TestSElem: - def test_square_footprint(self): - """Test square footprints""" - for k in range(0, 5): - actual_mask = footprints.square(k, dtype=cp.uint8) - expected_mask = np.ones((k, k), dtype="uint8") - assert_array_equal(expected_mask, actual_mask) - - def test_rectangle_footprint(self): - """Test rectangle footprints""" - for i in range(0, 5): - for j in range(0, 5): - actual_mask = footprints.rectangle(i, j, dtype=cp.uint8) - expected_mask = np.ones((i, j), dtype="uint8") - assert_array_equal(expected_mask, actual_mask) - - def test_cube_footprint(self): - """Test cube footprints""" - for k in range(0, 5): - actual_mask = footprints.cube(k, dtype=cp.uint8) - expected_mask = np.ones((k, k, k), dtype="uint8") - assert_array_equal(expected_mask, actual_mask) - def strel_worker(self, fn, func): matlab_masks = np.load(fetch(fn)) k = 0 @@ -163,11 +145,9 @@ def test_footprint_star(self): [ (footprints.disk, (3,), True), (footprints.ball, (3,), True), - (footprints.square, (3,), True), - (footprints.cube, (3,), True), (footprints.diamond, (3,), True), (footprints.octahedron, (3,), True), - (footprints.rectangle, (3, 4), True), + (footprint_rectangle, ((3, 5),), True), (footprints.ellipse, (3, 4), False), (footprints.octagon, (3, 4), True), (footprints.star, (3,), False), @@ -196,7 +176,7 @@ def test_nsphere_series_approximation(function, radius): footprint_sequence = fp_func( radius, strict_radius=False, decomposition="sequence" ) - approximate = footprints.footprint_from_sequence(footprint_sequence) + approximate = footprint_from_sequence(footprint_sequence) assert approximate.shape == expected.shape # verify that maximum error does not exceed some fraction of the size @@ -216,7 +196,7 @@ def test_disk_crosses_approximation(radius, strict_radius): footprint_sequence = fp_func( radius, strict_radius=strict_radius, decomposition="crosses" ) - approximate = footprints.footprint_from_sequence(footprint_sequence) + approximate = footprint_from_sequence(footprint_sequence) assert approximate.shape == expected.shape # verify that maximum error does not exceed some fraction of the size @@ -231,7 +211,7 @@ def test_ellipse_crosses_approximation(width, height): fp_func = footprints.ellipse expected = fp_func(width, height, decomposition=None) footprint_sequence = fp_func(width, height, decomposition="crosses") - approximate = footprints.footprint_from_sequence(footprint_sequence) + approximate = footprint_from_sequence(footprint_sequence) assert approximate.shape == expected.shape # verify that maximum error does not exceed some fraction of the size @@ -250,3 +230,49 @@ def test_ball_series_approximation_unavailable(): # ValueError if radius is too large (only precomputed up to radius=100) with pytest.raises(ValueError): footprints.ball(radius=10000, decomposition="sequence") + + +def assert_decomposition_equal(actual, desired): + assert len(actual) == len(desired) + for a, d in zip(actual, desired): + assert_array_equal(a[0], d[0]) + assert a[1] == d[1] + + +class Test_footprint_rectangle: + @pytest.mark.parametrize("i", [0, 1, 2, 3, 4]) + @pytest.mark.parametrize("j", [0, 1, 2, 3, 4]) + def test_rectangle(self, i, j): + desired = cp.ones((i, j), dtype="uint8") + actual = footprint_rectangle((i, j)) + assert_array_equal(actual, desired) + + @pytest.mark.parametrize("i", [0, 1, 2, 3, 4]) + @pytest.mark.parametrize("j", [0, 1, 2, 3, 4]) + @pytest.mark.parametrize("k", [0, 1, 2, 3, 4]) + def test_cuboid(self, i, j, k): + desired = cp.ones((i, j, k), dtype="uint8") + actual = footprint_rectangle((i, j, k)) + assert_array_equal(actual, desired) + + @pytest.mark.parametrize("shape", [(3,), (5, 5), (5, 5, 7)]) + @pytest.mark.parametrize("decomposition", ["separable", "sequence"]) + def test_decomposition(self, shape, decomposition): + regular = footprint_rectangle(shape) + decomposed = footprint_rectangle(shape, decomposition=decomposition) + recomposed = footprint_from_sequence(decomposed) + assert_array_equal(recomposed, regular) + + @pytest.mark.parametrize("shape", [(2,), (3, 4)]) + def test_uneven_sequence_decomposition_warning(self, shape): + """Should fall back to decomposition="separable" for uneven footprint + size. + """ + desired = footprint_rectangle(shape, decomposition="separable") + regex = ( + "decomposition='sequence' is only supported for uneven footprints" + ) + with pytest.warns(UserWarning, match=regex) as record: + actual = footprint_rectangle(shape, decomposition="sequence") + assert_stacklevel(record) + assert_decomposition_equal(actual, desired) diff --git a/python/cucim/src/cucim/skimage/morphology/tests/test_gray.py b/python/cucim/src/cucim/skimage/morphology/tests/test_gray.py index 4a986472f..18d6c3ed3 100755 --- a/python/cucim/src/cucim/skimage/morphology/tests/test_gray.py +++ b/python/cucim/src/cucim/skimage/morphology/tests/test_gray.py @@ -10,7 +10,7 @@ from cucim.skimage import color, morphology, transform from cucim.skimage._shared._warnings import expected_warnings from cucim.skimage._shared.testing import assert_stacklevel -from cucim.skimage.morphology import gray +from cucim.skimage.morphology import footprint_rectangle, gray from cucim.skimage.util import img_as_ubyte, img_as_uint @@ -41,11 +41,15 @@ def cell3d_image(): gray_modes = sorted(gray._SUPPORTED_MODES) +def square(n, decomposition=None): + return footprint_rectangle((n, n), decomposition=decomposition) + + class TestMorphology: @pytest.mark.parametrize( "footprint, footprint_kwargs", ( - (morphology.square, dict(decomposition=None)), + (square, dict(decomposition=None)), (morphology.diamond, dict(decomposition=None)), (morphology.disk, dict(decomposition=None)), (morphology.star, {}), @@ -118,10 +122,9 @@ def test_unsupported_mode(self, func, mode): eccentric_footprint_args = [ - (morphology.square, (2,)), - (morphology.rectangle, (2, 2)), - (morphology.rectangle, (2, 1)), - (morphology.rectangle, (1, 2)), + (footprint_rectangle, ((2, 2),)), + (footprint_rectangle, ((2, 1),)), + (footprint_rectangle, ((1, 2),)), ] eccentric_params = ("footprint_func, args", eccentric_footprint_args) @@ -382,34 +385,8 @@ def test_1d_erosion(): "black_tophat", ], ) -@pytest.mark.parametrize("size", (7,)) -@pytest.mark.parametrize("decomposition", ["separable", "sequence"]) -def test_square_decomposition(cam_image, function, size, decomposition): - """Validate footprint decomposition for various shapes. - - comparison is made to the case without decomposition. - """ - footprint_ndarray = morphology.square(size, decomposition=None) - footprint = morphology.square(size, decomposition=decomposition) - func = getattr(morphology, function) - expected = func(cam_image, footprint=footprint_ndarray) - out = func(cam_image, footprint=footprint) - cp.testing.assert_array_equal(expected, out) - - -@pytest.mark.parametrize( - "function", - [ - "erosion", - "dilation", - "closing", - "opening", - "white_tophat", - "black_tophat", - ], -) -@pytest.mark.parametrize("nrows", (3, 11)) -@pytest.mark.parametrize("ncols", (3, 11)) +@pytest.mark.parametrize("nrows", [3, 7, 11]) +@pytest.mark.parametrize("ncols", [3, 7, 11]) @pytest.mark.parametrize("decomposition", ["separable", "sequence"]) def test_rectangle_decomposition( cam_image, function, nrows, ncols, decomposition @@ -418,8 +395,8 @@ def test_rectangle_decomposition( comparison is made to the case without decomposition. """ - footprint_ndarray = morphology.rectangle(nrows, ncols, decomposition=None) - footprint = morphology.rectangle(nrows, ncols, decomposition=decomposition) + footprint_ndarray = footprint_rectangle((nrows, ncols), decomposition=None) + footprint = footprint_rectangle((nrows, ncols), decomposition=decomposition) func = getattr(morphology, function) expected = func(cam_image, footprint=footprint_ndarray) out = func(cam_image, footprint=footprint) @@ -466,6 +443,9 @@ def test_diamond_decomposition(cam_image, function, radius, decomposition): @pytest.mark.parametrize("m", (0, 1, 3, 5)) @pytest.mark.parametrize("n", (0, 1, 2, 3)) @pytest.mark.parametrize("decomposition", ["sequence"]) +@pytest.mark.filterwarnings( + "ignore:.*falling back to decomposition='separable':UserWarning:skimage" +) def test_octagon_decomposition(cam_image, function, m, n, decomposition): """Validate footprint decomposition for various shapes. @@ -494,15 +474,15 @@ def test_octagon_decomposition(cam_image, function, m, n, decomposition): "black_tophat", ], ) -@pytest.mark.parametrize("size", (5,)) +@pytest.mark.parametrize("shape", [(5, 5, 5), (5, 5, 7)]) @pytest.mark.parametrize("decomposition", ["separable", "sequence"]) -def test_cube_decomposition(cell3d_image, function, size, decomposition): +def test_cube_decomposition(cell3d_image, function, shape, decomposition): """Validate footprint decomposition for various shapes. comparison is made to the case without decomposition. """ - footprint_ndarray = morphology.cube(size, decomposition=None) - footprint = morphology.cube(size, decomposition=decomposition) + footprint_ndarray = footprint_rectangle(shape, decomposition=None) + footprint = footprint_rectangle(shape, decomposition=decomposition) func = getattr(morphology, function) expected = func(cell3d_image, footprint=footprint_ndarray) out = func(cell3d_image, footprint=footprint) diff --git a/python/cucim/src/cucim/skimage/morphology/tests/test_skeletonize.py b/python/cucim/src/cucim/skimage/morphology/tests/test_skeletonize.py index d4b414544..92d73c83c 100644 --- a/python/cucim/src/cucim/skimage/morphology/tests/test_skeletonize.py +++ b/python/cucim/src/cucim/skimage/morphology/tests/test_skeletonize.py @@ -11,7 +11,7 @@ class TestThin: @property def input_image(self): - """image to test thinning with""" + # Image to test thinning with ii = cp.array( [ [0, 0, 0, 0, 0, 0, 0], @@ -26,10 +26,18 @@ def input_image(self): ) return ii - def test_zeros(self): + def test_all_zeros(self): image = cp.zeros((10, 10), dtype=bool) assert cp.all(thin(image) == 0) + @pytest.mark.parametrize("dtype", [bool, float, int]) + def test_thin_copies_input(self, dtype): + """Ensure thinning does not modify the input image.""" + image = self.input_image.astype(dtype) + original = image.copy() + thin(image) + cp.testing.assert_array_equal(image, original) + @pytest.mark.parametrize("dtype", [bool, float, int]) def test_iter_1(self, dtype): image = self.input_image.astype(dtype) @@ -82,27 +90,27 @@ def test_compare_skimage(self, invert): class TestMedialAxis: - def test_00_00_zeros(self): - """Test skeletonize on an array of all zeros""" - result = medial_axis(cp.zeros((10, 10), bool)) + def test_all_zeros(self): + result = medial_axis(cp.zeros((10, 10), dtype=bool)) assert not cp.any(result) - def test_00_01_zeros_masked(self): - """Test skeletonize on an array that is completely masked""" - result = medial_axis(cp.zeros((10, 10), bool), cp.zeros((10, 10), bool)) + def test_all_zeros_masked(self): + result = medial_axis( + cp.zeros((10, 10), dtype=bool), cp.zeros((10, 10), dtype=bool) + ) assert not cp.any(result) def _test_vertical_line(self, dtype, **kwargs): """Test a thick vertical line, issue #3861""" - img = cp.zeros((9, 9), dtype=dtype) - img[:, 2] = 1 - img[:, 3] = 2 - img[:, 4] = 3 + image = cp.zeros((9, 9), dtype=dtype) + image[:, 2] = 1 + image[:, 3] = 2 + image[:, 4] = 3 - expected = cp.full(img.shape, False) + expected = cp.full(image.shape, False) expected[:, 3] = True - result = medial_axis(img, **kwargs) + result = medial_axis(image, **kwargs) assert_array_equal(result, expected) @pytest.mark.parametrize("dtype", [bool, float, int]) @@ -122,14 +130,11 @@ def test_rng_cupy(self): def test_rng_int(self): self._test_vertical_line(dtype=bool, rng=15) - def test_01_01_rectangle(self): - """Test skeletonize on a rectangle""" - image = cp.zeros((9, 15), bool) + def test_rectangle(self): + image = cp.zeros((9, 15), dtype=bool) image[1:-1, 1:-1] = True - # - # The result should be four diagonals from the - # corners, meeting in a horizontal line - # + # Excepted are four diagonals from the corners, meeting in a horizontal + # line expected = cp.array( [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -149,9 +154,8 @@ def test_01_01_rectangle(self): result, distance = medial_axis(image, return_distance=True) assert distance.max() == 4 - def test_01_02_hole(self): - """Test skeletonize on a rectangle with a hole in the middle""" - image = cp.zeros((9, 15), bool) + def test_rectangle_with_hole(self): + image = cp.zeros((9, 15), dtype=bool) image[1:-1, 1:-1] = True image[4, 4:-4] = False expected = cp.array( @@ -172,8 +176,8 @@ def test_01_02_hole(self): assert cp.all(result == expected) def test_narrow_image(self): - """Test skeletonize on a 1-pixel thin strip""" - image = cp.zeros((1, 5), bool) + # Image is a 1-pixel thin strip + image = cp.zeros((1, 5), dtype=bool) image[:, 1:-1] = True result = medial_axis(image) assert cp.all(result == image) diff --git a/python/cucim/src/cucim/skimage/registration/__init__.py b/python/cucim/src/cucim/skimage/registration/__init__.py index 62d86f776..8691f9051 100644 --- a/python/cucim/src/cucim/skimage/registration/__init__.py +++ b/python/cucim/src/cucim/skimage/registration/__init__.py @@ -1,3 +1,5 @@ -import lazy_loader as lazy +"""Image registration algorithms, e.g., optical flow or phase cross correlation.""" -__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/python/cucim/src/cucim/skimage/registration/_optical_flow.py b/python/cucim/src/cucim/skimage/registration/_optical_flow.py index b75b7f295..235c58df7 100644 --- a/python/cucim/src/cucim/skimage/registration/_optical_flow.py +++ b/python/cucim/src/cucim/skimage/registration/_optical_flow.py @@ -1,6 +1,4 @@ -"""TV-L1 optical flow algorithm implementation. - -""" +"""TV-L1 optical flow algorithm implementation.""" from functools import partial from itertools import combinations_with_replacement diff --git a/python/cucim/src/cucim/skimage/registration/_optical_flow_utils.py b/python/cucim/src/cucim/skimage/registration/_optical_flow_utils.py index 9d0477d7e..3329acbbb 100644 --- a/python/cucim/src/cucim/skimage/registration/_optical_flow_utils.py +++ b/python/cucim/src/cucim/skimage/registration/_optical_flow_utils.py @@ -1,6 +1,5 @@ -"""Common tools to optical flow algorithms. +"""Common tools to optical flow algorithms.""" -""" import cupy as cp import numpy as np diff --git a/python/cucim/src/cucim/skimage/restoration/__init__.py b/python/cucim/src/cucim/skimage/restoration/__init__.py index 62d86f776..13c8e7421 100644 --- a/python/cucim/src/cucim/skimage/restoration/__init__.py +++ b/python/cucim/src/cucim/skimage/restoration/__init__.py @@ -1,3 +1,5 @@ -import lazy_loader as lazy +"""Restoration algorithms, e.g., deconvolution algorithms, denoising, etc.""" -__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/python/cucim/src/cucim/skimage/restoration/deconvolution.py b/python/cucim/src/cucim/skimage/restoration/deconvolution.py index 0f1ea85d8..82e97d98e 100644 --- a/python/cucim/src/cucim/skimage/restoration/deconvolution.py +++ b/python/cucim/src/cucim/skimage/restoration/deconvolution.py @@ -398,8 +398,11 @@ def richardson_lucy(image, psf, num_iter=50, clip=True, filter_epsilon=None): Parameters ---------- - image : ndarray - Input degraded image (can be n-dimensional). + image : ([P, ]M, N) ndarray + Input degraded image (can be n-dimensional). If you keep the + default `clip=True` parameter, you may want to normalize + the image so that its values fall in the [-1, 1] interval to avoid + information loss. psf : ndarray The point spread function. num_iter : int, optional diff --git a/python/cucim/src/cucim/skimage/segmentation/__init__.py b/python/cucim/src/cucim/skimage/segmentation/__init__.py index 6e2969ae5..be6302e53 100644 --- a/python/cucim/src/cucim/skimage/segmentation/__init__.py +++ b/python/cucim/src/cucim/skimage/segmentation/__init__.py @@ -1,5 +1,4 @@ -"""Algorithms to partition images into meaningful regions or boundaries. -""" +"""Algorithms to partition images into meaningful regions or boundaries.""" from ._chan_vese import chan_vese from ._clear_border import clear_border diff --git a/python/cucim/src/cucim/skimage/segmentation/_join.py b/python/cucim/src/cucim/skimage/segmentation/_join.py index b6842d055..87eb0c031 100644 --- a/python/cucim/src/cucim/skimage/segmentation/_join.py +++ b/python/cucim/src/cucim/skimage/segmentation/_join.py @@ -49,7 +49,7 @@ def join_segmentations(s1, s2, return_mapping: bool = False): ) s1_relabeled, _, backward_map1 = relabel_sequential(s1) s2_relabeled, _, backward_map2 = relabel_sequential(s2) - factor = s2.max() + 1 + factor = s2.max() + cp.uint8(1) j_initial = factor * s1_relabeled + s2_relabeled j, _, map_j_to_j_initial = relabel_sequential(j_initial) if not return_mapping: diff --git a/python/cucim/src/cucim/skimage/segmentation/boundaries.py b/python/cucim/src/cucim/skimage/segmentation/boundaries.py index 7bdfb24ad..8a7412d55 100644 --- a/python/cucim/src/cucim/skimage/segmentation/boundaries.py +++ b/python/cucim/src/cucim/skimage/segmentation/boundaries.py @@ -4,7 +4,7 @@ from .._shared.utils import _supported_float_type from ..color import gray2rgb -from ..morphology import dilation, erosion, square +from ..morphology import dilation, erosion, footprint_rectangle from ..util import img_as_float @@ -256,7 +256,7 @@ def mark_boundaries( label_img, mode=mode, background=background_label ) if outline_color is not None: - outlines = dilation(boundaries, square(3)) + outlines = dilation(boundaries, footprint_rectangle((3, 3))) marked[outlines] = outline_color marked[boundaries] = color return marked diff --git a/python/cucim/src/cucim/skimage/transform/__init__.py b/python/cucim/src/cucim/skimage/transform/__init__.py index 62d86f776..296f0c33d 100644 --- a/python/cucim/src/cucim/skimage/transform/__init__.py +++ b/python/cucim/src/cucim/skimage/transform/__init__.py @@ -1,3 +1,5 @@ -import lazy_loader as lazy +"""Geometric and other transformations, e.g., rotations, warp, etc.""" -__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/python/cucim/src/cucim/skimage/transform/_geometric.py b/python/cucim/src/cucim/skimage/transform/_geometric.py index 47cc133b8..8910b36f8 100644 --- a/python/cucim/src/cucim/skimage/transform/_geometric.py +++ b/python/cucim/src/cucim/skimage/transform/_geometric.py @@ -736,7 +736,7 @@ def _apply_mat(self, coords, matrix): return dst[:, :ndim] - def __array__(self, dtype=None): + def __array__(self, dtype=None, copy=None): """Return the transform parameters as an array. Note, __array__ is not currently supported by CuPy @@ -1341,60 +1341,26 @@ def inverse(self, coords): return out -def _euler_rotation(axis, angle): - """Produce a single-axis Euler rotation matrix. +def _euler_rotation_matrix(angles, degrees=False): + """Produce an Euler rotation matrix from the given intrinsic rotation angles + for the axes x, y and z. Parameters ---------- - axis : int in {0, 1, 2} - The axis of rotation. - angle : float - The angle of rotation in radians. - - Returns - ------- - Ri : ndarray of float, shape (3, 3) - The rotation matrix along axis `axis`. - """ - i = axis - s = (-1) ** i * _sin(angle) - c = _cos(angle) - R2 = np.array([[c, -s], [s, c]]) # noqa # noqa - Ri = np.eye(3) - # We need the axes other than the rotation axis, in the right order: - # 0 -> (1, 2); 1 -> (0, 2); 2 -> (0, 1). - axes = sorted({0, 1, 2} - {axis}) - # We then embed the 2-axis rotation matrix into the full matrix. - # (1, 2) -> R[1:3:1, 1:3:1] = R2, (0, 2) -> R[0:3:2, 0:3:2] = R2, etc. - sl = slice(axes[0], axes[1] + 1, axes[1] - axes[0]) - Ri[sl, sl] = R2 - return Ri - - -def _euler_rotation_matrix(angles, axes=None): - """Produce an Euler rotation matrix from the given angles. - - The matrix will have dimension equal to the number of angles given. - - Parameters - ---------- - angles : ndarray of float, shape (3,) + angles : array of float, shape (3,) The transformation angles in radians. - axes : list of int - The axes about which to produce the rotation. Defaults to 0, 1, 2. + degrees : bool, optional + If True, then the given angles are assumed to be in degrees. Default is + False. Returns ------- - R : ndarray of float, shape (3, 3) + R : array of float, shape (3, 3) The Euler rotation matrix. """ - if axes is None: - axes = range(3) - dim = len(angles) - R = np.eye(dim) - for i, angle in zip(axes, angles): - R = R @ _euler_rotation(i, angle) - return R + return spatial.transform.Rotation.from_euler( + "XYZ", angles=cp.asnumpy(angles), degrees=degrees + ).as_matrix() class EuclideanTransform(ProjectiveTransform): diff --git a/python/cucim/src/cucim/skimage/util/__init__.py b/python/cucim/src/cucim/skimage/util/__init__.py index 434709449..12871f2f6 100644 --- a/python/cucim/src/cucim/skimage/util/__init__.py +++ b/python/cucim/src/cucim/skimage/util/__init__.py @@ -1,3 +1,8 @@ +"""Generic utilities. + +This module contains a number of utility functions to work with images in general. +""" + from ._invert import invert from ._map_array import map_array from .arraycrop import crop diff --git a/python/cucim/src/cucim/skimage/util/_map_array.py b/python/cucim/src/cucim/skimage/util/_map_array.py index 2fe073020..3ac650ccf 100644 --- a/python/cucim/src/cucim/skimage/util/_map_array.py +++ b/python/cucim/src/cucim/skimage/util/_map_array.py @@ -170,7 +170,7 @@ def _ascupy(self, dtype=None): return output # This array method is mainly just here for use in the tests - def __array__(self, dtype=None): + def __array__(self, dtype=None, copy=None): """Return a NumPy array that behaves like the arraymap when indexed. This array can be very large: it is the size of the largest value