AI Hero Club

视觉与三维场景 · 第 3 章

已自行标记完成0 / 8

仅保存在当前浏览器;完成标记不代表通过项目验收。

本章提纲 6 个小节
全课目录 8 章
完整课程计划 ↗
视觉与三维场景 / 课程计划 ↗

CHAPTER 03 / 8建议学习 120 分钟

从标定图像估计一般相机矩阵

已知三维标靶和像素对应时,怎样建立 DLT 约束,并识别无法确定相机的退化输入?

本章要解决的 3 个问题
  • 建立非共面标定靶的线性约束
  • 推导归一化 DLT 与反归一化
  • 识别退化、代数误差和像素误差的区别
课程项目 ZIP ↓

标定约束、归一化与相机可辨识性

第二章把相机矩阵当已知输入,本章用独立标定照片估它。标定靶是制造时已知三维位置的十六个彩色标记,坐标以米记录;未知珠子场景的坐标不提供给标定器。检测器从每张标定 PNG 自动提取区域,颜色描述子与标靶已知颜色匹配,才得到三维点与像素对。这样的已知几何是测量工具,不是偷用未知对象真值。标定靶误差也会进入结果,本合成器把它理想化为精确。

待估矩阵 P 有十二个元素,但整体非零比例不影响像素,因而一般投影相机有十一个独立比例自由度。每个三维到二维对应给两条独立线性约束,因此至少需要六个点,数量只是必要条件。若全体标定点共面,图像只能约束该平面到图像的单应关系,无法充分确定离平面的映射。程序检查点集减去均值后的秩为三,再进入 DLT;这不意味着所有接近共面的集合都数值稳定。

令三维齐次点为四维列向量,P 三行的转置分别记为列向量 p 一、p 二、p 三。透视式为 u 等于第一行点积除第三行点积,v 同理。把分母移到左边,得到第一行点积减 u 乘第三行点积为零,以及第二行点积减 v 乘第三行点积为零。把 P 的元素按行排成十二维未知向量,每个对应贡献两行设计矩阵 A。约束是齐次的,零矩阵虽满足方程,却没有相机意义。

(p1⊤−up3⊤)X~=0,(p2⊤−vp3⊤)X~=0,min⁡∥p∥=1∥Ap∥2(p_1^\top-u p_3^\top)\tilde X=0,\quad(p_2^\top-v p_3^\top)\tilde X=0,\qquad \min_{\|p\|=1}\|Ap\|^2
p 是 P 按行展开的十二维向量,单位范数排除全零解。

在无噪声且配置充分时,设计矩阵有一维零空间;有检测误差时,通常没有严格零空间,改为寻找单位向量使代数残差最小。这里导入奇异值分解的数值结论:A 等于 U 乘非负对角奇异值再乘 V 转置,正交变换保持欧氏长度,因此最小残差对应最小奇异值的右奇异向量。取 V 转置最后一行并恢复三乘四形状。这个代数目标计算方便,却还不是原始像素重投影目标。

为什么先归一化?世界坐标可能以毫米写成数千,像素坐标可能几百,常数齐次项仍为一,设计矩阵各列尺度会差很远。有限精度线性代数的误差可能被放大,也会让代数目标以单位大小决定不同项权重。我们把二维点平移到均值零,再统一缩放,使平均欧氏半径为根号二;三维点使半径为根号三。这里是平均距离,不是距离平方平均后的根号,二者必须按实现一致。

Xn=TXX~,xn=Txx~,Pn=TxPTX−1,P=Tx−1PnTXX_n=T_X\tilde X,\quad x_n=T_x\tilde x,\quad P_n=T_xPT_X^{-1},\qquad P=T_x^{-1}P_nT_X
先求归一化坐标中的矩阵,再恢复原坐标;矩阵顺序由代入关系确定。

反归一化顺序可以直接验证。将原世界齐次点先乘 T_X,归一化相机 P_n 将它送到归一化像素,再乘 T_x 的逆回到原像素。因此完整映射正是逆 T_x、P_n、T_X 的乘积。不能把两个变换都放到左侧。恢复后矩阵依然有整体尺度自由度;项目用第三行前三项的范数归一,再检查标靶的平均深度符号,必要时整体取负,固定前向约定。

