本文已参与「新人创作礼」活动,一起开启掘金创作之路。
参考论文:MLPNP - A REAL-TIME MAXIMUM LIKELIHOOD SOLUTION TO THE PERSPECTIVE-N-POINT PROBLEM
PnP的任务是找到一个旋转R∈SO(3)和平移t∈R3,将世界坐标系下的点pi映射到相机坐标系下的观测点vi,即:
λivi=Rpi+t(1)
这里的λi是每个点的深度,观测值vi是测量得到的结果,因此该不确定量具有单位长度∥vi∥=1。下图中描述了参数和观测值:
每个方位向量vi上的平面是由各自的零空间向量ri和si张成的。
观测与不确定性传播
设标定后的相机对世界点pi∈R3进行观测,在像平面的不确定观测为:
x′=[x′y′],Σx′x′=[σx′2σy′x′σx′y′σy′2]
关于观测点的不确定性用二维协方差矩阵Σx′x′来描述。
通过正投影π(K−1)将图像点x′变换到相机坐标系下的三维方向上(不确定深度信息)。
x=πx′==⎣⎡xy1⎦⎤,Jπ=⎣⎡∂x′∂πx′∂x′∂πy′0∂y′∂πx′∂y′∂πy′0⎦⎤
Jpi为正投影矩阵的雅可比,因此利用图像点x′的不确定性传播,可到的相机点x的协方差矩阵:
Σxx=JπΣx′x′JπT=⎣⎡σx2σyx0σxyσy20000⎦⎤
其中协方差矩阵的秩为2,因此它是奇异的,不可逆的。接下来球面归一化产生了最终的观察表达式,称之为方位向量:
v=⎣⎡vxvyvz⎦⎤=∥x∥x,Σvv=⎣⎡σvx2σvyvxσvzvxσvxvyσvy2σvzvyσvxvzσvyvzσvz2⎦⎤
根据协方差的传播:
Σvv=JΣxxJT,J=∥x∥1(I3−vvT)
此时的协方差矩阵Σxx依然是奇异的。
向量的零空间
零空间定义:已知A为一个m∗n矩阵。A的零空间(nullspace),又称核(kernel),是一组由下列公式定义的n维向量:ker(A)={x∈Cn:Ax=0}即线性方程组Ax=0 的所有解x的集合。
在数学中,一个算子A的零空间是方程Av=0的所有解v的集合。它也叫做A的核,核空间。用集合建造符号表示为
Null(A)={v∈V:Av=0}
v的零空间张成一个二维坐标系,其轴r和s垂直于v,并位于v的切线空间:
Jvr(v)=null(vT)=[rs]=⎣⎡r1r2r3s1s2s3⎦⎤(7)
函数null(⋅)计算v的奇异值分解(SVD),取两个零特征值对应的两个特征向量。即:
进一步假设Jvr是一个标准正交矩阵,JvrT(v)Jvr(v)=I2。这个Jvr还表示了从正切空间到原始向量的变换的雅可比矩阵?,因此,JvrT可以得到从原始齐次向量v到其简化后的等价向量vr的变换。
vr=[drds]=JvrT(v)v=0(8)
与非奇异的协方差:
Σvrvr=JvrT(v)ΣvvJvr(v)=[σvrx2σvrxyσvrxy′σvry2](9)
另一种看待vr的方式是把它看作是切线空间中的残差。在下面,我们将通过最小化正切空间的残差,利用Eq. 8来得到相机在世界坐标系下的旋转和平移的线性估计。
补充"slam"东对于该部分的理解:对于向量v,是经过相机观测图像的像素点,经过内参的反投影得到的。所谓的正切平面,就是通过计算向量v转置的零空间,得到与v垂直的r和s坐标轴。因此定义J为从正切空间到原始向量v的雅可比变换矩阵,对于雅可比的理解可参考知乎如何理解雅可比矩阵,雅可比就是切空间。因为r和s本身与v垂直,所以v在该正切空间上的投影为0。但是对于空间点p,是经过旋转R与平移t,归一化,然后再乘上变换雅可比,便可以落在对应向量v的正切平面上。但是该p'点虽然理论上也是在原点,但是实际上因为Rt的估计存在误差,所以投影并不为0,所以这里把它看作切线空间中的残差。
相机位姿的线性估计
利用式1和7,我们可以改写式8
[drds]=[rTsT]λi−1(Rpi+t)(10)
其中λ=0。因此,如果我们知道相机的绝对方位,世界点pi在v的正切空间中的投影,应该会得到相同的简化后的坐标。由公式10可得到:
0=r1(r^11px+r^12py+r^13pz+t^1)+r2(r^21px+r^22py+r^23pz+t^2)+r3(r^31px+r^32py+r^33pz+t^3)
0=s1(r^11px+r^12py+r^13pz+t^1)+s2(r^21px+r^22py+r^23pz+t^2)+s3(r^31px+r^32py+r^33pz+t^3)
现在,两个等式的未知数都是线性的,可以将他们叠加到一个矩阵A中,得到一个线性方程组的齐次方程组:
Au=0(12)
u=[r^11,r^12,r^13,r^21,r^22,r^23,r^31,r^32,r^33,t^1,t^2,t^3]T。由于每个观测产生两个残差,求解公式12至少需要5个点。利用不相关的观测结果,给出了随机模型:
P=⎣⎡Σvr1vr1−1⋮0⋯⋱⋯0⋮Σvrivri−1⎦⎤(13)
最终的正规方程为:
ATPAu=Nu=0(14)
在∥u∥=1的条件下,找到u使得等式14最小化,使用SVD分解:
N=UDVT(15)
解为V中对应于D中最小奇异值的特定列:
R^=⎣⎡r^11r^21r^31r^12r^22r^32r^13r^23r^33⎦⎤,t^=[t^1t^2t^3](16)
它由一个尺度因子决定,因为平移部分t^仅指向正确的方向。这个尺度可以从现实中恢复,旋转矩阵R^每一列的模必须等于1,因此最终的平移为:
t=3∥r^1∥∥r^2∥∥r^3∥t^(17)
约束表明,这9个旋转参数并没有定义一个正确的旋转矩阵。可以通过SVD分解计算R^:
R^=URDRVRT(18)
最小化Frobenius范式的最佳矩阵为:
R=URVRT(19)
到此为止,获得了相机绝对姿态的线性ML估计。为了提高精度,采用了非线性的优化方法。对初始估计进行后续的优化是一个常见的过程。
非线性优化
我们应用高斯-牛顿优化迭代改进相机姿态。具体地说,我们最小化Eq. 10中定义的正切空间残差。
这个速度相当快,有两个原因:
一方面,零空间向量已经计算过了我们只需要计算切空间向量和每个变换后的世界点之间的点积。
另一方面,线性估计的结果已经接近局部极小值,即高斯-牛顿优化算法收敛速度快。
在实践中我们发现,最大的5次迭代是完全足够的。
平面的情况下
在平面情况下,SVD(等式15)产生最多四个解向量,因为相应的奇异值变小(接近于零)。在这种情况下,解是这些向量的线性组合。为了求解系数,必须求解一个带有非线性约束的方程组。我们使用下面的技巧来避免这种情况。
设M=[p1,p2,...,pi]是包括所有世界点3∗I的矩阵。S=MMT的特征值给了我们关于世界点分布的信息,在普通的三维情况下,矩阵S的秩为3,最小特征值不接近于零。在平面情况下,最小特征值变小,矩阵S的秩为2。如果世界点位于由两个坐标轴张成的平面上,所有世界点中的一个元素是一个常数,我们可以简单地忽略矩阵A的相应列而得到一个不同的解。
一般来说,这些点可以位于世界坐标系中的任意平面上。因此,我们使用S的特征向量作为旋转矩阵RS,将世界点旋转到一个新的坐标系下:
p^i=RSTpi(20)
在这里,我们可以确定坐标的常量元素,忽略矩阵A中相应的列。注意,这种转换并不改变问题的结构。SVD后得到的旋转矩阵(式19)只需旋转回原始坐标系即可:
R=RSR(21)
为了使我们的方法尽可能一般化,我们总是计算矩阵S。然后我们简单地在平面和普通情况之间切换,取决于矩阵S的秩。为了确定秩,我们使用展示秩的QR分解,并设置一个最小特征值的阈值(1e-10)。
以上是论文理论部分,接下来结合ORBSLAM3的代码,完整的ORBSLAM代码注释参考 ORB-SLAM3详细注释的代码持续更新,主要实现的函数在computePose中。
step1:要判断点的数量是否满足计算条件
因为在论文中提到,因为每个观测值会产生2个残差,所以至少需要6个点来计算公式12,所以要检验当前的点个数是否满足大于5的条件。
size_t numberCorrespondences = indices.size();
assert(numberCorrespondences > 5);
当numberCorrespondences不满足>5的条件时会发生错误(如果小于6根本进不来)。
接下来定义一个标记,表明是否在平面上。
bool planar = false;
step2:计算点的方向向量的零空间
给每个向量都开辟一个零空间:
std::vector<Eigen::MatrixXd> nullspaces(numberCorrespondences);
存储世界坐标系下空间点的矩阵,3行N列,N是numberCorrespondences,即点的总个数
points3=⎣⎡x1y1z1x2y2z2⋯⋯⋯xnynzn⎦⎤
Eigen::MatrixXd points3(3, numberCorrespondences);
空间点向量,单个空间点的齐次坐标矩阵
points3v=⎣⎡xiyizi⎦⎤,points4v=⎣⎡xiyizi1⎦⎤
points_t points3v(numberCorrespondences);
points4_t points4v(numberCorrespondences);
由于indices里保存的是提出的内点的索引值,即indices[i]是当前点坐标和向量的索引值,因此其索引并不是连续的。f为单位向量,p为点的3D坐标。
取出当前点的单位向量(方向向量)f_current ,以及点的3D坐标组合在points3中,用于接下来求解矩阵S。
利用公式7.8,SVD分解计算零空间向量:Jvr(v)=null(vT)=[rs],N=UDVT
取特征值最小的那两个对应的2个特征向量,组成零空间向量nullspaces
for (size_t i = 0; i < numberCorrespondences; i++) {
bearingVector_t f_current = f[indices[i]];
points3.col(i) = p[indices[i]];
Eigen::JacobiSVD<Eigen::MatrixXd, Eigen::HouseholderQRPreconditioner>
svd_f(f_current.transpose(), Eigen::ComputeFullV);
nullspaces[i] = svd_f.matrixV().block(0, 1, 3, 2);
points3v[i] = p[indices[i]];
}
Step 3: 通过计算S的秩来判断是在平面情况还是在正常情况
设points3=[p1,p2,...,pi]是包括所有世界点3∗I的矩阵。S=MMT,如果矩阵S的秩为3且最小特征值不接近于0,则不属于平面条件,否则需要解决一下。
Eigen::Matrix3d planarTest = points3 * points3.transpose();
Eigen::FullPivHouseholderQR<Eigen::Matrix3d> rankTest(planarTest);
特征旋转矩阵,用在平面条件下的计算
Eigen::Matrix3d eigenRot;
eigenRot.setIdentity();
使用FullPivHouseholderQR分解可得到矩阵S的秩,秩为2时,则属于平面的情况。通过计算矩阵S的特征值和特征向量,得到特征旋转矩阵,利用公式20:p^i=RSTpi,得到在新的坐标系下的世界点pi′。
if (rankTest.rank() == 2) {
planar = true;
Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> eigen_solver(planarTest);
eigenRot = eigen_solver.eigenvectors().real();
eigenRot.transposeInPlace();
for (size_t i = 0; i < numberCorrespondences; i++)
points3.col(i) = eigenRot * points3.col(i);
}
Step 4: 计算随机模型中的协方差矩阵
(没用到,跳过)
Step 5: 构造矩阵A来完成线性方程组的构建
在公式10中,得到一个线性方程组的齐次方程组,Au=0,
[drds]=[rTsT]λi−1(Rpi+t)(10)
u=[r^11,r^12,r^13,r^21,r^22,r^23,r^31,r^32,r^33,t^1,t^2,t^3]T
对单位向量v的2个零空间向量做微分,所以有[dr,ds]T,一个点有2行,N个点就有2∗N行。
const int rowsA = 2 * numberCorrespondences;
对应上面u的列数,因为旋转矩阵有9个元素,加上平移矩阵3个元素,总共12个元素
int colsA = 12;
如果世界点位于分别跨2个坐标轴的平面上,即所有世界点的一个元素是常数的时候,可简单地忽略矩阵A中对应的列,而且这不影响问题的结构本身,所以在计算公式20:pi′=RST∗pi的时候,忽略了r11,r21,r31,即第一列,对应的u只有9个元素u=[r^12,r^13,r^22,r^23,r^32,r^33,t^1,t^2,t^3]T所以A的列个数是9个。
因此在构造矩阵A的时候要分平面和非平面两种情况,平面情况忽略了第一列,而非平面则完全保留所有列。
if (planar) {
colsA = 9;
A = Eigen::MatrixXd(rowsA, 9);
} else
A = Eigen::MatrixXd(rowsA, 12);
A.setZero();
Step 6: 计算线性方程组的最小二乘解
Eigen::MatrixXd AtPA;
if (use_cov)
AtPA = A.transpose() * P * A;
else
AtPA = A.transpose() * A;
Eigen::JacobiSVD<Eigen::MatrixXd> svd_A(AtPA, Eigen::ComputeFullV);
Eigen::MatrixXd result1 = svd_A.matrixV().col(colsA - 1);
Step 7: 根据平面和非平面情况下选择最终位姿解
1.平面的情况下:
暂时只估计了旋转矩阵的第1行和第2行, 第3行等于第1行和第2行的叉积(这里应该是列,后面转置后成了行)。
tmp << 0.0, result1(0, 0), result1(1, 0),
0.0, result1(2, 0), result1(3, 0),
0.0, result1(4, 0), result1(5, 0);
tmp.col(0) = tmp.col(1).cross(tmp.col(2));
tmp.transposeInPlace();
利用Frobenious范数计算最佳的旋转矩阵,利用公式(19):R=URVRT
本质上,采用矩阵,将其元素平方,将它们加在一起并对结果平方根。计算得出的数字是矩阵的Frobenious范数,由于列向量是单列矩阵,行向量是单行矩阵,所以这些矩阵的Frobenius范数等于向量的长度(L)。
Eigen::JacobiSVD<Eigen::MatrixXd> svd_R_frob(tmp, Eigen::ComputeFullU | Eigen::ComputeFullV);
rotation_t Rout1 = svd_R_frob.matrixU() * svd_R_frob.matrixV().transpose();
如果估计出来的旋转矩阵的行列式小于0,则乘以-1(EPnP也是同样的操作)
if (Rout1.determinant() < 0)
Rout1 *= -1.0;
因为是在平面情况下计算的,估计出来的旋转矩阵是要做一个转换的,根据公式(21),R=RSR其中,RS表示特征向量的旋转矩阵
注意eigenRot之前已经转置过了transposeInPlace(),所以这里Rout1在之前也转置了,即tmp.transposeInPlace()。
Rout1 = eigenRot.transpose() * Rout1;
估计最终的平移矩阵,带尺度信息的,根据公式(17):
t=2∥r^1∥∥r^2∥t^
double scale = 1.0 / std::sqrt(std::abs(tmp.col(1).norm() * tmp.col(2).norm()));
translation_t t = scale * translation_t(result1(6, 0), result1(7, 0), result1(8, 0));
由于在刚开始对temp做了转置,这里需要还原回去。
Rout1.transposeInPlace();
这样便可以得到4种结果,即:
Ts1=[R,t],Ts2=[R,−t],Ts3=[−R,t],Ts4=[−R,−t]
接下来便是遍历这4种解,计算世界点p到切线空间v的投影的残差,对应最小的就是最好的解。
2.非平面情况下:
在非平面情况下,并没有忽略第一行:
tmp << result1(0, 0), result1(3, 0), result1(6, 0),
result1(1, 0), result1(4, 0), result1(7, 0),
result1(2, 0), result1(5, 0), result1(8, 0);
因此尺度的计算为:
t=3∥r^1∥∥r^2∥∥r^3∥t^
double scale = 1.0 /std::pow(std::abs(tmp.col(0).norm() * tmp.col(1).norm() * tmp.col(2).norm()), 1.0 / 3.0);
接下来同理,使用Frobenious计算最佳的旋转矩阵:
Eigen::JacobiSVD<Eigen::MatrixXd> svd_R_frob(tmp, Eigen::ComputeFullU | Eigen::ComputeFullV);
Rout = svd_R_frob.matrixU() * svd_R_frob.matrixV().transpose();
if (Rout.determinant() < 0)
Rout *= -1.0;
已知公式(1),从相机坐标系到世界坐标系的转换关系是:
λivi=Rpi+t那么从世界坐标系到相机坐标系的转换关系为:
RT(λivi−t)=pi因此先恢复平移部分的尺度再计算,tout=RT∗t:
tout = Rout * (scale * translation_t(result1(9, 0), result1(10, 0), result1(11, 0)));
在非平面情况下,一共有2种解,R,t和R,-t,利用前6个点计算重投影误差,选择残差最小的一个解。
Step 8: 利用高斯牛顿法对位姿进行精确求解,提高位姿解的精度
以上就选出了残差最小的旋转和平移解。下面用高斯-牛顿优化迭代改进相机姿态。具体地说,我们最小化Eq. 10中定义的正切空间残差。