use crate::core::{FPix, Pix, PixelDepth, pix::RgbComponent, pixel};
use crate::filter::{FilterError, FilterResult, Kernel};
pub fn convolve_gray(pix: &Pix, kernel: &Kernel) -> FilterResult<Pix> {
check_grayscale(pix)?;
let w = pix.width();
let h = pix.height();
let kw = kernel.width();
let kh = kernel.height();
let kcx = kernel.center_x() as i32;
let kcy = kernel.center_y() as i32;
let out_pix = Pix::new(w, h, PixelDepth::Bit8)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let mut sum = 0.0f32;
for ky in 0..kh {
for kx in 0..kw {
let sx = x as i32 + (kx as i32 - kcx);
let sy = y as i32 + (ky as i32 - kcy);
let sx = sx.clamp(0, w as i32 - 1) as u32;
let sy = sy.clamp(0, h as i32 - 1) as u32;
let pixel = pix.get_pixel_unchecked(sx, sy) as f32;
let k = kernel.get(kx, ky).unwrap_or(0.0);
sum += pixel * k;
}
}
let result = sum.round().clamp(0.0, 255.0) as u32;
out_mut.set_pixel_unchecked(x, y, result);
}
}
Ok(out_mut.into())
}
pub fn convolve_color(pix: &Pix, kernel: &Kernel) -> FilterResult<Pix> {
check_color(pix)?;
let w = pix.width();
let h = pix.height();
let kw = kernel.width();
let kh = kernel.height();
let kcx = kernel.center_x() as i32;
let kcy = kernel.center_y() as i32;
let out_pix = Pix::new(w, h, PixelDepth::Bit32)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
out_mut.set_spp(pix.spp());
for y in 0..h {
for x in 0..w {
let mut sum_r = 0.0f32;
let mut sum_g = 0.0f32;
let mut sum_b = 0.0f32;
let mut sum_a = 0.0f32;
for ky in 0..kh {
for kx in 0..kw {
let sx = x as i32 + (kx as i32 - kcx);
let sy = y as i32 + (ky as i32 - kcy);
let sx = sx.clamp(0, w as i32 - 1) as u32;
let sy = sy.clamp(0, h as i32 - 1) as u32;
let pixel = pix.get_pixel_unchecked(sx, sy);
let (r, g, b, a) = pixel::extract_rgba(pixel);
let k = kernel.get(kx, ky).unwrap_or(0.0);
sum_r += r as f32 * k;
sum_g += g as f32 * k;
sum_b += b as f32 * k;
sum_a += a as f32 * k;
}
}
let r = sum_r.round().clamp(0.0, 255.0) as u8;
let g = sum_g.round().clamp(0.0, 255.0) as u8;
let b = sum_b.round().clamp(0.0, 255.0) as u8;
let a = sum_a.round().clamp(0.0, 255.0) as u8;
let result = pixel::compose_rgba(r, g, b, a);
out_mut.set_pixel_unchecked(x, y, result);
}
}
Ok(out_mut.into())
}
pub fn convolve(pix: &Pix, kernel: &Kernel) -> FilterResult<Pix> {
match pix.depth() {
PixelDepth::Bit8 => convolve_gray(pix, kernel),
PixelDepth::Bit32 => convolve_color(pix, kernel),
_ => Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: pix.depth().bits(),
}),
}
}
pub fn box_blur(pix: &Pix, radius: u32) -> FilterResult<Pix> {
let size = 2 * radius + 1;
let kernel = Kernel::box_kernel(size)?;
convolve(pix, &kernel)
}
pub fn gaussian_blur(pix: &Pix, radius: u32, sigma: f32) -> FilterResult<Pix> {
let size = 2 * radius + 1;
let kernel = Kernel::gaussian(size, sigma)?;
convolve(pix, &kernel)
}
pub fn gaussian_blur_auto(pix: &Pix, radius: u32) -> FilterResult<Pix> {
let sigma = (radius as f32).max(0.5);
gaussian_blur(pix, radius, sigma)
}
pub fn convolve_sep(pix: &Pix, kernel_x: &Kernel, kernel_y: &Kernel) -> FilterResult<Pix> {
match pix.depth() {
PixelDepth::Bit8 | PixelDepth::Bit32 => {}
_ => {
return Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: pix.depth().bits(),
});
}
}
let temp = convolve(pix, kernel_x)?;
let result = convolve(&temp, kernel_y)?;
Ok(result)
}
pub fn convolve_rgb_sep(pix: &Pix, kernel_x: &Kernel, kernel_y: &Kernel) -> FilterResult<Pix> {
check_color(pix)?;
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 result_r = convolve_sep(&pix_r, kernel_x, kernel_y)?;
let result_g = convolve_sep(&pix_g, kernel_x, kernel_y)?;
let result_b = convolve_sep(&pix_b, kernel_x, kernel_y)?;
let result = Pix::create_rgb_image(&result_r, &result_g, &result_b)?;
Ok(result)
}
fn check_grayscale(pix: &Pix) -> FilterResult<()> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8-bpp grayscale",
actual: pix.depth().bits(),
});
}
Ok(())
}
fn check_color(pix: &Pix) -> FilterResult<()> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32-bpp color",
actual: pix.depth().bits(),
});
}
Ok(())
}
pub fn census_transform(pix: &Pix, halfsize: u32) -> FilterResult<Pix> {
check_grayscale(pix)?;
if halfsize < 1 {
return Err(FilterError::InvalidParameters(
"halfsize must be >= 1".into(),
));
}
let pixav = crate::filter::block_conv::blockconv_gray(pix, None, halfsize, halfsize)?;
let w = pix.width();
let h = pix.height();
let pixd = Pix::new(w, h, PixelDepth::Bit1)?;
let mut pixd_mut = pixd.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let val_src = pix.get_pixel_unchecked(x, y);
let val_avg = pixav.get_pixel_unchecked(x, y);
if val_src > val_avg {
pixd_mut.set_pixel_unchecked(x, y, 1);
}
}
}
Ok(pixd_mut.into())
}
pub fn add_gaussian_noise(pix: &Pix, stdev: f32) -> FilterResult<Pix> {
let stdev = stdev.max(0.0);
match pix.depth() {
PixelDepth::Bit8 | PixelDepth::Bit32 => {}
_ => {
return Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: pix.depth().bits(),
});
}
}
let w = pix.width();
let h = pix.height();
let pixd = Pix::new(w, h, pix.depth())?;
let mut pixd_mut = pixd.try_into_mut().unwrap();
if pix.depth() == PixelDepth::Bit32 {
pixd_mut.set_spp(pix.spp());
}
struct GaussianSampler {
select: bool,
saved: f32,
state: u64,
}
impl GaussianSampler {
fn new() -> Self {
Self {
select: false,
saved: 0.0,
state: 1234567890u64,
}
}
fn rand_f32(&mut self) -> f32 {
const A: u64 = 1664525;
const C: u64 = 1013904223;
self.state = self.state.wrapping_mul(A).wrapping_add(C);
(self.state as f32) / (u64::MAX as f32)
}
fn sample(&mut self) -> f32 {
if self.select {
self.select = false;
self.saved
} else {
let (xval, yval, rsq) = loop {
let xval = 2.0 * self.rand_f32() - 1.0;
let yval = 2.0 * self.rand_f32() - 1.0;
let rsq = xval * xval + yval * yval;
if rsq > 0.0 && rsq < 1.0 {
break (xval, yval, rsq);
}
};
let factor = (-2.0 * rsq.ln() / rsq).sqrt();
self.saved = xval * factor;
self.select = true;
yval * factor
}
}
}
let mut sampler = GaussianSampler::new();
if pix.depth() == PixelDepth::Bit8 {
for y in 0..h {
for x in 0..w {
let val = pix.get_pixel_unchecked(x, y) as i32;
let noise = (stdev * sampler.sample()).round() as i32;
let result = (val + noise).clamp(0, 255) as u32;
pixd_mut.set_pixel_unchecked(x, y, result);
}
}
} else {
for y in 0..h {
for x in 0..w {
let pixel = pix.get_pixel_unchecked(x, y);
let (r, g, b, a) = pixel::extract_rgba(pixel);
let r_noise = (stdev * sampler.sample()).round() as i32;
let g_noise = (stdev * sampler.sample()).round() as i32;
let b_noise = (stdev * sampler.sample()).round() as i32;
let r_out = ((r as i32) + r_noise).clamp(0, 255) as u8;
let g_out = ((g as i32) + g_noise).clamp(0, 255) as u8;
let b_out = ((b as i32) + b_noise).clamp(0, 255) as u8;
let result = pixel::compose_rgba(r_out, g_out, b_out, a);
pixd_mut.set_pixel_unchecked(x, y, result);
}
}
}
Ok(pixd_mut.into())
}
pub fn blocksum(pix: &Pix, wc: u32, hc: u32) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit1 {
return Err(FilterError::UnsupportedDepth {
expected: "1 bpp",
actual: pix.depth().bits(),
});
}
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);
if wc == 0 || hc == 0 {
return Ok(pix.convert_1_to_8(0, 255)?);
}
let pix8 = pix.convert_1_to_8(0, 255)?;
let acc = crate::filter::block_conv::blockconv_accum(&pix8)?;
let pixd = Pix::new(w, h, PixelDepth::Bit8)?;
let mut pixd_mut = pixd.try_into_mut().unwrap();
let fwc = (2 * wc + 1) as f64;
let fhc = (2 * hc + 1) as f64;
let norm = 1.0 / (fwc * fhc);
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);
pixd_mut.set_pixel_unchecked(x, y, result);
}
}
Ok(pixd_mut.into())
}
pub fn blockrank(pix: &Pix, wc: u32, hc: u32, rank: f32) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit1 {
return Err(FilterError::UnsupportedDepth {
expected: "1 bpp",
actual: pix.depth().bits(),
});
}
if !(0.0..=1.0).contains(&rank) {
return Err(FilterError::InvalidParameters(
"rank must be in [0.0, 1.0]".into(),
));
}
if rank == 0.0 {
let w = pix.width();
let h = pix.height();
let pixd = Pix::new(w, h, PixelDepth::Bit1)?;
let mut pixd_mut = pixd.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
pixd_mut.set_pixel_unchecked(x, y, 1);
}
}
return Ok(pixd_mut.into());
}
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);
if wc == 0 || hc == 0 {
return Ok(pix.deep_clone());
}
let pixt = blocksum(pix, wc, hc)?;
let thresh = (255.0 * rank) as u32;
let pixd = Pix::new(w, h, PixelDepth::Bit1)?;
let mut pixd_mut = pixd.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let val = pixt.get_pixel_unchecked(x, y);
if val >= thresh {
pixd_mut.set_pixel_unchecked(x, y, 1);
}
}
}
Ok(pixd_mut.into())
}
pub fn fpix_convolve(fpix: &FPix, kernel: &Kernel, normalize: bool) -> FilterResult<FPix> {
let w = fpix.width() as i32;
let h = fpix.height() as i32;
let kw = kernel.width() as i32;
let kh = kernel.height() as i32;
let cx = kernel.center_x() as i32;
let cy = kernel.center_y() as i32;
let ksum = kernel.sum();
let scale = if normalize && ksum.abs() >= 1e-6 {
1.0 / ksum
} else {
1.0
};
let mut fpixd = FPix::new(w as u32, h as u32)?;
let kdata = kernel.data();
for y in 0..h {
for x in 0..w {
let mut sum = 0.0f32;
for ky in 0..kh {
let sy = (y + ky - cy).clamp(0, h - 1);
for kx in 0..kw {
let sx = (x + kx - cx).clamp(0, w - 1);
let val = fpix.get_pixel_unchecked(sx as u32, sy as u32);
let kidx = (ky * kw + kx) as usize;
sum += val * kdata[kidx] * scale;
}
}
fpixd.set_pixel_unchecked(x as u32, y as u32, sum);
}
}
Ok(fpixd)
}
pub fn fpix_convolve_sep(
fpix: &FPix,
kernel_x: &Kernel,
kernel_y: &Kernel,
normalize: bool,
) -> FilterResult<FPix> {
let tmp = fpix_convolve(fpix, kernel_x, normalize)?;
fpix_convolve(&tmp, kernel_y, normalize)
}
pub fn convolve_with_bias(
pix: &Pix,
kernel1: &Kernel,
kernel2: Option<&Kernel>,
force8: bool,
) -> FilterResult<(Pix, i32)> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::InvalidParameters(
"input must be 8-bpp grayscale".into(),
));
}
let min1 = kernel1.data().iter().cloned().fold(f32::INFINITY, f32::min);
let min2 = kernel2.map_or(0.0f32, |k| {
k.data().iter().cloned().fold(f32::INFINITY, f32::min)
});
let min = min1.min(min2);
if min >= 0.0 {
let result = if let Some(k2) = kernel2 {
convolve_sep(pix, kernel1, k2)?
} else {
convolve(pix, kernel1)?
};
return Ok((result, 0));
}
let fpix1 = FPix::from_pix(pix)?;
let fpix2 = if let Some(k2) = kernel2 {
fpix_convolve_sep(&fpix1, kernel1, k2, true)?
} else {
fpix_convolve(&fpix1, kernel1, true)?
};
let data = fpix2.data();
let minval = data.iter().cloned().fold(f32::INFINITY, f32::min);
let maxval = data.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let range = maxval - minval;
let bias = if minval < 0.0 {
(-minval).ceil() as i32
} else {
0
};
let mut fpix3 = FPix::new_with_value(fpix2.width(), fpix2.height(), 0.0)?;
for y in 0..fpix2.height() {
for x in 0..fpix2.width() {
let v = fpix2.get_pixel_unchecked(x, y) + bias as f32;
fpix3.set_pixel_unchecked(x, y, v);
}
}
let out_depth = if range <= 255.0 || !force8 {
if range > 255.0 { 16 } else { 8 }
} else {
let scale = 255.0 / range;
for y in 0..fpix3.height() {
for x in 0..fpix3.width() {
let v = fpix3.get_pixel_unchecked(x, y) * scale;
fpix3.set_pixel_unchecked(x, y, v);
}
}
8
};
let result = fpix3.to_pix(out_depth, crate::core::fpix::NegativeHandling::ClipToZero)?;
Ok((result, bias))
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_gray_image() -> Pix {
let pix = Pix::new(5, 5, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..5 {
for x in 0..5 {
let val = x * 50 + y * 10;
pix_mut.set_pixel_unchecked(x, y, val);
}
}
pix_mut.into()
}
fn create_test_color_image() -> Pix {
let pix = Pix::new(5, 5, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..5 {
for x in 0..5 {
let r = (x * 50) as u8;
let g = (y * 50) as u8;
let b = 128;
let pixel = pixel::compose_rgb(r, g, b);
pix_mut.set_pixel_unchecked(x, y, pixel);
}
}
pix_mut.into()
}
#[test]
fn test_convolve_gray_identity() {
let pix = create_test_gray_image();
let kernel = Kernel::from_slice(1, 1, &[1.0]).unwrap();
let result = convolve_gray(&pix, &kernel).unwrap();
for y in 0..5 {
for x in 0..5 {
let orig = pix.get_pixel_unchecked(x, y);
let conv = result.get_pixel_unchecked(x, y);
assert_eq!(orig, conv);
}
}
}
#[test]
fn test_box_blur_gray() {
let pix = create_test_gray_image();
let blurred = box_blur(&pix, 1).unwrap();
assert_eq!(blurred.width(), pix.width());
assert_eq!(blurred.height(), pix.height());
}
#[test]
fn test_gaussian_blur_gray() {
let pix = create_test_gray_image();
let blurred = gaussian_blur(&pix, 1, 1.0).unwrap();
assert_eq!(blurred.width(), pix.width());
assert_eq!(blurred.height(), pix.height());
}
#[test]
fn test_convolve_color() {
let pix = create_test_color_image();
let kernel = Kernel::box_kernel(3).unwrap();
let result = convolve_color(&pix, &kernel).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.depth(), PixelDepth::Bit32);
}
#[test]
fn test_convolve_auto_dispatch() {
let gray = create_test_gray_image();
let color = create_test_color_image();
let kernel = Kernel::box_kernel(3).unwrap();
let result_gray = convolve(&gray, &kernel).unwrap();
let result_color = convolve(&color, &kernel).unwrap();
assert_eq!(result_gray.depth(), PixelDepth::Bit8);
assert_eq!(result_color.depth(), PixelDepth::Bit32);
}
#[test]
fn test_convolve_sep_identity() {
let pix = create_test_gray_image();
let kernel_1d = Kernel::from_slice(1, 1, &[1.0]).unwrap();
let result = convolve_sep(&pix, &kernel_1d, &kernel_1d).unwrap();
for y in 0..5 {
for x in 0..5 {
let orig = pix.get_pixel_unchecked(x, y);
let conv = result.get_pixel_unchecked(x, y);
assert_eq!(orig, conv);
}
}
}
#[test]
fn test_convolve_sep_horizontal_vertical() {
let pix = create_test_gray_image();
let kernel_h = Kernel::from_slice(3, 1, &[1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0]).unwrap();
let kernel_v = Kernel::from_slice(1, 3, &[1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0]).unwrap();
let result_sep = convolve_sep(&pix, &kernel_h, &kernel_v).unwrap();
let kernel_full = Kernel::box_kernel(3).unwrap();
let result_full = convolve(&pix, &kernel_full).unwrap();
for y in 0..pix.height() {
for x in 0..pix.width() {
let sep_val = result_sep.get_pixel_unchecked(x, y);
let full_val = result_full.get_pixel_unchecked(x, y);
let diff = (sep_val as i32 - full_val as i32).abs();
assert!(
diff <= 1,
"Difference too large at ({}, {}): {} vs {}",
x,
y,
sep_val,
full_val
);
}
}
}
#[test]
fn test_convolve_sep_sobel_x() {
let pix = create_test_gray_image();
let kernel_h = Kernel::from_slice(3, 1, &[-1.0, 0.0, 1.0]).unwrap();
let kernel_v = Kernel::from_slice(1, 3, &[1.0, 2.0, 1.0]).unwrap();
let result = convolve_sep(&pix, &kernel_h, &kernel_v).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit8);
}
#[test]
fn test_convolve_sep_color() {
let pix = create_test_color_image();
let kernel_h = Kernel::from_slice(3, 1, &[1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0]).unwrap();
let kernel_v = Kernel::from_slice(1, 3, &[1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0]).unwrap();
let result = convolve_sep(&pix, &kernel_h, &kernel_v).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit32);
let kernel_full = Kernel::box_kernel(3).unwrap();
let result_full = convolve_color(&pix, &kernel_full).unwrap();
for y in 0..pix.height() {
for x in 0..pix.width() {
let sep_px = result.get_pixel_unchecked(x, y);
let full_px = result_full.get_pixel_unchecked(x, y);
let (sr, sg, sb, sa) = pixel::extract_rgba(sep_px);
let (fr, fg, fb, fa) = pixel::extract_rgba(full_px);
assert!(
(sr as i32 - fr as i32).abs() <= 1
&& (sg as i32 - fg as i32).abs() <= 1
&& (sb as i32 - fb as i32).abs() <= 1
&& (sa as i32 - fa as i32).abs() <= 1,
"Mismatch at ({}, {})",
x,
y
);
}
}
}
#[test]
fn test_convolve_rgb_sep_identity() {
let pix = create_test_color_image();
let kernel_1d = Kernel::from_slice(1, 1, &[1.0]).unwrap();
let result = convolve_rgb_sep(&pix, &kernel_1d, &kernel_1d).unwrap();
for y in 0..5 {
for x in 0..5 {
let orig = pix.get_pixel_unchecked(x, y);
let conv = result.get_pixel_unchecked(x, y);
assert_eq!(orig, conv);
}
}
}
#[test]
fn test_convolve_rgb_sep_box_blur() {
let pix = create_test_color_image();
let kernel_h = Kernel::from_slice(3, 1, &[1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0]).unwrap();
let kernel_v = Kernel::from_slice(1, 3, &[1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0]).unwrap();
let result_sep = convolve_rgb_sep(&pix, &kernel_h, &kernel_v).unwrap();
let kernel_full = Kernel::box_kernel(3).unwrap();
let result_full = convolve_color(&pix, &kernel_full).unwrap();
assert_eq!(result_sep.width(), pix.width());
assert_eq!(result_sep.height(), pix.height());
assert_eq!(result_sep.depth(), PixelDepth::Bit32);
for y in 0..pix.height() {
for x in 0..pix.width() {
let sep_px = result_sep.get_pixel_unchecked(x, y);
let full_px = result_full.get_pixel_unchecked(x, y);
let (sr, sg, sb, _) = pixel::extract_rgba(sep_px);
let (fr, fg, fb, _) = pixel::extract_rgba(full_px);
assert!(
(sr as i32 - fr as i32).abs() <= 1
&& (sg as i32 - fg as i32).abs() <= 1
&& (sb as i32 - fb as i32).abs() <= 1,
"Mismatch at ({}, {}): sep=({},{},{}) vs full=({},{},{})",
x,
y,
sr,
sg,
sb,
fr,
fg,
fb
);
}
}
}
#[test]
fn test_census_transform_basic() {
let pix = Pix::new(5, 5, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..5 {
for x in 0..5 {
let val = x * 20 + y * 20;
pix_mut.set_pixel_unchecked(x, y, val);
}
}
let pix = pix_mut.into();
let result = census_transform(&pix, 1).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit1);
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
}
#[test]
fn test_census_transform_uniform() {
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);
}
}
let pix = pix_mut.into();
let result = census_transform(&pix, 1).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit1);
for y in 0..10 {
for x in 0..10 {
assert_eq!(result.get_pixel_unchecked(x, y), 0);
}
}
}
#[test]
fn test_census_transform_invalid_depth() {
let pix = Pix::new(5, 5, PixelDepth::Bit1).unwrap();
let result = census_transform(&pix, 1);
assert!(result.is_err());
}
#[test]
fn test_add_gaussian_noise_8bpp() {
let pix = create_test_gray_image();
let result = add_gaussian_noise(&pix, 10.0).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit8);
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
}
#[test]
fn test_add_gaussian_noise_32bpp() {
let pix = create_test_color_image();
let result = add_gaussian_noise(&pix, 10.0).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit32);
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
}
#[test]
fn test_add_gaussian_noise_statistical_properties() {
let pix = Pix::new(100, 100, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
let original_value = 128u32;
for y in 0..100 {
for x in 0..100 {
pix_mut.set_pixel_unchecked(x, y, original_value);
}
}
let pix = pix_mut.into();
let result = add_gaussian_noise(&pix, 20.0).unwrap();
let mut sum = 0u64;
for y in 0..100 {
for x in 0..100 {
sum += result.get_pixel_unchecked(x, y) as u64;
}
}
let mean = sum / (100 * 100);
let diff = (mean as i64 - original_value as i64).abs();
assert!(
diff < 5,
"Mean {} too far from original {}",
mean,
original_value
);
}
#[test]
fn test_add_gaussian_noise_invalid_depth() {
let pix = Pix::new(5, 5, PixelDepth::Bit1).unwrap();
let result = add_gaussian_noise(&pix, 10.0);
assert!(result.is_err());
}
#[test]
fn test_blocksum_basic() {
let pix = Pix::new(10, 10, PixelDepth::Bit1).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 3..7 {
for x in 3..7 {
pix_mut.set_pixel_unchecked(x, y, 1);
}
}
let pix = pix_mut.into();
let result = blocksum(&pix, 1, 1).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit8);
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
let center_val = result.get_pixel_unchecked(5, 5);
assert!(
center_val > 200,
"Center value {} should be near 255",
center_val
);
}
#[test]
fn test_blocksum_all_zero() {
let pix = Pix::new(10, 10, PixelDepth::Bit1).unwrap();
let result = blocksum(&pix, 1, 1).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit8);
for y in 0..10 {
for x in 0..10 {
assert_eq!(result.get_pixel_unchecked(x, y), 0);
}
}
}
#[test]
fn test_blocksum_all_one() {
let pix = Pix::new(10, 10, PixelDepth::Bit1).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, 1);
}
}
let pix = pix_mut.into();
let result = blocksum(&pix, 1, 1).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit8);
for y in 0..10 {
for x in 0..10 {
assert_eq!(result.get_pixel_unchecked(x, y), 255);
}
}
}
#[test]
fn test_blocksum_invalid_depth() {
let pix = Pix::new(5, 5, PixelDepth::Bit8).unwrap();
let result = blocksum(&pix, 1, 1);
assert!(result.is_err());
}
#[test]
fn test_blockrank_basic() {
let pix = Pix::new(10, 10, PixelDepth::Bit1).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
if (x + y) % 2 == 0 {
pix_mut.set_pixel_unchecked(x, y, 1);
}
}
}
let pix = pix_mut.into();
let result = blockrank(&pix, 1, 1, 0.5).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit1);
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
}
#[test]
fn test_blockrank_threshold_zero() {
let pix = Pix::new(10, 10, PixelDepth::Bit1).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
pix_mut.set_pixel_unchecked(5, 5, 1);
let pix = pix_mut.into();
let result = blockrank(&pix, 1, 1, 0.0).unwrap();
assert_eq!(result.get_pixel_unchecked(5, 5), 1);
assert_eq!(result.get_pixel_unchecked(4, 5), 1);
assert_eq!(result.get_pixel_unchecked(6, 5), 1);
}
#[test]
fn test_blockrank_threshold_one() {
let pix = Pix::new(10, 10, PixelDepth::Bit1).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, 1);
}
}
let pix = pix_mut.into();
let result = blockrank(&pix, 1, 1, 1.0).unwrap();
assert_eq!(result.get_pixel_unchecked(5, 5), 1);
}
#[test]
fn test_blockrank_invalid_depth() {
let pix = Pix::new(5, 5, PixelDepth::Bit8).unwrap();
let result = blockrank(&pix, 1, 1, 0.5);
assert!(result.is_err());
}
#[test]
fn test_blockrank_invalid_rank() {
let pix = Pix::new(5, 5, PixelDepth::Bit1).unwrap();
let result_low = blockrank(&pix, 1, 1, -0.1);
let result_high = blockrank(&pix, 1, 1, 1.1);
assert!(result_low.is_err());
assert!(result_high.is_err());
}
#[test]
fn test_fpix_convolve_basic() {
let mut fpix = FPix::new(7, 7).unwrap();
fpix.set_pixel_unchecked(3, 3, 1.0);
let kernel = Kernel::from_slice(3, 3, &[1.0; 9]).unwrap();
let result = fpix_convolve(&fpix, &kernel, false).unwrap();
assert!((result.get_pixel_unchecked(3, 3) - 1.0).abs() < 0.01);
assert!((result.get_pixel_unchecked(0, 0) - 0.0).abs() < 0.01);
assert!((result.get_pixel_unchecked(4, 4) - 1.0).abs() < 0.01);
}
#[test]
fn test_fpix_convolve_normalized() {
let fpix = FPix::new_with_value(5, 5, 100.0).unwrap();
let kernel = Kernel::from_slice(3, 3, &[1.0; 9]).unwrap();
let result = fpix_convolve(&fpix, &kernel, true).unwrap();
assert!((result.get_pixel_unchecked(2, 2) - 100.0).abs() < 0.01);
}
#[test]
fn test_fpix_convolve_sep_matches_non_sep() {
let mut fpix = FPix::new(7, 7).unwrap();
for y in 0..7u32 {
for x in 0..7u32 {
fpix.set_pixel_unchecked(x, y, (x * 10 + y * 3) as f32);
}
}
let kernel_x = Kernel::from_slice(3, 1, &[1.0, 1.0, 1.0]).unwrap();
let kernel_y = Kernel::from_slice(1, 3, &[1.0, 1.0, 1.0]).unwrap();
let kernel_2d = Kernel::from_slice(3, 3, &[1.0; 9]).unwrap();
let result_sep = fpix_convolve_sep(&fpix, &kernel_x, &kernel_y, true).unwrap();
let result_non_sep = fpix_convolve(&fpix, &kernel_2d, true).unwrap();
let w = fpix.width();
let h = fpix.height();
let tol = 1e-4;
for y in 0..h {
for x in 0..w {
let v_sep = result_sep.get_pixel_unchecked(x, y);
let v_non = result_non_sep.get_pixel_unchecked(x, y);
assert!(
(v_sep - v_non).abs() <= tol,
"sep vs non-sep differ at ({},{}): {} vs {}, diff={}",
x,
y,
v_sep,
v_non,
(v_sep - v_non).abs()
);
}
}
}
#[test]
fn test_convolve_with_bias_no_negative_kernel() {
let pix = Pix::new(5, 5, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..5u32 {
for x in 0..5u32 {
pix_mut.set_pixel_unchecked(x, y, 100);
}
}
let pix: Pix = pix_mut.into();
let kernel = Kernel::from_slice(3, 3, &[1.0; 9]).unwrap();
let (result, bias) = convolve_with_bias(&pix, &kernel, None, true).unwrap();
assert_eq!(bias, 0);
assert_eq!(result.depth(), PixelDepth::Bit8);
}
#[test]
fn test_convolve_with_bias_negative_kernel() {
let pix = Pix::new(7, 7, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..7u32 {
for x in 0..7u32 {
pix_mut.set_pixel_unchecked(x, y, ((x + y) * 21).min(255));
}
}
let pix: Pix = pix_mut.into();
let kernel = Kernel::laplacian();
let (result, bias) = convolve_with_bias(&pix, &kernel, None, true).unwrap();
assert!(
bias > 0,
"Expected positive bias for Laplacian on gradient image, got {}",
bias
);
let w = result.width();
let h = result.height();
let mut min_val = 255u32;
for y in 0..h {
for x in 0..w {
let v = result.get_pixel_unchecked(x, y);
if v < min_val {
min_val = v;
}
}
}
assert_eq!(
min_val, 0,
"Expected minimum pixel value 0 after biasing, got {}",
min_val
);
}
}