均值模糊(每个像素 = 邻域平均)会产生"方块感"。
高斯模糊通过模拟自然界的光学衍射,让模糊更真实、更平滑。
而可分离性的发现,让它从"理论上漂亮"变成了"实践上高效"。
一、均值模糊的问题
均值模糊核(Box Filter):
┌ 1/9 1/9 1/9 ┐
│ 1/9 1/9 1/9 │
└ 1/9 1/9 1/9 ┘
每个邻域像素的权重完全相同,不管距离中心多远。
这有什么问题?不符合自然界的规律。
想象一张照片放在毛玻璃后面------近处的像素影响更大,远处的影响更小,而不是等权重。均值模糊无法模拟这种衰减,结果在边缘处会产生轻微的"阶梯感"(Box Filter 的频域特性是sinc函数,会产生振铃效应)。
二、高斯函数:自然界的"衰减规律"
高斯函数(正态分布)描述了很多自然现象的衰减模式:
一维高斯函数:
G(x) = (1 / √(2πσ²)) · exp(-x² / 2σ²)
二维高斯函数:
G(x, y) = (1 / 2πσ²) · exp(-(x² + y²) / 2σ²)
其中 σ(sigma)= 标准差,控制模糊的"半径":
- σ 小:曲线窄而高,权重集中在中心,模糊范围小
- σ 大:曲线宽而平,权重分布广,模糊范围大
二维高斯函数的可视化
σ = 1.0 的 5×5 高斯权重(示意):
0.0030 0.0133 0.0219 0.0133 0.0030
0.0133 0.0596 0.0983 0.0596 0.0133
0.0219 0.0983 0.1621 0.0983 0.0219
0.0133 0.0596 0.0983 0.0596 0.0133
0.0030 0.0133 0.0219 0.0133 0.0030
中心权重最大(0.1621),向四周递减
这就是高斯模糊"更自然"的原因:距离越近,影响越大,完全符合直觉。
三、σ 与核尺寸的关系
高斯函数理论上延伸到无穷远,但实际中,超过 3σ 距离的权重已经小到可以忽略(只有 0.3% 的能量)。
实践约定 :核半径 r = ⌈3σ⌉,核尺寸 = 2r + 1
| σ | 核半径 r | 核尺寸 |
|---|---|---|
| 0.5 | 2 | 5×5 |
| 1.0 | 3 | 7×7(或用 3-tap 近似) |
| 1.4 | 5 | 11×11(或用 5-tap 近似) |
| 2.0 | 6 | 13×13(或用 7-tap 近似) |
本框架的预定义核使用 Pascal 三角近似,这是高斯核的整数近似(误差很小):
σ≈1.0(3-tap): [1, 2, 1] / 4 = [0.25, 0.5, 0.25]
σ≈1.4(5-tap): [1, 4, 6, 4, 1] / 16 = [0.0625, 0.25, 0.375, 0.25, 0.0625]
σ≈2.0(7-tap): [1, 6, 15, 20, 15, 6, 1] / 64
Pascal 三角的每一行正好是二项式展开系数,形状与高斯核极为接近。
四、可分离性:高斯模糊最重要的性质
二维高斯 = 水平一维 × 垂直一维
G₂D(x, y) = (1/2πσ²) · exp(-(x²+y²)/2σ²)
= [(1/√(2πσ²)) · exp(-x²/2σ²)] × [(1/√(2πσ²)) · exp(-y²/2σ²)]
= G₁D(x) × G₁D(y)
二维高斯核可以分解为两个一维高斯核的乘积。
为什么这很重要?
方法一:直接 2D 卷积
每个像素 = kW × kH 次乘加
全图复杂度 = W × H × kW × kH
5×5 核,100×100 图:100×100×25 = 250,000 次运算
方法二:可分离(先水平再垂直)
水平 pass:每像素 kW 次,全图 = W × H × kW
垂直 pass:每像素 kH 次,全图 = W × H × kH
总计 = W × H × (kW + kH)
5×5 核,100×100 图:100×100×(5+5) = 100,000 次运算
→ 节省 60%!
更大核时,节省更显著:
| 核尺寸 | 2D 直接 | 可分离 | 节省比例 |
|---|---|---|---|
| 3×3 | 9 ops | 6 ops | 33% |
| 5×5 | 25 ops | 10 ops | 60% |
| 7×7 | 49 ops | 14 ops | 71% |
| 15×15 | 225 ops | 30 ops | 87% |
| k×k | k² ops | 2k ops | (k-2)/k |
可分离性是高斯模糊的核心优化,不懂这个就无法理解为什么高斯模糊在工业中被广泛使用。
五、可分离实现的正确性证明
可分离 = 先水平卷积,再垂直卷积
设水平核 H = [h₋₁, h₀, h₁](1D)
垂直核 V = [v₋₁, v₀, v₁](1D)
水平卷积后:
temp[x, y] = Σ input[x+i, y] · H[i]
i
再做垂直卷积:
output[x, y] = Σ temp[x, y+j] · V[j]
j
= Σ (Σ input[x+i, y+j] · H[i]) · V[j]
j i
= Σ Σ input[x+i, y+j] · H[i] · V[j]
i j
= Σ Σ input[x+i, y+j] · kernel2D[i][j]
i j
其中 kernel2D[i][j] = H[i] · V[j]
这正是 2D 卷积的定义,证毕。
六、实现:水平 + 垂直两 Pass
swift
public func apply(to bitmap: MLBitmap) -> MLBitmap {
let kernel1D = makeKernel1D(for: radius)
// 先水平卷积,再垂直卷积,等效于 2D 高斯卷积
let horizontal = applyHorizontal(bitmap, kernel: kernel1D)
return applyVertical(horizontal, kernel: kernel1D)
}
关键设计:显式独立输出缓冲区
swift
private func applyHorizontal(_ bitmap: MLBitmap, kernel: [Float]) -> MLBitmap {
let kRadius = kernel.count / 2
// 显式复制,而非依赖 CoW 隐式分离
// 这让读写分离在代码层面清晰可见
var outputPixels = bitmap.pixels // 复制(包括 Alpha 通道)
let src = bitmap.pixels // 只读源
for y in 0..<bitmap.height {
for x in 0..<bitmap.width {
var sumR: Float = 0
var sumG: Float = 0
var sumB: Float = 0
for k in 0..<kernel.count {
let px = min(max(x + k - kRadius, 0), bitmap.width - 1)
let idx = bitmap.index(x: px, y: y)
let w = kernel[k]
sumR += Float(src[idx]) * w
sumG += Float(src[idx + 1]) * w
sumB += Float(src[idx + 2]) * w
}
let i = bitmap.index(x: x, y: y)
outputPixels[i] = UInt8(clamping: Int(sumR.rounded()))
outputPixels[i + 1] = UInt8(clamping: Int(sumG.rounded()))
outputPixels[i + 2] = UInt8(clamping: Int(sumB.rounded()))
// outputPixels[i+3] 已从 bitmap.pixels 复制,Alpha 保持不变
}
}
return MLBitmap(width: bitmap.width, height: bitmap.height, pixels: outputPixels)
}
为什么显式独立缓冲区比 CoW 更安全?
隐式 CoW 写法:
swift
var result = bitmap // 还没写入,result 和 bitmap 共享底层存储
result.pixels[0] = 1 // 第一次写入时,CoW 触发,result 分离为独立副本
// 之后读 bitmap,写 result,是正确的
问题:如果有人"优化"成 let src = result.pixels(而非 bitmap.pixels),在 CoW 触发后,src 指向的是旧的共享存储,但 result.pixels 的写入已经在新存储上------这会产生微妙的 bug,很难察觉。
显式写法 让意图清晰:src = bitmap.pixels 明确说明"我在读原始数据",任何人修改时都能看出错误。
七、动态核生成
对于自定义 σ,需要动态计算核:
swift
private func gaussianKernel1D(sigma: Float) -> [Float] {
precondition(sigma > 0, "sigma must be positive")
let r = Int(ceil(3 * sigma)) // 半径:覆盖 ±3σ(99.7% 能量)
let size = 2 * r + 1 // 奇数尺寸
var kernel = [Float](repeating: 0, count: size)
var sum: Float = 0
for i in 0..<size {
let x = Float(i - r) // 相对中心的偏移(-r ~ 0 ~ +r)
kernel[i] = exp(-x * x / (2 * sigma * sigma)) // 高斯函数(省略常数项)
sum += kernel[i]
}
// 归一化:权重之和 = 1,保证亮度不变
return kernel.map { $0 / sum }
}
为什么省略常数项 1/√(2πσ²)?
因为最后要归一化(除以 sum),常数项会被消掉,省略它不影响结果,但减少了计算量。
八、高斯模糊 vs 均值模糊的视觉对比
对同一张图像:
| 操作 | 描述 | 视觉特点 |
|---|---|---|
| 不处理 | 原图 | 清晰,有噪点 |
| 均值模糊 3×3 | Box Filter | 轻微模糊,边缘略有"阶梯感" |
| 高斯模糊 σ=1 | 3-tap | 平滑自然,无阶梯感 |
| 高斯模糊 σ=2 | 7-tap | 更模糊,细节减少,但依然自然 |
频域解释:高斯模糊的频域响应也是高斯形状(高斯函数是自身的傅里叶变换),这是其不产生振铃效应的根本原因。均值模糊的频域响应是 sinc 函数,有旁瓣,会产生振铃。
九、应用场景
| 场景 | 使用原因 |
|---|---|
| Sobel 预处理 | 消除噪点,避免噪点被误识别为边缘 |
| 人像美化 | 皮肤磨皮(模糊皮肤,保留边缘) |
| 景深效果 | 模糊背景,突出主体 |
| 缩略图生成 | 先模糊再采样,避免摩尔纹 |
| 图像去噪 | 高斯噪声 + 高斯模糊 = 降噪(但会丢失细节) |
十、小结
| 概念 | 核心内容 |
|---|---|
| 高斯函数 | G(x)=exp(-x²/2σ²),距离越远权重越小 |
| σ 的意义 | 控制模糊半径,核尺寸 ≈ 6σ+1 |
| 可分离性 | 2D 高斯 = 水平 1D × 垂直 1D,复杂度从 O(k²) 降至 O(k) |
| Pascal 近似 | 整数权重近似高斯核,无需浮点运算(实际本框架用浮点) |
| 归一化 | 权重之和必须 = 1,否则亮度改变 |
| 比均值好 | 无振铃效应,更接近自然光学模糊 |
思考题
- 为什么
sigma = 0的高斯核等价于恒等核(不做任何处理)? - 对同一张图像先后做两次高斯模糊(σ₁ 和 σ₂),等效于一次 σ = √(σ₁² + σ₂²) 的高斯模糊,这个性质叫什么?它对实际应用有什么意义?
- "先缩小图像再放大"和"直接高斯模糊"在视觉效果上有什么区别?哪种方式产生的模糊更接近高斯模糊?
答案:1. σ=0 时 exp(-x²/0) 在 x=0 是 1,x≠0 是 0,即恒等核,只保留中心权重;2. 叫"高斯卷积的加法性质"或"卷积的递推性",意味着可以用多次小模糊替代一次大模糊,在 Metal 中特别有用(可以多次 ping-pong 渲染);3. 缩小再放大会产生像素化/方块感,高斯模糊更自然平滑,视觉质量更好。
如果这篇对你有一点启发:
点个赞,让更多人少踩一个坑
转发给那个正在纠结的人
也欢迎关注我------
我们一起,把认知变成长期复利。
往期推荐:
一图了解几种常用卷积核
一图了解卷积的核心原理
一张图带你了解------卷积到底是什么?
一图了解饱和度:控制色彩鲜艳程度的关键
一图了解OCR的处理流程及相关图像处理技术
一图了解二值化与阈值,从灰度到黑白的决策
一张图了解图像处理中的亮度、对比度与实现
颜色科学与灰度化
从"图片"到"内存"------你真正理解图像处理的第一天
iPhone相册背后的图像处理知识(下)
iPhone相册背后的图像处理知识(中)
iPhone相册背后的图像处理知识(上)
一张图了解图像处理的本质
图像到底是什么图像处理技术概要图
AI时代,软件工程师必备概念全景图