浏览器端的图像处理操作通常很慢,因为需要遍历每个像素。用 Rust + WASM 可以大幅提升性能。
图像处理涉及大量循环和数值计算:
实测数据(1920×1080 图片):
| 操作 | JavaScript | Rust + WASM | 提升 |
|---|---|---|---|
| 灰度转换 | 120ms | 18ms | 6.7x |
| 高斯模糊 | 850ms | 95ms | 9x |
| 边缘检测 | 340ms | 52ms | 6.5x |
| 亮度调整 | 95ms | 15ms | 6.3x |
1image-processor/
2├── Cargo.toml
3└── src/
4 └── lib.rs1[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 = true1use 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 --release1<!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>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}数据是连续的字节数组: [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}适合:
不适合: