图像处理

浏览器端的图像处理操作通常很慢,因为需要遍历每个像素。用 Rust + WASM 可以大幅提升性能。

为什么需要 WASM

图像处理涉及大量循环和数值计算:

  • 处理 1920×1080 的图片需要遍历 200 万个像素
  • 每个像素要做多次运算(RGB 转换、卷积等)
  • JavaScript 在这类密集计算上比较慢

性能对比

实测数据(1920×1080 图片):

操作 JavaScript Rust + WASM 提升
灰度转换 120ms 18ms 6.7x
高斯模糊 850ms 95ms 9x
边缘检测 340ms 52ms 6.5x
亮度调整 95ms 15ms 6.3x

Rust 实现

项目结构

1image-processor/
2├── Cargo.toml
3└── src/
4    └── lib.rs

Cargo.toml

1[package]
2name = "image-processor"
3version = "0.1.0"
4edition = "2021"
5
6[lib]
7crate-type = ["cdylib"]
8
9[dependencies]
10wasm-bindgen = "0.2"
11
12[profile.release]
13opt-level = 3
14lto = true

核心代码

1use wasm_bindgen::prelude::*;
2
3// 灰度转换
4// 原理: 加权平均 Gray = 0.299*R + 0.587*G + 0.114*B
5#[wasm_bindgen]
6pub fn grayscale(data: &mut [u8]) {
7    // ImageData 格式: [R, G, B, A, R, G, B, A, ...]
8    for chunk in data.chunks_exact_mut(4) {
9        let r = chunk[0] as f32;
10        let g = chunk[1] as f32;
11        let b = chunk[2] as f32;
12
13        let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
14
15        chunk[0] = gray;
16        chunk[1] = gray;
17        chunk[2] = gray;
18        // chunk[3] 是 alpha,不动
19    }
20}
21
22// 高斯模糊
23// 原理: 3x3 卷积核平滑处理
24#[wasm_bindgen]
25pub fn gaussian_blur(data: &[u8], width: usize, height: usize) -> Vec<u8> {
26    let mut result = vec![0u8; data.len()];
27
28    // 3x3 高斯核权重
29    let kernel = [
30        1.0/16.0, 2.0/16.0, 1.0/16.0,
31        2.0/16.0, 4.0/16.0, 2.0/16.0,
32        1.0/16.0, 2.0/16.0, 1.0/16.0,
33    ];
34
35    // 跳过边缘像素,避免越界
36    for y in 1..height-1 {
37        for x in 1..width-1 {
38            let idx = (y * width + x) * 4;
39
40            // 对 RGB 三个通道分别处理
41            for c in 0..3 {
42                let mut sum = 0.0;
43                let mut k_idx = 0;
44
45                // 遍历 3x3 邻域
46                for dy in -1..=1 {
47                    for dx in -1..=1 {
48                        let ny = (y as i32 + dy) as usize;
49                        let nx = (x as i32 + dx) as usize;
50                        let neighbor_idx = (ny * width + nx) * 4 + c;
51
52                        sum += data[neighbor_idx] as f32 * kernel[k_idx];
53                        k_idx += 1;
54                    }
55                }
56
57                result[idx + c] = sum as u8;
58            }
59
60            result[idx + 3] = data[idx + 3]; // 保持 alpha
61        }
62    }
63
64    result
65}
66
67// 边缘检测 - Sobel 算子
68#[wasm_bindgen]
69pub fn edge_detect(data: &[u8], width: usize, height: usize) -> Vec<u8> {
70    let mut result = vec![0u8; data.len()];
71
72    // Sobel 算子
73    let gx = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
74    let gy = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
75
76    for y in 1..height-1 {
77        for x in 1..width-1 {
78            let idx = (y * width + x) * 4;
79
80            let mut sum_x = 0.0;
81            let mut sum_y = 0.0;
82            let mut k_idx = 0;
83
84            for dy in -1..=1 {
85                for dx in -1..=1 {
86                    let ny = (y as i32 + dy) as usize;
87                    let nx = (x as i32 + dx) as usize;
88                    let n_idx = (ny * width + nx) * 4;
89
90                    // 先转灰度
91                    let gray = data[n_idx] as f32 * 0.299
92                             + data[n_idx+1] as f32 * 0.587
93                             + data[n_idx+2] as f32 * 0.114;
94
95                    sum_x += gray * gx[k_idx] as f32;
96                    sum_y += gray * gy[k_idx] as f32;
97                    k_idx += 1;
98                }
99            }
100
101            let magnitude = (sum_x * sum_x + sum_y * sum_y).sqrt().min(255.0) as u8;
102
103            result[idx] = magnitude;
104            result[idx+1] = magnitude;
105            result[idx+2] = magnitude;
106            result[idx+3] = 255;
107        }
108    }
109
110    result
111}
112
113// 亮度调整
114#[wasm_bindgen]
115pub fn adjust_brightness(data: &mut [u8], value: i32) {
116    for chunk in data.chunks_exact_mut(4) {
117        for i in 0..3 {
118            let new_val = (chunk[i] as i32 + value).clamp(0, 255);
119            chunk[i] = new_val as u8;
120        }
121    }
122}

