2021SC@SDUSC
TwoViewReconstruction.cc
Triangulate函数;三角化恢复三维点
void TwoViewReconstruction::Triangulate(const cv::KeyPoint &kp1, const cv::KeyPoint &kp2, const cv::Mat &P1, const cv::Mat &P2, cv::Mat &x3D)
{
cv::Mat A(4, 4, CV_32F);
// x = a*P*X, 左右两面乘Pc的反对称矩阵 a*[x]^ * P *X = 0 构成了A矩阵,中间涉及一个尺度a,因为都是归一化平面,但右面是0所以直接可以约掉不影响最后的尺度
// 0 -1 v P(0) -P.row(1) + v*P.row(2)
// 1 0 -u * P(1) = P.row(0) - u*P.row(2)
// -v u 0 P(2) u*P.row(1) - v*P.row(0)
// 发现上述矩阵线性相关,所以取前两维,两个点构成了4行的矩阵,就是如下的操作,求出的是4维的结果[X,Y,Z,A],所以需要除以最后一维使之为1,就成了[X,Y,Z,1]这种齐次形式
A.row(0) = kp1.pt.x * P1.row(2) - P1.row(0);
A.row(1) = kp1.pt.y * P1.row(2) - P1.row(1);
A.row(2) = kp2.pt.x * P2.row(2) - P2.row(0);
A.row(3) = kp2.pt.y * P2.row(2) - P2.row(1);
cv::Mat u, w, vt;
cv::SVD::compute(A, w, u, vt, cv::SVD::MODIFY_A | cv::SVD::FULL_UV);
x3D = vt.row(3).t();
x3D = x3D.rowRange(0, 3) / x3D.at<float>(3);
}
Normalize函数,像素坐标标准化,计算点集的横纵均值,与均值偏差的均值。最后返回变化矩阵T 直接乘以像素坐标的齐次向量即可获得去中心去均值后的特征点坐标
void TwoViewReconstruction::Normalize(const vector<cv::KeyPoint> &vKeys, vector<cv::Point2f> &vNormalizedPoints, cv::Mat &T)
{
float meanX = 0;
float meanY = 0;
const int N = vKeys.size();
vNormalizedPoints.resize(N);
for (int i = 0; i < N; i++)
{
meanX += vKeys[i].pt.x;
meanY += vKeys[i].pt.y;
}
// 1. 求均值
meanX = meanX / N;
meanY = meanY / N;
float meanDevX = 0;
float meanDevY = 0;
for (int i = 0; i < N; i++)
{
vNormalizedPoints[i].x = vKeys[i].pt.x - meanX;
vNormalizedPoints[i].y = vKeys[i].pt.y - meanY;
meanDevX += fabs(vNormalizedPoints[i].x);
meanDevY += fabs(vNormalizedPoints[i].y);
}
// 2. 确定新原点后计算与新原点的距离均值
meanDevX = meanDevX / N;
meanDevY = meanDevY / N;
// 3. 去均值化
float sX = 1.0 / meanDevX;
float sY = 1.0 / meanDevY;
for (int i = 0; i < N; i++)
{
vNormalizedPoints[i].x = vNormalizedPoints[i].x * sX;
vNormalizedPoints[i].y = vNormalizedPoints[i].y * sY;
}
// 4. 计算变化矩阵
T = cv::Mat::eye(3, 3, CV_32F);
T.at<float>(0, 0) = sX;
T.at<float>(1, 1) = sY;
T.at<float>(0, 2) = -meanX * sX;
T.at<float>(1, 2) = -meanY * sY;
}
CheckRT函数进行cheirality check,从而进一步找出F分解后最合适的解。
int TwoViewReconstruction::CheckRT(const cv::Mat &R, const cv::Mat &t, const vector<cv::KeyPoint> &vKeys1, const vector<cv::KeyPoint> &vKeys2,
const vector<Match> &vMatches12, vector<bool> &vbMatchesInliers,
const cv::Mat &K, vector<cv::Point3f> &vP3D, float th2, vector<bool> &vbGood, float ¶llax)
{
// Calibration parameters
const float fx = K.at<float>(0, 0);
const float fy = K.at<float>(1, 1);
const float cx = K.at<float>(0, 2);
const float cy = K.at<float>(1, 2);
vbGood = vector<bool>(vKeys1.size(), false);
vP3D.resize(vKeys1.size());
vector<float> vCosParallax;
vCosParallax.reserve(vKeys1.size());
// Camera 1 Projection Matrix K[I|0]
// 步骤1:得到一个相机的投影矩阵
// 以第一个相机的光心作为世界坐标系
cv::Mat P1(3, 4, CV_32F, cv::Scalar(0));
K.copyTo(P1.rowRange(0, 3).colRange(0, 3));
cv::Mat O1 = cv::Mat::zeros(3, 1, CV_32F);
// Camera 2 Projection Matrix K[R|t]
// 步骤2:得到第二个相机的投影矩阵
cv::Mat P2(3, 4, CV_32F);
R.copyTo(P2.rowRange(0, 3).colRange(0, 3));
t.copyTo(P2.rowRange(0, 3).col(3));
P2 = K * P2;
// 第二个相机的光心在世界坐标系下的坐标
cv::Mat O2 = -R.t() * t;
int nGood = 0;
for (size_t i = 0, iend = vMatches12.size(); i < iend; i++)
{
if (!vbMatchesInliers[i])
continue;
// kp1和kp2是匹配特征点
const cv::KeyPoint &kp1 = vKeys1[vMatches12[i].first];
const cv::KeyPoint &kp2 = vKeys2[vMatches12[i].second];
cv::Mat p3dC1;
// 步骤3:利用三角法恢复三维点p3dC1
Triangulate(kp1, kp2, P1, P2, p3dC1);
if (!isfinite(p3dC1.at<float>(0)) || !isfinite(p3dC1.at<float>(1)) || !isfinite(p3dC1.at<float>(2)))
{
vbGood[vMatches12[i].first] = false;
continue;
}
// Check parallax
// 步骤4:计算视差角余弦值
cv::Mat normal1 = p3dC1 - O1;
float dist1 = cv::norm(normal1);
cv::Mat normal2 = p3dC1 - O2;
float dist2 = cv::norm(normal2);
float cosParallax = normal1.dot(normal2) / (dist1 * dist2);
// 步骤5:判断3D点是否在两个摄像头前方
// Check depth in front of first camera (only if enough parallax, as "infinite" points can easily go to negative depth)
// 步骤5.1:3D点深度为负,在第一个摄像头后方,淘汰
if (p3dC1.at<float>(2) <= 0 && cosParallax < 0.99998)
continue;
// Check depth in front of second camera (only if enough parallax, as "infinite" points can easily go to negative depth)
// 步骤5.2:3D点深度为负,在第二个摄像头后方,淘汰
cv::Mat p3dC2 = R * p3dC1 + t;
if (p3dC2.at<float>(2) <= 0 && cosParallax < 0.99998)
continue;
// 步骤6:计算重投影误差
// Check reprojection error in first image
// 计算3D点在第一个图像上的投影误差
float im1x, im1y;
float invZ1 = 1.0 / p3dC1.at<float>(2);
im1x = fx * p3dC1.at<float>(0) * invZ1 + cx;
im1y = fy * p3dC1.at<float>(1) * invZ1 + cy;
float squareError1 = (im1x - kp1.pt.x) * (im1x - kp1.pt.x) + (im1y - kp1.pt.y) * (im1y - kp1.pt.y);
// 步骤6.1:重投影误差太大,跳过淘汰
// 一般视差角比较小时重投影误差比较大
if (squareError1 > th2)
continue;
// Check reprojection error in second image
// 计算3D点在第二个图像上的投影误差
float im2x, im2y;
float invZ2 = 1.0 / p3dC2.at<float>(2);
im2x = fx * p3dC2.at<float>(0) * invZ2 + cx;
im2y = fy * p3dC2.at<float>(1) * invZ2 + cy;
float squareError2 = (im2x - kp2.pt.x) * (im2x - kp2.pt.x) + (im2y - kp2.pt.y) * (im2y - kp2.pt.y);
// 步骤6.2:重投影误差太大,跳过淘汰
// 一般视差角比较小时重投影误差比较大
if (squareError2 > th2)
continue;
// 步骤7:统计经过检验的3D点个数,记录3D点视差角
vCosParallax.push_back(cosParallax);
vP3D[vMatches12[i].first] = cv::Point3f(p3dC1.at<float>(0), p3dC1.at<float>(1), p3dC1.at<float>(2));
nGood++;
if (cosParallax < 0.99998)
vbGood[vMatches12[i].first] = true;
}
// 7 得到3D点中较小的视差角,并且转换成为角度制表示
if (nGood > 0)
{
// 从小到大排序,注意vCosParallax值越大,视差越小
sort(vCosParallax.begin(), vCosParallax.end());
// !排序后并没有取最小的视差角,而是取一个较小的视差角
// 作者的做法:如果经过检验过后的有效3D点小于50个,那么就取最后那个最小的视差角(cos值最大)
// 如果大于50个,就取排名第50个的较小的视差角即可,为了避免3D点太多时出现太小的视差角
size_t idx = min(50, int(vCosParallax.size() - 1));
//将这个选中的角弧度制转换为角度制
parallax = acos(vCosParallax[idx]) * 180 / CV_PI;
}
else
parallax = 0;
return nGood;
}
DecomposeE函数;分解Essential矩阵,
F矩阵通过结合内参可以得到Essential矩阵,分解E矩阵将得到4组解
这4组解分别为[R1,t],[R1,-t],[R2,t],[R2,-t]
## 反对称矩阵性质
多视图几何上定义:一个3×3矩阵是本质矩阵的充要条件是它的奇异值中有两个相等而第三个是0
首先,E=[t]×R=SR其中S为反对称矩阵
结论1:如果 S 是实的反对称矩阵,那么S=UBU^T,其中 B 为形如diag(a1Z,a2Z...amZ,0,0...0)的分块对角阵,其中 Z = [0, 1; -1, 0]
反对称矩阵的特征矢量都是纯虚数并且奇数阶的反对称矩阵必是奇异的
那么根据这个结论我们可以将 S 矩阵写成 S=kUZU^⊤,而 Z 为
| 0, 1, 0 |
|-1, 0, 0 |
| 0, 0, 0 |
Z = diag(1, 1, 0) * W W 为
| 0,-1, 0 |
| 1, 0, 0 |
| 0, 0, 1 |
E=SR=Udiag(1,1,0)(WU^⊤R) 这样就证明了 E 拥有两个相等的奇异值
## 恢复相机矩阵
假定第一个摄像机矩阵是P=[I∣0],为了计算第二个摄像机矩阵P′,必须把 E 矩阵分解为反对成举着和旋转矩阵的乘积 SR。
还是根据上面的结论1,我们在相差一个常数因子的前提下有 S=UZU^T,我们假设旋转矩阵分解为UXV^T,注意这里是假设旋转矩阵分解形式为UXV^T,并不是旋转矩阵的svd分解,
其中 UV都是E矩阵分解出的
Udiag(1,1,0)V^T = E = SR = (UZU^T)(UXV^⊤) = U(ZX)V^T
则有 ZX = diag(1,1,0),因此 x=W或者 X=W^T
结论:如果 E 的SVD分解为 Udiag(1,1,0)V^⊤,E = SR有两种分解形式,分别是: S = UZU^⊤ R = UWVTor UW^TV^⊤
接着分析,又因为St=0(自己和自己叉乘肯定为0嘛)以及∥t∥=1(对两个摄像机矩阵的基线的一种常用归一化),因此 t = U(0,0,1)^T = u3,
即矩阵 U 的最后一列,这样的好处是不用再去求S了,应为t的符号不确定,R矩阵有两种可能,因此其分解有如下四种情况:
P′=[UWV^T ∣ +u3] or [UWV^T ∣ −u3] or [UW^TV^T ∣ +u3] or [UW^TV^T ∣ −u3]
void TwoViewReconstruction::DecomposeE(const cv::Mat &E, cv::Mat &R1, cv::Mat &R2, cv::Mat &t)
{
cv::Mat u, w, vt;
cv::SVD::compute(E, w, u, vt);
// 对 t 有归一化,但是这个地方并没有决定单目整个SLAM过程的尺度
// 因为CreateInitialMapMonocular函数对3D点深度会缩放,然后反过来对 t 有改变
u.col(2).copyTo(t);
t = t / cv::norm(t);
cv::Mat W(3, 3, CV_32F, cv::Scalar(0));
W.at<float>(0, 1) = -1;
W.at<float>(1, 0) = 1;
W.at<float>(2, 2) = 1;
R1 = u * W * vt;
if (cv::determinant(R1) < 0) // 旋转矩阵有行列式为1的约束
R1 = -R1;
R2 = u * W.t() * vt;
if (cv::determinant(R2) < 0)
R2 = -R2;
}