从像素反推三维:成像模型、标定与视觉流水线工程实践

发布时间:2026/11/3 23:00:11
从像素反推三维:成像模型、标定与视觉流水线工程实践

简介:《计算机视觉算法与应用》是一份面向计算机视觉学习者与研究者的PDF电子书,系统梳理了从图像形成到高层识别的完整知识链条,适合计算机、电子工程方向的高年级本科生、研究生,以及需要补强算法基础的工程实践者。全书按十余个模块展开:几何基元与变换、光度图像形成、点运算与线性滤波、傅里叶变换与金字塔小波、特征检测与匹配、基于特征的对齐与姿态估计、运动结构与光流估计、图像拼接、计算摄影中的高动态范围与超分辨率及抠图合成、立体对应与多视角立体、三维重建与表面表示、基于图像的渲染,以及目标检测、人脸识别、场景理解等识别任务,并附有常用数据集与测试集说明。整包仅1个PDF文件,约38.5MB,章节编排清晰,便于按主题检索或与课程大纲对照。目前已有1368人学习,可作为教材、科研参考,也可在项目动手前当作算法速查手册。

1. 像素坐标反推三维世界:视觉系统真正的坎不在算法,在成像模型

计算机视觉里有个反直觉的现象:同一套特征匹配代码,换一支镜头、换一档曝光,匹配率能从九成掉到三成,调阈值、换描述子都救不回来。问题不在匹配这一步,而在它上游——畸变没校正、位深被截断、采样已经混叠,送进算法的输入早就被污染了。《计算机视觉:算法与应用》这本教材想讲清的第一件事就是这个:视觉是一条从光子到语义的链路,每一环都有可逆的物理模型,算法只是链路上的反演工具。它把方法论拆成三条腿:建模,把成像过程写成方程;统计,把不确定性写成概率;工程,把能跑的技术压到可实现,并且要求三条腿最后都落回可验证的实验。搞懂链路之间的耦合关系,比多背几个算子有用得多。

2. 图像形成:为什么一个 3×4 矩阵就能把三维点压到像素上

全书从几何基元讲起,而不是直接从滤波和特征开始,这个顺序不是排版习惯。后面所有的逆问题——去模糊、超分辨、立体匹配、光流、三维重建——都默认你手里有一个正向模型,知道一个三维点是怎么变成某个像素上的某个灰度值的。正向模型错了,反演得越精细,错得越离谱。

2.1 针孔模型与齐次坐标:投影为什么能写成矩阵乘法

小孔成像的物理过程很简单:光线穿过一个点,落在传感器平面上。写成公式就是先把世界坐标搬到相机坐标系 $X_c = R X_w + t$,再除以深度得到归一化坐标 $x = X_c/Z_c$、$y = Y_c/Z_c$,最后乘焦距加主点变成像素。麻烦就麻烦在那个除法上——除以 $Z$ 是非线性的,一旦非线性,就没法连乘、没法求逆、没法直接塞进最小二乘。

齐次坐标干的事情,是把这一步除法推迟到最后。把三维点写成四维 $(X, Y, Z, 1)$,投影就变成一次纯粹的矩阵乘法 $x \sim P X$,其中 $P = K[R,|,t]$ 是 3×4 矩阵。这里的 $\sim$ 表示"相差一个非零尺度因子",也就是那步除法被藏进了符号里。这么做的好处很实在:多个变换可以直接相乘,一个矩阵就能表示"世界→相机→像素"的整条链路;投影矩阵的零空间恰好就是相机中心,三角化和对极几何全都建立在这个性质上。

内参矩阵 $K$ 是上三角形式:

参数含义典型处理
$f_x, f_y$以像素为单位的焦距现代方形像素下二者接近相等,但仍分开估计
$s$像素坐标轴的倾斜项传感器工艺保证正交,基本取 0
$c_x, c_y$主点,光轴与像平面的交点不等于图像中心,装配误差可达几十像素

$K$ 有 5 个自由度,$R$ 有 3 个,$t$ 有 3 个,合起来 11 个自由度,而一个 3×4 矩阵有 12 个元素,差的正好是那个尺度因子。这也解释了一个常见困惑:为什么标定出来的 $P$ 矩阵总是"差一个常数",因为它本来就只在射影意义下确定。

