Skip to content

Deprecate slow binary_* functions in skimage.morphology#7665

Merged
jarrodmillman merged 16 commits intoscikit-image:mainfrom
lagru:deprecate_binary_morphology
Feb 25, 2025
Merged

Deprecate slow binary_* functions in skimage.morphology#7665
jarrodmillman merged 16 commits intoscikit-image:mainfrom
lagru:deprecate_binary_morphology

Conversation

@lagru
Copy link
Copy Markdown
Member

@lagru lagru commented Jan 20, 2025

Description

As discussed in our last hackathon and detailed in our skimage2 agenda.

Most of the time, these functions seem to be slower than our regular gray-type functions, e.g., ski.morphology.binary_erosion vs ski.morphology.erosion. A potential reason for this might be that the binary_ funcs use SciPy's (regressed) slow versions under the hood (scipy/scipy#13991).

Also if we add back a faster binary version at some point, can just do it internally for binary input.

Checklist

Release note

For maintainers and optionally contributors, please refer to the instructions on how to document this PR for the release notes.

In `skimage.morphology`, deprecate `binary_erosion`, `binary_dilation`, `binary_opening`, 
and `binary_closing` in favor of `erosion`, `dilation`, `opening`, and `closing` 
respectively. The binary versions weren't actually significantly faster than their
non-binary counterparts and sometimes significantly slower. In the future, we might
add optimizations internally to the remaining (general, non-binary) functions for 
when they're used with binary inputs.

lagru added 2 commits January 20, 2025 19:50
Most of the time, these seem to be slower than our regular gray-type
functions, e.g., `ski.morphology.binary_erosion` vs
`ski.morphology.erosion`. A potential reason for this might be that the
`binary_` funcs use SciPy's (regressed) slow versions under the hood
(scipy issue 13991).

Also if we add back a faster binary version at some point, can just do
it internally for binary input.

See also
https://github.com/scikit-image/scikit-image/wiki/API-changes-for-skimage2#proposed-api-changes-doable-independent-of-skimage2
Creating and testing large 3D footprints with ball is very slow so that
the test took 26 s in total on my machine. Removing 50-100 shaves of
~25 s without sacrificing any test coverage I think.
@lagru lagru added 📜 type: API Involves API change(s) 🥾 Path to skimage2 A step towards the new "API 2.0" labels Jan 20, 2025
@lagru
Copy link
Copy Markdown
Member Author

lagru commented Jan 20, 2025

Here's a quick & dirty script (and it's output on my machine) to get an overview about the performance of the mentioned functions.

Details
# timeit.py

import timeit
import subprocess

import numpy as np
import skimage as ski

mask_sizes = [10, 1000]
footprint_sizes = [3, 9]
funcs = ["erosion", "dilation", "opening", "closing"]

for func in funcs:
    for mask_size in mask_sizes:
        for footprint_size in footprint_sizes:
            print(f"{mask_size=}, {footprint_size=}")
            setup = (
                "import skimage as ski; import numpy as np;"
                "import skimage.morphology as sm;"
                f"mask = ski.data.binary_blobs(length={mask_size});"
                f"footprint = sm.footprint_rectangle(({footprint_size},) * 2);"
            )
            test_gray = f"sm.{func}(mask, footprint)"
            print("  " + test_gray, end=": ")
            subprocess.run(
                ["python", "-m", "timeit", "-s", setup, test_gray]
            )

            test_binary = f"sm.binary_{func}(mask, footprint)"
            print("  " + test_binary, end=": ")
            subprocess.run(
                ["python", "-m", "timeit", "-s", setup, test_binary]
            )
            print()

prints

mask_size=10, footprint_size=3
  sm.erosion(mask, footprint): 5000 loops, best of 5: 74.4 usec per loop
  sm.binary_erosion(mask, footprint): 5000 loops, best of 5: 58.5 usec per loop

mask_size=10, footprint_size=9
  sm.erosion(mask, footprint): 5000 loops, best of 5: 70.8 usec per loop
  sm.binary_erosion(mask, footprint): 2000 loops, best of 5: 106 usec per loop

mask_size=1000, footprint_size=3
  sm.erosion(mask, footprint): 50 loops, best of 5: 7.61 msec per loop
  sm.binary_erosion(mask, footprint): 20 loops, best of 5: 11 msec per loop

mask_size=1000, footprint_size=9
  sm.erosion(mask, footprint): 50 loops, best of 5: 7.63 msec per loop
  sm.binary_erosion(mask, footprint): 10 loops, best of 5: 35.2 msec per loop

mask_size=10, footprint_size=3
  sm.dilation(mask, footprint): 5000 loops, best of 5: 75.4 usec per loop
  sm.binary_dilation(mask, footprint): 5000 loops, best of 5: 57.8 usec per loop

mask_size=10, footprint_size=9
  sm.dilation(mask, footprint): 5000 loops, best of 5: 75.8 usec per loop
  sm.binary_dilation(mask, footprint): 2000 loops, best of 5: 116 usec per loop

mask_size=1000, footprint_size=3
  sm.dilation(mask, footprint): 50 loops, best of 5: 7.33 msec per loop
  sm.binary_dilation(mask, footprint): 20 loops, best of 5: 10.9 msec per loop

mask_size=1000, footprint_size=9
  sm.dilation(mask, footprint): 50 loops, best of 5: 7.73 msec per loop
  sm.binary_dilation(mask, footprint): 10 loops, best of 5: 36.4 msec per loop

mask_size=10, footprint_size=3
  sm.opening(mask, footprint): 2000 loops, best of 5: 179 usec per loop
  sm.binary_opening(mask, footprint): 2000 loops, best of 5: 114 usec per loop

