三维重建SFM-stage-1特征匹配

三维重建SFM Stage 1-Feature Matching 特征匹配

本文章为电子科大大二数字动漫综合设计挑战项目,笔者主要对代码作解析,具体原理可以参考网上资料。

环境配置

系统:Linux Ubuntu

IDE: Clion

库:OpenCV 4.8.0

代码解析

图像读入

使用imread读取图像:

  • filename:图像路径
  • flags:图像读入方式。SIFT算法通常在灰度图像上运行,因为灰度图像更简单,计算效率更高,所以选择读入灰度图像
1
2
3
cv::Mat image1 = cv::imread("/home/maric/CLionProjects/reconstruction/s2/B21.jpg",cv::IMREAD_GRAYSCALE);
cv::Mat image2 = cv::imread("/home/maric/CLionProjects/reconstruction/s2/B22.jpg",cv::IMREAD_GRAYSCALE);

设置掩码

定义了一个与原始图像相同大小的二值图像,其中像素值为0或255。掩码的作用是限制特征点的检测和匹配区域,只对指定区域内的图像进行特征提取和匹配。

在这里,使用矩形区域定义了采样区域的ROI(Region of Interest),然后将掩码中对应区域的像素值设置为255,以便在该区域内进行特征点检测和匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
   // 定义掩码图像
//--------------
//设置掩码大小与图片等大,且全部置零
cv::Mat mask1 = cv::Mat::zeros(image1.size(), CV_8U);
cv::Mat mask2 = cv::Mat::zeros(image2.size(), CV_8U);

// 划定采样区域的矩形,从(100,30)坐标框选(300,600)大小区域作为采样区
cv::Rect roi1(100, 30, 300, 600);
cv::Rect roi2(100, 30, 300, 600);

//将该区域掩码设置为255
mask1(roi1).setTo(255);
mask2(roi2).setTo(255);

SIFT(Scale-Invariant Feature Transform)算法

SIFT检测能够提取出具有唯一性和尺度不变性的图像特征点,并计算出描述符以进行特征匹配。这使得SIFT在计算机视觉领域的很多应用中都发挥着重要的作用,例如目标识别、图像拼接、三维重建等。

  • SIFT参数:
    • nfeatures:要检测的关键点数量,默认为0,设定为1000提高效率
    • nOctaveLayers:每个金字塔组中的层数,设定为5提高精度
    • contrastThreshold:用于过滤关键点的对比度阈值,默认为0.04。对于灰度图像,该值影响较小,若使用彩色图像,则可以对该值进行一定的调整。
    • edgeThreshold:用于过滤边缘关键点的阈值,默认为10.0。
    • sigma:高斯滤波器的初始方差,默认为1.6
1
2
3
4
5
6
7
8
9
10
// 创建SIFT特征检测器
cv::Ptr<cv::Feature2D> sift = cv::SIFT::create
(
1000,
5,
0.03,
10.0,
1.6,
false
);//创建SIFT

特征点检测

SIFT算法能够在图像中检测出具有显著特征的位置,这些位置被称为关键点(keypoints)。关键点通常位于图像中的角点、边缘、纹理等具有唯一性的位置,能够在不同尺度和旋转下保持稳定。通过检测关键点,可以在图像中找到具有显著特征的位置。

特征描述

对于每个检测到的关键点,SIFT算法计算一个描述符(descriptor),描述符是一个包含了关键点及其周围区域的局部特征向量。描述符编码了关键点周围区域的梯度方向、梯度大小等信息,具有较高的区分度和尺度不变性。通过计算描述符,可以将关键点转换为数值表示,用于后续的特征匹配和识别任务。

1
2
3
4
5
6
//关键点列表: 图像中的特殊点,具有显著特征的位置,用于定位
std::vector<cv::KeyPoint> keypoints1, keypoints2;
cv::Mat descriptors1, descriptors2;
//根据图片和掩码获得关键点和描述符
sift->detectAndCompute(image1, mask1, keypoints1, descriptors1);
sift->detectAndCompute(image2, mask2, keypoints2, descriptors2);

特征匹配:

SIFT算法通过比较关键点的描述符,可以进行特征匹配。通过在两幅图像中找到具有相似特征描述符的关键点对,可以将两幅图像中的相应特征点进行匹配。特征匹配在图像拼接、目标跟踪、图像检索等应用中非常有用,目前openCV可以使用两种方法进行特征匹配:

  • Brute-Force暴力匹配:Brute-Force匹配的优点是简单直观,易于实现。然而,它的计算复杂度较高,特别是在具有大量特征点的情况下。因此,在处理大规模数据时,Brute-Force匹配可能会变得相对缓慢。

  • FLANN最近邻匹配:使用cv::FlannBasedMatcher类,它基于特征描述子之间的距离来找到最相似的k个特征点,并根据一定的阈值和筛选规则来决定是否进行匹配,这里笔者采用的是Flann匹配法。

    • 直接匹配:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 使用FLANN匹配器进行特征匹配
    //匹配处理器
    cv::Ptr<cv::FlannBasedMatcher> flann = cv::FlannBasedMatcher::create();
    //匹配结果列表
    std::vector<cv::DMatch> matches;
    //匹配结果处理
    flann->match(descriptors1, descriptors2, matches);

    // 绘制匹配结果
    cv::Mat matchImage;
    cv::drawMatches(image1, keypoints1, image2, keypoints2, matches, matchImage);

    // 显示匹配结果
    cv::imshow("SIFT Matches", matchImage);
    cv::waitKey(0);

    image-20231109203218795

    可以看到,直接匹配有太多的错误点,我们需要针对此进行优化

