4. 原始图像数据采集与预处理

拿到一颗 onsemi 图像传感器(例如 AR0234、AR0521、AR0821 这类常见型号)后,真正决定后续噪声与坏点治理效果的,往往不是算法本身,而是原始数据在进入算法之前是否被正确解析和预处理。本节围绕一条完整链路展开:从传感器输出的 raw 数据格式,到 Bayer 阵列的排布还原,再到黑电平校正与线性化,最后用 Python 或 MATLAB 把数据读成可用的二维数组。

一、先弄清 onsemi 传感器输出的是什么

onsemi 的 CMOS 图像传感器通常通过 MIPI CSI-2 或并行接口输出原始 Bayer 数据。以 10-bit 输出为例,每个像素的有效数据占 10 位,但传输时一般按 16 位对齐存放,即每个像素占 2 字节,高 10 位有效、低 6 位补零或填充。这一点非常关键:如果直接按 8 位读取,图像会完全错乱。

常见的几种存放方式:

  • MIPI RAW10:4 个 10-bit 像素打包成 5 字节,传输效率高,但解析时需要按位重组。
  • MIPI RAW12:2 个 12-bit 像素打包成 3 字节。
  • 16-bit 对齐:每个像素占 2 字节,低 6 位为 0,解析最简单,常用于调试阶段。

此外,onsemi 传感器在有效像素区之外通常还有嵌入式数据行(embedded data lines),里面包含曝光、增益、温度、帧计数等信息。这些行不属于图像内容,读取时必须跳过,否则会整体错位。

判断输出格式最直接的方法:查该型号的 datasheet 中 "Output Format" 与 "Embedded Data" 章节,确认 bit 深度、打包方式、embedded line 数量以及有效图像起始行。

二、Bayer 阵列:颜色从哪来

Bayer 阵列的本质是每个像素只感应一种颜色,通过 2×2 的重复单元恢复彩色信息。onsemi 传感器常见的起始相位有四种:GRBG、RGGB、BGGR、GBRG。相位判断错误,会导致红蓝通道互换或整体偏色。

以 RGGB 为例,2×2 单元为:

R  G
G  B

因此第 0 行第 0 列为 R,第 0 行第 1 列为 G,第 1 行第 0 列为 G,第 1 行第 1 列为 B。解析时,只要知道起始相位,就能把 raw 数组拆成 R、G、B 三个子平面。G 通道数量是 R、B 的两倍,这也是后续做白平衡和去马赛克时要特别注意的地方。

三、黑电平校正:把“零”找回来

传感器输出的 raw 值并不是从 0 开始的。由于暗电流、读出电路偏置等原因,即使完全没有光,像素也会输出一个非零的基底值,这就是黑电平(black level)。onsemi 传感器通常会在寄存器中给出一个标称黑电平,例如 10-bit 下为 64 或 128,但实际值会随增益和温度变化。

黑电平校正的做法是:从每个像素值中减去当前条件下的黑电平。更精细的做法是使用光学黑区(optical black,OB)行/列——这些像素被遮光,只输出黑电平,可以逐行或逐帧估计真实黑电平。

corrected = raw - black_level
corrected = max(corrected, 0)

注意减完后要截断到 0,避免出现负值。如果黑电平估计偏低,暗部会残留偏色;估计偏高,暗部会被压死、丢失细节。

四、线性化:让数值与光强成正比

理想情况下,传感器输出与入射光强成线性关系。但实际传感器在接近饱和区会压缩,在极暗区也可能有非线性。线性化就是通过查找表(LUT)或分段线性拟合,把 raw 值映射回线性响应。

对于大多数 onsemi 传感器,如果工作在线性区,线性化可以简化为归一化:

linear = (corrected) / (max_value - black_level)

其中 max_value 为满量程,例如 10-bit 为 1023。这样得到的值落在 [0,1] 区间,便于后续噪声建模和坏点检测统一处理。如果传感器启用了 HDR 或压缩曲线,则必须使用厂商提供的线性化 LUT,不能简单归一化。

