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

🦀 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 个是邻居
           └─────────────┘

📖 目录

  1. [什么是 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")
  2. 均值模糊:邻居求平均
  3. 中值模糊:邻居取中位数
  4. 关键代码
  5. 前端效果展示
  6. 两种算法对比
  7. [进阶:更大的核 / 多通道](#进阶:更大的核 / 多通道 "#%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")
  8. 参考资料

一、什么是 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 #图像处理 #模糊 #均值滤波 #中值滤波 #算法

相关推荐
子兮曰5 小时前
前端工具链的「Rust 化」:一场没有赢家的军备竞赛?
前端·后端·rust
星栈7 小时前
写 Dioxus Demo 不难,难的是把它写成项目
前端·rust·前端框架
mCell9 小时前
【锐评】桌面端技术营销:别拿跑分当工程判断
前端·rust·electron
SmalBox14 小时前
【节点】[Whirl节点]原理解析与实际应用
unity3d·游戏开发·图形学
武子康14 小时前
调查研究-201 Rust 里的 dev build 和 release build:为什么同一份代码性能差这么多?
后端·架构·rust
doiito15 小时前
【Agent Harness】Gliding Horse 的 L2 作战地图:让多 Agent 协作从“摸黑”变成“透明”
ai·rust·架构设计·系统设计·ai agent
星栈1 天前
我用 Rust + Dioxus 做了个全栈跨平台笔记应用:再把新建、编辑和交付补上
前端·rust·前端框架
SmalBox1 天前
【节点】[Truchet节点]原理解析与实际应用
unity3d·游戏开发·图形学
独孤留白2 天前
从C到Rust:基本类型 C 的隐式不确定 vs Rust 的显式确定
rust