Add revised peak_local_max to skimage2#8039
Conversation
Note, that wasn't a test in the old suite that covers `p_norm`! Should definitely be added.
This comment was marked as outdated.
This comment was marked as outdated.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
src/skimage2/feature/_peak.py (1)
1-6: Align imports with repo conventions (relative + SciPy alias).Prefer relative imports from
skimage2internals and theimport scipy as spconvention; then replacendi.*usages withsp.ndimage.*throughout the file.✅ Suggested update
-import scipy.ndimage as ndi - -from skimage._shared._warnings import warn_external -from skimage._shared.coord import ensure_spacing +import scipy as sp + +from .._shared._warnings import warn_external +from .._shared.coord import ensure_spacingAs per coding guidelines, use relative module imports (e.g., from .._shared import xyz) and the import convention: import scipy as sp.
src/skimage/feature/peak.py (1)
131-144: Considerdeprecate_funcfor standardized deprecation handling.Since this is a public API deprecation, using
skimage._shared.utils.deprecate_funcwould align with the project’s standard deprecation utilities while still allowing a custom warning category.As per coding guidelines, use skimage._shared.utils.deprecate_parameter and skimage._shared.utils.deprecate_func utility functions for removing keywords or entire functions.
tests/skimage2/feature/test_peak.py (1)
1-559: Missing test coverage forp_normparameter.Per PR objectives and Issue
#6608, a key change is settingp_norm=2(Euclidean distance) as the new default instead ofp_norm=np.inf(Chebyshev distance). The commit messages also note that "existing tests do not coverp_normand a test should be added."Consider adding a test that verifies:
- The default
p_norm=2behavior (Euclidean distance)- Explicit
p_normvalues produce expected results for peak suppression based on distance💡 Example test for p_norm coverage
def test_p_norm_euclidean_default(self): """Test that p_norm=2 (Euclidean) is the default for distance calculation.""" image = np.zeros((10, 10)) # Place two peaks at distance sqrt(8) ≈ 2.83 apart (Euclidean) # but distance 2 apart (Chebyshev/infinity norm) image[3, 3] = 2 image[5, 5] = 1 # With min_distance=2, Chebyshev would suppress the second peak # but Euclidean (sqrt(8) > 2) should keep both peaks = peak_local_max(image, min_distance=2, threshold_rel=0) assert len(peaks) == 2 def test_p_norm_infinity(self): """Test explicit p_norm=inf uses Chebyshev distance.""" image = np.zeros((10, 10)) image[3, 3] = 2 image[5, 5] = 1 # With p_norm=inf, distance is max(|5-3|, |5-3|) = 2 # With min_distance=3, should suppress second peak peaks = peak_local_max(image, min_distance=3, threshold_rel=0, p_norm=np.inf) assert len(peaks) == 1tests/skimage/feature/test_peak.py (1)
1-662: Consider adding ap_normdeprecation/behavior change test for the wrapper.Similar to the skimage2 test file, this file should also verify that the skimage wrapper correctly handles or documents the
p_normdefault change behavior. This is important since the oldskimageimplementation usedp_norm=np.infwhileskimage2usesp_norm=2.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/skimage2/feature/_peak.py (1)
98-103: UseTypeErrorfor type validation in tuple elements.When a tuple element is not an integer, this is a type error rather than a value error. The static analysis correctly identifies this.
♻️ Proposed fix
for exclude in exclude_border: if not isinstance(exclude, int): - raise ValueError( + raise TypeError( "`exclude_border`, when expressed as a tuple, must only " "contain ints." )tests/skimage2/feature/test_peak.py (2)
154-178: Consider seeding random number generator for test reproducibility.
test_reorder_labelsusesnp.random.uniformwithout setting a seed, which could lead to flaky tests. Consider usingnp.random.seed()or anumpy.random.Generatorfor deterministic behavior.♻️ Proposed fix
def test_reorder_labels(self): + np.random.seed(42) image = np.random.uniform(size=(40, 60))
13-560: Add tests forp_normparameter to verify Euclidean distance behavior.The PR changes the default
p_normfromnp.inf(Chebyshev distance) to2(Euclidean distance) per issue#6608. However, there are no tests verifying:
- The default
p_norm=2behavior differs fromp_norm=np.inf- Different
p_normvalues produce expected resultsConsider adding a test that demonstrates the difference between distance metrics.
💡 Example test for p_norm parameter
def test_p_norm_euclidean_vs_chebyshev(self): """Test that p_norm=2 (Euclidean) differs from p_norm=inf (Chebyshev).""" image = np.zeros((10, 10)) # Place peaks at positions where Euclidean and Chebyshev distances differ image[2, 2] = 2 # highest peak image[4, 5] = 1 # Euclidean dist ~3.6, Chebyshev dist = 3 # With min_distance=3 and Chebyshev (inf-norm), both peaks kept (dist=3) peaks_chebyshev = peak_local_max( image, min_distance=3, p_norm=np.inf, exclude_border=False ) # With min_distance=3 and Euclidean (2-norm), second peak excluded (dist~3.6>3) # Actually with ensure_spacing, higher intensity wins, so behavior depends on implementation peaks_euclidean = peak_local_max( image, min_distance=3, p_norm=2, exclude_border=False ) # Verify default uses Euclidean peaks_default = peak_local_max(image, min_distance=3, exclude_border=False) assert_equal(peaks_default, peaks_euclidean)Based on learnings: "Tests should be added to cover all modifications in behavior".
This fix is picked from scikit-imagegh-7889.
Co-authored-by: Stefan van der Walt <stefan@mentat.za.net>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
doc/source/user_guide/skimage2_migration.md (1)
77-89: Migration guidance is less concrete than thebinary_blobsexample.The
binary_blobsmigration section (lines 55–66) spells out exact values and formulas. Here,old_p_normandnew_exclude_borderare placeholders with only a prose explanation. Consider making this more actionable — e.g., showing concrete old defaults (p_norm=np.inf,exclude_border=min_distance) with a brief comment, similar to howbinary_blobsdoes it.src/skimage2/feature/_peaks.py (1)
54-62:_exclude_bordermutates its input in-place — safe given callers always pass a copy.In the labels path (line 248),
labels.astype(int, casting="safe")creates a new array before passing it here. In the mask path (line 240), the mask is freshly created by_get_peak_mask. Both are safe, but the in-place mutation could be surprising if this helper is reused elsewhere. A brief docstring note (e.g., "Modifieslabelin-place") would help.
Note, that `threshold_rel=0` is equivalent to `threshold=0`, since `image.max() * 0 == 0`.
stefanv
left a comment
There was a problem hiding this comment.
I think this can go in, whenever you are ready.
Co-authored-by: Stefan van der Walt <stefan@mentat.za.net>
This reverts commit e822781. We decided that, the status quo is okay for now. `threshold_rel` doesn't represent the footgun that it was in the `blob_doh` and similar funcs, because there's no dtype-based rescaling going on. Also, we can always deprecate later in skimage 2 if we want.
This makes using `p_norm=2` (Euclidean distance) more useful.
Adds the function `peak_local_max` to the `skimage2` namespace. Compared to the old version in `skimage` it has a few differences: * Parameter `p_norm` defaults to 2 (Euclidean distance), was `numpy.inf` (Chebyshev distance) * Parameter `exclude_border` defaults to 1, was `True` * Parameter `exclude_border` no longer accepts `False` and `True`, pass 0 instead of `False`, or `min_distance` instead of `True` * Parameters after `image` are keyword-only Deprecates passing `np.inf` to `num_peaks` and `num_peaks_per_label`. In `skimage.feature.peak_local_max`, support passing floats to the parameter `min_distance`. In `skimage.feature.peak_local_max`, use `None` as the new default for the parameters `num_peaks` and `num_peaks_per_label` – the default behavior is the same as before but passing `numpy.inf` is deprecated. Ensure `skimage.feature.peak_local_max` returns results with the correct shape when no peaks where found and `labels` where given. --------- Co-authored-by: Stefan van der Walt <stefan@mentat.za.net>
* origin/main: (27 commits) Move `_shared` to `_skimage2` (#8102) Port `pad_footprint` & `mirror_footprint` to `skimage2` (#8094) Add rescaling API to skimage2 (#8075) Move `skimage2` implementation into `_skimage2` (#8093) Undo (double) mirroring in `ski2.morphology.dilation` (#8060) Grammar (#8091) Move `ensure_spacing` into `skimage2` (#8067) Ensure that `skimage2` does not eagerly import `skimage` (#8087) Use lazy-loader 0.5 (#8080) MAINT: make ellipse fitting forward compatible (#8054) Avoid deprecated assign to ndarray.shape (#8020) Avoid circular import in `feature/corner.py` (#8077) Turn off coderabbit auto-labeling & label checks (#8070) Fix GIL being re-enabled by C++ extensions (#8059) Fix conventions following docstub and misc. (#8055) Port grayscale morphology operators to skimage2 (#8046) Add revised `peak_local_max` to `skimage2` (#8039) Allow read-only arrays as input to remap (#7535) Add `prescale` parameter to "blob functions" (#7858) Try to make coderabbit respect exisiting type labels (#8042) ...
Description
Adds the function
peak_local_maxto theskimage2namespace. Compared to the old version inskimageit has a few differences:p_norm=2(Euclidean distance) as the new default (closes Use Euclidean distance inpeak_local_maxas new default #6608)image.Still to do:
Waiting forwarn_externalwhich was added in Add skimage2 version of binary_blobs with updated signature #7976 – that PR should be merged first.threshold_absandthreshold_relaround. See discussion around deprecation ofthreshold_relin Addprescaleparameter to "blob functions" #7858!Checklist
./doc/examplesfor new featuresRelease note
For maintainers and optionally contributors, please refer to the instructions on how to document this PR for the release notes.