Skip to content

Grelee/skimage filters rank#1115

Open
grlee77 wants to merge 46 commits into
rapidsai:mainfrom
grlee77:grelee/skimage-filters-rank
Open

Grelee/skimage filters rank#1115
grlee77 wants to merge 46 commits into
rapidsai:mainfrom
grlee77:grelee/skimage-filters-rank

Conversation

@grlee77

@grlee77 grlee77 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds the cucim.skimage.filters.rank module, providing
GPU-accelerated rank filters for CuPy arrays with an API modeled after
skimage.filters.rank. It resolves #235.

The existing cucim.skimage.filters.median wrapper is also extended with
behavior='rank', which delegates to the new rank median implementation.
behavior='ndimage' remains the default.

The implementation is intended to be largely compatible with scikit-image for
common rank-filter workflows. The most visible behavioral difference is
boundary handling: cuCIM uses reflected boundary extension, while scikit-image
rank filters crop neighborhoods at image boundaries. cuCIM also supports N-D
inputs and optional native processing of integer and real floating-point
dtypes, with some output-dtype and percentile edge-case behavior differing
from scikit-image. otsu and windowed_histogram are not currently
implemented.

See the detailed compatibility overview in
cucim.skimage.filters.rank.

Implementation Approach

The module uses multiple backend implementations because no single algorithm is
best across all filters, dtypes, dimensionalities, and footprint sizes.

  • The elementwise backend handles the broadest set of cases: N-dimensional
    inputs, arbitrary footprint shapes, masks, shifts, and native integer and
    real floating-point dtypes. Operations that do not require sorted values use
    streaming reductions within this backend. It avoids requiring a fixed
    histogram range and is the general fallback.
  • The histogram-based implementation targets high-value 2-D uint8 cases with
    fully populated, odd-sized rectangular footprints. It maintains a local
    sliding histogram and can reuse work between adjacent pixels, which is much
    more favorable for larger rectangular windows and filters that can be
    expressed from histogram counts, order statistics, prefix counts, or prefix
    sums.

By default, non-uint8 inputs are converted to uint8 with img_as_ubyte before
backend selection, matching scikit-image rank-filter behavior more closely and
allowing eligible inputs to use the uint8 histogram fast path. Users can opt
out with cast_to_uint8=False when they need native-dtype elementwise
behavior.

Testing

The test suite provides several independent checks of correctness and backend
dispatch:

  • All rank-filter APIs are compared with stored scikit-image reference results
    for 2-D inputs, with generic filters also covered in 3-D. Comparisons account
    for the documented boundary behavior and explicitly identify the few known
    algorithmic differences.
  • Independent brute-force reference implementations validate the elementwise
    streaming operations and the histogram percentile, prefix-count, prefix-sum,
    bilateral, modal, and entropy operations. Forced elementwise and histogram
    results are also compared directly.
  • Parameterized coverage exercises integer and floating-point dtypes, default
    uint8 conversion, native-dtype processing, output dtypes and scaling,
    overflow-sensitive sums, masks, shifts, arbitrary footprints, and trivial or
    empty neighborhoods.
  • Backend tests cover automatic cutoff selection, explicit overrides,
    compatibility rejection and fallback, histogram counter widths, scratch
    partition limits, invalid inputs, and overlapping output aliases.
  • Integration tests verify that cucim.skimage.filters.median with
    behavior='rank' matches rank.median for array and tuple footprints,
    supports caller-provided output arrays, and retains the existing
    behavior='ndimage' default and warning behavior.

The complete rank-filter test module currently reports 281 passed tests and 3
documented skips for known algorithmic differences.

Reviewer Notes

The rank module includes support for generic rank filters, percentile-range
filters, bilateral rank filters, entropy, modal/majority, and related
operations. Backend selection is automatic by default, but a backend keyword
can force elementwise or histogram for testing, profiling, and threshold
tuning.

cuCIM Rank Filter Benchmark Summary

Timings are from cucim_filters_rank_results.csv files generated by run-nv-bench-filters-rank.sh or cucim_filters_rank_bench.py.

CUCIM_BENCHMARK_MAX_DURATION=3 FOOTPRINT_SIZES=3,7,15,25,51,75 ./run-nv-bench-filters-rank.sh

python summarize_rank_benchmark_results.py \
	cucim_filters_rank_results.csv \
	--shape 1920x1080 \
	--backend auto \
	--footprint 3x3,7x7,15x15,25x25,51x51,75x75 \
	-o rank_filter_pr_benchmarks.md

Speedup vs scikit-image CPU: uint8, rectangular footprint with "auto" algorithm tuning

