C++手写八点法计算基础矩阵F筛选匹配特征点

C++手写八点法计算基础矩阵F筛选匹配特征点

SLAM学习笔记,基于已有的图像与特征点,手写八点法,归一化坐标并计算出基础矩阵F,然后用基础矩阵筛选出错误的匹配点。

1. 数据

两张前后帧的jpg图片,两个记录着特征点坐标的txt文件。
txt文件内容格式:

194.240067 201.709167 
296.351013 387.698456 
19.343060 192.708969 
292.529175 353.677307 
143.711227 189.554550 
...

每一行都是当前图片上一个特征点的x和y坐标。

2. 理论

目标:通过相似变换矩阵T将坐标(x, y)转换到均值为0,方差为1的归一化坐标上,再求出归一化基础矩阵F’,最后得到适用于非归一化坐标的基础矩阵F。

归一化需要使用相似变换矩阵T:
[ S 0 t x 0 S t y 0 0 1 ] [ x y 1 ] = [ x ′ y ′ 1 ] (相似变换矩阵T) \left[ \begin{matrix} S & 0 & t_x \\ 0 & S & t_y \\ 0 & 0 & 1 \end{matrix} \right] \left[ \begin{matrix} x \\ y \\ 1 \end{matrix} \right]= \left[ \begin{matrix} x' \\ y' \\ 1 \end{matrix} \right]\tag{相似变换矩阵T} S000S0txty1xy1=xy1(T)
( x , y ) (x, y) (x,y)映射到 ( x ′ , y ′ ) (x', y') (x,y)上,可得:
x ′ = S x + t x x' = Sx + t_x x=Sx+tx
y ′ = S y + t y y' = Sy + t_y y=Sy+ty
因为目标是方差为1,所以目标为 1 n ∑ x ′ 2 + y ′ 2 = 2 \frac{1}{n}∑\sqrt{x'^2+y'^2} = \sqrt{2} n1x2+y2 =2

已知 x ′ = T x x' = Tx x=Tx,因此 x ′ T = x T T T x'^T = x^TT^T xT=xTTT
因此,对极约束 x 2 T x^T_2 x2T F x 1 x_1 x1变为
x 2 ′ T x'^T_2 x2T F ′ F' F x 1 ′ x'_1 x1
= x 2 T =x^T_2 =x2T T 2 T T^T_2 T2T F ′ F' F T 1 T_1 T1 x 1 x_1 x1
可得:
F = T 2 T F ′ T 1 F= T^T_2 F' T_1 F=T2TFT1
解得将F’转换为F的公式。

已知 x ′ = S x + t x x' = Sx + t_x x=Sx+tx
要使得均值为0,即:
1 n ∑ x i ′ = 0 \frac{1}{n}∑x'_i = 0 n1xi=0
1 n ∑ ( S x i + t x ) = 0 \frac{1}{n}∑(Sx_i + t_x) = 0 n1(Sxi+tx)=0
S n ∑ x i + t x = 0 \frac{S}{n}∑x_i + t_x = 0 nSxi+tx=0
S x ‾ + t x = 0 S\overline{x} + t_x = 0 Sx+tx=0
t x = − S x ‾ t_x = -S\overline{x} tx=Sx
同理可得: t y = − S y ‾ t_y = -S\overline{y} ty=Sy

要使得方差为1,则 1 n ∑ x ′ 2 + y ′ 2 = 2 \frac{1}{n}∑\sqrt{x'^2+y'^2} = \sqrt{2} n1x2+y2 =2
代入上方结论可得:
1 n ∑ ( S x + t x ) 2 + ( S y + t y ) 2 = 2 \frac{1}{n}∑\sqrt{(Sx + t_x)^2+(Sy + t_y)^2} = \sqrt{2} n1(Sx+tx)2+(Sy+ty)2 =2
1 n ∑ ( S x − S x ‾ ) 2 + ( S y − S y ‾ ) 2 = 2 \frac{1}{n}∑\sqrt{(Sx - S\overline{x})^2+(Sy-S\overline{y})^2} = \sqrt{2} n1(SxSx)2+(SySy)2 =2
S 1 n ∑ ( x − x ‾ ) 2 + ( y − y ‾ ) 2 = 2 S\frac{1}{n}∑\sqrt{(x - \overline{x})^2+(y-\overline{y})^2} = \sqrt{2} Sn1(xx)2+(yy)2 =2
S = 2 1 n ∑ ( x − x ‾ ) 2 + ( y − y ‾ ) 2 S = \frac{\sqrt{2}}{\frac{1}{n}∑\sqrt{(x - \overline{x})^2+(y-\overline{y})^2}} S=n1(xx)2+(yy)2 2

