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数据读取的完整流程:
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]浮点区间