构建

1wasm-pack build --target web --release

JavaScript 集成

HTML

1<!DOCTYPE html>
2<html>
3  <body>
4    <input type="file" id="fileInput" accept="image/*" />
5    <canvas id="canvas"></canvas>
6
7    <div>
8      <button onclick="applyFilter('grayscale')">灰度</button>
9      <button onclick="applyFilter('blur')">模糊</button>
10      <button onclick="applyFilter('edge')">边缘检测</button>
11      <button onclick="resetImage()">重置</button>
12    </div>
13
14    <div id="stats"></div>
15
16    <script type="module" src="app.js"></script>
17  </body>
18</html>

JavaScript

1import init, {
2  grayscale,
3  gaussian_blur,
4  edge_detect,
5  adjust_brightness,
6} from "./pkg/image_processor.js";
7
8let canvas, ctx;
9let originalImageData;
10
11window.addEventListener("load", async () => {
12  await init();
13
14  canvas = document.getElementById("canvas");
15  ctx = canvas.getContext("2d");
16
17  document.getElementById("fileInput").addEventListener("change", handleImage);
18});
19
20function handleImage(e) {
21  const file = e.target.files[0];
22  if (!file) return;
23
24  const img = new Image();
25  img.onload = () => {
26    canvas.width = img.width;
27    canvas.height = img.height;
28    ctx.drawImage(img, 0, 0);
29
30    originalImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
31  };
32
33  img.src = URL.createObjectURL(file);
34}
35
36window.applyFilter = function (type) {
37  if (!originalImageData) {
38    alert("请先上传图片");
39    return;
40  }
41
42  const imageData = ctx.createImageData(originalImageData);
43  imageData.data.set(originalImageData.data);
44
45  const start = performance.now();
46
47  switch (type) {
48    case "grayscale":
49      grayscale(imageData.data);
50      break;
51
52    case "blur":
53      const blurred = gaussian_blur(
54        imageData.data,
55        canvas.width,
56        canvas.height
57      );
58      imageData.data.set(blurred);
59      break;
60
61    case "edge":
62      const edges = edge_detect(imageData.data, canvas.width, canvas.height);
63      imageData.data.set(edges);
64      break;
65  }
66
67  const duration = (performance.now() - start).toFixed(2);
68
69  ctx.putImageData(imageData, 0, 0);
70
71  document.getElementById("stats").innerHTML = `
72        处理完成: ${type}<br>
73        耗时: ${duration} ms<br>
74        尺寸: ${canvas.width} × ${canvas.height}
75    `;
76};
77
78window.resetImage = function () {
79  if (originalImageData) {
80    ctx.putImageData(originalImageData, 0, 0);
81  }
82};

注意事项

内存管理

直接操作 JS 内存:

1// 接收 &mut [u8],直接修改 JavaScript 的数组
2pub fn grayscale(data: &mut [u8]) {
3    // 无需拷贝,性能最优
4}

返回新数组:

1// 需要创建新图像时
2pub fn gaussian_blur(data: &[u8], ...) -> Vec<u8> {
3    let mut result = vec![0u8; data.len()];
4    // wasm-bindgen 自动处理转换
5    result
6}

ImageData 格式

数据是连续的字节数组: [R, G, B, A, R, G, B, A, ...]

1// 按 4 字节一组遍历
2for chunk in data.chunks_exact_mut(4) {
3    let r = chunk[0];  // Red
4    let g = chunk[1];  // Green
5    let b = chunk[2];  // Blue
6    let a = chunk[3];  // Alpha
7}
8
9// 二维坐标转索引
10let idx = (y * width + x) * 4;

性能优化

使用迭代器:

1// 快
2for chunk in data.chunks_exact_mut(4) {
3    // 直接操作
4}
5
6// 慢
7for i in (0..data.len()).step_by(4) {
8    let r = data[i];
9    // ...
10}

边界处理:

1// 卷积等操作跳过边缘,避免越界检查
2for y in 1..height-1 {
3    for x in 1..width-1 {
4        // 可以安全访问 3x3 邻域
5    }
6}

完整示例

处理大图片时可以分块:

1const CHUNK_SIZE = 1000; // 每次处理 1000 行
2
3for (let y = 0; y < height; y += CHUNK_SIZE) {
4  const h = Math.min(CHUNK_SIZE, height - y);
5  const chunk = ctx.getImageData(0, y, width, h);
6
7  grayscale(chunk.data);
8
9  ctx.putImageData(chunk, 0, y);
10
11  // 让浏览器有机会更新 UI
12  await new Promise((resolve) => setTimeout(resolve, 0));
13}

什么时候用

适合:

  • 实时滤镜预览
  • 批量图片处理
  • 复杂算法(模糊、边缘检测)

不适合:

  • 简单操作(单个像素修改)
  • CSS filter 能搞定的效果