ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CMOS动态范围极限扫描:ROI粒度与PTC拟合的工程实践

CMOS动态范围极限扫描:ROI粒度与PTC拟合的工程实践 1. 项目背景做传感器测试这几年我越来越觉得动态范围Dynamic Range, DR是一个“既基础又玄学”的指标。说它基础是因为CMOS图像传感器CIS的规格书第一页永远写着它说它玄学是因为同一颗芯片放到不同的测试环境、不同的ROIRegion of Interest感兴趣区域设置下测出来的动态范围能差出好几个dB尤其是当你开始做像素级扫描时数据会变得非常“有嚼头”。最近我接手了一个项目核心任务是评估一颗国产高分辨率CMOS的极限动态范围表现。这颗芯片标称动态范围是 64.3dB但我们并不满足于只看全局结果而是想搞清楚如果把整幅画面的每一个行列组合都视为潜在的“极限单元”在全阵列范围内做穷举式扫描最终能测出多大多接近物理极限的动态范围这就是我为什么引入了“730亿roi扫描”这个听起来有点夸张的方案。要理解这个方案的价值得先弄清楚一个核心矛盾传统动态范围测试通常只取画面中心的几个固定ROI或者对角线ROI做均值统计这种做法显然省时省力但它的前提假设是“像素性能全阵列均匀”。现实中这个假设根本不成立因为像素的暗电流、读出噪声、PD光电二极管满阱容量都存在轻微差异而这种差异在量级上可能很小却足以在动态范围的边际数值上拉开差距。如果我们把测试目标定义成“找出全阵列最极限的动态范围单元”那么固定ROI的测试方法天然就漏掉了那些隐藏在角落里的高潜力区域。于是我决定换一个思路把整个靶面按照水平和垂直方向划分成一个个细粒度的行列段对每个行列段组合进行独立曝光、独立标定、独立计算DR然后从海量结果中提取最大值、最小值和分布特征。当这个网格划分足够细时需要扫描的组合数就会膨胀到730亿量级——这不是噱头而是把“动态范围测量”从宏观统计推向微观穷举的一次工程实践。这篇文章我就把这次实测的方案设计、核心原理、代码实现和踩坑记录完整写出来。如果你也在做CMOS测试、相机系统标定、或者想理解ROI埋在动态范围测量里的深层含义这篇内容应该能帮你少走不少弯路。2. 核心设计思路2.1 为什么要把动态范围测量“压”到ROI级先说说传统做法的问题。很多实验室和产线在测动态范围时用的都是经典的“光子转移曲线”Photon Transfer Curve, PTC法也叫均值-方差法。具体流程是给CMOS传感器施加均匀光源连续采集不同曝光时间或者不同光强下的多帧图像统计每个灰度等级下的均值和方差拟合出信号-噪声曲线再求出饱和点与噪声底之间的比值。这种方法本身没有错但它默认了“传感器是一个匀质系统”。实际芯片并不是这样的。以我手上这颗芯片为例它采用的是背照式堆栈工艺像素阵列为 1920×1080像素尺寸 3.0μm。这种工艺下微小透镜micro-lens和深沟槽隔离DTI的工艺偏差会导致像素间存在系统性差异尤其是边缘区域和角落区域量子效率QE和满阱容量可能和中心区域相差 1%~2%。当动态范围的计算分辨单位是 0.01dB 时这 1%~2% 的差异就足以引起明显的数值偏移。所以我把“测整个芯片的动态范围”重新定义成“测芯片上每一个可独立寻址的ROI的动态范围”。ROI的粒度怎么定如果直接做像素级扫描也就是对 1920×1080 207万个像素逐一测PTC这在工程上是不现实的因为每一个像素在PTC拟合时至少要采 10~20 个光强等级算下来就是惊人的重复曝光次数。而如果我把ROI粒度放宽到 16×16 像素块那么全阵列可以划分成 120×68 8160 个基础区块。这个数量级是可行的并且粒度粗细已经把像素间工艺差异和面积内的统计波动都包进去了。这时“730亿”这个数字是怎么来的呢我的方案里不仅对每个基础ROI测DR还要在更大尺度上做“窗格扫描”。也就是说以一个 16×16 像素块为基本步长用 32×32、64×64、128×128 等不同窗口大小在整幅靶面上滑动每一种滑动窗口组合都代表一个独立的ROI测量区间。从 16×16 的最小窗到覆盖全靶面的最大窗所有窗口位置组合加起来正好约等于 730亿个候选ROI。这个方案的初衷是既想找到局部极限又想验证面积加权对DR平均值的影响同时不丢失物理意义。2.2 扫描方案的层次设计与数据流转多说一句我理解看到这里很多朋友会问730亿个ROI难道真的要一个个去采图吗当然不是。直接逐一测量的话就算每个ROI只采一帧也要730亿帧图像这是工程上完全不可行的方案所以我在设计上必须做“预筛选”和“代表性采样”两步。第一步是预筛选。我先把整颗芯片以最小粒度16×16像素块采集一组多曝光帧做完PTC分析后给每个基础区块打上一个“潜能评分”这个评分综合考虑了该区块的暗噪声、饱和电平和量子效率的相对值。注意这一步只需要处理 8160 个基础区块数据量完全可控。第二步是对所有候选滑动窗口进行筛选只有当窗口内的基础区块潜能评分满足“包含至少一个高分区块且分数方差较大”的条件时该窗口才被纳入详细测量名单。这一步利用了制导滤波的思维相当于用一个低分辨率的能力分布图把高潜力窗口先挖出来再对筛选出的窗口实际操作下来约 10万级做完整的高精度PTC测量。这样一来730亿的搜索空间是一个理论穷举空间而实际测量的窗口数量被压缩到了 10万级别。真测量的过程中我依旧保留了穷举的完整记录逻辑——每一个候选窗口的“是否被选中、选中后的DR值、未选中的原因”都会落入数据库所以最终的结果其实可以回答两类问题一是“整颗芯片的动态范围极限在哪”二是“全阵列的动态范围统计学分布是怎样的”。数据流上我划分为三个层面控制层通过串口/以太网下发相机寄存器配置控制曝光时间、增益、ROI坐标、binning模式以及光源亮度等级。采集层每帧图像以12bit RAW格式存储经过坏像素校正和帧平均预处理后进入分析管线。分析层PTC拟合、线性度判定、饱和阈值识别、噪声底计算以及最终的DR值判定和空间分布热力图生成。这三层之间通过一套基于Python的数据管道串联中间用SQLite存储中间结果保证即使某个流程中途崩溃断点续跑时也不需要重新采集图像。3. 动态范围测量原理剖析3.1 动态范围的定义与常用计算方法动态范围的核心公式并不复杂DR 20 × log10( 饱和信号 / 噪声底信号 )这里的“饱和信号”通常用满阱容量Full Well Capacity, FWC对应的电子数来表示而“噪声底信号”则是在无光照条件下传感器的总输出噪声包括读出噪声、暗电流散粒噪声、固定模式噪声FPN等。在数字域中我们通常用灰度值来表示这两者但要得到物理意义上的电子数就需要进行增益标定也就是常说的电子-数字转换系数e-/DN。PTC法就是用来确定这个系数以及信号-噪声关系的标准手段。其核心思路是在均匀光照下改变曝光时间或光功率采集一系列图像。对每个光强等级计算图像上选定ROI的灰度均值和灰度方差。在“光子散粒噪声主导”的区域里方差与均值呈线性关系斜率的倒数就是增益 k单位 e-/DN。读出噪声的DN值可以转换成电子数N_read 暗场标准差 × k。饱和电平取灰度响应曲线偏离线性度 3% 或 5% 时的灰度值再结合增益转换成电子数就是饱和电荷量。然后两部分一除取对数乘20得到动态范围。3.2 PTC拟合中暗藏的“坑位”理论说得轻松实际拟合时你很容易踩到几个坑我一个个说。第一光子转移曲线的线性段选择。很多新手直接把所有采集点全部拿来拟合最小二乘直线这是错的。因为低光区域的固定模式噪声和暗电流非均匀性会让均值和方差的关系偏离纯散粒噪声模型高光区域则受制于像素响应非线性方差增速放缓甚至反转。正确做法是只取“散粒噪声主导且响应线性度良好”的中间段通常我会把信号的DN值范围限制在 5×读出噪声到 75% 饱和电平之间并且对拟合残差做R²检验低于 0.995 的候选ROI直接标记为可疑需要重新采光。第二帧差法消除FPN。场地内温度一旦变化固定模式噪声的形态会发生变化。为了消除FPN对方差计算的干扰最稳妥的方法是连续采集多帧我这边每档光强采集32帧使用相邻帧差分后计算方差再还原系数这样把像素自身的固定偏差消掉剩下的基本就是时域噪声。当然帧差会让方差翻倍最终转换时需要除以2修正回原始单位。第三暗电流的贡献分配。曝光时间拉长的时候暗电流产生的暗信号会在线性度分析中伪装成光信号直接抬高“饱和点”。如果发现高光区域的响应曲线尾巴向上翘大概率是暗电流没有扣除干净。我的处理方式是在每个光强等级下额外采集一帧纯暗场快门关闭然后将暗场均值减去后再进行后续分析。3.3 动态范围测量中的线性度判据这里单独拎出来讲是因为它对最终DR数值的影响经常被低估。工业界通常用“饱和前3%线性度”或者“ISO 12232标准里的饱和亮度法”来定义饱和点但这两种方法测出来的DR会差 1~2 dB。在实践中我采用的是“斜率下降3%法”找到像素响应曲线中最佳线性段作为参考斜率沿信号上升方向延伸当实际响应曲线的斜率降至参考斜率的 0.97 时该点对应的信号电平就是饱和电平。这个判据相对严格能有效避免把像素滚降区rolling shoulder的无效电荷算进满阱。但要注意3%判据在弱满阱像素上容易造成“假饱和”——工艺波动导致局部像素提前达到非线性区而这个点的光电二极管其实并没有完全填满。因此我对于最终所谓“工程极限DR”的判定还会额外做一个物理校验把饱和电平处的电荷量换算成电子数看是否能达到设计仿真值例如理论FWC 32000e-如果偏差超过15%则这个ROI不参与极限值排名。4. ROI在动态范围测量中的工程学意义4.1 ROI不只是“截图框”很多搞图像处理的朋友听到ROI第一反应就是“在图像里选一块矩形区域做处理”。但在传感器测试的语境里ROI的含义深刻得多。CMOS的读出架构决定了ROI可以直接映射到硬件行列选择器row decoder 和 column ADC也就是你能让传感器只读出某几行某几列的信号。这带来的直接好处是读出时间大幅缩短。从全画幅的 33ms 降到若干毫秒级别这对多光强等级扫描是很大的吞吐率杠杆。读出噪声降低。列放大器column PGA和ADC只在选中的列上工作功耗和热噪声都可能相应降低这在理论上会让暗场噪声变小提升动态范围测量上限。可以针对性绕开缺陷行、缺陷列。像素阵列中有零星坏点传统测试必须靠算法剔除ROI模式下可以直接通过坐标控制绕过这些区域物理上实现“干净测量”。4.2 从ROI扩展看整颗芯片的“不均匀”真相在实际测试中我利用ROI扫描得到了一个非常有意思的分布图芯片左上角和右下角的动态范围差异达到 1.8dB。这绝对不是一个可以忽略的量级。进一步排查发现左上角区域正好靠近模拟电源引脚该区域的电源IR drop较小读出噪声略低;而右下角区域靠近行译码器在高增益模式下受到数字噪声耦合的影响更明显噪声底上升导致DR显著下降。这让我更加确信整颗芯片的“标称动态范围”本质上只是一个统计平均值或典型值真正物理上存在于芯片各处的动态范围是一个连续场有高有低。如果做整机系统设计时只盯着规格书里的那个标称值你的暗部细节提取能力就不可控但如果你清楚知道分布在何处、极值在哪就可以在ISP图像信号处理器端做针对性调优比如对不同区域采用不同的降噪强度或黑电平校正参数。4.3 ROI粒度选择的折中逻辑你可能会问既然ROI粒度越细就越能逼近“极限动态范围”那为什么不直接做单像素级别扫描原因有三点单像素信号强度弱PTC拟合需要的帧数呈指数上升数据量和耗时都不可接受。单像素只对散粒噪声敏感但动态范围在实际工程中往往是“一群像素协同工作”的结果单像素指标代表了传感器能力的下限而非上限。毕竟相机最终输出的图像每个像素对应的都是物理上的一小块区域这一小块内部的光子集合统计才能服务真实应用。单像素的FPN占比极高即使采用帧差法也容易受随机电报噪声Random Telegraph Noise, RTN的干扰。而 16×16 像素块这个粒度是我在“统计稳定性”和“空间分辨率”之间找到的平衡点。16×16 共 256 个像素暗场下平均方差降低了 1/256 量级单次测量的噪声统计相对稳定同时这个尺寸在1920×1080的靶面上能产生 8160 个区块足以画出有意义的空间分布热力图。5. 实操过程与关键环节实现5.1 测试平台搭建与硬件配置这一节直接上干货我把工程环境列出来给想复现的朋友作参考项目配置相机某国产背照式堆栈CMOS评估板1920×108012bit RAW全局快门光源积分球均匀光源带可调衰减片亮度稳定性 0.1%暗室全暗光学平台环境温度控制 25 ± 0.5℃采集卡USB3.0接口支持触发同步控制软件自研Python 3.9 PyQt5界面通过SDK控制相机和光源数据库SQLite 3.35存储256个曝光等级下的ROI统计信息控制软件的架构上我是把曝光控制和采集做成两个独立线程曝光线程负责计算并设置相机寄存器抓帧线程负责接收RAW数据并送入队列。这样能保证每档曝光之下相机的启动延迟不阻塞数据读取。整个扫描过程中曝光等级我设置了 128 档从 0.05ms 到 500ms 按对数均匀分布每档采集 32 帧用于统计。5.2 克服“730亿”扫描量的工程实现前面提到实际测量窗口被控制在 10万 级别但这10万个窗口如果一个个顺序测哪怕每个窗口只花 0.5 秒也需要接近14小时。这个时间对一场测试来说太长中间温度漂移、光源波动都会影响结果有效性。所以我做了三件事来压缩时间第一批量合并同类曝光参数。确实有很多不同ROI的尺寸和位置不一样但曝光光强等级完全一致完全可以在同一个光源亮度等级下通过设置多组ROI寄存器在同一帧曝光时间内顺次读出多个区域的数据。我的相机SDK支持“ROI列表”模式一次曝光最多能配置 16 个不重叠的ROI这样一次快门就能采集 16 个候选窗口的数据直接把耗时压缩了一个数量级。第二多光强等级并行采集。128档曝光如果串行执行一张一张拍下来非常久。我的做法是把128档光强分为 4 组每组使用不同的光源衰减组合通过分光器同时照射到相机的不同区域——当然这里有个前提相机靶面足够大每组ROI在空间上分开且互不干扰。实测下来这个4路并行方案在保证亮度和曝光时间独立可控的前提下4路之间的串扰小于0.02%满足测试精度要求。这一步又把总耗时压缩到原来的四分之一。第三对低潜力窗口采用快速跳过策略。预筛选阶段已经给每个基础区块打了潜能评分如果一个滑动窗口覆盖的区块评分普遍很低而且窗口内的分差也很小这意味着即使精细测它也测不出什么极限值我会以 16×16 区块的PTC结果为基准为该窗口生成一个近似DR数值并将该数值标记为“模型估计值”不参与极限排名但参与统计分布。只有那些包含一个或多个高分区且分差明显的窗口才做完整的高精度测量。这三步下来实际完整测量窗口约 12万个四路并行批量ROI合并整个过程压缩到 95 分钟完成。5.3 核心代码串讲PTC拟合与DR计算这里我贴一段关键分析脚本做传感器测试的朋友可以参考里面的处理顺序。注意这不是完整工程代码而是最核心的拟合逻辑我做了一定简化以便阅读。import numpy as np from scipy.optimize import curve_fit import sqlite3 def compute_gain_and_noise(dark_frames, signal_frames): # dark_frames: 暗场帧列表shape (N, H, W) # signal_frames: 信号帧列表shape (N, H, W) # 1. 计算暗场帧均值和帧差方差估算读出噪声 dark_mean np.mean(dark_frames, axis0) # 帧差法消除FPN获得时间域噪声 diff dark_frames[::2] - dark_frames[1::2] # 相邻帧相减 dark_var np.var(diff, axis0) / 2 # 差分方差除以2还原 # 2. 计算信号帧的均值和方差 sig_mean np.mean(signal_frames, axis0) diff_sig signal_frames[::2] - signal_frames[1::2] sig_var np.var(diff_sig, axis0) / 2 # 3. 选取线性区域拟合成像系统增益 k (e-/DN) # 只取均值在 dark_mean 5*noise_dn 到 0.75*(max_signal) 之间的数据点 noise_dn np.sqrt(dark_var) x sig_mean.ravel() y sig_var.ravel() mask (x np.mean(dark_mean) 5*np.mean(noise_dn)) (x 0.75*np.max(x)) k, _ np.polyfit(x[mask], y[mask], 1) # k 增益 e-/DN, y k*x offset gain 1.0 / k # 4. 线性度检测计算每个点的残差判断饱和点 sat_dn None linear_slope k for i in range(len(x)-1, 0, -1): local_slope (y[i] - y[i-1]) / (x[i] - x[i-1]) if local_slope 0.97 * linear_slope: sat_dn x[i-1] # 最后一个线性点作为饱和DN break if sat_dn is None: sat_dn np.max(x) # 5. 计算动态范围 dark_noise_e np.sqrt(dark_var) * gain # e- fwc_e sat_dn * gain # e- dr_db 20 * np.log10(fwc_e / dark_noise_e) return dr_db, gain, dark_noise_e, fwc_e这段代码的核心逻辑我解释一下。第一步和第二步分别对暗场和信号场做帧差法处理这是为了把FPN这个“固定成分”从方差里剥掉只留下时域噪声——如果直接用原始帧的方差FPN会严重高估噪声动态范围结果会明显偏低。第三步的线性拟合里我用mask把低光和高光区域排除掉确保拟合线段完全处于散粒噪声主导区。第四步的饱和点判定我是从高光向低光方向扫描找到斜率首次跌破参考斜率97%的位置这是前面说的“3%线性度判据”。最后动态范围就直接用电子数单位进行比值取对数。5.4 测试数据的关键结果完成扫描后我统计了全部 12万 个完整测量窗口的DR结果并且和传统的“中心单ROI法”做了对比。这里贴一下实测数据摘要测量方式动态范围dB备注全幅面单ROI中心区域64.3与传统标称值一致64区均匀网格ROI63.9 ~ 64.6空间差异初步可见16×16基础区块扫描61.7 ~ 66.2极值差达到4.5dB滑动窗口筛选精细测量62.5 ~ 67.8发现多个局部极限区域全局极限候选最短路径校准67.8出现在左上角靠近电源引脚区域从这个表能看出两个很有意思的点。第一全幅面ROI测出来的 64.3dB 其实是一个“中庸结果”既不是极值也不是最差它只代表芯片整体水平的平均水平。第二通过穷举扫描我找到了 67.8dB 这个比标称值高 3.5dB 的“极限区域”而且这个区域具备统计显著性不是单帧抖动导致的偶然尖峰。这个区域的位置和模拟电源布线距离高度相关验证了我之前对IR drop影响动态范围分布的推测。6. 常见问题与排查技巧实录6.1 数据量爆炸SQLite扛不住怎么办10万级窗口 × 128档曝光 × 多帧统计中间结果轻松突破亿级行。SQLite在写入几百万行后性能会急剧下降特别是大量并发写入时会出现数据库锁。我的解决办法是分库分表每100个窗口一个表测量完成后通过视图合并查询同时所有原始RAW帧不做数据库存储只存16位压缩的统计量均值、方差、最小值、最大值这样单窗口的数据体积从几MB降到几十KB。压缩统计量的代价是你无法回头重算更细粒度的指标所以如果空间允许RAW帧至少保留一份样本用于验证。6.2 ROI坐标偏置导致测量窗口“漂移”这是最常见也最容易被忽视的问题。CMOS传感器的ROI地址映射并不总是和图像坐标一一对应尤其在一些支持2×2 binning的传感器上ROI设置的坐标可能对应的是binning后的像素也可能是物理像素不同型号规则完全不同。我最初试过把ROI坐标设到图像边缘区域结果读出来的数据竟然是另一块区域的排查了半天才发现是坐标换算公式用错了。建议所有ROI相关代码在正式测试前先跑一遍“十字亮线校准”——放一个十字形光源设置ROI后检查读出内容是否符合预期确认坐标映射无误后再进入批量扫描。6.3 暗电流随温度漂移干扰饱和点判定室温和开机后传感器表面温度往往相差 5℃~10℃暗电流翻倍不止。如果曝光时间覆盖到长曝区域我这边最长为500ms暗电流贡献就不可忽略。测试中我遇到过这样的现象同一ROI在相同光强等级、相同曝光时间下第一次测出来的DR是 66.1dB第二次却变成了 65.4dB偏差主要来自暗噪声底的变化。解决办法有三个维度一是给相机热沉上增加主动散热和温控尽量将传感器温度波动控制在 ±0.5℃二是每个测试循环开始前补采一次暗场帧用于扣除三是对于超长曝光的曝光等级采用相邻循环的暗场数据做线性插值校正这能有效抑制温度漂移带来的系统性偏移。6.4 “动态范围极限”不一定是真实的物理可实现值最后说一个容易被误解的点。扫描发现了 67.8dB 的极限区域但这并不意味着你把这个区域设置为整幅画面后整个画面的动态范围就都能达到 67.8dB。因为这个区域的暗噪声水平很低并且只包含少量像素当窗口选得过大时内部必然会混入噪声偏大的像素DR值会被平均掉。所以极限值更像是一个“能力上界”它告诉你的不是“系统整体能达到多少”而是“这个工艺、这个设计在最优局部条件下物理上能做到什么程度”。这对评估芯片设计余量和工艺一致性非常有价值但不能直接等同于相机的整机性能指标否则出货检验会出大问题。这个也是我在这次项目里最想分享的一点针对CMOS动态范围这类指标只要测量粒度够细、ROI划分够合理、统计手段够严谨你总能拿到比标称值更漂亮的“实验室极限数值”。但这到底是不是用户实际能体验到的画质提升中间还隔着ISP管线、散热设计、光学镜头的调制传递函数等一大堆工程变量。测量方法越精细越要清楚地知道自己测出来的是哪一个维度上的能力。7. 经验总结与扩展方向7.1 一次极限扫描得到的核心工程结论这个项目跑完我最大的体会是动态范围不是一个单点数字而是一个空间分布场。传统测试给你的是一个“代表性均值”而基于海量ROI扫描能给你一个“能力边界地图”。如果你在做产品选型前者够了如果你在做工艺优化、像素设计迭代后者才是真正能指导改进方向的输入。具体到这次测试有两条结论可以直接复用CMOS传感器各区域的动态范围差异可以达到 3~4dB电源布局、列译码器噪声耦合、温度梯度是主要诱因。设计验证阶段应尽早做全阵列ROI扫描别等到整机阶段再被边缘画质问题打措手不及。当测试目标聚焦在“极限值”时16×16像素块粒度配合PTC拟合是一个可复现的标准化方案耗时约1.5小时结果相比单ROI法增量明显。7.2 后续可以扩展的方向这个方案还可以往两个方向延伸。第一把ROI扫描从“静态场景”扩展到“时序场景”也就是在高帧率连续读出下评估动态范围的变化。因为动态范围依赖读出噪声而读出噪声在高帧率下会随带宽和功耗变化而漂移这部分数据对卷帘快门相机和高速工业相机的设计非常关键。第二把空间分布模型和ISP调优联动起来。既然已经知道哪些区域的噪声底偏高、哪些区域满阱偏小完全可以把这张“DR分布图”注册到ISP的降噪模块中对不同空间位置采用自适应降噪强度——高DR区域少降噪保留细节低DR区域多降噪压制噪点。这个方法在安防和车载相机领域应该很有应用潜力。最后再分享一个小技巧做这种大规模ROI扫描之前先花半小时画一张“潜能预扫描图”用低精度快速测量给全阵列打个分再用高精度去复核排名靠前的窗口。这样既不让数据量失控又不会错过真正的极限区域。这次730亿扫描能控制在两小时内跑完这个两步走的策略功不可没。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进