RAW图基础:Bayer阵列、数据存储与黑电平
做ISP的人,天天跟RAW图打交道。说白了,RAW就是传感器最原始的“底片”。你拍到的所有漂亮照片,都是从这张底片一步步修出来的。今天我就带你把这底片看透。
2.1 Bayer阵列——为什么是RGGB?
人眼对绿色最敏感,这事传感器厂商也知道。所以Bayer阵列里,绿色像素占了总数的一半,红色和蓝色各占四分之一。典型的排列就是RGGB。
核心记忆点:每个像素只记录一种颜色。红色像素只记红光强度,绿色只记绿光,蓝色只记蓝光。缺的两种颜色,全靠后期插值算出来。
我刚开始做ISP时,总觉得这设计太浪费——明明传感器有几百万像素,真正“全彩”的像素一个都没有。后来才明白,这叫“牺牲分辨率换灵敏度”。每个像素只收一种光,信噪比能高不少。
常见的Bayer模式有四种:
| 模式 | 第一行 | 第二行 | 典型应用 |
|---|---|---|---|
| RGGB | R G R G ... | G B G B ... | 最主流,索尼、OV常用 |
| BGGR | B G B G ... | G R G R ... | 部分安森美、三星传感器 |
| GRBG | G R G R ... | B G B G ... | 部分老款OmniVision |
| GBRG | G B G B ... | R G R G ... | 较少见,但偶尔遇到 |
避坑指南:我曾经在项目里把Bayer模式搞反了。Sensor datasheet写的是BGGR,我默认当成RGGB处理。结果出来的图像偏色严重,调试了三天才发现是Bayer顺序错了。拿到新传感器,第一件事就是确认Bayer模式,别信“默认”。
你想想看,如果传感器输出的是10bit RAW,那每个像素的值范围就是0~1023。红色像素在R位置读出1023,代表这个点红光饱和了。但旁边的G位置读出500,B位置读出200——这些值怎么变成彩色?那是后面Demosaic的事,现在你只需要记住:RAW图就是一张单通道的马赛克图。
2.2 RAW数据的存储格式
RAW数据怎么存?这个问题看似简单,坑却不少。
传感器输出的RAW,通常是“裸数据”——没有文件头,没有元数据,就是一串像素值按行排列。常见的位深有:
- 8bit:0~255,老款传感器或低端应用
- 10bit:0~1023,最主流,手机传感器几乎全是
- 12bit:0~4095,高端相机、工业相机
- 14bit/16bit:0~16383/0~65535,专业相机、科学级传感器
但这里有个麻烦——10bit数据怎么存?按字节对齐的话,每个像素占2字节,浪费6个bit。所以厂商发明了各种打包方式:
| 存储格式 | 说明 | 每像素占用 |
|---|---|---|
| RAW10 | 4个像素打包成5字节 | 1.25字节 |
| RAW12 | 2个像素打包成3字节 | 1.5字节 |
| RAW16 | 直接按16bit对齐 | 2字节 |
| MIPI RAW | MIPI CSI-2协议定义的打包方式 | 取决于配置 |
我个人习惯在项目初期,先把RAW数据转成16bit的PGM或TIFF来调试。虽然文件大了点,但省心——不用反复纠结打包格式。等算法稳定了,再优化成RAW10/RAW12的存储。
调试小技巧:用Python读RAW10打包数据时,记得先按5字节一组解包。我写过一段代码专门做这个,核心逻辑就是:
# RAW10 4像素打包成5字节的解包示例
def unpack_raw10(data):
pixels = []
for i in range(0, len(data), 5):
b0, b1, b2, b3, b4 = data[i:i+5]
p0 = (b0 << 2) | ((b1 >> 6) & 0x03)
p1 = ((b1 & 0x3F) << 4) | ((b2 >> 4) & 0x0F)
p2 = ((b2 & 0x0F) << 6) | ((b3 >> 2) & 0x3F)
p3 = ((b3 & 0x03) << 8) | b4
pixels.extend([p0, p1, p2, p3])
return pixels
2.3 黑电平与白电平
这两个概念,很多初学者容易搞混。我简单说清楚。
黑电平(Black Level):传感器在完全无光(盖上镜头盖)时,输出的像素值不是0。因为电路有暗电流、有噪声,读出来的值可能是64(10bit下)。这个值就是黑电平。
白电平(White Level):传感器能输出的最大有效值。10bit下理论是1023,但实际可能设成1020或1018,留一点余量防止饱和溢出。
为什么要减黑电平?因为RAW里的像素值 = 真实信号 + 黑电平。不减掉它,暗部会发灰,而且白平衡计算也会被干扰。
校正公式:
pixel_corrected = (pixel_raw - black_level) * white_level_gain
# 其中 white_level_gain = target_white / (white_level - black_level)
# 通常 target_white 设为 white_level 或 2^bitdepth - 1
我在项目中遇到过一款传感器,黑电平在不同温度下会漂移。常温下是64,高温到80°C时漂到了72。如果不做温度补偿,暗部偏色会越来越严重。后来我加了一行代码,每帧读取传感器温度,动态调整黑电平值,问题就解决了。
注意:黑电平校正必须在Demosaic之前做。因为Bayer各通道的黑电平可能不同——R通道和B通道的暗电流特性不一样。我曾经见过一个案例,工程师在RGB图像上做黑电平校正,结果偏色怎么都调不好。后来发现是Bayer域各通道黑电平不一致导致的。
2.4 本章知识结构图
下面这张图把RAW图的核心概念串起来了。你可以把它当作一张“地图”,以后遇到RAW相关的问题,先看这张图定位。
2.5 实战:用Python读取并显示RAW图
光说不练假把式。下面这段代码,我经常在新项目里用来快速验证RAW数据是否正确。
import numpy as np
import matplotlib.pyplot as plt
def load_raw10_to_16bit(filepath, width, height):
"""
读取RAW10打包数据,转为16bit numpy数组
"""
with open(filepath, 'rb') as f:
raw_bytes = f.read()
# 计算像素数
total_pixels = width * height
# RAW10每4像素占5字节
expected_bytes = (total_pixels * 5) // 4
if len(raw_bytes) != expected_bytes:
print(f"文件大小异常:期望{expected_bytes}字节,实际{len(raw_bytes)}字节")
return None
# 解包
pixels = []
for i in range(0, len(raw_bytes), 5):
b0, b1, b2, b3, b4 = raw_bytes[i:i+5]
p0 = (b0 << 2) | ((b1 >> 6) & 0x03)
p1 = ((b1 & 0x3F) << 4) | ((b2 >> 4) & 0x0F)
p2 = ((b2 & 0x0F) << 6) | ((b3 >> 2) & 0x3F)
p3 = ((b3 & 0x03) << 8) | b4
pixels.extend([p0, p1, p2, p3])
# 转为16bit(左移6位,填满16bit范围)
img = np.array(pixels, dtype=np.uint16).reshape(height, width)
img = img << 6
return img
# 使用示例
# img = load_raw10_to_16bit('sensor_raw.raw', 1920, 1080)
# if img is not None:
# plt.imshow(img, cmap='gray', vmin=0, vmax=65535)
# plt.title('RAW图(黑电平校正前)')
# plt.show()
调试建议:第一次读RAW图时,先别急着做任何处理。直接显示灰度图,看看图像轮廓是否正常。如果图像看起来像“条纹布”,大概率是Bayer模式搞错了。如果图像整体偏亮或偏暗,检查黑电平值是否正确。
嗯,RAW图的基础就这些。Bayer阵列决定了数据的“马赛克”结构,存储格式决定了你怎么把字节变成像素,黑电平与白电平决定了像素值的真实含义。这三件事搞不清楚,后面的白平衡、Demosaic、去噪全是空中楼阁。