Skip to content

Add revised peak_local_max to skimage2#8039

Merged
lagru merged 39 commits intoscikit-image:mainfrom
lagru:sk2-peak-local-max
Feb 13, 2026
Merged

Add revised peak_local_max to skimage2#8039
lagru merged 39 commits intoscikit-image:mainfrom
lagru:sk2-peak-local-max

Conversation

@lagru
Copy link
Copy Markdown
Member

@lagru lagru commented Jan 26, 2026

Description

Adds the function peak_local_max to the skimage2 namespace. Compared to the old version in skimage it has a few differences:

Still to do:

Checklist

Release note

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

Port `skimage.feature.peak_local_max` to `skimage2.feature` with a clearer
API and improved defaults.
{label="Maintenance"}
In `skimage.feature.peak_local_max`, support passing floats to the
parameter `min_distance`.
{label="Enhancement"}
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.
{label="API"}
Ensure `skimage.feature.peak_local_max` returns results with the correct shape
when no peaks where found and `labels` where given.
{label="bug fix"}

@lagru lagru added 🔧 type: Maintenance Refactoring and maintenance of internals 🥾 Path to skimage2 A step towards the new "API 2.0" labels Jan 26, 2026
@lagru lagru marked this pull request as ready for review January 30, 2026 19:19
@coderabbitai

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 skimage2 internals and the import scipy as sp convention; then replace ndi.* usages with sp.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_spacing

As 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: Consider deprecate_func for standardized deprecation handling.

Since this is a public API deprecation, using skimage._shared.utils.deprecate_func would 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 for p_norm parameter.

Per PR objectives and Issue #6608, a key change is setting p_norm=2 (Euclidean distance) as the new default instead of p_norm=np.inf (Chebyshev distance). The commit messages also note that "existing tests do not cover p_norm and a test should be added."

Consider adding a test that verifies:

  1. The default p_norm=2 behavior (Euclidean distance)
  2. Explicit p_norm values 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) == 1
tests/skimage/feature/test_peak.py (1)

1-662: Consider adding a p_norm deprecation/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_norm default change behavior. This is important since the old skimage implementation used p_norm=np.inf while skimage2 uses p_norm=2.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/skimage2/feature/_peak.py (1)

98-103: Use TypeError for 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_labels uses np.random.uniform without setting a seed, which could lead to flaky tests. Consider using np.random.seed() or a numpy.random.Generator for deterministic behavior.

♻️ Proposed fix
     def test_reorder_labels(self):
+        np.random.seed(42)
         image = np.random.uniform(size=(40, 60))

13-560: Add tests for p_norm parameter to verify Euclidean distance behavior.

The PR changes the default p_norm from np.inf (Chebyshev distance) to 2 (Euclidean distance) per issue #6608. However, there are no tests verifying:

  1. The default p_norm=2 behavior differs from p_norm=np.inf
  2. Different p_norm values produce expected results

Consider 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".

@lagru lagru added this to skimage2 Jan 31, 2026
@github-project-automation github-project-automation bot moved this to To Do in skimage2 Jan 31, 2026
@lagru lagru moved this from To Do to To Review in skimage2 Jan 31, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
doc/source/user_guide/skimage2_migration.md (1)

77-89: Migration guidance is less concrete than the binary_blobs example.

The binary_blobs migration section (lines 55–66) spells out exact values and formulas. Here, old_p_norm and new_exclude_border are 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 how binary_blobs does it.

src/skimage2/feature/_peaks.py (1)

54-62: _exclude_border mutates 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., "Modifies label in-place") would help.

lagru added 2 commits February 9, 2026 19:47
Note, that `threshold_rel=0` is equivalent to `threshold=0`, since
`image.max() * 0 == 0`.
Copy link
Copy Markdown
Member

@stefanv stefanv left a comment

Choose a reason for hiding this comment

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

I think this can go in, whenever you are ready.

lagru and others added 5 commits February 10, 2026 14:03
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.
@lagru lagru merged commit b3b128c into scikit-image:main Feb 13, 2026
26 checks passed
@github-project-automation github-project-automation bot moved this from To Review to Done in skimage2 Feb 13, 2026
@stefanv stefanv added this to the 0.27 milestone Feb 13, 2026
@lagru lagru deleted the sk2-peak-local-max branch February 13, 2026 14:25
matusvalo pushed a commit to matusvalo/scikit-image that referenced this pull request Mar 4, 2026
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>
matthew-brett added a commit that referenced this pull request Apr 10, 2026
* 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)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🥾 Path to skimage2 A step towards the new "API 2.0" 🔧 type: Maintenance Refactoring and maintenance of internals

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Use Euclidean distance in peak_local_max as new default

2 participants