use fast_canny::{CannyConfig, CannyWorkspace, canny};
use image::GrayImage;
use std::fs;
use std::time::Instant;
const OUT_DIR: &str = "target/visual_demo";
fn save_f32_as_gray(pixels: &[f32], w: usize, h: usize, path: &str) {
let max = pixels.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let min = pixels.iter().cloned().fold(f32::INFINITY, f32::min);
let range = (max - min).max(1e-6);
let data: Vec<u8> = pixels
.iter()
.map(|&v| ((v - min) / range * 255.0) as u8)
.collect();
save_gray(&data, w, h, path);
}
fn save_gray(pixels: &[u8], w: usize, h: usize, path: &str) {
let img = GrayImage::from_raw(w as u32, h as u32, pixels.to_vec())
.expect("failed to create GrayImage");
img.save(path).expect("failed to save image");
log::info!("[save] {}", path);
}
fn edge_stats(edge_map: &[u8], w: usize, h: usize) -> (usize, f64) {
let count = edge_map.iter().filter(|&&v| v == 255).count();
let density = count as f64 / (w * h) as f64 * 100.0;
(count, density)
}
fn assert_binary(edge_map: &[u8], label: &str) {
for (i, &v) in edge_map.iter().enumerate() {
assert!(
v == 0 || v == 255,
"[{}] non-binary value {} at idx {}",
label,
v,
i
);
}
}
fn make_uniform(w: usize, h: usize, value: f32) -> Vec<f32> {
vec![value; w * h]
}
fn make_horizontal_step(w: usize, h: usize) -> Vec<f32> {
(0..w * h)
.map(|i| if i / w < h / 2 { 0.0 } else { 255.0 })
.collect()
}
fn make_vertical_step(w: usize, h: usize) -> Vec<f32> {
(0..w * h)
.map(|i| if i % w < w / 2 { 0.0 } else { 255.0 })
.collect()
}
fn make_checkerboard(w: usize, h: usize, cell: usize) -> 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()
}
fn make_circle(w: usize, h: usize, radius: f32) -> Vec<f32> {
let cx = w as f32 / 2.0;
let cy = h as f32 / 2.0;
(0..w * h)
.map(|i| {
let x = (i % w) as f32 - cx;
let y = (i / w) as f32 - cy;
if (x * x + y * y).sqrt() < radius {
255.0
} else {
0.0
}
})
.collect()
}
fn make_noise(w: usize, h: usize, seed: u64) -> Vec<f32> {
let mut state = seed;
(0..w * h)
.map(|_| {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((state >> 33) & 0xFF) as f32
})
.collect()
}
fn make_gradient(w: usize, h: usize) -> Vec<f32> {
if w <= 1 {
return vec![0.0f32; w * h];
}
(0..w * h)
.map(|i| (i % w) as f32 / (w - 1) as f32 * 255.0)
.collect()
}
fn make_concentric_rects(w: usize, h: usize) -> Vec<f32> {
let mut img = vec![0.0f32; w * h];
for margin in [10usize, 25, 45, 70] {
if margin * 2 >= w || margin * 2 >= h {
break;
}
for x in margin..w - margin {
img[margin * w + x] = 255.0;
img[(h - margin - 1) * w + x] = 255.0;
}
for y in margin..h - margin {
img[y * w + margin] = 255.0;
img[y * w + (w - margin - 1)] = 255.0;
}
}
img
}
struct Case {
label: &'static str,
src: Vec<f32>,
w: usize,
h: usize,
cfg: CannyConfig,
expect_min: Option<usize>,
expect_max: Option<usize>,
}
impl Case {
fn run(&self, ws: &mut CannyWorkspace) {
let t0 = Instant::now();
let src_path = format!("{}/{}_src.png", OUT_DIR, self.label);
save_f32_as_gray(&self.src, self.w, self.h, &src_path);
let edge_map = canny(&self.src, ws, &self.cfg)
.unwrap_or_else(|e| panic!("[{}] canny failed: {}", self.label, e));
assert_binary(edge_map, self.label);
assert_eq!(
edge_map.len(),
self.w * self.h,
"[{}] output length mismatch",
self.label
);
let (count, density) = edge_stats(edge_map, self.w, self.h);
if let Some(min) = self.expect_min {
assert!(
count >= min,
"[{}] too few edges: {} < {} (density={:.2}%)",
self.label,
count,
min,
density
);
}
if let Some(max) = self.expect_max {
assert!(
count <= max,
"[{}] too many edges: {} > {} (density={:.2}%)",
self.label,
count,
max,
density
);
}
let edge_path = format!("{}/{}_edge.png", OUT_DIR, self.label);
save_gray(edge_map, self.w, self.h, &edge_path);
save_overlay(
&self.src,
edge_map,
self.w,
self.h,
&format!("{}/{}_overlay.png", OUT_DIR, self.label),
);
log::info!(
"[{}] edges={} ({:.2}%), elapsed={:.2}ms",
self.label,
count,
density,
t0.elapsed().as_secs_f64() * 1000.0
);
println!(
" ✓ {:30} edges={:6} ({:5.2}%) [{:.2}ms]",
self.label,
count,
density,
t0.elapsed().as_secs_f64() * 1000.0
);
}
}
fn save_overlay(src: &[f32], edge_map: &[u8], w: usize, h: usize, path: &str) {
use image::RgbImage;
let max = src.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let min = src.iter().cloned().fold(f32::INFINITY, f32::min);
let range = (max - min).max(1e-6);
let mut img = RgbImage::new(w as u32, h as u32);
for (i, pixel) in img.pixels_mut().enumerate() {
let gray = ((src[i] - min) / range * 255.0) as u8;
if edge_map[i] == 255 {
*pixel = image::Rgb([255u8, 0, 0]); } else {
*pixel = image::Rgb([gray, gray, gray]);
}
}
img.save(path).expect("failed to save overlay");
log::info!("[save] {}", path);
}
fn verify_multiframe_reset(ws: &mut CannyWorkspace) {
let w = ws.width;
let h = ws.height;
let cfg = CannyConfig::builder()
.sigma(0.0)
.thresholds(10.0, 30.0)
.build()
.unwrap();
let src_step = make_vertical_step(w, h);
let edge1 = canny(&src_step, ws, &cfg).unwrap().to_vec();
let (count1, _) = edge_stats(&edge1, w, h);
let src_uniform = make_uniform(w, h, 128.0);
let edge2 = canny(&src_uniform, ws, &cfg).unwrap().to_vec();
let (count2, _) = edge_stats(&edge2, w, h);
let edge3 = canny(&src_step, ws, &cfg).unwrap().to_vec();
let (count3, _) = edge_stats(&edge3, w, h);
assert_eq!(
count2, 0,
"frame2 (uniform) should have 0 edges, got {}",
count2
);
assert_eq!(
count1, count3,
"frame1 and frame3 (same input) should have same edge count: {} vs {}",
count1, count3
);
assert_eq!(
edge1, edge3,
"frame1 and frame3 edge maps must be identical"
);
println!(
" ✓ frame1(step)={} edges, frame2(uniform)={} edges, frame3(step)={} edges — reset OK",
count1, count2, count3
);
log::info!(
"[multiframe] count1={}, count2={}, count3={}",
count1,
count2,
count3
);
}
fn threshold_sensitivity_scan(ws: &mut CannyWorkspace) {
println!("\n[阈值敏感性扫描 — 棋盘格 128x128]");
let (w, h) = (128, 128);
let src = make_checkerboard(w, h, 16);
let configs: &[(&str, f32, f32)] = &[
("very_low", 5.0, 15.0),
("low", 20.0, 60.0),
("medium", 50.0, 150.0),
("high", 100.0, 200.0),
("very_high", 200.0, 400.0),
];
for &(label, low, high) in configs {
let cfg = CannyConfig::builder()
.sigma(1.0)
.thresholds(low, high)
.build()
.unwrap();
let edge_map = canny(&src, ws, &cfg).unwrap();
let (count, density) = edge_stats(edge_map, w, h);
let path = format!("{}/threshold_{}_{}_edge.png", OUT_DIR, label, w);
save_gray(edge_map, w, h, &path);
println!(
" {:12} low={:5.0} high={:5.0} edges={:5} ({:.2}%)",
label, low, high, count, density
);
}
}
fn sigma_sensitivity_scan(ws: &mut CannyWorkspace) {
println!("\n[Sigma 敏感性扫描 — 圆形 128x128]");
let (w, h) = (128, 128);
let src = make_circle(w, h, 40.0);
let sigmas: &[f32] = &[0.0, 0.5, 1.0, 2.0, 4.0];
for &sigma in sigmas {
let cfg = CannyConfig::builder()
.sigma(sigma)
.thresholds(20.0, 60.0)
.build()
.unwrap();
let edge_map = canny(&src, ws, &cfg).unwrap();
let (count, density) = edge_stats(edge_map, w, h);
let label = format!("sigma_{:.1}", sigma).replace('.', "_");
let path = format!("{}/circle_{}_edge.png", OUT_DIR, label);
save_gray(edge_map, w, h, &path);
println!(" sigma={:.1} edges={:5} ({:.2}%)", sigma, count, density);
}
}
fn main() {
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();
fs::create_dir_all(OUT_DIR).expect("failed to create output dir");
println!("输出目录:{}\n", OUT_DIR);
let cfg_default = || {
CannyConfig::builder()
.sigma(1.0)
.thresholds(30.0, 90.0)
.build()
.unwrap()
};
let cfg_sharp = || {
CannyConfig::builder()
.sigma(0.0)
.thresholds(10.0, 30.0)
.build()
.unwrap()
};
let (w, h) = (256, 256);
let cases: Vec<Case> = vec![
Case {
label: "01_uniform",
src: make_uniform(w, h, 128.0),
w,
h,
cfg: cfg_default(),
expect_min: None,
expect_max: Some(0),
},
Case {
label: "02_vertical_step",
src: make_vertical_step(w, h),
w,
h,
cfg: cfg_sharp(),
expect_min: Some(1),
expect_max: None,
},
Case {
label: "03_horizontal_step",
src: make_horizontal_step(w, h),
w,
h,
cfg: cfg_sharp(),
expect_min: Some(1),
expect_max: None,
},
Case {
label: "04_checkerboard",
src: make_checkerboard(w, h, 32),
w,
h,
cfg: cfg_default(),
expect_min: Some(10),
expect_max: None,
},
Case {
label: "05_circle",
src: make_circle(w, h, 80.0),
w,
h,
cfg: cfg_default(),
expect_min: Some(10),
expect_max: None,
},
Case {
label: "06_noise_high_thresh",
src: make_noise(w, h, 42),
w,
h,
cfg: CannyConfig::builder()
.sigma(1.5)
.thresholds(80.0, 200.0)
.build()
.unwrap(),
expect_min: None,
expect_max: None,
},
Case {
label: "07_noise_low_thresh",
src: make_noise(w, h, 42),
w,
h,
cfg: CannyConfig::builder()
.sigma(0.5)
.thresholds(10.0, 30.0)
.build()
.unwrap(),
expect_min: None,
expect_max: None,
},
Case {
label: "08_gradient",
src: make_gradient(w, h),
w,
h,
cfg: CannyConfig::builder()
.sigma(1.0)
.thresholds(100.0, 200.0)
.build()
.unwrap(),
expect_min: None,
expect_max: None,
},
Case {
label: "09_concentric_rects",
src: make_concentric_rects(w, h),
w,
h,
cfg: cfg_sharp(),
expect_min: Some(10),
expect_max: None,
},
Case {
label: "10_min_size_3x3",
src: make_vertical_step(3, 3),
w: 3,
h: 3,
cfg: CannyConfig::builder()
.sigma(0.0)
.thresholds(1.0, 10.0)
.build()
.unwrap(),
expect_min: None,
expect_max: None,
},
];
let mut ws = CannyWorkspace::new(w, h).expect("workspace creation failed");
println!("=== 基础用例验证 ===");
for case in &cases {
if case.w != w || case.h != h {
let mut small_ws =
CannyWorkspace::new(case.w, case.h).expect("small workspace creation failed");
case.run(&mut small_ws);
} else {
case.run(&mut ws);
}
}
let mut ws64 = CannyWorkspace::new(64, 64).unwrap();
verify_multiframe_reset(&mut ws64);
let mut ws128 = CannyWorkspace::new(128, 128).unwrap();
threshold_sensitivity_scan(&mut ws128);
sigma_sensitivity_scan(&mut ws128);
println!("\n✅ 所有验证通过,结果图像已保存至:{}/", OUT_DIR);
println!(" 每个用例生成三张图:_src.png / _edge.png / _overlay.png");
}