完整数值构造:八个点怎样变成十六行方程

先把所有可复算数字固定下来。相机取P=[[2,0,0,0],[0,2,0,0],[0,0,1,0]],像素主点为零、焦距为二,世界点单位为米。八个点是X、Y各取正负一,Z取三或五,故不是同一平面。这里只为手算采用小焦距;实际PNG项目焦距仍是95像素,不因这个算例而修改。

点号XYZu=2X/Zv=2Y/Z
11132/32/3
2−113−2/32/3
31−132/3−2/3
4−1−13−2/3−2/3
51152/52/5
6−115−2/52/5
71−152/5−2/5
8−1−15−2/5−2/5
每一点给两行独立透视约束;全部观测由同一明确相机生成。

将每点齐次坐标q=[X,Y,Z,1]按行存储,A的u行是[q,0,0,0,0,−u q],v行是[0,0,0,0,q,−v q]。例如第一个点的u行为[1,1,3,1,0,0,0,0,−2/3,−2/3,−2,−2/3];v行为[0,0,0,0,1,1,3,1,−2/3,−2/3,−2,−2/3]。第二个点的u行为[−1,1,3,1,0,0,0,0,−2/3,2/3,2,2/3],v行为[0,0,0,0,−1,1,3,1,2/3,−2/3,−2,−2/3]。这四行已把符号、行展开顺序和齐次常数明确到每个元素。

剩余六点逐行代入同一个规则,得到16×12矩阵。无噪声时它的秩应为11,零空间只有整体比例一个方向。P按行展开后的欧氏长度为三,因此SVD可能返回正或负的[2,0,0,0,0,2,0,0,0,0,1,0]/3。我们最后令第三行第三项为一,恢复表中的P,而不是把SVD向量第一项强行设为一。

这组点的三维均值为(0,0,4),所有点距均值均为根号三,所以TX只将Z减四,不需额外缩放。像素均值为零,平均半径是8根号二/15,故Tx的两条像素轴缩放均为15/8。由Pn=Tx P TX逆,得到Pn=[[15/4,0,0,0],[0,15/4,0,0],[0,0,1,4]],仍允许共同非零比例。恢复时必须左乘Tx逆、右乘TX。

独立运行独立SVD重建明确相机与未参加估计的投影;不调用项目DLT。
import numpy as np
P=np.array([[2.,0,0,0],[0,2,0,0],[0,0,1,0]])
X=np.array([[x,y,z,1.] for z in [3.,5.] for y in [1.,-1.] for x in [1.,-1.]])
def pixels(camera,points):
    q=points@camera.T
    return q[:,:2]/q[:,2:3]
uv=pixels(P,X)
# 从正文的两行规则构造;不导入项目calibrate_dlt。
A=np.array([row for q,(u,v) in zip(X,uv)
            for row in [np.r_[q,np.zeros(4),-u*q],np.r_[np.zeros(4),q,-v*q]]])
assert np.linalg.matrix_rank(A)==11
_,singular,Vt=np.linalg.svd(A)
Q=Vt[-1].reshape(3,4);Q=Q/Q[2,2]
np.testing.assert_allclose(Q,P,atol=1e-12)
TX=np.eye(4);TX[2,3]=-4
Tx=np.diag([15/8,15/8,1.])
Pn=np.array([[15/4,0,0,0],[0,15/4,0,0],[0,0,1,4.]])
restored=np.linalg.inv(Tx)@Pn@TX
np.testing.assert_allclose(restored,P,atol=1e-14)
heldout=np.array([[.3,-.4,4.,1.]])
np.testing.assert_allclose(pixels(Q,heldout),[[.15,-.2]],atol=1e-12)
print('rank',np.linalg.matrix_rank(A),'smallest singular',singular[-1])
print('camera',Q.tolist(),'heldout',pixels(Q,heldout).tolist())

同平面伪解也给出两台具体矩阵

P=[200002000010],P′=[201/2−202000010]P=\begin{bmatrix}2&0&0&0\\0&2&0&0\\0&0&1&0\end{bmatrix},\qquad P^{\prime}=\begin{bmatrix}2&0&1/2&-2\\0&2&0&0\\0&0&1&0\end{bmatrix}
两台矩阵均为秩三;它们的区别恰好在z=4平面上抵消。

