寫完銳化、卷積之後,下一步就是「如何找出邊緣」。Sobel 幾乎是所有影像處理教材裡的第一個邊緣偵測器。它的概念很簡單~用兩個小小的 3×3 卷積核,分別測量水平與垂直的強度變化,再把這兩個梯度合起來。哪裡亮度變化最大,哪裡就有邊。
這個方法的好處在於簡潔。係數全是 -2、-1、0、1、2 這種小整數,不需要浮點乘法,甚至不用開根號。直接拿 gx、gy 做個近似 |gx| + |gy|
,再右移幾位縮放,就能得到足夠清楚的邊緣圖。對我們這種跑在 WASM 裡的 memory-bound 管線來說,穩的辣。
照 Day 7 的結構,這裡提供兩個版本:
Vec<u8>
,給慢速 apply_pipeline
用。步驟分三個:
(|gx| + |gy|) >> 3
,夾在 0–255 間,寫回 RGB。// Op 枚舉加一個 edge_sobel
#[derive(Deserialize)]
#[serde(tag = "kind")]
enum Op {
/* ...既有分支... */
#[serde(rename = "edge_sobel")]
EdgeSobel,
}
// 零配置版:ping-pong 用
fn edge_sobel_into(src: &[u8], dst: &mut [u8], w: u32, h: u32) {
let w = w as usize;
let h = h as usize;
// 亮度緩衝
let mut ybuf = vec![0u8; w * h];
let mut i = 0usize;
let mut p = 0usize;
while i < src.len() {
let r = src[i] as u16;
let g = src[i + 1] as u16;
let b = src[i + 2] as u16;
ybuf[p] = ((77 * r + 150 * g + 29 * b) >> 8) as u8;
i += 4;
p += 1;
}
// Sobel 卷積
for y in 0..h {
for x in 0..w {
let mut gx: i32 = 0;
let mut gy: i32 = 0;
for ky in 0..3 {
for kx in 0..3 {
let sx = clamp_i(x as isize + kx as isize - 1, 0, (w - 1) as isize) as usize;
let sy = clamp_i(y as isize + ky as isize - 1, 0, (h - 1) as isize) as usize;
let v = ybuf[sy * w + sx] as i32;
let (cx, cy) = match (ky, kx) {
(0,0)=> (-1, 1),(0,1)=>(0, 2),(0,2)=>( 1, 1),
(1,0)=> (-2, 0),(1,1)=>(0, 0),(1,2)=>( 2, 0),
(2,0)=> (-1, -1),(2,1)=>(0,-2),(2,2)=>( 1, -1),
_ => (0,0),
};
gx += cx * v;
gy += cy * v;
}
}
let mag = ((gx.abs() + gy.abs()) >> 3).clamp(0,255) as u8;
let i_dst = (y * w + x) * 4;
dst[i_dst] = mag;
dst[i_dst + 1] = mag;
dst[i_dst + 2] = mag;
dst[i_dst + 3] = src[i_dst + 3];
}
}
}
// 配置回傳版:慢速管線用
fn edge_sobel(input: &[u8], w: u32, h: u32) -> Vec<u8> {
let mut out = vec![0u8; input.len()];
edge_sobel_into(input, &mut out, w, h);
out
}
// apply_pipeline 加分支
buf = match op {
/* ...既有分支... */
Op::EdgeSobel => edge_sobel(&buf, w, h),
};
// apply_pipeline_fast 加分支
match op {
/* ...既有分支... */
Op::EdgeSobel => {
if !toggle { edge_sobel_into(&a, &mut b, w, h); }
else { edge_sobel_into(&b, &mut a, w, h); }
}
}
rm -rf pkg
wasm-pack build --target web --out-dir pkg --out-name rustwasm_test
<button id="sobel">Sobel 邊緣</button> // 加在 app.innerHTML 裡
// 跟 sobel 互動
const sobelBtn = document.querySelector<HTMLButtonElement>('#sobel')!
sobelBtn.onclick = () => { runPipeline([{ kind: 'edge_sobel' }]) }
cd demo
pnpm remove rustwasm-test
pnpm add file:../pkg
pnpm dev
跑過 Sobel 之後會變成黑底白線。
關於 Sobel 這裡有很好的介紹:
https://medium.com/%E9%9B%BB%E8%85%A6%E8%A6%96%E8%A6%BA/%E9%82%8A%E7%B7%A3%E5%81%B5%E6%B8%AC-%E7%B4%A2%E4%BC%AF%E7%AE%97%E5%AD%90-sobel-operator-95ca51c8d78a