视觉SLAM笔记(33) 对极约束求解相机运动


1. 本质矩阵求解相机运动

通过 Essential 矩阵求解相机运动
视觉SLAM笔记(31) 特征提取和匹配 的程序提供了特征匹配
而这次就使用匹配好的特征点来计算 E E , F F H H
进而分解 E E 得到 R R , t t

创建/VSLAM_note/033/pose_estimation_2d2d.cpp 文件

在这里插入图片描述


2. 特征提取函数

整个程序使用 OpenCV 提供的算法进行求解
视觉SLAM笔记(31) 特征提取和匹配 的特征提取封装成函数 find_feature_matches

void find_feature_matches(const Mat& img_1, const Mat& img_2,
    std::vector<KeyPoint>& keypoints_1,
    std::vector<KeyPoint>& keypoints_2,
    std::vector< DMatch >& matches)
{
    //-- 初始化
    Mat descriptors_1, descriptors_2;                    // 创建描述子
    Ptr<FeatureDetector> detector = ORB::create();       // 创建ORB特征点检测
    Ptr<DescriptorExtractor> descriptor = ORB::create(); // 使用ORB特征来描述特征点
    Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("BruteForce-Hamming"); // 设置特征匹配使用汉明距离计算

    //-- 第一步:检测 Oriented FAST 角点位置
    detector->detect(img_1, keypoints_1);
    detector->detect(img_2, keypoints_2);

    //-- 第二步:根据角点位置计算 BRIEF 描述子
    descriptor->compute(img_1, keypoints_1, descriptors_1);
    descriptor->compute(img_2, keypoints_2, descriptors_2);

    //-- 第三步:对两幅图像中的BRIEF描述子进行匹配,使用 Hamming 距离
    vector<DMatch> match;
    matcher->match(descriptors_1, descriptors_2, match);

    //-- 第四步:匹配点对筛选
    double min_dist = 10000, max_dist = 0;

    // 找出所有匹配之间的最小距离和最大距离, 即是最相似的和最不相似的两组点之间的距离
    for (int i = 0; i < descriptors_1.rows; i++)
    {
        double dist = match[i].distance;
        if (dist < min_dist) min_dist = dist;
        if (dist > max_dist) max_dist = dist;
    }
    // 显示所有匹配之间的最大距离和最小距离
    printf("-- Max dist : %f \n", max_dist);
    printf("-- Min dist : %f \n", min_dist);

    // 当描述子之间的距离大于两倍的最小距离时,即认为匹配有误
    // 但有时候最小距离会非常小,设置一个经验值30作为下限
    for (int i = 0; i < descriptors_1.rows; i++)
    {
        if (match[i].distance <= max(2 * min_dist, 30.0))
        {
            matches.push_back(match[i]);
        }
    }
}

以供后面使用


3. 2D-2D姿态估计函数

2D-2D姿态估计函数pose_estimation_2d2d,提供了从特征点求解相机运动的部分
然后,在主函数中调用它,就能得到相机的运动

void pose_estimation_2d2d(std::vector<KeyPoint> keypoints_1,
    std::vector<KeyPoint> keypoints_2,
    std::vector< DMatch > matches,
    Mat& R, Mat& t)
{
    // 相机内参,TUM Freiburg2
    double cx = 325.1;  // 像素坐标系与成像平面之间的原点平移
    double cy = 249.7;
    double fx = 520.9;  // 焦距
    double fy = 521.0;
    Mat K = (Mat_<double>(3, 3) << fx, 0, cx, 0, fy, cy, 0, 0, 1);

    //-- 把匹配点转换为vector<Point2f>的形式
    vector<Point2f> points1;
    vector<Point2f> points2;

    for (int i = 0; i < (int)matches.size(); i++)
    {
        points1.push_back(keypoints_1[matches[i].queryIdx].pt);
        points2.push_back(keypoints_2[matches[i].trainIdx].pt);
    }

    //-- 计算基础矩阵
    Mat fundamental_matrix;
    fundamental_matrix = findFundamentalMat(points1, points2, CV_FM_8POINT);
    cout << "fundamental_matrix is " << endl << fundamental_matrix << endl;

    //-- 计算本质矩阵
    Point2d principal_point(cx, cy);          //相机光心, TUM dataset标定值
    double focal_length = fy;                 //相机焦距, TUM dataset标定值
    Mat essential_matrix;
    essential_matrix = findEssentialMat(points1, points2, focal_length, principal_point);
    cout << "essential_matrix is " << endl << essential_matrix << endl;

    //-- 计算单应矩阵
    Mat homography_matrix;
    homography_matrix = findHomography(points1, points2, RANSAC, 3);
    cout << "homography_matrix is " << endl << homography_matrix << endl;

    //-- 从本质矩阵中恢复旋转和平移矩阵
    recoverPose(essential_matrix, points1, points2, R, t, focal_length, principal_point);
    cout << "R is " << endl << R << endl;
    cout << "t is " << endl << t << endl;
}

