iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0
Rust

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

Day 7|模糊 × 銳化一次滿足

  • 分享至 

  • xImage
  •  

昨天把「一串效果」丟進 WASM 一口氣跑完,今天就加兩個最常用的功能:模糊銳化。模糊我選擇「方框模糊(box blur)」,因為它好寫、速度穩、視覺感受也很直覺;銳化則用一個 3×3 的卷積核就能有感。整體思路不變:前端用陣列描述操作,Rust 端把它反序列化成 enum,依序對 Vec<u8> 走過去。你會發現效果越來越多,但呼叫方式幾乎沒變。

先把 enum 擴充一下。前兩天我們有 grayscalebc(亮度/對比),今天再加 blurconv3x3 兩個變體。blur 只要一個半徑 rconv3x3 就是九個係數的陣列。為了效能,盒狀模糊用兩段式寫法(先水平、再垂直),用滑動視窗把加總維持在 O(1) 更新;邊界用「延伸邊界」的直覺處理(超出就黏在邊上),Alpha 全程保留原值不動。

Rust 部分

方框模糊:兩段式 + 滑動視窗

fn box_blur_rgba(input: &[u8], w: u32, h: u32, r: u32) -> Vec<u8> { /* ... */ }

這裡我先做一維的水平平均,再做一維的垂直平均,合起來就是二維的方盒濾波。每一維裡面我們用滑動視窗維持加總,只要把新進來的像素加上、離開視窗的像素減掉,就能在 O(1) 更新平均值,不需要每個像素都重算整個窗口。

  • u32 做加總避免溢位。
  • 視窗寬 win = 2*r+1,平均就是 sum / win
  • 先水平寫入 tmp,再用 tmp 做垂直得到 out

銳化:3×3 卷積

fn convolve3x3(input: &[u8], w: u32, h: u32, k: &[f32; 9]) -> Vec<u8> { /* ... */ }

銳化這裡走的是一般化的 3×3 卷積。核心邏輯就是對每個像素把周圍九個點取出來,乘上對應的 kernel 係數累加成新的 RGB,再把結果四捨五入、夾回 0 到 255;邊界一樣用夾取,Alpha 一律原封不動。這樣你要銳化就丟 [0,-1,0,-1,5,-1,0,-1,0],要邊緣就丟 [-1,-1,-1,-1,8,-1,-1,-1,-1],要做其他效果只要換核即可。把模糊和卷積都放進 enum 之後,前端的呼叫方式沒有任何改變,依舊是組一串 { kind, ...params } 的物件丟進 apply_pipeline

Full code

#[derive(Deserialize)] 後的整個改掉,改成以下的 code:

#[derive(Deserialize)]
#[serde(tag = "kind")]
enum Op {
    #[serde(rename = "grayscale")]
    Grayscale,
    #[serde(rename = "bc")]
    BrightnessContrast { b: f64, c: f64 },
    #[serde(rename = "blur")]
    Blur { r: u32 },
    #[serde(rename = "conv3x3")]
    Conv3x3 { k: [f32; 9] },
}

#[wasm_bindgen]
pub fn apply_pipeline(input: &[u8], w: u32, h: u32, ops: &JsValue) -> Result<Vec<u8>, JsValue> {
    let expected = (w as usize) * (h as usize) * 4;
    if input.len() != expected {
        return Err(JsValue::from_str("input length mismatch"));
    }
    let ops: Vec<Op> = swb::from_value(ops.clone())
        .map_err(|e| JsValue::from_str(&format!("bad ops: {e}")))?;

    let mut buf = input.to_vec();
    for op in ops {
        match op {
            Op::Grayscale => {
                buf = grayscale(&buf, w, h);
            }
            Op::BrightnessContrast { b, c } => {
                buf = brightness_contrast(&buf, w, h, b, c);
            }
            Op::Blur { r } => {
                buf = box_blur_rgba(&buf, w, h, r);
            }
            Op::Conv3x3 { k } => {
                buf = convolve3x3(&buf, w, h, &k);
            }
        }
    }
    Ok(buf)
}

