iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
Rust

把前端加速到天花板:Rust+WASM 即插即用外掛系列 第 11

Day 10|Sobel 邊緣偵測:抓到你了,臭邊緣!

  • 分享至 

  • xImage
  •  

寫完銳化、卷積之後,下一步就是「如何找出邊緣」。Sobel 幾乎是所有影像處理教材裡的第一個邊緣偵測器。它的概念很簡單~用兩個小小的 3×3 卷積核,分別測量水平與垂直的強度變化,再把這兩個梯度合起來。哪裡亮度變化最大,哪裡就有邊。

這個方法的好處在於簡潔。係數全是 -2、-1、0、1、2 這種小整數,不需要浮點乘法,甚至不用開根號。直接拿 gx、gy 做個近似 |gx| + |gy|,再右移幾位縮放,就能得到足夠清楚的邊緣圖。對我們這種跑在 WASM 裡的 memory-bound 管線來說,穩的辣。

怎麼寫

照 Day 7 的結構,這裡提供兩個版本:

  • edge_sobel:配置回傳 Vec<u8>,給慢速 apply_pipeline 用。
  • edge_sobel_into:吃來源 slice、寫目的 slice,零配置,給 ping-pong 管子用。

步驟分三個:

  1. 先把 RGBA 轉成亮度緩衝(BT.601 加權:77R + 150G + 29B >> 8)。這樣卷積時不用每次處理三通道。
  2. 對每個像素做兩個方向的 3×3 卷積,算 gx 和 gy。邊界用 clamp 避免越界。
  3. 計算強度 (|gx| + |gy|) >> 3,夾在 0–255 間,寫回 RGB。

Rust 部分

// 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 之後會變成黑底白線。

https://ithelp.ithome.com.tw/upload/images/20250925/20162491Wj5XQyMmF9.png

關於 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


上一篇
Day 9|銳化與對比效果組合(Unsharp Mask)
系列文
把前端加速到天花板:Rust+WASM 即插即用外掛11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言