在特征提取后得到匹配点 p p 1, p p 2
根据下列简化对极约束中的第三等式:
在这里插入图片描述通过findFundamentalMat()函数可以计算出基础矩阵F
再确认内参K,根据第二等式,可通过findEssentialMat函数得出本质矩阵E
通过findHomography函数得出单应矩阵H,不过特征点不在一个平面,所以意义不大
最后通过recoverPose函数从本质矩阵中恢复旋转和平移矩阵


4. 归一化坐标函数

像素坐标转相机归一化坐标函数pixel2cam

Point2d pixel2cam(const Point2d& p, const Mat& K)
{
    return Point2d
    (
        (p.x - K.at<double>(0, 2)) / K.at<double>(0, 0),
        (p.y - K.at<double>(1, 2)) / K.at<double>(1, 1)
    );
}

5. 求解相机运动

int main(int argc, char** argv)
{
    if (argc != 3)
    {
        cout << "usage: pose_estimation_2d2d img1 img2" << endl;
        return 1;
    }
    //-- 读取图像
    Mat img_1 = imread(argv[1], CV_LOAD_IMAGE_COLOR);
    Mat img_2 = imread(argv[2], CV_LOAD_IMAGE_COLOR);

    //-- 特征提取
    vector<KeyPoint> keypoints_1, keypoints_2;
    vector<DMatch> matches;
    find_feature_matches(img_1, img_2, keypoints_1, keypoints_2, matches);
    cout << "一共找到了" << matches.size() << "组匹配点" << endl;

    //-- 估计两张图像间运动
    Mat R, t;
    pose_estimation_2d2d(keypoints_1, keypoints_2, matches, R, t);

    //-- 验证E=t^R*scale
    Mat t_x = (Mat_<double>(3, 3) <<
        0, -t.at<double>(2, 0), t.at<double>(1, 0),
        t.at<double>(2, 0), 0, -t.at<double>(0, 0),
        -t.at<double>(1.0), t.at<double>(0, 0), 0);

    cout << "t^R=" << endl << t_x * R << endl;

    //-- 验证对极约束
    // 相机内参,TUM Freiburg2
    double cx = 325.1;  // 像素坐标系与成像平面之间的原点平移
    double cy = 249.7;
    double fx = 520.9;  // 焦距
    double fy = 521.0;
    Mat K = (Mat_<double>(3, 3) << fx, 0, cx, 0, fy, cy, 0, 0, 1);
    // 计算对极约束
    for (DMatch m : matches)
    {
        Point2d pt1 = pixel2cam(keypoints_1[m.queryIdx].pt, K);
        Mat y1 = (Mat_<double>(3, 1) << pt1.x, pt1.y, 1);

        Point2d pt2 = pixel2cam(keypoints_2[m.trainIdx].pt, K);
        Mat y2 = (Mat_<double>(3, 1) << pt2.x, pt2.y, 1);

        Mat d = y2.t() * t_x * R * y1;                 // 对极约束方程
        cout << "epipolar constraint = " << d << endl; // 应该无限接近0
    }
    return 0;
}

