#[cfg(test)]
mod canny_u8_tests {
use fast_canny::{CannyConfig, CannyError, CannyWorkspace, canny_u8};
fn init_logger() {
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
}
fn make_workspace(w: usize, h: usize) -> CannyWorkspace {
CannyWorkspace::new(w, h).expect("workspace creation should succeed")
}
#[test]
fn test_canny_u8_uniform_no_edges() {
init_logger();
let (w, h) = (64, 64);
let mut ws = make_workspace(w, h);
let cfg = CannyConfig::builder()
.sigma(1.0)
.thresholds(30.0, 90.0)
.build()
.unwrap();
let src = vec![128u8; w * h];
let edge_map = canny_u8(&src, &mut ws, &cfg).expect("canny_u8 should succeed");
assert_eq!(edge_map.len(), w * h);
let edge_count = edge_map.iter().filter(|&&v| v == 255).count();
assert_eq!(edge_count, 0, "uniform u8 image should produce no edges");
log::info!("[test] test_canny_u8_uniform_no_edges PASSED");
}
#[test]
fn test_canny_u8_output_binary() {
init_logger();
let (w, h) = (32, 32);
let mut ws = make_workspace(w, h);
let cfg = CannyConfig::builder()
.sigma(1.0)
.thresholds(30.0, 90.0)
.build()
.unwrap();
let src = vec![0u8; w * h];
let edge_map = canny_u8(&src, &mut ws, &cfg).unwrap();
for &v in edge_map {
assert!(v == 0 || v == 255, "canny_u8 output must be binary, got {}", v);
}
log::info!("[test] test_canny_u8_output_binary PASSED");
}
#[test]
fn test_canny_u8_step_image_has_edges() {
init_logger();
let (w, h) = (64, 64);
let mut ws = make_workspace(w, h);
let cfg = CannyConfig::builder()
.sigma(0.0)
.thresholds(10.0, 30.0)
.build()
.unwrap();
let src: Vec<u8> = (0..w * h)
.map(|i| if (i % w) < w / 2 { 0u8 } else { 255u8 })
.collect();
let edge_map = canny_u8(&src, &mut ws, &cfg).unwrap();
let edge_count = edge_map.iter().filter(|&&v| v == 255).count();
assert!(edge_count > 0, "u8 step image should produce edges");
log::info!("[test] test_canny_u8_step_image_has_edges PASSED — edges={}", edge_count);
}
#[test]
fn test_canny_u8_matches_f32_path() {
init_logger();
let (w, h) = (64, 64);
let cfg = CannyConfig::builder()
.sigma(0.0) .thresholds(10.0, 30.0)
.build()
.unwrap();
let src_u8: Vec<u8> = (0..w * h)
.map(|i| if (i % w) < w / 2 { 0u8 } else { 255u8 })
.collect();
let src_f32: Vec<f32> = src_u8.iter().map(|&v| v as f32).collect();
let mut ws_u8 = make_workspace(w, h);
let mut ws_f32 = make_workspace(w, h);
let edge_u8 = canny_u8(&src_u8, &mut ws_u8, &cfg).unwrap().to_vec();
let edge_f32 = fast_canny::canny(&src_f32, &mut ws_f32, &cfg).unwrap().to_vec();
assert_eq!(
edge_u8, edge_f32,
"canny_u8 and canny(f32) must produce identical results for same input"
);
log::info!("[test] test_canny_u8_matches_f32_path PASSED");
}
#[test]
fn test_canny_u8_length_mismatch() {
init_logger();
let (w, h) = (32, 32);
let mut ws = make_workspace(w, h);
let cfg = CannyConfig::builder()
.sigma(1.0)
.thresholds(30.0, 90.0)
.build()
.unwrap();
let src = vec![0u8; w * h - 1];
let err = canny_u8(&src, &mut ws, &cfg).unwrap_err();
assert_eq!(
err,
CannyError::InputLengthMismatch { expected: w * h, actual: w * h - 1 }
);
log::info!("[test] test_canny_u8_length_mismatch PASSED");
}
#[test]
fn test_canny_u8_invalid_thresholds() {
init_logger();
let (w, h) = (32, 32);
let mut ws = make_workspace(w, h);
let cfg = CannyConfig {
sigma: 1.0,
low_thresh: 200.0,
high_thresh: 50.0,
};
let src = vec![0u8; w * h];
let err = canny_u8(&src, &mut ws, &cfg).unwrap_err();
assert_eq!(
err,
CannyError::InvalidThresholds { low: 200.0, high: 50.0 }
);
log::info!("[test] test_canny_u8_invalid_thresholds PASSED");
}
}
#[cfg(test)]
mod nms_boundary_tests {
use fast_canny::{CannyConfig, CannyWorkspace, canny};
fn init_logger() {
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
}
#[test]
fn test_boundary_pixels_always_zero() {
init_logger();
let (w, h) = (32, 32);
let mut ws = CannyWorkspace::new(w, h).unwrap();
let cfg = CannyConfig::builder()
.sigma(0.0)
.thresholds(1.0, 10.0)
.build()
.unwrap();
let src = vec![255.0f32; w * h];
let edge_map = canny(&src, &mut ws, &cfg).unwrap();
for x in 0..w {
assert_eq!(edge_map[x], 0, "top row pixel [{}] must be 0", x);
assert_eq!(edge_map[(h - 1) * w + x], 0, "bottom row pixel [{}] must be 0", x);
}
for y in 0..h {
assert_eq!(edge_map[y * w], 0, "left col pixel [{}] must be 0", y);
assert_eq!(edge_map[y * w + w - 1], 0, "right col pixel [{}] must be 0", y);
}
log::info!("[test] test_boundary_pixels_always_zero PASSED");
}
#[test]
fn test_step_edges_not_on_boundary() {
init_logger();
let (w, h) = (64, 64);
let mut ws = CannyWorkspace::new(w, h).unwrap();
let cfg = CannyConfig::builder()
.sigma(0.0)
.thresholds(10.0, 30.0)
.build()
.unwrap();
let src: Vec<f32> = (0..w * h)
.map(|i| if (i % w) < w / 2 { 0.0 } else { 255.0 })
.collect();
let edge_map = canny(&src, &mut ws, &cfg).unwrap();
for x in 0..w {
assert_eq!(edge_map[x], 0);
assert_eq!(edge_map[(h - 1) * w + x], 0);
}
for y in 0..h {
assert_eq!(edge_map[y * w], 0);
assert_eq!(edge_map[y * w + w - 1], 0);
}
log::info!("[test] test_step_edges_not_on_boundary PASSED");
}
}
#[cfg(test)]
mod hysteresis_tests {
use fast_canny::{CannyConfig, CannyWorkspace, canny};
fn init_logger() {
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
}
#[test]
fn test_no_strong_seeds_yields_no_edges() {
init_logger();
let (w, h) = (64, 64);
let mut ws = CannyWorkspace::new(w, h).unwrap();
let cfg = CannyConfig::builder()
.sigma(0.0)
.thresholds(1.0, f32::MAX)
.build()
.unwrap();
let src: Vec<f32> = (0..w * h)
.map(|i| if (i % w) < w / 2 { 0.0 } else { 255.0 })
.collect();
let edge_map = canny(&src, &mut ws, &cfg).unwrap();
let edge_count = edge_map.iter().filter(|&&v| v == 255).count();
assert_eq!(edge_count, 0, "no strong seeds should yield no edges");
log::info!("[test] test_no_strong_seeds_yields_no_edges PASSED");
}
#[test]
fn test_no_weak_edges_in_output() {
init_logger();
let (w, h) = (64, 64);
let mut ws = CannyWorkspace::new(w, h).unwrap();
let cfg = CannyConfig::builder()
.sigma(1.0)
.thresholds(20.0, 60.0)
.build()
.unwrap();
let src: Vec<f32> = (0..w * h)
.map(|i| if (i % w) < w / 2 { 0.0 } else { 255.0 })
.collect();
let edge_map = canny(&src, &mut ws, &cfg).unwrap();
let weak_count = edge_map.iter().filter(|&&v| v == 127).count();
assert_eq!(weak_count, 0, "hysteresis must eliminate all weak edges (127)");
log::info!("[test] test_no_weak_edges_in_output PASSED");
}
#[test]
fn test_equal_thresholds_no_weak_edges() {
init_logger();
let (w, h) = (64, 64);
let mut ws = CannyWorkspace::new(w, h).unwrap();
let cfg = CannyConfig::builder()
.sigma(0.0)
.thresholds(50.0, 50.0)
.build()
.unwrap();
let src: Vec<f32> = (0..w * h)
.map(|i| if (i % w) < w / 2 { 0.0 } else { 255.0 })
.collect();
let edge_map = canny(&src, &mut ws, &cfg).unwrap();
for &v in edge_map {
assert!(v == 0 || v == 255, "equal thresholds: output must be binary, got {}", v);
}
log::info!("[test] test_equal_thresholds_no_weak_edges PASSED");
}
}
#[cfg(test)]
mod determinism_tests {
use fast_canny::{CannyConfig, CannyWorkspace, canny};
fn init_logger() {
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
}
#[test]
fn test_deterministic_output() {
init_logger();
let (w, h) = (64, 64);
let mut ws = CannyWorkspace::new(w, h).unwrap();
let cfg = CannyConfig::builder()
.sigma(1.0)
.thresholds(30.0, 90.0)
.build()
.unwrap();
let src: Vec<f32> = (0..w * h)
.map(|i| if (i % w) < w / 2 { 0.0 } else { 255.0 })
.collect();
let result1 = canny(&src, &mut ws, &cfg).unwrap().to_vec();
let result2 = canny(&src, &mut ws, &cfg).unwrap().to_vec();
let result3 = canny(&src, &mut ws, &cfg).unwrap().to_vec();
assert_eq!(result1, result2, "run 1 and 2 must be identical");
assert_eq!(result2, result3, "run 2 and 3 must be identical");
log::info!("[test] test_deterministic_output PASSED");
}
#[test]
fn test_interleaved_inputs_deterministic() {
init_logger();
let (w, h) = (32, 32);
let cfg = CannyConfig::builder()
.sigma(0.0)
.thresholds(10.0, 30.0)
.build()
.unwrap();
let src_a: Vec<f32> = (0..w * h)
.map(|i| if (i % w) < w / 2 { 0.0 } else { 255.0 })
.collect();
let src_b: Vec<f32> = vec![128.0f32; w * h];
let mut ws_ref = CannyWorkspace::new(w, h).unwrap();
let ref_a = canny(&src_a, &mut ws_ref, &cfg).unwrap().to_vec();
let ref_b = canny(&src_b, &mut ws_ref, &cfg).unwrap().to_vec();
let mut ws = CannyWorkspace::new(w, h).unwrap();
for _ in 0..3 {
let out_a = canny(&src_a, &mut ws, &cfg).unwrap().to_vec();
let out_b = canny(&src_b, &mut ws, &cfg).unwrap().to_vec();
assert_eq!(out_a, ref_a, "interleaved src_a must match reference");
assert_eq!(out_b, ref_b, "interleaved src_b must match reference");
}
log::info!("[test] test_interleaved_inputs_deterministic PASSED");
}
}
#[cfg(test)]
mod threshold_behavior_tests {
use fast_canny::{CannyConfig, CannyWorkspace, canny};
fn init_logger() {
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
}
fn step_src(w: usize, h: usize) -> Vec<f32> {
(0..w * h)
.map(|i| if (i % w) < w / 2 { 0.0 } else { 255.0 })
.collect()
}
#[test]
fn test_higher_threshold_fewer_or_equal_edges() {
init_logger();
let (w, h) = (64, 64);
let src = step_src(w, h);
let thresholds = [(5.0f32, 15.0f32), (20.0, 60.0), (50.0, 150.0), (100.0, 300.0)];
let mut prev_count = usize::MAX;
for (low, high) in thresholds {
let mut ws = CannyWorkspace::new(w, h).unwrap();
let cfg = CannyConfig::builder()
.sigma(0.0)
.thresholds(low, high)
.build()
.unwrap();
let edge_map = canny(&src, &mut ws, &cfg).unwrap();
let count = edge_map.iter().filter(|&&v| v == 255).count();
assert!(
count <= prev_count,
"higher threshold should not increase edge count: prev={}, curr={} (low={}, high={})",
prev_count, count, low, high
);
prev_count = count;
}
log::info!("[test] test_higher_threshold_fewer_or_equal_edges PASSED");
}
#[test]
fn test_zero_low_thresh_keeps_all_gradient_pixels() {
init_logger();
let (w, h) = (64, 64);
let mut ws_zero = CannyWorkspace::new(w, h).unwrap();
let mut ws_normal = CannyWorkspace::new(w, h).unwrap();
let src = step_src(w, h);
let cfg_zero = CannyConfig::builder()
.sigma(0.0)
.thresholds(0.0, 1000.0)
.build()
.unwrap();
let cfg_normal = CannyConfig::builder()
.sigma(0.0)
.thresholds(50.0, 1000.0)
.build()
.unwrap();
let count_zero = canny(&src, &mut ws_zero, &cfg_zero)
.unwrap()
.iter()
.filter(|&&v| v == 255)
.count();
let count_normal = canny(&src, &mut ws_normal, &cfg_normal)
.unwrap()
.iter()
.filter(|&&v| v == 255)
.count();
assert!(
count_zero >= count_normal,
"low_thresh=0 should produce >= edges than low_thresh=50: {} vs {}",
count_zero, count_normal
);
log::info!(
"[test] test_zero_low_thresh_keeps_all_gradient_pixels PASSED — zero={}, normal={}",
count_zero, count_normal
);
}
}
#[cfg(test)]
mod special_input_tests {
use fast_canny::{CannyConfig, CannyWorkspace, canny};
fn init_logger() {
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
}
#[test]
fn test_all_zero_input() {
init_logger();
let (w, h) = (32, 32);
let mut ws = CannyWorkspace::new(w, h).unwrap();
let cfg = CannyConfig::builder()
.sigma(1.0)
.thresholds(10.0, 30.0)
.build()
.unwrap();
let src = vec![0.0f32; w * h];
let edge_map = canny(&src, &mut ws, &cfg).unwrap();
let edge_count = edge_map.iter().filter(|&&v| v == 255).count();
assert_eq!(edge_count, 0, "all-zero input should produce no edges");
log::info!("[test] test_all_zero_input PASSED");
}
#[test]
fn test_all_max_input() {
init_logger();
let (w, h) = (32, 32);
let mut ws = CannyWorkspace::new(w, h).unwrap();
let cfg = CannyConfig::builder()
.sigma(1.0)
.thresholds(10.0, 30.0)
.build()
.unwrap();
let src = vec![255.0f32; w * h];
let edge_map = canny(&src, &mut ws, &cfg).unwrap();
let edge_count = edge_map.iter().filter(|&&v| v == 255).count();
assert_eq!(edge_count, 0, "all-max input should produce no edges");
log::info!("[test] test_all_max_input PASSED");
}
#[test]
fn test_horizontal_line_has_edges() {
init_logger();
let (w, h) = (64, 32);
let mut ws = CannyWorkspace::new(w, h).unwrap();
let cfg = CannyConfig::builder()
.sigma(0.0)
.thresholds(10.0, 30.0)
.build()
.unwrap();
let mid = h / 2;
let src: Vec<f32> = (0..w * h)
.map(|i| if i / w == mid { 255.0 } else { 0.0 })
.collect();
let edge_map = canny(&src, &mut ws, &cfg).unwrap();
let edge_count = edge_map.iter().filter(|&&v| v == 255).count();
assert!(edge_count > 0, "horizontal line should produce edges");
log::info!("[test] test_horizontal_line_has_edges PASSED — edges={}", edge_count);
}
#[test]
fn test_checkerboard_edge_count_vs_cell_size() {
init_logger();
let (w, h) = (128, 128);
let cfg = CannyConfig::builder()
.sigma(0.0)
.thresholds(10.0, 30.0)
.build()
.unwrap();
let mut prev_count = usize::MAX;
for cell in [4usize, 8, 16, 32] {
let src: Vec<f32> = (0..w * h)
.map(|i| {
let x = i % w;
let y = i / w;
if (x / cell + y / cell) % 2 == 0 { 0.0 } else { 255.0 }
})
.collect();
let mut ws = CannyWorkspace::new(w, h).unwrap();
let edge_map = canny(&src, &mut ws, &cfg).unwrap();
let count = edge_map.iter().filter(|&&v| v == 255).count();
assert!(
count <= prev_count,
"larger cell should produce fewer edges: cell={}, count={}, prev={}",
cell, count, prev_count
);
prev_count = count;
}
log::info!("[test] test_checkerboard_edge_count_vs_cell_size PASSED");
}
#[test]
fn test_larger_sigma_fewer_or_equal_edges() {
init_logger();
let (w, h) = (64, 64);
let src: Vec<f32> = {
let mut state = 12345u64;
(0..w * h)
.map(|_| {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((state >> 33) & 0xFF) as f32
})
.collect()
};
let cfg_base = CannyConfig::builder()
.thresholds(30.0, 90.0)
.build()
.unwrap();
let mut prev_count = usize::MAX;
for sigma in [0.5f32, 1.0, 2.0, 4.0] {
let mut ws = CannyWorkspace::new(w, h).unwrap();
let cfg = CannyConfig { sigma, ..cfg_base.clone() };
let edge_map = canny(&src, &mut ws, &cfg).unwrap();
let count = edge_map.iter().filter(|&&v| v == 255).count();
assert!(
count <= prev_count,
"larger sigma should not increase edge count: sigma={}, count={}, prev={}",
sigma, count, prev_count
);
prev_count = count;
}
log::info!("[test] test_larger_sigma_fewer_or_equal_edges PASSED");
}
}