/// 兩段式盒狀模糊:先水平再垂直;Alpha 保留不變
fn box_blur_rgba(input: &[u8], w: u32, h: u32, r: u32) -> Vec<u8> {
    if r == 0 { return input.to_vec(); }
    let w = w as usize;
    let h = h as usize;
    let win = (2 * r + 1) as usize;

    // 水平 pass
    let mut tmp = vec![0u8; input.len()];
    for y in 0..h {
        // 初始化第一個視窗加總(延伸邊界)
        let mut sr: u32 = 0; let mut sg: u32 = 0; let mut sb: u32 = 0;
        for dx in 0..win {
            let x = clamp_i(dx as isize - r as isize, 0, (w - 1) as isize) as usize;
            let i = (y * w + x) * 4;
            sr += input[i] as u32;
            sg += input[i + 1] as u32;
            sb += input[i + 2] as u32;
        }
        // 寫第一個像素
        let mut i0 = (y * w) * 4;
        tmp[i0] = (sr / win as u32) as u8;
        tmp[i0 + 1] = (sg / win as u32) as u8;
        tmp[i0 + 2] = (sb / win as u32) as u8;
        tmp[i0 + 3] = input[i0 + 3]; // alpha 原樣

        // 滑動視窗
        for x in 1..w {
            let x_add = clamp_i(x as isize + r as isize, 0, (w - 1) as isize) as usize;
            let x_sub = clamp_i(x as isize - 1 - r as isize, 0, (w - 1) as isize) as usize;
            let i_add = (y * w + x_add) * 4;
            let i_sub = (y * w + x_sub) * 4;
            sr = sr + input[i_add] as u32 - input[i_sub] as u32;
            sg = sg + input[i_add + 1] as u32 - input[i_sub + 1] as u32;
            sb = sb + input[i_add + 2] as u32 - input[i_sub + 2] as u32;

            let i = (y * w + x) * 4;
            tmp[i] = (sr / win as u32) as u8;
            tmp[i + 1] = (sg / win as u32) as u8;
            tmp[i + 2] = (sb / win as u32) as u8;
            tmp[i + 3] = input[i + 3];
        }
    }

    // 垂直 pass(從 tmp 到 out)
    let mut out = vec![0u8; input.len()];
    for x in 0..w {
        let mut sr: u32 = 0; let mut sg: u32 = 0; let mut sb: u32 = 0;
        for dy in 0..win {
            let y = clamp_i(dy as isize - r as isize, 0, (h - 1) as isize) as usize;
            let i = (y * w + x) * 4;
            sr += tmp[i] as u32;
            sg += tmp[i + 1] as u32;
            sb += tmp[i + 2] as u32;
        }
        let mut i0 = x * 4;
        out[i0] = (sr / win as u32) as u8;
        out[i0 + 1] = (sg / win as u32) as u8;
        out[i0 + 2] = (sb / win as u32) as u8;
        out[i0 + 3] = input[i0 + 3];

        for y in 1..h {
            let y_add = clamp_i(y as isize + r as isize, 0, (h - 1) as isize) as usize;
            let y_sub = clamp_i(y as isize - 1 - r as isize, 0, (h - 1) as isize) as usize;
            let i_add = (y_add * w + x) * 4;
            let i_sub = (y_sub * w + x) * 4;
            sr = sr + tmp[i_add] as u32 - tmp[i_sub] as u32;
            sg = sg + tmp[i_add + 1] as u32 - tmp[i_sub + 1] as u32;
            sb = sb + tmp[i_add + 2] as u32 - tmp[i_sub + 2] as u32;

            let i = (y * w + x) * 4;
            out[i] = (sr / win as u32) as u8;
            out[i + 1] = (sg / win as u32) as u8;
            out[i + 2] = (sb / win as u32) as u8;
            out[i + 3] = input[i + 3];
        }
    }
    out
}

fn clamp_i(v: isize, lo: isize, hi: isize) -> isize {
    if v < lo { lo } else if v > hi { hi } else { v }
}

/// 一般 3×3 卷積:RGB 套核、Alpha 保留;kernel 長度必須為 9
fn convolve3x3(input: &[u8], w: u32, h: u32, k: &[f32; 9]) -> Vec<u8> {
    let w = w as usize;
    let h = h as usize;
    let mut out = vec![0u8; input.len()];

    for y in 0..h {
        for x in 0..w {
            let mut acc = [0f32; 3];
            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 s = (sy * w + sx) * 4;
                    let kv = k[ky * 3 + kx];
                    acc[0] += kv * input[s] as f32;
                    acc[1] += kv * input[s + 1] as f32;
                    acc[2] += kv * input[s + 2] as f32;
                }
            }
            let i = (y * w + x) * 4;
            out[i]     = acc[0].round().clamp(0.0, 255.0) as u8;
            out[i + 1] = acc[1].round().clamp(0.0, 255.0) as u8;
            out[i + 2] = acc[2].round().clamp(0.0, 255.0) as u8;
            out[i + 3] = input[i + 3];
        }
    }
    out
}

重新打包

rm -rf pkg
wasm-pack build --target web --out-dir pkg --out-name rustwasm_test

前端部分

// 加兩顆按鈕到 UI(innerHTML裡)
  <div style="margin-top:12px;display:flex;gap:12px;">
    <button id="blur2">模糊 r=2</button>
    <button id="sharpen">銳化</button>
  </div>
  
  
// 下面就是呼叫韓式的邏輯,放在最下面就好了

const blur2Btn = document.querySelector<HTMLButtonElement>('#blur2')!
const sharpenBtn = document.querySelector<HTMLButtonElement>('#sharpen')!

blur2Btn.onclick = () => {
  runPipeline([{ kind: 'blur', r: 2 }]) // 盒狀模糊半徑 2
}

sharpenBtn.onclick = () => {
  runPipeline([{ kind: 'conv3x3', k: [0,-1,0, -1,5,-1, 0,-1,0] }]) // 3x3 銳化核
}

重裝套件,啟動

cd demo
pnpm remove rustwasm-test
pnpm add file:../pkg
pnpm dev

https://ithelp.ithome.com.tw/upload/images/20250922/20162491D7oH8WJaBl.png


上一篇
Day 6|小小的 pipeline 串起來 o(☆Ф∇Ф☆)o
系列文
把前端加速到天花板:Rust+WASM 即插即用外掛8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言