由于使用的数据相对参数提供的不够精确导致结果精度不高,因此需要研究一下外参的在线估计。
VINS外参标定指的是对相机坐标系到IMU坐标系的变换矩阵进行在线标定与优化。
1、参数配置
在参数配置文件yaml中,参数estimate_extrinsic反映了外参的情况:
1、等于0表示这外参已经是准确的了,之后不需要优化;
2、等于1表示外参只是一个估计值,后续还需要将其作为初始值放入非线性优化中;
3、等于2表示不知道外参,需要进行标定,从代码estimator.cpp中的processImage()中的以下代码进入,主要是标定外参的旋转矩阵。
if (ESTIMATE_EXTRINSIC == 2) // 不知道相机外参
{
ROS_WARN("have no prior about extrinsic param, calibrate extrinsic param");
RIC.push_back(Eigen::Matrix3d::Identity());
TIC.push_back(Eigen::Vector3d::Zero());
EX_CALIB_RESULT_PATH = OUTPUT_PATH + "/extrinsic_parameter.csv";
}
else // 知道相机外参
{
if ( ESTIMATE_EXTRINSIC == 1) // 虽然知道相机外参,但是在优化过程中还是去优化外参,这里的1只是标记了一种状态,并不是指优化的参数的数量
{
ROS_WARN(" Optimize extrinsic param around initial guess!");
EX_CALIB_RESULT_PATH = OUTPUT_PATH + "/extrinsic_parameter.csv";
}
if (ESTIMATE_EXTRINSIC == 0) // 知道相机外参,而且在优化过程中该参数是固定的,不参与优化
ROS_WARN(" fix extrinsic param ");
cv::Mat cv_R, cv_T;
fsSettings["extrinsicRotation"] >> cv_R;
fsSettings["extrinsicTranslation"] >> cv_T;
Eigen::Matrix3d eigen_R;
Eigen::Vector3d eigen_T;
cv::cv2eigen(cv_R, eigen_R);
cv::cv2eigen(cv_T, eigen_T);
Eigen::Quaterniond Q(eigen_R);
eigen_R = Q.normalized();
RIC.push_back(eigen_R);
TIC.push_back(eigen_T);
ROS_INFO_STREAM("Extrinsic_R : " << std::endl << RIC[0]);
ROS_INFO_STREAM("Extrinsic_T : " << std::endl << TIC[0].transpose());
}
如果需要在线估计参数且给出初始值时,则需要先读取矩阵R,t,并将其从opencv矩阵转化为eigen矩阵,得到容器RIC,TIC的第一个矩阵分量。
2、原理
其中q_bk_bk+1是陀螺仪预积分得到的,q_ck_ck+1是用8点法对前后帧对应的特征点进行计算得到的。
3、包含函数
在线估计外参的具体实现方式在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的外参
};
4、估计实现
CalibrationExRotation() 标定外参旋转矩阵,该函数目的是标定外参的旋转矩阵。函数的入参 corres 是包含 两帧之间 的多个匹配点对的归一化坐标的vector数组,入参 delta_q 是通过 两帧间 陀螺仪积分得到相对旋转;
bool InitialEXRotation::CalibrationExRotation(vector<pair<Vector3d, Vector3d>>
corres, Quaterniond delta_q_imu, Matrix3d &calib_ric_result)
{
/*记录第几次进入这个函数,标定的过程中会多次进入这个函数,
直到标定成功,每进一次通过入参得到公式(6)这样一个约束。*/
frame_count++;
/*过帧间的匹配点对计算得到本质矩阵,然后分解得到旋转矩阵R_ck+1^ck*/
Rc.push_back(solveRelativeR(corres));
Rimu.push_back(delta_q_imu.toRotationMatrix());//帧间陀螺仪积分得到R_bk+1^bk
/*每次迭代前先用前一次估计的ric将R_bk+1^bk变换成R_ck+1^ck,即公式(1)*/
Rc_g.push_back(ric.inverse() * delta_q_imu * ric);
Eigen::MatrixXd A(frame_count * 4, 4); //构建公式(7)中的A矩阵
A.setZero();
int sum_ok = 0;
for (int i = 1; i <= frame_count; i++)
{
Quaterniond r1(Rc[i]);
Quaterniond r2(Rc_g[i]);
/* angularDistance就是计算两个坐标系之间相对旋转矩阵在做轴角变换后(u * theta)
的角度theta, theta越小说明两个坐标系的姿态越接近,这个角度距离用于后面计算权重,
这里计算权重就是为了降低外点的干扰,意思就是为了防止出现误差非常大的R_bk+1^bk和
R_ck+1^ck约束导致估计的结果偏差太大*/
double angular_distance = 180 / M_PI * r1.angularDistance(r2);
ROS_DEBUG(
"%d %f", i, angular_distance);
//核函数做加权
double huber = angular_distance > 5.0 ? 5.0 / angular_distance : 1.0;
++sum_ok;
Matrix4d L, R;
/*L R 分别为四元数左乘和四元数右乘矩阵
下面的这几步就是从公式(3)到公式(6)的过程 ,每次得到4行约束填入A矩阵中*/
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);
}
//对A矩阵做SVD分解,最小奇异值对应的右向量就是四元数解
JacobiSVD<MatrixXd> svd(A, ComputeFullU | ComputeFullV);
//这里的四元数存储的顺序是[x,y,z,w]',即[qv qw]'
Matrix<double, 4, 1> x = svd.matrixV().col(3);
Quaterniond estimated_R(x);
ric = estimated_R.toRotationMatrix().inverse();
Vector3d ric_cov;
ric_cov = svd.singularValues().tail<3>();
/* 至少会迭代WINDOW_SIZE次,并且第二小的奇异值大于0.25才认为标定成功
(singularValues拿到的奇异值是从大到小存储的),
意思就是最小的奇异值要足够接近于0,和第二小之间要有足够差距才行,
这样才算求出了最优解 */
if (frame_count >= WINDOW_SIZE && ric_cov(1) > 0.25)
{
calib_ric_result = ric;
return true;
}
else
return false;
}
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;
}