外参里的旋转怎么参数化,直接决定了后面优化的手感。欧拉角直观但有万向锁,四元数没有奇异但要额外维护单位约束,轴角在接近零旋转时数值不稳。做光束法平差时更常见的做法是在切空间上加一个小扰动再更新,把"旋转是流形上的点"这件事交给更新步骤处理,优化变量就退化成一个普通的三维向量。书里在 3D 旋转那一节铺开讲这些表示,不是数学炫技,是因为选错参数化,优化器会在某些姿态附近原地打转。

2.2 内参、外参与镜头畸变:标定到底在解什么

标定的输入是一组已知的三维点和它们在图像上的像素坐标,输出是让重投影误差最小的那一组参数。听上去就是个最小二乘,但直接上非线性优化一定会翻车,因为焦距和主点是强耦合的——焦距拉长、主点平移,能在画面中央一小块区域内产生几乎相同的投影。所以标准流程分两步:先用线性方法(直接线性变换,或者平面标定板的单应分解)求一个闭式初值,再用 Levenberg-Marquardt 迭代精化。初值的意义不是"差不多就行",而是把优化器送进正确的收敛域。

理想针孔之外还得补畸变。径向畸变来自镜头曲率,表现为直线拍成弯的,通常取 $k_1, k_2$ 两项就够,广角鱼眼才需要 $k_3$;切向畸变来自镜头组和传感器不完全平行,用 $p_1, p_2$ 描述。完整的正向模型是"归一化坐标 → 加畸变 → 乘内参",反演(去畸变)没有闭式解,工程上用几次不动点迭代或者预先建一张查找表。

import numpy as np def project(X_world, K, R, t, dist=None): """世界坐标 -> 像素坐标,附带径向+切向畸变""" Xc = R @ X_world + t # 世界 -> 相机 x = Xc[0] / Xc[2] # 归一化平面,注意 Z 接近 0 时会炸 y = Xc[1] / Xc[2] if dist is not None: k1, k2, p1, p2 = dist[:4] r2 = x * x + y * y radial = 1 + k1 * r2 + k2 * r2 * r2 # 径向畸变,两项足够 xd = x * radial + 2 * p1 * x * y + p2 * (r2 + 2 * x * x) # 切向 yd = y * radial + p1 * (r2 + 2 * y * y) + 2 * p2 * x * y x, y = xd, yd return K @ np.array([x, y, 1.0]) # 乘内参,最后一步才除以齐次分量

这段代码里有两个容易忽略的细节。第一,除法放在最前面是为了物理意义清晰,但工程实现里通常先判断 $Z$ 是否大于近裁剪面,否则相机后方的点会被"折叠"到图像上,产生莫名其妙的投影。第二,畸变是在归一化平面上施加的,不是在像素坐标上——顺序搞反,标定出来的系数会差一个焦距的量级,而且换个分辨率就完全失效。

提示:重投影误差的 RMS 到 0.2 到 0.5 像素算合格,但只看这个数字会骗人。要把误差按图像位置画出来看空间分布,如果四角明显大于中心,说明畸变模型欠拟合或者标定板没覆盖到边缘,这时候 RMS 再小也没意义。

2.3 光度成像:像素值不是亮度,是一条非线性链路的产物

几何决定"这个点落在哪个像素",光度决定"这个像素值是多少"。从场景辐照度到最终存进文件的整数值,中间隔着一条很长的链路:镜头渐晕和衍射、传感器的量子效率和满阱容量、读出噪声和暗电流、模拟增益、ADC 量化、去马赛克、色调曲线、最后的压缩。任何一环饱和或者截断,后面的算法都拿不回来。

最关键的一环是相机响应函数。理想情况下像素值和曝光量成正比,实际上厂商会施加一条单调但非线性的曲线(gamma 是其中最常见的一种),目的是把人眼不敏感的高光压缩掉。这条曲线未知,而且每台机器、每种拍摄模式都不一样。计算摄影那一章要解决的高动态范围成像,本质就是把这个未知函数估出来:对同一场景拍多档曝光,利用"同一位置的辐照度不变、只有曝光时间在变"这个约束,解 $g(Z) = \ln E + \ln \Delta t$,再加一个平滑正则防止解在低梯度区域抖。这套做法被称作光度校准,是后面做图像拼接、纹理合成、重打光的前提。