mask_size=10, footprint_size=9
  sm.opening(mask, footprint): 2000 loops, best of 5: 184 usec per loop
  sm.binary_opening(mask, footprint): 1000 loops, best of 5: 234 usec per loop

mask_size=1000, footprint_size=3
  sm.opening(mask, footprint): 20 loops, best of 5: 15.4 msec per loop
  sm.binary_opening(mask, footprint): 10 loops, best of 5: 21.9 msec per loop

mask_size=1000, footprint_size=9
  sm.opening(mask, footprint): 20 loops, best of 5: 15.7 msec per loop
  sm.binary_opening(mask, footprint): 5 loops, best of 5: 79.8 msec per loop

mask_size=10, footprint_size=3
  sm.closing(mask, footprint): 2000 loops, best of 5: 177 usec per loop
  sm.binary_closing(mask, footprint): 2000 loops, best of 5: 115 usec per loop

mask_size=10, footprint_size=9
  sm.closing(mask, footprint): 2000 loops, best of 5: 189 usec per loop
  sm.binary_closing(mask, footprint): 1000 loops, best of 5: 247 usec per loop

mask_size=1000, footprint_size=3
  sm.closing(mask, footprint): 20 loops, best of 5: 15.7 msec per loop
  sm.binary_closing(mask, footprint): 10 loops, best of 5: 20.6 msec per loop

mask_size=1000, footprint_size=9
  sm.closing(mask, footprint): 20 loops, best of 5: 14.8 msec per loop
  sm.binary_closing(mask, footprint): 5 loops, best of 5: 71.4 msec per loop

binary_* seem to have a slight advantage for small footprints (3x3) and images (10x10), but are slower, sometimes significantly, for large footprints (9x9) and images (1000x1000).

@lagru lagru requested review from mkcor and stefanv January 20, 2025 19:03
@lagru lagru added the 🔽 Deprecation Involves deprecation label Jan 20, 2025
Copy link
Copy Markdown
Member

@mkcor mkcor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling the skimage2 agenda! 🙏

There's also an instance of skimage.morphology.binary_dilation in

from skimage.morphology import binary_dilation, disk

Do we want to update the scipy.ndimage.binary_* as well?

And should we clean up

# These function names are the same in ndimage.
funcs = (
'binary_erosion',
'binary_dilation',
'binary_opening',
'binary_closing',
'black_tophat',
'white_tophat',
)
skimage2ndimage.update({x: x for x in funcs})

? Maybe that's implied in the TODO for v0.28?

@lagru
Copy link
Copy Markdown
Member Author

lagru commented Jan 21, 2025

# These function names are the same in ndimage.
funcs = (
'binary_erosion',
'binary_dilation',
'binary_opening',
'binary_closing',
'black_tophat',
'white_tophat',
)
skimage2ndimage.update({x: x for x in funcs})

That whole skimage2ndimage seems outdated and not to be used anywhere. But for the sake of the scope of this PR I'll only remove the references to the deprecated functions. We can always come back and clean this up later but I don't think it's a notable problem, since it's not really advertised by us anywhere. Long-term / for skimage2 we should make misc.py and similar private though I think.

lagru and others added 4 commits January 21, 2025 18:10
Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org>
Just moves `params`, `param_names` and `setup` over to the inheriting
class since the base classes are removed.
Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org>
@lagru
Copy link
Copy Markdown
Member Author

lagru commented Jan 21, 2025

Note the release note in the PR description. This time I chose to expand a bit on the reasoning behind the deprecation since it might otherwise look like a unnecessary deprecation.

@mkcor
Copy link
Copy Markdown
Member

mkcor commented Jan 23, 2025

Note the release note in the PR description. This time I chose to expand a bit on the reasoning behind the deprecation since it might otherwise look like a unnecessary deprecation.

Thanks for this thorough release note, indeed! I would just add Oxford commas (before "and binary_closing" and before "and closing"). And another comma before "respectively," but here it's me being picky!

I would replace "to the remaining non-deprecated functions" with "to the remaining (general, non-binary) functions for when they're used with binary inputs."

Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org>
lagru and others added 3 commits January 26, 2025 13:57
Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org>
Inspired by the examples of the gray morphology functions.

(cherry picked from commit 3b2b1a5)
Because this function takes a `radius` to specify the neighborhood,
the implied "footprint" is always circular and of uneven size. The
rewording tries to make that clearer.

Also use the more expressive term "neighborhood" instead of "region"
in the description of the argument `radius`.
Copy link
Copy Markdown
Member

@mkcor mkcor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @lagru!

5776eb2 is a very nice addition, but I'm missing what's going on for the isotropic_opening and isotropic_closing examples, since np.testing.assert_array_equal(image, result) (no change?).

lagru and others added 4 commits February 3, 2025 18:24
Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org>
as suggested by Marianne.

Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org>
@lagru
Copy link
Copy Markdown
Member Author

lagru commented Feb 6, 2025

I addressed all your comments @mkcor. Should be ready to go. :D

Copy link
Copy Markdown
Member

@mkcor mkcor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

@lagru lagru added this to the 0.26 milestone Feb 10, 2025
@lagru lagru marked this pull request as draft February 10, 2025 16:18
@lagru
Copy link
Copy Markdown
Member Author

lagru commented Feb 10, 2025

Converting to draft so this is not accidentally merged into the pending 0.25.2 patch release.

@mkcor mkcor marked this pull request as ready for review February 19, 2025 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔽 Deprecation Involves deprecation 🥾 Path to skimage2 A step towards the new "API 2.0" 📜 type: API Involves API change(s)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

morphology.binary_closing() is ~50x slower than morphology.closing(). The ratio should be reversed

3 participants