2. RAW数据读取:使用Python读取RAW文件(.dng, .raw)、解析RAW元数据、理解黑电平与白电平

好,咱们正式开始动手了。上一章聊了RAW是什么,这一章直接上代码——怎么把RAW文件读进Python里,顺便把藏在文件头里的那些关键参数挖出来。

说实话,我第一次接触RAW解析时,以为就是读个二进制数组那么简单。结果打开文件一看,满屏的字节流,完全不知道哪是哪。后来踩了不少坑,才慢慢摸清楚门道。

2.1 RAW文件不是一张“图片”

先纠正一个常见误区。很多人觉得RAW文件跟JPEG一样,打开就能看到图像。其实不是。RAW文件更像一个“传感器数据的容器”。里面除了像素值,还塞了一大堆元数据——相机型号、ISO、快门速度、黑电平、白电平、白平衡系数……

你想想看,如果不知道黑电平是多少,你连像素值是不是“纯黑”都判断不了。所以,解析元数据是第一步。

2.2 用Python读取DNG文件

DNG(Digital Negative)是Adobe推出的开放RAW格式。很多相机厂商也支持直接输出DNG。我个人习惯用 rawpy 这个库,它封装了LibRaw,用起来非常顺手。

核心工具链:

  • rawpy —— 读取RAW数据的主力库
  • exifread —— 解析EXIF元数据(备选)
  • numpy —— 处理像素数组

安装很简单:

pip install rawpy numpy exifread

然后看一段最基础的读取代码:

import rawpy
import numpy as np

# 读取DNG文件
raw = rawpy.imread('sample.dng')

# 获取原始Bayer数据(未去马赛克)
raw_data = raw.raw_image  # 这是一个二维numpy数组
print(f'图像尺寸: {raw_data.shape}')
print(f'数据类型: {raw_data.dtype}')
print(f'像素值范围: {raw_data.min()} ~ {raw_data.max()}')

# 关闭文件
raw.close()

运行这段代码,你会看到一个二维数组。它的形状是 (height, width),每个像素值通常是16位整数(0-65535)。但注意,这个范围是“原始”的,还没减去黑电平。

小提示: raw.raw_image 返回的是Bayer模式下的原始数据,不是RGB图像。如果你直接把它当灰度图显示,会看到密密麻麻的棋盘格纹理——那是正常的。

2.3 解析RAW元数据

光有像素值不够。你得知道这些值是怎么来的。元数据里藏着关键信息。

用 rawpy 可以轻松拿到:

# 获取元数据
print(f'相机型号: {raw.camera_white_level}')
print(f'白电平: {raw.white_level}')
print(f'黑电平: {raw.black_level_per_channel}')
print(f'Bayer模式: {raw.color_desc}')
print(f'ISO: {raw.iso}')
print(f'快门速度: {raw.shutter}')
print(f'光圈: {raw.aperture}')

这里有个细节——black_level_per_channel 返回的是一个数组。为什么是数组?因为传感器不同通道的黑电平可能不一样。我遇到过一台相机,R通道黑电平是512,G通道是508,B通道是515。如果不分别处理,暗部就会偏色。

参数 含义 典型值
white_level 传感器最大可记录值(白电平) 1023(10bit)/ 16383(14bit)/ 65535(16bit)
black_level 传感器最小有效值(黑电平) 512 ~ 2048(取决于相机和ISO)
color_desc Bayer滤镜排列顺序 'RGBG' 或 'BGGR' 等
camera_white_balance 相机白平衡乘数 [R_gain, G_gain, B_gain]

2.4 理解黑电平与白电平

这两个概念太重要了,我单独拿出来说。

黑电平(Black Level):传感器在没有光照时,仍然会输出一个非零值。这是电路噪声和暗电流造成的。这个值必须减去,否则暗部会发灰。

白电平(White Level):传感器能记录的最大值。超过这个值就过曝了,信息完全丢失。

说白了,RAW数据的有效范围是 [black_level, white_level]。处理的第一步,就是把数据归一化到 [0, 1] 区间:

def normalize_raw(raw_image, black_level, white_level):
    """
    将RAW数据归一化到[0, 1]区间
    """
    # 减去黑电平
    normalized = raw_image.astype(np.float32) - black_level
    # 确保不为负
    normalized = np.maximum(normalized, 0)
    # 除以动态范围
    normalized = normalized / (white_level - black_level)
    # 截断到[0, 1]
    normalized = np.clip(normalized, 0, 1)
    return normalized

