极线几何与立体视觉
目标:
极限几何的约束网上有很多资料,但是大多数是理论知识和一些推导。但是在具体的应用中,**常常拿极限约束来判断相机的位姿是否准确,对于这个介绍相对较少。**本博客将介绍在一直相机的内外参数的情况下,怎么实现两个相机的极限约束。
双目视觉对应关系,同时也可用于相邻两帧间的运动估计。见下图:
配点必须在极线上
基线:左右像机光心连线(黄色线段 O 1 O 2 O_1O_2 O1O2);
极平面:空间点,两像机光心决定的平面(灰色平面 O 1 P O 2 O_1PO_2 O1PO2);
极点:基线与两摄像机图像平面的交点;
极线:极平面与图像平面交线(蓝色线段)
基于极限的几何关系,需要知道它们的参数关系如下。
两个相机的转化为 R , T R,T R,T.
如果知道两个相机的内外参数。可以计算它们的转化矩阵。假设中左边的相机为世界坐标系。它们可以表示为:
上述是假设两个相机之前的关系。其中世界坐标系为左边相机的坐标系。它们重合。因此通过 R , T R,T R,T计算得到的第二个相机的参数(它是相机位姿态,它是指的是在第一个坐标系下的 R , T R,T R,T),
具体的计算可以参考如下。
p 2 p_2 p2坐标点(原本在 O 2 O_2 O2坐标系)转化为在 O 1 O_1 O1坐标系 p 1 p_1 p1的( p 1 p_1 p1表示它在 O 1 O_1 O1坐标系下的坐标)坐标为:
p 1 = R p 2 + T = > p 2 = R T p 1 − R T T ( 1 ) p_1=Rp_2+T=>p_2=R^Tp_1-R^TT\space \space\space \space (1) p1=Rp2+T=>p2=RTp1−RTT (1)
上述可以得到,从 O 1 O_1 O1坐标系转换到 O 2 O_2 O2坐标系为:
[ R T − R T T ] [R^T \space \space -R^TT] [RT −RTT]
因此两个相互转换的矩阵为:
[ R , T ] [R,T] [R,T]:表示从 O 2 O_2 O2坐标系到 O 1 O_1 O1坐标系的转换。
[ R T − R T T ] [R^T \space \space -R^TT] [RT −RTT]:表示从 O 1 O_1 O1坐标系到 O 2 O_2 O2坐标系的转换。
最后加上相机的内参矩阵 K , K ′ K,K' K,K′可以得到整体的矩阵转换如下:
M = K [ I 0 ] , M ′ = K ′ [ R T − R T T ] M=K[I \space \space 0] ,\space \space M'=K'[R^T \space \space -R^TT] M=K[I 0], M′=K′[RT −RTT]
为了方便计算,假设相机的内参矩阵 K , K ′ K,K' K,K′为单位矩阵 I I I。因此
M = [ I 0 ] , M ′ = [ R T − R T T ] M=[I \space \space 0] ,\space \space M'=[R^T \space \space -R^TT] M=[I 0], M′=[RT −RTT]
从图二可以知道 p ′ p' p′在坐标系 O 1 O_1 O1的坐标为: R p ′ + T Rp'+T Rp′+T,因为 R p ′ + T Rp'+T Rp′+T这个点在 O 1 O_1 O1坐标系的极线 O 1 P O_1P O1P上,因为极限平面 O 1 O 2 P O_1O_2P O1O2P是平面:
T × ( R p ′ + T ) = T × ( R p ′ ) ( 2 ) T\times(Rp'+T)=T\times(Rp')\space \space\space \space (2) T×(Rp′+T)=T×(Rp′) (2)
向量 T × ( R p ′ ) T\times(Rp') T×(Rp′)表示垂直于平面 O 1 O 2 P O_1O_2P O1O2P
利用叉乘得到:
p T ⋅ [ T × ( R p ′ ) ] = 0 ( 3 ) p^T \cdot[T\times(Rp')]=0 \space \space\space \space (3) pT⋅[T×(Rp′)]=0 (3)
其中叉乘 T T T可以表示为矩阵形式,一般表示为 s k e w ( T ) skew(T) skew(T)或者 [ T × ] [T_{\times}] [T×]
[ T × ] = [ 0 − T [ 2 ] T [ 1 ] T [ 2 ] 0 − T [ 0 ] − T [ 1 ] T [ 0 ] 0 ] ( 4 ) [T_{\times}]=\begin{bmatrix} 0 & -T[2] & T[1] \\ T[2] & 0 & -T[0] \\ -T[1] & T[0] & 0 \end{bmatrix} \space \space\space \space (4) [T×]=⎣⎡0T[2]−T[1]−T[2]0T[0]T[1]−T[0]0⎦⎤ (4)
公式 ( 3 ) (3) (3)写成矩阵的形式为:
p T ⋅ [ T × ( R p ′ ) ] = 0 = > p T [ T × ] R p ′ = 0 ( 5 ) p^T \cdot[T\times(Rp')]=0=>p^T[T_{\times}]Rp'=0 \space \space\space \space (5) pT⋅[T×(Rp′)]=0=>pT[T×]Rp′=0 (5)
从公式 ( 5 ) (5) (5)得到 [ T × ] R [T_{\times}]R [T×]R为Essential Matrix,这个矩阵写成: [ T × ] R = E [T_{\times}]R=E [T×]R=E,因此:
p T E p ′ = 0 ( 6 ) p^TEp'=0 \space \space\space \space (6) pTEp′=0 (6)
直线的表达为 l ⋅ p = 0 l\cdot p=0 l⋅p=0(在齐次坐标表示下)
可以把 E p ′ Ep' Ep′看做是直线的方程, p p p看做是直线上的点,也就是说 E p ′ Ep' Ep′就是以 O 1 O_1 O1为原点坐标系中的极线了(图一 O 1 e O_1e O1e线段),反之也可。
上述都是将 K , K ′ K,K' K,K′设置为单位矩阵。当不再是单位矩阵的时候。得到矩阵为”
M = K [ I 0 ] , M ′ = K ′ [ R T − R T T ] ( 7 ) M=K[I \space \space 0] ,\space \space M'=K'[R^T \space \space -R^TT]\space \space\space \space (7) M=K[I 0], M′=K′[RT −RTT] (7)
因为 K , K ′ K,K' K,K′是相机内参数,它表示从相机坐标系转换为图像坐标系上。因此: p I = K − 1 p p_I=K^{-1}p pI=K−1p; p I ′ = K ′ − 1 p ′ p_I'=K'^{-1}p' pI′=K′−1p′
因为公式:
p T E p ′ = 0 ( 6 ) p^TEp'=0 \space \space\space \space (6) pTEp′=0 (6)
将 p I = K − 1 p p_I=K^{-1}p pI=K−1p; p I ′ = K ′ − 1 p ′ p_I'=K'^{-1}p' pI′=K′−1p′ 带入 ( 6 ) (6) (6)得到:
p T K − T [ T × ] R K ′ − 1 p ′ = 0 ( 8 ) p^TK^{-T}[T_{\times}]RK'^{-1}p'=0 \space \space\space \space (8) pTK−T[T×]RK′−1p′=0 (8)
将 F = K − T [ T × ] R K ′ − 1 F=K^{-T}[T_{\times}]RK'^{-1} F=K−T[T×]RK′−1,这就是Fundamental Matrix了。也是opencv里面计算的Fundametal矩阵
上述都是将第一个相机设置为和世界坐标系一致。但是在一直两个相机的情况下。可能需要计算 R , T R,T R,T矩阵。它们是:表示从 O 2 O_2 O2坐标系到 O 1 O_1 O1坐标系的转换。
已知两个相机(都是相机的外参)为:
M 1 = [ R 1 T 1 0 1 ] M 2 = [ R 2 T 2 0 1 ] M_1=\begin{bmatrix} R_1 & T_1 \\ 0 & 1 \end{bmatrix} \\ \\ \\ M_2=\begin{bmatrix} R_2 & T_2 \\ 0 & 1 \end{bmatrix} M1=[R10T11]M2=[R20T21]
需要计算从 O 2 O_2 O2坐标系到 O 1 O_1 O1坐标系的转换的矩阵 R , T R,T R,T
推导:
假设两个对应点在各自的相机坐标系为 p 1 p_1 p1, p 2 p_2 p2
p 1 p_1 p1在世界坐标系的顶点为
p w = M 1 p 1 ( 9 ) p_w=M_1p_1 \space \space\space \space (9) pw=M1p1 (9)
p 2 p_2 p2在世界坐标系的顶点为
p w = M 2 p 2 ( 10 ) p_w=M_2p_2 \space \space\space \space (10) pw=M2p2 (10)
( 9 ) ( 10 ) (9)(10) (9)(10)公式得到:
p 1 = M 1 − 1 M 2 p 2 p_1=M^{-1}_1M_2p_2 p1=M1−1M2p2
其中 R , T R,T R,T为 M 1 − 1 M 2 M^{-1}_1M_2 M1−1M2旋转矩阵和平移向量。
代码如下:
void RunTwoImagesCorrspondEpilinesWithPath(int src_idx, int tgt_idx, const std::string& dir)
{
std::cerr << "the cams num: " << cams_modes_.size() << std::endl;
if (src_idx < 0 || tgt_idx < 0 ||
src_idx >= cams_modes_.size() || tgt_idx >= cams_modes_.size())
{
std::cerr << "RunTwoImagesCorrespondEpilines invalid idx..." << std::endl;
return;
}
if (!cams_modes_[src_idx].valid_ || !cams_modes_[tgt_idx].valid_)
{
std::cerr << "RunTwoImagesCorrespondEpilines invalid cams existed..." << std::endl;
return;
}
CameraModel src_cam_model = cams_modes_[src_idx];
CameraModel tgt_cam_model = cams_modes_[tgt_idx];
std::string src_name = images_files_path_[src_idx];
std::string tgt_name = images_files_path_[tgt_idx];
std::cerr << "src_name: " << src_name << std::endl;
std::cerr << "tgt_name: " << tgt_name << std::endl;
std::string src_file_name = src_name.substr(src_name.find_last_of("/") + 1, src_name.find_last_of(".") - src_name.find_last_of("/") - 1);
std::string tgt_file_name = tgt_name.substr(tgt_name.find_last_of("/") + 1, tgt_name.find_last_of(".") - tgt_name.find_last_of("/") - 1);
cv::Mat src_rgb = cv::imread(src_name);
cv::Mat tgt_rgb = cv::imread(tgt_name);
//cv::imshow("tgt_rgb", tgt_rgb);
//cv::waitKey(0);
// used in OpenCV3
cv::Ptr<cv::FeatureDetector> detector = cv::ORB::create();
cv::Ptr<cv::DescriptorExtractor> descriptor = cv::ORB::create();
std::vector< cv::KeyPoint > src_kp, tgt_kp;
detector->detect(src_rgb, src_kp);
detector->detect(tgt_rgb, tgt_kp);
// ¼ÆËãÃèÊö×Ó
cv::Mat src_desp, tgt_desp;
descriptor->compute(src_rgb, src_kp, src_desp);
descriptor->compute(tgt_rgb, tgt_kp, tgt_desp);
// Æ¥ÅäÃèÊö×Ó
std::vector< cv::DMatch > matches;
cv::BFMatcher matcher;
matcher.match(src_desp, tgt_desp, matches);
cout << "Find total " << matches.size() << " matches." << endl;
if (matches.size() < match_pnts_num_)
{
std::cerr << "match num is too much fewer..." << std::endl;
return;
}
// ɸѡƥÅä¶Ô
std:vector< cv::DMatch > goodMatches;
double minDis = 9999;
for (size_t i = 0; i < matches.size(); i++)
{
if (matches[i].distance < minDis)
minDis = matches[i].distance;
}
for (size_t i = 0; i < matches.size(); i++)
{
if (matches[i].distance < 10 * minDis)
goodMatches.push_back(matches[i]);
}
std::vector< cv::Point2f > src_pts, tgt_pts;
for (size_t i = 0; i<goodMatches.size(); i++)
{
src_pts.push_back(src_kp[goodMatches[i].queryIdx].pt);
tgt_pts.push_back(tgt_kp[goodMatches[i].trainIdx].pt);
}
//通过两个相机的位姿计算它们之间的e_matrix
Eigen::Matrix4f src_pose = src_cam_model.cam_pose_;
Eigen::Matrix4f tgt_pose = tgt_cam_model.cam_pose_;
Eigen::Matrix4f src_extr = src_pose.inverse();
Eigen::Matrix4f tgt_extr = tgt_pose.inverse();
Eigen::Matrix4f relative_extr = tgt_extr*src_pose;
Eigen::Matrix3f r_extr = relative_extr.topLeftCorner(3, 3);
Eigen::Vector3f t_extr = relative_extr.topRightCorner(3, 1);
//
Eigen::Matrix3f em = ComputeCrossMatrixFromLVector(t_extr)*r_extr;
//已知e_matrix,计算fundamental_matrix
Eigen::Matrix3f src_km = Eigen::Matrix3f::Identity();
Eigen::Matrix3f tgt_km = Eigen::Matrix3f::Identity();
src_km(0, 0) = src_cam_model.fx_;
src_km(1, 1) = src_cam_model.fy_;
src_km(0, 2) = src_cam_model.cx_;
src_km(1, 2) = src_cam_model.cy_;
tgt_km(0, 0) = src_cam_model.fx_;
tgt_km(1, 1) = src_cam_model.fy_;
tgt_km(0, 2) = src_cam_model.cx_;
tgt_km(1, 2) = src_cam_model.cy_;
Eigen::Matrix3f fm = (tgt_km.inverse()).transpose()*em*src_km.inverse();
std::cerr << "fm: \n" << fm << std::endl;
Eigen::Matrix3f fmt = fm;
//²âÊÔ
cv::Mat fundm = (cv::Mat_<float>(3, 3)
<< fmt(0, 0), fmt(0, 1), fmt(0, 2),
fmt(1, 0), fmt(1, 1), fmt(1, 2),
fmt(2, 0), fmt(2, 1), fmt(2, 2));
std::cerr << fundm << std::endl;
std::vector<cv::Vec<float, 3>> epilines1, epilines2;
computeCorrespondEpilines(src_pts, 1, fundm, epilines1);
computeCorrespondEpilines(tgt_pts, 2, fundm, epilines2);
cv::RNG &rng = cv::theRNG();
for (int i = 0; i < match_pnts_num_; ++i) {
//Ëæ»ú²úÉúÑÕÉ«
std::cerr << "epilines1: " << i << ": " << epilines1[i] << std::endl;
std::cerr << "epilines2: " << i << ": " << epilines2[i] << std::endl;
cv::Scalar color = cv::Scalar(rng(255), rng(255), rng(255));
circle(src_rgb, src_pts[i], 5, color, 3);
//»æÖÆÍ⼫ÏßµÄʱºò£¬Ñ¡ÔñÁ½¸öµã£¬Ò»¸öÊÇx=0´¦µÄµã£¬Ò»¸öÊÇxΪͼƬ¿í¶È´¦
line(src_rgb, cv::Point(0, -epilines2[i][2] / epilines2[i][1]), cv::Point(src_rgb.cols, -(epilines2[i][2] + epilines2[i][0] * src_rgb.cols) / epilines2[i][1]), color);
circle(tgt_rgb, tgt_pts[i], 5, color, 3);
line(tgt_rgb, cv::Point(0, -epilines1[i][2] / epilines1[i][1]), cv::Point(tgt_rgb.cols, -(epilines1[i][2] + epilines1[i][0] * tgt_rgb.cols) / epilines1[i][1]), color);
}
std::string abs_src_file_name = dir + "/" + src_file_name + ".png";
std::string abs_tgt_file_name = dir + "/" + tgt_file_name + ".png";
std::cerr << "abs_src_file_name: " << abs_src_file_name << std::endl;
std::cerr << "abs_tgt_file_name: " << abs_tgt_file_name << std::endl;
//std::system("pause");
//cv::imshow("epiline1", tgt_rgb);
//cv::waitKey(0);
//std::cerr << "tgt_rgb rows, cols: " << tgt_rgb.rows << ", " << tgt_rgb.cols << std::endl;
//std::system("pause");
cv::imwrite(abs_tgt_file_name, tgt_rgb);
//std::system("pause");
//cv::imshow("epiline2", src_rgb);
cv::imwrite(abs_src_file_name, src_rgb);
cv::waitKey(0);
}
测试的结果如下: