use crate::bitmap::Bitmap;
use crate::pixmap::Pixmap;
#[derive(Debug, Clone, Copy)]
pub struct SegmentOptions {
pub threshold: u8,
pub bg_subsample: u32,
}
impl Default for SegmentOptions {
fn default() -> Self {
Self {
threshold: 128,
bg_subsample: 12,
}
}
}
pub struct SegmentedPage {
pub mask: Bitmap,
pub bg: Pixmap,
}
pub fn segment_page(rgba: &Pixmap, opts: &SegmentOptions) -> SegmentedPage {
let w = rgba.width;
let h = rgba.height;
let sub = opts.bg_subsample.max(1);
let mut mask = Bitmap::new(w, h);
if w == 0 || h == 0 {
return SegmentedPage {
mask,
bg: Pixmap::default(),
};
}
let threshold = opts.threshold as u32;
for y in 0..h {
for x in 0..w {
let (r, g, b) = rgba.get_rgb(x, y);
let lum = ((r as u32) * 306 + (g as u32) * 601 + (b as u32) * 117) >> 10;
if lum < threshold {
mask.set(x, y, true);
}
}
}
let bw = w.div_ceil(sub);
let bh = h.div_ceil(sub);
let mut bg = Pixmap::white(bw, bh);
for by in 0..bh {
let y0 = by * sub;
let y1 = (y0 + sub).min(h);
for bx in 0..bw {
let x0 = bx * sub;
let x1 = (x0 + sub).min(w);
let (mut r_unmasked, mut g_unmasked, mut b_unmasked, mut n_unmasked) =
(0u32, 0u32, 0u32, 0u32);
let (mut r_all, mut g_all, mut b_all, mut n_all) = (0u32, 0u32, 0u32, 0u32);
for y in y0..y1 {
for x in x0..x1 {
let (r, g, b) = rgba.get_rgb(x, y);
r_all += r as u32;
g_all += g as u32;
b_all += b as u32;
n_all += 1;
if !mask.get(x, y) {
r_unmasked += r as u32;
g_unmasked += g as u32;
b_unmasked += b as u32;
n_unmasked += 1;
}
}
}
let mean = |sum: u32, n: u32| sum.checked_div(n).map(|v| v as u8);
let triple =
|r: u32, g: u32, b: u32, n: u32| Some((mean(r, n)?, mean(g, n)?, mean(b, n)?));
let (r, g, b) = triple(r_unmasked, g_unmasked, b_unmasked, n_unmasked)
.or_else(|| triple(r_all, g_all, b_all, n_all))
.unwrap_or((255, 255, 255));
bg.set_rgb(bx, by, r, g, b);
}
}
SegmentedPage { mask, bg }
}
#[cfg(test)]
mod tests {
use super::*;
fn fill(pm: &mut Pixmap, r: u8, g: u8, b: u8) {
for y in 0..pm.height {
for x in 0..pm.width {
pm.set_rgb(x, y, r, g, b);
}
}
}
#[test]
fn all_white_page_yields_empty_mask() {
let pm = Pixmap::white(24, 24);
let seg = segment_page(&pm, &SegmentOptions::default());
assert_eq!(seg.mask.width, 24);
assert_eq!(seg.mask.height, 24);
for y in 0..24 {
for x in 0..24 {
assert!(
!seg.mask.get(x, y),
"white pixel at ({x},{y}) should not be mask"
);
}
}
assert_eq!(seg.bg.width, 2);
assert_eq!(seg.bg.height, 2);
for chunk in seg.bg.data.chunks_exact(4) {
assert_eq!(&chunk[..3], &[255, 255, 255]);
}
}
#[test]
fn all_black_page_yields_full_mask_and_black_bg_fallback() {
let mut pm = Pixmap::white(12, 12);
fill(&mut pm, 0, 0, 0);
let seg = segment_page(&pm, &SegmentOptions::default());
for y in 0..12 {
for x in 0..12 {
assert!(seg.mask.get(x, y));
}
}
assert_eq!(seg.bg.width, 1);
assert_eq!(seg.bg.height, 1);
assert_eq!(&seg.bg.data[..3], &[0, 0, 0]);
}
#[test]
fn threshold_boundary_is_strict() {
let mut pm = Pixmap::white(4, 1);
pm.set_rgb(0, 0, 0, 0, 0);
pm.set_rgb(1, 0, 127, 127, 127);
pm.set_rgb(2, 0, 128, 128, 128);
pm.set_rgb(3, 0, 255, 255, 255);
let seg = segment_page(
&pm,
&SegmentOptions {
threshold: 128,
bg_subsample: 1,
},
);
assert!(seg.mask.get(0, 0));
assert!(seg.mask.get(1, 0));
assert!(!seg.mask.get(2, 0));
assert!(!seg.mask.get(3, 0));
}
#[test]
fn bg_excludes_mask_pixels() {
let mut pm = Pixmap::white(4, 4);
fill(&mut pm, 240, 230, 100);
pm.set_rgb(1, 1, 0, 0, 0);
let seg = segment_page(
&pm,
&SegmentOptions {
threshold: 128,
bg_subsample: 4,
},
);
assert!(seg.mask.get(1, 1));
assert!(!seg.mask.get(0, 0));
assert_eq!(seg.bg.width, 1);
assert_eq!(seg.bg.height, 1);
let (r, g, b) = (seg.bg.data[0], seg.bg.data[1], seg.bg.data[2]);
assert_eq!(
(r, g, b),
(240, 230, 100),
"ink pixel should not contaminate BG mean"
);
}
#[test]
fn empty_input_returns_empty_outputs() {
let pm = Pixmap::default();
let seg = segment_page(&pm, &SegmentOptions::default());
assert_eq!(seg.mask.width, 0);
assert_eq!(seg.mask.height, 0);
assert_eq!(seg.bg.width, 0);
assert_eq!(seg.bg.height, 0);
}
#[test]
fn bg_dims_round_up() {
let pm = Pixmap::white(13, 7);
let seg = segment_page(
&pm,
&SegmentOptions {
threshold: 128,
bg_subsample: 12,
},
);
assert_eq!(seg.bg.width, 2);
assert_eq!(seg.bg.height, 1);
}
#[test]
fn bg_subsample_zero_is_clamped_to_one() {
let pm = Pixmap::white(3, 3);
let seg = segment_page(
&pm,
&SegmentOptions {
threshold: 128,
bg_subsample: 0,
},
);
assert_eq!(seg.bg.width, 3);
assert_eq!(seg.bg.height, 3);
}
}