P′横向分子比P多Z/2−2。在Z=4时增量为零,因此平面内任意点都同投影;在X=1、Y=0、Z=5时,P给u=.4,P′给u=.5,差.1像素。两台相机都能完美解释同一平面观测,却对离平面点给不同答案,这才是“多收集同平面点仍无帮助”的具体自由度。

独立运行两台同平面相机的逐数字反例。
import numpy as np
P=np.array([[2.,0,0,0],[0,2,0,0],[0,0,1,0]])
Q=P.copy();Q[0,2]=.5;Q[0,3]=-2
plane=np.array([[1.,0,4,1],[-2.,1,4,1],[.3,-.7,4,1]])
def project(M,X):
    y=X@M.T
    return y[:,:2]/y[:,2:3]
np.testing.assert_allclose(project(P,plane),project(Q,plane),atol=1e-14)
other=np.array([[1.,0,5,1]])
assert abs(project(P,other)[0,0]-.4)<1e-14
assert abs(project(Q,other)[0,0]-.5)<1e-14
print('same-plane equality; heldout difference',project(Q,other)-project(P,other))

算例一:反归一化可逆性与单位变化

设二维归一化把像素减去一百、五十后乘零点零一,三维归一化把世界点减去中心后乘二。若已经求得归一化相机,把原三维点依次按这三个步骤映射,再除齐次尺度,应与直接用恢复的 P 相同。检验不需要知道相机真实姿态,只需抽任意未参加估计的点比较两个计算路径。如果结果不同,最先检查矩阵乘法顺序与行列存储,而不是重新调奇异值容差。

把全部世界标靶坐标从米改写为毫米,相机矩阵前三列应相应除以一千,平移列相对保持,使齐次乘积不变;整体归一后的具体矩阵数值还可再乘共同因子。重投影不变并不意味着三角化输出的单位不变,新输出将是毫米。项目把输入单位和坐标约定一起保存,之后比较三维误差必须先统一单位,不能把米与毫米的差当算法精度提高一千倍。

估出 P 后要计算几何残差:将每个已知三维点重投影到像素,再减检测位置,二维长度平方平均后开根号。这个 RMS 使用每点二维欧氏误差,不能与每坐标 MSE 的根号混同。还要保存逐点误差与图像位置,检查残差是否在边缘较大、是否有少数错误匹配被平均遮住。项目五张标定图的 RMS 约零点零二二到零点零二九像素,是受控干净标记的实测,不是现实相机精度承诺。

算例二:代数残差小仍能有错误相机

假设所有已知点深度都为四米。齐次三维点的第三分量总是第四分量的四倍,设计矩阵相关列无法区分某些相机系数。可以改变 P 的深度列并相应补偿平移列,让平面上的像素完全不变,却让深度五米的未知点投影大幅改变。这不是多收集几千个同平面点就能解决的独立信息缺失。需要多姿态平面标定加内参约束,或本项目采用的非共面靶来恢复一般 P。

另一个独立退化来自像素侧:世界点虽非共面,若错误观测全部落在图像同一条直线上,就可能得到秩二映射,而非合法的秩三透视相机。新增实现先要求N×3与N×2行数一致、所有数字有限,再检查归一化世界点和二维像素的空间展开;设计矩阵的次小奇异值必须与最大值保持可分辨比例,最后检查恢复相机左3×3块的相对奇异值。不能只看最小奇异值接近零,因为合法齐次解本来就要有一个零方向,退化的危险是出现第二个未约束方向。

本教学实现明确使用归一化空间比例阈值:世界/像素展开和设计次小奇异值为1e−8,相机左块为1e−10。它们是数值可辨识性策略,不是所有噪声模型下的统计最优阈值。归一化减少米/毫米和像素尺度影响;直接判断未归一化行列式“小于某常数”会把单位变化误当退化。新增回归包含非共面世界加共线像素、重复观测、近共面/近共线、错行数与NaN,并确认原正常标定相机不变。