采样与混叠是另一条容易被跳过的坑。传感器像素不是点采样,是在一个方块上做积分,然后以像素间距为周期取样。场景中高于奈奎斯特频率的细节不会被丢弃,而是折叠成低频的摩尔纹——这就是为什么拍条纹衬衫会出现奇怪的彩色波纹,也是为什么相机前面要加光学低通滤镜。Bayer 阵列把这个问题又叠了一层:每个像素只有红绿蓝中的一个通道,另外两个颜色是插值猜出来的,去马赛克的误差构成了所有颜色相关算法的误差下限。

3. 线性滤波与多尺度:为什么卷积能拆,为什么模糊几乎不可逆

图像处理这一章看着最像工具清单,其实它在回答一个更基础的问题:哪些操作可以在可接受的代价下做得又快又好,哪些操作在原理上就注定病态。理解了这个分界,才知道什么时候该换思路,而不是继续调参数。

3.1 可分离核:从 $O(k^2)$ 到 $O(k)$ 的那一步

二维卷积的定义很朴素:把核在图像上滑动,逐点乘加。一个 $k \times k$ 的核,每个输出像素要算 $k^2$ 次乘加,整幅图就是 $O(WHk^2)$。当核大到 31×31,这个代价在实时场景里是不能接受的。

救场的是可分离性。判定条件很干净:把核当成矩阵,如果它的秩为 1,就能写成两个一维向量的外积,二维卷积退化成"先沿行做一维卷积,再沿列做一维卷积",代价降到 $O(WHk)$。高斯核、均值核、Sobel 算子全都满足这个条件,因为它们本来就是从一维推广出来的。

import numpy as np def gaussian_kernel_1d(sigma, radius=None): radius = radius or int(3 * sigma) # 3 sigma 之外权重可忽略 xs = np.arange(-radius, radius + 1) k = np.exp(-(xs ** 2) / (2 * sigma ** 2)) return k / k.sum() # 归一化,保证不改变整体亮度 def blur_separable(img, k1d): """可分离高斯模糊:两次一维卷积代替一次二维卷积""" # 核对称,np.convolve 的翻转不影响结果;mode='same' 保持尺寸 rows = np.apply_along_axis(lambda r: np.convolve(r, k1d, mode='same'), 1, img) out = np.apply_along_axis(lambda c: np.convolve(c, k1d, mode='same'), 0, rows) return out

参数上要留意两处。radius取 $3\sigma$ 是经验值,覆盖了 99.7% 的能量,再大边际收益极低;但 $\sigma$ 很大时(比如做远景模糊),这个核会变得很长,此时应该转去频域做,因为 FFT 的复杂度是 $O(WH \log WH)$,与核大小无关。三种实现的分界线大致是:

方式复杂度适用核尺寸
直接二维卷积$O(WHk^2)$小于 7×7
可分离一维卷积$O(WHk)$7×7 到 50×50
FFT 频域乘法$O(WH\log WH)$更大,或需要频域操作时

还有一个常被忽略的细节是边界处理。零填充会让图像边缘变暗,复制填充会拖出一条亮边,镜像填充相对自然但要额外索引。单张图上看不出差别,可一旦进了金字塔或者拼接流水线,边缘的几个像素会被逐层放大,最后变成接缝处的明显色带。

顺带说一句均值核。三次盒式滤波逼近高斯,依据是中心极限定理——这招在实时渲染和嵌入式里用得极多,因为盒式滤波可以用积分图做到与核大小无关。代价是它的频率响应有旁瓣,会在高频处产生振铃,做图像融合时会在强边缘附近露出光环。

3.2 傅里叶视角:模糊是乘法,去模糊是除法,而除法会放大噪声

卷积定理说,空域卷积等价于频域逐点相乘。这条定理一摆出来,很多现象瞬间就通了。高斯模糊在频域就是一个低通滤波器,$\sigma$ 越大,截止频率越低;边缘检测是在抑制低频、保留高频;图像里那些周期性条纹,在频谱上是几个孤立的亮点,用一个陷波滤波器就能精准拔掉。

更重要的是它解释了"为什么去模糊这么难"。模糊是 $B = H \cdot E$(频域),那么理论上 $E = B / H$ 就还原了。问题是 $H$ 在高频处趋近于 0,而观测里永远有噪声,除以一个接近 0 的数,噪声被放大成天文数字,结果就是满屏的彩色噪点和振铃。这就是直接逆滤波必然翻车的原因,不是实现不够精细,是问题本身病态。