onsemi raw 数据采集与预处理链路 onsemi 传感器 MIPI / 并行输出 RAW10 / RAW12 格式解析 跳过 embedded 行 按位重组 10/12bit Bayer 阵列还原 RGGB / GRBG 拆 R / G / B 平面 黑电平校正 减去 OB 估计值 截断到 0 线性化 归一化 / LUT 映射到 [0,1] 可用 raw 数组 float32 / uint16 供噪声与坏点分析 示例:10-bit RGGB,黑电平 64,满量程 1023 raw 原始值 R=320 G=280 减黑电平 64 R=256 G=216 除以 959 R≈0.267 G≈0.225 线性输出 [0,1]

五、用 Python 读取 onsemi raw 数据

假设我们拿到一帧 1920×1080、10-bit、16 位对齐、RGGB 相位的 raw 文件,文件名为 frame.raw。用 NumPy 读取并完成预处理:

import numpy as np

W, H = 1920, 1080
BLACK = 64
MAXV = 1023

# 16-bit 对齐,小端
raw = np.fromfile("frame.raw", dtype=np.<u2)
raw = raw[:W*H].reshape(H, W).astype(np.float32)

# 黑电平校正
corr = np.clip(raw - BLACK, 0, None)

# 线性化归一化
linear = corr / (MAXV - BLACK)

# 按 RGGB 拆通道
R = linear[0::2, 0::2]
Gr = linear[0::2, 1::2]
Gb = linear[1::2, 0::2]
B = linear[1::2, 1::2]

print(R.shape, Gr.shape, Gb.shape, B.shape)

如果文件是 MIPI RAW10 打包格式,则需要先按 5 字节一组解包:

def unpack_raw10(data):
    data = data.reshape(-1, 5).astype(np.uint16)
    out = np.zeros((data.shape[0], 4), dtype=np.uint16)
    out[:,0] = (data[:,0] << 2) | (data[:,4] & 0x03)
    out[:,1] = (data[:,1] << 2) | ((data[:,4] >> 2) & 0x03)
    out[:,2] = (data[:,2] << 2) | ((data[:,4] >> 4) & 0x03)
    out[:,3] = (data[:,3] << 2) | ((data[:,4] >> 6) & 0x03)
    return out.ravel()

六、用 MATLAB 读取与验证

MATLAB 中可以用 fread 直接读取,并快速可视化 Bayer 相位是否正确:

fid = fopen('frame.raw','r');
raw = fread(fid, W*H, 'uint16=>double');
fclose(fid);
raw = reshape(raw, [W, H])';

corr = max(raw - 64, 0);
linear = corr / (1023 - 64);

% 查看 2x2 单元,确认相位
disp(linear(1:2, 1:2));

% 简单可视化
imagesc(linear); colormap(gray); axis image;

判断相位是否正确,可以观察图像中天空、白墙等中性区域:如果 R 和 B 平面均值差异过大,而 G 平面明显偏高,通常说明相位判断有误,需要把起始坐标偏移一个像素再试。

七、预处理结果如何判断

完成上述步骤后,建议检查三个指标:

  • 暗区均值:遮光区域校正后应接近 0,若明显大于 0,说明黑电平估计偏低。
  • 饱和区:亮部归一化后应接近 1,若大量像素被截断在 1,说明曝光过度或满量程设置错误。
  • 通道均值比:在均匀白光下,R、G、B 均值应大致接近,偏差过大提示 Bayer 相位或白平衡问题。
不要跳过 embedded data 行,也不要把 10-bit 数据当 8-bit 读。这两类错误在 raw 域非常隐蔽,但会直接导致后续坏点检测和噪声估计全部失效。

把 raw 数据正确读成线性、已校正的浮点数组,是后续所有噪声建模与坏点治理工作的起点。这一节的处理链路虽然不长,但每一步的相位、位深、黑电平和满量程参数都必须与具体 onsemi 型号严格对应,建议在正式分析前先用一帧均匀光照图像做一次完整验证。