Rust图像处理第6节- 均值模糊 & 中值模糊:3×3 邻域的两种经典玩法

3 阅读6分钟

🦀 Rust + WASM 实战系列 第 6 篇 阅读时间:约 6 分钟 | 实战可运行

📌 写在前面

前 5 篇都是单像素操作——每个像素独立算,互不干扰。

从这一篇开始,进入邻域操作——每个像素要参考周围 9 个邻居第一个登场的是"模糊"

  • 均值模糊:邻居求平均
  • 中值模糊:邻居取中位数

看似简单,但这是 Sobel、浮雕、锐化的基础。先把这两个搞清楚,后面的都好懂。


🚀 TL;DR

算法做法优点缺点
均值模糊3×3 邻域求平均简单、快、效果柔和把噪声也"抹"进去
中值模糊3×3 邻域排序取中位数椒盐噪声杀手慢一点(要排序)

核心

3×3 邻域:  ┌─────────────┐
           │ ppp₃  │
           │ ppp₆  │     ← p₅ = 当前像素
           │ ppp₉  │        周围 8 个是邻居
           └─────────────┘

📖 目录

  1. 什么是 3×3 邻域
  2. 均值模糊:邻居求平均
  3. 中值模糊:邻居取中位数
  4. 关键代码
  5. 前端效果展示
  6. 两种算法对比
  7. 进阶:更大的核 / 多通道
  8. 参考资料

一、什么是 3×3 邻域

对图片中任意一个像素 p₅,它周围有 8 个邻居

         上一行     同一行     下一行
         ┌─────┐   ┌─────┐   ┌─────┐
同一列前 │ p₁  │   │ p₂  │   │ p₃  │
         ├─────┤   ├─────┤   ├─────┤
同一列   │ p₄  │   │ p₅ ★│   │ p₆  │
         ├─────┤   ├─────┤   ├─────┤
同一列后 │ p₇  │   │ p₈  │   │ p₉  │
         └─────┘   └─────┘   └─────┘

模糊 = 用 p₁~p₉ 这 9 个值的某种"组合"替代 p₅


二、均值模糊:邻居求平均

公式

new_p₅ = (p₁ + p₂ + p₃ + p₄ + p₅ + p₆ + p₇ + p₈ + p₉) / 9

栗子

原 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 累加即可。

视觉效果

原图:                        均值模糊后:
   ████████                     ▓▓▓▓▓▓▓▓
   ████  ████                   ▓▓▓▓▓▓▓▓
   ████████                     ▓▓▓▓▓▓▓▓
   (硬边)                     (柔和渐变)

降噪的同时也让边缘变模糊


三、中值模糊:邻居取中位数

公式

new_p₅ = median(p₁, p₂, p₃, p₄, p₅, p₆, p₇, p₈, p₉)
         ↑ 把 9 个值从小到大排序,取第 5

栗子

原 3×3:    100  120  110
            130  125  115
            140  135  128

排序后:    100  110  115  120  125  128  130  135  140
                              ↑
                           中位数 = 125

关键特性:对椒盐噪声免疫

假设图里有个噪点(一个像素异常亮):

3×3:     100  120  110
          130  255  115       ← 255 是噪点
          140  135  128

均值:  (sum) / 9 = 1370 / 9152  ← 噪点"污染"了结果
中值:  排序取第 5 个 = 120         ← 噪点被忽略 ✅

这就是中值模糊的核心价值异常值被自动排除

视觉效果

原图(带椒盐噪声)              均值模糊              中值模糊
   █ ███ █ █                     ▓▓▓▓▓▓▓              █ ███ █ █
   ████████                     ▓▓▓▓▓▓▓              ████████
   █ ███ █ █                     ▓▓▓▓▓▓▓              █ ███ █ █
  (白点噪点)                    (柔但有痕迹)         (噪点消失!)

四、关键代码

均值模糊

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;
    }
}

中值模糊

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 次

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等效大小视觉效果
13×3非常微弱
2≈5×5轻微可见
3≈7×7明显模糊 ⭐ 推荐
5≈11×11强模糊(油画感)

中心极限定理:均匀卷积多次 ≈ 正态分布 → 比单次大盒式模糊更柔和自然


五、前端效果展示


04d3bcde-554a-4a4d-a98f-e39cf055bd22.png

d4e43880-e90d-4089-b046-418135cd4bfa.png

六、两种算法对比

维度均值模糊中值模糊
计算量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 次即可

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 计算量

// 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 #图像处理 #模糊 #均值滤波 #中值滤波 #算法