维纳滤波的处理方式是在分母上加一项:$W = H^* / (|H|^2 + K)$,其中 $K$ 是噪声功率与信号功率之比。$K$ 趋近 0 时退化为逆滤波,$K$ 变大则变得保守、宁可糊一点也不放大噪声。这个 $K$ 不是玄学参数,它有明确的物理含义,可以从传感器的信噪比估出来。理解了这个结构,再看后面那些基于正则化和马尔可夫随机场的全局优化方法,会发现它们是同一件事的两种表达:一个在频域加阻尼,一个在空域加平滑先验。

锐化也是这套逻辑的推论。锐化的本质是高频提升,把中高频乘上一个大于 1 的增益,振铃和噪声随之一起放大。所以工程上从来不会先锐化再降噪,顺序一定是反的。

3.3 金字塔与小波:多尺度不是技巧,是必然

降采样听起来简单,隔行隔列取值就行了。这条路上翻车的人非常多:直接取样得到的图看着"也对",可图像平移一个像素之后,结果完全不同,缩略图会随平移跳动。原因是采样定理——图像里高于新奈奎斯特频率的成分没有消失,而是折叠到了低频,变成虚假的细节。

正确做法是先低通再抽样,这个组合叫高斯金字塔。把相邻两层相减,得到拉普拉斯金字塔,它是一个带通分解:每一层只保留该尺度上的细节,最顶层保留的是一个极模糊的基底。分解之后可以完美重建,把各层加回去就能还原原图(在量化误差范围内)。这正是多曝光融合、多焦距融合的实现骨架——在每一层上按局部显著性挑系数,再重建,接缝在视觉上就消失了,因为融合发生在不同尺度上,而不是在像素上硬拼。

小波是同一思想的另一支。它用一组正交基做分解,没有冗余,也能完美重建,压缩领域用得更多。金字塔和小波的区别主要在冗余度和方向选择性上,但共通点是:它们都把"尺度"显式地变成一个维度。

这个维度为什么重要?因为算子的响应和尺度是绑定的。一个角点在细尺度上是角点,退到粗尺度就糊成一个平滑的亮斑。检测窗口固定,同一物体在不同距离上的响应就完全不同。把 $\sigma$ 变成可以搜索的一维,才让"这个特征有多大"从假设变成可估计的量,后面特征那一章的尺度不变性,根就在这里。

多尺度在运动估计里还有一层用处。光流和配准的能量函数是非凸的,直接在大位移上优化会掉进局部极小。分层估计的做法是从最粗的一层开始,那时候大位移被缩成了小位移,估出来的结果上采样到下一层作为初值,逐级修正。粗尺度定大局、细尺度修精度,本质上是把一个大范围的非凸问题拆成一串局部近似凸的小问题。

4. 特征、匹配与几何验证:为什么单靠描述子一定不够

从像素走到几何,中间必须经过"哪些点值得信任"这个判断。这一章的三层结构非常清晰:先找出可重复定位的点,再给每个点一个能跨视角比对的描述,最后用几何约束把错误匹配筛掉。三层缺一不可,任何一层偷懒,后面都会以"点云糊成一团"的形式报复回来。

4.1 角点响应与尺度空间:什么样的点值得被跟踪

从跟踪的角度看,一个好特征点的定义很明确:在它周围开一个小窗口,窗口稍微移动一点,窗口内的内容要有明显变化。如果沿某个方向移动窗口内容不变,那这个方向上的位置就无法确定,跟踪就会沿这条线滑移——这就是边缘的问题。数学上把这个"变化量"写成结构张量,它的两个特征值 $\lambda_1, \lambda_2$ 分别对应两个主方向上的变化强度:都大是角点,一大一小是边缘,都小是平坦区域。

直接求特征值要做开方和分解,代价不低。Harris 的贡献是用一个闭式响应替代它:

