use crate::core::{Pix, PixelDepth, pixel};
use crate::filter::{FilterError, FilterResult};
struct RankHistogram {
coarse: [u32; 16],
fine: [u32; 256],
}
impl RankHistogram {
fn new() -> Self {
Self {
coarse: [0; 16],
fine: [0; 256],
}
}
fn clear(&mut self) {
self.coarse.fill(0);
self.fine.fill(0);
}
#[inline]
fn add(&mut self, value: u8) {
let idx = value as usize;
self.fine[idx] += 1;
self.coarse[idx >> 4] += 1;
}
#[inline]
fn remove(&mut self, value: u8) {
let idx = value as usize;
debug_assert!(self.fine[idx] > 0, "Removing value not in histogram");
debug_assert!(self.coarse[idx >> 4] > 0, "Coarse histogram underflow");
self.fine[idx] = self.fine[idx].saturating_sub(1);
self.coarse[idx >> 4] = self.coarse[idx >> 4].saturating_sub(1);
}
fn get_rank_value(&self, rank_position: u32) -> u8 {
let mut sum = 0u32;
let mut coarse_bin = 0usize;
for i in 0..16 {
sum += self.coarse[i];
if sum > rank_position {
sum -= self.coarse[i];
coarse_bin = i;
break;
}
if i == 15 {
coarse_bin = 15;
sum -= self.coarse[15];
}
}
let start = coarse_bin * 16;
for i in 0..16 {
let idx = start + i;
if idx >= 256 {
return 255;
}
sum += self.fine[idx];
if sum > rank_position {
return idx as u8;
}
}
(coarse_bin * 16 + 15).min(255) as u8
}
}
pub fn rank_filter(pix: &Pix, width: u32, height: u32, rank: f32) -> FilterResult<Pix> {
if width < 1 || height < 1 {
return Err(FilterError::InvalidParameters(
"filter dimensions must be >= 1".to_string(),
));
}
if !(0.0..=1.0).contains(&rank) {
return Err(FilterError::InvalidParameters(
"rank must be in [0.0, 1.0]".to_string(),
));
}
if width == 1 && height == 1 {
return Ok(pix.deep_clone());
}
match pix.depth() {
PixelDepth::Bit8 => rank_filter_gray(pix, width, height, rank),
PixelDepth::Bit32 => rank_filter_color(pix, width, height, rank),
_ => Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: pix.depth().bits(),
}),
}
}
pub fn rank_filter_gray(pix: &Pix, width: u32, height: u32, rank: f32) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8-bpp grayscale",
actual: pix.depth().bits(),
});
}
if width < 1 || height < 1 {
return Err(FilterError::InvalidParameters(
"filter dimensions must be >= 1".to_string(),
));
}
if !(0.0..=1.0).contains(&rank) {
return Err(FilterError::InvalidParameters(
"rank must be in [0.0, 1.0]".to_string(),
));
}
if width == 1 && height == 1 {
return Ok(pix.deep_clone());
}
let img_w = pix.width();
let img_h = pix.height();
let wf = width;
let hf = height;
let half_w = (wf / 2) as i32;
let half_h = (hf / 2) as i32;
let filter_size = wf * hf;
let rank_position = ((rank * (filter_size - 1) as f32) + 0.5) as u32;
let out_pix = Pix::new(img_w, img_h, PixelDepth::Bit8)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
let mut histogram = RankHistogram::new();
if hf > wf {
for x in 0..img_w {
histogram.clear();
for y in 0..img_h {
if y == 0 {
for ky in 0..hf {
for kx in 0..wf {
let sx =
(x as i32 + kx as i32 - half_w).clamp(0, img_w as i32 - 1) as u32;
let sy = (ky as i32 - half_h).clamp(0, img_h as i32 - 1) as u32;
let val = pix.get_pixel_unchecked(sx, sy) as u8;
histogram.add(val);
}
}
} else {
let old_y = (y as i32 - 1 - half_h).clamp(0, img_h as i32 - 1) as u32;
let new_y =
(y as i32 + hf as i32 - 1 - half_h).clamp(0, img_h as i32 - 1) as u32;
for kx in 0..wf {
let sx = (x as i32 + kx as i32 - half_w).clamp(0, img_w as i32 - 1) as u32;
let old_val = pix.get_pixel_unchecked(sx, old_y) as u8;
histogram.remove(old_val);
let new_val = pix.get_pixel_unchecked(sx, new_y) as u8;
histogram.add(new_val);
}
}
let result = histogram.get_rank_value(rank_position);
out_mut.set_pixel_unchecked(x, y, result as u32);
}
}
} else {
for y in 0..img_h {
histogram.clear();
for x in 0..img_w {
if x == 0 {
for ky in 0..hf {
for kx in 0..wf {
let sx = (kx as i32 - half_w).clamp(0, img_w as i32 - 1) as u32;
let sy =
(y as i32 + ky as i32 - half_h).clamp(0, img_h as i32 - 1) as u32;
let val = pix.get_pixel_unchecked(sx, sy) as u8;
histogram.add(val);
}
}
} else {
let old_x = (x as i32 - 1 - half_w).clamp(0, img_w as i32 - 1) as u32;
let new_x =
(x as i32 + wf as i32 - 1 - half_w).clamp(0, img_w as i32 - 1) as u32;
for ky in 0..hf {
let sy = (y as i32 + ky as i32 - half_h).clamp(0, img_h as i32 - 1) as u32;
let old_val = pix.get_pixel_unchecked(old_x, sy) as u8;
histogram.remove(old_val);
let new_val = pix.get_pixel_unchecked(new_x, sy) as u8;
histogram.add(new_val);
}
}
let result = histogram.get_rank_value(rank_position);
out_mut.set_pixel_unchecked(x, y, result as u32);
}
}
}
Ok(out_mut.into())
}
pub fn rank_filter_color(pix: &Pix, width: u32, height: u32, rank: f32) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32-bpp color",
actual: pix.depth().bits(),
});
}
if width < 1 || height < 1 {
return Err(FilterError::InvalidParameters(
"filter dimensions must be >= 1".to_string(),
));
}
if !(0.0..=1.0).contains(&rank) {
return Err(FilterError::InvalidParameters(
"rank must be in [0.0, 1.0]".to_string(),
));
}
if width == 1 && height == 1 {
return Ok(pix.deep_clone());
}
let img_w = pix.width();
let img_h = pix.height();
let wf = width;
let hf = height;
let half_w = (wf / 2) as i32;
let half_h = (hf / 2) as i32;
let filter_size = wf * hf;
let rank_position = ((rank * (filter_size - 1) as f32) + 0.5) as u32;
let out_pix = Pix::new(img_w, img_h, PixelDepth::Bit32)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
out_mut.set_spp(pix.spp());
let mut hist_r = RankHistogram::new();
let mut hist_g = RankHistogram::new();
let mut hist_b = RankHistogram::new();
let mut hist_a = RankHistogram::new();
if hf > wf {
for x in 0..img_w {
hist_r.clear();
hist_g.clear();
hist_b.clear();
hist_a.clear();
for y in 0..img_h {
if y == 0 {
for ky in 0..hf {
for kx in 0..wf {
let sx =
(x as i32 + kx as i32 - half_w).clamp(0, img_w as i32 - 1) as u32;
let sy = (ky as i32 - half_h).clamp(0, img_h as i32 - 1) as u32;
let pixel = pix.get_pixel_unchecked(sx, sy);
let (r, g, b, a) = pixel::extract_rgba(pixel);
hist_r.add(r);
hist_g.add(g);
hist_b.add(b);
hist_a.add(a);
}
}
} else {
let old_y = (y as i32 - 1 - half_h).clamp(0, img_h as i32 - 1) as u32;
let new_y =
(y as i32 + hf as i32 - 1 - half_h).clamp(0, img_h as i32 - 1) as u32;
for kx in 0..wf {
let sx = (x as i32 + kx as i32 - half_w).clamp(0, img_w as i32 - 1) as u32;
let old_pixel = pix.get_pixel_unchecked(sx, old_y);
let (or, og, ob, oa) = pixel::extract_rgba(old_pixel);
hist_r.remove(or);
hist_g.remove(og);
hist_b.remove(ob);
hist_a.remove(oa);
let new_pixel = pix.get_pixel_unchecked(sx, new_y);
let (nr, ng, nb, na) = pixel::extract_rgba(new_pixel);
hist_r.add(nr);
hist_g.add(ng);
hist_b.add(nb);
hist_a.add(na);
}
}
let result_r = hist_r.get_rank_value(rank_position);
let result_g = hist_g.get_rank_value(rank_position);
let result_b = hist_b.get_rank_value(rank_position);
let result_a = hist_a.get_rank_value(rank_position);
let result = pixel::compose_rgba(result_r, result_g, result_b, result_a);
out_mut.set_pixel_unchecked(x, y, result);
}
}
} else {
for y in 0..img_h {
hist_r.clear();
hist_g.clear();
hist_b.clear();
hist_a.clear();
for x in 0..img_w {
if x == 0 {
for ky in 0..hf {
for kx in 0..wf {
let sx = (kx as i32 - half_w).clamp(0, img_w as i32 - 1) as u32;
let sy =
(y as i32 + ky as i32 - half_h).clamp(0, img_h as i32 - 1) as u32;
let pixel = pix.get_pixel_unchecked(sx, sy);
let (r, g, b, a) = pixel::extract_rgba(pixel);
hist_r.add(r);
hist_g.add(g);
hist_b.add(b);
hist_a.add(a);
}
}
} else {
let old_x = (x as i32 - 1 - half_w).clamp(0, img_w as i32 - 1) as u32;
let new_x =
(x as i32 + wf as i32 - 1 - half_w).clamp(0, img_w as i32 - 1) as u32;
for ky in 0..hf {
let sy = (y as i32 + ky as i32 - half_h).clamp(0, img_h as i32 - 1) as u32;
let old_pixel = pix.get_pixel_unchecked(old_x, sy);
let (or, og, ob, oa) = pixel::extract_rgba(old_pixel);
hist_r.remove(or);
hist_g.remove(og);
hist_b.remove(ob);
hist_a.remove(oa);
let new_pixel = pix.get_pixel_unchecked(new_x, sy);
let (nr, ng, nb, na) = pixel::extract_rgba(new_pixel);
hist_r.add(nr);
hist_g.add(ng);
hist_b.add(nb);
hist_a.add(na);
}
}
let result_r = hist_r.get_rank_value(rank_position);
let result_g = hist_g.get_rank_value(rank_position);
let result_b = hist_b.get_rank_value(rank_position);
let result_a = hist_a.get_rank_value(rank_position);
let result = pixel::compose_rgba(result_r, result_g, result_b, result_a);
out_mut.set_pixel_unchecked(x, y, result);
}
}
}
Ok(out_mut.into())
}
pub fn median_filter(pix: &Pix, width: u32, height: u32) -> FilterResult<Pix> {
rank_filter(pix, width, height, 0.5)
}
pub fn min_filter(pix: &Pix, width: u32, height: u32) -> FilterResult<Pix> {
rank_filter(pix, width, height, 0.0)
}
pub fn max_filter(pix: &Pix, width: u32, height: u32) -> FilterResult<Pix> {
rank_filter(pix, width, height, 1.0)
}
#[derive(Debug, Clone, Copy)]
pub enum MinMaxOp {
Min,
Max,
MaxDiff,
}
pub fn scale_gray_min_max(pix: &Pix, xfact: u32, yfact: u32, op: MinMaxOp) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8-bpp grayscale",
actual: pix.depth().bits(),
});
}
if pix.colormap().is_some() {
return Err(FilterError::InvalidParameters(
"colormapped 8-bpp images are not supported; remove the colormap first".to_string(),
));
}
if xfact < 1 || yfact < 1 {
return Err(FilterError::InvalidParameters(
"xfact and yfact must be >= 1".to_string(),
));
}
let ws = pix.width();
let hs = pix.height();
let (wd, xfact) = {
let d = ws / xfact;
if d == 0 { (1, ws) } else { (d, xfact) }
};
let (hd, yfact) = {
let d = hs / yfact;
if d == 0 { (1, hs) } else { (d, yfact) }
};
let out_pix = Pix::new(wd, hd, PixelDepth::Bit8)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
for i in 0..hd {
for j in 0..wd {
let mut minval = 255u8;
let mut maxval = 0u8;
for k in 0..yfact {
let sy = yfact * i + k;
for m in 0..xfact {
let sx = xfact * j + m;
let val = pix.get_pixel_unchecked(sx, sy) as u8;
if val < minval {
minval = val;
}
if val > maxval {
maxval = val;
}
}
}
let out_val = match op {
MinMaxOp::Min => minval,
MinMaxOp::Max => maxval,
MinMaxOp::MaxDiff => maxval - minval,
};
out_mut.set_pixel_unchecked(j, i, out_val as u32);
}
}
Ok(out_mut.into())
}
pub fn scale_gray_rank2(pix: &Pix, rank: u8) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8-bpp grayscale",
actual: pix.depth().bits(),
});
}
if pix.colormap().is_some() {
return Err(FilterError::InvalidParameters(
"colormapped 8-bpp images are not supported; remove the colormap first".to_string(),
));
}
if !(1..=4).contains(&rank) {
return Err(FilterError::InvalidParameters(
"rank must be in [1, 4]".to_string(),
));
}
let ws = pix.width();
let hs = pix.height();
if ws < 2 || hs < 2 {
return Err(FilterError::InvalidParameters(
"image too small for 2x downscaling (need at least 2x2)".to_string(),
));
}
match rank {
1 => return scale_gray_min_max(pix, 2, 2, MinMaxOp::Min),
4 => return scale_gray_min_max(pix, 2, 2, MinMaxOp::Max),
_ => {}
}
let wd = ws / 2;
let hd = hs / 2;
let out_pix = Pix::new(wd, hd, PixelDepth::Bit8)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
for i in 0..hd {
for j in 0..wd {
let mut vals = [
pix.get_pixel_unchecked(2 * j, 2 * i) as u8,
pix.get_pixel_unchecked(2 * j + 1, 2 * i) as u8,
pix.get_pixel_unchecked(2 * j, 2 * i + 1) as u8,
pix.get_pixel_unchecked(2 * j + 1, 2 * i + 1) as u8,
];
vals.sort_unstable();
let out_val = vals[(rank - 1) as usize];
out_mut.set_pixel_unchecked(j, i, out_val as u32);
}
}
Ok(out_mut.into())
}
pub fn scale_gray_rank_cascade(
pix: &Pix,
level1: u8,
level2: u8,
level3: u8,
level4: u8,
) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8-bpp grayscale",
actual: pix.depth().bits(),
});
}
if pix.colormap().is_some() {
return Err(FilterError::InvalidParameters(
"colormapped 8-bpp images are not supported; remove the colormap first".to_string(),
));
}
if level1 > 4 || level2 > 4 || level3 > 4 || level4 > 4 {
return Err(FilterError::InvalidParameters(
"all levels must be in [0, 4]".to_string(),
));
}
if level1 == 0 {
return Ok(pix.deep_clone());
}
let pix1 = scale_gray_rank2(pix, level1)?;
if level2 == 0 {
return Ok(pix1);
}
let pix2 = scale_gray_rank2(&pix1, level2)?;
if level3 == 0 {
return Ok(pix2);
}
let pix3 = scale_gray_rank2(&pix2, level3)?;
if level4 == 0 {
return Ok(pix3);
}
scale_gray_rank2(&pix3, level4)
}
pub fn rank_filter_with_scaling(
pix: &Pix,
wf: u32,
hf: u32,
rank: f32,
scalefactor: f32,
) -> FilterResult<Pix> {
if pix.has_colormap() {
return Err(FilterError::InvalidParameters(
"input must not have a colormap".to_string(),
));
}
let d = pix.depth();
if d != PixelDepth::Bit8 && d != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: d.bits(),
});
}
if wf < 1 || hf < 1 {
return Err(FilterError::InvalidParameters(
"filter dimensions must be >= 1".to_string(),
));
}
if !(0.0..=1.0).contains(&rank) {
return Err(FilterError::InvalidParameters(
"rank must be in [0.0, 1.0]".to_string(),
));
}
if wf == 1 && hf == 1 {
return Ok(pix.deep_clone());
}
if !(0.2..=0.7).contains(&scalefactor) {
return rank_filter(pix, wf, hf, rank);
}
use crate::transform::{ScaleMethod, scale, scale_to_size};
let w = pix.width();
let h = pix.height();
let sw = (w as f32 * scalefactor + 0.5) as u32;
let sh = (h as f32 * scalefactor + 0.5) as u32;
if sw == 0 || sh == 0 {
return rank_filter(pix, wf, hf, rank);
}
let pix1 = scale(pix, scalefactor, scalefactor, ScaleMethod::AreaMap)?;
let wfs = 1u32.max((scalefactor * wf as f32 + 0.5) as u32);
let hfs = 1u32.max((scalefactor * hf as f32 + 0.5) as u32);
let pix2 = rank_filter(&pix1, wfs, hfs, rank)?;
let pixd = scale_to_size(&pix2, w, h)?;
Ok(pixd)
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_gray_image() -> Pix {
let pix = Pix::new(10, 10, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
let val = x * 25 + y * 5;
pix_mut.set_pixel_unchecked(x, y, val.min(255));
}
}
pix_mut.into()
}
fn create_test_color_image() -> Pix {
let pix = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
let r = (x * 25) as u8;
let g = (y * 25) as u8;
let b = 128;
let pixel = pixel::compose_rgb(r, g, b);
pix_mut.set_pixel_unchecked(x, y, pixel);
}
}
pix_mut.into()
}
fn create_noisy_gray_image() -> Pix {
let pix = Pix::new(10, 10, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
pix_mut.set_pixel_unchecked(x, y, 128);
}
}
pix_mut.set_pixel_unchecked(2, 3, 255);
pix_mut.set_pixel_unchecked(7, 5, 255);
pix_mut.set_pixel_unchecked(4, 6, 0);
pix_mut.set_pixel_unchecked(8, 2, 0);
pix_mut.into()
}
#[test]
fn test_rank_filter_invalid_params() {
let pix = create_test_gray_image();
assert!(rank_filter(&pix, 0, 3, 0.5).is_err());
assert!(rank_filter(&pix, 3, 0, 0.5).is_err());
assert!(rank_filter(&pix, 3, 3, -0.1).is_err());
assert!(rank_filter(&pix, 3, 3, 1.1).is_err());
}
#[test]
fn test_rank_filter_noop() {
let pix = create_test_gray_image();
let result = rank_filter(&pix, 1, 1, 0.5).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), pix.depth());
for y in 0..pix.height() {
for x in 0..pix.width() {
let orig = pix.get_pixel_unchecked(x, y);
let res = result.get_pixel_unchecked(x, y);
assert_eq!(orig, res);
}
}
}
#[test]
fn test_rank_filter_gray_basic() {
let pix = create_test_gray_image();
let result = rank_filter(&pix, 3, 3, 0.5).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit8);
}
#[test]
fn test_rank_filter_color_basic() {
let pix = create_test_color_image();
let result = rank_filter(&pix, 3, 3, 0.5).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit32);
}
#[test]
fn test_median_filter_noise_removal() {
let pix = create_noisy_gray_image();
let result = median_filter(&pix, 3, 3).unwrap();
let orig = pix.get_pixel_unchecked(2, 3);
let filtered = result.get_pixel_unchecked(2, 3);
assert_eq!(orig, 255); assert!(filtered < 200); }
#[test]
fn test_min_filter_erosion_like() {
let pix = create_test_gray_image();
let result = min_filter(&pix, 3, 3).unwrap();
let center_orig = pix.get_pixel_unchecked(5, 5);
let center_filtered = result.get_pixel_unchecked(5, 5);
assert!(center_filtered <= center_orig);
}
#[test]
fn test_max_filter_dilation_like() {
let pix = create_test_gray_image();
let result = max_filter(&pix, 3, 3).unwrap();
let center_orig = pix.get_pixel_unchecked(5, 5);
let center_filtered = result.get_pixel_unchecked(5, 5);
assert!(center_filtered >= center_orig);
}
#[test]
fn test_rank_filter_row_major_traversal() {
let pix = create_test_gray_image();
let result = rank_filter(&pix, 3, 5, 0.5).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
}
#[test]
fn test_rank_filter_column_major_traversal() {
let pix = create_test_gray_image();
let result = rank_filter(&pix, 5, 3, 0.5).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
}
#[test]
fn test_rank_histogram() {
let mut hist = RankHistogram::new();
hist.add(10);
hist.add(20);
hist.add(30);
hist.add(40);
hist.add(50);
assert_eq!(hist.get_rank_value(0), 10);
assert_eq!(hist.get_rank_value(2), 30);
assert_eq!(hist.get_rank_value(4), 50);
}
#[test]
fn test_rank_histogram_remove() {
let mut hist = RankHistogram::new();
hist.add(10);
hist.add(20);
hist.add(30);
hist.remove(20);
assert_eq!(hist.get_rank_value(0), 10);
assert_eq!(hist.get_rank_value(1), 30);
}
#[test]
fn test_unsupported_depth() {
let pix = Pix::new(10, 10, PixelDepth::Bit1).unwrap();
assert!(rank_filter(&pix, 3, 3, 0.5).is_err());
}
}