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)。我个人习惯先确认文件头大小,再读像素数据。

小技巧: 如果你不确定文件头大小,可以用十六进制编辑器打开RAW文件,看看前几个字节是不是"II"(小端)或"MM"(大端),这是TIFF/DNG的标志。

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,后来才发现是位深搞错了。

注意: 10位/12位RAW数据有两种存储方式:
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读取与解析流程

下面这张图总结了整个流程,我建议你把它记在脑子里:

RAW文件读取与Bayer解析流程 RAW文件读取与Bayer解析流程 RAW文件 文件头解析 像素数据读取 numpy数组 Bayer模式识别 R通道 G1通道 G2通道 B通道 注意事项 • 位深对齐 • 文件头大小 • Bayer模式确认

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。
我的经验: 拿到一个新Sensor的RAW文件,我第一件事就是打印前100个像素的值,看看范围是否合理。如果全是0或全是4095(12位最大值),那肯定读错了。