https://zhuanlan.zhihu.com/p/141799551
在机器人SLAM、自动驾驶中经常会遇到一个问题:如何通过相机拍摄的一组画面反推出相机在真实世界中的运动轨迹。这就是典型的视觉里程计问题。
一般来说,对于通过单相机拍摄的画面估计运动轨迹,需要用到对极几何知识。所以本文主要分析如何通过两张图对极几何估计相机运动,也是对《视觉SLAM十四讲》中视觉里程计章节中没有说的地方进行补充。
一、基础知识
在讨论对极几何之前,先来看一些向量和矩阵的基础知识。
- 内积(点乘)
有向量 和 ,他们的內积为:
显然当 和 互相垂直时 。
- 外积(叉乘)
与内积 结果是一个数值不同,外积 的结果是一个向量。
假设 和 向量所在平面 ,那么从几何上看 是一个垂直于 平面的向量。
写成坐标形式:
由于 垂直于 和 ,那么必然有:
将向量叉乘写成矩阵乘以坐标点形式:
已知 ,定义叉乘矩阵 为:
这个矩阵之后会用到。
- 反对称矩阵
若 满足 关系,则 为反对称矩阵(其中 代表矩阵转置)。
显然上面的叉乘矩阵 是反对称矩阵。
此处不经证明给出,对于任意3x3实反对称矩阵 都可以表示为:
其中 是3x3正交矩阵。这个结论很重要,一会要用到。
二、对极几何(本质矩阵和基础矩阵)
在之前相机模型文章中有分析到,从相机坐标系变换为像素坐标系的公式为:
其中相机内参矩阵 是已知的(预先标定好)。
世界中有一点 ,相机 拍摄 并在成像平面行形成像素点 。由于相机在成像过程中丢失了深度信息,所以我们无法知道 的深度(即 )。
那么问题来了,当相机 移动到另一个外置 再拍一张图时如何?
假设相机 拍摄 在成像平面形成对应的像素点 ,此时我们只能知道 在 射线上(但是无法确定具体位置);当相机移动到 后再去拍摄 生成新的像素点 ,如果图像通过特征点匹配确定 和 是同一点,那么这时 的位置也就确定了。
-
本质矩阵
需要特别说明,这里的 是坐标系 下的向量;而 是坐标系 下的向量。
现在我们要将所有的向量和坐标统一在同一个坐标系内,那么在 坐标系下 为:
其中 是3x3旋转矩阵, 是3x1平移向量。
由于 、 和 共面,所以他们的混合积为0:
参考前面点积
因为三线共面,o'o和oP的点积垂直平面,故和平面内的o'p在相乘为0
那么在 坐标系下,可以表示为:
由于 (只是一条线 ,没有面)
这里的 就是对极几何中的本质矩阵(essential matrix)。
特别注意 是由反对称矩阵 和正交矩阵 相乘获得,这是一个非常重要的性质!
- 基础矩阵
进一步,又由相机模型可以得到到像素坐标的转换关系:
带入后:
等式右边为0,直接消去常数 和 :
整理得:
这里的 就是基础矩阵(fundamental matrix),基础矩阵实际与本质矩阵只是相差了相机内参 。
三、本质矩阵求解相机运动
在实际中我们可以通过特征点匹配(如ORBSLAM2使用ORB特征)计算出相机运动前后两幅视图中较多组匹配特征点坐标 与 ,另外相机内参矩阵 已知。
通过对极几何恢复相机运动的过程为:特征点匹配 → 计算本质矩阵 → 恢复相机运动。
- 计算本质矩阵
以OpenCV cv::findEssentialMat函数计算 为例,具体代码如下:
// _points1对应p,_points2对应p', _cameraMatrix是相机内参矩阵K cv::Mat cv::findEssentialMat( InputArray _points1, InputArray _points2, InputArray _cameraMatrix, int method, double prob, double threshold, OutputArray _mask) { CV_INSTRUMENT_REGION(); // 将匹配的特征点和相机矩阵等数据转化为浮点型 Mat points1, points2, cameraMatrix; _points1.getMat().convertTo(points1, CV_64F); _points2.getMat().convertTo(points2, CV_64F); _cameraMatrix.getMat().convertTo(cameraMatrix, CV_64F); int npoints = points1.checkVector(2); CV_Assert( npoints >= 0 && points2.checkVector(2) == npoints && points1.type() == points2.type()); CV_Assert(cameraMatrix.rows == 3 && cameraMatrix.cols == 3 && cameraMatrix.channels() == 1); if (points1.channels() > 1) { points1 = points1.reshape(1, npoints); points2 = points2.reshape(1, npoints); } double fx = cameraMatrix.at<double>(0,0); double fy = cameraMatrix.at<double>(1,1); double cx = cameraMatrix.at<double>(0,2); double cy = cameraMatrix.at<double>(1,2); // 按照公式(19)和公式(20)计算 E points1.col(0) = (points1.col(0) - cx) / fx; points2.col(0) = (points2.col(0) - cx) / fx; points1.col(1) = (points1.col(1) - cy) / fy; points2.col(1) = (points2.col(1) - cy) / fy; // Reshape data to fit opencv ransac function points1 = points1.reshape(2, npoints); points2 = points2.reshape(2, npoints); threshold /= (fx+fy)/2; Mat E; if( method == RANSAC ) createRANSACPointSetRegistrator(makePtr<EMEstimatorCallback>(), 5, threshold, prob)->run(points1, points2, E, _mask); else createLMeDSPointSetRegistrator(makePtr<EMEstimatorCallback>(), 5, prob)->run(points1, points2, E, _mask); return E; }
以本质矩阵 的定义公式 来看,两边乘以 ,得:
化简得
再回头来看公式 :
其中 且 ,带表相机主点位置。这时明显可以看到findEssentialMat代码通过上述过程计算本质矩阵 。
- 奇异值分解 计算相机运动
对 进行初等行变换:
这里的 是一个正交矩阵( )。通过上述初等行变换可以得到:
那么通过公式 改写 为:
同理通过公式 改写 为:
观察公式 和 ,这就是典型的奇异值分解(SVD):
其中 和 都是正交矩阵, 且 。同时可以得到结论:
一个矩阵是本质矩阵的充要条件是其奇异值中有两个相等且第三个是0。
那么看到这儿应该比较清晰了,通过两幅图对极几何计算相机运动的方法是:
- 特征点匹配计算 和
- 通过对极约束 计算本质量矩阵
- 用奇异值分解 计算 和 ,从而得到旋转矩阵 和平移向量
- 相机运动 和 的四个解
已知 并进行奇异值分解求得 和 之后,那么根据公式 和 可以得到旋转矩阵的两个解:
接着可以得到:
对于对极约束 ,两边乘以 等式依然成立;对本质矩阵的数值解来说, 和 只是相差一个负号;但是对于平移向量, 和 代表方向相反。
综合考虑,求解得到的 和 共有4种如下可能位置:
由于真实物理条件限定,两个相机和被拍摄点在同一侧,且被拍摄点在相机前方,所以只有上图中(a)是符合真实的解。
在OpenCV中cv::recoverPose函数帮我们做这一堆事情:
int cv::recoverPose( InputArray E, InputArray _points1, InputArray _points2, InputArray _cameraMatrix, OutputArray _R, OutputArray _t, double distanceThresh, InputOutputArray _mask, OutputArray triangulatedPoints)
输入本质矩阵、匹配特征点 _points1 + _points2、相机矩阵 _cameraMatrix,输出旋转矩阵 _R和平移向量 _t 。代码太长就不列出来了。
四、若干问题
- 对极几何不能求解纯旋转问题
当相机无平移且只有旋转时 为全 矩阵,等式两边都等于 显然会导致无法求解 。反过来当相机只有平移且无旋转时 ,并不影响 的求解,这时问题退化成双目测距。
- 对极几何“结构性”恢复运动
对于对极约束 ,在等式两边乘以任意不为 的常数 都成立:
换句话说,如果 是 的解,那么 也都是 的解。这就是说,即使求解出平移向量 ,我们也无法知道 的单位具体是米、厘米还是毫米。所以计算出出的 只是恢复移动的“结构性”,并不是真实值。