use crate::core::{Pix, PixelDepth, RopOp};
use crate::morph::thin_sels::{ThinSelSet, make_thin_sels};
use crate::morph::{MorphError, MorphResult, Sel, hit_miss_transform};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ThinType {
#[default]
Foreground,
Background,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Connectivity {
#[default]
Four,
Eight,
}
const DEFAULT_MAX_ITERS: u32 = 10000;
pub fn thin_connected(
pix: &Pix,
thin_type: ThinType,
connectivity: Connectivity,
max_iters: u32,
) -> MorphResult<Pix> {
check_binary(pix)?;
let sel_set = match connectivity {
Connectivity::Four => ThinSelSet::Set4cc1,
Connectivity::Eight => ThinSelSet::Set8cc1,
};
let sels = make_thin_sels(sel_set);
thin_connected_by_set(pix, thin_type, &sels, max_iters)
}
pub fn thin_connected_by_set(
pix: &Pix,
thin_type: ThinType,
sels: &[Sel],
max_iters: u32,
) -> MorphResult<Pix> {
check_binary(pix)?;
if sels.is_empty() {
return Err(MorphError::InvalidParameters(
"SEL set cannot be empty".to_string(),
));
}
let max_iters = if max_iters == 0 {
DEFAULT_MAX_ITERS
} else {
max_iters
};
let mut pixd = if thin_type == ThinType::Foreground {
pix.clone()
} else {
invert(pix)?
};
let rotated_sels_by_rotation: Vec<Vec<Sel>> = (0..4)
.map(|rotation| sels.iter().map(|sel| sel.rotate_orth(rotation)).collect())
.collect();
for _iter in 0..max_iters {
let pix_prev = pixd.clone();
for rotation_sels in &rotated_sels_by_rotation {
let mut hmt_iter = rotation_sels.iter();
let first_sel = hmt_iter
.next()
.expect("thin_connected_by_set requires non-empty SEL set");
let accumulated = hit_miss_transform(&pixd, first_sel)?;
let mut accumulated_mut = accumulated.try_into_mut().unwrap();
for sel in hmt_iter {
let hmt_result = hit_miss_transform(&pixd, sel)?;
accumulated_mut.or_inplace(&hmt_result)?;
}
let accumulated: Pix = accumulated_mut.into();
pixd = subtract_images(&pixd, &accumulated)?;
}
if images_equal(&pixd, &pix_prev) {
break;
}
}
if thin_type == ThinType::Background {
pixd = invert(&pixd)?;
let diff = subtract_images(&pixd, pix)?;
let border_cc = extract_border_connected_components(&diff)?;
pixd = subtract_images(&pixd, &border_cc)?;
}
Ok(pixd)
}
fn invert(pix: &Pix) -> MorphResult<Pix> {
Ok(pix.invert())
}
#[cfg(test)]
fn or_images(a: &Pix, b: &Pix) -> MorphResult<Pix> {
Ok(a.or(b)?)
}
fn subtract_images(a: &Pix, b: &Pix) -> MorphResult<Pix> {
Ok(a.rop(b, RopOp::AndNotSrc)?)
}
fn images_equal(a: &Pix, b: &Pix) -> bool {
a.equals(b)
}
fn extract_border_connected_components(pix: &Pix) -> MorphResult<Pix> {
let w = pix.width();
let h = pix.height();
let out_pix = Pix::new(w, h, PixelDepth::Bit1)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
let mut visited = vec![false; (w * h) as usize];
let mut seeds: Vec<(u32, u32)> = Vec::new();
for x in 0..w {
if pix.get_pixel_unchecked(x, 0) != 0 {
seeds.push((x, 0));
}
if h > 1 && pix.get_pixel_unchecked(x, h - 1) != 0 {
seeds.push((x, h - 1));
}
}
for y in 1..h.saturating_sub(1) {
if pix.get_pixel_unchecked(0, y) != 0 {
seeds.push((0, y));
}
if w > 1 && pix.get_pixel_unchecked(w - 1, y) != 0 {
seeds.push((w - 1, y));
}
}
while let Some((x, y)) = seeds.pop() {
let idx = (y * w + x) as usize;
if visited[idx] {
continue;
}
if pix.get_pixel_unchecked(x, y) == 0 {
continue;
}
visited[idx] = true;
out_mut.set_pixel_unchecked(x, y, 1);
if x > 0 {
seeds.push((x - 1, y));
}
if x + 1 < w {
seeds.push((x + 1, y));
}
if y > 0 {
seeds.push((x, y - 1));
}
if y + 1 < h {
seeds.push((x, y + 1));
}
}
Ok(out_mut.into())
}
fn check_binary(pix: &Pix) -> MorphResult<()> {
if pix.depth() != PixelDepth::Bit1 {
return Err(MorphError::UnsupportedDepth {
expected: "1-bpp binary",
actual: pix.depth().bits(),
});
}
Ok(())
}
pub fn pixa_thin_connected(
pixa: &crate::core::Pixa,
thin_type: ThinType,
connectivity: Connectivity,
max_iters: u32,
) -> MorphResult<crate::core::Pixa> {
let mut result = crate::core::Pixa::with_capacity(pixa.len());
for i in 0..pixa.len() {
let pix = pixa.get(i).ok_or_else(|| {
MorphError::InvalidParameters(format!("pixa index {} out of bounds", i))
})?;
check_binary(pix)?;
let thinned = thin_connected(pix, thin_type, connectivity, max_iters)?;
result.push(thinned);
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
fn create_vertical_line(width: u32, height: u32) -> Pix {
let pix = Pix::new(width, height, PixelDepth::Bit1).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
let center_x = width / 2;
for y in 0..height {
pix_mut.set_pixel_unchecked(center_x, y, 1);
}
pix_mut.into()
}
fn create_horizontal_line(width: u32, height: u32) -> Pix {
let pix = Pix::new(width, height, PixelDepth::Bit1).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
let center_y = height / 2;
for x in 0..width {
pix_mut.set_pixel_unchecked(x, center_y, 1);
}
pix_mut.into()
}
fn create_thick_horizontal_line(width: u32, height: u32, thickness: u32) -> Pix {
let pix = Pix::new(width, height, PixelDepth::Bit1).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
let center_y = height / 2;
let half_thick = thickness / 2;
for y in center_y.saturating_sub(half_thick)..=(center_y + half_thick).min(height - 1) {
for x in 0..width {
pix_mut.set_pixel_unchecked(x, y, 1);
}
}
pix_mut.into()
}
fn count_foreground_pixels(pix: &Pix) -> u32 {
let mut count = 0;
for y in 0..pix.height() {
for x in 0..pix.width() {
if pix.get_pixel_unchecked(x, y) != 0 {
count += 1;
}
}
}
count
}
#[test]
fn test_thin_vertical_line() {
let pix = create_vertical_line(9, 9);
let original_count = count_foreground_pixels(&pix);
let thinned = thin_connected(&pix, ThinType::Foreground, Connectivity::Four, 0).unwrap();
let thinned_count = count_foreground_pixels(&thinned);
assert!(thinned_count > 0);
assert!(thinned_count <= original_count);
}
#[test]
fn test_thin_horizontal_line() {
let pix = create_horizontal_line(9, 9);
let original_count = count_foreground_pixels(&pix);
let thinned = thin_connected(&pix, ThinType::Foreground, Connectivity::Four, 0).unwrap();
let thinned_count = count_foreground_pixels(&thinned);
assert!(thinned_count > 0);
assert!(thinned_count <= original_count);
}
#[test]
fn test_thin_thick_line() {
let pix = create_thick_horizontal_line(15, 15, 5);
let original_count = count_foreground_pixels(&pix);
let thinned = thin_connected(&pix, ThinType::Foreground, Connectivity::Four, 0).unwrap();
let thinned_count = count_foreground_pixels(&thinned);
assert!(thinned_count < original_count);
assert!(thinned_count > 0);
}
#[test]
fn test_thin_8_connected() {
let pix = create_thick_horizontal_line(15, 15, 5);
let thinned = thin_connected(&pix, ThinType::Foreground, Connectivity::Eight, 0).unwrap();
let thinned_count = count_foreground_pixels(&thinned);
assert!(thinned_count > 0);
}
#[test]
fn test_thin_with_max_iters() {
let pix = create_thick_horizontal_line(15, 15, 5);
let thinned_1 = thin_connected(&pix, ThinType::Foreground, Connectivity::Four, 1).unwrap();
let thinned_full =
thin_connected(&pix, ThinType::Foreground, Connectivity::Four, 0).unwrap();
let count_1 = count_foreground_pixels(&thinned_1);
let count_full = count_foreground_pixels(&thinned_full);
assert!(count_1 >= count_full);
}
#[test]
fn test_invert() {
let pix = Pix::new(3, 3, PixelDepth::Bit1).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
pix_mut.set_pixel_unchecked(1, 1, 1);
let pix: Pix = pix_mut.into();
let inverted = invert(&pix).unwrap();
assert_eq!(inverted.get_pixel_unchecked(1, 1), 0);
assert_eq!(inverted.get_pixel_unchecked(0, 0), 1);
assert_eq!(inverted.get_pixel_unchecked(2, 2), 1);
}
#[test]
fn test_or_images() {
let pix1 = Pix::new(3, 3, PixelDepth::Bit1).unwrap();
let mut pix1_mut = pix1.try_into_mut().unwrap();
pix1_mut.set_pixel_unchecked(0, 0, 1);
let pix1: Pix = pix1_mut.into();
let pix2 = Pix::new(3, 3, PixelDepth::Bit1).unwrap();
let mut pix2_mut = pix2.try_into_mut().unwrap();
pix2_mut.set_pixel_unchecked(2, 2, 1);
let pix2: Pix = pix2_mut.into();
let result = or_images(&pix1, &pix2).unwrap();
assert_eq!(result.get_pixel_unchecked(0, 0), 1);
assert_eq!(result.get_pixel_unchecked(2, 2), 1);
assert_eq!(result.get_pixel_unchecked(1, 1), 0);
}
#[test]
fn test_subtract_images() {
let pix1 = Pix::new(3, 3, PixelDepth::Bit1).unwrap();
let mut pix1_mut = pix1.try_into_mut().unwrap();
pix1_mut.set_pixel_unchecked(0, 0, 1);
pix1_mut.set_pixel_unchecked(1, 1, 1);
let pix1: Pix = pix1_mut.into();
let pix2 = Pix::new(3, 3, PixelDepth::Bit1).unwrap();
let mut pix2_mut = pix2.try_into_mut().unwrap();
pix2_mut.set_pixel_unchecked(1, 1, 1);
let pix2: Pix = pix2_mut.into();
let result = subtract_images(&pix1, &pix2).unwrap();
assert_eq!(result.get_pixel_unchecked(0, 0), 1); assert_eq!(result.get_pixel_unchecked(1, 1), 0); }
#[test]
fn test_images_equal() {
let pix1 = create_vertical_line(5, 5);
let pix2 = create_vertical_line(5, 5);
let pix3 = create_horizontal_line(5, 5);
assert!(images_equal(&pix1, &pix2));
assert!(!images_equal(&pix1, &pix3));
}
#[test]
fn test_empty_sel_set_error() {
let pix = create_vertical_line(5, 5);
let result = thin_connected_by_set(&pix, ThinType::Foreground, &[], 0);
assert!(result.is_err());
}
#[test]
fn test_non_binary_error() {
let pix = Pix::new(5, 5, PixelDepth::Bit8).unwrap();
let result = thin_connected(&pix, ThinType::Foreground, Connectivity::Four, 0);
assert!(result.is_err());
}
#[test]
fn test_extract_border_connected() {
let pix = Pix::new(5, 5, PixelDepth::Bit1).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
pix_mut.set_pixel_unchecked(0, 0, 1);
pix_mut.set_pixel_unchecked(1, 0, 1);
pix_mut.set_pixel_unchecked(0, 1, 1);
pix_mut.set_pixel_unchecked(2, 2, 1);
let pix: Pix = pix_mut.into();
let border_cc = extract_border_connected_components(&pix).unwrap();
assert_eq!(border_cc.get_pixel_unchecked(0, 0), 1);
assert_eq!(border_cc.get_pixel_unchecked(1, 0), 1);
assert_eq!(border_cc.get_pixel_unchecked(0, 1), 1);
assert_eq!(border_cc.get_pixel_unchecked(2, 2), 0);
}
}