4. 图像预处理:高斯滤波、中值滤波、直方图均衡化、图像金字塔
图像预处理,说白了就是给算法「喂」干净、好消化的数据。我做了这么多年嵌入式视觉,见过太多算法在实验室跑得飞起,一上硬件就崩——十有八九是预处理没做好。
运动对焦追踪对实时性要求极高,每一帧的处理时间都是硬约束。所以这一章,我们只聊四个最常用、也最能在嵌入式平台上落地的预处理操作。
4.1 高斯滤波:平滑噪声,保留边缘
高斯滤波是线性滤波的代表。它的核函数服从二维高斯分布,中心权重最大,越往边缘权重越小。
为什么用高斯?因为自然图像中的噪声大多服从高斯分布,用高斯核去滤高斯噪声,数学上最匹配。我早期做安防摄像头时,发现 sensor 在低照度下噪点特别多,试过均值滤波,结果边缘糊成一片。换成高斯滤波后,画面干净了,边缘细节也保住了。
实际项目中,我习惯用 3×3 或 5×5 的核。再大,计算量翻倍,效果提升有限。
// C 语言实现 3×3 高斯滤波(定点数优化版)
// 核系数:1/16 * [1 2 1; 2 4 2; 1 2 1]
void gaussian_blur_3x3(uint8_t *src, uint8_t *dst, int w, int h) {
for (int y = 1; y < h-1; y++) {
for (int x = 1; x < w-1; x++) {
int sum = 0;
sum += src[(y-1)*w + (x-1)] * 1;
sum += src[(y-1)*w + x] * 2;
sum += src[(y-1)*w + (x+1)] * 1;
sum += src[y*w + (x-1)] * 2;
sum += src[y*w + x] * 4;
sum += src[y*w + (x+1)] * 2;
sum += src[(y+1)*w + (x-1)] * 1;
sum += src[(y+1)*w + x] * 2;
sum += src[(y+1)*w + (x+1)] * 1;
dst[y*w + x] = sum >> 4; // 除以16
}
}
}
4.2 中值滤波:干掉椒盐噪声
中值滤波是非线性滤波。它取邻域内所有像素的中位数作为输出值。
这东西对付椒盐噪声特别管用。椒盐噪声是随机的黑白点,均值或高斯滤波会把它扩散开,反而让一片区域都脏了。中值滤波直接把它替换成周围正常像素的值,干净利落。
我曾经在一个工业检测项目里,产线上有颗螺丝偶尔反光,在图像上形成极亮的斑点。用中值滤波 3×3 窗口,一帧就搞定了。换成高斯滤波,反而把反光区域模糊成一片,导致后续边缘检测误判。
| 滤波类型 | 适用噪声 | 边缘保留 | 计算量 | 嵌入式推荐 |
|---|---|---|---|---|
| 高斯滤波 | 高斯噪声 | 中等 | 低 | 3×3 或 5×5 |
| 中值滤波 | 椒盐噪声 | 好 | 中 | 3×3,不超过 5×5 |
| 均值滤波 | 均匀噪声 | 差 | 最低 | 尽量不用 |
4.3 直方图均衡化:让暗部细节现形
直方图均衡化,说白了就是把图像的灰度分布「拉平」。原本集中在暗区的像素,会被映射到更宽的灰度范围,暗部细节就出来了。
运动对焦追踪里,这个操作特别有用。比如无人机在黄昏时追踪地面目标,画面整体偏暗,对比度低。不做均衡化,特征点提取根本找不到足够的角点。
但要注意——全局直方图均衡化会放大噪声。我建议只在感兴趣区域(ROI)上做局部均衡化,或者用 CLAHE(限制对比度自适应直方图均衡化)。
// 直方图均衡化核心步骤(伪代码)
// 1. 统计灰度直方图 hist[256]
// 2. 计算累积分布函数 cdf[i] = sum(hist[0..i])
// 3. 归一化映射:new_pixel = (cdf[pixel] - cdf_min) / (w*h - cdf_min) * 255
void hist_equalize(uint8_t *img, int w, int h) {
int hist[256] = {0};
for (int i = 0; i < w*h; i++) hist[img[i]]++;
int cdf[256];
int sum = 0;
for (int i = 0; i < 256; i++) {
sum += hist[i];
cdf[i] = sum;
}
int cdf_min = cdf[0];
for (int i = 0; i < w*h; i++) {
img[i] = (uint8_t)((cdf[img[i]] - cdf_min) * 255.0f / (w*h - cdf_min));
}
}
4.4 图像金字塔:多尺度追踪的基石
图像金字塔,就是同一张图生成多个分辨率版本。从原图往下采样,一层比一层小。
为什么需要它?运动对焦追踪里,目标可能在画面中快速移动。如果只在原图上做特征匹配,搜索范围太大,计算量爆炸。先在低分辨率层找到大致位置,再回到高分辨率层精确定位——这就是金字塔的经典用法。
我记得做手机摄像头追焦时,目标从画面边缘突然闯入,位移超过 100 像素。如果不用金字塔,光流法直接崩掉。用了 3 层金字塔,顶层位移只有 20 像素,光流轻松收敛。
// 构建高斯金字塔(简化版)
// 每层先高斯滤波,再隔行隔列采样
void build_gaussian_pyramid(uint8_t *src, int w, int h,
uint8_t *pyramid[], int levels) {
pyramid[0] = src;
for (int l = 1; l < levels; l++) {
int prev_w = w >> (l-1);
int prev_h = h >> (l-1);
int cur_w = w >> l;
int cur_h = h >> l;
// 先高斯滤波(3×3)
gaussian_blur_3x3(pyramid[l-1], pyramid[l], prev_w, prev_h);
// 再下采样
for (int y = 0; y < cur_h; y++) {
for (int x = 0; x < cur_w; x++) {
pyramid[l][y*cur_w + x] =
pyramid[l][(y*2)*prev_w + (x*2)];
}
}
}
}
4.5 本章知识体系
下面这张图,把四个预处理操作的关系和适用场景串起来了。你想想看,实际项目中往往是组合使用:先滤波去噪,再均衡化增强,最后用金字塔做多尺度分析。
4.6 避坑指南
- 滤波顺序不能乱: 先滤波,再均衡化。如果先均衡化,噪声会被放大,滤波效果大打折扣。
- 金字塔层数不是越多越好: 层数太多,顶层图像太小,特征信息丢失严重。我一般不超过 4 层。
- 中值滤波的边界处理: 边界像素没法取完整窗口,我习惯直接复制原值,或者用镜像填充。
- 直方图均衡化后的量化误差: 8 位图像均衡化后,灰度级可能从 256 压缩到更少,出现「伪轮廓」。这时候可以加一点微小的随机抖动(dithering)。