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 pix.colormap().is_some() {
let decoded = pix.remove_colormap(crate::core::pix::RemoveColormapTarget::ToGrayscale)?;
return background_norm(&decoded, options);
}
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 (bg_map_r, bg_map_g, bg_map_b) = get_background_rgb_map_inner(
pix,
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 (pixr, pixg, pixb) = extract_rgb_channels(pix)?;
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.colormap().is_some() {
let decoded = pix.remove_colormap(crate::core::pix::RemoveColormapTarget::ToGrayscale)?;
return get_background_gray_map(&decoded, mask, tile_w, tile_h, fg_threshold, min_count);
}
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8 bpp",
actual: pix.depth().bits(),
});
}
if fg_threshold > 255 {
return Err(FilterError::InvalidParameters(format!(
"fg_threshold must be in 0..=255 (got {fg_threshold})"
)));
}
let _ = mask;
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)> {
let _ = mask;
let _ = pixg;
get_background_rgb_map_inner(pix, tile_w, tile_h, fg_threshold, min_count)
}
pub fn get_foreground_gray_map(
pix: &Pix,
mask: Option<&Pix>,
sx: u32,
sy: u32,
thresh: u32,
) -> FilterResult<Pix> {
use crate::color::threshold::threshold_to_binary;
use crate::filter::block_conv::blockconv;
use crate::filter::rank::{MinMaxOp, scale_gray_min_max};
use crate::transform::binreduce::reduce_rank_binary_2;
use crate::transform::scale::{expand_replicate, scale_by_sampling};
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8-bpp grayscale",
actual: pix.depth().bits(),
});
}
if let Some(m) = mask
&& m.depth() != PixelDepth::Bit1
{
return Err(FilterError::InvalidParameters(
"mask must be 1 bpp".to_string(),
));
}
if sx < 2 || sy < 2 {
return Err(FilterError::InvalidParameters(
"sx and sy must be >= 2".to_string(),
));
}
let w = pix.width();
let h = pix.height();
let wd = w.div_ceil(sx);
let hd = h.div_ceil(sy);
let fg_pixels;
if let Some(m) = mask {
let inverted = m.invert();
if inverted.is_zero() {
return Ok(Pix::new(wd, hd, PixelDepth::Bit8)?);
}
fg_pixels = !m.is_zero();
} else {
fg_pixels = false;
}
let mut pixs2 = scale_by_sampling(pix, 0.5, 0.5)?;
if let Some(m) = mask
&& fg_pixels
{
let pixim2 = reduce_rank_binary_2(m, 1)?;
let mut pixs2_mut = pixs2.try_into_mut().unwrap();
pixs2_mut.paint_through_mask(&pixim2, 0, 0, 255)?;
pixs2 = pixs2_mut.into();
}
let pixt1 = scale_gray_min_max(&pixs2, sx, sy, MinMaxOp::Min)?;
if thresh > u8::MAX as u32 {
return Err(FilterError::InvalidParameters(format!(
"thresh value {} exceeds maximum of 255",
thresh
)));
}
let pixb = threshold_to_binary(&pixt1, thresh as u8)
.map_err(|e| FilterError::InvalidParameters(e.to_string()))?;
let pixb_inv = pixb.invert();
let pixt1_mut_pix: Pix = {
let mut pixt1_mut = pixt1.try_into_mut().unwrap();
pixt1_mut.paint_through_mask(&pixb_inv, 0, 0, 255)?;
pixt1_mut.into()
};
let pixt2 = expand_replicate(&pixt1_mut_pix, 2)?;
let valid_x = (w / sx).min(pixt2.width());
let valid_y = (h / sy).min(pixt2.height());
let pixt2_filled = fill_map_holes(&pixt2, valid_x, valid_y)?;
let pixt3 = blockconv(&pixt2_filled, 8, 8)?;
let pixd = Pix::new(wd, hd, PixelDepth::Bit8)?;
let mut pixd_mut = pixd.try_into_mut().unwrap();
let copy_w = wd.min(pixt3.width());
let copy_h = hd.min(pixt3.height());
for y in 0..copy_h {
for x in 0..copy_w {
let val = pixt3.get_pixel(x, y).unwrap_or(0);
pixd_mut.set_pixel_unchecked(x, y, val);
}
}
if let Some(m) = mask {
let pixims = scale_by_sampling(m, 1.0 / sx as f32, 1.0 / sy as f32)?;
pixd_mut.paint_through_mask(&pixims, 0, 0, 0)?;
}
Ok(pixd_mut.into())
}
pub fn fill_map_holes(pix: &Pix, nx: u32, ny: u32) -> FilterResult<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();
if nx == 0 || ny == 0 || nx > w || ny > h || w > nx + 1 || h > ny + 1 {
return Err(FilterError::InvalidParameters(format!(
"fill_map_holes: expected nx ∈ {{w, w-1}} and ny ∈ {{h, h-1}} \
(nx={nx}, ny={ny}, w={w}, h={h})"
)));
}
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(),
});
}
if inv_map.depth() != PixelDepth::Bit16 {
return Err(FilterError::UnsupportedDepth {
expected: "16 bpp inv_map (use get_inv_background_map)",
actual: inv_map.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(),
});
}
for (channel, inv_map) in [
("inv_map_r", inv_map_r),
("inv_map_g", inv_map_g),
("inv_map_b", inv_map_b),
] {
if inv_map.depth() != PixelDepth::Bit16 {
return Err(FilterError::InvalidParameters(format!(
"{channel}: expected 16 bpp inv map (use get_inv_background_map), \
got {} bpp",
inv_map.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> {
use crate::color::threshold::threshold_to_binary;
use crate::morph::sequence::morph_sequence;
let w = pix.width();
let h = pix.height();
if tile_width == 0 || tile_height == 0 {
return Err(FilterError::InvalidParameters(format!(
"tile dimensions must be non-zero (tile_width={tile_width}, tile_height={tile_height})"
)));
}
if fg_threshold > 255 {
return Err(FilterError::InvalidParameters(format!(
"fg_threshold must be in 0..=255 (got {fg_threshold})"
)));
}
let thresh_u8 = fg_threshold as u8;
let pixb = threshold_to_binary(pix, thresh_u8).map_err(|e| match e {
crate::color::ColorError::Core(core) => FilterError::Core(core),
other => FilterError::InvalidParameters(format!("threshold_to_binary: {other}")),
})?;
let pixf = morph_sequence(&pixb, "d7.1 + d1.7")?;
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;
if nx == 0 || ny == 0 {
return Err(FilterError::InvalidParameters(format!(
"get_background_gray_map: tile size larger than image \
(w={w}, h={h}, tile_width={tile_width}, tile_height={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: u64 = 0;
let mut count: u64 = 0;
for y in tile_y..(tile_y + tile_height) {
for x in tile_x..(tile_x + tile_width) {
if pixf.get_pixel_unchecked(x, y) == 0 {
sum += pix.get_pixel_unchecked(x, y) as u64;
count += 1;
}
}
}
if count >= min_count as u64 {
let avg = (sum / count) as u32;
map_mut.set_pixel_unchecked(tx, ty, avg);
}
}
}
let map_pix = map_mut.into();
fill_map_holes_inner(&map_pix, nx, ny)
}
fn get_background_rgb_map_inner(
pix: &Pix,
tile_width: u32,
tile_height: u32,
fg_threshold: u32,
min_count: u32,
) -> FilterResult<(Pix, Pix, Pix)> {
use crate::color::threshold::threshold_to_binary;
use crate::morph::sequence::morph_sequence;
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
if tile_width == 0 || tile_height == 0 {
return Err(FilterError::InvalidParameters(format!(
"tile dimensions must be non-zero (tile_width={tile_width}, \
tile_height={tile_height})"
)));
}
if fg_threshold > 255 {
return Err(FilterError::InvalidParameters(format!(
"fg_threshold must be in 0..=255 (got {fg_threshold})"
)));
}
let w = pix.width();
let h = pix.height();
let pixgc = pix.convert_rgb_to_gray_fast()?;
let thresh_u8 = fg_threshold as u8;
let pixb = threshold_to_binary(&pixgc, thresh_u8).map_err(|e| match e {
crate::color::ColorError::Core(core) => FilterError::Core(core),
other => FilterError::InvalidParameters(format!("threshold_to_binary: {other}")),
})?;
let pixf = morph_sequence(&pixb, "d7.1 + d1.7")?;
let map_w = w.div_ceil(tile_width);
let map_h = h.div_ceil(tile_height);
let pixmr = Pix::new(map_w, map_h, PixelDepth::Bit8)?;
let pixmg = Pix::new(map_w, map_h, PixelDepth::Bit8)?;
let pixmb = Pix::new(map_w, map_h, PixelDepth::Bit8)?;
let mut mr_mut = pixmr.try_into_mut().unwrap();
let mut mg_mut = pixmg.try_into_mut().unwrap();
let mut mb_mut = pixmb.try_into_mut().unwrap();
let nx = w / tile_width;
let ny = h / tile_height;
if nx == 0 || ny == 0 {
return Err(FilterError::InvalidParameters(format!(
"get_background_rgb_map: tile size larger than image \
(w={w}, h={h}, tile_width={tile_width}, tile_height={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 rsum: u64 = 0;
let mut gsum: u64 = 0;
let mut bsum: u64 = 0;
let mut count: u64 = 0;
for y in tile_y..(tile_y + tile_height) {
for x in tile_x..(tile_x + tile_width) {
if pixf.get_pixel_unchecked(x, y) == 0 {
let pixel = pix.get_pixel_unchecked(x, y) as u64;
rsum += (pixel >> 24) & 0xff;
gsum += (pixel >> 16) & 0xff;
bsum += (pixel >> 8) & 0xff;
count += 1;
}
}
}
if count >= min_count as u64 {
mr_mut.set_pixel_unchecked(tx, ty, (rsum / count) as u32);
mg_mut.set_pixel_unchecked(tx, ty, (gsum / count) as u32);
mb_mut.set_pixel_unchecked(tx, ty, (bsum / count) as u32);
}
}
}
let pixmr: Pix = mr_mut.into();
let pixmg: Pix = mg_mut.into();
let pixmb: Pix = mb_mut.into();
let pixmr = fill_map_holes_inner(&pixmr, nx, ny)?;
let pixmg = fill_map_holes_inner(&pixmg, nx, ny)?;
let pixmb = fill_map_holes_inner(&pixmb, nx, ny)?;
Ok((pixmr, pixmg, pixmb))
}
fn fill_map_holes_inner(pix: &Pix, nx: u32, ny: 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();
let nx_idx = nx as usize;
let mut column_has_data = vec![false; nx_idx];
let mut nmiss: u32 = 0;
for j in 0..nx {
let mut first: Option<u32> = None;
for i in 0..ny {
if out_mut.get_pixel_unchecked(j, i) != 0 {
first = Some(i);
break;
}
}
match first {
None => {
column_has_data[j as usize] = false;
nmiss += 1;
}
Some(y) => {
column_has_data[j as usize] = true;
let val = out_mut.get_pixel_unchecked(j, y);
for i in 0..y {
out_mut.set_pixel_unchecked(j, i, val);
}
let mut lastval = out_mut.get_pixel_unchecked(j, 0);
for i in 1..h {
let v = out_mut.get_pixel_unchecked(j, i);
if v == 0 {
out_mut.set_pixel_unchecked(j, i, lastval);
} else {
lastval = v;
}
}
}
}
}
if nmiss == nx {
return Err(FilterError::InvalidParameters(
"fill_map_holes: no data in any column".to_string(),
));
}
if nmiss > 0 {
let goodcol = (0..nx)
.find(|&j| column_has_data[j as usize])
.expect("nmiss < nx implies at least one valid column");
for j in (0..goodcol).rev() {
for i in 0..h {
let v = out_mut.get_pixel_unchecked(j + 1, i);
out_mut.set_pixel_unchecked(j, i, v);
}
}
for j in (goodcol + 1)..nx {
if !column_has_data[j as usize] {
for i in 0..h {
let v = out_mut.get_pixel_unchecked(j - 1, i);
out_mut.set_pixel_unchecked(j, i, v);
}
}
}
}
if w > nx {
for i in 0..h {
let v = out_mut.get_pixel_unchecked(w - 2, i);
out_mut.set_pixel_unchecked(w - 1, i, v);
}
}
Ok(out_mut.into())
}
fn get_inv_background_map_inner(
pix: &Pix,
bg_val: u32,
smooth_x: u32,
smooth_y: u32,
) -> FilterResult<Pix> {
use crate::filter::block_conv::blockconv;
let w = pix.width();
let h = pix.height();
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8 bpp",
actual: pix.depth().bits(),
});
}
if pix.colormap().is_some() {
return Err(FilterError::InvalidParameters(
"get_inv_background_map: pix must be non-colormapped".to_string(),
));
}
if w < 5 || h < 5 {
return Err(FilterError::InvalidParameters(format!(
"get_inv_background_map: w/h must be >= 5 (got {w}x{h})"
)));
}
let smoothed = blockconv(pix, smooth_x, smooth_y)?;
let out_pix = Pix::new(w, h, PixelDepth::Bit16)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
let numerator = 256u64 * bg_val as u64;
for y in 0..h {
for x in 0..w {
let val = smoothed.get_pixel_unchecked(x, y);
let factor = if val > 0 {
(numerator / val as u64).min(65535) as u32
} else {
bg_val / 2
};
out_mut.set_pixel_unchecked(x, y, factor.min(65535));
}
}
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.colormap().is_some() {
let decoded = pix.remove_colormap(crate::core::pix::RemoveColormapTarget::ToGrayscale)?;
return contrast_norm(&decoded, options);
}
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)> {
use crate::filter::block_conv::blockconv;
use crate::filter::rank::{MinMaxOp, scale_gray_min_max};
let pix_min = scale_gray_min_max(pix, tile_width, tile_height, MinMaxOp::Min)?;
let pix_max = scale_gray_min_max(pix, tile_width, tile_height, MinMaxOp::Max)?;
let pix_min = extend_right_bottom_by_one(&pix_min)?;
let pix_max = extend_right_bottom_by_one(&pix_max)?;
let map_w = pix_min.width();
let map_h = pix_min.height();
let pix_min = pix_min.add_constant(1)?;
let pix_max = pix_max.add_constant(1)?;
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);
blockconv(&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);
blockconv(&pix_max, sx, sy)?
} else {
pix_max
};
Ok((pix_min, pix_max))
}
fn extend_right_bottom_by_one(pix: &Pix) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8 bpp",
actual: pix.depth().bits(),
});
}
if pix.colormap().is_some() {
return Err(FilterError::InvalidParameters(
"extend_right_bottom_by_one: colormapped input not supported; \
remove the colormap first"
.to_string(),
));
}
let w = pix.width();
let h = pix.height();
let new_w = w
.checked_add(1)
.ok_or_else(|| FilterError::InvalidParameters("width + 1 overflow".into()))?;
let new_h = h
.checked_add(1)
.ok_or_else(|| FilterError::InvalidParameters("height + 1 overflow".into()))?;
let out = Pix::new(new_w, new_h, PixelDepth::Bit8)?;
let mut out_mut = out.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
out_mut.set_pixel_unchecked(x, y, pix.get_pixel_unchecked(x, y));
}
}
for y in 0..h {
let v = pix.get_pixel_unchecked(w - 1, y);
out_mut.set_pixel_unchecked(w, y, v);
}
for x in 0..(w + 1) {
let v = out_mut.get_pixel_unchecked(x, h - 1);
out_mut.set_pixel_unchecked(x, h, v);
}
Ok(out_mut.into())
}
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();
if w != pix_max.width() || h != pix_max.height() {
return Err(FilterError::InvalidParameters(
"set_low_contrast: pix_min and pix_max must have equal dimensions".to_string(),
));
}
if min_diff > 254 {
return Ok((pix_min, pix_max));
}
let mut found = false;
'outer: for y in 0..h {
for x in 0..w {
let v1 = pix_min.get_pixel_unchecked(x, y) as i32;
let v2 = pix_max.get_pixel_unchecked(x, y) as i32;
if (v1 - v2).unsigned_abs() >= min_diff {
found = true;
break 'outer;
}
}
}
if !found {
return Err(FilterError::InvalidParameters(format!(
"set_low_contrast: no tile pair has |max - min| >= {min_diff}; \
input is below the requested contrast threshold"
)));
}
let mut min_mut = pix_min.try_into_mut().unwrap_or_else(|p| p.to_mut());
let mut max_mut = pix_max.try_into_mut().unwrap_or_else(|p| p.to_mut());
for y in 0..h {
for x in 0..w {
let v1 = min_mut.get_pixel_unchecked(x, y) as i32;
let v2 = max_mut.get_pixel_unchecked(x, y) as i32;
if (v1 - v2).unsigned_abs() < 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);
let max_val = pix_max.get_pixel_unchecked(tx, ty);
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_f32 / diff as f32;
for (i, slot) in new_lut.iter_mut().enumerate().take(diff + 1) {
*slot = ((factor * i as f32) + 0.5).min(255.0) as u8;
}
for slot in new_lut.iter_mut().skip(diff + 1) {
*slot = 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 sval = val.saturating_sub(min_val) as usize;
let mapped = lut[sval.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 (w, h) = (50u32, 75u32);
let pix = Pix::new(w, h, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let bg = 100 + x * 2;
let val = if (15..35).contains(&x) && y > 15 && y < (h - 15) {
bg / 2
} else {
bg
};
pix_mut.set_pixel_unchecked(x, y, val.min(255));
}
}
pix_mut.into()
}
fn create_test_color_image() -> Pix {
let (w, h) = (50u32, 75u32);
let pix = Pix::new(w, h, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
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);
assert!(
result.is_err(),
"expected Err on uniform low-contrast input, got {:?}",
result
);
}
#[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());
}
}