4、RAW文件读取与解析:用Python搞定Bayer数据
说实话,很多做ISP的工程师,真正动手写代码的第一关就是RAW文件读取。我记得刚入行那会儿,拿到一块Sensor的RAW数据,连怎么打开都不知道——直接双击,结果系统告诉我“无法识别”。嗯,RAW文件不是标准图片格式,它就是个裸数据,你得自己解析。
这一章,我们就来亲手把RAW文件读进内存,把Bayer数据拆开,看看每个通道到底长什么样。
4.1 RAW文件的结构:先搞清楚它长什么样
RAW文件说白了就是Sensor直接输出的数字信号,没有经过任何ISP处理。它通常包含两部分:文件头 + 像素数据。
| 组成部分 | 说明 | 常见大小 |
|---|---|---|
| 文件头 | 包含图像尺寸、Bayer模式、位深、厂商信息等 | 几十到几百字节 |
| 像素数据 | Bayer排列的原始像素值,按行顺序存储 | width × height × 像素字节数 |
不同厂商的RAW格式不一样。比如DNG有标准TIFF头,而一些Sensor厂商直接给纯裸数据(.raw或.bin)。我个人习惯先确认文件头大小,再读像素数据。
4.2 用Python读取RAW文件:从裸数据到numpy数组
Python里最常用的工具就是numpy和struct。我一般这么干:
import numpy as np
import struct
def read_raw_file(file_path, width, height, bit_depth=10, header_size=0):
"""
读取RAW文件,返回numpy数组
:param file_path: RAW文件路径
:param width: 图像宽度(像素)
:param height: 图像高度(像素)
:param bit_depth: 位深(8/10/12/14/16)
:param header_size: 文件头字节数
:return: numpy数组,shape=(height, width)
"""
# 计算像素字节数
if bit_depth <= 8:
dtype = np.uint8
bytes_per_pixel = 1
elif bit_depth <= 16:
dtype = np.uint16
bytes_per_pixel = 2
else:
raise ValueError("不支持的位深")
# 读取文件
with open(file_path, 'rb') as f:
# 跳过文件头
f.seek(header_size)
# 读取像素数据
raw_data = f.read(width * height * bytes_per_pixel)
# 转换为numpy数组
img = np.frombuffer(raw_data, dtype=dtype).reshape(height, width)
return img
这里有个坑——10位和12位数据。很多Sensor输出10位数据,但为了对齐,每个像素占2字节(16位),高6位是0。我曾经遇到过这种情况:读出来全是0,后来才发现是位深搞错了。
1. 每个像素占2字节,高位补0(最常见)
2. 5个10位像素打包成8字节(少见,需要位操作解包)
4.3 解析Bayer数据:把RGGB拆开
拿到完整的RAW图像后,下一步就是提取Bayer通道。常见的Bayer模式有RGGB、BGGR、GRBG、GBRG。我习惯用坐标偏移来提取:
def extract_bayer_channels(raw_img, bayer_pattern='RGGB'):
"""
提取Bayer四个通道
:param raw_img: 原始RAW图像 (H, W)
:param bayer_pattern: Bayer模式,如'RGGB', 'BGGR'
:return: dict, 包含R, G1, G2, B通道
"""
h, w = raw_img.shape
channels = {}
# 根据Bayer模式确定每个通道的偏移
pattern_map = {
'RGGB': {'R': (0, 0), 'G1': (0, 1), 'G2': (1, 0), 'B': (1, 1)},
'BGGR': {'B': (0, 0), 'G1': (0, 1), 'G2': (1, 0), 'R': (1, 1)},
'GRBG': {'G1': (0, 0), 'R': (0, 1), 'B': (1, 0), 'G2': (1, 1)},
'GBRG': {'G1': (0, 0), 'B': (0, 1), 'R': (1, 0), 'G2': (1, 1)},
}
offsets = pattern_map.get(bayer_pattern)
if offsets is None:
raise ValueError(f"不支持的Bayer模式: {bayer_pattern}")
for ch, (row_off, col_off) in offsets.items():
channels[ch] = raw_img[row_off:h:2, col_off:w:2]
return channels
提取出来的每个通道,尺寸都是原图的一半。比如1920×1080的RAW,R通道就是960×540。你想想看,这其实就是把彩色图像拆成了四个马赛克块。
4.4 可视化Bayer通道:看看每个通道长啥样
光看数字没意思,我习惯把每个通道显示出来,直观感受一下:
import matplotlib.pyplot as plt
def visualize_bayer_channels(channels):
"""
可视化Bayer四个通道
"""
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
titles = ['R', 'G1', 'G2', 'B']
colors = ['Reds', 'Greens', 'Greens', 'Blues']
for ax, ch, title, cmap in zip(axes.flat,
[channels['R'], channels['G1'],
channels['G2'], channels['B']],
titles, colors):
im = ax.imshow(ch, cmap=cmap, aspect='auto')
ax.set_title(title)
plt.colorbar(im, ax=ax)
plt.tight_layout()
plt.show()
运行这段代码,你会看到R通道偏暗(因为红色分量少),G通道最亮(人眼对绿色最敏感),B通道介于中间。这就是Bayer的物理特性——绿色像素占一半,红色和蓝色各占四分之一。
- RAW文件 = 文件头 + 裸Bayer数据
- 用numpy读取时,注意位深和字节对齐
- Bayer通道提取本质是隔行隔列采样
- G通道有两个子通道(G1和G2),它们位置不同但都是绿色
4.5 实战:完整读取流程
把上面几段代码串起来,就是一个完整的RAW读取流水线:
# 完整流程
raw_img = read_raw_file('sensor_raw.raw', width=1920, height=1080,
bit_depth=10, header_size=0)
channels = extract_bayer_channels(raw_img, bayer_pattern='RGGB')
print(f"RAW图像尺寸: {raw_img.shape}")
print(f"R通道尺寸: {channels['R'].shape}")
print(f"G1通道尺寸: {channels['G1'].shape}")
print(f"G2通道尺寸: {channels['G2'].shape}")
print(f"B通道尺寸: {channels['B'].shape}")
print(f"R通道均值: {channels['R'].mean():.2f}")
print(f"G通道均值: {(channels['G1'].mean() + channels['G2'].mean()) / 2:.2f}")
print(f"B通道均值: {channels['B'].mean():.2f}")
输出结果大概是这样:
RAW图像尺寸: (1080, 1920)
R通道尺寸: (540, 960)
G1通道尺寸: (540, 960)
G2通道尺寸: (540, 960)
B通道尺寸: (540, 960)
R通道均值: 512.34
G通道均值: 768.21
B通道均值: 601.45
看到没?G通道均值明显高于R和B,这就是Bayer传感器的典型特征。我在项目中经常用这个均值比例来判断Bayer模式是否正确——如果R和B均值差不多,但G明显高,那基本就对了。
4.6 知识结构:RAW读取与解析流程
下面这张图总结了整个流程,我建议你把它记在脑子里:
4.7 常见问题与避坑
我在项目中踩过不少坑,挑几个典型的说说:
- 字节序搞反了:有些Sensor输出小端,有些大端。读出来图像花花的,八成是字节序错了。用
np.frombuffer(..., dtype='<u2')或'>u2'试试。 - 文件头大小猜错:我曾经读一个DNG文件,以为头是0字节,结果图像全是斜条纹。后来发现DNG有8KB的TIFF头。
- Bayer模式搞混:RGGB和BGGR就差一个像素偏移,但颜色完全反了。我习惯先拍一张纯白画面,看R和B通道的均值——如果R比B高,那可能是RGGB;反之是BGGR。
- 10位数据当8位读:有些Sensor输出10位但存成8位(截断低位),图像会偏暗且丢失细节。最好先确认Sensor的datasheet。