前言
本文主要介紹VINS狀態估計器模塊(estimator)初始化環節中視覺慣性對齊求解陀螺儀偏置、尺度、重力加速度、每幀速度以及相機到IMU的外參估計,其中前半部分對應論文第五章(V. ESTIMATOR INITIALIZATION B. Visual-Inertial Alignment),後半部分參考了沈老師組的之前的論文。
總的來說,視覺慣性對齊主要包括以下流程:
1、若旋轉外參數未知,則先估計旋轉外參
2、利用旋轉約束估計陀螺儀bias
3、利用平移約束估計重力方向,速度,以及尺度初始值
4、對重力向量進一步優化
5、求解世界座標系w和初始相機座標系之間的旋轉矩陣,並講軌跡對齊到世界座標系
視覺慣性對齊
首先我們先推導論文式(14),所有幀的位姿表示相對於第一幀相機座標系(·)c0。相機到IMU的外參爲,得到姿態從相機座標系轉換到IMU座標系的關係。
將T展開有成R與p有:
左側矩陣的兩項寫開:
對於VINS初始化中視覺慣性對齊之外的部分已在博客VINS-Mono代碼解讀——視覺慣性聯合初始化 中詳細介紹了。這裏就直接從VisualIMUAlignment()函數開始,結合相關數學推導進行解讀。這裏的代碼均位於vins_estimator/src/initial/initial_aligment.cpp/.h。
VisualIMUAlignment()中調用了兩個函數,分別用於對陀螺儀的偏置進行標定,以及估計尺度、重力以及速度。
bool VisualIMUAlignment(map<double, ImageFrame> &all_image_frame, Vector3d* Bgs, Vector3d &g, VectorXd &x)
{
solveGyroscopeBias(all_image_frame, Bgs);
if(LinearAlignment(all_image_frame, g, x))
return true;
else
return false;
}
陀螺儀偏置標定 solveGyroscopeBias()
對於窗口中得連續兩幀和,已經從視覺SFM中得到了旋轉和,從IMU預積分中得到了相鄰幀旋轉。根據約束方程,聯立所有相鄰幀,最小化代價函數(論文式(15)):
其中對陀螺儀偏置求IMU預積分項線性化,有:
在具體實現的時候,因爲上述約束方程爲:
有:
代入得:
只考慮虛部,有:
兩側乘以,用LDLT分解求得。
在代碼中,Quaterniond q_ij即
tmp_A即
tmp_B即
根據上面的代價函數構造Ax = B即
然後採用LDLT分解求得。
void solveGyroscopeBias(map<double, ImageFrame> &all_image_frame, Vector3d* Bgs)
{
Matrix3d A;
Vector3d b;
Vector3d delta_bg;
A.setZero();
b.setZero();
map<double, ImageFrame>::iterator frame_i;
map<double, ImageFrame>::iterator frame_j;
for (frame_i = all_image_frame.begin(); next(frame_i) != all_image_frame.end(); frame_i++)
{
frame_j = next(frame_i);
MatrixXd tmp_A(3, 3);
tmp_A.setZero();
VectorXd tmp_b(3);
tmp_b.setZero();
//R_ij = (R^c0_bk)^-1 * (R^c0_bk+1) 轉換爲四元數 q_ij = (q^c0_bk)^-1 * (q^c0_bk+1)
Eigen::Quaterniond q_ij(frame_i->second.R.transpose() * frame_j->second.R);
//tmp_A = J_j_bw
tmp_A = frame_j->second.pre_integration->jacobian.template block<3, 3>(O_R, O_BG);
//tmp_b = 2 * ((r^bk_bk+1)^-1 * (q^c0_bk)^-1 * (q^c0_bk+1))_vec
// = 2 * ((r^bk_bk+1)^-1 * q_ij)_vec
tmp_b = 2 * (frame_j->second.pre_integration->delta_q.inverse() * q_ij).vec();
//tmp_A * delta_bg = tmp_b
A += tmp_A.transpose() * tmp_A;
b += tmp_A.transpose() * tmp_b;
}
//LDLT方法
delta_bg = A.ldlt().solve(b);
ROS_WARN_STREAM("gyroscope bias initial calibration " << delta_bg.transpose());
for (int i = 0; i <= WINDOW_SIZE; i++)
Bgs[i] += delta_bg;
for (frame_i = all_image_frame.begin(); next(frame_i) != all_image_frame.end( ); frame_i++)
{
frame_j = next(frame_i);
frame_j->second.pre_integration->repropagate(Vector3d::Zero(), Bgs[0]);
}
}
速度、重力和尺度初始化 LinearAlignment()
該函數需要優化的變量有速度、重力加速度和尺度(論文式(16)):
其中,是第k幀圖像在其IMU座標系下的速度,是在第0幀相機座標系下的重力向量。
在IMU預積分中我們已經推導過論文式(5):
將替代可以寫成:
將論文式(14)帶入有:
這裏是待優化變量,將其轉換成的形式:
寫成矩陣形式:
對於有:
寫成矩陣形式:
由此得到論文式(18)(19):
即,使用LDLT分解求解:
對應代碼實現在以下函數中,因爲內容和上述理論一致就不放代碼了。裏面的MatrixXd tmp_A(6, 10)就是H,VectorXd tmp_b(6,1)就是b
bool LinearAlignment(map<double, ImageFrame> &all_image_frame, Vector3d &g, VectorXd &x)
最後求得尺度s和重力加速度g
double s = x(n_state - 1) / 100.0;
g = x.segment<3>(n_state - 4);
//如果重力加速度與參考值差太大或者尺度爲負則說明計算錯誤
if(fabs(g.norm() - G.norm()) > 1.0 || s < 0)
{
return false;
}
重力細化 RefineGravity()
考慮到上一步求得的g存在誤差,一般認爲重力矢量的模是已知的,因此重力向量只剩兩個自由度,在切線空間上用兩個變量重新參數化重力,將其表示爲
其中
爲上一步求得的重力方向,,在的正切平面上且正交。與的設置爲:
將其帶入論文式(18)(19)中,有:
即。使用LDLT分解求解:
在代碼實現中,以下函數用於重力細化,其流程基本對應上文推導,方程迭代求解4次:
void RefineGravity(map<double, ImageFrame> &all_image_frame, Vector3d &g, VectorXd &x)
函數中的MatrixXd lxly(3, 2)= TangentBasis(g0); 即對應,
VectorXd dg = x.segment<2>(n_state - 3); 即對應
以下函數對應論文中Algorithm 1,用於在半徑爲g的半球找到切面的一對正交基。
MatrixXd TangentBasis(Vector3d &g0)
{
Vector3d b, c;
Vector3d a = g0.normalized();
Vector3d tmp(0, 0, 1);
if(a == tmp)
tmp << 1, 0, 0;
b = (tmp - a * (a.transpose() * tmp)).normalized();
c = a.cross(b);
MatrixXd bc(3, 2);
bc.block<3, 1>(0, 0) = b;
bc.block<3, 1>(0, 1) = c;
return bc;
}
外參標定
VINS外參標定指的是對相機座標系到IMU座標系的變換矩陣進行在線標定與優化。
在參數配置文件yaml中,參數estimate_extrinsic反映了外參的情況:
1、等於0表示這外參已經是準確的了,之後不需要優化;
2、等於1表示外參只是一個估計值,後續還需要將其作爲初始值放入非線性優化中;
3、等於2表示不知道外參,需要進行標定,從代碼estimator.cpp中的processImage()中的以下代碼進入,主要是標定外參的旋轉矩陣。
if(ESTIMATE_EXTRINSIC == 2)//如果沒有外參則進行標定
{
ROS_INFO("calibrating extrinsic param, rotation movement is needed");
if (frame_count != 0)
{
//得到當前幀與前一幀之間歸一化特徵點
vector<pair<Vector3d, Vector3d>> corres = f_manager.getCorresponding(frame_count - 1, frame_count);
Matrix3d calib_ric;
//標定從camera到IMU之間的旋轉矩陣
if (initial_ex_rotation.CalibrationExRotation(corres, pre_integrations[frame_count]->delta_q, calib_ric))
{
ROS_WARN("initial extrinsic rotation calib success");
ROS_WARN_STREAM("initial extrinsic rotation: " << endl << calib_ric);
ric[0] = calib_ric;
RIC[0] = calib_ric;
ESTIMATE_EXTRINSIC = 1;
}
}
}
InitialEXRotation類
該類位於vins_estimator/src/initial/initial_ex_rotation.cpp/.h中,用於標定外參旋轉矩陣。首先簡要介紹一下InitialEXRotation類的成員:
class InitialEXRotation
{
public:
InitialEXRotation();//構造函數
//標定外參旋轉矩陣
bool CalibrationExRotation(vector<pair<Vector3d, Vector3d>> corres, Quaterniond delta_q_imu, Matrix3d &calib_ric_result);
private:
//求解幀間cam座標系的旋轉矩陣
Matrix3d solveRelativeR(const vector<pair<Vector3d, Vector3d>> &corres);
//三角化驗證Rt
double testTriangulation(const vector<cv::Point2f> &l,
const vector<cv::Point2f> &r,
cv::Mat_<double> R, cv::Mat_<double> t);
//本質矩陣SVD分解計算4組Rt值
void decomposeE(cv::Mat E,
cv::Mat_<double> &R1, cv::Mat_<double> &R2,
cv::Mat_<double> &t1, cv::Mat_<double> &t2);
int frame_count;//幀計數器
vector< Matrix3d > Rc;//幀間cam的R,由對極幾何得到
vector< Matrix3d > Rimu;//幀間IMU的R,由IMU預積分得到
vector< Matrix3d > Rc_g;//每幀IMU相對於起始幀IMU的R
Matrix3d ric;//cam到IMU的外參
};
CalibrationExRotation() 標定外參旋轉矩陣
該函數目的是標定外參的旋轉矩陣。由於函數內部並不複雜,將所有內部調用的函數也放在一起介紹。
1、solveRelativeR(corres)根據對極幾何計算相鄰幀間相機座標系的旋轉矩陣,這裏的corres傳入的是當前幀和其之前一幀的對應特徵點對的歸一化座標。
1.1、將corres中對應點的二維座標分別存入ll與rr中。
vector<cv::Point2f> ll, rr;
for (int i = 0; i < int(corres.size()); i++)
{
ll.push_back(cv::Point2f(corres[i].first(0), corres[i].first(1)));
rr.push_back(cv::Point2f(corres[i].second(0), corres[i].second(1)));
}
1.2、根據對極幾何求解這兩幀的本質矩陣
cv::Mat E = cv::findFundamentalMat(ll, rr);
1.3、對本質矩陣進行svd分解得到4組Rt解
cv::Mat_<double> R1, R2, t1, t2;
decomposeE(E, R1, R2, t1, t2);
1.4、採用三角化對每組Rt解進行驗證,選擇是正深度的Rt解
double ratio1 = max(testTriangulation(ll, rr, R1, t1), testTriangulation(ll, rr, R1, t2));
double ratio2 = max(testTriangulation(ll, rr, R2, t1), testTriangulation(ll, rr, R2, t2));
cv::Mat_<double> ans_R_cv = ratio1 > ratio2 ? R1 : R2;
1.5、這裏的R是上一幀到當前幀的變換矩陣,對其求轉置爲當前幀相對於上一幀的姿態。
Matrix3d ans_R_eigen;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
ans_R_eigen(j, i) = ans_R_cv(i, j);
return ans_R_eigen;
}
2、獲取相鄰幀之間相機座標系和IMU座標系的旋轉矩陣,存入vector中
frame_count++;
Rc.push_back(solveRelativeR(corres));//幀間cam的R,由對極幾何得到
Rimu.push_back(delta_q_imu.toRotationMatrix());//幀間IMU的R,由IMU預積分得到
Rc_g.push_back(ric.inverse() * delta_q_imu * ric);//每幀IMU相對於起始幀IMU的R
3、求解相機到IMU的外參旋轉矩陣,根據等式:
其中爲IMU座標系之間的旋轉矩陣,爲相機座標系之間的旋轉矩陣,爲從相機到IMU的旋轉矩陣。
用四元素重寫:
將多個幀之間的等式關係一起構建超定方程。對A進行svd分解,其中最小奇異值對應的右奇異向量便爲結果x,即旋轉四元數。
在代碼中,L爲相機旋轉四元數的左乘矩陣,R爲IMU旋轉四元數的右乘矩陣,因此其實構建的是:
所求的x是,在最後需要轉換成旋轉矩陣並求逆。
Eigen::MatrixXd A(frame_count * 4, 4);
A.setZero();
int sum_ok = 0;
for (int i = 1; i <= frame_count; i++)
{
Quaterniond r1(Rc[i]);
Quaterniond r2(Rc_g[i]);
double angular_distance = 180 / M_PI * r1.angularDistance(r2);
ROS_DEBUG(
"%d %f", i, angular_distance);
//huber核函數做加權
double huber = angular_distance > 5.0 ? 5.0 / angular_distance : 1.0;
++sum_ok;
Matrix4d L, R;
//L R 分別爲左乘和右乘矩陣
double w = Quaterniond(Rc[i]).w();
Vector3d q = Quaterniond(Rc[i]).vec();
L.block<3, 3>(0, 0) = w * Matrix3d::Identity() + Utility::skewSymmetric(q);
L.block<3, 1>(0, 3) = q;
L.block<1, 3>(3, 0) = -q.transpose();
L(3, 3) = w;
Quaterniond R_ij(Rimu[i]);
w = R_ij.w();
q = R_ij.vec();
R.block<3, 3>(0, 0) = w * Matrix3d::Identity() - Utility::skewSymmetric(q);
R.block<3, 1>(0, 3) = q;
R.block<1, 3>(3, 0) = -q.transpose();
R(3, 3) = w;
A.block<4, 4>((i - 1) * 4, 0) = huber * (L - R);
}
//svd分解中最小奇異值對應的右奇異向量作爲旋轉四元數
JacobiSVD<MatrixXd> svd(A, ComputeFullU | ComputeFullV);
Matrix<double, 4, 1> x = svd.matrixV().col(3);
Quaterniond estimated_R(x);
ric = estimated_R.toRotationMatrix().inverse();
4、至少迭代計算了WINDOW_SIZE次,且R的奇異值大於0.25才認爲標定成功
Vector3d ric_cov;
ric_cov = svd.singularValues().tail<3>();
if (frame_count >= WINDOW_SIZE && ric_cov(1) > 0.25)
{
calib_ric_result = ric;
return true;
}
else
return false;
在初步標定完外參的旋轉矩陣後,對旋轉矩陣與平移向量的優化將在非線性優化中一起介紹。