use crate::core::{Pix, PixelDepth, pixel};
use crate::filter::{FilterError, FilterResult};
pub const DEFAULT_TILE_WIDTH: u32 = 10;
pub const DEFAULT_TILE_HEIGHT: u32 = 15;
pub const DEFAULT_FG_THRESHOLD: u32 = 60;
pub const DEFAULT_MIN_COUNT: u32 = 40;
pub const DEFAULT_BG_VAL: u32 = 200;
pub const DEFAULT_SMOOTH_X: u32 = 2;
pub const DEFAULT_SMOOTH_Y: u32 = 1;
pub const DEFAULT_MIN_DIFF: u32 = 50;
pub const DEFAULT_CONTRAST_TILE_SIZE: u32 = 20;
#[derive(Debug, Clone)]
pub struct BackgroundNormOptions {
pub tile_width: u32,
pub tile_height: u32,
pub fg_threshold: u32,
pub min_count: u32,
pub bg_val: u32,
pub smooth_x: u32,
pub smooth_y: u32,
}
impl Default for BackgroundNormOptions {
fn default() -> Self {
Self {
tile_width: DEFAULT_TILE_WIDTH,
tile_height: DEFAULT_TILE_HEIGHT,
fg_threshold: DEFAULT_FG_THRESHOLD,
min_count: DEFAULT_MIN_COUNT,
bg_val: DEFAULT_BG_VAL,
smooth_x: DEFAULT_SMOOTH_X,
smooth_y: DEFAULT_SMOOTH_Y,
}
}
}
#[derive(Debug, Clone)]
pub struct ContrastNormOptions {
pub tile_width: u32,
pub tile_height: u32,
pub min_diff: u32,
pub smooth_x: u32,
pub smooth_y: u32,
}
impl Default for ContrastNormOptions {
fn default() -> Self {
Self {
tile_width: DEFAULT_CONTRAST_TILE_SIZE,
tile_height: DEFAULT_CONTRAST_TILE_SIZE,
min_diff: DEFAULT_MIN_DIFF,
smooth_x: 2,
smooth_y: 2,
}
}
}
pub fn background_norm_simple(pix: &Pix) -> FilterResult<Pix> {
background_norm(pix, &BackgroundNormOptions::default())
}
pub fn background_norm(pix: &Pix, options: &BackgroundNormOptions) -> FilterResult<Pix> {
if options.tile_width < 4 || options.tile_height < 4 {
return Err(FilterError::InvalidParameters(
"tile dimensions must be >= 4".to_string(),
));
}
if options.bg_val < 128 || options.bg_val > 255 {
return Err(FilterError::InvalidParameters(
"bg_val should be between 128 and 255".to_string(),
));
}
let mut min_count = options.min_count;
if min_count > options.tile_width * options.tile_height {
min_count = (options.tile_width * options.tile_height) / 3;
}
match pix.depth() {
PixelDepth::Bit8 => background_norm_gray(pix, options, min_count),
PixelDepth::Bit32 => background_norm_color(pix, options, min_count),
_ => Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: pix.depth().bits(),
}),
}
}
fn background_norm_gray(
pix: &Pix,
options: &BackgroundNormOptions,
min_count: u32,
) -> FilterResult<Pix> {
let bg_map = get_background_gray_map_inner(
pix,
options.tile_width,
options.tile_height,
options.fg_threshold,
min_count,
)?;
let inv_map =
get_inv_background_map_inner(&bg_map, options.bg_val, options.smooth_x, options.smooth_y)?;
apply_inv_background_gray_map_inner(pix, &inv_map, options.tile_width, options.tile_height)
}
fn background_norm_color(
pix: &Pix,
options: &BackgroundNormOptions,
min_count: u32,
) -> FilterResult<Pix> {
let (pixr, pixg, pixb) = extract_rgb_channels(pix)?;
let bg_map_r = get_background_gray_map_inner(
&pixr,
options.tile_width,
options.tile_height,
options.fg_threshold,
min_count,
)?;
let bg_map_g = get_background_gray_map_inner(
&pixg,
options.tile_width,
options.tile_height,
options.fg_threshold,
min_count,
)?;
let bg_map_b = get_background_gray_map_inner(
&pixb,
options.tile_width,
options.tile_height,
options.fg_threshold,
min_count,
)?;
let inv_map_r = get_inv_background_map_inner(
&bg_map_r,
options.bg_val,
options.smooth_x,
options.smooth_y,
)?;
let inv_map_g = get_inv_background_map_inner(
&bg_map_g,
options.bg_val,
options.smooth_x,
options.smooth_y,
)?;
let inv_map_b = get_inv_background_map_inner(
&bg_map_b,
options.bg_val,
options.smooth_x,
options.smooth_y,
)?;
let result_r = apply_inv_background_gray_map_inner(
&pixr,
&inv_map_r,
options.tile_width,
options.tile_height,
)?;
let result_g = apply_inv_background_gray_map_inner(
&pixg,
&inv_map_g,
options.tile_width,
options.tile_height,
)?;
let result_b = apply_inv_background_gray_map_inner(
&pixb,
&inv_map_b,
options.tile_width,
options.tile_height,
)?;
combine_rgb_channels(&result_r, &result_g, &result_b, pix.spp())
}
pub fn get_background_gray_map(
pix: &Pix,
_mask: Option<&Pix>,
tile_w: u32,
tile_h: u32,
fg_threshold: u32,
min_count: u32,
) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8 bpp",
actual: pix.depth().bits(),
});
}
get_background_gray_map_inner(pix, tile_w, tile_h, fg_threshold, min_count)
}
pub fn get_background_rgb_map(
pix: &Pix,
_mask: Option<&Pix>,
_pixg: Option<&Pix>,
tile_w: u32,
tile_h: u32,
fg_threshold: u32,
min_count: u32,
) -> FilterResult<(Pix, Pix, Pix)> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
let (pixr, pixg, pixb) = extract_rgb_channels(pix)?;
let map_r = get_background_gray_map_inner(&pixr, tile_w, tile_h, fg_threshold, min_count)?;
let map_g = get_background_gray_map_inner(&pixg, tile_w, tile_h, fg_threshold, min_count)?;
let map_b = get_background_gray_map_inner(&pixb, tile_w, tile_h, fg_threshold, min_count)?;
Ok((map_r, map_g, map_b))
}
pub fn fill_map_holes(pix: &Pix, nx: u32, ny: u32) -> FilterResult<Pix> {
fill_map_holes_inner(pix, nx, ny)
}
pub fn get_inv_background_map(
pix: &Pix,
bg_val: u32,
smooth_x: u32,
smooth_y: u32,
) -> FilterResult<Pix> {
get_inv_background_map_inner(pix, bg_val, smooth_x, smooth_y)
}
pub fn apply_inv_background_gray_map(
pix: &Pix,
inv_map: &Pix,
tile_w: u32,
tile_h: u32,
) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8 bpp",
actual: pix.depth().bits(),
});
}
apply_inv_background_gray_map_inner(pix, inv_map, tile_w, tile_h)
}
pub fn apply_inv_background_rgb_map(
pix: &Pix,
inv_map_r: &Pix,
inv_map_g: &Pix,
inv_map_b: &Pix,
tile_w: u32,
tile_h: u32,
) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
let (pixr, pixg, pixb) = extract_rgb_channels(pix)?;
let result_r = apply_inv_background_gray_map_inner(&pixr, inv_map_r, tile_w, tile_h)?;
let result_g = apply_inv_background_gray_map_inner(&pixg, inv_map_g, tile_w, tile_h)?;
let result_b = apply_inv_background_gray_map_inner(&pixb, inv_map_b, tile_w, tile_h)?;
combine_rgb_channels(&result_r, &result_g, &result_b, pix.spp())
}
pub fn background_norm_gray_array(pix: &Pix, options: &BackgroundNormOptions) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8 bpp",
actual: pix.depth().bits(),
});
}
let o = options;
let bg_map = get_background_gray_map_inner(
pix,
o.tile_width,
o.tile_height,
o.fg_threshold,
o.min_count,
)?;
get_inv_background_map_inner(&bg_map, o.bg_val, o.smooth_x, o.smooth_y)
}
pub fn background_norm_rgb_arrays(
pix: &Pix,
options: &BackgroundNormOptions,
) -> FilterResult<(Pix, Pix, Pix)> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
let (pixr, pixg, pixb) = extract_rgb_channels(pix)?;
let o = options;
let inv_r = {
let bg = get_background_gray_map_inner(
&pixr,
o.tile_width,
o.tile_height,
o.fg_threshold,
o.min_count,
)?;
get_inv_background_map_inner(&bg, o.bg_val, o.smooth_x, o.smooth_y)?
};
let inv_g = {
let bg = get_background_gray_map_inner(
&pixg,
o.tile_width,
o.tile_height,
o.fg_threshold,
o.min_count,
)?;
get_inv_background_map_inner(&bg, o.bg_val, o.smooth_x, o.smooth_y)?
};
let inv_b = {
let bg = get_background_gray_map_inner(
&pixb,
o.tile_width,
o.tile_height,
o.fg_threshold,
o.min_count,
)?;
get_inv_background_map_inner(&bg, o.bg_val, o.smooth_x, o.smooth_y)?
};
Ok((inv_r, inv_g, inv_b))
}
pub fn clean_background_to_white(
pix: &Pix,
_mask: Option<&Pix>,
_pixg: Option<&Pix>,
) -> FilterResult<Pix> {
let normalized = background_norm_simple(pix)?;
let w = normalized.width();
let h = normalized.height();
match normalized.depth() {
PixelDepth::Bit8 => {
let mut out = normalized.to_mut();
for y in 0..h {
for x in 0..w {
let val = out.get_pixel_unchecked(x, y);
if val >= 180 {
out.set_pixel_unchecked(x, y, 255);
}
}
}
Ok(out.into())
}
PixelDepth::Bit32 => {
let mut out = normalized.to_mut();
for y in 0..h {
for x in 0..w {
let pixel = out.get_pixel_unchecked(x, y);
let (r, g, b, _a) = pixel::extract_rgba(pixel);
let nr = if r >= 180 { 255 } else { r };
let ng = if g >= 180 { 255 } else { g };
let nb = if b >= 180 { 255 } else { b };
out.set_pixel_unchecked(x, y, pixel::compose_rgb(nr, ng, nb));
}
}
Ok(out.into())
}
_ => Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: pix.depth().bits(),
}),
}
}
fn get_background_gray_map_inner(
pix: &Pix,
tile_width: u32,
tile_height: u32,
fg_threshold: u32,
min_count: u32,
) -> FilterResult<Pix> {
let w = pix.width();
let h = pix.height();
let map_w = w.div_ceil(tile_width);
let map_h = h.div_ceil(tile_height);
let map_pix = Pix::new(map_w, map_h, PixelDepth::Bit8)?;
let mut map_mut = map_pix.try_into_mut().unwrap();
let nx = w / tile_width;
let ny = h / tile_height;
for ty in 0..ny {
for tx in 0..nx {
let tile_x = tx * tile_width;
let tile_y = ty * tile_height;
let mut sum: u32 = 0;
let mut count: u32 = 0;
for y in tile_y..(tile_y + tile_height) {
for x in tile_x..(tile_x + tile_width) {
let val = pix.get_pixel_unchecked(x, y);
if val >= fg_threshold {
sum += val;
count += 1;
}
}
}
if count >= min_count {
let avg = sum / count;
map_mut.set_pixel_unchecked(tx, ty, avg);
}
}
}
let map_pix = map_mut.into();
fill_map_holes_inner(&map_pix, nx, ny)
}
fn fill_map_holes_inner(pix: &Pix, valid_x: u32, valid_y: u32) -> FilterResult<Pix> {
let w = pix.width();
let h = pix.height();
let out_pix = pix.deep_clone();
let mut out_mut = out_pix.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let val = out_mut.get_pixel_unchecked(x, y);
if val == 0 {
if x > 0 {
let left = out_mut.get_pixel_unchecked(x - 1, y);
if left > 0 {
out_mut.set_pixel_unchecked(x, y, left);
continue;
}
}
if y > 0 {
let top = out_mut.get_pixel_unchecked(x, y - 1);
if top > 0 {
out_mut.set_pixel_unchecked(x, y, top);
}
}
}
}
}
for y in (0..h).rev() {
for x in (0..w).rev() {
let val = out_mut.get_pixel_unchecked(x, y);
if val == 0 {
if x + 1 < w {
let right = out_mut.get_pixel_unchecked(x + 1, y);
if right > 0 {
out_mut.set_pixel_unchecked(x, y, right);
continue;
}
}
if y + 1 < h {
let bottom = out_mut.get_pixel_unchecked(x, y + 1);
if bottom > 0 {
out_mut.set_pixel_unchecked(x, y, bottom);
}
}
}
}
}
for y in 0..h {
if valid_x < w {
let last_valid = if valid_x > 0 {
out_mut.get_pixel_unchecked(valid_x - 1, y)
} else {
128 };
for x in valid_x..w {
let val = out_mut.get_pixel_unchecked(x, y);
if val == 0 {
out_mut.set_pixel_unchecked(x, y, last_valid);
}
}
}
}
for x in 0..w {
if valid_y < h {
let last_valid = if valid_y > 0 {
out_mut.get_pixel_unchecked(x, valid_y - 1)
} else {
128 };
for y in valid_y..h {
let val = out_mut.get_pixel_unchecked(x, y);
if val == 0 {
out_mut.set_pixel_unchecked(x, y, last_valid);
}
}
}
}
Ok(out_mut.into())
}
fn get_inv_background_map_inner(
pix: &Pix,
bg_val: u32,
smooth_x: u32,
smooth_y: u32,
) -> FilterResult<Pix> {
let w = pix.width();
let h = pix.height();
let smoothed = if smooth_x > 0 || smooth_y > 0 {
block_convolve_gray(pix, smooth_x, smooth_y)?
} else {
pix.deep_clone()
};
let out_pix = Pix::new(w, h, PixelDepth::Bit32)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let val = smoothed.get_pixel_unchecked(x, y);
let factor = if val > 0 {
(256 * bg_val) / val
} else {
bg_val / 2 };
out_mut.set_pixel_unchecked(x, y, factor.min(65535));
}
}
Ok(out_mut.into())
}
fn block_convolve_gray(pix: &Pix, half_width_x: u32, half_width_y: u32) -> FilterResult<Pix> {
let w = pix.width();
let h = pix.height();
let out_pix = Pix::new(w, h, PixelDepth::Bit8)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
let kw = 2 * half_width_x + 1;
let kh = 2 * half_width_y + 1;
let kernel_size = kw * kh;
for y in 0..h {
for x in 0..w {
let mut sum: u32 = 0;
for ky in 0..kh {
for kx in 0..kw {
let sx =
(x as i32 + kx as i32 - half_width_x as i32).clamp(0, w as i32 - 1) as u32;
let sy =
(y as i32 + ky as i32 - half_width_y as i32).clamp(0, h as i32 - 1) as u32;
sum += pix.get_pixel_unchecked(sx, sy);
}
}
let avg = sum / kernel_size;
out_mut.set_pixel_unchecked(x, y, avg);
}
}
Ok(out_mut.into())
}
fn apply_inv_background_gray_map_inner(
pix: &Pix,
inv_map: &Pix,
tile_width: u32,
tile_height: u32,
) -> FilterResult<Pix> {
let w = pix.width();
let h = pix.height();
let map_w = inv_map.width();
let map_h = inv_map.height();
let out_pix = Pix::new(w, h, PixelDepth::Bit8)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
for ty in 0..map_h {
for tx in 0..map_w {
let factor = inv_map.get_pixel_unchecked(tx, ty);
let x_start = tx * tile_width;
let y_start = ty * tile_height;
let x_end = (x_start + tile_width).min(w);
let y_end = (y_start + tile_height).min(h);
for y in y_start..y_end {
for x in x_start..x_end {
let val = pix.get_pixel_unchecked(x, y);
let normalized = (val * factor) / 256;
out_mut.set_pixel_unchecked(x, y, normalized.min(255));
}
}
}
}
Ok(out_mut.into())
}
fn extract_rgb_channels(pix: &Pix) -> FilterResult<(Pix, Pix, Pix)> {
let w = pix.width();
let h = pix.height();
let pix_r = Pix::new(w, h, PixelDepth::Bit8)?;
let pix_g = Pix::new(w, h, PixelDepth::Bit8)?;
let pix_b = Pix::new(w, h, PixelDepth::Bit8)?;
let mut r_mut = pix_r.try_into_mut().unwrap();
let mut g_mut = pix_g.try_into_mut().unwrap();
let mut b_mut = pix_b.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let pixel = pix.get_pixel_unchecked(x, y);
let (r, g, b, _) = pixel::extract_rgba(pixel);
r_mut.set_pixel_unchecked(x, y, r as u32);
g_mut.set_pixel_unchecked(x, y, g as u32);
b_mut.set_pixel_unchecked(x, y, b as u32);
}
}
Ok((r_mut.into(), g_mut.into(), b_mut.into()))
}
fn combine_rgb_channels(pix_r: &Pix, pix_g: &Pix, pix_b: &Pix, spp: u32) -> FilterResult<Pix> {
let w = pix_r.width();
let h = pix_r.height();
let out_pix = Pix::new(w, h, PixelDepth::Bit32)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
out_mut.set_spp(spp);
for y in 0..h {
for x in 0..w {
let r = pix_r.get_pixel_unchecked(x, y) as u8;
let g = pix_g.get_pixel_unchecked(x, y) as u8;
let b = pix_b.get_pixel_unchecked(x, y) as u8;
let pixel = pixel::compose_rgb(r, g, b);
out_mut.set_pixel_unchecked(x, y, pixel);
}
}
Ok(out_mut.into())
}
pub fn contrast_norm_simple(pix: &Pix) -> FilterResult<Pix> {
contrast_norm(pix, &ContrastNormOptions::default())
}
pub fn contrast_norm(pix: &Pix, options: &ContrastNormOptions) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8 bpp",
actual: pix.depth().bits(),
});
}
if options.tile_width < 5 || options.tile_height < 5 {
return Err(FilterError::InvalidParameters(
"tile dimensions must be >= 5".to_string(),
));
}
if options.smooth_x > 8 || options.smooth_y > 8 {
return Err(FilterError::InvalidParameters(
"smooth parameters must be <= 8".to_string(),
));
}
let (pix_min, pix_max) = min_max_tiles(
pix,
options.tile_width,
options.tile_height,
options.min_diff,
options.smooth_x,
options.smooth_y,
)?;
linear_trc_tiled(
pix,
options.tile_width,
options.tile_height,
&pix_min,
&pix_max,
)
}
fn min_max_tiles(
pix: &Pix,
tile_width: u32,
tile_height: u32,
min_diff: u32,
smooth_x: u32,
smooth_y: u32,
) -> FilterResult<(Pix, Pix)> {
let w = pix.width();
let h = pix.height();
let map_w = w.div_ceil(tile_width);
let map_h = h.div_ceil(tile_height);
let pix_min = Pix::new(map_w, map_h, PixelDepth::Bit8)?;
let pix_max = Pix::new(map_w, map_h, PixelDepth::Bit8)?;
let mut min_mut = pix_min.try_into_mut().unwrap();
let mut max_mut = pix_max.try_into_mut().unwrap();
let nx = w / tile_width;
let ny = h / tile_height;
for ty in 0..ny {
for tx in 0..nx {
let tile_x = tx * tile_width;
let tile_y = ty * tile_height;
let mut min_val = 255u32;
let mut max_val = 0u32;
for y in tile_y..(tile_y + tile_height) {
for x in tile_x..(tile_x + tile_width) {
let val = pix.get_pixel_unchecked(x, y);
min_val = min_val.min(val);
max_val = max_val.max(val);
}
}
min_mut.set_pixel_unchecked(tx, ty, min_val.saturating_add(1).min(255));
max_mut.set_pixel_unchecked(tx, ty, max_val.saturating_add(1).min(255));
}
}
for ty in 0..map_h {
for tx in nx..map_w {
let src_x = if nx > 0 { nx - 1 } else { 0 };
let min_val = min_mut.get_pixel_unchecked(src_x, ty);
let max_val = max_mut.get_pixel_unchecked(src_x, ty);
min_mut.set_pixel_unchecked(tx, ty, min_val);
max_mut.set_pixel_unchecked(tx, ty, max_val);
}
}
for tx in 0..map_w {
for ty in ny..map_h {
let src_y = if ny > 0 { ny - 1 } else { 0 };
let min_val = min_mut.get_pixel_unchecked(tx, src_y);
let max_val = max_mut.get_pixel_unchecked(tx, src_y);
min_mut.set_pixel_unchecked(tx, ty, min_val);
max_mut.set_pixel_unchecked(tx, ty, max_val);
}
}
let pix_min: Pix = min_mut.into();
let pix_max: Pix = max_mut.into();
let (pix_min, pix_max) = set_low_contrast(pix_min, pix_max, min_diff)?;
let pix_min = fill_map_holes_inner(&pix_min, map_w, map_h)?;
let pix_max = fill_map_holes_inner(&pix_max, map_w, map_h)?;
let pix_min = if smooth_x > 0 || smooth_y > 0 {
let sx = smooth_x.min((map_w - 1) / 2);
let sy = smooth_y.min((map_h - 1) / 2);
block_convolve_gray(&pix_min, sx, sy)?
} else {
pix_min
};
let pix_max = if smooth_x > 0 || smooth_y > 0 {
let sx = smooth_x.min((map_w - 1) / 2);
let sy = smooth_y.min((map_h - 1) / 2);
block_convolve_gray(&pix_max, sx, sy)?
} else {
pix_max
};
Ok((pix_min, pix_max))
}
fn set_low_contrast(pix_min: Pix, pix_max: Pix, min_diff: u32) -> FilterResult<(Pix, Pix)> {
let w = pix_min.width();
let h = pix_min.height();
let out_min = pix_min.deep_clone();
let out_max = pix_max.deep_clone();
let mut min_mut = out_min.try_into_mut().unwrap();
let mut max_mut = out_max.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let min_val = pix_min.get_pixel_unchecked(x, y);
let max_val = pix_max.get_pixel_unchecked(x, y);
let actual_min = min_val.saturating_sub(1);
let actual_max = max_val.saturating_sub(1);
if actual_max.saturating_sub(actual_min) < min_diff {
min_mut.set_pixel_unchecked(x, y, 0);
max_mut.set_pixel_unchecked(x, y, 0);
}
}
}
Ok((min_mut.into(), max_mut.into()))
}
fn linear_trc_tiled(
pix: &Pix,
tile_width: u32,
tile_height: u32,
pix_min: &Pix,
pix_max: &Pix,
) -> FilterResult<Pix> {
let w = pix.width();
let h = pix.height();
let map_w = pix_min.width();
let map_h = pix_min.height();
let out_pix = pix.deep_clone();
let mut out_mut = out_pix.try_into_mut().unwrap();
let mut lut_cache: [Option<[u8; 256]>; 256] = [None; 256];
for ty in 0..map_h {
for tx in 0..map_w {
let min_val = pix_min.get_pixel_unchecked(tx, ty).saturating_sub(1);
let max_val = pix_max.get_pixel_unchecked(tx, ty).saturating_sub(1);
if max_val <= min_val {
continue; }
let diff = (max_val - min_val) as usize;
let lut = if let Some(existing) = &lut_cache[diff] {
existing
} else {
let mut new_lut = [0u8; 256];
let factor = 255.0 / diff as f32;
for (i, lut_val) in new_lut.iter_mut().enumerate().take(diff + 1) {
*lut_val = ((factor * i as f32) + 0.5).min(255.0) as u8;
}
for lut_val in new_lut.iter_mut().skip(diff + 1) {
*lut_val = 255;
}
lut_cache[diff] = Some(new_lut);
lut_cache[diff].as_ref().unwrap()
};
let x_start = tx * tile_width;
let y_start = ty * tile_height;
let x_end = (x_start + tile_width).min(w);
let y_end = (y_start + tile_height).min(h);
for y in y_start..y_end {
for x in x_start..x_end {
let val = pix.get_pixel_unchecked(x, y);
let shifted = val.saturating_sub(min_val) as usize;
let mapped = lut[shifted.min(255)];
out_mut.set_pixel_unchecked(x, y, mapped as u32);
}
}
}
}
Ok(out_mut.into())
}
pub fn extend_by_replication(pix: &Pix, extend_x: u32, extend_y: u32) -> FilterResult<Pix> {
let w = pix.width();
let h = pix.height();
let depth = pix.depth();
if extend_x == 0 && extend_y == 0 {
return Ok(pix.deep_clone());
}
let double_x = extend_x
.checked_mul(2)
.ok_or_else(|| FilterError::InvalidParameters("extend_x overflow".into()))?;
let double_y = extend_y
.checked_mul(2)
.ok_or_else(|| FilterError::InvalidParameters("extend_y overflow".into()))?;
let new_w = w
.checked_add(double_x)
.ok_or_else(|| FilterError::InvalidParameters("resulting width overflow".into()))?;
let new_h = h
.checked_add(double_y)
.ok_or_else(|| FilterError::InvalidParameters("resulting height overflow".into()))?;
let out_pix = Pix::new(new_w, new_h, depth)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
out_mut.set_spp(pix.spp());
if let Some(cmap) = pix.colormap() {
let _ = out_mut.set_colormap(Some(cmap.clone()));
}
for y in 0..h {
for x in 0..w {
let val = pix.get_pixel_unchecked(x, y);
out_mut.set_pixel_unchecked(x + extend_x, y + extend_y, val);
}
}
for y in 0..h {
let left_val = pix.get_pixel_unchecked(0, y);
let right_val = pix.get_pixel_unchecked(w - 1, y);
for ex in 0..extend_x {
out_mut.set_pixel_unchecked(ex, y + extend_y, left_val);
out_mut.set_pixel_unchecked(extend_x + w + ex, y + extend_y, right_val);
}
}
for x in 0..new_w {
let top_val = out_mut.get_pixel_unchecked(x, extend_y);
let bottom_val = out_mut.get_pixel_unchecked(x, extend_y + h - 1);
for ey in 0..extend_y {
out_mut.set_pixel_unchecked(x, ey, top_val);
out_mut.set_pixel_unchecked(x, extend_y + h + ey, bottom_val);
}
}
Ok(out_mut.into())
}
pub fn get_background_gray_map_morph(
pix: &Pix,
_mask: Option<&Pix>,
reduction: u32,
size: u32,
) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8bpp",
actual: pix.depth().bits(),
});
}
if pix.has_colormap() {
return Err(FilterError::InvalidParameters(
"colormapped images not supported".into(),
));
}
if !(2..=16).contains(&reduction) {
return Err(FilterError::InvalidParameters(
"reduction must be between 2 and 16".into(),
));
}
let scale = 1.0 / reduction as f32;
let pix1 = crate::transform::scale_by_sampling(pix, scale, scale)?;
let pix2 = crate::morph::close_gray(&pix1, size, size)?;
let pix3 = extend_by_replication(&pix2, 1, 1)?;
let nx = pix.width() / reduction;
let ny = pix.height() / reduction;
let filled = fill_map_holes_inner(&pix3, nx, ny)?;
Ok(filled)
}
pub fn get_background_rgb_map_morph(
pix: &Pix,
_mask: Option<&Pix>,
reduction: u32,
size: u32,
) -> FilterResult<(Pix, Pix, Pix)> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32bpp",
actual: pix.depth().bits(),
});
}
if !(2..=16).contains(&reduction) {
return Err(FilterError::InvalidParameters(
"reduction must be between 2 and 16".into(),
));
}
let nx = pix.width() / reduction;
let ny = pix.height() / reduction;
let map_r =
get_background_single_channel_morph(pix, reduction, size, nx, ny, pixel::RED_SHIFT)?;
let map_g =
get_background_single_channel_morph(pix, reduction, size, nx, ny, pixel::GREEN_SHIFT)?;
let map_b =
get_background_single_channel_morph(pix, reduction, size, nx, ny, pixel::BLUE_SHIFT)?;
Ok((map_r, map_g, map_b))
}
fn get_background_single_channel_morph(
pix: &Pix,
reduction: u32,
size: u32,
nx: u32,
ny: u32,
shift: u32,
) -> FilterResult<Pix> {
let pix1 = scale_rgb_to_gray_fast(pix, reduction, shift)?;
let pix2 = crate::morph::close_gray(&pix1, size, size)?;
let pix3 = extend_by_replication(&pix2, 1, 1)?;
fill_map_holes_inner(&pix3, nx, ny)
}
fn scale_rgb_to_gray_fast(pix: &Pix, factor: u32, shift: u32) -> FilterResult<Pix> {
let ws = pix.width();
let hs = pix.height();
let wd = ws / factor;
let hd = hs / factor;
if wd == 0 || hd == 0 {
return Err(FilterError::InvalidParameters(
"reduction factor too large for image size".into(),
));
}
let out = Pix::new(wd, hd, PixelDepth::Bit8)?;
let mut out_mut = out.try_into_mut().unwrap();
for y in 0..hd {
let src_y = y * factor;
for x in 0..wd {
let src_x = x * factor;
let pixel = pix.get_pixel_unchecked(src_x, src_y);
let val = (pixel >> shift) & 0xff;
out_mut.set_pixel_unchecked(x, y, val);
}
}
Ok(out_mut.into())
}
pub fn background_norm_morph(
pix: &Pix,
mask: Option<&Pix>,
reduction: u32,
size: u32,
bgval: u32,
) -> FilterResult<Pix> {
let d = pix.depth();
if d != PixelDepth::Bit8 && d != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: d.bits(),
});
}
if !(2..=16).contains(&reduction) {
return Err(FilterError::InvalidParameters(
"reduction must be between 2 and 16".into(),
));
}
if d == PixelDepth::Bit8 {
let bg_map = get_background_gray_map_morph(pix, mask, reduction, size)?;
let inv_map = get_inv_background_map_inner(&bg_map, bgval, 0, 0)?;
apply_inv_background_gray_map_inner(pix, &inv_map, reduction, reduction)
} else {
let (map_r, map_g, map_b) = get_background_rgb_map_morph(pix, mask, reduction, size)?;
let inv_r = get_inv_background_map_inner(&map_r, bgval, 0, 0)?;
let inv_g = get_inv_background_map_inner(&map_g, bgval, 0, 0)?;
let inv_b = get_inv_background_map_inner(&map_b, bgval, 0, 0)?;
apply_inv_background_rgb_map(pix, &inv_r, &inv_g, &inv_b, reduction, reduction)
}
}
pub fn background_norm_gray_array_morph(
pix: &Pix,
mask: Option<&Pix>,
reduction: u32,
size: u32,
bgval: u32,
) -> FilterResult<Pix> {
let bg_map = get_background_gray_map_morph(pix, mask, reduction, size)?;
get_inv_background_map_inner(&bg_map, bgval, 0, 0)
}
pub fn background_norm_rgb_arrays_morph(
pix: &Pix,
mask: Option<&Pix>,
reduction: u32,
size: u32,
bgval: u32,
) -> FilterResult<(Pix, Pix, Pix)> {
let (map_r, map_g, map_b) = get_background_rgb_map_morph(pix, mask, reduction, size)?;
let inv_r = get_inv_background_map_inner(&map_r, bgval, 0, 0)?;
let inv_g = get_inv_background_map_inner(&map_g, bgval, 0, 0)?;
let inv_b = get_inv_background_map_inner(&map_b, bgval, 0, 0)?;
Ok((inv_r, inv_g, inv_b))
}
pub fn apply_variable_gray_map(pix: &Pix, pixg: &Pix, target: u32) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8bpp",
actual: pix.depth().bits(),
});
}
if pixg.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8bpp",
actual: pixg.depth().bits(),
});
}
let w = pix.width();
let h = pix.height();
if pixg.width() != w || pixg.height() != h {
return Err(FilterError::InvalidParameters(
"pix and pixg must have the same dimensions".into(),
));
}
let target_f = target as f32;
let use_lut = (w as u64) * (h as u64) > 100_000;
let lut = if use_lut {
let mut table = vec![0u8; 0x10000];
for s in 0..256u32 {
for g in 0..256u32 {
let fval = (s as f32) * target_f / (g as f32 + 0.5);
table[((s << 8) | g) as usize] = (fval + 0.5).min(255.0) as u8;
}
}
Some(table)
} else {
None
};
let out = Pix::new(w, h, PixelDepth::Bit8)?;
let mut out_mut = out.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let vals = pix.get_pixel_unchecked(x, y);
let valg = pixg.get_pixel_unchecked(x, y);
let vald = if let Some(ref lut) = lut {
lut[((vals << 8) | valg) as usize]
} else {
let fval = (vals as f32) * target_f / (valg as f32 + 0.5);
(fval + 0.5).min(255.0) as u8
};
out_mut.set_pixel_unchecked(x, y, vald as u32);
}
}
Ok(out_mut.into())
}
pub fn global_norm_rgb(
pix: &Pix,
rval: u32,
gval: u32,
bval: u32,
mapval: u32,
) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32bpp",
actual: pix.depth().bits(),
});
}
let mapval = if mapval == 0 { 255 } else { mapval };
let r_max = (255 * rval / mapval).max(1);
let g_max = (255 * gval / mapval).max(1);
let b_max = (255 * bval / mapval).max(1);
let r_lut = crate::filter::gamma_trc(1.0, 0, r_max as i32)?;
let g_lut = crate::filter::gamma_trc(1.0, 0, g_max as i32)?;
let b_lut = crate::filter::gamma_trc(1.0, 0, b_max as i32)?;
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_spp(pix.spp());
for y in 0..h {
for x in 0..w {
let pixel = pix.get_pixel_unchecked(x, y);
let (r, g, b, _) = pixel::extract_rgba(pixel);
let nr = r_lut[r as usize];
let ng = g_lut[g as usize];
let nb = b_lut[b as usize];
out_mut.set_pixel_unchecked(x, y, pixel::compose_rgb(nr, ng, nb));
}
}
Ok(out_mut.into())
}
pub fn convert_to_8_min_max(pix: &Pix) -> FilterResult<Pix> {
use crate::core::pix::MinMaxType;
match pix.depth() {
PixelDepth::Bit32 => Ok(pix.convert_rgb_to_gray_min_max(MinMaxType::Min)?),
_ => Ok(pix.convert_to_8()?),
}
}
pub fn global_norm_no_sat_rgb(
pix: &Pix,
rval: u32,
gval: u32,
bval: u32,
factor: u32,
rank: f32,
) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32bpp",
actual: pix.depth().bits(),
});
}
if factor < 1 {
return Err(FilterError::InvalidParameters("factor must be >= 1".into()));
}
if !(0.0..=1.0).contains(&rank) {
return Err(FilterError::InvalidParameters(
"rank must be in [0.0, 1.0]".into(),
));
}
if rval == 0 || gval == 0 || bval == 0 {
return Err(FilterError::InvalidParameters(
"rval, gval, bval must be > 0".into(),
));
}
let (rankrval, rankgval, rankbval) = pix
.rank_value_masked_rgb(None, 0, 0, factor, rank)
.map_err(|e| FilterError::InvalidParameters(e.to_string()))?;
let rfract = rankrval / rval as f32;
let gfract = rankgval / gval as f32;
let bfract = rankbval / bval as f32;
let maxfract = rfract.max(gfract).max(bfract);
if maxfract <= 0.0 {
return Ok(pix.deep_clone());
}
let mapval = (255.0 / maxfract) as u32;
global_norm_rgb(pix, rval, gval, bval, mapval)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EdgeFilterType {
Sobel,
}
#[derive(Debug, Clone)]
pub struct FlexNormOptions {
pub tile_width: u32,
pub tile_height: u32,
pub smooth_x: u32,
pub smooth_y: u32,
pub delta: u32,
}
impl Default for FlexNormOptions {
fn default() -> Self {
Self {
tile_width: 5,
tile_height: 5,
smooth_x: 2,
smooth_y: 2,
delta: 0,
}
}
}
pub fn threshold_spread_norm(
pix: &Pix,
_filter_type: EdgeFilterType,
edge_thresh: u8,
smooth_x: u32,
smooth_y: u32,
thresh_norm: f32,
) -> FilterResult<Pix> {
use crate::region::{conncomp::ConnectivityType, seedfill::seedspread};
use crate::filter::{
block_conv::blockconv,
edge::{EdgeOrientation, sobel_edge},
enhance::gamma_trc_pix,
};
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8 bpp",
actual: pix.depth().bits(),
});
}
let w = pix.width();
let h = pix.height();
let thresh = edge_thresh as u32;
let edge_pix = sobel_edge(pix, EdgeOrientation::Vertical)?;
let seed = Pix::new(w, h, PixelDepth::Bit8)?;
let mut seed_mut = seed.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let edge_val = edge_pix.get_pixel_unchecked(x, y);
if edge_val <= thresh {
seed_mut.set_pixel_unchecked(x, y, pix.get_pixel_unchecked(x, y));
}
}
}
let seed: Pix = seed_mut.into();
let spread = seedspread(&seed, ConnectivityType::FourWay)
.map_err(|e| FilterError::InvalidParameters(e.to_string()))?;
let smoothed = blockconv(&spread, smooth_x, smooth_y)?;
let thresh_map = if (thresh_norm - 1.0).abs() < 1e-6 {
smoothed
} else {
gamma_trc_pix(&smoothed, thresh_norm, 0, 255)?
};
apply_variable_gray_map(pix, &thresh_map, 128)
}
pub fn background_norm_flex(pix: &Pix, options: &FlexNormOptions) -> FilterResult<Pix> {
use crate::transform::scale::scale_smooth;
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8 bpp",
actual: pix.depth().bits(),
});
}
let sx = options.tile_width;
let sy = options.tile_height;
let smooth_x = options.smooth_x;
let smooth_y = options.smooth_y;
if sx < 3 || sy < 3 {
return Err(FilterError::InvalidParameters(
"tile_width and tile_height must be >= 3".into(),
));
}
if sx > 10 || sy > 10 {
return Err(FilterError::InvalidParameters(
"tile_width and tile_height must be <= 10".into(),
));
}
if smooth_x < 1 || smooth_y < 1 {
return Err(FilterError::InvalidParameters(
"smooth_x and smooth_y must be >= 1".into(),
));
}
if smooth_x > 3 || smooth_y > 3 {
return Err(FilterError::InvalidParameters(
"smooth_x and smooth_y must be <= 3".into(),
));
}
if options.delta > 0 {
return Err(FilterError::InvalidParameters(
"delta > 0 (basin filling) is not yet supported".into(),
));
}
let scale_x = 1.0 / sx as f32;
let scale_y = 1.0 / sy as f32;
let scaled = scale_smooth(pix, scale_x, scale_y)
.map_err(|e| FilterError::InvalidParameters(e.to_string()))?;
let extended = extend_by_replication(&scaled, 1, 1)?;
let inv_map = get_inv_background_map(&extended, 200, smooth_x, smooth_y)?;
apply_inv_background_gray_map(pix, &inv_map, sx, sy)
}
pub fn smooth_connected_regions(pix: &Pix, mask: Option<&Pix>, factor: u32) -> FilterResult<Pix> {
use crate::core::PixelStatType;
use crate::region::conncomp::{ConnectivityType, conncomp_pixa};
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8 bpp",
actual: pix.depth().bits(),
});
}
if factor == 0 {
return Err(FilterError::InvalidParameters("factor must be >= 1".into()));
}
let mask = match mask {
None => return Ok(pix.deep_clone()),
Some(m) => m,
};
if mask.depth() != PixelDepth::Bit1 {
return Err(FilterError::InvalidParameters("mask must be 1 bpp".into()));
}
let (boxa, pixa) = conncomp_pixa(mask, ConnectivityType::EightWay)
.map_err(|e| FilterError::InvalidParameters(e.to_string()))?;
let n = boxa.len();
if n == 0 {
return Ok(pix.deep_clone());
}
let result = pix.deep_clone();
let mut result_mut = result.try_into_mut().unwrap_or_else(|p| p.to_mut());
for i in 0..n {
let comp_mask = match pixa.get(i) {
Some(m) => m,
None => continue,
};
let b = match boxa.get(i) {
Some(b) => b,
None => continue,
};
let avg = pix
.average_masked(Some(comp_mask), b.x, b.y, factor, PixelStatType::MeanAbsVal)
.map_err(|e| FilterError::InvalidParameters(e.to_string()))?;
let avg_val = avg.round() as u32;
result_mut
.paint_through_mask(comp_mask, b.x, b.y, avg_val)
.map_err(|e| FilterError::InvalidParameters(e.to_string()))?;
}
Ok(result_mut.into())
}
pub fn background_norm_to_1_min_max(pix: &Pix, contrast: u32) -> FilterResult<Pix> {
use crate::filter::enhance::gamma_trc_pix;
if !(1..=10).contains(&contrast) {
return Err(FilterError::InvalidParameters(
"contrast must be between 1 and 10 inclusive".into(),
));
}
let gray = convert_to_8_min_max(pix)?;
let normalized = background_norm_simple(&gray)?;
let (gamma, minval, maxval) = match contrast {
1 => (2.0f32, 50i32, 200i32),
2 => (1.8, 60, 200),
3 => (1.6, 70, 200),
4 => (1.4, 80, 200),
5 => (1.2, 90, 200),
6 => (1.0, 100, 200),
7 => (0.85, 110, 200),
8 => (0.7, 120, 200),
9 => (0.6, 130, 200),
_ => (0.5, 140, 200), };
let contrasted = gamma_trc_pix(&normalized, gamma, minval, maxval)?;
threshold_8bpp_to_1bpp(&contrasted, 180)
}
fn threshold_8bpp_to_1bpp(pix: &Pix, thresh: u32) -> FilterResult<Pix> {
let w = pix.width();
let h = pix.height();
let out = Pix::new(w, h, PixelDepth::Bit1)?;
let mut out_mut = out.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let val = pix.get_pixel_unchecked(x, y);
out_mut.set_pixel_unchecked(x, y, if val < thresh { 1 } else { 0 });
}
}
Ok(out_mut.into())
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_gray_image() -> Pix {
let pix = Pix::new(50, 50, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..50 {
for x in 0..50 {
let bg = 100 + x * 2;
let val = if x > 15 && x < 35 && y > 15 && y < 35 {
bg / 2
} else {
bg
};
pix_mut.set_pixel_unchecked(x, y, val.min(255));
}
}
pix_mut.into()
}
fn create_test_color_image() -> Pix {
let pix = Pix::new(50, 50, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..50 {
for x in 0..50 {
let r = (100 + x * 2).min(255) as u8;
let g = (150 + y).min(255) as u8;
let b = 180u8;
let pixel = pixel::compose_rgb(r, g, b);
pix_mut.set_pixel_unchecked(x, y, pixel);
}
}
pix_mut.into()
}
fn create_low_contrast_image() -> Pix {
let pix = Pix::new(40, 40, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..40 {
for x in 0..40 {
let val = 100 + ((x + y) % 20);
pix_mut.set_pixel_unchecked(x, y, val);
}
}
pix_mut.into()
}
#[test]
fn test_background_norm_simple_gray() {
let pix = create_test_gray_image();
let result = background_norm_simple(&pix).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit8);
}
#[test]
fn test_background_norm_simple_color() {
let pix = create_test_color_image();
let result = background_norm_simple(&pix).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit32);
}
#[test]
fn test_background_norm_custom_options() {
let pix = create_test_gray_image();
let options = BackgroundNormOptions {
tile_width: 8,
tile_height: 8,
fg_threshold: 50,
min_count: 20,
bg_val: 180,
smooth_x: 1,
smooth_y: 1,
};
let result = background_norm(&pix, &options).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
}
#[test]
fn test_background_norm_invalid_tile_size() {
let pix = create_test_gray_image();
let options = BackgroundNormOptions {
tile_width: 2, ..Default::default()
};
assert!(background_norm(&pix, &options).is_err());
}
#[test]
fn test_background_norm_invalid_bg_val() {
let pix = create_test_gray_image();
let options = BackgroundNormOptions {
bg_val: 50, ..Default::default()
};
assert!(background_norm(&pix, &options).is_err());
}
#[test]
fn test_contrast_norm_simple() {
let pix = create_test_gray_image();
let result = contrast_norm_simple(&pix).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit8);
}
#[test]
fn test_contrast_norm_custom_options() {
let pix = create_test_gray_image();
let options = ContrastNormOptions {
tile_width: 10,
tile_height: 10,
min_diff: 30,
smooth_x: 1,
smooth_y: 1,
};
let result = contrast_norm(&pix, &options).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
}
#[test]
fn test_contrast_norm_invalid_tile_size() {
let pix = create_test_gray_image();
let options = ContrastNormOptions {
tile_width: 3, ..Default::default()
};
assert!(contrast_norm(&pix, &options).is_err());
}
#[test]
fn test_contrast_norm_invalid_smooth() {
let pix = create_test_gray_image();
let options = ContrastNormOptions {
smooth_x: 10, ..Default::default()
};
assert!(contrast_norm(&pix, &options).is_err());
}
#[test]
fn test_contrast_norm_color_not_supported() {
let pix = create_test_color_image();
let result = contrast_norm_simple(&pix);
assert!(result.is_err());
}
#[test]
fn test_contrast_norm_low_contrast_image() {
let pix = create_low_contrast_image();
let result = contrast_norm_simple(&pix).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
}
#[test]
fn test_block_convolve_gray() {
let pix = Pix::new(20, 20, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..20 {
for x in 0..20 {
pix_mut.set_pixel_unchecked(x, y, 128);
}
}
let pix = pix_mut.into();
let result = block_convolve_gray(&pix, 2, 2).unwrap();
for y in 0..20 {
for x in 0..20 {
let val = result.get_pixel_unchecked(x, y);
assert!((127..=129).contains(&val), "Expected ~128, got {}", val);
}
}
}
#[test]
fn test_fill_map_holes() {
let pix = Pix::new(5, 5, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
pix_mut.set_pixel_unchecked(0, 0, 100);
pix_mut.set_pixel_unchecked(4, 4, 200);
let pix = pix_mut.into();
let filled = fill_map_holes_inner(&pix, 5, 5).unwrap();
for y in 0..5 {
for x in 0..5 {
let val = filled.get_pixel_unchecked(x, y);
assert!(val > 0, "Hole at ({}, {}) not filled", x, y);
}
}
}
#[test]
fn test_background_norm_to_1_min_max_basic() {
let pix = create_test_gray_image(); let result = background_norm_to_1_min_max(&pix, 1).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit1);
}
#[test]
fn test_background_norm_to_1_min_max_contrast_range() {
let pix = create_test_gray_image();
for contrast in 1..=10 {
let result = background_norm_to_1_min_max(&pix, contrast).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit1);
}
}
#[test]
fn test_background_norm_to_1_min_max_invalid_contrast() {
let pix = create_test_gray_image();
assert!(background_norm_to_1_min_max(&pix, 0).is_err());
assert!(background_norm_to_1_min_max(&pix, 11).is_err());
}
#[test]
fn test_background_norm_flex_basic() {
let pix = create_test_gray_image(); let opts = FlexNormOptions {
tile_width: 5,
tile_height: 5,
smooth_x: 1,
smooth_y: 1,
delta: 0,
};
let result = background_norm_flex(&pix, &opts).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit8);
}
#[test]
fn test_background_norm_flex_invalid_tile_size() {
let pix = create_test_gray_image();
let opts = FlexNormOptions {
tile_width: 2,
tile_height: 5,
smooth_x: 1,
smooth_y: 1,
delta: 0,
};
assert!(background_norm_flex(&pix, &opts).is_err());
}
#[test]
fn test_threshold_spread_norm_basic() {
let pix = create_test_gray_image(); let result = threshold_spread_norm(&pix, EdgeFilterType::Sobel, 18, 4, 4, 1.0).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit8);
}
#[test]
fn test_smooth_connected_regions_no_mask() {
let pix = create_test_gray_image(); let result = smooth_connected_regions(&pix, None, 1).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit8);
}
#[test]
fn test_smooth_connected_regions_with_mask() {
let pix = create_test_gray_image(); let mask = Pix::new(50, 50, PixelDepth::Bit1).unwrap();
let mut mask_mut = mask.try_into_mut().unwrap();
for y in 10..20 {
for x in 10..20 {
mask_mut.set_pixel_unchecked(x, y, 1);
}
}
let mask = mask_mut.into();
let result = smooth_connected_regions(&pix, Some(&mask), 1).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
let val_ref = result.get_pixel_unchecked(10, 10);
for y in 10..20 {
for x in 10..20 {
assert_eq!(result.get_pixel_unchecked(x, y), val_ref);
}
}
}
fn create_test_color_image_for_norm() -> Pix {
let pix = Pix::new(20, 20, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..20u32 {
for x in 0..20u32 {
let r: u8 = 180;
let g: u8 = 120;
let b: u8 = 90;
pm.set_pixel_unchecked(x, y, crate::core::pixel::compose_rgb(r, g, b));
}
}
pm.into()
}
#[test]
fn test_global_norm_no_sat_rgb_basic() {
let pix = create_test_color_image_for_norm();
let result = global_norm_no_sat_rgb(&pix, 180, 120, 90, 1, 1.0).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit32);
let (r, g, b) = crate::core::pixel::extract_rgb(result.get_pixel_unchecked(10, 10));
assert!(r >= 180 && g >= 120 && b >= 90 && (r > 180 || g > 120 || b > 90));
}
#[test]
fn test_global_norm_no_sat_rgb_invalid_params() {
let pix = create_test_color_image_for_norm();
assert!(global_norm_no_sat_rgb(&pix, 180, 120, 90, 0, 0.5).is_err());
assert!(global_norm_no_sat_rgb(&pix, 180, 120, 90, 1, 1.5).is_err());
assert!(global_norm_no_sat_rgb(&pix, 0, 120, 90, 1, 0.5).is_err());
let gray = Pix::new(10, 10, PixelDepth::Bit8).unwrap();
assert!(global_norm_no_sat_rgb(&gray, 100, 100, 100, 1, 0.5).is_err());
}
}