昨天把「一串效果」丟進 WASM 一口氣跑完,今天就加兩個最常用的功能:模糊和銳化。模糊我選擇「方框模糊(box blur)」,因為它好寫、速度穩、視覺感受也很直覺;銳化則用一個 3×3 的卷積核就能有感。整體思路不變:前端用陣列描述操作,Rust 端把它反序列化成 enum,依序對 Vec<u8>
走過去。你會發現效果越來越多,但呼叫方式幾乎沒變。
先把 enum 擴充一下。前兩天我們有 grayscale
與 bc
(亮度/對比),今天再加 blur
和 conv3x3
兩個變體。blur
只要一個半徑 r
,conv3x3
就是九個係數的陣列。為了效能,盒狀模糊用兩段式寫法(先水平、再垂直),用滑動視窗把加總維持在 O(1) 更新;邊界用「延伸邊界」的直覺處理(超出就黏在邊上),Alpha 全程保留原值不動。
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
。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
。
把 #[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