2. 图像传感器与RAW数据
做ISP调试这些年,我接触过各种各样的传感器。说实话,很多刚入行的工程师容易忽略一个事实——你调出来的图像质量,有一半在传感器阶段就已经决定了。RAW数据是ISP的起点,也是整个图像质量的根基。这一章,我们就把传感器和RAW数据这件事彻底讲透。
CCD与CMOS传感器:核心差异
先问个问题:为什么现在工业相机几乎全是CMOS,CCD反而越来越少见?
我2015年刚入行时,公司还有一条CCD相机的产线。那时候老师傅跟我说,CCD画质干净,CMOS噪声大。但你看现在,高端工业相机清一色CMOS。原因其实很简单——工艺进步让CMOS的噪声问题基本解决了,但CCD的功耗和速度瓶颈始终突破不了。
| 对比项 | CCD | CMOS |
|---|---|---|
| 读出方式 | 逐行转移,统一读出 | 每个像素独立寻址读出 |
| 功耗 | 较高(需要多路高压驱动) | 低(单电源即可工作) |
| 读出速度 | 较慢(串行读出瓶颈) | 快(并行读出,轻松上百fps) |
| 噪声特性 | 读出噪声低,暗电流均匀 | 固定模式噪声(FPN)明显,但可校正 |
| 动态范围 | 通常较高 | 受限于像素内放大器,但近年已追平 |
| 成本 | 高(特殊工艺) | 低(标准CMOS工艺,可集成逻辑电路) |
| 工业应用现状 | 基本停产,仅特殊科研需求 | 主流 |
我的经验之谈:在工业检测场景中,CMOS的FPN(固定模式噪声)完全可以通过暗场校正和双采样技术消除。而CCD的高功耗带来的散热问题,在长曝光应用中反而会引入热噪声。所以别迷信CCD,选CMOS就对了。
Bayer格式:为什么是RGGB?
传感器本身只能感知光的强度,分辨不出颜色。那彩色图像怎么来的?答案是在像素表面覆盖彩色滤光片阵列(CFA)。最常用的就是Bayer格式。
你可能会问:为什么Bayer用RGGB,而不是RGBG或者别的排列?
嗯,这里有个生理学背景——人眼对绿色最敏感。Bayer格式中绿色像素占50%,红色和蓝色各占25%,就是为了模拟人眼的亮度感知特性。我在调试一个医疗内窥镜项目时,曾经试过把Bayer改成RGB等间距排列,结果图像细节锐度直接掉了15%。
调试小技巧:拿到一款新传感器,第一件事就是确认Bayer排列顺序。不同厂商可能用RGGB、BGGR、GRBG或GBRG。我曾经因为搞错排列顺序,调试了三天才发现是Bayer读反了——图像全是紫色和绿色的伪影。
RAW数据格式与位深
传感器输出的原始数据就是RAW。它没经过任何ISP处理,是最纯粹的感光信息。
RAW数据有几个关键参数:
- 位深(Bit Depth):常见8bit、10bit、12bit、14bit、16bit。工业相机主流是10bit或12bit。位深越高,动态范围越大,但数据量也越大。
- 数据排列:Bayer格式下,数据按像素矩阵逐行排列。每个像素值对应一个颜色通道。
- 黑电平(Black Level):传感器在无光照时的输出值,通常不为0。比如12bit传感器黑电平可能是64。
注意:很多工程师直接把RAW数据当成线性数据来处理,这是个大坑。RAW数据需要先减去黑电平,再做线性化校正,才能进入后续的ISP流程。否则暗部细节会丢失,亮部会提前饱和。
RAW数据读取与解析实战
下面我给出一个实际的RAW数据读取示例。假设我们有一个12bit的Bayer RAW文件,排列为RGGB。
#include <stdio.h>
#include <stdint.h>
// 解析12bit RAW数据(RGGB排列)
// 每个像素占用12bit,每2个像素打包成3个字节
void parse_raw12_rggb(const char* filename,
uint16_t* r_plane,
uint16_t* g_plane,
uint16_t* b_plane,
int width, int height) {
FILE* fp = fopen(filename, "rb");
if (!fp) return;
int row_bytes = (width * 12 + 7) / 8; // 每行字节数
uint8_t* line_buf = (uint8_t*)malloc(row_bytes);
for (int y = 0; y < height; y++) {
fread(line_buf, 1, row_bytes, fp);
for (int x = 0; x < width; x++) {
// 从字节流中提取12bit像素值
int bit_offset = x * 12;
int byte_offset = bit_offset / 8;
int bit_remain = bit_offset % 8;
uint16_t pixel = 0;
pixel = (line_buf[byte_offset] << 4) |
(line_buf[byte_offset + 1] >> 4);
// 根据Bayer排列分配到对应颜色平面
if (y % 2 == 0) { // 偶数行
if (x % 2 == 0) { // 偶数列 -> R
r_plane[(y/2) * (width/2) + (x/2)] = pixel;
} else { // 奇数列 -> G
g_plane[y * width + x] = pixel; // G在R行
}
} else { // 奇数行
if (x % 2 == 0) { // 偶数列 -> G
g_plane[y * width + x] = pixel; // G在B行
} else { // 奇数列 -> B
b_plane[(y/2) * (width/2) + (x/2)] = pixel;
}
}
}
}
free(line_buf);
fclose(fp);
}
个人习惯:我一般会在解析RAW数据后,先做一个简单的直方图统计。如果直方图在低端有大量堆积,说明黑电平没减干净;如果在高端有截断,说明增益设置过高。这两个问题在RAW阶段发现,比等到ISP输出再排查要高效得多。
RAW数据预处理流程
拿到RAW数据后,别急着做去马赛克。先走一遍预处理流程:
- 黑电平校正:减去每通道的黑电平值。不同通道的黑电平可能不同。
- 增益归一化:将数字增益还原为物理光强比例。12bit数据通常映射到0-4095范围。
- 坏点校正:传感器总有坏点,需要在RAW阶段先做校正,否则后续处理会放大瑕疵。
- 镜头阴影校正(LSC):镜头边缘亮度衰减,需要在RAW域做增益补偿。
我曾经在一个高精度测量项目中,忽略了黑电平校正,结果测量结果总是偏暗0.5%。排查了两天才发现是黑电平没减干净——12bit传感器黑电平是64,对应到8bit就是1个灰阶的偏差,但在高精度场景下这个误差不可接受。
避坑指南:我曾经在一个项目中,坏点校正放在了去马赛克之后做。结果坏点周围的颜色信息已经被插值污染了,校正后留下明显的色斑。记住:坏点校正必须在RAW域、在去马赛克之前完成。
不同位深RAW数据的存储格式
实际项目中,你会遇到各种RAW存储格式。我整理了一份对照表:
| 位深 | 存储方式 | 每像素占用 | 常见场景 |
|---|---|---|---|
| 8bit | 1字节直接存储 | 1字节 | 低端USB相机、预览模式 |
| 10bit | 打包模式(5像素占6字节)或MSB对齐 | 1.25字节 | 工业相机主流格式 |
| 12bit | 打包模式(2像素占3字节)或MSB对齐 | 1.5字节 | 高动态范围工业相机 |
| 14bit | 打包模式(4像素占7字节)或MSB对齐 | 1.75字节 | 科学相机、医疗成像 |
| 16bit | 2字节直接存储(低4位为0) | 2字节 | 方便处理,但数据量大 |
我个人习惯在调试阶段用16bit存储,虽然数据量大一倍,但省去了位操作解析的麻烦。等算法稳定后,再根据实际带宽需求选择打包格式。
注意字节序:不同厂商的RAW文件字节序可能不同。有的用Little-Endian,有的用Big-Endian。解析时一定要确认。我见过一个团队因为字节序搞反,整批图像都出现了条纹状噪声——其实是高低字节交换导致的数值跳变。
好了,这一章的内容就到这里。传感器和RAW数据是ISP的起点,把这些基础打牢,后面的调试才能事半功倍。记住:RAW数据里藏着图像质量的全部秘密,学会读懂它,你就掌握了ISP调试的主动权。