filter image dtype backend 3x3 7x7 15x15 25x25 51x51 75x75
autolevel 1920x1080 uint8 auto 860.3x 993.1x 796.7x 171.8x 63.9x 39.2x
autolevel_percentile 1920x1080 uint8 auto 809.7x 106.9x 17.7x 11.7x 16.5x 20.5x
enhance_contrast 1920x1080 uint8 auto 1172.1x 988.7x 691.8x 155.6x 59.4x 36.8x
enhance_contrast_percentile 1920x1080 uint8 auto 819.1x 106.9x 17.8x 21.0x 29.2x 36.1x
entropy 1920x1080 uint8 auto 247.4x 108.5x 47.9x 20.6x 24.8x 27.7x
equalize 1920x1080 uint8 auto 566.3x 703.1x 585.9x 152.1x 65.6x 42.9x
geometric_mean 1920x1080 uint8 auto 130.7x 45.4x 19.8x 26.1x 40.1x 50.5x
gradient 1920x1080 uint8 auto 1223.0x 978.4x 703.6x 159.0x 59.6x 37.9x
gradient_percentile 1920x1080 uint8 auto 767.9x 100.6x 17.0x 20.3x 28.9x 35.8x
maximum 1920x1080 uint8 auto 549.6x 570.5x 524.5x 134.9x 59.6x 39.6x
mean 1920x1080 uint8 auto 690.5x 336.7x 119.7x 60.3x 21.9x 14.8x
mean_bilateral 1920x1080 uint8 auto 504.5x 152.5x 44.5x 20.1x 17.7x 21.9x
mean_percentile 1920x1080 uint8 auto 1062.4x 151.5x 25.1x 19.6x 26.5x 32.2x
median 1920x1080 uint8 auto 558.2x 87.1x 16.3x 19.4x 28.8x 36.0x
modal (majority) 1920x1080 uint8 auto 803.3x 113.8x 32.4x 41.9x 56.6x 70.0x
minimum 1920x1080 uint8 auto 706.5x 674.6x 594.0x 150.1x 59.6x 45.0x
noise_filter 1920x1080 uint8 auto 435.2x 417.2x 332.6x 108.0x 51.3x 33.0x
percentile 1920x1080 uint8 auto 532.8x 75.8x 14.0x 17.9x 27.1x 34.9x
pop 1920x1080 uint8 auto 340.3x 574.6x 1047.3x 1760.2x 3286.8x 4824.1x
pop_bilateral 1920x1080 uint8 auto 847.5x 252.0x 66.4x 29.2x 30.9x 37.1x
pop_percentile 1920x1080 uint8 auto 1633.2x 204.0x 31.4x 27.7x 30.4x 33.7x
subtract_mean 1920x1080 uint8 auto 723.3x 344.7x 120.2x 61.6x 22.6x 13.7x
subtract_mean_percentile 1920x1080 uint8 auto 1192.4x 154.1x 25.2x 17.8x 23.9x 29.0x
sum 1920x1080 uint8 auto 892.4x 376.2x 122.2x 61.1x 22.4x 13.7x
sum_bilateral 1920x1080 uint8 auto 551.8x 154.0x 47.2x 20.4x 26.7x 33.3x
sum_percentile 1920x1080 uint8 auto 1187.0x 153.1x 25.9x 31.9x 41.5x 50.8x
threshold 1920x1080 uint8 auto 781.0x 357.7x 122.6x 60.4x 22.3x 13.8x
threshold_percentile 1920x1080 uint8 auto 754.6x 101.8x 17.7x 20.9x 29.0x 36.1x

We can see that the histogram-based kernels are consistently much faster than their scikit-image counterparts.

Speedup vs scikit-image CPU: uint8, disk footprint (so only the elementwise backend can be used)

The benchmark was invoked with backend='auto', but disk footprints are not
compatible with the histogram backend, so every row dispatches to elementwise.

