🦀 Rust + WASM 实战系列 第 6 篇 阅读时间:约 6 分钟 | 实战可运行
📌 写在前面
前 5 篇都是单像素操作------每个像素独立算,互不干扰。
从这一篇开始,进入邻域操作 ------每个像素要参考周围 9 个邻居 。第一个登场的是"模糊":
- 均值模糊:邻居求平均
- 中值模糊:邻居取中位数
看似简单,但这是 Sobel、浮雕、锐化的基础。先把这两个搞清楚,后面的都好懂。
🚀 TL;DR
| 算法 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 均值模糊 | 3×3 邻域求平均 | 简单、快、效果柔和 | 把噪声也"抹"进去 |
| 中值模糊 | 3×3 邻域排序取中位数 | 椒盐噪声杀手 | 慢一点(要排序) |
核心:
less
3×3 邻域: ┌─────────────┐
│ p₁ p₂ p₃ │
│ p₄ p₅ p₆ │ ← p₅ = 当前像素
│ p₇ p₈ p₉ │ 周围 8 个是邻居
└─────────────┘
📖 目录
- [什么是 3×3 邻域](#什么是 3×3 邻域 "#%E4%B8%80%E4%BB%80%E4%B9%88%E6%98%AF-3%C3%973-%E9%82%BB%E5%9F%9F")
- 均值模糊:邻居求平均
- 中值模糊:邻居取中位数
- 关键代码
- 前端效果展示
- 两种算法对比
- [进阶:更大的核 / 多通道](#进阶:更大的核 / 多通道 "#%E4%B8%83%E8%BF%9B%E9%98%B6%E6%9B%B4%E5%A4%A7%E7%9A%84%E6%A0%B8--%E5%A4%9A%E9%80%9A%E9%81%93")
- 参考资料
一、什么是 3×3 邻域
对图片中任意一个像素 p₅ ,它周围有 8 个邻居:
css
上一行 同一行 下一行
┌─────┐ ┌─────┐ ┌─────┐
同一列前 │ p₁ │ │ p₂ │ │ p₃ │
├─────┤ ├─────┤ ├─────┤
同一列 │ p₄ │ │ p₅ ★│ │ p₆ │
├─────┤ ├─────┤ ├─────┤
同一列后 │ p₇ │ │ p₈ │ │ p₉ │
└─────┘ └─────┘ └─────┘
模糊 = 用 p₁~p₉ 这 9 个值的某种"组合"替代 p₅。
二、均值模糊:邻居求平均
公式
css
new_p₅ = (p₁ + p₂ + p₃ + p₄ + p₅ + p₆ + p₇ + p₈ + p₉) / 9
栗子
ini
原 3×3: 100 120 110 new_p₅ = (100+120+110+
130 125 115 130+125+115+
140 135 128 140+135+128) / 9
= 1103 / 9
≈ 122.6 → 122
边界处理
图片最外圈的像素没有完整的 8 个邻居:
| 位置 | 实际可用邻居数 |
|---|---|
| 角点 | 4 个(3×3 只有 1/4 在图内) |
| 边(非角) | 6 个 |
| 内部像素 | 9 个 |
常见处理 :只算存在的邻居 ,分母也跟着变。代码里用 count 累加即可。
视觉效果
原图: 均值模糊后:
████████ ▓▓▓▓▓▓▓▓
████ ████ ▓▓▓▓▓▓▓▓
████████ ▓▓▓▓▓▓▓▓
(硬边) (柔和渐变)
降噪的同时也让边缘变模糊。
三、中值模糊:邻居取中位数
公式
css
new_p₅ = median(p₁, p₂, p₃, p₄, p₅, p₆, p₇, p₈, p₉)
↑ 把 9 个值从小到大排序,取第 5 个
栗子
markdown
原 3×3: 100 120 110
130 125 115
140 135 128
排序后: 100 110 115 120 125 128 130 135 140
↑
中位数 = 125
关键特性:对椒盐噪声免疫
假设图里有个噪点(一个像素异常亮):
ini
3×3: 100 120 110
130 255 115 ← 255 是噪点
140 135 128
均值: (sum) / 9 = 1370 / 9 ≈ 152 ← 噪点"污染"了结果
中值: 排序取第 5 个 = 120 ← 噪点被忽略 ✅
这就是中值模糊的核心价值 :异常值被自动排除。
视觉效果
原图(带椒盐噪声) 均值模糊 中值模糊
█ ███ █ █ ▓▓▓▓▓▓▓ █ ███ █ █
████████ ▓▓▓▓▓▓▓ ████████
█ ███ █ █ ▓▓▓▓▓▓▓ █ ███ █ █
(白点噪点) (柔但有痕迹) (噪点消失!)
四、关键代码
均值模糊
rust
for y in 0..h {
for x in 0..w {
let mut sum_r = 0u32; let mut sum_g = 0u32; let mut sum_b = 0u32;
let mut count = 0u32;
// 遍历 3×3 邻域
for dy in -1..=1 {
for dx in -1..=1 {
let (nx, ny) = (x + dx, y + dy);
if nx < 0 || nx >= w || ny < 0 || ny >= h { continue; } // 越界跳过
let i = ((ny * w + nx) * 4) as usize;
sum_r += pixels[i] as u32;
sum_g += pixels[i + 1] as u32;
sum_b += pixels[i + 2] as u32;
count += 1;
}
}
// 平均 = 总和 / 数量
let o = ((y * w + x) * 4) as usize;
result[o] = (sum_r / count) as u8;
result[o + 1] = (sum_g / count) as u8;
result[o + 2] = (sum_b / count) as u8;
}
}
中值模糊
rust
for y in 0..h {
for x in 0..w {
// 收集 9 个邻居
let mut rs = [0u8; 9]; let mut gs = [0u8; 9]; let mut bs = [0u8; 9];
let mut count = 0;
for dy in -1..=1 {
for dx in -1..=1 {
let (nx, ny) = (x + dx, y + dy);
if nx < 0 || nx >= w || ny < 0 || ny >= h { continue; }
let i = ((ny * w + nx) * 4) as usize;
rs[count] = pixels[i];
gs[count] = pixels[i + 1];
bs[count] = pixels[i + 2];
count += 1;
}
}
// 排序,取中位数
rs[..count].sort();
gs[..count].sort();
bs[..count].sort();
let mid = count / 2;
result[o] = rs[mid];
result[o + 1] = gs[mid];
result[o + 2] = bs[mid];
}
}
⚠️ 一次 3×3 看不出效果?用 iterations
3×3 太微弱,对 1080p 图几乎察觉不到。支持迭代 N 次:
rust
pub fn mean_blur(pixels, w, h, iterations) -> Vec<u8> {
let mut result = pixels.to_vec();
for _ in 0..iterations {
result = mean_blur_once(&result, w, h); // 复用上面的逻辑
}
result
}
iterations |
等效大小 | 视觉效果 |
|---|---|---|
| 1 | 3×3 | 非常微弱 |
| 2 | ≈5×5 | 轻微可见 |
| 3 | ≈7×7 | 明显模糊 ⭐ 推荐 |
| 5 | ≈11×11 | 强模糊(油画感) |
中心极限定理:均匀卷积多次 ≈ 正态分布 → 比单次大盒式模糊更柔和自然。
五、前端效果展示


六、两种算法对比
| 维度 | 均值模糊 | 中值模糊 |
|---|---|---|
| 计算量 | 8 次加法 + 1 次除法 | 8 次收集 + 排序 9 个 |
| 速度 | 快(~5ms / 1080p) | 慢(~20ms) |
| 适用噪声 | 高斯噪声 | 椒盐噪声 |
| 保留边缘 | ❌ 边缘被抹掉 | ✅ 边缘保持更好 |
| 多次叠加 | 会越抹越糊 | 多次效果稳定 |
| 可分离性 | ✅(横向 + 纵向 5×5) | ❌(必须 2D 排序) |
选哪个?
- 一般照片降噪 → 均值模糊
- 去椒盐/扫描噪点 → 中值模糊 ⭐
- 需要速度 → 均值模糊
- 需要保边 → 中值模糊
七、进阶:更大的核 / 多通道
5×5 均值模糊
把内层循环从 -1..=1 改成 -2..=2,count 最大 25。
多次 3×3 模拟 5×5
数学上 :3 次 3×3 均值模糊 ≈ 1 次 5×5 均值模糊(但有 27 vs 25 的计算差)
实现:循环调 3 次即可
rust
let blurred_once = mean_blur(pixels, w, h);
let blurred_twice = mean_blur(&blurred_once, w, h);
let result = mean_blur(&blurred_twice, w, h);
灰度化 + 模糊
通常先转灰度再模糊,少 2/3 计算量:
js
// 1. 灰度
const gray = grayscale_with(pixels, w, h, 'luminance709')
// 2. 模糊
const blurred = mean_blur(gray, w, h)
八、参考资料
- Wikipedia - Box blur:均值模糊的数学
- Wikipedia - Median filter:中值滤波
- OpenCV 文档 :
cv2.blur()和cv2.medianBlur() - GIMP 文档:Filters → Blur → Gaussian Blur / Median Blur
🎁 写在最后
均值和中值模糊是邻域操作的"开胃菜"------接下来:
- Sobel 边缘检测 = 均值模糊的"亲戚"(用卷积核替代"求平均")
- 浮雕滤镜 = 中值模糊的"亲戚"(用差值替代"取中值")
- 高斯模糊 = 均值模糊的"升级版"(权重不是均匀的)
核心套路已经掌握:3×3 邻域 → 某种组合 → 输出。后面 5 个项目都跑不出这个框架。
觉得有帮助的话,点赞 👍、收藏 ⭐、关注 三连支持一下!
下篇预告:《马赛克像素化:分块取平均色实现打码风格》------ 模糊的"硬核版",把整块涂成一个色,敬请期待。
📦 项目地址 :pixel-math-wasm 🦀 Rust + WebAssembly 实战系列:图像处理 → 几何变换 → 分形 → 线代 → 概率 → 综合项目
🏷️ 标签 :#Rust #WebAssembly #图像处理 #模糊 #均值滤波 #中值滤波 #算法