Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
34cfecb
start adding percentile range support to CUDA rank filters to support…
grlee77 Dec 17, 2025
e1582a1
start adding cucim.skimage.filters percentile functions
grlee77 Dec 21, 2025
906f3f5
add test_rank.py stub
grlee77 Dec 21, 2025
f8e55c4
reduce degree of duplicate code in the kernel generation logic
grlee77 Dec 21, 2025
c0535d1
bump copyright
grlee77 Apr 9, 2026
9f26b37
fix bugs in pop_percentile, threshold_percentile, percentile
grlee77 Apr 10, 2026
21035fb
fix subtract_mean_percentile
grlee77 Apr 10, 2026
c001927
Refactor docstrings to avoid duplicate definitions
grlee77 Apr 10, 2026
7d058e1
start adding generic equivalents
grlee77 Apr 10, 2026
64d10f7
docstring updates
grlee77 Apr 10, 2026
d04aa4f
fix bug in string formatting for masked percentile filters
grlee77 Apr 10, 2026
3f8185b
add maximum, minimum, median
grlee77 Apr 10, 2026
b5c3da0
add threshold
grlee77 Apr 10, 2026
0aa2454
add equalize, noise_filer, modal, majority, geometric_mean
grlee77 Apr 10, 2026
34c8e66
add entropy
grlee77 Apr 10, 2026
585af15
add mean_bilateral, sum_bilateral, pop_bilateral
grlee77 Apr 10, 2026
83724f1
docs
grlee77 Apr 10, 2026
36a7681
test update
grlee77 Apr 27, 2026
f93c0e2
move skimage-specific code out of _vendored submodule and into skimag…
grlee77 Apr 27, 2026
3ec0a6b
avoid overhead of a full sort for rank filters where it is not required
grlee77 May 3, 2026
09cf1ab
add benchmarks for cucim.skimage.filters.rank module
grlee77 May 3, 2026
c5e9987
remove dead code from percentile kernel generator
grlee77 May 3, 2026
fcdb490
add histogram-based implementation
grlee77 May 4, 2026
5dba53d
update with automatic algorithm selection tuning
grlee77 May 5, 2026
39066db
Tune rank histogram row partition selection
grlee77 May 5, 2026
4c7ac84
use operation-specific histogram kernel specialization
grlee77 May 5, 2026
b908dd4
update filter names involved in full range check
grlee77 May 5, 2026
260f2a9
introduce prefix-sum optimization for some optimizations
grlee77 May 5, 2026
1caef03
add histogram implementations for equalize, pop_bilateral, sum_bilate…
grlee77 May 5, 2026
2df94b2
use int16 instead of int32 scratch memory when possible for histogram…
grlee77 May 5, 2026
da273a9
update auto-tuning thresholds for rank-based filters
grlee77 May 6, 2026
19462a0
enable more test cases
grlee77 May 6, 2026
a59cbc1
add cast_to_uint8 option for skimage compatibility
grlee77 May 6, 2026
514fb3c
finish test and docstring updates
grlee77 May 7, 2026
bf17f6a
add histogram-based modal/majority filter
grlee77 May 13, 2026
663b01c
add histogram-based implementation of geometric_mean
grlee77 May 13, 2026
3a92ed4
update module docstring
grlee77 May 13, 2026
2f32163
remove output dtype restriction on when histogram-based kernels can b…
grlee77 May 13, 2026
be9cda2
change default for all rank filters to use cast_to_uint8=True for sci…
grlee77 May 14, 2026
09c62c6
update cucim.skimage.filters.median to support behavior='rank'
grlee77 May 22, 2026
3e6c347
fix rank filter output correctness
grlee77 Jul 7, 2026
40715a2
Correct median rank behavior documentation
grlee77 Jul 8, 2026
d6c860d
Clarify rank filter backend documentation
grlee77 Jul 8, 2026
d0d0022
Format histogram rank CUDA kernel
grlee77 Jul 8, 2026
b119d8d
Document shared rank filter implementation
grlee77 Jul 8, 2026
f353ef4
Rename shared rank filter implementation module
grlee77 Jul 8, 2026
eb2b908
Merge branch 'main' into grelee/skimage-filters-rank
jakirkham Aug 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ repos:
^python/cucim/src/cucim/skimage/filters/_window[.]py$|
^python/cucim/src/cucim/skimage/filters/cuda/histogram_median[.]cu$|
^python/cucim/src/cucim/skimage/filters/lpi_filter[.]py$|
^python/cucim/src/cucim/skimage/filters/rank/__init__[.]py$|
^python/cucim/src/cucim/skimage/filters/rank/_generic[.]py$|
^python/cucim/src/cucim/skimage/filters/rank/_percentile[.]py$|
^python/cucim/src/cucim/skimage/filters/rank/_rank_filter[.]py$|
^python/cucim/src/cucim/skimage/filters/rank/tests/test_rank[.]py$|
^python/cucim/src/cucim/skimage/filters/ridges[.]py$|
^python/cucim/src/cucim/skimage/filters/tests/test_correlate[.]py$|
^python/cucim/src/cucim/skimage/filters/tests/test_edges[.]py$|
Expand Down Expand Up @@ -434,6 +439,11 @@ repos:
python/cucim/src/cucim/skimage/filters/_window[.]py$|
python/cucim/src/cucim/skimage/filters/lpi_filter[.]py$|
python/cucim/src/cucim/skimage/filters/ridges[.]py$|
python/cucim/src/cucim/skimage/filters/rank/__init__[.]py$|
python/cucim/src/cucim/skimage/filters/rank/_generic[.]py$|
python/cucim/src/cucim/skimage/filters/rank/_percentile[.]py$|
python/cucim/src/cucim/skimage/filters/rank/_rank_filter[.]py$|
python/cucim/src/cucim/skimage/filters/rank/tests/test_rank[.]py$|
python/cucim/src/cucim/skimage/filters/tests/test_correlate[.]py$|
python/cucim/src/cucim/skimage/filters/tests/test_edges[.]py$|
python/cucim/src/cucim/skimage/filters/tests/test_fft_based[.]py$|
Expand Down
10 changes: 8 additions & 2 deletions benchmarks/skimage/_image_bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,17 @@ def __init__(
module_gpu=cupyx.scipy.ndimage,
function_is_generator=False,
run_cpu=True,
fixed_kwargs_cpu=None,
fixed_kwargs_gpu=None,
):
self.shape = shape
self.function_name = function_name
self.fixed_kwargs_cpu = self._update_kwargs_arrays(fixed_kwargs, "cpu")
self.fixed_kwargs_gpu = self._update_kwargs_arrays(fixed_kwargs, "gpu")
if fixed_kwargs_cpu is None:
fixed_kwargs_cpu = fixed_kwargs
if fixed_kwargs_gpu is None:
fixed_kwargs_gpu = fixed_kwargs
self.fixed_kwargs_cpu = self._update_kwargs_arrays(fixed_kwargs_cpu, "cpu")
self.fixed_kwargs_gpu = self._update_kwargs_arrays(fixed_kwargs_gpu, "gpu")
self.var_kwargs = var_kwargs
self.index_str = index_str
# self.set_args_kwargs = set_args_kwargs
Expand Down
213 changes: 213 additions & 0 deletions benchmarks/skimage/cucim_filters_rank_bench.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import argparse
import os

