色彩处理流水线:从Bayer到人眼感知
色彩处理这块,说实话是仿生眼系统里最容易被低估的环节。很多人觉得不就是调个颜色嘛,但我在实际项目中吃过不少亏——有一次在户外测试,画面整体偏蓝,我以为是传感器坏了,折腾半天才发现是白平衡参数没适配场景。嗯,从那以后我再也不敢小看这条流水线了。
今天咱们就聊聊色彩处理流水线的四个核心环节:Bayer去马赛克、白平衡校正、色彩空间转换、Gamma校正。这四步走完,传感器拿到的原始数据才能变成我们人眼看着舒服的画面。
核心逻辑图:色彩处理流水线
1. Bayer模式与去马赛克
大多数CMOS传感器都使用Bayer滤色阵列。说白了,每个像素只能感知一种颜色——红、绿或蓝。绿色像素占一半,因为人眼对绿色最敏感。我刚开始做嵌入式视觉时,看到原始数据还以为传感器坏了,怎么每个像素只有一个值?后来才明白,这就是Bayer模式。
常见的Bayer模式排列:
| 行\列 | 第1列 | 第2列 | 第3列 | 第4列 |
|---|---|---|---|---|
| 第1行 | R | G | R | G |
| 第2行 | G | B | G | B |
| 第3行 | R | G | R | G |
| 第4行 | G | B | G | B |
去马赛克就是通过插值算法,给每个像素补全缺失的两个颜色通道。常用的方法有三种:
- 最近邻插值:速度最快,但边缘有锯齿。我一般只在调试阶段用。
- 双线性插值:取周围像素的平均值。效果还行,但容易产生伪色。
- 自适应插值:根据边缘方向选择插值策略。这是工程中最常用的方案。
我的经验:在仿生眼项目中,我推荐使用自适应插值。虽然计算量稍大,但能有效避免伪色。如果你用双线性插值,记得在边缘检测后做一次后处理滤波。
2. 白平衡校正
白平衡解决的是「色温」问题。你想想看,同一张白纸,在日光下是白的,在钨丝灯下偏黄,在阴天偏蓝。人眼会自动校正,但传感器不会。所以我们需要手动把「白色」拉回来。
经典的白平衡算法:灰度世界法
假设场景中所有颜色的平均值是灰色。那么R、G、B三个通道的平均值应该相等。如果不相等,就调整增益。
// 灰度世界法实现
void white_balance(uint16_t* img, int width, int height) {
uint64_t sum_r = 0, sum_g = 0, sum_b = 0;
int total = width * height;
for (int i = 0; i < total; i++) {
sum_r += img[i*3 + 0];
sum_g += img[i*3 + 1];
sum_b += img[i*3 + 2];
}
float avg_r = (float)sum_r / total;
float avg_g = (float)sum_g / total;
float avg_b = (float)sum_b / total;
float gain_r = avg_g / avg_r;
float gain_b = avg_g / avg_b;
for (int i = 0; i < total; i++) {
img[i*3 + 0] = (uint16_t)(img[i*3 + 0] * gain_r);
img[i*3 + 2] = (uint16_t)(img[i*3 + 2] * gain_b);
}
}
注意:灰度世界法在单一色调场景下会失效。比如一片蓝色大海,算法会把蓝色压下去,导致画面偏黄。我曾经在无人机航拍时遇到过这个问题,后来改用「完美反射法」才解决。
3. 色彩空间转换:RGB → YCbCr
为什么要把RGB转成YCbCr?说白了,人眼对亮度敏感,对颜色不敏感。YCbCr把亮度和色度分开,我们可以对Y通道做更多处理(比如降噪、增强),而对Cb/Cr通道可以适当压缩,节省带宽。
转换公式:
| 分量 | 计算公式 |
|---|---|
| Y (亮度) | 0.299R + 0.587G + 0.114B |
| Cb (蓝色色度) | -0.169R - 0.331G + 0.500B + 128 |
| Cr (红色色度) | 0.500R - 0.419G - 0.081B + 128 |
我个人习惯用定点数实现,避免浮点运算拖慢速度。比如把系数放大256倍,用整数乘加后再右移8位。
// 定点数实现 RGB → YCbCr
void rgb_to_ycbcr(uint8_t r, uint8_t g, uint8_t b,
uint8_t* y, uint8_t* cb, uint8_t* cr) {
int y_tmp = (77 * r + 150 * g + 29 * b) >> 8;
int cb_tmp = ((-43 * r - 85 * g + 128 * b) >> 8) + 128;
int cr_tmp = ((128 * r - 107 * g - 21 * b) >> 8) + 128;
*y = (uint8_t)(y_tmp > 255 ? 255 : (y_tmp < 0 ? 0 : y_tmp));
*cb = (uint8_t)(cb_tmp > 255 ? 255 : (cb_tmp < 0 ? 0 : cb_tmp));
*cr = (uint8_t)(cr_tmp > 255 ? 255 : (cr_tmp < 0 ? 0 : cr_tmp));
}
避坑指南:转换后记得做饱和处理。我曾经没做饱和,结果高亮区域出现负值,画面出现诡异的紫色条纹。查了两天才找到原因。
4. Gamma校正
Gamma校正解决的是「显示非线性」问题。CRT显示器时代,输入电压和亮度不是线性关系。虽然现在LCD已经线性了,但为了兼容性,我们仍然做Gamma校正。
人眼对暗部细节更敏感,Gamma校正相当于把暗部拉伸,亮部压缩。标准Gamma值是2.2。
Gamma校正公式:
// Gamma校正(查找表法,速度最快)
uint8_t gamma_lut[256];
void init_gamma_lut(float gamma) {
for (int i = 0; i < 256; i++) {
float normalized = i / 255.0f;
float corrected = powf(normalized, 1.0f / gamma);
gamma_lut[i] = (uint8_t)(corrected * 255.0f + 0.5f);
}
}
// 应用查找表
uint8_t apply_gamma(uint8_t pixel) {
return gamma_lut[pixel];
}
关键点:Gamma校正必须在色彩空间转换之后做。如果顺序搞反了,颜色会严重失真。我见过有人把Gamma放在白平衡前面,结果画面整体偏色,怎么调都调不回来。
总结一下
色彩处理流水线这四步,每一步都有坑。Bayer去马赛克要注意伪色,白平衡要小心单一色调场景,色彩空间转换记得做饱和,Gamma校正顺序不能错。我在仿生眼项目里,把这四步封装成一个流水线模块,用DMA和双缓冲来加速,最终做到了30fps的实时处理。
嗯,今天就聊到这儿。记住一句话:色彩处理没有银弹,每个场景都需要调参。多试几次,你就能找到感觉。