use crate::core::pix::RgbComponent;
use crate::core::{Pix, PixelDepth};
use crate::filter::{FilterError, FilterResult};
fn check_8bpp(pix: &Pix) -> FilterResult<()> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8-bpp grayscale",
actual: pix.depth().bits(),
});
}
Ok(())
}
pub fn blockconv_accum(pix: &Pix) -> FilterResult<Pix> {
check_8bpp(pix)?;
let w = pix.width();
let h = pix.height();
let out = Pix::new(w, h, PixelDepth::Bit32)?;
let mut out_mut = out.try_into_mut().unwrap();
out_mut.set_pixel_unchecked(0, 0, pix.get_pixel_unchecked(0, 0));
for x in 1..w {
let val = pix.get_pixel_unchecked(x, 0) + out_mut.get_pixel_unchecked(x - 1, 0);
out_mut.set_pixel_unchecked(x, 0, val);
}
for y in 1..h {
let val = pix.get_pixel_unchecked(0, y) + out_mut.get_pixel_unchecked(0, y - 1);
out_mut.set_pixel_unchecked(0, y, val);
}
for y in 1..h {
for x in 1..w {
let val = pix.get_pixel_unchecked(x, y)
+ out_mut.get_pixel_unchecked(x - 1, y)
+ out_mut.get_pixel_unchecked(x, y - 1)
- out_mut.get_pixel_unchecked(x - 1, y - 1);
out_mut.set_pixel_unchecked(x, y, val);
}
}
Ok(out_mut.into())
}
pub fn blockconv_gray(pix: &Pix, pixacc: Option<&Pix>, wc: u32, hc: u32) -> FilterResult<Pix> {
check_8bpp(pix)?;
let w = pix.width();
let h = pix.height();
if wc == 0 || hc == 0 {
return Ok(pix.deep_clone());
}
let wc = wc.min((w - 1) / 2);
let hc = hc.min((h - 1) / 2);
let owned_acc;
let acc = match pixacc {
Some(a) => {
if a.depth() != PixelDepth::Bit32 || a.width() != w || a.height() != h {
return Err(FilterError::InvalidParameters(
"accumulator must be 32 bpp with same dimensions as input".into(),
));
}
a
}
None => {
owned_acc = blockconv_accum(pix)?;
&owned_acc
}
};
let fwc = (2 * wc + 1) as f64;
let fhc = (2 * hc + 1) as f64;
let norm = 1.0 / (fwc * fhc);
let out = Pix::new(w, h, PixelDepth::Bit8)?;
let mut out_mut = out.try_into_mut().unwrap();
for y in 0..h {
let ymin = if y > hc { y - hc - 1 } else { 0 };
let ymax = (y + hc).min(h - 1);
let hn = if y > hc {
(ymax - ymin) as f64
} else {
(ymax + 1) as f64
};
for x in 0..w {
let xmin = if x > wc { x - wc - 1 } else { 0 };
let xmax = (x + wc).min(w - 1);
let wn = if x > wc {
(xmax - xmin) as f64
} else {
(xmax + 1) as f64
};
let mut val = acc.get_pixel_unchecked(xmax, ymax) as i64;
if y > hc {
val -= acc.get_pixel_unchecked(xmax, ymin) as i64;
}
if x > wc {
val -= acc.get_pixel_unchecked(xmin, ymax) as i64;
}
if y > hc && x > wc {
val += acc.get_pixel_unchecked(xmin, ymin) as i64;
}
let result = (norm * val as f64 * fwc / wn * fhc / hn + 0.5) as u32;
let result = result.min(255);
out_mut.set_pixel_unchecked(x, y, result);
}
}
Ok(out_mut.into())
}
pub fn blockconv(pix: &Pix, wc: u32, hc: u32) -> FilterResult<Pix> {
match pix.depth() {
PixelDepth::Bit8 => blockconv_gray(pix, None, wc, hc),
PixelDepth::Bit32 => {
let pix_r = pix.get_rgb_component(RgbComponent::Red)?;
let pix_g = pix.get_rgb_component(RgbComponent::Green)?;
let pix_b = pix.get_rgb_component(RgbComponent::Blue)?;
let conv_r = blockconv_gray(&pix_r, None, wc, hc)?;
let conv_g = blockconv_gray(&pix_g, None, wc, hc)?;
let conv_b = blockconv_gray(&pix_b, None, wc, hc)?;
Ok(Pix::create_rgb_image(&conv_r, &conv_g, &conv_b)?)
}
_ => Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: pix.depth().bits(),
}),
}
}
pub fn blockconv_gray_unnormalized(pix: &Pix, wc: u32, hc: u32) -> FilterResult<Pix> {
check_8bpp(pix)?;
if wc == 0 || hc == 0 {
return Ok(pix.deep_clone());
}
let w = pix.width();
let h = pix.height();
let wc = wc.min((w - 1) / 2);
let hc = hc.min((h - 1) / 2);
let bordered = pix.add_mirrored_border(wc + 1, wc, hc + 1, hc)?;
let acc = blockconv_accum(&bordered)?;
let out = Pix::new(w, h, PixelDepth::Bit32)?;
let mut out_mut = out.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let xmax = x + 2 * wc + 1;
let val = acc.get_pixel_unchecked(xmax, y + 2 * hc + 1) as i64
- acc.get_pixel_unchecked(xmax, y) as i64
- acc.get_pixel_unchecked(x, y + 2 * hc + 1) as i64
+ acc.get_pixel_unchecked(x, y) as i64;
out_mut.set_pixel_unchecked(x, y, val as u32);
}
}
Ok(out_mut.into())
}
pub fn blockconv_gray_tile(
pixs: &Pix,
pixacc: Option<&Pix>,
wc: u32,
hc: u32,
) -> FilterResult<Pix> {
check_8bpp(pixs)?;
let w = pixs.width();
let h = pixs.height();
if wc == 0 || hc == 0 {
return Ok(pixs.deep_clone());
}
let mut wc = wc;
let mut hc = hc;
if w < 2 * wc + 3 || h < 2 * hc + 3 {
wc = wc.min((w - 1) / 2);
hc = hc.min((h - 1) / 2);
}
if wc == 0 || hc == 0 {
return Ok(pixs.deep_clone());
}
let border_w = 2 * (wc + 2);
let border_h = 2 * (hc + 2);
let wd = w
.checked_sub(border_w)
.ok_or_else(|| FilterError::InvalidParameters("tile width too small for kernel".into()))?;
let hd = h
.checked_sub(border_h)
.ok_or_else(|| FilterError::InvalidParameters("tile height too small for kernel".into()))?;
let owned_acc;
let acc = match pixacc {
Some(a) => {
if a.depth() != PixelDepth::Bit32 || a.width() != w || a.height() != h {
return Err(FilterError::InvalidParameters(
"accumulator must be 32 bpp with same dimensions as input".into(),
));
}
a
}
None => {
owned_acc = blockconv_accum(pixs)?;
&owned_acc
}
};
let norm = 1.0 / ((2 * wc + 1) as f64 * (2 * hc + 1) as f64);
let out = Pix::new(wd, hd, PixelDepth::Bit8)?;
let mut out_mut = out.try_into_mut().unwrap();
for oy in 0..hd {
let i = oy + hc + 2;
let imin = i - hc - 1; let imax = i + hc;
for ox in 0..wd {
let j = ox + wc + 2;
let jmin = j - wc - 1; let jmax = j + wc;
let val = acc.get_pixel_unchecked(jmax, imax) as i64
- acc.get_pixel_unchecked(jmin, imax) as i64
+ acc.get_pixel_unchecked(jmin, imin) as i64
- acc.get_pixel_unchecked(jmax, imin) as i64;
let result = (norm * val as f64 + 0.5) as u32;
out_mut.set_pixel_unchecked(ox, oy, result.min(255));
}
}
Ok(out_mut.into())
}
pub fn blockconv_tiled(pix: &Pix, wc: u32, hc: u32, nx: u32, ny: u32) -> FilterResult<Pix> {
if wc == 0 || hc == 0 {
return Ok(pix.deep_clone());
}
if nx == 0 || ny == 0 {
return Err(FilterError::InvalidParameters(
"nx and ny must be non-zero".into(),
));
}
if nx <= 1 && ny <= 1 {
return blockconv(pix, wc, hc);
}
let w = pix.width();
let h = pix.height();
let mut wc = wc;
let mut hc = hc;
if w < 2 * wc + 3 || h < 2 * hc + 3 {
wc = wc.min((w - 1) / 2);
hc = hc.min((h - 1) / 2);
}
if wc == 0 || hc == 0 {
return Ok(pix.deep_clone());
}
let d = pix.depth();
if d != PixelDepth::Bit8 && d != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: d.bits(),
});
}
let mut nx = nx;
let mut ny = ny;
if w / nx < wc + 2 {
nx = (w / (wc + 2)).max(1);
}
if h / ny < hc + 2 {
ny = (h / (hc + 2)).max(1);
}
if nx <= 1 && ny <= 1 {
return blockconv(pix, wc, hc);
}
match d {
PixelDepth::Bit8 => blockconv_tiled_gray(pix, wc, hc, nx, ny),
_ => {
let pix_r = pix.get_rgb_component(RgbComponent::Red)?;
let pix_g = pix.get_rgb_component(RgbComponent::Green)?;
let pix_b = pix.get_rgb_component(RgbComponent::Blue)?;
let conv_r = blockconv_tiled_gray(&pix_r, wc, hc, nx, ny)?;
let conv_g = blockconv_tiled_gray(&pix_g, wc, hc, nx, ny)?;
let conv_b = blockconv_tiled_gray(&pix_b, wc, hc, nx, ny)?;
Ok(Pix::create_rgb_image(&conv_r, &conv_g, &conv_b)?)
}
}
}
fn blockconv_tiled_gray(pix: &Pix, wc: u32, hc: u32, nx: u32, ny: u32) -> FilterResult<Pix> {
let w = pix.width();
let h = pix.height();
let wt = w / nx;
let ht = h / ny;
let out = Pix::new(w, h, PixelDepth::Bit8)?;
let mut out_mut = out.try_into_mut().unwrap();
for iy in 0..ny {
for jx in 0..nx {
let out_x = jx * wt;
let out_y = iy * ht;
let out_w = if jx == nx - 1 { w - out_x } else { wt };
let out_h = if iy == ny - 1 { h - out_y } else { ht };
let clip_x = out_x.saturating_sub(wc + 1);
let clip_y = out_y.saturating_sub(hc + 1);
let clip_right = (out_x + out_w + wc).min(w - 1);
let clip_bottom = (out_y + out_h + hc).min(h - 1);
let clip_w = clip_right - clip_x + 1;
let clip_h = clip_bottom - clip_y + 1;
let tile = pix.clip_rectangle(clip_x, clip_y, clip_w, clip_h)?;
let convolved = blockconv_gray(&tile, None, wc, hc)?;
let off_x = out_x - clip_x;
let off_y = out_y - clip_y;
for dy in 0..out_h {
for dx in 0..out_w {
let val = convolved.get_pixel_unchecked(off_x + dx, off_y + dy);
out_mut.set_pixel_unchecked(out_x + dx, out_y + dy, val);
}
}
}
}
Ok(out_mut.into())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{PixelDepth, pixel};
fn create_test_gray_image(w: u32, h: u32) -> Pix {
let pix = Pix::new(w, h, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let val = (x + y * w) % 256;
pm.set_pixel_unchecked(x, y, val);
}
}
pm.into()
}
fn create_uniform_gray_image(w: u32, h: u32, val: u32) -> Pix {
let pix = Pix::new(w, h, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
pm.set_pixel_unchecked(x, y, val);
}
}
pm.into()
}
fn create_test_color_image(w: u32, h: u32) -> Pix {
let pix = Pix::new(w, h, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let r = (x * 50 % 256) as u8;
let g = (y * 50 % 256) as u8;
let b = 128u8;
pm.set_pixel_unchecked(x, y, pixel::compose_rgb(r, g, b));
}
}
pm.into()
}
#[test]
fn test_blockconv_accum_basic() {
let pix = Pix::new(3, 3, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, 1);
pm.set_pixel_unchecked(1, 0, 2);
pm.set_pixel_unchecked(2, 0, 3);
pm.set_pixel_unchecked(0, 1, 4);
pm.set_pixel_unchecked(1, 1, 5);
pm.set_pixel_unchecked(2, 1, 6);
pm.set_pixel_unchecked(0, 2, 7);
pm.set_pixel_unchecked(1, 2, 8);
pm.set_pixel_unchecked(2, 2, 9);
let pix: Pix = pm.into();
let acc = blockconv_accum(&pix).unwrap();
assert_eq!(acc.depth(), PixelDepth::Bit32);
assert_eq!(acc.width(), 3);
assert_eq!(acc.height(), 3);
assert_eq!(acc.get_pixel_unchecked(0, 0), 1);
assert_eq!(acc.get_pixel_unchecked(1, 0), 3);
assert_eq!(acc.get_pixel_unchecked(2, 0), 6);
assert_eq!(acc.get_pixel_unchecked(0, 1), 5);
assert_eq!(acc.get_pixel_unchecked(1, 1), 12);
assert_eq!(acc.get_pixel_unchecked(2, 1), 21);
assert_eq!(acc.get_pixel_unchecked(0, 2), 12);
assert_eq!(acc.get_pixel_unchecked(1, 2), 27);
assert_eq!(acc.get_pixel_unchecked(2, 2), 45);
}
#[test]
fn test_blockconv_accum_rejects_non_8bpp() {
let pix = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
assert!(blockconv_accum(&pix).is_err());
}
#[test]
fn test_blockconv_gray_uniform_image() {
let pix = create_uniform_gray_image(20, 20, 100);
let result = blockconv_gray(&pix, None, 3, 3).unwrap();
assert_eq!(result.width(), 20);
assert_eq!(result.height(), 20);
assert_eq!(result.depth(), PixelDepth::Bit8);
for y in 0..20 {
for x in 0..20 {
let val = result.get_pixel_unchecked(x, y);
assert!(
(val as i32 - 100).unsigned_abs() <= 1,
"pixel ({},{}) = {}, expected ~100",
x,
y,
val
);
}
}
}
#[test]
fn test_blockconv_gray_preserves_dimensions() {
let pix = create_test_gray_image(30, 25);
let result = blockconv_gray(&pix, None, 2, 3).unwrap();
assert_eq!(result.width(), 30);
assert_eq!(result.height(), 25);
assert_eq!(result.depth(), PixelDepth::Bit8);
}
#[test]
fn test_blockconv_gray_zero_kernel_returns_copy() {
let pix = create_test_gray_image(10, 10);
let result = blockconv_gray(&pix, None, 0, 3).unwrap();
for y in 0..10 {
for x in 0..10 {
assert_eq!(
pix.get_pixel_unchecked(x, y),
result.get_pixel_unchecked(x, y)
);
}
}
}
#[test]
fn test_blockconv_gray_with_precomputed_accum() {
let pix = create_test_gray_image(20, 20);
let acc = blockconv_accum(&pix).unwrap();
let result1 = blockconv_gray(&pix, None, 2, 2).unwrap();
let result2 = blockconv_gray(&pix, Some(&acc), 2, 2).unwrap();
for y in 0..20 {
for x in 0..20 {
assert_eq!(
result1.get_pixel_unchecked(x, y),
result2.get_pixel_unchecked(x, y),
"mismatch at ({},{})",
x,
y
);
}
}
}
#[test]
fn test_blockconv_gray_kernel_reduction() {
let pix = create_uniform_gray_image(5, 5, 50);
let result = blockconv_gray(&pix, None, 10, 10).unwrap();
assert_eq!(result.width(), 5);
assert_eq!(result.height(), 5);
}
#[test]
fn test_blockconv_gray_rejects_non_8bpp() {
let pix = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
assert!(blockconv_gray(&pix, None, 2, 2).is_err());
}
#[test]
fn test_blockconv_gray_rejects_mismatched_accum() {
let pix = create_test_gray_image(20, 20);
let bad_acc = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
assert!(blockconv_gray(&pix, Some(&bad_acc), 2, 2).is_err());
let bad_acc = Pix::new(20, 20, PixelDepth::Bit8).unwrap();
assert!(blockconv_gray(&pix, Some(&bad_acc), 2, 2).is_err());
}
#[test]
fn test_blockconv_gray_dispatch() {
let pix = create_test_gray_image(20, 20);
let result = blockconv(&pix, 2, 2).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit8);
}
#[test]
fn test_blockconv_color_dispatch() {
let pix = create_test_color_image(20, 20);
let result = blockconv(&pix, 2, 2).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit32);
assert_eq!(result.width(), 20);
assert_eq!(result.height(), 20);
}
#[test]
fn test_blockconv_color_uniform() {
let pix = Pix::new(20, 20, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..20 {
for x in 0..20 {
pm.set_pixel_unchecked(x, y, pixel::compose_rgb(100, 150, 200));
}
}
let pix: Pix = pm.into();
let result = blockconv(&pix, 3, 3).unwrap();
for y in 0..20 {
for x in 0..20 {
let (r, g, b) = pixel::extract_rgb(result.get_pixel_unchecked(x, y));
assert!(
(r as i32 - 100).unsigned_abs() <= 1,
"R mismatch at ({},{})",
x,
y
);
assert!(
(g as i32 - 150).unsigned_abs() <= 1,
"G mismatch at ({},{})",
x,
y
);
assert!(
(b as i32 - 200).unsigned_abs() <= 1,
"B mismatch at ({},{})",
x,
y
);
}
}
}
#[test]
fn test_blockconv_rejects_unsupported_depth() {
let pix = Pix::new(10, 10, PixelDepth::Bit1).unwrap();
assert!(blockconv(&pix, 2, 2).is_err());
}
#[test]
fn test_blockconv_gray_unnormalized_basic() {
let pix = create_uniform_gray_image(20, 20, 10);
let result = blockconv_gray_unnormalized(&pix, 2, 2).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit32);
assert_eq!(result.width(), 20);
assert_eq!(result.height(), 20);
let window_area = (2 * 2 + 1) * (2 * 2 + 1); let expected = 10 * window_area;
for y in 0..20u32 {
for x in 0..20u32 {
let val = result.get_pixel_unchecked(x, y);
assert_eq!(val, expected, "mismatch at ({},{}): got {}", x, y, val);
}
}
}
#[test]
fn test_blockconv_gray_unnormalized_zero_kernel_returns_copy() {
let pix = create_test_gray_image(10, 10);
let result = blockconv_gray_unnormalized(&pix, 0, 3).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit8);
for y in 0..10 {
for x in 0..10 {
assert_eq!(
pix.get_pixel_unchecked(x, y),
result.get_pixel_unchecked(x, y)
);
}
}
}
#[test]
fn test_blockconv_gray_unnormalized_rejects_non_8bpp() {
let pix = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
assert!(blockconv_gray_unnormalized(&pix, 2, 2).is_err());
}
#[test]
fn test_blockconv_gray_tile_basic() {
let pix = create_uniform_gray_image(20, 20, 100);
let bordered = pix.add_mirrored_border(3, 3, 3, 3).unwrap();
assert_eq!(bordered.width(), 26);
assert_eq!(bordered.height(), 26);
let result = blockconv_gray_tile(&bordered, None, 2, 2).unwrap();
assert_eq!(result.width(), 18);
assert_eq!(result.height(), 18);
for y in 0..result.height() {
for x in 0..result.width() {
let val = result.get_pixel_unchecked(x, y);
assert!(
(val as i32 - 100).unsigned_abs() <= 1,
"pixel ({},{}) = {}, expected ~100",
x,
y,
val
);
}
}
}
#[test]
fn test_blockconv_gray_tile_noop_zero_kernel() {
let pix = create_test_gray_image(10, 10);
let result = blockconv_gray_tile(&pix, None, 0, 2).unwrap();
for y in 0..10 {
for x in 0..10 {
assert_eq!(
pix.get_pixel_unchecked(x, y),
result.get_pixel_unchecked(x, y)
);
}
}
}
#[test]
fn test_blockconv_tiled_matches_non_tiled() {
let pix = create_test_gray_image(40, 40);
let non_tiled = blockconv(&pix, 3, 3).unwrap();
let tiled = blockconv_tiled(&pix, 3, 3, 2, 2).unwrap();
assert_eq!(tiled.width(), 40);
assert_eq!(tiled.height(), 40);
for y in 0..40u32 {
for x in 0..40u32 {
let v1 = non_tiled.get_pixel_unchecked(x, y) as i32;
let v2 = tiled.get_pixel_unchecked(x, y) as i32;
assert!(
(v1 - v2).unsigned_abs() <= 1,
"mismatch at ({},{}): non_tiled={}, tiled={}",
x,
y,
v1,
v2
);
}
}
}
#[test]
fn test_blockconv_tiled_single_tile_delegates() {
let pix = create_uniform_gray_image(20, 20, 128);
let result = blockconv_tiled(&pix, 2, 2, 1, 1).unwrap();
let expected = blockconv(&pix, 2, 2).unwrap();
for y in 0..20u32 {
for x in 0..20u32 {
assert_eq!(
expected.get_pixel_unchecked(x, y),
result.get_pixel_unchecked(x, y),
"mismatch at ({},{})",
x,
y
);
}
}
}
#[test]
fn test_blockconv_tiled_color() {
let pix = create_test_color_image(30, 30);
let non_tiled = blockconv(&pix, 2, 2).unwrap();
let tiled = blockconv_tiled(&pix, 2, 2, 3, 3).unwrap();
assert_eq!(tiled.depth(), PixelDepth::Bit32);
for y in 0..30u32 {
for x in 0..30u32 {
let (r1, g1, b1) = pixel::extract_rgb(non_tiled.get_pixel_unchecked(x, y));
let (r2, g2, b2) = pixel::extract_rgb(tiled.get_pixel_unchecked(x, y));
assert!(
(r1 as i32 - r2 as i32).unsigned_abs() <= 1
&& (g1 as i32 - g2 as i32).unsigned_abs() <= 1
&& (b1 as i32 - b2 as i32).unsigned_abs() <= 1,
"mismatch at ({},{}): non_tiled=({},{},{}), tiled=({},{},{})",
x,
y,
r1,
g1,
b1,
r2,
g2,
b2
);
}
}
}
}