第4章 去噪:空间域去噪与时域融合
去噪这章,我琢磨了很久该怎么讲。说实话,刚入行那会儿我觉得去噪就是「把图弄干净」,后来踩了无数坑才明白——去噪的本质是在分辨率和噪声之间找平衡。你压噪声压得太狠,细节就糊了;你保细节保得太猛,噪声又在那杵着。嗯,咱们今天就把这事掰扯清楚。
4.1 空间域去噪:三种经典滤波器
空间域去噪,说白了就是直接在像素点上做运算。我习惯把它分成两类:线性滤波和非线性滤波。线性滤波快,但容易把边缘也抹了;非线性滤波慢点,但能保边。
4.1.1 均值滤波
均值滤波是最朴素的思路——把中心像素周围一圈像素的值加起来取平均。你想想看,噪声是随机抖动的,平均一下不就拉回来了吗?
// 3x3 均值滤波核
[1/9, 1/9, 1/9]
[1/9, 1/9, 1/9]
[1/9, 1/9, 1/9]
// 实现示例(C风格伪代码)
void boxFilter(uint8_t* src, uint8_t* dst, int w, int h, int ksize) {
int half = ksize / 2;
float scale = 1.0f / (ksize * ksize);
for (int y = half; y < h - half; y++) {
for (int x = half; x < w - half; x++) {
float sum = 0;
for (int ky = -half; ky <= half; ky++)
for (int kx = -half; kx <= half; kx++)
sum += src[(y+ky)*w + (x+kx)];
dst[y*w + x] = (uint8_t)(sum * scale + 0.5f);
}
}
}
4.1.2 高斯滤波
高斯滤波和均值滤波的区别在哪?均值滤波是「一碗水端平」,高斯滤波是「近亲远疏」——离中心越近的像素权重越大,越远权重越小。这个权重分布服从高斯函数,所以叫高斯滤波。
| 滤波器 | 权重分布 | 去噪效果 | 边缘保留 | 计算量 |
|---|---|---|---|---|
| 均值滤波 | 均匀 | 一般 | 差 | 低 |
| 高斯滤波 | 高斯分布 | 较好 | 中等 | 中 |
| 中值滤波 | 排序取中 | 椒盐噪声极好 | 好 | 高 |
我个人习惯用 σ(标准差) 来控制高斯滤波的强度。σ越大,核越平缓,去噪越强但细节损失也越大。一般 σ 取 0.8~1.5 比较合适,超过 2.0 就开始糊了。
// 3x3 高斯核(σ=1.0)
[1/16, 2/16, 1/16]
[2/16, 4/16, 2/16]
[1/16, 2/16, 1/16]
// 生成高斯核的公式
float gaussian(int x, int y, float sigma) {
float coeff = 1.0f / (2.0f * M_PI * sigma * sigma);
float exp_val = -(x*x + y*y) / (2.0f * sigma * sigma);
return coeff * expf(exp_val);
}
4.1.3 中值滤波
中值滤波的思路完全不同——它不求和,而是把窗口内的像素值排序,取中间那个值。这招对付椒盐噪声特别管用。为什么?因为椒盐噪声是极端值(要么特别亮要么特别暗),排序后肯定被挤到两头去了,中间那个值大概率是正常像素。
4.2 时域去噪原理
空间域去噪有个硬伤——它只看了当前这一帧。如果噪声在时间上是随机抖动的,那多帧之间取平均不是更好吗?这就是时域去噪的核心思想。
时域去噪的公式其实很简单:
// 时域递归滤波
dst(t) = α * src(t) + (1 - α) * dst(t-1)
// α 是更新率,范围 0~1
// α 越大,当前帧权重越大,去噪越弱但运动拖影越少
// α 越小,历史帧权重越大,去噪越强但运动拖影越明显
这里有个关键问题——运动检测。如果场景里有运动物体,你还傻乎乎地把前后帧平均,那运动物体就拖影了。我做过一个项目,在监控摄像头上做时域去噪,没加运动检测,结果人走过去后面拖了一条尾巴,客户直接退货。
- 计算当前帧和上一帧的像素差
- 差值大的区域认为是运动区域,减小 α(少用历史帧)
- 差值小的区域认为是静止区域,增大 α(多用历史帧)
- 用运动掩码(motion mask)来控制 α 的空间分布
4.3 3D去噪:多帧融合
3D去噪,说白了就是把空间域和时间域结合起来。空间域处理单帧内的噪声,时间域利用帧间相关性,两者互补,效果1+1>2。
我习惯把3D去噪的流程画成下面这张图,你看一眼就明白了:
4.3.1 多帧融合的工程实现
实际工程里,多帧融合一般分三步走:
- 运动估计:用块匹配或光流法算出帧间运动矢量
- 运动补偿:把历史帧按运动矢量对齐到当前帧
- 加权融合:对齐后的多帧按权重叠加
权重怎么定?我一般用帧差置信度来做权重。帧差小的像素,说明对齐得好,权重给大点;帧差大的像素,说明可能有遮挡或运动估计不准,权重给小点。
// 多帧融合伪代码
void multiFrameDenoise(Frame* frames[], int numFrames, Frame* result) {
// 以中间帧为参考帧
int refIdx = numFrames / 2;
Frame* ref = frames[refIdx];
for (int y = 0; y < ref->h; y++) {
for (int x = 0; x < ref->w; x++) {
float sum = 0, weightSum = 0;
for (int i = 0; i < numFrames; i++) {
// 运动补偿:找到第i帧中对应(x,y)的位置
int mx = x + motionVectors[i][y][x].x;
int my = y + motionVectors[i][y][x].y;
// 计算帧差置信度
float diff = abs(ref->data[y][x] - frames[i]->data[my][mx]);
float weight = expf(-diff * diff / (2 * sigma * sigma));
sum += weight * frames[i]->data[my][mx];
weightSum += weight;
}
result->data[y][x] = sum / weightSum;
}
}
}
4.3.2 3D去噪的典型参数
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 融合帧数 | 3~5 | 超过7帧收益递减 |
| 时域更新率 α | 0.1~0.3 | 静止场景用0.1,运动场景用0.3 |
| 运动检测阈值 | 10~20(8bit) | 低于阈值认为是静止区域 |
| 空间去噪强度 | 弱(σ=0.5~1.0) | 时域已经去了一部分,空间别太猛 |
4.4 实战建议:怎么选去噪方案?
我做了这么多年ISP,总结了一套选型思路,分享给你:
- 场景一:高光充足,噪声不明显 → 只用弱高斯滤波(σ=0.8),别上时域,省算力
- 场景二:低光环境,噪声明显但场景静止 → 3D去噪,3帧融合,时域α=0.15
- 场景三:低光且有运动物体 → 运动自适应时域滤波 + 弱空间去噪
- 场景四:椒盐噪声(传感器坏点) → 中值滤波,3x3窗口就够了
去噪这事,说到底就是取舍。没有万能方案,只有最适合当前场景的方案。你多试几次,慢慢就能找到感觉了。