即便不严格共面,若标定点只在非常薄的深度层里变化,设计矩阵也可能病态。末尾两个奇异值的相对大小、标靶空间覆盖、对像素扰动的敏感性都应一起看。最小奇异值很小只说明有一个近似解,并不保证该解对噪声稳定。固定种子给检测位置加零点一像素扰动,重新标定,再看留出点重投影变化,是一种具体压力测试;它给数值证据,不是普适误差界。

一般 P 估计不直接保证其左三乘三部分具有期望的零斜切内参形式。若要提取 K、R、t,可对左块做 RQ 分解,调整对角符号和旋转行列式,再设内参末元素为一;这是另一条有约束的相机解释步骤。本项目的三角化只需要 P,受控渲染另使用公开的焦距尺度,并未实现真实镜头的全内外参与畸变联合非线性标定。不能把 DLT 输出的任意矩阵默认为所有现实约束均满足。

进一步精化通常最小化像素重投影平方和,而不是代数残差。把相机用合法旋转、平移和内参参数化,对参数求导后迭代,才不会在优化中任意离开旋转流形。非线性目标可能有局部极小值,所以 DLT 的作用是给合理初值,不是保证全局最优。本课程将该扩展的接口与原理讲清,当前项目不冒称运行了 OpenCV 标定器或全场景束调整。

程序会对五个相机分别读取各自标定图片,包括保留场景视角的标定图。保留视角的相机参数来自独立标靶,并不使用保留场景图像来调整珠子位置。这对应“已标定新视角”的评价条件,而不是完全未知相机姿态的重建。评价时必须把这个条件写出来,否则读者可能以为项目还实现了任意视角定位。未知姿态可以作为后续求解问题,但不在当前实测范围。

源数据的可审计性与数值稳定性是互补问题。清单记录每个 PNG 和标靶文件的散列,能够确认运行读的是什么;秩、奇异值和残差能够检查这些输入是否足以支持估计。散列正确的错误对应仍会给错误相机,残差很小的结果也可能来自错误单位。验收必须沿着文件、观测、对应、矩阵、残差和单位逐层检查,不让某一个通过标志遮蔽其他层的失败。

在课程项目目录运行用已知一般旋转相机的无噪声投影验证 DLT。
import numpy as np
from vision3d.core import look_at,project,calibrate_dlt
rng=np.random.default_rng(23)
X=rng.uniform([-1,-1,3],[1,1,5],(20,3))
P,_=look_at([.8,-.2,.1])
u,_=project(X,P)
Q,s=calibrate_dlt(X,u)
np.testing.assert_allclose(project(X,Q)[0],u,atol=1e-10)
print('reprojection max',abs(project(X,Q)[0]-u).max())

TRY IT YOURSELF

练习一:约束数

一般三乘四投影相机为什么至少要六个对应?六个同平面对应是否足够?说明自由度与退化两个条件。

提示 1
十二个相机元素有一个整体尺度自由度,每对应提供几条独立方程?
提示 2
计数只是必要条件,再检查点布局与设计矩阵秩;更多共面点并不提供缺失深度信息。
对照参考解答
十二个元素去掉整体比例有十一个自由度;每个对应在消去齐次像素尺度后给两条独立约束,五点至多十条不足,六点可给十二条,但还必须具有足够秩。共面点只约束平面单应,深度方向与平移可互相补偿,因此六个或更多共面点都不能单独恢复一般相机。程序既检查数量,也检查三维中心化坐标秩;接近共面仍需敏感性测试。

TRY IT YOURSELF

练习二:反归一化

已知归一化关系为 x_n=T_x x、X_n=T_X X,且 x_n 与 P_n X_n 成比例。逐步导出原坐标相机,不省略逆变换。

提示 1
从两个归一化定义逐项代入同一个比例关系,不先背反归一化公式。
提示 2
左侧像素恢复需左乘T_x逆,右侧世界点先乘T_X;用同一测试点两条路径核对。
对照参考解答
先将第二个定义代入得到 T_x x 与 P_n T_X X 成比例。左乘 T_x 的逆,得到 x 与 T_x^{-1}P_nT_X X 成比例,所以原矩阵 P 等于该乘积,允许额外整体非零比例。左边的逆来自像素坐标恢复,右边正变换来自世界坐标先归一化。把一个测试点按两条路径投影并比较是独立检查,切勿将 T_X 也误求逆。

