Repository navigation
feat: peak_local_max with footprint, labels and num_peaks_per_label - #54
Merged
Merged
Conversation
RollingBall.background (radius and kernel overloads) takes nanSafe: Bool = false, the nansafe=True of scikit-image 0.26.0 rolling_ball: a NaN anywhere in the kernel rectangle, cells outside the ball included, makes the pixel NaN, and so does -inf under such a cell. Without nanSafe a NaN is still refused. 21 cases pinned by tools/rolling-ball-nansafe/pins.py at assert_allclose dtype defaults (the wheel ships no tests). Done: rolling_ball nansafe from #10. Next: the EDT return_indices gap of #10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DistanceTransform.euclideanWithIndices returns the distances and, per pixel, the row and column of the nearest background pixel: SciPy 1.18.1 distance_transform_edt with return_indices=True, 2D. The feature transform is the Voronoi sweep of SciPy ported term by term, with the fused multiply-adds of its arm64 build, which decide exact ties under a non-dyadic sampling. 33 cases pinned by tools/edt-indices/pins.py: indices exact, distances 1e-12. The sampling and shape guards are shared with euclidean. Done: EDT return_indices from #10. Next: watershed compactness and watershed_line of #10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An integer and .nan literal list leaves the generic sample type to inference; a typed array makes the check compile the same way on every toolchain. Done: consumer check typed. Next: the EDT return_indices PR stacked on this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Segmentation.watershed takes compactness: Double = 0 and watershedLine: Bool = false, scikit-image 0.26.0 segmentation.watershed with compactness= and watershed_line=, 2D: the pixel is labelled when it leaves the queue, each queued copy keeps its marker, the compact cost is image + compactness * distance to the marker (fused as the arm64 build computes it) raised to the popped value, no line is drawn with compactness, and a line pixel leaves the mask with label 0 but still passes the flood on. 59 cases pinned by tools/watershed-compact/pins.py, labels exact. A negative or non-finite compactness is refused. The plain path is unchanged. Done: watershed compactness and watershed_line from #10. Next: the docs/parity.md and open-items rows for it once the two PRs below it merge; the N-D and denoise gaps need a decision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#44 landed on main as a squash. docs/open-items.md conflicted on the 0.8.0 gaps line: kept this branch wording, which drops "without return_indices" now that euclideanWithIndices is in. The diff against main equals the diff this PR had against its old base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WindowType gains triang, parzen, bohman, barthann, cosine, exponential(tau:), taylor(nbar:sll:norm:), lanczos, kaiserBesselDerived(beta:), generalCosine(a:), generalGaussian(p:sig:), generalHamming(alpha:), dpss(nw:) and chebwin(at:), so window1D and window2D now cover every family of SciPy 1.18.1 get_window (symmetric, as scikit-image 0.26.0 filters.window asks for it). dpss is the largest eigenvector of the SciPy tridiagonal matrix by bisection and inverse iteration (the LAPACK stebz and stein that SciPy calls), with norm approximate; chebwin sums its DFT directly from one cosine table. kaiserBesselDerived refuses an odd length, dpss an NW outside (0, M/2), taylor an nbar below 1, as SciPy does. Pinned by tools/window-families/pins.py: 162 2D cases, 34 1D cases (length 2 for every family, lengths 1023 to 4096 for dpss and chebwin) and 4 refusals. The 1D tolerance is the one scipy/signal/tests/test_windows.py uses for each family; the 2D window has no upstream test and takes the float64 assert_allclose defaults widened by that family tolerance. Migration: WindowType is a public enum without a default case; a switch over it with no default must handle the new cases or add one. Done: the window families of #12. Next: butterworth and difference_of_gaussians with channel_axis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…axis) BandpassFilters.butterworth (both overloads) and differenceOfGaussians gain channels: Int = 1. Above 1 the image is read channel-last, as scikit-image 0.26.0 channel_axis=-1, and each channel is filtered alone: scikit-image filters only the spatial axes (butterworth broadcasts its mask over the channels and crops away the npad it adds to the channel axis; difference_of_gaussians has sigma 0 there). One channel takes the old path unchanged. A channel count that does not fit the pixel count is refused (.shape). Pinned by tools/bandpass-channels/pins.py: 16 butterworth and 9 difference_of_gaussians cases, Float32 and Float64, at the single-channel filter gate (assert_allclose dtype defaults; the wheel ships no tests). The pin script also checks that channel_axis=0 and each channel alone give the same answer. Done: the channel_axis bullet of #12. Next: profile_line multichannel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LineProfile.sample(channels:) and unreduced(channels:) take one Micrograph per channel (scikit-image 0.26.0 profile_line on a channel-last 3D image) and profile each channel on the same perpendicular lines, which is what scikit-image does: one map_coordinates per channel, then reduce_func(pixels, axis=1). The answer is one LineProfile per channel, or [channel][sample][k] unreduced. No channel, or channels of different sizes, throw the new LineProfileError.channelShape. Pinned by tools/profile-multichannel/pins.py: 360 cases (orders 0, 1, 3, 5; the five modes; widths 1 to 16; the five reductions and reduce_func None), float32 and float64, at assert_allclose dtype defaults per element (the wheel ships no tests). Also corrects the header note on the float32 width sum (it is the pairwise sum of the flipped row, as the code says). Migration: LineProfileError is a public enum; an exhaustive switch over it must handle .channelShape. Done: the profile_line multichannel bullet of #12. Next: peak_local_max with footprint and labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PeakLocalMax.find gains footprint: PeakFootprint?, labels: [Int]? and numPeaksPerLabel: Int?, between numPeaks and pNorm, all defaulted, so every existing call compiles unchanged. As scikit-image 0.26.0: the footprint replaces the (2 min_distance + 1) square in the maximum filter (origin at size / 2, mode nearest, no flip; a full rectangle takes the separable path), while min_distance still spaces the peaks and sets the default border. With labels each positive label is searched in the bounding box of what the border cut leaves of it, other pixels set to the lowest finite value, with the trivial-image rule that keeps the pixels the binary opening of the label mask removes; at most numPeaksPerLabel per label, in label order, then a global re-sort and re-spacing when more than numPeaks remain. An all-false footprint of more than one cell is refused as SciPy refuses it; symmetry with labels is refused. Pinned by tools/peak-footprint-labels/pins.py: 198 cases, coordinates exact and in order (asymmetric, centreless and corner footprints, negative and gapped labels, a constant one-pixel-wide label, labels side by side under num_peaks, min_distance 0). Refusals in ImagoFiltersTests. Done: the peak_local_max footprint and labels bullet of #12. Next: spline shifts constant and wrap at orders 2 to 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The watershed row says compactness and watershed_line are held (#46, tools/watershed-compact); the rolling_ball row names the kernel overflow fix (#47, tools/rolling-ball-kernel-overflow); tools/README.md lists tools/edt-indices and tools/watershed-compact; open-items no longer says watershed lacks compactness. Every row on main stays. The rest of this chain inherits these lines. Done: chain base re-synced with main and its docs brought up to main. Next: merge it up the chain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/parity.md: the butterworth and difference_of_gaussians rows keep this branch, which adds channels:; the window row next to them keeps the all-families text from main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/open-items.md: main dropped the profile_line multichannel item and this branch closes the peak_local_max footprint/labels item, so the study-row line goes away entirely. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Paul · project thread
Part of #12 (the
peak_local_maxfootprint/labels bullet). Still open in #12: spline shifts inconstantandwrapmodes at orders 2 to 5, and the masked-registration tie rule.This PR sits on top of #53 (
issue-12-profile-multichannel), which sits on #52 and #51. The stack already touchesdocs/parity.md,docs/open-items.mdand the consumer check, so this branch builds on it instead of becoming another PR that edits those files. Retarget it once #53 merges.Summary
PeakLocalMax.findgains three parameters:footprint: PeakFootprint?,labels: [Int]?andnumPeaksPerLabel: Int?. They sit betweennumPeaksandpNormand all have defaults, so every existing call compiles unchanged. The behaviour follows scikit-image 0.26.0.footprintnearest, and the footprint is not flipped. A full rectangle takes the separable path.min_distancestill spaces the peaks and sets the default border.labelsnumPeaksPerLabelpeaks, collected in label order. When more thannumPeaksremain, they are sorted and spaced again over the whole image.symmetrytogether withlabelsis refused.Tests
tools/peak-footprint-labels/pins.py(arm64, NumPy 2.5.4, SciPy 1.18.1, scikit-image 0.26.0) writespeak-footprint-labels-pins.jsonwith 198 cases, float32 and float64. Coordinates must match exactly, in order. The cases cover:num_peaksnum_peaks_per_label=0andmin_distance=0ImagoFiltersTests.PeakFootprintLabelsTestsfailed to compile on the base (cannot find 'PeakFootprint').--skip ImagoKitParityTests) all pass.labelsandthreshold_abs=-inf(background pixels then pass the threshold). The JSON fixture format carries no infinities; the behaviour follows from the same code path.🤖 Generated with Claude Code