import numpy as np
import pandas as pd
import skimage.filters.rank
from _image_bench import ImageBench
from skimage.morphology import disk

import cucim.skimage.filters.rank

RANK_FILTERS = [
# generic.py
("autolevel", dict(), dict()),
("enhance_contrast", dict(), dict()),
("entropy", dict(), dict()),
("equalize", dict(), dict()),
("geometric_mean", dict(), dict()),
("gradient", dict(), dict()),
("majority", dict(), dict()),
("maximum", dict(), dict()),
("mean", dict(), dict()),
("median", dict(), dict()),
("minimum", dict(), dict()),
("modal", dict(), dict()),
("noise_filter", dict(), dict()),
("pop", dict(), dict()),
("subtract_mean", dict(), dict()),
("sum", dict(), dict()),
("threshold", dict(), dict()),
# percentile.py
("autolevel_percentile", dict(p0=0.1, p1=0.9), dict()),
("enhance_contrast_percentile", dict(p0=0.1, p1=0.9), dict()),
("gradient_percentile", dict(p0=0.1, p1=0.9), dict()),
("mean_percentile", dict(p0=0.1, p1=0.9), dict()),
("percentile", dict(p0=0.5), dict()),
("pop_percentile", dict(p0=0.1, p1=0.9), dict()),
("subtract_mean_percentile", dict(p0=0.1, p1=0.9), dict()),
("sum_percentile", dict(p0=0.1, p1=0.9), dict()),
("threshold_percentile", dict(p0=0.5), dict()),
# bilateral.py
("mean_bilateral", dict(s0=10, s1=10), dict()),
("pop_bilateral", dict(s0=10, s1=10), dict()),
("sum_bilateral", dict(s0=10, s1=10), dict()),
]


