之前在做立体匹配的时候,一直使用opencv3的stereoRectify函数,不过后来出了点问题,也一直没法找到原因,最后决定阅读经典论文Fusiello A, Trucco E, Verri A. A compact algorithm for rectification of stereo pairs[J]. Machine Vision & Applications, 2000, 12(1):16-22.
使用该论文里所介绍的极线矫正算法,该论文还提供了相应的Matlab代码,由于编程需要使用C++,所以在此记录下其C++代码
变量说明
根据相机针孔投影模型
如图所示, 为相机中心 , 是世界座标系下的空间点, 是 在图像平面上经过针孔投影后所得到的点(像点)
现有 的齐次座标为 像点 的齐次座标为 , 则可以定义 为投影矩阵 , 是相机内参数矩阵, 是相机外参数矩阵,则投影变换关系如下:
其中 是相机内参数矩阵
矩阵
若定义相机中心为 , 的齐次座标为 ,则有
若定义 为深度值,则有世界座标与像点座标的关系为 ,该式子可以理解为从相机中心出发,沿着相机中心至 像点的射线上距离 处的某一点即为物方空间点
极线矫正
首先,在进行极限矫正之前,先确定我们的已知条件:①相机已经过标定,相机内参数矩阵 与 是已知的。②原相机的外方位元素是已知的,即我们已知相机的投影矩阵 与 。已知以上 条件后,我们即将下图进行极线矫正
如上图可以知道,经过极线矫正后,相机中心座标并未发生改变,即①矫正后的新的相机中心座标 未发生改变。②矫正后新的左右相机内参数矩阵 是相同的(此时的内参数矩阵可任意选取,只要相同就行)。 ③矫正后的左右相机的旋转方向都是相同的。
因此可以将新的左右相机看作只沿着相机基线X轴平移了某个长度而已,他们的旋转朝向都是相同的,都假设为
因此可以根据以上矫正条件,写出新的矫正后的新的左右投影矩阵 与 ,有如下:
其中,矫正后的相机内参数矩阵 可任意选取, 与矫正前的 相同,因此,需要求解的仅仅只有一个参数 而已, 那么根据以上推论, 可作以下假设:
首先将新投影矩阵中的外方位元素的旋转矩阵 写为列向量形式:
三个分量 , , 分别对应相机参考座标系的 , , 轴
根据以上推论得到以下三个条件:
①新的 轴应该与基线向量baseline平行:即
②新的 轴应该与 轴垂直,且同时垂直于 轴,该 轴论文原文里选择了矫正前座标系的 轴的单位向量,因此有:
③ 新的 轴 应该同时垂直于新的 轴与 轴, 因此有
完成,由此便可得到新的投影矩阵 与 了。
下一步,就该计算由矫正前至矫正后的映射矩阵了
由前文知 ,则有:
因此得到矫正后的像点座标与矫正前的像点座标之间的映射关系为
因此有映射矩阵
对于右相机同理:
右相机映射矩阵
C++代码
原文给出了Matlab代码,若需要查看原文代码的可以直接下载原文查看,这里就是直接将MATLAB的代码转化为了C++的而已,矩阵等运算都是基于OpenCV3的
#include <iostream>
#include <vector>
#include <string>
#include <opencv2/opencv.hpp>
using namespace std;
void art(const cv::Mat &P , cv::Mat &A , cv::Mat &R , cv::Mat &t){
cv::Mat t_temp;
cv::decomposeProjectionMatrix(P ,A , R , t_temp); // 得到的t 是一个四维向量 , 是指相机所在的位置的3D齐次座标, 需要将其化为非齐次座标并乘以一个-R转化为平移向量
t = cv::Mat::zeros(3 , 1 , CV_64F);
t.row(0) = t_temp.row(0) / t_temp.row(3);
t.row(1) = t_temp.row(1) / t_temp.row(3);
t.row(2) = t_temp.row(2) / t_temp.row(3);
t = -R * t ;
}
void my_stereoRectify(const cv::Mat &P_o1,
const cv::Mat &P_o2,
cv::Mat &T1,
cv::Mat &T2,
cv::Mat &P_n1,
cv::Mat &P_n2)
{
cv::Mat A1 , R1 , t1;
cv::Mat A2 , R2 , t2;
art(P_o1 , A1 , R1 , t1);
art(P_o2 , A2 , R2 , t2);
// 相机中心c1 , c2
cv::Mat c1 = cv::Mat::zeros(3 , 1 , CV_64F);
cv::Mat c2 = cv::Mat::zeros(3 , 1 , CV_64F);
c1 = - R1.inv() * t1;
c2 = - R2.inv() * t2;
cv::Mat v1 = cv::Mat::zeros(3 , 1 , CV_64F);
cv::Mat v2 = cv::Mat::zeros(3 , 1 , CV_64F);
cv::Mat v3 = cv::Mat::zeros(3 , 1 , CV_64F);
// 新的x轴new x axis (baseline , from c1 to c2)
v1 = c2 - c1 ;
v2 = R1.row(2).t().cross(v1);
v3 = v1.cross(v2);
//新的外方位元素中的R
cv::Mat R = cv::Mat::eye(3 , 3 , CV_64F);
R.row(0) = v1.t() / cv::norm(v1);
R.row(1) = v2.t() / cv::norm(v2);
R.row(2) = v3.t() / cv::norm(v3);
// 新的内方位元素
cv::Mat A_n1 = cv::Mat::eye(3 , 3 , CV_64F);
cv::Mat A_n2 = cv::Mat::eye(3 , 3 , CV_64F);
A_n1 = A2;
A_n1.at<double>(0 , 1) = 0;
A_n2 = A2;
A_n2.at<double>(0 , 1) = 0;
A_n1.at<double>(0 , 2) = A_n1.at<double>(0 , 2);
A_n1.at<double>(1 , 2) = A_n1.at<double>(1 , 2);
A_n2.at<double>(0 , 2) = A_n2.at<double>(0 , 2);
A_n2.at<double>(1 , 2) = A_n2.at<double>(1 , 2);
// 新的投影矩阵
cv::Mat t1_new = cv::Mat::zeros(3 , 1 , CV_64F);
cv::Mat t2_new = cv::Mat::zeros(3 , 1 , CV_64F);
t1_new = -R * c1;
t2_new = -R * c2;
P_n1.create(3 , 4 , CV_64F);
P_n2.create(3 , 4 , CV_64F);
R.convertTo(P_n1.colRange(0 , 3) , CV_64F);
R.convertTo(P_n2.colRange(0 , 3) , CV_64F);
t1_new.convertTo(P_n1.col(3) , CV_64F);
t2_new.convertTo(P_n2.col(3) , CV_64F);
//cout << "A_n1 = " << A_n1 << "\nA_n2 = " << A_n2 << endl;
cout << "P_n1 = " << P_n1 << "\nP_n2 = " << P_n2 << endl;
P_n1 = A_n1 * P_n1;
P_n2 = A_n2 * P_n2;
// 立体矫正的变换矩阵
T1.create(3 , 3 , CV_64F);
T2.create(3 , 3 , CV_64F);
T1 = P_n1.colRange(0 , 3) * P_o1.colRange(0 , 3).inv();
T2 = P_n2.colRange(0 , 3) * P_o2.colRange(0 , 3).inv();
}
测试
本人使用了Middleburry网站的temple数据,temple数据里明确的给出了每一张相片的内外参数矩阵,可直接将其参数拿来使用
矫正前:
矫正后:
结束