init
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "hcie-selection"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
hcie-protocol = { path = "../hcie-protocol" }
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.23"
|
||||
proptest = "1.5"
|
||||
approx = "0.5"
|
||||
@@ -0,0 +1,285 @@
|
||||
#![allow(dead_code)]
|
||||
//! Selection mask operations.
|
||||
|
||||
/// Create a rectangular selection mask.
|
||||
pub fn create_rect_mask(w: u32, h: u32, x1: u32, y1: u32, x2: u32, y2: u32) -> Vec<u8> {
|
||||
let mut mask = vec![0u8; (w * h) as usize];
|
||||
let (x0, x1o) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
|
||||
let (y0, y1o) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
|
||||
for y in y0..=y1o.min(h - 1) {
|
||||
for x in x0..=x1o.min(w - 1) {
|
||||
mask[(y * w + x) as usize] = 255;
|
||||
}
|
||||
}
|
||||
mask
|
||||
}
|
||||
|
||||
/// Create an elliptical selection mask.
|
||||
pub fn create_ellipse_mask(w: u32, h: u32, cx: u32, cy: u32, rx: u32, ry: u32) -> Vec<u8> {
|
||||
let mut mask = vec![0u8; (w * h) as usize];
|
||||
let cx_f = cx as f32;
|
||||
let cy_f = cy as f32;
|
||||
let rx_f = rx.max(1) as f32;
|
||||
let ry_f = ry.max(1) as f32;
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let dx = (x as f32 - cx_f) / rx_f;
|
||||
let dy = (y as f32 - cy_f) / ry_f;
|
||||
if dx * dx + dy * dy <= 1.0 {
|
||||
mask[(y * w + x) as usize] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
mask
|
||||
}
|
||||
|
||||
/// Grow a mask by `px` pixels (4-connected dilation).
|
||||
pub fn grow_mask(mask: &mut [u8], w: u32, h: u32, px: u32) {
|
||||
if px == 0 { return; }
|
||||
let mut temp = mask.to_vec();
|
||||
for _ in 0..px {
|
||||
let old = temp.clone();
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let i = (y * w + x) as usize;
|
||||
if old[i] > 0 { continue; }
|
||||
let neighbors = [
|
||||
(x.wrapping_sub(1), y), (x + 1, y),
|
||||
(x, y.wrapping_sub(1)), (x, y + 1),
|
||||
];
|
||||
for (nx, ny) in neighbors {
|
||||
if nx < w && ny < h {
|
||||
let ni = (ny * w + nx) as usize;
|
||||
if old[ni] > 0 {
|
||||
temp[i] = 255;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mask.copy_from_slice(&temp);
|
||||
}
|
||||
|
||||
/// Shrink a mask by `px` pixels (4-connected erosion).
|
||||
pub fn shrink_mask(mask: &mut [u8], w: u32, h: u32, px: u32) {
|
||||
if px == 0 { return; }
|
||||
let mut temp = mask.to_vec();
|
||||
for _ in 0..px {
|
||||
let old = temp.clone();
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let i = (y * w + x) as usize;
|
||||
if old[i] == 0 { continue; }
|
||||
let neighbors = [
|
||||
(x.wrapping_sub(1), y), (x + 1, y),
|
||||
(x, y.wrapping_sub(1)), (x, y + 1),
|
||||
];
|
||||
let mut has_zero = false;
|
||||
for (nx, ny) in neighbors {
|
||||
if nx >= w || ny >= h || old[(ny * w + nx) as usize] == 0 {
|
||||
has_zero = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if has_zero {
|
||||
temp[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mask.copy_from_slice(&temp);
|
||||
}
|
||||
|
||||
/// Feather (soften edges) of a mask by expanding outward then applying
|
||||
/// a smooth falloff. This properly creates soft transitions at the border
|
||||
/// by including previously-unselected pixels within the feather radius.
|
||||
pub fn feather_mask(mask: &mut [u8], w: u32, h: u32, radius: f32) {
|
||||
if radius <= 0.0 { return; }
|
||||
let r = radius.ceil() as i32;
|
||||
let r_sq = (radius * radius) as f32;
|
||||
let size = (w * h) as usize;
|
||||
|
||||
// Step 1: Expand the selection outward by the feather radius
|
||||
// so that unselected border pixels get included in the soft transition.
|
||||
let mut expanded = vec![0u8; size];
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let i = (y * w + x) as usize;
|
||||
if mask[i] > 0 {
|
||||
expanded[i] = 255;
|
||||
continue;
|
||||
}
|
||||
// Check if any selected pixel is within radius
|
||||
'outer: for dy in -r..=r {
|
||||
for dx in -r..=r {
|
||||
if (dx * dx + dy * dy) as f32 > r_sq { continue; }
|
||||
let nx = (x as i32 + dx).clamp(0, w as i32 - 1) as u32;
|
||||
let ny = (y as i32 + dy).clamp(0, h as i32 - 1) as u32;
|
||||
if mask[(ny * w + nx) as usize] > 0 {
|
||||
expanded[i] = 1; // mark as newly added (will become transition)
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Compute distance-based alpha for smooth falloff
|
||||
// Original selected pixels keep 255, newly added get smooth falloff
|
||||
let mut result = vec![0u8; size];
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let i = (y * w + x) as usize;
|
||||
if mask[i] > 0 {
|
||||
// Originally selected — stays fully selected
|
||||
result[i] = 255;
|
||||
continue;
|
||||
}
|
||||
if expanded[i] == 0 {
|
||||
// Outside feather zone — stays unselected
|
||||
continue;
|
||||
}
|
||||
// Newly added pixel — compute distance to nearest selected pixel
|
||||
let mut min_dist_sq = f32::MAX;
|
||||
for dy in -r..=r {
|
||||
for dx in -r..=r {
|
||||
let nx = (x as i32 + dx).clamp(0, w as i32 - 1) as u32;
|
||||
let ny = (y as i32 + dy).clamp(0, h as i32 - 1) as u32;
|
||||
if mask[(ny * w + nx) as usize] > 0 {
|
||||
let dist_sq = (dx * dx + dy * dy) as f32;
|
||||
if dist_sq < min_dist_sq {
|
||||
min_dist_sq = dist_sq;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Smooth falloff: pixels closer to selection are more opaque
|
||||
let dist = min_dist_sq.sqrt();
|
||||
let t = 1.0 - (dist / radius).min(1.0);
|
||||
// Apply ease-out curve for natural feather feel
|
||||
let alpha = (t * t * (3.0 - 2.0 * t) * 255.0).round() as u8;
|
||||
result[i] = alpha;
|
||||
}
|
||||
}
|
||||
|
||||
mask.copy_from_slice(&result);
|
||||
}
|
||||
|
||||
/// Invert a mask.
|
||||
pub fn invert_mask(mask: &mut [u8]) {
|
||||
for v in mask.iter_mut() {
|
||||
*v = 255 - *v;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get selection mask value at (x, y).
|
||||
pub fn mask_at(mask: Option<&[u8]>, w: u32, x: u32, y: u32) -> u8 {
|
||||
match mask {
|
||||
Some(m) => m.get((y * w + x) as usize).copied().unwrap_or(255),
|
||||
None => 255,
|
||||
}
|
||||
}
|
||||
|
||||
/// Magic Wand selection — BFS flood-fill based on color similarity.
|
||||
pub fn magic_wand_mask(
|
||||
pixels: &[u8],
|
||||
w: u32,
|
||||
h: u32,
|
||||
seed_x: u32,
|
||||
seed_y: u32,
|
||||
tolerance: u8,
|
||||
) -> Vec<u8> {
|
||||
let mut mask = vec![0u8; (w * h) as usize];
|
||||
|
||||
if seed_x >= w || seed_y >= h {
|
||||
return mask;
|
||||
}
|
||||
|
||||
let seed_idx = ((seed_y * w + seed_x) as usize) * 4;
|
||||
if seed_idx + 3 >= pixels.len() {
|
||||
return mask;
|
||||
}
|
||||
|
||||
let sr = pixels[seed_idx];
|
||||
let sg = pixels[seed_idx + 1];
|
||||
let sb = pixels[seed_idx + 2];
|
||||
let sa = pixels[seed_idx + 3];
|
||||
|
||||
let tol = tolerance as i32;
|
||||
|
||||
let mut queue = std::collections::VecDeque::new();
|
||||
queue.push_back((seed_x, seed_y));
|
||||
let mask_idx = (seed_y * w + seed_x) as usize;
|
||||
mask[mask_idx] = 255;
|
||||
|
||||
while let Some((cx, cy)) = queue.pop_front() {
|
||||
let neighbours: [(i32, i32); 4] = [
|
||||
(cx as i32 - 1, cy as i32),
|
||||
(cx as i32 + 1, cy as i32),
|
||||
(cx as i32, cy as i32 - 1),
|
||||
(cx as i32, cy as i32 + 1),
|
||||
];
|
||||
|
||||
for (nx, ny) in neighbours {
|
||||
if nx < 0 || ny < 0 || nx >= w as i32 || ny >= h as i32 {
|
||||
continue;
|
||||
}
|
||||
let nx = nx as u32;
|
||||
let ny = ny as u32;
|
||||
let mi = (ny * w + nx) as usize;
|
||||
|
||||
if mask[mi] > 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pi = mi * 4;
|
||||
if pi + 3 >= pixels.len() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dr = (pixels[pi] as i32 - sr as i32).abs();
|
||||
let dg = (pixels[pi + 1] as i32 - sg as i32).abs();
|
||||
let db = (pixels[pi + 2] as i32 - sb as i32).abs();
|
||||
let da = (pixels[pi + 3] as i32 - sa as i32).abs();
|
||||
|
||||
if dr <= tol && dg <= tol && db <= tol && da <= tol {
|
||||
mask[mi] = 255;
|
||||
queue.push_back((nx, ny));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mask
|
||||
}
|
||||
|
||||
/// Lasso / Polygon fill — ray-casting scanline fill for an arbitrary polygon.
|
||||
pub fn lasso_fill_mask(mask: &mut [u8], w: u32, h: u32, points: &[(u32, u32)]) {
|
||||
if points.len() < 3 {
|
||||
return;
|
||||
}
|
||||
for py in 0..h {
|
||||
let mut nodes: Vec<i32> = Vec::new();
|
||||
let mut j = points.len() - 1;
|
||||
for i in 0..points.len() {
|
||||
let (pi_x, pi_y) = (points[i].0 as f32, points[i].1 as f32);
|
||||
let (pj_x, pj_y) = (points[j].0 as f32, points[j].1 as f32);
|
||||
let y = py as f32;
|
||||
if (pi_y < y && pj_y >= y) || (pj_y < y && pi_y >= y) {
|
||||
nodes.push((pi_x + (y - pi_y) / (pj_y - pi_y) * (pj_x - pi_x)) as i32);
|
||||
}
|
||||
j = i;
|
||||
}
|
||||
nodes.sort_unstable();
|
||||
let mut k = 0;
|
||||
while k + 1 < nodes.len() {
|
||||
let x_start = nodes[k].max(0) as u32;
|
||||
let x_end = nodes[k + 1].min(w as i32 - 1) as u32;
|
||||
for px in x_start..=x_end {
|
||||
mask[(py * w + px) as usize] = 255;
|
||||
}
|
||||
k += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
#[test]
|
||||
fn test_create_rect_mask_full() {
|
||||
let mask = hcie_selection::create_rect_mask(4, 4, 0, 0, 3, 3);
|
||||
assert!(mask.iter().all(|&v| v == 255), "full rect mask should be all 255");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_rect_mask_partial() {
|
||||
let mask = hcie_selection::create_rect_mask(4, 4, 1, 1, 2, 2);
|
||||
assert_eq!(mask[0], 0, "top-left should be 0");
|
||||
assert_eq!(mask[(1 * 4 + 1) as usize], 255, "center should be 255");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_ellipse_mask() {
|
||||
let mask = hcie_selection::create_ellipse_mask(5, 5, 2, 2, 2, 2);
|
||||
assert_eq!(mask[(2 * 5 + 2) as usize], 255, "center should be selected");
|
||||
assert!(mask[(0 * 5 + 0) as usize] == 0, "corner should be 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invert_mask() {
|
||||
let mut mask = vec![0u8; 16];
|
||||
mask[0] = 255;
|
||||
hcie_selection::invert_mask(&mut mask);
|
||||
assert_eq!(mask[0], 0, "originally 255 should become 0");
|
||||
assert_eq!(mask[1], 255, "originally 0 should become 255");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invert_twice_is_identity() {
|
||||
let original = vec![
|
||||
0, 255, 0, 255,
|
||||
255, 0, 255, 0,
|
||||
0, 255, 0, 255,
|
||||
255, 0, 255, 0,
|
||||
];
|
||||
let mut mask = original.clone();
|
||||
hcie_selection::invert_mask(&mut mask);
|
||||
hcie_selection::invert_mask(&mut mask);
|
||||
assert_eq!(mask, original, "double invert should be identity");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grow_mask() {
|
||||
let mut mask = vec![0u8; 25];
|
||||
mask[12] = 255;
|
||||
hcie_selection::grow_mask(&mut mask, 5, 5, 1);
|
||||
let new_count = mask.iter().filter(|&&v| v > 0).count();
|
||||
assert!(new_count > 1, "grow should add more selected pixels");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grow_zero_is_noop() {
|
||||
let mut mask = vec![
|
||||
0, 255, 0,
|
||||
255, 0, 255,
|
||||
0, 255, 0,
|
||||
];
|
||||
let original = mask.clone();
|
||||
hcie_selection::grow_mask(&mut mask, 3, 3, 0);
|
||||
assert_eq!(mask, original, "px=0 should not change mask");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shrink_mask() {
|
||||
let mut mask = vec![255u8; 25];
|
||||
hcie_selection::shrink_mask(&mut mask, 5, 5, 1);
|
||||
let new_count = mask.iter().filter(|&&v| v > 0).count();
|
||||
assert!(new_count < 25, "shrink should remove border pixels");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shrink_zero_is_noop() {
|
||||
let mut mask = vec![255u8; 25];
|
||||
let original = mask.clone();
|
||||
hcie_selection::shrink_mask(&mut mask, 5, 5, 0);
|
||||
assert_eq!(mask, original, "px=0 should not change mask");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grow_shrink_roundtrip() {
|
||||
let original = vec![
|
||||
0, 0, 0, 0, 0,
|
||||
0, 255, 255, 255, 0,
|
||||
0, 255, 255, 255, 0,
|
||||
0, 255, 255, 255, 0,
|
||||
0, 0, 0, 0, 0,
|
||||
];
|
||||
let mut mask = original.clone();
|
||||
hcie_selection::grow_mask(&mut mask, 5, 5, 1);
|
||||
hcie_selection::shrink_mask(&mut mask, 5, 5, 1);
|
||||
assert_eq!(mask, original, "grow then shrink should restore original");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_feather_mask() {
|
||||
let mut mask = vec![0u8; 25];
|
||||
for y in 1..4 {
|
||||
for x in 1..4 {
|
||||
mask[(y * 5 + x) as usize] = 255;
|
||||
}
|
||||
}
|
||||
hcie_selection::feather_mask(&mut mask, 5, 5, 1.0);
|
||||
assert_eq!(mask[12], 255, "center should stay 255");
|
||||
assert!(mask[0] < 255, "corner should have some feathering");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_feather_zero_is_noop() {
|
||||
let mut mask = vec![0u8; 16];
|
||||
mask[5] = 255;
|
||||
let original = mask.clone();
|
||||
hcie_selection::feather_mask(&mut mask, 4, 4, 0.0);
|
||||
assert_eq!(mask, original, "radius=0 should not change mask");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mask_at() {
|
||||
let mask = vec![
|
||||
0, 255, 0,
|
||||
255, 0, 255,
|
||||
0, 255, 0,
|
||||
];
|
||||
assert_eq!(hcie_selection::mask_at(Some(&mask), 3, 1, 1), 0);
|
||||
assert_eq!(hcie_selection::mask_at(Some(&mask), 3, 0, 1), 255);
|
||||
assert_eq!(hcie_selection::mask_at(None, 3, 5, 5), 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_magic_wand() {
|
||||
let pixels = vec![
|
||||
255, 0, 0, 255, 255, 0, 0, 255,
|
||||
255, 0, 0, 255, 0, 255, 0, 255,
|
||||
];
|
||||
let mask = hcie_selection::magic_wand_mask(&pixels, 2, 2, 0, 0, 10);
|
||||
assert_eq!(mask[0], 255, "seed itself");
|
||||
assert_eq!(mask[1], 255, "adjacent same color");
|
||||
assert_eq!(mask[3], 0, "different pixel at (1,1)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lasso_fill_mask() {
|
||||
let mut mask = vec![0u8; 25];
|
||||
let points = [(0u32, 0u32), (4, 0), (4, 4), (0, 4)];
|
||||
hcie_selection::lasso_fill_mask(&mut mask, 5, 5, &points);
|
||||
assert_eq!(mask[12], 255, "center should be filled");
|
||||
}
|
||||
Reference in New Issue
Block a user