可得: S = 2 / 方 差 S=\sqrt{2}/方差 S=2 /
求出S后,代入 t x = − S x ‾ t_x = -S\overline{x} tx=Sx t y = − S y ‾ t_y = -S\overline{y} ty=Sy即可得到 t x , t y t_x, t_y tx,ty

注:每张图片对应一个单独的归一化变换矩阵T!

因此,算法执行步骤如下:
1、输入1张图中的8个点
2、遍历点,求出x与y的均值
3、再次遍历,用均值求出该图所有点的方差
4、用 S = 2 / 方 差 S=\sqrt{2}/方差 S=2 /求得S,代入S求得 t x , t y t_x, t_y tx,ty
5、重复步骤,获得两张图的变换矩阵 T 1 , T 2 T_1,T_2 T1T2
6、利用变换矩阵对原始坐标进行归一化,解矩阵方程计算 F ′ F' F
7、代入 F ′ F' F求解得到 F F F
8、用基础矩阵 F F F筛选正确与错误的匹配点

3. 代码实现

#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include "math.h"

#include <Eigen/Core>
#include <Eigen/Dense>

using namespace Eigen;

#include <opencv2/opencv.hpp>
#include <opencv2/core/eigen.hpp>

using namespace std;
using namespace cv;

int main(int argc, char **argv)
{
    // -----------------------------------------------------------从磁盘读取文件-----------------------------------------------------------
    // 包含两张前后帧的图片文件,和两个记录着特征点坐标的txt文件
    // 因为在build文件中编译,所以要向上级目录读取文件
    string save_path = "../";
    // 从磁盘上读图片
    cv::Mat cur_img = cv::imread(save_path + "cur_img.jpg", 0);
    cv::Mat forw_img = cv::imread(save_path + "forw_img.jpg", 0);

    // 从磁盘上读取特征点,并以vector<cv::KeyPoint>的形式储存
    string temp;
    int pos;
    float x;
    float y;
    // 读取第1张特征点
    ifstream cur_pts_file(save_path + "cur_pts.txt");
    // 以vector<cv::KeyPoint >的形式储存
    vector<cv::KeyPoint> cur_pts;
    if (!cur_pts_file.is_open())
    {
        cout << "未成功打开文件cur_pts.txt" << endl;
    }
    while (getline(cur_pts_file, temp))
    {
        if (temp.length() != 0)
        {
            // 获得字符串
            // 找到用于分割坐标的空格位置
            pos = temp.find(" ");
            // x坐标
            x = stof(temp.substr(0, pos));
            // y坐标
            y = stof(temp.substr(pos + 1, temp.length() - pos - 1));
            // 将<cv::Point2f >转换为<cv::KeyPoint >
            KeyPoint temp_keypoint;
            temp_keypoint.pt = Point2f(x, y);
            // 将<cv::KeyPoint >放入vector
            cur_pts.push_back(temp_keypoint);
        }
    }
    cur_pts_file.close();

    // 读取第2张特征点
    ifstream forw_pts_file(save_path + "forw_pts.txt");
    vector<cv::KeyPoint> forw_pts;
    if (!forw_pts_file.is_open())
    {
        cout << "未成功打开文件forw_pts.txt" << endl;
    }
    while (getline(forw_pts_file, temp))
    {
        if (temp.length() != 0)
        {
            // 获得字符串
            // 找到用于分割坐标的空格位置
            pos = temp.find(" ");
            // x坐标
            x = stof(temp.substr(0, pos));
            // y坐标
            y = stof(temp.substr(pos + 1, temp.length() - pos - 1));
            // 将<cv::Point2f >转换为<cv::KeyPoint >
            KeyPoint temp_keypoint;
            temp_keypoint.pt = Point2f(x, y);
            // 将<cv::KeyPoint >放入vector
            forw_pts.push_back(temp_keypoint);
        }
    }
    forw_pts_file.close();

    // // 打印行数进行验证是否都加载正常
    // cout << cur_pts.size() << endl;
    // cout << forw_pts.size() << endl;

    // 创建特征点之间的匹配
    vector<DMatch> matches;
    BFMatcher bfMatcher(NORM_L2);

    //计算特征点描述子,特征向量提取
    Mat dst1, dst2;
    // 使用ORB算法提取特征
    Ptr<DescriptorExtractor> descriptor = ORB::create();
    // 计算描述子
    descriptor->compute(cur_img, cur_pts, dst1);
    descriptor->compute(forw_img, forw_pts, dst2);
    // 进行描述子的匹配
    bfMatcher.match(dst1, dst2, matches);
    // 创建用于输出的图像
    cv::Mat out_image;
    // 绘制输出图像
    drawMatches(cur_img, cur_pts, forw_img, forw_pts, matches, out_image);
    // 显示输出图像
    imshow("全部匹配的连线图像", out_image);
    waitKey(0);

    // 截取前8个特征点匹配对,绘制图像
    vector<DMatch> matches_8(matches.begin(), matches.begin() + 8);

    // 创建用于输出前8个特征点的图像
    cv::Mat out_image_8;
    // 用于输出的特征点
    vector<cv::KeyPoint> cur_pts_8;
    vector<cv::KeyPoint> forw_pts_8;
    // 提取出需要的8对点,放入新的vector<cv::KeyPoint >
    for (auto p = matches_8.begin(); p != matches_8.end(); p++)
    {
        // 根据vector<DMatch>中记载的索引取出vector<cv::KeyPoint >中对应的关键点
        // queryIdx对应查询图像的特征描述子索引
        // trainIdx对应训练(模板)图像的特征描述子索引
        cur_pts_8.push_back(cur_pts.at(p->queryIdx));
        forw_pts_8.push_back(forw_pts.at(p->trainIdx));
    }

    // 绘制前8对点的输出图像
    drawMatches(cur_img, cur_pts_8, forw_img, forw_pts_8, matches_8, out_image_8);
    // 显示只有8对匹配点的输出图像
    imshow("8点连线图像", out_image_8);
    waitKey(0);
    // 依靠人工方式肉眼辨认确认这8个点都匹配正确
    // 然后将这8对特征点作为八点法的运算数据

    // 用于以vector<cv::Point2f>形式储存这八对点
    vector<cv::Point2f> cur_pts_8_pointf;
    vector<cv::Point2f> forw_pts_8_pointf;

    // 遍历需要的八对点
    for (auto p = matches_8.begin(); p != matches_8.end(); p++)
    {
        // 根据vector<DMatch>中记载的索引取出vector<cv::KeyPoint >中对应的关键点
        // CV_PROP_RW int queryIdx; // query descriptor index - 此匹配对应的查询图像的特征描述子索引
        // 一行
        cur_pts_8_pointf.push_back(cur_pts.at(p->trainIdx).pt);
        forw_pts_8_pointf.push_back(forw_pts.at(p->trainIdx).pt);
    }

    // -----------------------------------------------------------方法1:直接使用opencv库函数-----------------------------------------------------------
    // 使用opencv库函数的八点法完成计算基础矩阵
    Mat f_matrix_from_cv = findFundamentalMat(cur_pts_8_pointf, forw_pts_8_pointf, CV_FM_8POINT);
    cout << "findFundamentalMat" << endl << f_matrix_from_cv << endl;

    // -----------------------------------------------------------方法2:手写归一化并使用SVD分解-----------------------------------------------------------
    // 与opencv库函数使用的是相同的算法

    // 对八对点分别进行归一化
    // 每张图都使用一个变换矩阵T用于将x与y归一化到均值为1的状态
    // 用于储存输出的归一化点
    std::vector<cv::Point2f> cur_pts_8_pointf_norm = cur_pts_8_pointf;
    std::vector<cv::Point2f> forw_pts_8_pointf_norm = forw_pts_8_pointf;

    // 第1张图
    // 第一步得到所有特征点的均值
    float mean_x = 0;
    float mean_y = 0;
    for (int i = 0; i < 8; i++)
    {
        mean_x += cur_pts_8_pointf.at(i).x;
        mean_y += cur_pts_8_pointf.at(i).y;
    }
    mean_x /= 8;
    mean_y /= 8;

    //第二步求方差sum( sqrt( (x-mean(x))^2 + (y-mean(y))^2 ) ) / n
    float variance = 0;
    for (int i = 0; i < 8; i++)
    {
        variance += sqrt((cur_pts_8_pointf.at(i).x - mean_x) * (cur_pts_8_pointf.at(i).x - mean_x) +
                         (cur_pts_8_pointf.at(i).y - mean_y) * (cur_pts_8_pointf.at(i).y - mean_y));
    }
    variance /= 8;

    // 根据方差计算S
    float S = sqrt(2) / variance;
    // 计算出tx与ty
    float tx = -S * mean_x;
    float ty = -S * mean_y;

    // 得到变换矩阵T1
    Matrix<float, 3, 3> T1;
    T1 << S, 0, tx, 0, S, ty, 0, 0, 1;

    // 输出归一化点
    for (int i = 0; i < 8; i++)
    {
        cur_pts_8_pointf_norm.at(i).x = S * cur_pts_8_pointf.at(i).x + tx;
        cur_pts_8_pointf_norm.at(i).y = S * cur_pts_8_pointf.at(i).y + ty;
    }

    // 第2张图,重复上述步骤
    // 第一步得到所有特征点的均值
    mean_x = 0;
    mean_y = 0;
    for (int i = 0; i < 8; i++)
    {
        mean_x += forw_pts_8_pointf.at(i).x;
        mean_y += forw_pts_8_pointf.at(i).y;
    }
    mean_x /= 8;
    mean_y /= 8;

    //第二步求方差sum( sqrt( (x-mean(x))^2 + (y-mean(y))^2 ) ) / n
    variance = 0;
    for (int i = 0; i < 8; i++)
    {
        variance +=
            sqrt((forw_pts_8_pointf.at(i).x - mean_x) * (forw_pts_8_pointf.at(i).x - mean_x) +
                 (forw_pts_8_pointf.at(i).y - mean_y) * (forw_pts_8_pointf.at(i).y - mean_y));
    }
    variance /= 8;

    // 根据方差计算S
    S = sqrt(2) / variance;
    // 计算出tx与ty
    tx = -S * mean_x;
    ty = -S * mean_y;

    // 得到变换矩阵T2
    Matrix<float, 3, 3> T2;
    T2 << S, 0, tx, 0, S, ty, 0, 0, 1;

    // 输出归一化点(S*x + tx, S*y+ ty)
    for (int i = 0; i < 8; i++)
    {
        forw_pts_8_pointf_norm.at(i).x = S * forw_pts_8_pointf.at(i).x + tx;
        forw_pts_8_pointf_norm.at(i).y = S * forw_pts_8_pointf.at(i).y + ty;
    }

    // 打印两幅图像的归一化转换矩阵与归一化坐标
    cout << "T1" << endl << T1 << endl << endl;
    cout << "T2" << endl << T2 << endl << endl;

    // 对于求解矩阵Ax=0,首先构建系数矩阵A
    Mat matrix_a(8, 9, CV_32FC1);
    Vec<float, 9> params;

    auto p = matches_8.begin();
    float x1, y1, x2, y2;

    // 为矩阵A赋值
    for (int i = 0; i < 8; i++)
    {
        x1 = cur_pts_8_pointf_norm.at(i).x;
        y1 = cur_pts_8_pointf_norm.at(i).y;
        x2 = forw_pts_8_pointf_norm.at(i).x;
        y2 = forw_pts_8_pointf_norm.at(i).y;
        // 为A的其中一行赋值
        params[0] = x2 * x1;
        params[1] = x2 * y1;
        params[2] = x2;
        params[3] = y2 * x1;
        params[4] = y2 * y1;
        params[5] = y2;
        params[6] = x1;
        params[7] = y1;
        params[8] = 1.0;

        matrix_a.at<Vec<float, 9>>(i) = params;
        p++;
    }
    // 构建矩阵A完成
    // 打印系数矩阵A
    cout << "\nmatrix A:\n" << matrix_a << endl << endl;

    // 求归一化的基础矩阵F'
    Matrix<float, 8, 9> matrix_a_eigen;
    // 将A转换为Eigen矩阵
    cv2eigen(matrix_a, matrix_a_eigen);
    // SVD分解
    // A=USV^T
    JacobiSVD<Eigen::MatrixXf> svd(matrix_a_eigen, ComputeThinU | ComputeThinV);
    auto V = svd.matrixV();
    auto U = svd.matrixU();
    // 计算S中所有的奇异值
    auto A = svd.singularValues();
    cout << "V" << V << endl << endl;
    cout << "A" << A << endl << endl;

    // 使用V矩阵的最后一列,即最小奇异值对应的奇异向量作为F'
    Eigen::Matrix<float, 9, 1> v_col = V.col(7);
    // 按照顺序将它reshape成3x3矩阵
    Map<Matrix3f> f_matrix_from_svd(v_col.data(), 3, 3);
    f_matrix_from_svd = f_matrix_from_svd.transpose().eval();

    cout << endl << "F'" << endl << f_matrix_from_svd << endl << endl;

    // 将F'还原为F
    // F=T2^T * F' * T1
    f_matrix_from_svd = T2.transpose() * f_matrix_from_svd.eval() * T1;

    // 打印基础矩阵F
    cout << endl << "F" << endl << f_matrix_from_svd << endl << endl;

    // -----------------------------------------------------------构造可视化图像验证-----------------------------------------------------------
    // 使用基础矩阵F筛选出错误的匹配点
    Matrix3f f_matrix_eigen;

    // 使用方法1:opencv库函数得到的基础矩阵
    // cv2eigen(f_matrix_from_cv, f_matrix_eigen);

    // 使用方法2:手写八点法得到的基础矩阵
    f_matrix_eigen = f_matrix_from_svd;

    // 记录匹配错误的点,并且绘制可视化图像
    std::vector<cv::DMatch> wrong_matches;
    std::vector<cv::KeyPoint> wrong_kpoints1;
    std::vector<cv::KeyPoint> wrong_kpoints2;
    cv::Mat wrong_image;

    cout << "error match:" << endl;

    int index = 0;

    // 利用基础矩阵F筛选匹配
    for (auto p = matches.begin() + 8; p != matches.end(); p++)
    {
        // 计算每一对特征点代入基础矩阵得到的值
        Eigen::Vector3f v1;
        v1 << cur_pts.at(p->queryIdx).pt.x, cur_pts.at(p->queryIdx).pt.y, 1.0;
        Eigen::Vector3f v2;
        v2 << forw_pts.at(p->trainIdx).pt.x, forw_pts.at(p->trainIdx).pt.y, 1.0;

        auto match_value = v2.transpose() * (f_matrix_eigen * v1);

        // 如果匹配误差超出阈值1,视为错误匹配
        if (match_value > 1)
        {
            cout << "match value: " << setw(12) << match_value << "             ";
            cout << "point1:(" << setw(10) << cur_pts.at(p->queryIdx).pt.x << setw(10)
                 << cur_pts.at(p->queryIdx).pt.y << ")   ";
            cout << "point2:(" << setw(10) << forw_pts.at(p->trainIdx).pt.x << setw(10)
                 << forw_pts.at(p->trainIdx).pt.y << ")   " << endl;
            // 记录错误点匹配用于可视化显示
            wrong_kpoints1.push_back(cur_pts.at(p->queryIdx));
            wrong_kpoints2.push_back(forw_pts.at(p->trainIdx));
            p->queryIdx = index;
            p->trainIdx = index;
            index += 1;
            wrong_matches.push_back(*p);
        }
    }

    // 绘制错误匹配可视化图像
    cv::Mat error_img;
    // 绘制输出图像
    drawMatches(cur_img, wrong_kpoints1, forw_img, wrong_kpoints2, wrong_matches, error_img);
    // 显示错误匹配点的输出图像
    imshow("错误连线图像", error_img);
    waitKey(0);

    // 人工肉眼确认筛选出的匹配的确是错误的匹配点

    return 0;
}

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值