读取图像,然后特征提取,然后估计两张图像间运动
得出R, t
根据 x2 = Rx1 + t 就可以估计现在的位姿

在函数中输出了 E, F 和 H 的数值
在这里插入图片描述

接着验证 E=t^ R,通过 t^ R的结果比较

然后验证对极约束是否成立
在这里插入图片描述
在程序的输出结果中可以看出,对极约束的满足精度约在 10−3 量级


6. 尺度不确定性

对平移矩阵 t 长度的归一化,直接导致了单目视觉的尺度不确定性(Scale Ambiguity)
例如,程序中输出的 t 第一维约 0.883
这个 0.883 究竟是指 0.883m,还是 0.883cm,是没法确定的
因为对 t 乘以任意比例常数后,对极约束依然是成立的

换言之,在单目 SLAM 中,对轨迹和地图同时缩放任意倍数,得到的图像依然是一样的
这在 视觉SLAM笔记(2) 相机 介绍过了
在单目视觉中,对两张图像的 t 归一化,相当于固定了尺度
虽然不知道它的实际长度为多少,但以这时的 t 为单位 1,计算相机运动和特征点的 3D 位置
这被称为单目 SLAM 的初始化

在初始化之后,就可以用 3D-2D 来计算相机运动了
初始化之后的轨迹和地图的单位,就是初始化时固定的尺度
因此,单目 SLAM 有一步不可避免的初始化
初始化的两张图像必须有一定程度的平移,而后的轨迹和地图都将以此步的平移为单位

除了对 t 进行归一化之外,另一种方法是令初始化时所有的特征点平均深度为 1,也可以固定一个尺度
相比于令 t 长度为 1 的做法,把特征点深度归一化可以控制场景的规模大小,使计算在数值上更稳定些
不过这并没有理论上的差别


7. 多于八对点的情况

当给定的匹配点数多于八对时(例程找到了 81 对匹配),可以计算一个最小二乘解
视觉SLAM笔记(32) 2D-2D: 对极几何 中八点法提到线性化后的对极约束
在这里插入图片描述
把左侧的系数矩阵记为 A:Ae = 0
对于八点法, A 的大小为 8 × 9
如果给定的匹配点多于 8,该方程构成一个超定方程,即不一定存在 e 使得上式成立
因此,可以通过最小化一个二次型来求:
在这里插入图片描述
于是就求出了在最小二乘意义下的 E 矩阵
不过,当可能存在误匹配的情况时
会更倾向于使用 随机采样一致性(Random Sample Concensus,RANSAC) 来求,而不是最小二乘
RANSAC 是一种通用的做法,适用于很多带错误数据的情况,可以处理带有错误匹配的数据


8. 初始化的纯旋转问题

从 E 分解到 R, t 的过程中,如果相机发生的是纯旋转,导致 t 为零
那么,得到的 E 也将为零,这将导致无从求解 R
不过,此时可以依靠 H 求取旋转
但仅有旋转时,无法用三角测量估计特征点的空间位置
于是,另一个结论是, 单目初始化不能只有纯旋转,必须要有一定程度的平移
如果没有平移,单目将无法初始化
在实践当中,如果初始化时平移太小,会使得位姿求解与三角化结果不稳定,从而导致失败
相对的,如果把相机左右移动而不是原地旋转,就容易让单目 SLAM 初始化
因而有经验的 SLAM 研究人员
在单目 SLAM 情况下,经常选择让相机进行左右平移以顺利地进行初始化


参考:

《视觉SLAM十四讲》


相关推荐:

视觉SLAM笔记(32) 2D-2D: 对极几何
视觉SLAM笔记(31) 特征提取和匹配
视觉SLAM笔记(30) 特征点法
视觉SLAM笔记(29) g2o
视觉SLAM笔记(28) Ceres


发布了217 篇原创文章 · 获赞 290 · 访问量 288万+

猜你喜欢

转载自blog.csdn.net/qq_32618327/article/details/102533056