Mastering Opencv ch3: markerless AR(一)

最近分析了下mastering OpenCV 第三章markerless AR的源码,这个例子是基于自然标志物的增强现实,在这个例子中,使用一个图片作为模板标识,这个图片应该有一定的特征,感觉像天空大海什么的,没有太多特征的图片,在这上面做增强现实,就很不好弄。
源码及编译文件下载地址:无标识的AR的OpenCV实现-Linux
好把,少扯淡,赶紧上源码分析。
我觉得能看到这个主题博客的,对于编译什么的都没问题吧,要么是VS在windows上,要么linux终端或QT上,MAC本人屌丝,没玩过,不过对于编译OpenCV来说,想来也差不多吧。
这里要注意的一点是,编译OpenCV的时候要添加支持OpenGL,因为本源码是基于OpenGL GUI窗口的,这和第二章中的使用GLUT作为显示窗口是有区别的,不过,要是不想弄OpenCV编译的话,改成GLUT显示的也不太费事,我这里并没有改,说好的源码分析,就按源码来吧。
注意:sudo apt-get install libgtkglext1 libgtkglext1-dev
要编译支持OpenGL的OpenCV,在有OpenGL的情况下,要安装这两个库。才能在cmake时配置OpenCV support。

关于人工标识和自然标识对实现增强现实的区别,请自行百度,这里只提下,自然标识的更好玩点,全是人工标识,这没法发展啊。好吧,对部分遮挡可以识别,可以做到更好的虚实融合。

Begin:
1:首先找main函数,在main函数开始,

 // Change this calibration to yours:
    CameraCalibration calibration(526.58037684199849f, 524.65577209994706f, 318.41744018680112f, 202.96659047014398f);

这里是对相机的内参数进行处理,这个调用在CameraCalibration.hpp和.cpp中进行实现,这几个参数就是在相机校正时得到的内参数矩阵中的值,关于求相机内参数的问题请查看前几篇博客,有详细的说明,这里只使用了内参数,在后面会构造内参数矩阵,作为透视变换的矩阵,失真向量全为0。

2:接下来开始检测在运行程序时,输入参数的个数,若小于2个,那退出,等于两个,这个就是摄像头实时拍摄了。

 cv::VideoCapture cap;
 if (cap.open(0))
    {
      processVideo(patternImage, calibration, cap);
    }