import numpy as np def harris_response(gray, sigma=1.5, k=0.04): """结构张量 + Harris 响应,无需显式求特征值""" g = blur_separable(gray, gaussian_kernel_1d(sigma)) # 先平滑,压噪声 Ix = np.gradient(g, axis=1) # 列方向偏导 Iy = np.gradient(g, axis=0) # 行方向偏导 # 窗口内加权求和:等价于对梯度乘积做一次高斯加权 Sxx = blur_separable(Ix * Ix, gaussian_kernel_1d(sigma)) Syy = blur_separable(Iy * Iy, gaussian_kernel_1d(sigma)) Sxy = blur_separable(Ix * Iy, gaussian_kernel_1d(sigma)) det = Sxx * Syy - Sxy ** 2 # 等于 lambda1 * lambda2 tr = Sxx + Syy # 等于 lambda1 + lambda2 return det - k * tr ** 2 # k 通常取 0.04 ~ 0.06

这个响应函数之所以好用,一是闭式,二是它对两类退化情形都有惩罚:边缘处 $\lambda_1 \gg \lambda_2$,行列式小而迹大,响应被压下去;平坦区两者都小,响应也小。$k$ 的取值影响灵敏度,取大一些更严格,特征点更少但更可靠,实战里 0.04 到 0.06 之间都行,不必纠结。

Harris 的硬伤是不具尺度不变性。窗口固定,物体靠近或远离,响应就完全变了。解决方案是把尺度也做成搜索维度:对图像做一系列不同 $\sigma$ 的高斯模糊,相邻两层相减得到差分高斯,然后在 $(x, y, \sigma)$ 的三维空间里找极值。差分高斯是尺度归一化高斯拉普拉斯的近似,而后者本质是一个带通滤波器,对斑点状结构响应最强——所以它能自动挑出"这个特征在这个尺度上最突出"。

这一步的深意在于:特征不再是一个像素坐标,而是一个带尺度的坐标。后面所有关于匹配、重建的推理,都建立在这个三元组上。

4.2 描述子与匹配:距离比准则在防什么

检出的点只是一个坐标,要跨视角比对,还得给它一个"指纹"。经典的构造方式是:在特征点周围取一个按主方向旋转对齐的方形邻域,切成 4×4 个子区域,每个子区域统计 8 个方向的梯度幅值直方图,拼起来就是 128 维向量。这个构造里每一步都在对抗一种变化——按主方向对齐抗旋转,按尺度取邻域抗缩放,直方图统计抗小的形变,最后归一化抗光照的线性缩放,再把过大的分量截断后重新归一化,抗非线性光照。

匹配阶段的规则比描述子本身更容易被忽视。最直接的想法是"距离小于某个阈值就算匹配",这个做法在高维空间里几乎必然失败,因为维度一高,最近邻和次近邻的距离会趋于接近,绝对阈值没有可迁移性。工程上通用的是比值判据:取最近邻和次近邻的距离之比,小于 0.7 到 0.8 才接受。它的逻辑是,如果这个点真的只对应一个位置,那么第二近的那个候选应该明显更远;如果第二近的和第一近的差不多,说明这块区域有重复结构,匹配本身就不可信。

再加一层互查——正向匹配和反向匹配必须互为最近邻——能去掉那些"被很多点共享"的单向匹配。经过这两道,剩下的匹配通常已经很干净,但仍然会有成片的错误,尤其是在重复纹理(一排窗户、一片瓷砖)和视角跨度过大的情况下。视角一大,描述子背后的局部平面假设就破了,形变超出直方图统计能吸收的范围。

4.3 RANSAC 与光束法平差:先把外点投掉,再把误差摊平

到这一步,手里是一堆候选匹配,其中混着一定比例的错误。用最小二乘直接拟合模型,外点会把结果拽得面目全非,所以要用随机采样一致性:反复随机抽最小样本集(单应矩阵要 4 对点,基础矩阵用 7 点或 8 点解),解出模型,统计有多少点符合这个模型,保留内点最多的那个。

import numpy as np def ransac(pairs, solver, min_samples=4, conf=0.99, thresh=3.0): """pairs: N x 4 的匹配点对;solver: 最小样本集求解器""" best_inliers, best_model, iters = [], None, 10000 i = 0 while i < iters: idx = np.random.choice(len(pairs), min_samples, replace=False) model = solver(pairs[idx]) if model is None: # 退化样本(如三点共线) i += 1 continue err = residuals(model, pairs) # 对称转移误差,单位像素 inliers = np.where(err < thresh)[0] if len(inliers) > len(best_inliers): best_inliers, best_model = inliers, model w = len(inliers) / len(pairs) # 内点率,用来自适应迭代次数 p_bad = 1 - w ** min_samples iters = int(np.log(1 - conf) / np.log(max(p_bad, 1e-12))) i += 1 return refit(solver, pairs[best_inliers]), best_inliers

