第一章:RAW数据读取与存储
各位同学,咱们今天正式开讲。第一章,我打算从最基础也最容易被忽略的地方说起——怎么把RAW文件从磁盘里读出来,再存成我们能处理的形式。
你可能觉得这有什么难的?不就是open一个文件,read一下嘛。嗯,我当年也是这么想的。直到有一次,我花了两天时间调试一个ISP流水线,最后发现是字节序搞反了……那感觉,真是一言难尽。
1.1 二进制RAW文件长什么样?
RAW文件,说白了就是传感器直接输出的原始数据。没有头,没有尾,没有元数据。就是一串二进制数,一个像素接一个像素地排在那里。
举个例子。一个1000x1000的12bit RAW图,文件大小就是1000 * 1000 * 12 / 8 = 1,500,000字节。注意,这里不是1,500,000个像素,而是1,500,000个字节。因为12bit不是整数个字节,所以存储的时候会有一些“坑”。
我个人习惯把RAW文件分成三类:
- 纯裸数据:没有任何封装,直接是像素值
- 带简单头部的RAW:比如DNG、某些厂商的私有格式
- 打包格式:比如MIPI RAW,数据是按特定方式打包的
咱们这一章,先讲第一种。后面章节会逐步深入。
1.2 字节序:Little-Endian vs Big-Endian
这是个老生常谈的问题,但也是我见过最多人翻车的地方。
什么是字节序?简单说,就是一个多字节数据在内存里怎么排列。比如一个16bit的值0x1234:
- Little-Endian:低字节在前,存成 0x34, 0x12
- Big-Endian:高字节在前,存成 0x12, 0x34
你想想看,如果读错了字节序,一个暗区的像素值可能从0x0010变成0x1000,直接变成高光区域。我在项目中遇到过这种情况,当时查了半天,还以为是传感器坏了。
重要提醒:大多数PC和ARM平台都是Little-Endian。但有些工业相机、老式传感器会用Big-Endian。一定要看传感器的datasheet确认。
怎么判断?我一般先读几个像素,看看值是否在合理范围内。比如12bit RAW,像素值应该在0-4095之间。如果读出来一堆大于4095的数,那八成是字节序搞反了。
1.3 位深处理:10bit, 12bit, 14bit, 16bit
位深,就是每个像素用多少bit来表示。常见的就这四种。但存储方式各有不同。
| 位深 | 每像素字节数 | 存储方式 | 常见场景 |
|---|---|---|---|
| 10bit | 1.25 | 打包或补齐16bit | 手机摄像头、MIPI RAW |
| 12bit | 1.5 | 打包或补齐16bit | 工业相机、高端手机 |
| 14bit | 1.75 | 通常补齐16bit | 单反相机、科学相机 |
| 16bit | 2 | 直接存储 | 天文相机、医疗影像 |
这里有个关键点:10bit和12bit的数据,很多时候不是按bit位紧密打包的,而是每个像素占2个字节(16bit),高位对齐或低位对齐。为什么?因为这样读写快,CPU处理起来方便。
我曾经接手过一个项目,对方给的RAW文件是10bit打包格式,5个字节存4个像素。嗯,那个解析代码写得我头皮发麻。后面我会专门讲打包格式的处理。
1.4 Python读取RAW文件实战
好了,理论说完了,咱们直接上代码。我习惯用numpy来处理,因为后面做图像处理方便。
import numpy as np
import struct
def read_raw_16bit(filepath, width, height, endian='little'):
"""
读取16bit RAW文件
:param filepath: 文件路径
:param width: 图像宽度
:param height: 图像高度
:param endian: 'little' 或 'big'
:return: numpy数组,shape=(height, width)
"""
with open(filepath, 'rb') as f:
raw_data = f.read()
# 检查文件大小
expected_size = width * height * 2
if len(raw_data) != expected_size:
print(f"警告:文件大小不匹配。期望{expected_size},实际{len(raw_data)}")
# 根据字节序选择dtype
if endian == 'little':
dtype = np.dtype('<u2') # little-endian unsigned 16bit
else:
dtype = np.dtype('>u2') # big-endian unsigned 16bit
img = np.frombuffer(raw_data, dtype=dtype)
img = img.reshape((height, width))
return img
小技巧:我一般会在读取后打印前几个像素的值,快速验证字节序是否正确。比如打印img[0, :5],看看值是否在合理范围内。
对于12bit数据,如果每个像素占2个字节,那读取方式跟16bit一样。只是后面处理时要注意,有效数据只有低12位。
def read_raw_12bit_as_16bit(filepath, width, height, endian='little'):
"""
读取12bit RAW文件(每个像素占2字节)
"""
img = read_raw_16bit(filepath, width, height, endian)
# 只取低12位
img = img & 0x0FFF
return img
如果是打包格式呢?比如10bit打包,5个字节存4个像素。这个稍微复杂一点。
def read_raw_10bit_packed(filepath, width, height):
"""
读取10bit打包RAW文件
5个字节存4个像素
"""
with open(filepath, 'rb') as f:
raw_data = f.read()
# 每5个字节解出4个像素
num_pixels = width * height
num_groups = num_pixels // 4
img = np.zeros(num_pixels, dtype=np.uint16)
for i in range(num_groups):
group = raw_data[i*5 : (i+1)*5]
# 解包逻辑
p0 = (group[0] << 2) | (group[1] >> 6)
p1 = ((group[1] & 0x3F) << 4) | (group[2] >> 4)
p2 = ((group[2] & 0x0F) << 6) | (group[3] >> 2)
p3 = ((group[3] & 0x03) << 8) | group[4]
img[i*4 : (i+1)*4] = [p0, p1, p2, p3]
img = img.reshape((height, width))
return img
注意:打包格式的解析一定要仔细核对datasheet。不同厂商的打包方式可能不同。我曾经因为少看了一行文档,导致解析出来的图像全是条纹,查了整整一天。
1.5 存储RAW数据
读取之后,我们经常需要把处理过的数据存回去。存储时要注意几点:
- 保持原始位深:如果原始是12bit,存成16bit会浪费空间,但方便后续处理
- 字节序一致性:存的时候要跟读取时保持一致,不然别人读你的文件会出问题
- 文件命名规范:我习惯在文件名里带上宽高和位深信息,比如
img_1920x1080_12bit.raw
def save_raw_16bit(img, filepath, endian='little'):
"""
保存16bit RAW文件
"""
if endian == 'little':
dtype = np.dtype('<u2')
else:
dtype = np.dtype('>u2')
raw_data = img.astype(dtype).tobytes()
with open(filepath, 'wb') as f:
f.write(raw_data)
嗯,这一章的内容差不多就这些。总结一下:
- RAW文件就是裸数据,没有头信息
- 字节序搞错了,图像就全乱了
- 不同位深的存储方式不同,10bit和12bit经常有坑
- Python用numpy读取很方便,但要注意dtype和字节序
下一章,我会讲Bayer格式和去马赛克。那是ISP流水线里最核心的一步,也是我花时间最多的地方。咱们下章见。
公众号:蓝海资料掘金营,微信deep3321