极线几何与立体视觉
目标:
极限几何的约束网上有很多资料,但是大多数是理论知识和一些推导。但是在具体的应用中,**常常拿极限约束来判断相机的位姿是否准确,对于这个介绍相对较少。**本博客将介绍在一直相机的内外参数的情况下,怎么实现两个相机的极限约束。
双目视觉对应关系,同时也可用于相邻两帧间的运动估计。见下图:
配点必须在极线上
基线:左右像机光心连线(黄色线段
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}]=[0−T[2]T[1]T[2]0−T[0]−T[1]T[0]0] \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=[R1T101] \\ \\ \\ M_2=[R2T201]
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);
}
测试的结果如下: