4、图像预处理基础(下):色彩空间转换、直方图均衡化、Gamma校正
上一节我们聊了聊图像缩放和滤波,那些操作基本不改变像素的“颜色”含义。但这一节不一样——我们要动真格的了。
色彩空间转换、直方图均衡化、Gamma校正,这三板斧在嵌入式视觉部署里几乎天天见。我最早在ARM Cortex-A7上做车牌识别时,就吃过色彩空间没选对的亏。嗯,今天咱们就把这些坑一个个填上。
4.1 色彩空间转换:从RGB说起
摄像头出来的原始数据,绝大多数是RGB格式。但RGB有个毛病——它对亮度变化太敏感,而且三个通道相关性高,不适合做后续分析。
所以我们要转。转成什么?看场景。
4.1.1 RGB转灰度
最常用的转换,没有之一。为什么?因为很多算法根本不需要颜色信息,比如边缘检测、轮廓提取、二值化。你想想看,一个640×480的彩色图,三个通道就是921KB;转成灰度,直接省掉2/3的内存。在嵌入式设备上,这省下来的可能就是帧率从15fps提到30fps的关键。
灰度转换不是简单的(R+G+B)/3。人眼对绿色最敏感,对蓝色最迟钝。所以标准公式是:
Gray = 0.299 × R + 0.587 × G + 0.114 × B
我个人习惯在嵌入式平台上用整数近似,避免浮点运算:
Gray = (77 × R + 150 × G + 29 × B) >> 8
这个移位操作比浮点乘法快一个数量级。我在STM32H7上实测过,处理一帧VGA图像,整数版本比浮点版本快了约3.2ms。
4.1.2 RGB转YUV
YUV是视频编码和ISP管线里的常客。Y是亮度分量,U和V是色度分量。为什么视频压缩喜欢YUV?因为人眼对亮度细节敏感,对颜色细节不敏感——你可以把UV分量压缩到更低的分辨率,人眼看不出区别。
转换公式(BT.601标准):
Y = 0.299 × R + 0.587 × G + 0.114 × B
U = -0.147 × R - 0.289 × G + 0.436 × B + 128
V = 0.615 × R - 0.515 × G - 0.100 × B + 128
我在项目中遇到过一个问题:直接从摄像头读到的YUV数据,显示出来颜色偏绿。查了半天,发现是U和V的顺序搞反了——有的传感器输出YUYV,有的输出YVYU。这个坑,踩过一次就记住了。
| 格式 | 存储顺序 | 常见场景 |
|---|---|---|
| YUYV | Y0 U0 Y1 V0 Y2 U1 Y3 V1 ... | USB摄像头、OV系列传感器 |
| YVYU | Y0 V0 Y1 U0 Y2 V1 Y3 U1 ... | 部分索尼传感器 |
| NV12 | Y平面 + UV交错平面 | 视频编码、H.264/H.265 |
| NV21 | Y平面 + VU交错平面 | Android Camera、高通平台 |
4.2 直方图均衡化
说白了,就是把图像的灰度分布“拉平”。如果一张图整体偏暗,直方图都挤在低灰度区;偏亮就挤在高灰度区。均衡化就是把这些像素重新分配,让它们均匀分布在0~255之间。
效果很直观:对比度提升,细节变明显。我在做夜间车牌识别时,直方图均衡化几乎是必做的一步。
4.2.1 算法原理
步骤不复杂:
- 统计每个灰度级的像素个数(直方图)
- 计算累积分布函数(CDF)
- 用CDF映射原始灰度到新灰度
映射公式:
new_gray = round( (CDF(gray) - CDF_min) / (N - CDF_min) × 255 )
其中N是总像素数,CDF_min是第一个非零累积值。
4.2.2 嵌入式实现要点
直方图均衡化在PC上跑很简单,但在嵌入式设备上要注意几点:
- 内存占用: 256个int的数组,才1KB,没问题
- 计算量: 两遍遍历图像(一遍统计,一遍映射),O(n)复杂度
- 查表加速: 映射关系可以预计算成256字节的查找表
// 嵌入式C实现片段
uint8_t lut[256];
uint32_t hist[256] = {0};
uint32_t cdf[256] = {0};
uint32_t total = width * height;
// 第一步:统计直方图
for (int i = 0; i < total; i++) {
hist[src[i]]++;
}
// 第二步:计算CDF
cdf[0] = hist[0];
for (int i = 1; i < 256; i++) {
cdf[i] = cdf[i-1] + hist[i];
}
// 第三步:生成查找表
uint32_t cdf_min = 0;
for (int i = 0; i < 256; i++) {
if (cdf[i] != 0) { cdf_min = cdf[i]; break; }
}
for (int i = 0; i < 256; i++) {
lut[i] = (uint8_t)((cdf[i] - cdf_min) * 255 / (total - cdf_min));
}
// 第四步:查表映射
for (int i = 0; i < total; i++) {
dst[i] = lut[src[i]];
}
4.3 Gamma校正
Gamma校正解决的是“显示非线性”问题。人眼对暗部的变化比亮部敏感,但显示器的亮度响应是线性的。所以需要对图像做非线性映射,让显示效果更自然。
公式很简单:
out = 255 × (in / 255) ^ (1 / gamma)
gamma值通常取2.2。小于1时图像变亮,大于1时变暗。
4.3.1 什么时候用Gamma校正?
- 显示优化: 图像在LCD/OLED上显示偏暗时,调低gamma值
- 预处理: 某些特征提取算法(如SIFT、SURF)对光照敏感,Gamma校正可以归一化光照
- 低光照增强: 夜间图像用gamma=0.5~0.7,能明显提亮暗部
4.3.2 嵌入式优化
浮点幂运算在MCU上很慢。我一般用查表法:
// 预计算256级gamma查找表
float gamma = 0.5f; // 提亮
uint8_t gamma_lut[256];
for (int i = 0; i < 256; i++) {
gamma_lut[i] = (uint8_t)(255.0f * powf(i / 255.0f, 1.0f / gamma));
}
// 运行时直接查表
for (int i = 0; i < total; i++) {
dst[i] = gamma_lut[src[i]];
}
查表法把每次像素处理从几十个周期降到了几个周期。我在ESP32-S3上做过测试,查表法处理320×240图像只需要0.8ms,而浮点幂运算需要12ms——差了15倍。
4.4 本章知识结构
下面这张图把三个核心知识点串起来了。你可以看到它们在嵌入式视觉流水线中的位置和关系。
4.5 小结
这一节的内容,说白了就是三件事:
- 色彩空间转换: 把RGB变成更适合处理的形式。灰度省内存,YUV方便压缩
- 直方图均衡化: 拉平灰度分布,提升对比度。注意先降噪再均衡
- Gamma校正: 非线性映射,补偿人眼感知。查表法是嵌入式标配
这三个操作在嵌入式视觉流水线里几乎天天用。我建议你在自己的开发板上把代码跑一遍,看看不同参数对实际图像的影响。只有亲手调过gamma值、看过均衡化前后的直方图变化,才能真正理解这些算法的脾气。