自适应迭代次数这一段是 RANSAC 的关键。理论值是 $N = \log(1-p)/\log(1-w^s)$:内点率 $w$ 越低、最小样本数 $s$ 越大,需要迭代的次数增长非常快。举个例子,$w=0.3$、$s=4$(单应矩阵)、置信度 0.99,算下来大约 900 次;同样的内点率换成 $s=7$(基础矩阵的七点解),次数直接上万。这就解释了为什么基础矩阵估计在低内点率场景下如此难做,也解释了为什么要先做比值判据和互查——把 $w$ 从 0.2 提到 0.6,迭代次数会降两个数量级。

阈值thresh用像素单位的重投影误差,通常取 2 到 3 像素。设太小会把正确的匹配也踢掉,设太大会让错误匹配混进来,最后的重拟合被带偏。

拿到内点之后还有一步不能省:用全部内点重新拟合,并且从代数误差切换到几何误差。最小化代数误差方便,但它和物理上关心的"投影差了多少像素"并不等价,在靠近退化配置时偏差更明显。所以最后的精化一定要用重投影误差做非线性最小二乘。

再往上就是光束法平差。它同时优化所有相机位姿和所有三维点,目标函数是全部观测的重投影误差平方和。听起来是个几十万维的优化问题,实际上雅可比矩阵极其稀疏——每个残差只跟一个相机和一个点有关。利用这个结构做消元,先把点边缘化掉,解一个只含相机的稠密系统,再回代求点,这就是大规模重建能够落地的核心技巧。书里单列一节讲"约束结构和运动",说的是另一条路:当相机运动有先验(比如固定焦距、平面运动、有惯性测量辅助)时,自由度减少,问题从病态变良态,代价是模型假设不成立时误差会系统性地偏。

5. 排查:视觉流水线上最容易翻车的五处

调参之前先做体检。下面这几条是从实际项目里攒下来的,每一条都符合"现象看着像算法问题、根因却在上游"的特征。

5.1 特征点成片消失:先查位深和动态范围

现象:换了一段夜景素材,检测到的特征点从几万个掉到几百个,阈值一路降到 1 也没改善。

原因:8 位图像在暗部只剩几个量化级,相邻像素的梯度几乎全是量化噪声,结构张量算出来的响应是纯噪声,和真实纹理没关系。另一种情况是自动曝光把高光压平,亮区的纹理被截断成一片常数。这两种都属于信息在进入算法之前已经丢了。

解决:在位深更高、未经色调曲线的数据上做检测,或者检测前先做一次局部对比度归一化,把暗部拉回可用范围。同时确认数据是否已经施加过 gamma——在线性数据上算梯度和在 gamma 后的数据上算梯度,数值能差出几倍。

5.2 全景拼接出现鬼影:增益没对齐,权重也不对

现象:拼接结果在接缝处有一条明显的亮带,重叠区域出现重影。

原因:相机自动曝光让每张图的增益不同,直接把不同增益的信号叠加,接缝两侧的亮度天然对不上。另一个原因是融合权重用了硬接缝或者简单的线性渐变,重叠区里两边的错位被原样保留下来。

解决:先估每张图的增益和响应曲线(用重叠区的对应像素做最小二乘,取对数后是线性的),把辐射量统一到同一尺度;融合改到拉普拉斯金字塔上做,每一层按局部对比度取系数。重影的根源是配准误差,这个要靠提高几何对齐精度解决,融合只能掩盖不能消除。

5.3 光流方向对、幅度全错:单位在你没注意的地方

现象:把光流可视化出来,箭头方向漂亮,但位移量差不多只有真值的一半。

原因:分层估计时,粗尺度的结果上采样到细尺度要乘 2,少乘一次幅度就整体缩水一半,而方向完全不受影响,所以肉眼很难发现。另一种情况是把归一化坐标下的位移直接当像素位移用,差了一个焦距的因子。

