Mastering Opencv ch3: markerless AR(二)

来源:互联网 发布:国家顶级域名是 编辑:程序博客网 时间:2024/05/19 17:05

上一篇分析了特征点和匹配器的问题,使用模板图像得到了优化的匹配器,匹配器里有建立索引表的训练特征描述子,用于匹配。

这一篇将详细分析测试图像的问题:
还是看main函数,接上一节的

ARDrawingContext drawingCtx("Markerless AR", frameSize, calibration);//这个应该是OpenGL初始化的阶段。

这个是OpenGL的初始化阶段,先不关注。
1:在processFrame函数中找到这一行

 drawingCtx.isPatternPresent = pipeline.processFrame(cameraFrame);//计算位姿矩阵

这是接下来重点关注的。通过追踪,我们将追到下面:

bool PatternDetector::findPattern(const cv::Mat& image, PatternTrackingInfo& info){    // Convert input image to gray    getGray(image, m_grayImg);//这里是优化性能,因为检测角点和提取特征描述子都需要灰度化图像,这样就减小一次灰度化。#if _DEBUG    cv::showAndSave("Gray image",m_grayImg);#endif        // Extract feature points from input gray image,这里是计算测试图像的角点和特征描述子。和模板图像的计算一致。    extractFeatures(m_grayImg, m_queryKeypoints, m_queryDescriptors);    // Get matches with current pattern    getMatches(m_queryDescriptors, m_matches);//对测试图像的特征描述子,在训练描述子中找到一个最佳匹配,二者组成一个匹配,放在匹配集中。在下面有具体函数实现。#if _DEBUG    //给出两幅图,以及在这两幅图中的角点(待查询的角点特征描述子,模板角点特征描述子),及匹配集合,绘制匹配图。内部调用OpenCV的函数,最后一个参数是只绘制100个匹配。这里绘制的是很粗糙的匹配图,有很多匹配错误。    cv::showAndSave("Raw matches", getMatchesImage(image, m_pattern.frame, m_queryKeypoints, m_pattern.keypoints, m_matches, 100));#endif

这里写图片描述
//这个就是根据给定的查询描述子集,之前计算出的训练描述子集,对每一个查询描述子集,找到一个最佳的训练描述子,并把找到的匹配放在容器里,就是二者的下标,匹配距离等一些参数。
/*OpenCV提供了两种Matching方式:
• Brute-force matcher (cv::BFMatcher)
• Flann-based matcher (cv::FlannBasedMatcher)
Brute-force matcher就是用暴力方法找到点集一中每个descriptor在点集二中距离最近的descriptor;
Flann-based matcher 使用快速近似最近邻搜索算法寻找(用快速的第三方库近似最近邻搜索算法)
一般把点集一称为 train set (训练集)对应模板图像,点集二称为 query set(查询集)对应查找模板图的目标图像。
为了提高检测速度,你可以调用matching函数前,先训练一个matcher。训练过程可以首先使用cv::FlannBasedMatcher来优化,为descriptor建立索引树,这种操作将在匹配大量数据时发挥巨大作用(比如在上百幅图像的数据集中查找匹配图像)。而Brute-force matcher在这个过程并不进行操作,它只是将train descriptors保存在内存中。*/

void PatternDetector::getMatches(const cv::Mat& queryDescriptors, std::vector<cv::DMatch>& matches)//DMatch用于匹配特征关键点的特征描述子的类:查询特征描述子索引, 特征描述子索引, 训练图像索引, 以及不同特征描述子之间的距离.{    matches.clear();    if (enableRatioTest)    {        // To avoid NaN's when best match has zero distance we will use inversed ratio.         ////为了避免NaNs,当最好的匹配是零距离时,我们将使用相反的比例        const float minRatio = 1.f / 1.5f;        // KNN match will return 2 nearest matches for each query descriptor给定查询集合中的每个特征描述子,寻找K个最佳匹配。        //使用KNN-matching算法,令K=2。则每个match得到两个最接近的descriptor,然后计算最接近距离和次接近距离之间的比值,当比值大于既定值时,才作为最终match。        m_matcher->knnMatch(queryDescriptors, m_knnMatches, 2);        for (size_t i=0; i<m_knnMatches.size(); i++)        {            const cv::DMatch& bestMatch   = m_knnMatches[i][0];//最佳匹配            const cv::DMatch& betterMatch = m_knnMatches[i][1];//次佳匹配            float distanceRatio = bestMatch.distance / betterMatch.distance;//计算这俩的比值            // Pass only matches where distance ratio between             // nearest matches is greater than 1.5 (distinct criteria)            if (distanceRatio < minRatio)//如果比值小于最小比率,则把那个最佳匹配放在匹配集合里返回。            {                matches.push_back(bestMatch);            }        }    }    else    {        // Perform regular match//这个就是寻找最佳匹配,应该是用的初始化里的匹配方法。        m_matcher->match(queryDescriptors, matches);    }}

以上得到初始的匹配,匹配阶段错误的匹配可能发生。这很正常。在匹配中有两种类型的错误。
1、 错误的正样本匹配:当特征点的对应是错误的。
2、 错误负样本匹配:当特征点在两个图像上可视时,没有匹配。

错误负样本匹配是明显的错误。但是,我们不能处理他们,因为匹配算法已经拒绝了他们。
因此我们的目的是最小化错误正样本匹配的数量。为了拒绝错误的相应部分,我们可以使用交叉匹配技术。这种思想是用查询集匹配训练描述子,反之亦然。只返回这两个匹配的共同匹配。当有足够的匹配时,这种技术通常产生最好的结果,带有最少量的异常值。
比率检定:
第二个众所周知的异常值移除的计算叫做比率检定。我们首先执行KNN-matching,参数K=2。对于每一个匹配返回最临近的描述子。仅当第一个和第二个匹配的距离率足够大,匹配才返回,否则就被去除。(ratio阈值通常接近与2)。

2:比率检定可以移除几乎所用的异常值。但是在一些情况下,错误正样本匹配可以通过这个检定。在这一部分,我们将向你展示怎样移除剩下的异常值并且只留下正确的匹配。
为了进一步改善我们的匹配,我们使用随机采样一致性方法来过滤异常值。当我们对图像进行处理时(一个平面物体)并且我们期望它是刚性的。在模板图像特征点和测试图像查询特征点之间找到单应性变换矩阵。单应性变换矩阵将点集从模板图像带到测试图像坐标系统。为了找到这种变换。我们使用cv::findHomography函数。它使用随机采样一致性通过输入点子集的搜索得到的单应性矩阵。作为一个副作用,这个函数标记每一个相应的顶点围线和异常值,这取决于计算得到的单应性的重投影误差。

//#if _DEBUG    cv::Mat tmp = image.clone();//#endif    // Find homography transformation and detect good matches,若找到八组以上优化后的匹配,就返回1.    bool homographyFound = refineMatchesWithHomography(//计算单应性矩阵,然后根据匹配组合是否是离群值得到最后的优化匹配组,去除离群值即错误的匹配。最后一个参数是单应性矩阵。下面有函数定义。        m_queryKeypoints,         m_pattern.keypoints,         homographyReprojectionThreshold,         m_matches,         m_roughHomography);    if (homographyFound)    {//#if _DEBUG  //这里显示的是优化后的匹配图像。去除了错误的匹配        cv::showAndSave("Refined matches using RANSAC", getMatchesImage(image, m_pattern.frame, m_queryKeypoints, m_pattern.keypoints, m_matches, 100));//#endif       

这里写图片描述

3:下面对测试灰度图利用单应性矩阵进行透视变换。计算得到的透视灰度图的特征描述子,并与模板图进行匹配,计算透视图与模板图之间的单应性矩阵,并得到二者的匹配优化组合。
当我们查找单应性转换时,我们已经有了所有需要的角点数据来找到他们在三维空间中的位置。然而,我们可以通过找到更精确的模板角点来进一步改善他们的位置。我们使用估计的单应性矩阵将输入图像变换以获得已经找到的一个模式。结果应当非常接近于源训练图像。单应性提纯可以帮助我们找到更精确的单应性转换。

// If homography refinement enabled improve found transformation        if (enableHomographyRefinement)//是否进行二次单应性变换        {            // Warp image using found homography 使用单应性矩阵进行透视变换            //第四个参数是若指定 matrix 是输出图像到输入图像的反变换,因此可以直接用来做象素插值。否则, 函数从 map_matrix 得到反变换。双三次插值。            cv::warpPerspective(m_grayImg, m_warpedImg, m_roughHomography, m_pattern.size, cv::WARP_INVERSE_MAP | cv::INTER_CUBIC);//这里得到的是测试图像经单应性矩阵变换后的灰度图像。差不多就是一个和模板图像类似的正面灰度图,是把测试图像中和模板图像匹配的区域变换正面的。在人工描述里,也有这一步。#if _DEBUG            cv::showAndSave("Warped image",m_warpedImg);//这里显示的是测试灰度图像通过单应性矩阵反变换差值得到的图像,只有与模板图案一致的图像。#endif            // Get refined matches:            std::vector<cv::KeyPoint> warpedKeypoints;            std::vector<cv::DMatch> refinedMatches;            // Detect features on warped image            extractFeatures(m_warpedImg, warpedKeypoints, m_queryDescriptors);            // Match with pattern            getMatches(m_queryDescriptors, refinedMatches);            // Estimate new refinement homography            homographyFound = refineMatchesWithHomography(//这里是计算经过之前计算的单应性矩阵反变换后的透视图像与模板图案的单应性矩阵,得到匹配组合                warpedKeypoints,                 m_pattern.keypoints,                 homographyReprojectionThreshold,                 refinedMatches,                 m_refinedHomography);#if _DEBUG   //显示的是透视图与模板重新得到的匹配组的图像            cv::showAndSave("MatchesWithRefinedPose", getMatchesImage(m_warpedImg, m_pattern.grayImg, warpedKeypoints, m_pattern.keypoints, refinedMatches, 100));#endif         

这里写图片描述
这里写图片描述

4:计算最终的优化的单应性矩阵(这个在计算模板图像在测试图像中的三维位置时更精确。),这个矩阵将对模板图像的四个顶点进行变换,转换成在测试图像上的匹配区域的四个顶点,计算这四个点的二维坐标,并画出矩形。

// Get a result homography as result of matrix product of refined and rough homographies:  info.homography = m_roughHomography * m_refinedHomography;//得到最终的单应性矩阵,是第一次计算的单应性矩阵与第二次得到的相乘。            // Transform contour with rough homography#if _DEBUG  cv::perspectiveTransform(m_pattern.points2d, info.points2d, m_roughHomography);//使用第一次计算的单应性矩阵对模板图像的四个顶点变换成目标图像的四个顶点。            info.draw2dContour(tmp, CV_RGB(0,200,0));//用直线把这四个顶点连起来。#endif            // Transform contour with precise homography    cv::perspectiveTransform(m_pattern.points2d, info.points2d, info.homography);//使用优化后的单应性矩阵进行转换。在测试图像上得到用优化的单应性矩阵计算出的四个顶点的二维坐标。#if _DEBUG            info.draw2dContour(tmp, CV_RGB(200,0,0));#endif        }#if _DEBUG    cv::showAndSave("tmp image",tmp);#endif      }//#if _DEBUG    if (1)    {        //这个还是第一次变换后的匹配,不过程序不退出就一直运行,得到的图像也几乎不变。        cv::showAndSave("Final matches", getMatchesImage(tmp, m_pattern.frame, m_queryKeypoints, m_pattern.keypoints, m_matches, 100));    }    std::cout << "Features:" << std::setw(4) << m_queryKeypoints.size() << " Matches: " << std::setw(4) << m_matches.size() << std::endl;//#endif    return homographyFound;}

这里写图片描述

这里写图片描述

5:接下来将计算位姿矩阵,包括旋转矩阵和平移矩阵。
模板图像的2D和3D轮廓是初始化的时候就给出的:

// Build 2d and 3d contours (3d contour lie in XY plane since it's planar)    pattern.points2d.resize(4);//2D边缘向量的大小是4,这里感觉是把point2d的容器大小变为4,使用默认的构造函数构造两个新的元素。这里的容器里的元素是一个个的二维点    pattern.points3d.resize(4);//3D边缘是XY平面,因为是平面的。这个是三维点的容器。    // Image dimensions图像尺寸    const float w = image.cols;    const float h = image.rows;    // Normalized dimensions:标准化尺寸    const float maxSize = std::max(w,h);    const float unitW = w / maxSize;    const float unitH = h / maxSize;    //这里和mark一样处理成大小已知的图案。得到模板图案的大小,四个顶角点的相对坐标。在下面将计算经单应性矩阵变换的四个顶点坐标,就是在测试图像上的四个顶点坐标    pattern.points2d[0] = cv::Point2f(0,0);    pattern.points2d[1] = cv::Point2f(w,0);    pattern.points2d[2] = cv::Point2f(w,h);    pattern.points2d[3] = cv::Point2f(0,h);    //同理是模板图案坐标系统的四个角点的三维相对坐标。直接用于计算旋转矩阵和平移矩阵。    pattern.points3d[0] = cv::Point3f(-unitW, -unitH, 0);    pattern.points3d[1] = cv::Point3f( unitW, -unitH, 0);    pattern.points3d[2] = cv::Point3f( unitW,  unitH, 0);    pattern.points3d[3] = cv::Point3f(-unitW,  unitH, 0);
m_patternInfo.computePose(m_pattern, m_calibration);//得到标识在相机坐标系下的位姿矩阵,作为视景矩阵。

找到这个函数调用的函数,注意到还是用之前用过的一个函数,solvePnP:

void PatternTrackingInfo::computePose(const Pattern& pattern, const CameraCalibration& calibration){  cv::Mat Rvec;  cv::Mat_<float> Tvec;  cv::Mat raux,taux;  //第一个参数是模板图像的四个顶点的三维坐标,在初始化中给出,第二个参数是在测试图像中,由模板图像的四个顶点经单应性优化矩阵变化后得到的四个顶点的二维坐标,然后就是相机内参数和失身参数,得出两个向量,旋转向量和平移向量。旋转向量要转换成旋转矩阵。  cv::solvePnP(pattern.points3d, points2d,   calibration.getIntrinsic(), calibration.getDistorsion(),raux,taux);  raux.convertTo(Rvec,CV_32F);//转换成32位浮点数  taux.convertTo(Tvec ,CV_32F);  cv::Mat_<float> rotMat(3,3);   cv::Rodrigues(Rvec, rotMat);//由向量转换成3x3矩阵。  // Copy to transformation matrix   把旋转矩阵和平移矩阵组合成位姿矩阵,在OpenGl中做视景矩阵。  for (int col=0; col<3; col++)  {    for (int row=0; row<3; row++)    {             pose3d.r().mat[row][col] = rotMat(row,col); // Copy rotation component    }    pose3d.t().data[col] = Tvec(col); // Copy translation component  }  // Since solvePnP finds camera location, w.r.t to marker pose, to get marker pose w.r.t to the camera we invert it.  //计算出的位姿矩阵是相机在标识坐标系下的转换矩阵,要进行转换,变成标识在相机坐标系下的位姿矩阵。  pose3d = pose3d.getInverted();}

至此,我们就求出了模板图像在相机坐标系中的位姿矩阵,这个是在进行虚实融合时,对虚拟目标进行视景矩阵变换渲染的。
以上我们做的事,简短的列一下我们执行的步骤:
1、 转换输出图到灰度图像
2、 使用我们的特征检测算法在查询图像上检测特征
3、 从输入的图像中检测到的特征点中抽取描述子
4、 与模式描述子进行匹配
5、 使用交叉验证或者比率检定移除异常值
6、 使用内围层匹配找到单应性变换
7、 通过使用上一步骤得到的单应性变换查询图像来提纯单应性。
8、 找到精确的单应性作为粗略的增值和提纯的单应性
9、 转换模式角点到一副图像的坐标系统,来获得模式在这个图像上的位置。

下一篇将分析OpenGL生成虚拟目标的代码。

0 0
原创粉丝点击