做计算成像这些年,被问到最多的问题大概有两类:一类是“为什么相机标定结果每次都不一样”,一类是“为什么同一个相机、同一个场景,换一组拍摄参数,重建出来的效果就千差万别”。早期我通常把这两个问题分开回答,后来才发现它们根子上是同一件事——我们没有把相机当成一个“测量系统”来理解。今天这篇第 6 讲,就专门把几何标定和噪声模型放在同一张桌面上去讲,核心关键词很明确:泊松噪声、光子转移曲线(PTC)、张正友标定。
这篇文章适合谁看?如果你刚进入计算成像领域,想搞懂传感器原始数据里那些“颗粒感”到底从哪来;如果你正在做多帧合成、三维重建、HDR 或超分辨率,总觉得结果不够稳;又或者你已经用棋盘格标定过相机,但重投影误差就是压不下去——那这篇文章大概率能给你一个系统性思路。我不准备堆公式,而是想把“噪声为什么存在”“噪声怎么测量”“几何参数怎么标定”以及“三者如何联动”讲清楚,争取让不同基础的读者都能照着落地。
1. 噪声模型和几何标定为什么必须放在一起讲
1.1 计算成像不是在“等光拍图”,而是在“测量光子分布”
传统摄影流程里,相机是个黑盒子:光进去,图像出来,至于传感器怎么响应、读出噪声多大、像素之间有多不一致,默认都会被忽略。但计算成像的起点恰恰相反——所有环节都被当成可测量、可建模的量。
拿多帧超分辨率举例,如果默认帧间噪声是独立同分布的高斯噪声,那置信权重就设计错了;拿去马赛克举例,如果默认 RGB 三通道噪声独立同分布,边缘处很容易出现彩色伪影;再比如相位恢复、光场重建、无透镜成像,本质上都是在干同一件事:从含噪声的测量值里反推场景信息。一旦进入这种“反问题”的框架,第一步就必须建立测量模型。
完整的相机测量模型通常由三部分组成:几何映射负责“世界坐标到像素坐标”,辐射响应负责“场景亮度到像素值”,随机噪声负责“像素值为什么会有涨落”。几何映射靠张正友标定完成,随机噪声则靠泊松噪声模型和光子转移曲线建立。三者缺一个,后续所有算法都是在跟一个错误模型较劲,结果自然不稳定。
1.2 一个低光照产线的真实教训
我印象里有个工业检测项目,同款相机用在两条产线上,一条光照充足,一条光照偏暗。暗产线那边反馈:同样的标定板,重投影误差从 0.1 像素涨到 0.8 像素;同样的三维重建算法,时不时出空洞。排查到最后,问题根本不在标定板或者算法,而是暗光下图像信噪比变差,棋盘格角点检测产生了系统性偏移,进而污染了内参估计。如果把噪声模型纳入考量,结论其实很直接:标定用图像本身也需要达到一定信噪比门槛。至于这个门槛怎么定,可以由光子转移曲线算出来。
类似这种例子在计算成像项目里特别常见。很多人对算法调参折腾一个月,最后发现根因是噪声模型没建对。所以我坚持认为,噪声建模和几何标定不是两个独立模块,而是同一个测量模型的两个侧面。
1.3 噪声建模能带来三个直接收益
第一是估计更准。噪声方差已知后,可以用最大似然估计替换朴素最小二乘。弱光下像素值接近泊松分布,强光下近似高斯,不同信号强度需要不同的最优权重。第二是参数选择更理性。知道增益、满阱容量、读出噪声后,就能判断一个场景应该优先加曝光时间还是加模拟增益。第三是系统诊断更简单。当重建质量下降时,通过 PTC 能快速判断是传感器异常、照明变了还是算法失效,不用靠猜。
2. 泊松噪声:不是“噪声”的噪声
2.1 光子到达天生就是泊松过程
先说一个反直觉的事实:一束均匀光照射传感器,每个像素接收到的光子数目并不固定。单位时间平均到 λ 个,但如果盯着单个像素看,每一时刻到达几个完全随机,而且每次到达互不影响。这种过程在概率论里就是标准泊松过程。曝光时间 T 内收到 k 个光子的概率可以写成:
P(k) = (λT)^k × e^(-λT) / k!
这个分布最核心的性质就一句话:均值等于方差。如果平均收到 100 个光子,你实际收到 90 个或者 110 个都完全正常;如果平均收到 10000 个光子,标准差是 100,相对波动只有 1%。这就是为什么“噪点”在暗部总比亮部刺眼——不是图像处理算法的错,而是光子的统计涨落本身在暗处更明显。
想理解这件事,可以把它类比成接雨:浴缸里的积水高度很稳定,但如果用一排小杯子放在院子里接雨,每杯的雨量总有高有低;杯子越小,杯与杯之间的相对差异越大。像素就像那只小杯子,光子就像雨滴。
新手往往会问:既然曝光越长光子数越多,那把曝光拉到很长不就行了吗?道理没错,但传感器像元的满阱容量有限——能存下的电子总量有上限,超过之后信号饱和,光子涨落的“积分平均”就被切断了。这也是后面 PTC 要专门看饱和点的原因。
2.2 电子和 DN 的换算:别在单位上栽跟头
传感器先把光子转成电子,再把电子通过增益变成数字量 DN。增益 K 的单位通常是 e-/DN,表示一个 DN 对应多少个电子。像素值 DN = N / K,N 是电子数。在电子域里,泊松噪声的方差等于均值;但到了 DN 域,方差要除以 K²,均值要除以 K,所以 DN 域的方差和均值不再是相等关系,而是线性关系。这个线性关系正是 PTC 的基础。
所以看图像噪声时,一定要先分清楚是在哪个域里说话。两个像素,一个 DN=100,一个 DN=10000,前者的泊松方差在 DN 域里明显更小,但在相对意义上更大。很多图像处理新手把 DN 域方差当成常数来处理,这就是低光照算法翻车的常见根因之一。
2.3 完整噪声源清单:不只是泊松噪声
完整相机噪声模型至少还包含三类:
- 暗电流噪声:即使完全遮光,传感器内部热激发也会产生电子,并且随时间累积。暗电流在统计上也近似泊松分布,只是“源”换成了热激发。长曝光时暗电流形成的固定图案尤其明显。
- 读出噪声:电荷被读出、放大、模数转换时引入的噪声,和信号强度基本无关,可近似看成高斯分布。它决定了传感器的本底噪声下限。
- 固定模式噪声(FPN):每个像素的响应率、暗电流不完全一致,于是同样光照下不同像素输出不同。暗电流部分用暗帧扣除,响应率差异用平场校正处理。
把这几项汇总,图像域总方差可以写成:
σ_total² = σ_read² / K² + DN_mean / K + 暗电流贡献
这正是光子转移曲线要拟合的主体关系。PTC 之所以有用,就是因为它把以上噪声源全压在一张图上,一次测量就能拆出系统增益、读出噪声和满阱容量。
3. 光子转移曲线:一台相机的“体检报告”
3.1 PTC 是什么,怎么测
光子转移曲线全称 Photon Transfer Curve,横轴是像素灰度均值,纵轴是灰度方差。它是传感器领域的经典标定手段,设备门槛很低,一块均匀光源和一组连续拍摄就能完成,特别适合实验室和创业团队自己动手。
实测步骤我建议这样走:
- 准备均匀光源。用漫射板加匀光片,把画面中央区域的光照调均匀。
- 固定增益、光圈、白平衡,只改变曝光时间。曝光序列从很暗到接近饱和,取 10 到 15 个点。
- 每个曝光点连续拍摄 20 帧以上。对每个像素做帧间均值和方差,再对中央平坦区域做空间平均,得到一组(均值,方差)。
- 完全遮光条件下拍一组暗帧序列,用“亮场减暗场”的方式去除暗电流和固定图案。不扣暗帧的 PTC,截距会被明显抬高。
为什么取中央区域?因为边缘有渐晕和镜头畸变,亮度不均会破坏“方差随均值变化”的直线关系。还有一个容易忽略的坑:光源频闪。市电频率的频闪或 LED 驱动波纹会让方差偏大,所以在质量不高的 LED 灯下测 PTC,结果基本不能用。建议优先用钨灯加漫射板,或者用积分球。
3.2 从 PTC 上可以读出哪些参数
在电子域,假设光电子数 N 服从泊松分布,DN = N/K,那么在 DN 域会有:
σ_DN² = (1/K) × DN_mean + σ_read² / K²
也就是说,PTC 在饱和前是一条直线,斜率是 1/K,截距是 σ_read² / K²。
- 系统增益 K:用斜率倒数得到,单位 e-/DN。
- 读出噪声 σ_read:用截距和 K 算出来,单位 e-。
- 满阱容量:PTC 开始脱离直线、进入饱和平台时对应的横坐标。
- 动态范围:满阱容量除以读出噪声,再取对数,常见单位是 dB。
参数和来源先列个表:
| 参数 | 获取方式 | 单位 | 典型意义 |
|---|---|---|---|
| 系统增益 K | PTC 线性段斜率倒数 | e-/DN | 一个 DN 对应多少电子 |
| 读出噪声 | 截距推算 | e- | 传感器暗场本底噪声 |
| 满阱容量 | 饱和点横坐标 | e- | 像素最多容纳的电子数 |
| 暗电流 | 暗帧均值随时间变化 | e-/s | 长曝光性能 |
| 动态范围 | 满阱容量 / 读出噪声 | dB | 一次曝光能覆盖的光比 |
举个简化例子:某相机在中心区域拍出的数据拟合结果是 y = 0.5x + 4。则 K = 2 e-/DN,读出噪声 = 2 × sqrt(4) = 4 e-。如果满阱容量是 30000 e-,动态范围就是 30000/4 ≈ 7500 倍,约 77 dB,按位数算是约 12.9 bit。这套换算手算一遍之后,再看传感器手册会轻松很多。
3.3 PTC 和信噪比曲线是孪生兄弟
PTC 标定的参数不仅能描述噪声,还能预测任意曝光下的信噪比。像素信噪比在电子域中大约为:
SNR = N / sqrt(N + σ_read² + 暗电流项)
用 PTC 得到的增益 K 和 σ_read,把 N 换算成像素均值 DN × K,就能画出一条“信噪比-曝光量”曲线。这能直接回答一个实际问题:“我想输出 40dB 信噪比,最低需要多少曝光?”在多帧合成、HDR、超分辨率的任务设计里,这套预测非常实用,甚至可以写进采集控制程序,让它动态决定每一帧曝光和增益,替代凭感觉“亮一点再亮一点”的土办法。
3.4 PTC 实测中几个高频失误
第一次做 PTC,最容易翻车的是三件事。一是光照不均匀,方差曲线压根拟合不出来,解决方法是只取中央 1/4 区域并做平场校正。二是拍得太少,有的人每个曝光点只拍 3 帧,方差估计本身就带巨大不确定度,拟合斜率自然飘;经验值是至少 20 帧。三是混淆曝光时间和增益。改增益会同步改变斜率,制造 PTC 平移的假象,所以必须约定“固定增益扫描曝光”或“固定曝光扫描增益”,不能混着算。我自己的实测偏好是尽量用曝光时间扫描,因为很多相机的模拟增益并不严格线性,扫增益容易引入额外误差。
4. 张正友标定:棋盘格背后的几何求解
4.1 先把成像模型写明白
张正友标定基于针孔模型。三维世界点 P = [X, Y, Z, 1]^T 投影到像素点 p = [u, v, 1]^T 的关系是:
s × p = K × [R | t] × P
s 是深度因子,R 和 t 是外参,K 是内参矩阵:
K = [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]
fx、fy 是归一化焦距,cx、cy 是主点坐标。实际镜头还要加畸变模型,常见的是径向畸变 k1、k2、k3 和切向畸变 p1、p2。需要提醒的是,这个畸变模型作用于归一化坐标,而不是直接在像素坐标上做加法。很多初学者先纠结公式再跑程序,我的建议是反着来——先跑通标定流程,再回头对着模型理解每一步在求什么。张正友方法本身就是为这种学习路径设计的,它把整个问题拆解得非常干净。
4.2 张正友核心思路:用平面板替代立体靶标
在张正友的工作之前,传统标定通常需要精密加工的立体靶标。张正友的突破在于:只用一块平面棋盘格,改变姿态拍多张照片,就能同时恢复内参、外参和畸变系数。设备要求低到极点,打印一张棋盘格就能用,因此很快成为相机标定的工业标准。
背后的数学逻辑概括成四步:
- 棋盘格角点都在同一平面,设 Z=0,单应矩阵 H 就把平面点映射到像素点:s×p = H×P',其中 H = K×[r1, r2, t]。
- 单张图只能给出一个单应,但多张不同姿态的图能通过旋转向量单位正交性建立约束,解出内参 K。
- 内参确定后,每张图的外参 R、t 可以从单应里直接恢复。
- 用所有图像的所有角点做非线性优化,最小化重投影误差,同时估计畸变系数。
一句话总结:先用多视角几何约束给出初始解,再用全局优化精修。这种“先线性初始化、后非线性精修”的思路,在视觉计算里几乎无处不在。
4.3 实操流程:从打印棋盘格到拿到内参
一份比较稳的流程大概是这样的:
- 准备标定板。棋盘格用硬底板贴平,不能有褶皱。普通 A4 打印纸加纸板也能入门,但要小心受潮变形。格子边长用尺子量准,比如 20mm 或 30mm,填进程序时出错会让内参整体偏尺度。
- 固定相机,移动标定板。目标拍 15 到 30 张,棋盘格在画面中的大小、角度、位置都要有足够变化。倾斜角度在 20 度到 45 度之间比较合理,别只拍正对着的图。
- 角点检测。用
cv2.findChessboardCorners找到角点,再用cv2.cornerSubPix做亚像素精化。每张图检测完要检查,别直接一股脑塞给标定函数。 - 调用标定函数。核心是
cv2.calibrateCamera,传入世界坐标点、图像角点、图像尺寸,返回内参矩阵、畸变系数、旋转向量、平移向量和重投影误差。 - 评估结果。RMS 重投影误差一般低于 0.2 像素算不错,低于 0.1 像素很好。但 RMS 低不代表一定正确,还要看内参是否合理、角点分布是否覆盖整个画面。
4.4 张正友标定常见的三个坑
第一个坑是标定板不平。打印纸贴在弯曲纸板上,角点位置直接带毫米级误差,标定的主点和焦距都会有系统性偏差。用玻璃板或铝板贴棋盘格才稳,至少也要把纸板放在桌面上压平再拍。
第二个坑是姿态分布单一。有人把标定板固定在同一个平面,换换左右位置就开拍,结果单应之间高度相关,内参解不出来,压出来的 RMS 看着不高,内参却不符合物理常识。正确做法是让棋盘格像“一个移动的镜框”一样充满整个视场:左右、上下、前后、倾斜全都要覆盖。
第三个坑是角点检测被噪声和模糊干扰。图像过暗、过曝或失焦时,角点亚像素位置会产生偏差。对计算成像项目,我强烈建议先按后面的方法设定标定图最低曝光,保证棋盘格亮部信噪比足够高,再开始标定。
5. 噪声模型和几何标定其实是一套系统
5.1 低信噪比下张正友标定会退化
前面产线的例子不是孤例。棋盘格黑白交界处的梯度方向一旦被噪声扰动,亚像素精化就会把角点往错误方向推。随机误差还能靠多图平均抵消一部分,但暗电流、渐晕这类系统误差会直接变成内参偏差。因此在计算成像项目里,我把“标定图像信噪比”当成显式参数去控制。
具体做法是三步走:先用 PTC 测出传感器增益和读出噪声,再按目标信噪比(比如 35dB)算出标定场景需要的最低灰度均值,最后用这个值指导曝光设定。看起来繁琐,固定成流程后,反而能省掉大量排查时间。
顺便推荐一个小技巧:如果现场光源不够亮,可以在同一姿态下连拍 5 帧取平均,再把平均图送进角点检测。这样能把泊松噪声压低到原来的约 45%,角点位置的抖动会明显减少。
5.2 把噪声方差当权重,做更精细的标定
标准张正友标定对所有角点一视同仁,但更精细的做法是给每条角点加一个权重,权重来自它的定位不确定度。不同位置、不同亮度下,角点检测的不确定度并不一样。你可以在角点检测时同时估计局部方差,然后自己写带权重的优化,至少也可以用方差做异常点剔除。
这个技巧我实测过,对弱光场景和镜头边缘区域尤其有效,重投影误差能再压下去一部分,有时是 20%,有时是 30%。当然,对普通产品项目不是必需,但它体现了噪声模型和几何标定协同的正确姿势——它们不是独立组件,而是同一误差链上的两个环节。
5.3 标定参数不是一劳永逸,要定期体检
工业相机和普通相机模组还有个区别:标定参数会漂移。镜头松动、温度变化、剧烈振动都会让内参和畸变系数慢慢偏离。塑料镜头筒的膨胀会让 k1、k2 明显变化,这种畸变漂移往往很难直接察觉。所以在高精度项目里,建议把“标定基线 + 定期复核”制度化:每次大保养重跑一次快速标定,或者在工作位旁保留一组已知坐标的参考点,用单张图判断内参是否漂移。
6. 走一遍全流程:模拟数据下的噪声标定与几何标定
6.1 先设置一台“可解释”的虚拟相机
为了让理论落地,可以在 Python 里构造一台虚拟相机,故意设置一组真实感参数,再从参数出发生成含噪图像,完整跑一遍 PTC 和标定流程。这样做最大的好处是:你提前知道真值,能清楚判断每一步算法的误差来源。
比如设置:增益 K = 2.0 e-/DN,读出噪声 5 e-,满阱容量 30000 e-,暗电流 1 e-/s。生成虚拟平场图像时,对每个像素先给 5000 个“入射光电子”,做泊松采样得到实际电子数,除以 K 后变为 DN,最后加高斯读出噪声。注意顺序别搞反:光子到达、光电转换、读出是先后有物理含义的三个步骤,不能把泊松采样和加性高斯采样混成一步。
6.2 用 Python 拟合并提取 PTC 参数
下面是一段非常简化的 PTC 拟合代码,实验室可以直接拿去改:
import numpy as np # frames_stack: 列表,shape = (n_frames, H, W),已完成暗帧扣除 mus = [] vars_ = [] for stack in frames_stack: # 只取中心平坦区域,避开渐晕 center = stack[:, H//4:3*H//4, W//4:3*W//4] # 先做帧间均值/方差,再对像素做空间平均 mean_img = center.mean(axis=0) var_img = center.var(axis=0) mus.append(mean_img.mean()) vars_.append(var_img.mean()) mus = np.array(mus) vars_ = np.array(vars_) # 用饱和前的数据点拟合直线 mask = mus < 10000 coeff = np.polyfit(mus[mask], vars_[mask], 1) slope, intercept = coeff gain = 1.0 / slope # e-/DN read_noise = gain * np.sqrt(intercept) # e- print(f"gain = {gain:.3f} e-/DN") print(f"read noise = {read_noise:.2f} e-")这段代码里最容易忽视的点是“统计顺序”。正确做法是:先在时间方向计算每个像素的方差,再对空间像素平均;反过来做会把空间不均匀性抹平,方差被严重低估。拟合前加的饱和掩膜也很关键,否则进入饱和平台期的点会把直线斜率拖弯。
6.3 用 OpenCV 跑张正友标定
几何标定的 OpenCV 实现很紧凑,核心流程如下:
import cv2 import numpy as np pattern_size = (9, 6) # 内角点数 square_size = 0.030 # 格子边长,单位米 objp = np.zeros((pattern_size[0]*pattern_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp *= square_size obj_points = [] img_points = [] for fname in image_files: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if ret: criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) corners = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners) ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print("内参矩阵:\n", mtx) print("畸变系数:", dist.ravel())square_size的单位在这里非常关键。张正友标定本身对三维坐标没有绝对尺度概念,如果你把 30 写成 0.03,内参里的 fx、fy 会成比例缩放大不同量级。很多新手第一次标出的大得离谱的焦距,多半是把格子边长单位写错了。另外注意,findChessboardCorners返回的是内角点,比如 9×6 的棋盘格,实际是 9 列 6 行共 54 个内角点,和棋盘格的小方格总数不是一回事。
6.4 模拟全流程能得到什么
用虚拟数据跑完这套流程,会看到两个典型现象。第一,PTC 求出的增益和预设真值通常偏差在 5% 以内,偏差主要来自有限帧数和空间不均匀性。第二,如果给标定图像注入高强度泊松噪声,内参矩阵估计会出现明显波动,重投影误差相应上升。这正好验证了前面说的“低信噪比下标定退化”。
更有意思的是,中等噪声下重投影误差可能依然很低,但内参却已经偏离真值。这说明只看 RMS 数值不够,还要看误差在不同视场区域的分布。如果中间误差小、四周误差大,多半是畸变模型或角点分布问题;如果各方向误差均匀偏大,更可能是标定板不平或噪声过大。学会从误差分布里读信息,是标定经验进阶的重要标志。
7. 常见问题与避坑速查
7.1 问题排查速查表
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| PTC 拟合不出直线 | 光照不均、光源闪烁、未扣暗帧 | 换匀光光源,逐档多拍,先做暗帧扣除 |
| PTC 斜率异常偏大 | 增益设置改变,或图像经过压缩与去马赛克 | 强制使用原始数据或无损格式,固定增益 |
| 标定重投影误差偏高 | 棋盘格不平、角点检测差、图像太暗 | 换硬底板,亚像素精化,提高标定图信噪比 |
| 主点偏移严重 | 姿态覆盖不足,标定板只在单一位置 | 拍 15 到 30 张,覆盖画面四角和倾斜姿态 |
| 标定参数随时间漂移 | 镜头松动、温度变化 | 定期复核,用参考点快速校验 |
7.2 几条能直接省时间的实战经验
第一,图像格式优先选 PNG、BMP 或相机原始数据,不要用 JPEG。JPEG 有损压缩会打乱噪声的高频统计,PTC 的方差估计会完全失真,标定角点的亚像素位置也会被轻微扰动。
第二,优先加曝光而不是加增益。多数传感器上,模拟增益会把读出噪声一起放大,而曝光时间不放大读出噪声。如果场景有运动不能长曝光,再考虑增益,但要明确知道它的代价。这条逻辑在处理弱光图像时尤为重要。
第三,把标定流程脚本化。建议把 PTC 测试、暗帧采集、棋盘格拍照写成固定脚本,每次更换镜头、传感器或长期运行后都重跑一遍。只要这个过程标准化,计算成像系统的工程可靠性会明显提升。
7.3 一个容易被忽略的小技巧
当你觉得棋盘格角点检测不稳时,可以试试把亚像素窗口从 (5,5) 改到 (11,11),或者先对图像做一次轻微高斯模糊再做检测。低光照下这样做能显著提升角点定位稳定性。不要担心模糊损伤精度——亚像素精化的基础本来就是局部邻域同质性,适度平滑恰好能压制泊松噪声带来的随机跳动,但模糊半径不能太大,否则角点位置会被“推平”。
8. 一点个人体会
我自己把这一讲的内容拆成两部分学,再合起来用,体会特别深。早期我只做几何标定,后来被一个低光照三维重建项目逼着去读传感器手册和 PTC,才意识到之前很多“玄学调参”其实都是噪声模型没建对。从那以后,我的习惯变成:拿到新相机,先花半小时测 PTC,再花一小时拍标定板,最后把结果存成一份“相机体检档案”。后续所有算法开发都在这个档案基础上做;一旦遇到重建效果差、内参漂移这类问题,我不再盲目调参,而是先查档案、看 PTC 和标定记录有没有异常。这个习惯帮我省掉的排查时间,远比建档案本身花掉的时间多。
如果这一讲对你有帮助,建议不要只停留在“看过”的层面。找一台相机,按上面的流程,把泊松噪声、光子转移曲线和张正友标定老老实实跑一遍,数据会比公式给你更直观的体感。等你亲手把那条直线拟合出来,再把棋盘格角点重投影误差压到 0.1 像素以下的时候,你才算真正开始“读懂”手里的相机。