注意: 黑电平可能随ISO变化。我曾经在调试一款手机摄像头时发现,ISO 100时黑电平是512,ISO 1600时变成了1024。如果你用固定值去减,高ISO下暗部细节全没了。所以,一定要从元数据里读实时值。

2.5 读取.raw裸文件

有些相机或传感器输出的是裸RAW文件(.raw后缀),没有DNG那样的封装结构。这时候你得自己知道图像的尺寸、位深、Bayer模式。

读取方式更原始:

import numpy as np

def read_raw_file(filepath, width, height, bpp=16):
    """
    读取裸RAW文件
    bpp: bits per pixel,通常是10、12、14、16
    """
    with open(filepath, 'rb') as f:
        raw_data = np.fromfile(f, dtype=np.uint16 if bpp > 8 else np.uint8)
    
    # 有些RAW文件是打包的(10bit打包成16bit),需要解包
    # 这里假设是标准16bit对齐
    raw_image = raw_data.reshape((height, width))
    return raw_image

嗯,这里要注意。很多工业相机输出的是10bit或12bit数据,但为了对齐,每个像素用16bit存储。也有的是打包格式——3个10bit像素塞进4个字节里。遇到这种情况,你得写解包函数。

2.6 知识结构图

下面这张图总结了RAW数据读取的完整流程:

RAW数据读取与解析流程 RAW数据读取与解析流程 RAW文件 (.dng / .raw) 文件格式判断 rawpy.imread() np.fromfile() + reshape 解析元数据 黑电平 / 白电平 / Bayer模式 / ISO 归一化到 [0, 1] (raw - black_level) / (white_level - black_level) 归一化RAW数据 💡 关键点 • 黑电平必须从元数据读取 • 白电平决定动态范围上限 • 不同通道黑电平可能不同

2.7 实战:完整的读取管线

把上面所有知识点串起来,写一个完整的读取函数:

import rawpy
import numpy as np

def load_and_normalize_raw(filepath):
    """
    完整的RAW读取与归一化管线
    返回: normalized_data (H, W), metadata (dict)
    """
    # 1. 读取文件
    raw = rawpy.imread(filepath)
    
    # 2. 提取原始数据
    raw_image = raw.raw_image.copy()
    
    # 3. 提取元数据
    black_level = raw.black_level_per_channel
    white_level = raw.white_level
    bayer_pattern = raw.color_desc.decode('utf-8')
    
    # 4. 处理黑电平(取平均值或按通道处理)
    if isinstance(black_level, (list, np.ndarray)):
        black_level_avg = np.mean(black_level)
    else:
        black_level_avg = black_level
    
    # 5. 归一化
    normalized = raw_image.astype(np.float32) - black_level_avg
    normalized = np.maximum(normalized, 0)
    normalized = normalized / (white_level - black_level_avg)
    normalized = np.clip(normalized, 0, 1)
    
    # 6. 打包元数据
    metadata = {
        'black_level': black_level,
        'white_level': white_level,
        'bayer_pattern': bayer_pattern,
        'iso': raw.iso,
        'shutter': raw.shutter,
        'aperture': raw.aperture,
        'camera_model': raw.camera_white_level  # 实际是white_level的别名
    }
    
    raw.close()
    return normalized, metadata

# 使用示例
data, meta = load_and_normalize_raw('sample.dng')
print(f'归一化后数据范围: {data.min():.4f} ~ {data.max():.4f}')
print(f'相机元数据: {meta}')

避坑指南: 我曾经在处理一批无人机拍摄的RAW文件时,发现黑电平值在图像边缘和中心不一样。后来查资料才知道,有些传感器用了“光学黑电平”区域——传感器边缘被遮挡的像素,用来实时测量黑电平。如果你用全局黑电平去减,边缘会偏暗。解决办法是:用边缘像素的平均值作为该行的黑电平。

2.8 小结

这一章的核心就三件事:

  • 读文件 —— 用 rawpy 读DNG,用 numpy 读裸RAW
  • 挖元数据 —— 黑电平、白电平、Bayer模式,一个都不能少
  • 归一化 —— 把原始整数映射到 [0, 1] 浮点区间