2、空间域滤波:均值滤波、高斯滤波原理与C++实现,双边滤波原理与参数调优
各位同学,欢迎来到第二章。
上一章我们聊了噪声模型,知道了对手长什么样。这一章,咱们直接上家伙——空间域滤波。说白了,就是在图像本身上做文章,用像素周围的邻居来“拉”它一把,把噪声抹平。
我个人习惯把空间域滤波比作“和稀泥”。噪声点太突兀,我们就把它和周围的点搅和在一起,颜色就均匀了。但怎么搅,搅多大范围,这里面的门道可深了。
2.1 均值滤波:最朴素的“和稀泥”
均值滤波,思路简单到令人发指。你想想看,一个噪声点,它的像素值跟周围格格不入。那我干脆把周围一圈像素的平均值算出来,替换掉这个点,不就完事了?
嗯,就是这么回事。
2.1.1 原理与核
我们用一个“窗口”在图像上滑动。这个窗口,专业术语叫“核”(Kernel)或“模板”。比如一个3x3的核:
1/9 1/9 1/9
1/9 1/9 1/9
1/9 1/9 1/9
这个核里的每个值都是1/9。当它盖在图像上时,就把核下面9个像素的值加起来,乘以1/9,得到平均值。这个平均值,就是输出图像对应位置的像素值。
公式很简单:
g(x, y) = (1 / (m * n)) * Σ f(i, j)
其中m*n是核的大小,f(i,j)是核覆盖的原始像素。
核心要点:核越大,平滑效果越强,但图像也越模糊。这是一个典型的“去噪 vs 保边”的 trade-off。
2.1.2 C++实现(OpenCV风格)
我直接给你看一个最朴素的实现,不用OpenCV的blur函数,我们自己写一遍,你才能理解底层在干什么。
#include <opencv2/opencv.hpp>
#include <iostream>
void meanFilter(const cv::Mat& src, cv::Mat& dst, int kernelSize) {
// 确保核大小为奇数
if (kernelSize % 2 == 0) {
kernelSize++;
}
dst = src.clone();
int border = kernelSize / 2;
// 边界处理:我习惯用复制边界,简单粗暴
cv::Mat padded;
cv::copyMakeBorder(src, padded, border, border, border, border, cv::BORDER_REPLICATE);
for (int y = border; y < src.rows + border; ++y) {
for (int x = border; x < src.cols + border; ++x) {
float sum = 0.0f;
// 累加核覆盖区域
for (int ky = -border; ky <= border; ++ky) {
for (int kx = -border; kx <= border; ++kx) {
sum += padded.at<uchar>(y + ky, x + kx);
}
}
// 取平均
dst.at<uchar>(y - border, x - border) =
static_cast<uchar>(sum / (kernelSize * kernelSize));
}
}
}
int main() {
cv::Mat img = cv::imread("noisy_image.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat result;
meanFilter(img, result, 5); // 5x5均值滤波
cv::imshow("Original", img);
cv::imshow("Mean Filtered", result);
cv::waitKey(0);
return 0;
}
我的小经验:实际项目中,我很少用3x3的均值滤波,效果太弱。5x5起步,但超过7x7图像就糊得没法看了。均值滤波最大的问题是——它一视同仁,把边缘也抹平了。
2.2 高斯滤波:带权重的“和稀泥”
均值滤波有个硬伤:窗口中心点和边缘点贡献一样大。这合理吗?不合理。你想想看,离中心越近的像素,理论上跟中心点关系越密切。高斯滤波就是来解决这个问题的。
2.2.1 原理与高斯核
高斯滤波用高斯函数来生成核的权重。离中心越近,权重越大;越远,权重越小。这个权重分布,就是二维高斯函数:
G(x, y) = (1 / (2πσ²)) * exp(-(x² + y²) / (2σ²))
这里的σ(sigma)是关键参数。σ越大,曲线越平坦,权重分布越均匀,效果就越接近均值滤波。σ越小,中心权重越大,边缘保留得越好。
一个3x3的高斯核(σ≈0.85)长这样:
1/16 2/16 1/16
2/16 4/16 2/16
1/16 2/16 1/16
你看,中心点的权重是4/16,是角点的4倍。这就比均值滤波合理多了。
2.2.2 C++实现
生成高斯核是关键。我习惯先算好核,再卷积。
cv::Mat createGaussianKernel(int size, double sigma) {
cv::Mat kernel(size, size, CV_64F);
int center = size / 2;
double sum = 0.0;
for (int y = 0; y < size; ++y) {
for (int x = 0; x < size; ++x) {
int dx = x - center;
int dy = y - center;
double value = exp(-(dx*dx + dy*dy) / (2 * sigma * sigma));
kernel.at<double>(y, x) = value;
sum += value;
}
}
// 归一化,确保所有权重加起来为1
kernel /= sum;
return kernel;
}
void gaussianFilter(const cv::Mat& src, cv::Mat& dst, int kernelSize, double sigma) {
cv::Mat kernel = createGaussianKernel(kernelSize, sigma);
cv::filter2D(src, dst, -1, kernel);
}
// 使用示例
// cv::Mat result;
// gaussianFilter(img, result, 5, 1.0);
避坑指南:我曾经在项目中直接用OpenCV的GaussianBlur,没注意sigma的默认值。结果σ设得太小,核几乎退化成单位矩阵,滤波了个寂寞。记住:σ一般取0.8到2.0之间,核大小取6σ+1(向上取奇数)。
2.3 双边滤波:既要又要的“和稀泥”
均值和高斯滤波,本质上都是低通滤波器。它们把噪声和边缘一起抹掉了。那有没有一种滤波,能只抹噪声、不碰边缘?
有,双边滤波(Bilateral Filter)。
2.3.1 原理:空间域 + 值域
双边滤波的核心思想是:只有像素值相近的邻居,才参与加权平均。
它有两个权重:
- 空间权重(Spatial Weight):跟高斯滤波一样,距离越近权重越大。
- 值域权重(Range Weight):像素值越接近,权重越大。
公式长这样:
g(i,j) = (1/Wp) * Σ f(k,l) * w_s * w_r
其中:
w_s = exp(-((i-k)² + (j-l)²) / (2σ_s²))—— 空间高斯w_r = exp(-(f(i,j) - f(k,l))² / (2σ_r²))—— 值域高斯Wp是归一化系数
你想想看,在边缘处,像素值突变。值域权重会让边缘另一侧的像素权重变得极小,所以边缘被保留下来了。而在平坦区域,像素值相近,值域权重接近1,就退化成高斯滤波。
我的理解:双边滤波就像一个有原则的“和稀泥”——只跟志同道合的人(像素值相近)抱团,跟价值观不同的人(边缘另一侧)划清界限。
2.3.2 参数调优:σ_s 和 σ_r 的艺术
双边滤波有两个核心参数,调起来很讲究:
| 参数 | 含义 | 调大 | 调小 | 我的建议 |
|---|---|---|---|---|
| σ_s | 空间标准差 | 平滑范围更大,图像更模糊 | 只影响小范围,去噪弱 | 一般取5-20,跟图像尺寸有关 |
| σ_r | 值域标准差 | 更多像素参与加权,边缘变弱 | 只保留极相似像素,去噪弱 | 一般取10-50(对8位图) |
重要提醒:双边滤波的计算量非常大!它是非线性的,没法用FFT加速。我曾经在1080p图像上用双边滤波,σ_s=15,σ_r=30,一帧处理了200多毫秒。实时系统慎用!
2.3.3 调优实战经验
我总结了一套调参的“三步走”策略:
- 先定σ_s:根据噪声的“颗粒大小”来定。噪声颗粒大,σ_s就大一点。一般从5开始试。
- 再定σ_r:根据图像的对比度来定。对比度大的场景,σ_r可以大一些。一般从20开始试。
- 微调:如果边缘被磨平了,说明σ_r太大,减小它。如果噪声没去掉,说明σ_s太小或σ_r太小。
举个例子。我做过一个手机夜景去噪的项目。夜景照片噪声大,但边缘细节(比如建筑轮廓)必须保留。我试了多组参数:
- σ_s=10, σ_r=25:效果不错,但天空区域有轻微色块
- σ_s=15, σ_r=35:色块没了,但建筑边缘有点软
- 最终选了σ_s=12, σ_r=30:平衡了去噪和保边
核心总结:均值滤波是入门,高斯滤波是进阶,双边滤波是高手。但高手也有弱点——慢。实际工程中,我经常用高斯滤波做预处理,再用双边滤波做精细去噪。或者用引导滤波(下一章讲)来替代双边滤波,速度能快一个数量级。
好了,这一章就到这里。空间域滤波是ISP图像处理的基础,也是你理解后续频域滤波、非局部均值滤波的基石。回去把代码跑一遍,调一调参数,感受一下不同参数对结果的影响。下一章,我们聊聊频域滤波——换个角度看世界。