filter image dtype backend 3x3 7x7 15x15 25x25 51x51 75x75
autolevel 1920x1080 uint8 elementwise 778.1x 803.6x 308.7x 154.0x 55.3x 26.5x
autolevel_percentile 1920x1080 uint8 elementwise 774.0x 221.7x 28.6x 10.1x 2.1x 0.9x
enhance_contrast 1920x1080 uint8 elementwise 911.9x 683.9x 269.0x 139.1x 52.7x 25.7x
enhance_contrast_percentile 1920x1080 uint8 elementwise 1056.6x 214.2x 28.5x 10.2x 2.2x 0.9x
entropy 1920x1080 uint8 elementwise 332.3x 141.3x 64.7x 26.5x 5.2x 2.2x
equalize 1920x1080 uint8 elementwise 466.1x 567.7x 238.3x 139.9x 57.8x 30.8x
geometric_mean 1920x1080 uint8 elementwise 176.4x 69.2x 21.8x 10.5x 3.7x 2.0x
gradient 1920x1080 uint8 elementwise 933.4x 697.1x 274.2x 142.1x 52.8x 25.5x
gradient_percentile 1920x1080 uint8 elementwise 1027.5x 204.3x 26.9x 9.7x 2.1x 0.8x
majority 1920x1080 uint8 elementwise 1351.5x 276.1x 36.8x 12.6x 2.5x 1.1x
maximum 1920x1080 uint8 elementwise 428.5x 427.2x 201.7x 118.7x 50.2x 25.7x
mean 1920x1080 uint8 elementwise 605.4x 320.7x 170.6x 83.6x 29.6x 13.5x
mean_bilateral 1920x1080 uint8 elementwise 485.0x 154.3x 68.4x 30.3x 10.0x 5.3x
mean_percentile 1920x1080 uint8 elementwise 1316.9x 298.4x 39.3x 13.4x 2.7x 1.0x
median 1920x1080 uint8 elementwise 695.6x 171.7x 25.7x 9.4x 2.0x 0.8x
minimum 1920x1080 uint8 elementwise 574.4x 513.3x 230.3x 131.0x 52.8x 26.5x
modal 1920x1080 uint8 elementwise 1030.2x 217.4x 30.0x 10.8x 2.3x 1.1x
noise_filter 1920x1080 uint8 elementwise 392.8x 355.9x 158.0x 98.8x 45.4x 19.6x
percentile 1920x1080 uint8 elementwise 689.3x 151.9x 22.4x 8.2x 1.9x 0.8x
pop 1920x1080 uint8 elementwise 263.2x 392.8x 472.4x 450.7x 260.2x 137.6x
pop_bilateral 1920x1080 uint8 elementwise 794.5x 255.6x 101.4x 44.1x 13.7x 6.1x
pop_percentile 1920x1080 uint8 elementwise 2010.9x 411.1x 48.5x 15.0x 2.6x 0.9x
subtract_mean 1920x1080 uint8 elementwise 630.4x 322.9x 169.8x 83.8x 29.3x 13.5x
subtract_mean_percentile 1920x1080 uint8 elementwise 1428.5x 309.2x 38.4x 12.9x 2.6x 1.0x
sum 1920x1080 uint8 elementwise 776.5x 353.8x 181.1x 84.2x 29.9x 13.5x
sum_bilateral 1920x1080 uint8 elementwise 520.8x 154.6x 64.6x 28.7x 9.6x 5.2x
sum_percentile 1920x1080 uint8 elementwise 1437.4x 304.8x 37.2x 12.8x 2.5x 1.0x
threshold 1920x1080 uint8 elementwise 685.3x 342.3x 175.5x 83.5x 29.1x 13.3x
threshold_percentile 1920x1080 uint8 elementwise 946.8x 203.8x 27.9x 10.1x 2.1x 0.8x

We can see that at large enough footprint size, the "elementwise" backend eventually becomes slower than scikit-image's histogram-based approach.

grlee77 added 30 commits July 7, 2026 14:04
… eventual cucim.skimage.filter.rank.mean_percentile, etc
  Replace the fixed 16-partition default for the uint8 2D rank histogram
  backend with a scratch-budgeted partition selector. Add environment overrides
  for forcing the partition count, scratch memory budget, and maximum partition
  count so benchmark runs can tune the backend without code changes.
grlee77 added 16 commits July 7, 2026 14:04
- Exclude the shifted anchor pixel from noise_filter footprints so standard disks, balls, and all-ones neighborhoods can detect isolated noise.
- Preserve floating-point and wide-integer precision when computing noise distances instead of truncating through int.
- Add native uint16 histogram outputs and reject or fall back for unsupported output dtypes, preventing count and sum truncation through uint8 temporaries.
- Pass the output dtype scale into histogram kernels so threshold, equalize, autolevel, and subtract-mean match elementwise results for float and uint16 outputs.
- Reject all overlapping input/output aliases, including views and transposes, before rank dispatch.
- Add regression coverage for shifted anchors, native-distance precision, wide histogram results, output scaling, automatic fallback, and output aliases.

Validation:
- 281 passed, 3 skipped in filters/rank/tests/test_rank.py
- ruff checks passed
- git diff --check passed
- Document N-D footprints and output shapes for median filtering.\n- Clarify that cuCIM rank filters support N-D images with reflected boundaries.\n- Correct the scikit-image comparison to its supported 2-D and 3-D inputs.
- Distinguish the per-pixel elementwise implementation from the cooperative sliding-histogram backend.

- Document the histogram backend's uint8, 2-D, output-dtype, and fully populated odd rectangular footprint requirements.

- Correct dimensionality, dtype conversion, output dtype, overflow, percentile, and boundary-handling descriptions.

- Align generated public and internal docstrings with current signatures, dispatch behavior, and supported operations.

- Identify the histogram footprint-area cutoffs as RTX A6000 performance-tuning values.
- Apply the repository clang-format style to histogram_rank.cu.

- Keep NVRTC-sensitive preprocessor conditions on single lines with narrow formatting guards.
- Clarify that the percentile range module provides internal machinery shared by generic, percentile, and bilateral rank filters.

- Summarize elementwise kernel generation and sliding-histogram backend dispatch.
- Rename _percentile_range_filter.py to _rank_filter.py to reflect its shared role across rank-filter APIs.

- Update the internal import and scikit-image license-hook path mappings for the new filename.
@grlee77 grlee77 requested review from a team as code owners July 8, 2026 20:27
@grlee77 grlee77 requested a review from msarahan July 8, 2026 20:27
@grlee77 grlee77 added improvement Improves an existing functionality non-breaking Introduces a non-breaking change labels Jul 8, 2026
@grlee77 grlee77 added this to cucim Jul 8, 2026
@grlee77 grlee77 added this to the v26.10.00 milestone Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improves an existing functionality non-breaking Introduces a non-breaking change

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[FEA] cucim skimage local entropy

2 participants