
前阵子帮朋友调试一颗工业相机他抱怨raw直出的图噪点特别多换了好几个降噪算法都不理想。我问他有没有先做过噪声标定他愣了一下。其实这个问题在相机开发里太典型了raw数据不是“没调好”的图它是传感器光电转换后最原始的数字信号噪声特性在raw域里是可建模、可预测的。很多人降噪效果差根子在于拿一个固定方差去套所有亮度区域而实际噪声是随亮度变化的。raw数据噪声模型和标定解决的就是这件事——把不同亮度下的噪声行为量化成参数后续去噪、HDR融合、传感器评估都能直接受益。这篇文章适合ISP算法工程师、视觉算法开发、相机模组测试人员也适合做光度立体、三维重建的实验党。1. raw噪声模型标定的对象到底是什么1.1 为什么必须在raw域里建模噪声很多人第一次接触raw以为它就是一张“没处理完的图片”导出来之后还要自己调白平衡、提亮度。这个理解不算错但忽略了更关键的一点raw是传感器上每一个像素经过光电转换、模拟放大、模数转换之后得到的原始数字量直接对应那个像素接收到的光电子数。它没有经过去马赛克没有经过白平衡也没有经过锐化和降噪所以它和入射光强之间保持近似线性的关系。线性关系对噪声建模极其重要。一旦图像从raw域进入sRGB域中间经历了gamma、色调映射、色彩校正矩阵这些非线性变换噪声的幅度会被压扁、拉伸而且和周围像素的颜色互相耦合变得非常难描述。反过来在raw域里每个通道的亮度均值可以直接反映信号大小噪声的统计特性和信号强度之间的关系也相对固定这就是为什么所有严肃的噪声标定都会选择raw数据而不是成品图。在实际工程里raw域的噪声模型有几个用途给降噪算法提供逐像素的噪声方差估计给多帧融合算法提供权重分配依据给传感器选型和验证提供量化指标。做光度立体、结构光、三维重建的人也会用到这个模型因为这类算法经常要反演像素值和光照之间的关系如果噪声模型不准重建出来的法向和深度会有系统性偏差。1.2 泊松-高斯模型怎么变成一条直线raw域的噪声源大体可以拆成三类光子散粒噪声、读出噪声、暗电流。先看最核心的光子散粒噪声。光子到达像素的过程是随机的单位时间内到达的光子数量服从泊松分布泊松分布有一个很漂亮的特性期望等于方差。如果一个像素在曝光时间内平均产生了N个光电子那么这些光电子数的方差也是N标准差是根号N。也就是说信号越强噪声的绝对幅度越大但信噪比也越高因为信噪比等于根号N。这是很多初学者最容易忽略的一点raw噪声不是恒定的它随亮度在变。读出噪声则是信号链路上叠加的电子噪声包括像素复位噪声、源跟随器噪声、列放大器噪声、ADC量化噪声等等。这些噪声的综合效果很接近高斯分布而且和信号强度基本无关可以近似为一个常数方差。暗电流则是即使完全没有光照传感器内部热激发也会产生的电子它同样服从泊松分布而且和曝光时间、传感器温度强相关曝光时间越长、温度越高暗电流就越不可忽视。把这三者合并起来就能推出raw域最经典的模型。假设光电转换后的信号电子数是S暗电流电子数是D系统增益是K单位是DN/e-意思是每个电子最终变成多少个数字量黑电平偏移是O那么最终输出可以写成E[X] ≈ K * (S D) OVar[X] ≈ K² * (S D) σ_read²把第一个式子代入第二个就会得到Var[X] ≈ K * (E[X] - O) σ_read²如果先把黑电平O扣掉得到的均值就是K*(SD)于是模型就简化成Var[X] ≈ K * E[X] σ_read²这就是为什么我们常说raw噪声模型是一个方差对均值的线性函数var a * mean b。这里的a就是系统增益K单位是DN/e-b就是读出噪声方差σ_read²单位是DN²。标定的核心任务就是把这两个参数以及暗电流、黑电平、坏点信息从一个测量数据集合里估计出来。1.3 先分清噪声标定与几何标定、RAW格式问题搜索这个主题的时候很容易被一堆同名概念带跑。“张正友标定法”“手眼标定”“九点标定”“相机imu联合标定”这些都属于几何或者空间变换标定解决的是像素坐标、相机坐标、世界坐标之间的映射关系和噪声标定完全是两条线。噪声标定属于光度学的范畴关心的是“这个像素的数值有多可信”而不是“这个像素对应空间哪个点”。两者在算法流程上也不冲突通常一个相机既要标内参外参也要标噪声参数。还有一个容易混淆的点文件系统里的“u盘raw格式无法格式化”、磁盘工具中的“hdd raw copy tool”这些raw指的是底层存储设备的原始扇区格式和图像传感器raw数据没有任何关系。只有在相机和计算摄影语境下raw才代表传感器原始数据。搜索的时候看到这些词直接跳过就行不要被干扰。2. 标定流程设计拍摄条件与数据采集方案2.1 采集数据前先把实验条件定死噪声标定对拍摄条件极其敏感实验条件不固定后面拟合出来的参数就是废的。我的建议是先把下面几项全部定死再开始拍。第一相机工作模式。固定ISO、固定位深、固定raw输出格式关闭所有自动功能包括自动曝光、自动白平衡、自动增益、降噪、缺陷像素校正。手机相机尤其要注意很多手机raw实际是短帧多帧合成后再输出的噪声特性已经被算法改过了不能代表真实传感器所以标定尽量用工业相机或者相机厂商提供的原始raw至少确认关闭了多帧合成。第二环境光。整个过程要在暗室或者接近暗室的条件下进行避免杂散光射入镜头。拍摄暗帧的时候镜头盖必须盖严最好再蒙一层遮光布。有同行用过一种方法拿黑色电工胶带把镜头前端整个封住暗帧结果干净很多我后来也一直这么干。第三光源。均匀光源是核心积分球是首选没有积分球可以用透射式灯箱加硫酸纸或者大尺寸灰卡配合稳定光源。光源必须是直流供电避免频闪亮度要能连续调节这样我们才能从暗到亮覆盖整个动态范围。光源不稳定的话多帧方差会被“光源抖动”污染拟合出来的读出噪声会偏大。第四拍摄序列。建议先固定曝光时间只改变光源亮度因为这样暗电流对每组数据的影响是恒定的。每组亮度档位连续拍20到50帧用于计算时间方差。亮度档位从接近黑电平一直排到饱和值的80%到90%至少要10到15个档位。如果只有几个档位线性拟合的可靠性会差很多。2.2 黑电平、暗电流、坏点和ROI预处理四件套拿到raw图像之后不能直接算均值和方差必须做几步预处理否则参数估计会出系统误差。第一步是黑电平扣除。每个传感器都有黑电平这是为了让暗电流和读出噪声的负向摆幅不至于被ADC截断而人为加上的偏置。raw文件里通常有黑电平信息比如rawpy读取后的black_level_per_channel。不同颜色通道的黑电平可能不同所以要按通道分别扣除。如果不扣黑电平均值里混入了一个很大的常数直接套用var a * mean b模型时截距b会被污染斜率a也会跟着偏。第二步是暗电流评估。暗帧不是全黑的长曝光暗帧里能看到明显的亮斑和整体抬升。暗电流对噪声方差的贡献会随曝光时间线性增加所以要么固定曝光时间让暗电流成为常数要么采集多组不同曝光时间的暗帧单独拟合暗电流速率。如果标定目的是算法落地固定曝光时间就够了如果要做传感器性能评估暗电流这部分需要单独统计。第三步是坏点剔除。raw图像里总有一些响应异常的死点、亮点和闪烁点。在暗帧里表现为恒亮的像素在亮场里表现为恒暗的像素。计算均值和方差前最好用一个掩膜把这些像素去掉。最简单的办法是采集一组暗帧把亮度明显高于背景均值3到5倍的像素标为坏点再采集一组过曝光图像把亮度明显低于周围均值的像素也标为坏点。坏点数量通常很少但如果不剔除它们对方差的影响会被放大尤其是空间方差。第四步是ROI选择。镜头渐晕会让画面边缘亮度低于中心如果直接拿整幅raw图像的像素算方差亮度不均匀会导致方差被显著高估。所以一般选择画面中央相对均匀的区域比如512x512或者更小的纯色区块。如果一定要用整个画面先做平场校正把每个像素的增益差异归一化这也是一种方案但工程复杂度高不少。2.3 用多帧还是单帧方差口径先想清楚计算方差有两种常见口径空间方差和时间方差。空间方差是对单帧图像某个ROI内所有像素求方差时间方差是固定场景对同一像素在多帧之间求方差再把所有像素的方差取平均。这两者看起来都是“方差”但物理含义不一样。时间方差只包含随机噪声部分也就是散粒噪声和读出噪声中的随机成分它不受固定模式噪声FPN影响。空间方差则把像素响应不一致性、暗电流不均匀性这些空间固定图案也一并算进去了。反映到数值上空间方差通常会比时间方差大一些。如果你标定的目的是给降噪算法用我建议以时间方差为主因为降噪算法实时处理面对的主要是随机噪声如果目的是传感器工艺评估空间方差也有参考价值但需要注明口径。实际操作中时间方差要求每个亮度条件连拍多帧数据量大但计算很简单。如果连拍不方便至少也要拍两帧以上否则没法做时间方差。我一直建议用户别省这一步多拍几十帧对设备来说几乎零成本换来的是参数稳定性和可复现性。3. 核心实现均值-方差拟合与参数验证3.1 均值-方差拟合的完整流程标定流程走到这里手里应该已经有一批预处理后的raw数据了。接下来要做的事情就是两点对每组数据计算均值计算时间方差然后把散点拟合成直线。具体步骤可以拆成五步。第一步读取raw文件保留拜耳阵列原始数据不要插值成RGB。第二步按通道拆分RGGB四个通道逐个通道做黑电平扣除、坏点剔除。第三步对每个亮度档位先计算每帧ROI的均值再对多帧做逐像素时间方差最后把所有像素的时间方差取平均。第四步得到一个数据点mean和var把10到15个亮度档位的数据点画在坐标轴上横轴是mean纵轴是var。第五步用最小二乘拟合一条直线var a * mean b斜率a就是系统增益KDN/e-截距b就是读出噪声方差σ_read²。这一套方法的学名叫光子转移曲线Photon Transfer CurvePTC是欧洲机器视觉标准EMVA 1288里最核心的测量手段。PTC本身是log-log坐标下的均值-方差曲线在暗端读出噪声主导方差几乎水平在中高亮段散粒噪声主导方差随均值线性上升斜率是1。从这条曲线上不仅能拿到增益和读出噪声还能推算出满阱容量、信噪比、动态范围等一堆传感器核心指标。3.2 可以直接用的标定代码用Python做这套标定非常方便核心库是rawpy和numpy。下面这段代码是简化版但流程完整可以直接跑通一整套均值-方差拟合。import rawpy import numpy as np def load_bayer(path): # 读取raw并扣除黑电平返回拜耳数组和2x2 CFA pattern with rawpy.imread(path) as raw: bayer np.asarray(raw.raw_image_visible, dtypenp.float32) black_levels np.asarray(raw.black_level_per_channel, dtypenp.float32) pattern np.asarray(raw.raw_pattern[:2, :2]) h, w bayer.shape for i in range(2): for j in range(2): channel pattern[i, j] bayer[i::2, j::2] - black_levels[channel] return bayer def temporal_variance(frames_roi): # frames_roi: list of 2D arrays, shape (n, roi_h, roi_w) stack np.stack(frames_roi, axis0) per_pixel_var np.var(stack, axis0) return np.mean(per_pixel_var) def fit_noise_model(means, variances): # 拟合 var a * mean b a, b np.polyfit(means, variances, 1) return a, b # 示例假设已经按亮度档位组织好了数据 # brightness_levels: list of list, 每个元素是同一个亮度条件下的多帧bayer数组 means [] variances [] for frame_group in brightness_levels: rois [f[roi_slice] for f in frame_group] means.append(np.mean(rois)) variances.append(temporal_variance(rois)) gain, read_noise_var fit_noise_model(means, variances) print(f系统增益: {gain:.3f} DN/e-) print(f读出噪声标准差: {np.sqrt(read_noise_var):.3f} DN)用这段代码的时候有几个注意点。rawpy里的black_level_per_channel返回的是一个四元素数组但顺序和raw_pattern里的通道编号是对应的所以要像代码里那样按pattern逐通道扣。ROI切片建议取图像中央区域比如roi_slice (slice(h//2-256, h//2256), slice(w//2-256, w//2256))。另外代码里用np.mean(rois)代表整个ROI的亮度均值如果两个颜色通道的响应差异很大建议四个通道分别计算不要混在一起。如果不想写代码也可以用RawDigger或dcraw把raw导出成16位TIF再用ImageJ或者Excel做逐帧ROI统计。流程一样只是速度慢适合验证阶段用。3.3 从拟合直线到光子转移曲线拟合出斜率a和截距b之后不要急着收工先画一张PTC图验证模型是否合理。横轴是log10(mean)纵轴是log10(var)把刚才每一组数据点画上去。理想情况下暗端的方差趋近一个常数对应读出噪声floor中高亮段方差随均值线性上升曲线斜率接近1。如果高亮段曲线向下弯说明图像已经饱和需要剔除对应数据点如果曲线在暗端就明显上翘说明背景有杂散光或者暗电流过大需要查采集环境。从PTC上还能读出几个重要指标。饱和点对应的均值就是饱和信号值用饱和信号值减去黑电平再除以增益得到的就是满阱容量单位是电子数。动态范围是饱和信号值除以读出噪声标准差通常换算成dB。信噪比可以直接用公式SNR ≈ sqrt(mean * a)估算因为散粒噪声主导时信噪比等于信号电子数的平方根。这些指标对传感器选型和系统架构设计很有用比厂商datasheet上的数字更可信因为这是你自己在同一套测试条件下测出来的。验证阶段还有一步独立数据验证。拿一组没有参与拟合的raw数据进来用拟合出的a和b预测每个均值对应的方差然后和实测时间方差对比看残差是否在合理范围内。如果验证集上预测偏差超过20%说明模型可能没覆盖好暗端或亮端需要补拍数据。4. 常见坑位与标定结果落地用法4.1 高频问题排查方差曲线不正常怎么办我在实际测试中遇到过不少次拟合结果不对劲的情况最常见的是下面几类。第一高亮段方差曲线掉头向下。这个基本可以断定是图像饱和了传感器在接近满阱时会进入非线性区电子-电压转换关系不再保持线性散粒噪声的方差贡献被压缩。解决办法是把最高亮度档位降到饱和值的80%到90%以下重新采集。第二截距b异常大也就是暗端方差下不去。先检查黑电平是否扣干净再检查暗室是否真的暗还要看曝光时间是不是太长导致暗电流显著。有时候是光源微亮肉眼看不出但传感器在长曝光下能感知到所以暗帧必须用相同曝光时间拍摄。第三时间方差和空间方差差距特别大。这是固定模式噪声太明显常见于CMOS sensor的行噪声、列噪声。如果发现空间方差远大于时间方差并且图像上有明显的横条纹或者竖条纹需要先做列/行校正或者在ROI统计时避开条纹明显的区域。第四拟合出来的斜率a是负的。这个情况说明数据质量极差基本可以排除光源不稳定、温度漂移、图像处理被打开这些原因从上到下重新检查采集环境。我在调试时遇到过是自动曝光没有完全关闭导致每帧亮度其实在变化多帧方差被污染算出来的均值又不对。第五不同RGGB通道参数相差太大。这是正常的拜耳阵列四个通道的量子效率和微透镜响应不完全相同绿通道通常增益高一些、噪声低一些红蓝通道稍微差一点。只要不是数量级的差异一般不用太担心。4.2 标定参数在工程里的几种用法拿到增益和读出噪声之后噪声模型可以真正跑进算法里。最常见的用途是信号依赖去噪。经典降噪算法比如BM3D、非局部均值通常需要用户提供一个噪声标准差。很多人直接给一个固定值导致亮部偏模糊、暗部还有噪点。有了raw噪声模型之后对每个像素只要知道它的均值就能算出对应的噪声标准差sigma(x) sqrt(a * mean(x) b)。把这张逐像素sigma图喂给降噪算法亮部不会误伤细节暗部也不会漏噪。第二个用途是多帧融合和HDR的权重设计。用多帧raw做超分、降噪或者HDR合成时不同亮度区域的可信度不一样。亮的区域信噪比高权重可以给大一点暗的区域信噪比低权重要压低。这个权重可以直接由噪声模型导出的信噪比决定而不是凭经验设定。第三个用途是传感器性能评估。利用PTC算出满阱容量、动态范围、读出噪声可以用一组可复现的数据横向对比不同相机模组。在项目选型阶段这个结果比单纯看官方样张客观得多。第四个用途是合成仿真数据。在算法开发阶段很难拿到各种极端场景的raw图但如果有了准确的噪声模型就可以给干净的合成图像添加泊松-高斯混合噪声生成接近真实传感器输出的训练数据。这对训练去噪网络、超分网络尤其有用比直接加高斯噪声逼真得多。4.3 快速避坑清单与个人建议常见问题大概率原因排查建议高亮段方差变平或下降图像饱和、抗溢出电路启动降低最高亮度控制在80%到90%饱和值内暗端方差过高暗电流、杂散光、黑电平未扣检查暗室扣黑电平缩短曝光时间空间方差远大于时间方差固定模式噪声显著确认用途去噪优先用时间方差斜率a为负光源波动、自动曝光未关、温度漂移固定曝光关闭自动模式等待热稳定拟合结果在验证集偏差大亮度档位覆盖不足补拍暗端和亮端数据至少10到15档最后分享一个个人习惯正式标定前我会先用一组最基础的数据快速验证链路是否通畅。具体做法是固定ISO固定曝光时间只调节光源亮度拍5个点当场拟合一条直线看一眼斜率和截距是否在合理范围。如果这条快速曲线都是乱的就说明要么采集条件有问题要么代码预处理有问题不值得继续往后拍大量数据。这个“先跑通再跑全”的流程帮我省过很多次返工。如果你现在正要开始做raw噪声标定我给的建议是别追求一步到位先固定好测试条件采集一组暗帧和几组亮帧把均值-方差散点图画出来亲眼看到一条直线后再谈后续。这个基础打牢了传感器评估和算法落地都会顺畅很多。