解决:用已知平移量的合成序列做一次端到端验证,把输入位移和输出位移画成散点,斜率应该是 1。变量命名里带上单位,flow_px和flow_norm分开,这招看着笨,能省掉大量排查时间。

5.4 点云呈碗状弯曲:标定误差被三角化放大

现象:重建出来的平面点云整体向内弯,像一个浅碗,越靠边缘越明显。

原因:畸变系数在标定板没覆盖到的区域属于外推,而径向畸变的误差随半径增长得很快。另一种情况是标定板只占了画面中央一小块,焦距和主点强耦合,两个参数互相补偿,重投影误差看着很小,但几何意义已经错了。三角化对角度误差极其敏感,基线越短、距离越远,同样的标定误差被放大得越厉害。

解决:标定板覆盖到画面四角,姿态要有多样性(倾斜、旋转、不同距离);检查误差的空间分布而不只看总体 RMS;用一组完全独立的拍摄做交叉验证,比如用另一个距离的标定板检查重投影是否仍然一致。

5.5 合成数据上九成九,真实图上一半:噪声模型骗了你

现象:在自制的合成数据集上指标漂亮,换到真实拍摄的图上准确率断崖式下跌。

原因:合成数据加的是高斯白噪声,而真实传感器有固定模式噪声、暗电流、去马赛克伪影、压缩块效应,这些都不是各向同性的。更要命的是参数在合成集上被反复调到最优,等于对着测试集调参。

解决:按三段法验证——先在无噪合成数据上确认实现没写错,再逐步加噪看性能退化曲线是否平滑,最后在完全不参与调参的真实数据上报告结果,并且把失败样本单独列出来看。性能随噪声平滑下降说明算法是稳的,断崖式下跌说明某处有硬阈值在起作用。

6. 设计取舍:这本书为什么把「可验证」排在「优雅」前面

翻完整本书会发现一个反复出现的选择:明明有更完整的模型,作者却常常退回到一个更粗糙但能解的版本。表面看是妥协,实际上是这个领域最核心的工程判断。

最典型的是各种简化假设。完整的辐射传输方程没法反演,所以假设表面是朗伯的;任意光照下的反射没法建模,所以假设光源在无穷远;真实镜头有衍射和像差,所以用针孔加两项径向畸变近似。这些假设不是"错的",它们是一份约定:在约定成立的范围内,反演结果是可信的;约定失效的地方,算法一定崩,而且崩的方式可以预测。知道自己站在哪份约定上,比多记一个公式重要得多。

第二个取舍是稀疏与稠密。稀疏特征点快、能全局优化、能处理大基线,但只在纹理丰富处有效,白墙和天空一个点都没有。稠密方法给出完整场,代价是必须引入平滑先验,而平滑先验在物体边界上一定是错的,边缘会被糊掉。实战里常见的组合是稀疏做粗对齐、稠密做精修,让两者各自负责自己擅长的部分。

第三个取舍是局部与全局优化。局部方法快,对初值敏感,一旦掉进局部极小就出不来;全局方法(图割、马尔可夫随机场、光束法平差)能跳出局部极小,但代价函数非凸,只能求近似解。选哪个不取决于哪个"更好",取决于你对实时性和精度的实际需求。

方法族核心假设典型失效条件
特征匹配 + 单应场景近似平面,或相机纯旋转大景深 + 明显平移,视差无法用单应吸收
光流亮度恒定、小位移、局部平滑高光反射、运动边界、大位移
立体匹配表面近似正面、纹理足够弱纹理、重复纹理、倾斜表面
光束法平差重投影误差服从近似高斯分布存在严重误匹配且未被剔除

最后说验证。作者在方法论里反复强调的那套三段式——干净合成数据、加噪扫描、真实多样数据——看着笨,但它把"我觉得能用"和"我知道它在什么条件下不能用"区分开了。在视觉这个领域,一个算法在几张图上看着合理、实际是错的,实在太容易了。

我自己有过一次教训:一个配准流程在实验室的十几张图上跑得很好,换到户外素材就整体偏移,查了两天才发现是标定用的棋盘只覆盖了画面中间三分之一,畸变系数在边缘外推得一塌糊涂。从那以后我每次接一个新的视觉任务,都强制先把正向模型写一遍——相机怎么投影、光怎么衰减、噪声从哪来、量化在哪一步——再动

本文还有配套的精品资源,点击获取