2、RAW数据结构解析:RAW文件头部信息、元数据(EXIF)提取、图像数据载荷、黑电平(Black Level)与白平衡(White Balance)参数
好,咱们直接进入正题。
上一章我们聊了RAW是什么,以及它为什么这么「硬核」。这一章,我们来拆解它。说白了,就是要把一个RAW文件大卸八块,看看里面到底藏了些什么。
我个人习惯把RAW文件想象成一个「集装箱」。最外面是包装箱(文件头部),里面有一张说明书(元数据/EXIF),然后才是真正的货物(图像数据载荷)。货物旁边还贴着两张标签,一张写着「黑电平」,一张写着「白平衡」。嗯,咱们一个一个来拆。
2.1 RAW文件头部信息:集装箱的「包装箱」
每个RAW文件的开头,都有一段固定的头部信息。不同厂商(索尼、佳能、尼康)的格式不同,但核心逻辑一样:告诉解析器「我是谁,我多大,我怎么排列的」。
我举个例子,这是最常见的 Bayer RAW 文件头部结构(以某款常见传感器为例):
// 伪代码,展示头部关键字段
typedef struct {
uint16_t magic; // 魔数,用于校验文件类型,比如 0x002A
uint32_t image_width; // 图像宽度,单位像素
uint32_t image_height; // 图像高度
uint16_t bayer_pattern; // Bayer排列模式:0=RGGB, 1=BGGR, 2=GRBG, 3=GBRG
uint16_t bit_depth; // 位深,常见10bit、12bit、14bit
uint32_t data_offset; // 图像数据起始偏移地址
uint32_t file_size; // 整个文件大小
// ... 其他厂商自定义字段
} RAW_HEADER;
这里有个坑,我踩过。很多初学者以为 data_offset 就是固定的,直接硬编码去读。结果换了台相机,文件就读不出来了。为什么?因为不同型号的相机,头部长度可能不一样。所以,永远不要硬编码偏移量,一定要从头部字段里读。
image_width 和实际数据不匹配——厂商在头部里填的是「有效像素」,但数据里包含了「暗像素」。从那以后,我每次都会用 data_offset 和 file_size 反算一下数据长度,做一次交叉校验。
2.2 元数据(EXIF)提取:集装箱的「说明书」
头部信息之后,紧跟着的就是元数据。大部分RAW格式(如DNG、CR2、NEF)都内嵌了EXIF数据。这里面记录了拍摄时的所有参数:快门速度、ISO、光圈、镜头型号,甚至GPS坐标。
提取EXIF,我个人推荐用 libexif 或者 ExifTool。但在嵌入式环境里,我们往往需要自己解析。核心思路是:找到EXIF的起始标记(通常是 0xFFE1 或特定字符串),然后按TIFF结构逐级解析。
一个典型的EXIF提取流程:
- 扫描文件,定位到EXIF APP1 标记。
- 读取TIFF头部,判断字节序(Intel 还是 Motorola)。
- 遍历IFD(图像文件目录),找到你需要的Tag。
- 根据Tag的数据类型,读取对应的值。
举个例子,提取ISO感光度:
// 伪代码:从EXIF中提取ISO
uint16_t exif_get_iso(uint8_t *exif_data, uint32_t exif_size) {
// 1. 跳过APP1标记和长度
// 2. 解析TIFF头部,获取字节序
// 3. 找到IFD0,遍历Tag
// 4. 当Tag == 0x8827 (ISOSpeedRatings) 时,读取值
// 注意:ISO值可能存储在SubIFD中
return iso_value;
}
2.3 图像数据载荷:真正的「货物」
跳过头部和元数据,就到了最核心的部分——图像数据。这部分就是纯粹的像素值,没有任何修饰。每个像素点只记录一个颜色通道的亮度值(R、G、B之一),这就是Bayer模式。
数据排列方式很简单:按行扫描,从上到下,从左到右。但要注意位深和字节对齐。
比如一个12bit的RAW,每个像素占12位。但存储时,为了对齐,可能会用16位(2字节)来存一个像素,也可能把两个12bit像素拼成3个字节。这取决于厂商的设计。
我见过最坑的一种情况:某款传感器宣称是10bit RAW,但实际数据里,每个像素占了2个字节,高6位全是0。说白了就是「假10bit」。怎么发现的?我统计了一下数据分布,发现低10位有值,高6位永远为0。嗯,这种坑,不踩一次很难记住。
// 读取12bit packed RAW数据的示例(C++)
// 假设数据是 packed 格式:每3个字节存2个像素
void unpack_raw12(const uint8_t *packed, uint16_t *unpacked, int num_pixels) {
for (int i = 0; i < num_pixels / 2; i++) {
uint32_t triple = (packed[0] << 16) | (packed[1] << 8) | packed[2];
unpacked[i * 2] = (triple >> 12) & 0xFFF;
unpacked[i * 2 + 1] = triple & 0xFFF;
packed += 3;
}
}
2.4 黑电平(Black Level):传感器的「零点校准」
好,货物卸下来了。但你会发现,这些像素值并不是从0开始的。即使你把镜头盖盖上,传感器依然会输出一个非零的值。这个值就是黑电平。
为什么会有黑电平?因为传感器有暗电流、有读出噪声。如果不减去这个底噪,你拍出来的黑色物体就会发灰。
黑电平参数通常存储在RAW文件的元数据里,或者头部信息中。它可能是一个全局值,也可能是每个通道独立的值(R、G1、G2、B各有不同)。
校正方法很简单:
// 黑电平校正
pixel_corrected = pixel_raw - black_level;
if (pixel_corrected < 0) pixel_corrected = 0;
你想想看,如果黑电平是512(12bit RAW),而你直接拿原始数据去显示,那画面会整体偏亮,对比度也会很差。我见过有人做ISP流水线,忘了做黑电平校正,结果整个画面的暗部细节全丢了。后来加上这一步,效果立竿见影。
2.5 白平衡(White Balance)参数:还原真实色彩
黑电平搞定之后,下一个问题来了:不同光源下,传感器对RGB三个通道的响应是不一样的。日光灯下偏蓝,白炽灯下偏黄。白平衡参数就是用来纠正这个偏差的。
白平衡参数通常以增益系数的形式存在:R_gain、G_gain、B_gain。有些传感器还会给两个绿色通道不同的增益(G1_gain、G2_gain)。
这些参数从哪里来?
- 自动白平衡(AWB):相机在拍摄时,根据场景自动计算出来的。会写入EXIF。
- 手动白平衡:用户设定的色温值,或者对着灰卡自定义的。
- DNG格式:会存储ColorMatrix和AsShotNeutral,需要结合色彩校正矩阵一起使用。
应用白平衡的代码很简单:
// 白平衡校正(在Bayer域进行)
pixel_r_corrected = pixel_r_raw * r_gain;
pixel_g_corrected = pixel_g_raw * g_gain;
pixel_b_corrected = pixel_b_raw * b_gain;
但这里有个细节:白平衡增益通常是一个浮点数,而像素值是整数。直接乘完再取整,会有精度损失。我的做法是:先把增益放大到定点数(比如Q8.8格式),再乘,最后移位。这样既快又准。
2.6 小结:解析RAW的「四步走」
好了,这一章的内容就这些。我们来捋一下思路:
- 读头部:拿到图像尺寸、位深、Bayer排列、数据偏移。
- 提EXIF:获取拍摄参数,尤其是白平衡和黑电平信息。
- 取载荷:按正确的位深和排列方式,把像素数据读出来。
- 做校正:先减黑电平,再乘白平衡增益。
这四步走完,你手里的RAW数据才算「干净」了,可以进入后续的ISP流水线了。下一章,我们会聊Bayer插值(去马赛克),也就是如何把单通道的Bayer数据,变成全彩的RGB图像。
到时候见。