def _parse_shape(img_size):
return tuple(list(map(int, img_size.split(","))))


def _parse_radii(radii):
return [int(r) for r in radii.split(",")]


def _parse_footprint_sizes(footprint_sizes):
return [int(size) for size in footprint_sizes.split(",")]


def _make_footprints(args):
if args.footprint_shape == "disk":
return [disk(radius).astype(bool) for radius in _parse_radii(args.radii)]

footprint_sizes = _parse_footprint_sizes(args.footprint_sizes)
if any(size <= 1 or size % 2 == 0 for size in footprint_sizes):
raise ValueError("rank filter benchmark footprint sizes must be odd and > 1")

return [np.ones((size, size), dtype=bool) for size in footprint_sizes]


def main(args):
cfile = "cucim_filters_rank_results.csv"
if getattr(args, "no_resume", False) or not os.path.exists(cfile):
all_results = pd.DataFrame()
else:
all_results = pd.read_csv(cfile, index_col=0)
dtypes = [np.dtype(args.dtype)]

shape = _parse_shape(args.img_size)
if len(shape) != 2:
raise ValueError("rank filter benchmarks use 2D images")

footprints = _make_footprints(args)

for function_name, fixed_kwargs, var_kwargs in RANK_FILTERS:
if function_name != args.func_name:
continue

var_kwargs = dict(var_kwargs)
var_kwargs["footprint"] = footprints
fixed_kwargs_gpu = dict(fixed_kwargs)
fixed_kwargs_gpu["backend"] = args.backend

B = ImageBench(
function_name=function_name,
shape=shape,
dtypes=dtypes,
fixed_kwargs=fixed_kwargs,
fixed_kwargs_gpu=fixed_kwargs_gpu,
var_kwargs=var_kwargs,
module_cpu=skimage.filters.rank,
module_gpu=cucim.skimage.filters.rank,
run_cpu=not args.no_cpu,
)
results = B.run_benchmark(duration=args.duration)
all_results = pd.concat([all_results, results["full"]])