在proccessVideo开头添加一下,就可以得到一帧帧图像了。

    cv::Mat currentFrame;  
    capture >> currentFrame;

    // Check the capture succeeded:
    if (currentFrame.empty())
    {
        std::cout << "Cannot open video capture device" << std::endl;
        return;

输入参数是三个的时候,两种情况,一种是视频,这个和上面差不多;另一种是图片,下面代码是单幅图片处理的方式。

        std::string input = argv[2];
        cv::Mat testImage = cv::imread(input);
        if (!testImage.empty())
        {
            processSingleImage(patternImage, calibration, testImage);
        }

2:在处理单幅图像或者视频摄像头时,除了采集图像的方式不同,接下来就是一致的了。根据输入的模板图像,进行建立模板图像特征描述子,训练优化匹配器。
如果我们处理的图像,通常是24位的彩色的,分辨率为640*480,912kb数据的图像。我们在真实世界怎样找到我们的模式图像呢?像素到像素的匹配花费很长时间并且我们也需要处理旋转和尺度化。这肯定不是可选的。使用特征点可以解决这个问题。通过检测特征点,我们可以确保返回含有大量信息的图像的特征描述部分(这是因为,基于角点检测器返回边缘,角点和其他尖锐形状)。因为为了找到两帧的对应部分,我们仅需要匹配关键点。

从关键点定义的块部分,我们提出一个矢量,特征描述子。它是特征点的一个表现形式。从特征点提出描述子的方法有很多。他们都有优点和弱点。例如,SIFT和SURT描述子提出算法是密集型但是提带有好的区分性的鲁棒性的描述子。在我们的样本工程中,我们使用ORB描述子提取算法。因为我们也选它作为一个特征检测器。

注意:使用来自同一个算法的特征检测器和描述子提取总是一个好的方法,因为他们相互完美的适合。

特征描述子表现为固定大小的矢量(16或者更多元素)。我们假定我们的图像拥有640*480像素的分辨率,并且含有1500个特征点。那么,将需要1500*16*sizeof(float)=96kb(对于Surf)。
它比原图像的数据小十倍。同样的,更容易操作描述子而不是光栅位图。对于两个特征描述子,我们可以引入一个相似度的评分——一个定义两个矢量相似水平的度量标准。通常使用L2范数或者汉明距离(基于使用的特征描述子的类型)。

ARPipeline pipeline(patternImage, calibration);//检测模板图像的关键点,然后计算特征描述子,训练一个特征描述子匹配器。

这句话在ARPipeline类的构造函数中实现,

ARPipeline::ARPipeline(const cv::Mat& patternImage, const CameraCalibration& calibration)
  : m_calibration(calibration)
{
  m_patternDetector.buildPatternFromImage(patternImage, m_pattern);//从模板图像中建立标识的特征描述子。
  m_patternDetector.train(m_pattern);//训练标识
}
void PatternDetector::buildPatternFromImage(const cv::Mat& image, Pattern& pattern) const
{
    int numImages = 4;
    float step = sqrtf(2.0f);//步长是2的平方根

    // Store original image in pattern structure
    pattern.size = cv::Size(image.cols, image.rows);
    pattern.frame = image.clone();//复制整个Mat的数据,不只是Mat的矩阵头和矩阵指针
    getGray(image, pattern.grayImg);//灰度化输入图像,这是整个程序的开始过程啊,感觉!!!!//中间这些暂时还用不到,用到了再说。
    。
    。
extractFeatures(pattern.grayImg, pattern.keypoints, pattern.descriptors);//图像特征提取,看下面的程序分析
}

这里要注意看一下在PatternDetector.hpp中对角点检测器,特征描述子提取器,匹配器的预设置。
OpenCV有几个特征检测算法。他们都是来源于基类cv::FeatureDetector。创建特征检测算法可以通过两个方式:
1、 通过显式调用具体的特征检测器类的构造函数
cv::Ptr detector=cv::Ptr(new cv::SurfFeatureDetector());
2、 或者通过算法的名字创建一个特征检测子
cv::Ptr detector=cv::FeatureDetector::create(“SURF”);
这两个方法都有各自的优点,因此选择你最喜欢的那一个。显式类的创建允许你给特征检测构造函数传递额外的参数,同时,通过算法名字的创建使得在运行时切换算法更容易。

在这里将使用特征点(feature point)术语,它是定义了一个中心点,半径和方向的图像中的一部分。每一个特征检测算法试图检测同样的特征点,忽略透视变换的应用,也就是特征检测算法容忍旋转变换和尺度变换,光照变化。
角点检测是分析图像中的边缘。一种基于角点的边缘检测算法寻找图中梯度的快速变化。通常,这种做法通过寻找图像梯度在x,y方向上的一阶导数的极值来实现。
特征点的方向通常使用在特征区域的占优势的的图像梯度方向来计算。当图像旋转或者尺度化,占优势梯度(dominant gradient)的方向通过特征检测算法重新计算。这意味着不管图像旋转,特征点的方向不会改变。这种特征成为旋转不变。
同样地,我们必须提及一些特征点的大小。一些特征检测算法使用固定大小的特征,然而其他一些算法分别地计算每个关键点的最优化大小。知道了特征点的大小,我们可以在尺度化的中找到同样的特征点。这使得特征尺度不变性。

cv::Ptr<cv::FeatureDetector>     detector  = new cv::ORB(1000), //FeatureDetetor是虚类,通过定义FeatureDetector的对象可以使用多种特征检测方法,ORB是一种检测算法。ORB(1000)是构造函数,初始化参数。感觉这句话就是新建一个基于ORB的特征检测类的实例:detector
        cv::Ptr<cv::DescriptorExtractor> extractor = new cv::FREAK(false, false), //DescriptorExtractor 的子类都是描述子提取器,包括FRAKE。// 用Freak特征来描述特征点
        cv::Ptr<cv::DescriptorMatcher>   matcher   = new cv::BFMatcher(cv::NORM_HAMMING, true),// 特征匹配,计算Hamming距离,这个已经支持交叉验证,建立 cv::BFMatcher将第二参数声明为true

分别使用了ORB算法进行角点检测,用Freak对检测到的角点进行特征提取,BFMatcher是暴力匹配器,还有一种是flann匹配器,通过knn计算得到最佳匹配,和次佳匹配,最佳匹配和次佳匹配大于一定比值,就把最佳匹配作为匹配组合。ORB检测是FAST特征检测器的一个修改。最初的FAST检测器惊人的快但是不计算关键点的方向和大小。幸运的是,ORB算法估计关键点的方向,但是特征大小任然是固定的。

//使用指定的角点检测算法对输入的模板图像检测出角点,存在容器里。由检测出的角点使用特征提取算法计算特征描述子,都保存在pattern结构体中。
bool PatternDetector::extractFeatures(const cv::Mat& image, std::vector& keypoints, cv::Mat& descriptors) const
{
    assert(!image.empty());
    assert(image.channels() == 1);
    //检测到的特征点存储在keypoints容器中。每一个关键点包含它的中心,半径,角度和分数,以及一些与特征点相关的”质量”或者”强度”。每一个特征检测算法拥有自己的评分计算算法,因此比较通过特定的检测算法得到的关键点的评分是很有效的。
    m_detector->detect(image, keypoints);//检测模板图案中的角点,保存在keypoints中,这是一个角点的容器。
    if (keypoints.empty())
        return false;

    m_extractor->compute(image, keypoints, descriptors);//根据检测到的图像中的关键点计算描述子.
    if (keypoints.empty())//这里是不是应该是descriptors
        return false;

    return true;
}

基于角点的检测器使用灰度图像找到特征点。描述子提取算法同样的处理灰度图像。当然,他们两个可以隐式地做颜色转换。但是,这这种情况下,颜色转换将做两次。我们可以通过做输入图像的隐式转换到灰度图像来提高性能,并且用这种方法做特征检测和描述子的抽取。

这里就得到了模板图案的角点和特征描述子,接下来就进行训练这些特征,进行特征点匹配。
寻找帧到帧中对应部分的过程可以阐述为从一组描述子从找到与另外一个描述子最相邻的描述子。这称为匹配过程。在OpenCV中有两个主要的用来描述子匹配的算法:
1、 Brute-force 匹配器 (cv::BFMacher)
2、 Flann-based匹配器(cv::FlannBasedMatcher)

Brute-force匹配器在第一个描述子集中通过尝试每一个,寻找在第二个描述子集中最近的描述子(穷举搜索)。cv::FlannBasedMatcher使用快速近似邻近搜索算法来找到相应的部分。(该算法使用第三方库作为最临近算法的库)

描述子匹配的结果是两个描述子集的一系列的对应部分。描述子的第一个集通常成为训练集,因为它对应于我们的模式图像。第二个集成为查询集,因为它属于我们将要搜索的图像。找到越多的正确匹配(多模式图像对应的存在),在图像上模式呈现更高的可能性。

为了提高匹配速度,在调用匹配函数之前,你可以训练一个匹配器。训练阶段可用于最优化cv::FlannBaseMatcher的性能。为此,train函数将要为训练描述子建立索引树。并且对于大的数据集这将提高匹配的速率(例如,如果你想从数百张图像中什么找到一个匹配的图像)。对于cv::BFmatcher,train函数不做什么,因为没有什么要处理的。在内部它简单地存储训练描述子。

//就是把特征描述子加入到匹配器中,然后对这些描述子进行训练,训练一个描述子匹配器,默认是flann,训练这个算法的内部结构,应该更适合这个描述子。
void PatternDetector::train(const Pattern& pattern)
{
    // Store the pattern object
    //保存模板图像数据结构体,有角点数据,描述子数据。
    m_pattern = pattern;

    // API of cv::DescriptorMatcher is somewhat tricky
    // First we clear old train data:
    //首先清除训练数据
    m_matcher->clear();//清空特征描述子训练集.

    // Then we add vector of descriptors (each descriptors matrix describe one image). 
    // This allows us to perform search across multiple images:
    //然后增加描述符向量,包含很多描述符,每一个描述符矩阵描述一个图像,这可以让我们在多个图像中进行搜索,当下就用了一个描述符
    std::vector descriptors(1);
    descriptors[0] = pattern.descriptors.clone(); //把模板图像的描述符复制给描述符矩阵
    m_matcher->add(descriptors);//在描述符匹配器中增加描述符。增加特征描述子用于特征描述子集训练.在之后的匹配中,已经默认有这个训练描述符子集了。

    // After adding train data perform actual train:
    m_matcher->train();//利用指定的描述符匹配器进行训练模板匹配器,训练一个特征描述子匹配器,在匹配之前都要先进行训练的。为训练描述子建立索引树,更快查找。
}

这样之后我们就对模板图像处理完了,检测角点,提取特征描述子,训练一个匹配器,为提取的特征描述子建立索引树,便于数据量比较大时进行快速匹配。这个匹配器是后面重点应用的,重点关注。

下一篇文章将继续进行分析。

你可能感兴趣的:(AR)