TRY IT YOURSELF

练习三:目标函数

把某个代数 DLT 方程整行乘一千,零噪声解与带噪声最小二乘解各可能怎样变化?为什么最终还要报告像素残差?

提示 1
零噪声看零空间,带噪声看残差平方的加权目标。
提示 2
一行放大1000时该行平方残差权重怎样变?最终另经透视除法测有像素单位的误差。
对照参考解答
零噪声时原方程等于零与乘一千后的方程等于零具有同一零空间,所以理想解不变。带噪声时最小化行残差平方,乘一千使该行权重增为一百万,最优折中一般会改变。这说明代数目标依赖方程尺度。归一化减轻不均衡,但不能把代数误差变成像素误差;最终必须经透视除法计算每点重投影,并保留单位、逐点及总体统计。

BUILD THE MECHANISM

从投影方程组装DLT

实现design_rows与calibrate;只可使用通用SVD,不调用现有calibrate_dlt。 下载本课完整项目,在项目根目录运行;只编辑 learner/starter.py,已有构造框架由项目提供。

输入与输出 至少6个非共面3D/2D对应;设计矩阵需至少秩11、输出camera秩3。
相机允许非零尺度差,用未参与拟合点检验投影。

在课程项目目录运行待完成的学习任务
# design_rows
def design_rows(world,pixels):
    raise NotImplementedError('dlt')

# calibrate
def calibrate(world,pixels):
    raise NotImplementedError('dlt')
提示 1

u*(p3·X)=p1·X,v方程同理;每点两行。

提示 2

A行按[X,0,−uX]及[0,X,−vX]排列12列。

提示 3

最小右奇异向量reshape为3×4;非共面并不自动保证投影可辨识。

独立检查 .venv/bin/python -m learner.check --stage dlt --output runs/learner-dlt.json

迁移检验 改变明确P的焦距与平移,再选未用于估计的点复核;噪声/弱基线会影响稳定性,不只看训练重投影。

参考实现与解释
在课程项目目录运行参考实现
# design_rows
def design_rows(world,pixels):
 X=np.c_[np.asarray(world,dtype=float),np.ones(len(world))];uv=np.asarray(pixels,dtype=float);rows=[]
 for point,(u,v)in zip(X,uv):rows.extend([np.r_[point,np.zeros(4),-u*point],np.r_[np.zeros(4),point,-v*point]])
 return np.asarray(rows)

# calibrate
def calibrate(world,pixels):
 world=np.asarray(world,dtype=float);pixels=np.asarray(pixels,dtype=float)
 if world.shape[0]<6 or world.shape[1:]!=(3,)or pixels.shape!=(len(world),2)or not np.isfinite(world).all()or not np.isfinite(pixels).all():raise ValueError('finite Nx3/Nx2, N>=6')
 if np.linalg.matrix_rank(np.c_[world,np.ones(len(world))])<4:raise ValueError('coplanar input')
 A=design_rows(world,pixels);u,s,v=np.linalg.svd(A,full_matrices=False)
 if np.linalg.matrix_rank(A,tol=s[0]*1e-10)<11:raise ValueError('unidentifiable camera')
 P=v[-1].reshape(3,4)
 if np.linalg.matrix_rank(P,tol=np.linalg.norm(P)*1e-10)<3:raise ValueError('rank deficient camera')
 return P/np.linalg.norm(P)

CHECK YOUR UNDERSTANDING

检验理解

先作答,再决定是否展开解析。选择题用于发现理解缺口;独立实现与迁移任务仍需另行检验。

1. 一般投影矩阵的整体比例为何不能由像素确定?

查看解析P整体乘非零常数不改变归一化像素。
2. 六个点足以估相机的完整条件是什么?

查看解析自由度计数只是必要条件,设计矩阵秩决定是否可识别。
3. DLT最小右奇异向量直接最小化什么?

查看解析透视除法使几何误差与线性代数目标不同。
4. 保留视角已用独立标靶标定,实验验证的条件是什么?

查看解析新视角的相机参数已提供,不能冒称实现未知姿态估计。

自行标记用于记录学习进度,不代表通过项目验收。