优化

距离比例筛选(Distance Ratio Filtering)

在特征匹配中,距离比例筛选是一种常用的筛选方法,用于排除一些不可靠的匹配结果。通过比较匹配结果中最近邻和次近邻之间的距离比例,可以判断匹配的可靠性。通常情况下,如果最近邻的距离远远小于次近邻的距离,那么可以认为这是一个可靠的匹配。

笔者通过遍历所有的匹配结果(matches向量),对每个匹配结果进行以下判断:如果第一个最近邻的距离小于距离比例阈值k倍数的第二个最近邻的距离,将该匹配结果添加到新的匹配结果中,从而排除不可靠的匹配。

  • 我们对flann匹配代码部分进行重写,首先我们需要计算出新的次近邻匹配

    1
    2
    3
    4
    5
    6
    //匹配处理器
    cv::Ptr<cv::FlannBasedMatcher> flann = cv::FlannBasedMatcher::create();
    //匹配结果列表
    std::vector<std::vector<cv::DMatch>> matches;
    //匹配结果处理
    flann->knnMatch(descriptors1, descriptors2, matches,2);
  • 定义一个新的Dmatch变量good,用来存放好的匹配结果,同时设置距离比例阈值,通过调整阈值,我们可以对匹配结果进行筛选,最后通过这个good匹配结果生成匹配点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    std::vector<cv::DMatch> good;
    float ratio = 0.65;
    for (size_t i = 0; i < matches.size(); i++) {
    if (matches[i][0].distance < ratio * matches[i][1].distance) {
    good.push_back(matches[i][0]);
    }
    }
    // 绘制匹配结果
    cv::Mat matchImage;
    cv::drawMatches(image1, keypoints1, image2, keypoints2, good, matchImage);

    // 显示匹配结果
    cv::imshow("SIFT Matches", matchImage);
    cv::waitKey(0);

匹配结果

可以看到,匹配结果精确了很多。

image-20231109212448428

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//
// Created by maric on 23-10-9.
//
#include <opencv2/opencv.hpp>
using namespace std;
const int MIN_MATCH_COUNT = 10;
//SIFT匹配的算法,包含各语句含义解释
int main() {
// 读取两个图像
cv::Mat image1 = cv::imread("/home/maric/CLionProjects/reconstruction/s2/B21.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat image2 = cv::imread("/home/maric/CLionProjects/reconstruction/s2/B22.jpg", cv::IMREAD_GRAYSCALE);

// 定义掩码图像
cv::Mat mask1 = cv::Mat::zeros(image1.size(), CV_8U); //设置掩码大小与图片等大,且全部置零
cv::Mat mask2 = cv::Mat::zeros(image2.size(), CV_8U);

// 划定采样区域的矩形
cv::Rect roi1(100, 30, 300, 600); //从(100,30)坐标框选(300,600)大小区域作为采样区
cv::Rect roi2(100, 30, 300, 600);
mask1(roi1).setTo(255); //将该区域掩码设置为255
mask2(roi2).setTo(255);

// 创建SIFT特征检测器SS
cv::Ptr<cv::Feature2D> sift = cv::SIFT::create
(
1000,
5,
0.03,
10.0,
1.6,
false
);//创建SIFT

// 检测特征点和计算描述符
//关键点列表: 图像中的特殊点,具有显著特征的位置,用于定位
std::vector<cv::KeyPoint> keypoints1, keypoints2;
//关键描述列表: 包含了某点及附近向量信息的描述,用来确定唯一点
cv::Mat descriptors1, descriptors2;
//参数(输入图片,掩码,关键点向量(列表),关键描述向量(列表))
sift->detectAndCompute(image1, mask1, keypoints1, descriptors1);
sift->detectAndCompute(image2, mask2, keypoints2, descriptors2);

// 使用FLANN匹配器进行特征匹配
//匹配处理器
cv::Ptr<cv::FlannBasedMatcher> flann = cv::FlannBasedMatcher::create();
//匹配结果列表
std::vector<std::vector<cv::DMatch>> matches;
//匹配结果处理
flann->knnMatch(descriptors1, descriptors2, matches, 2);

//将符合条件的特征点分别存入p1和p2
std::vector<cv::DMatch> good;
float ratio = 0.65;
for (size_t i = 0; i < matches.size(); i++) {
if (matches[i][0].distance < ratio * matches[i][1].distance) {
good.push_back(matches[i][0]);
}
}
// 绘制匹配结果
cv::Mat matchImage;
cv::drawMatches(image1, keypoints1, image2, keypoints2, good, matchImage);

// 显示匹配结果
cv::imshow("SIFT Matches", matchImage);
cv::waitKey(0);

}