fbase = os.path.splitext(cfile)[0]
all_results.to_csv(cfile, index=True)
with open(fbase + ".md", "w") as f:
f.write(all_results.to_markdown())


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Benchmarking cuCIM rank filters")
func_name_choices = [filter_spec[0] for filter_spec in RANK_FILTERS]
dtype_choices = [
"float16",
"float32",
"float64",
"int8",
"int16",
"int32",
"int64",
"uint8",
"uint16",
"uint32",
"uint64",
]
parser.add_argument(
"-i", "--img_size", type=str, help="Size of input image", required=True
)
parser.add_argument(
"-d",
"--dtype",
type=str,
help="Dtype of input image",
choices=dtype_choices,
required=True,
)
parser.add_argument(
"-f",
"--func_name",
type=str,
help="function to benchmark",
choices=func_name_choices,
required=True,
)
parser.add_argument(
"-t",
"--duration",
type=int,
help="time to run benchmark",
required=True,
)
parser.add_argument(
"--footprint_sizes",
type=str,
help=(
"Comma-separated odd square footprint side lengths to benchmark. "
"The all-ones rectangular footprints exercise the histogram fast "
"path for supported uint8 2D rank filters."
),
default="3,7,15,31",
)
parser.add_argument(
"--radii",
type=str,
help=(
"Comma-separated disk footprint radii to benchmark when "
"--footprint_shape=disk."
),
default="1,3,7,15",
)
parser.add_argument(
"--footprint_shape",
type=str,
choices=["rectangle", "disk"],
help=(
"Footprint family to benchmark. rectangle uses all-ones square "
"footprints and is the default."
),
default="rectangle",
)
parser.add_argument(
"--backend",
type=str,
choices=["auto", "histogram", "elementwise"],
help=(
"cuCIM rank backend to benchmark. auto uses automatic dispatch, "
"histogram requires the uint8 2D rectangular histogram backend, "
"and elementwise forces the generic per-output-pixel backend."
),
default="auto",
)
parser.add_argument(
"--no_cpu",
action="store_true",
help="disable cpu measurements",
default=False,
)
parser.add_argument(
"--no_resume",
action="store_true",
help="do not load existing results CSV; save only this run's results (overwrite)",
default=False,
)

args = parser.parse_args()
main(args)
23 changes: 23 additions & 0 deletions benchmarks/skimage/run-nv-bench-filters-rank.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Use env var if set/non-empty, otherwise default to 3
MAX_DURATION="${CUCIM_BENCHMARK_MAX_DURATION:-3}"

# Use env var if set/non-empty, otherwise default to "3,7,15,31"
FOOTPRINT_SIZES="${CUCIM_BENCHMARK_RANK_FOOTPRINT_SIZES:-3,7,15,31}"

# Use env var if set/non-empty, otherwise default to automatic dispatch
BACKEND="${CUCIM_BENCHMARK_RANK_BACKEND:-auto}"

param_shape=("512,512" "1920,1080")
param_filt=(autolevel enhance_contrast entropy equalize geometric_mean gradient majority maximum mean median minimum modal noise_filter pop subtract_mean sum threshold autolevel_percentile enhance_contrast_percentile gradient_percentile mean_percentile percentile pop_percentile subtract_mean_percentile sum_percentile threshold_percentile mean_bilateral pop_bilateral sum_bilateral)
param_dt=(uint8)
for shape in "${param_shape[@]}"; do
for filt in "${param_filt[@]}"; do
for dt in "${param_dt[@]}"; do
python cucim_filters_rank_bench.py -f "$filt" -i "$shape" -d "$dt" -t "$MAX_DURATION" --footprint_sizes "$FOOTPRINT_SIZES" --backend "$BACKEND"
done
done
done
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ def _call_kernel(
weights,
output,
structure=None,
mask=None,
weights_dtype=numpy.float64,
structure_dtype=numpy.float64,
):
Expand Down Expand Up @@ -200,6 +201,9 @@ def _call_kernel(
if structure is not None:
structure = cupy.ascontiguousarray(structure, structure_dtype)
args.append(structure)
if mask is not None:
mask = cupy.ascontiguousarray(mask, bool)
args.append(mask)
output = _util._get_output(output, input, None, complex_output) # noqa
needs_temp = cupy.shares_memory(output, input, "MAY_SHARE_BOUNDS")
if needs_temp:
Expand Down
4 changes: 3 additions & 1 deletion python/cucim/src/cucim/skimage/filters/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: 2009-2022 the scikit-image team
# SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION. All rights reserved.
# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION. All rights reserved.
# SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause

__all__ = [
Expand All @@ -24,6 +24,7 @@ __all__ = [
"prewitt",
"prewitt_h",
"prewitt_v",
"rank",
"rank_order",
"roberts",
"roberts_neg_diag",
Expand Down Expand Up @@ -52,6 +53,7 @@ __all__ = [
"window",
]

from . import rank
from ._fft_based import butterworth
from ._gabor import gabor, gabor_kernel
from ._gaussian import difference_of_gaussians, gaussian
Expand Down
Loading
Loading