OpenCV开发笔记(八十):基于特征点匹配实现全景图片拼接

前言

一个摄像头视野不大的时候,我们希望进行两个视野合并,这样让正视的视野增大,从而可以看到更广阔的标准视野。拼接的方法分为两条路,第一条路是Sticher类,第二条思路是特征点匹配。

本篇使用特征点匹配,进行两张图来视野合并拼接。

Demo

100%的点匹配


  换了一幅图:


  所以,opencv传统的方式,对这些特征点是有一些要求的。(注意:这两张图使用sStitcher类实现全景图片拼接,是无法拼接成功的!!!)

两张图的拼接过程

步骤一:打开图片

cpp 复制代码
 cv::Mat leftImageMat = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/30.jpg"); cv::Mat rightImageMat = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/31.jpg"); 

步骤二:提取特征点

cpp 复制代码
// 提取特征点
cv::Ptr<cv::xfeatures2d::SurfFeatureDetector> pSurfFeatureDetector = cv::xfeatures2d::SurfFeatureDetector::create(); std::vector<cv::KeyPoint> leftKeyPoints; std::vector<cv::KeyPoint> rightKeyPoints; cv::Mat leftMatch; cv::Mat rightMatch; std::vector<cv::DMatch> matches; pSurfFeatureDetector->detectAndCompute(leftImageGrayMat, cv::Mat(), leftKeyPoints, leftMatch); pSurfFeatureDetector->detectAndCompute(rightImageGrayMat, cv::Mat(), rightKeyPoints, rightMatch); 

步骤三:暴力匹配

cpp 复制代码
// 暴力匹配
cv::Ptr<cv::FlannBasedMatcher> pFlannBasedMatcher = cv::FlannBasedMatcher::create(); pFlannBasedMatcher->match(leftMatch, rightMatch, matches); 

步骤四:提取暴力匹配后比较好的点

cpp 复制代码
// 筛选匹配点, 根据match的距离从小到大排序
std::sort(matches.begin(), matches.end()); // 筛选匹配点,根据排序留下最好的匹配点 std::vector<cv::DMatch> goodMatchs; // 阈值最小个数点 int count = 40; // 阈值点个数 小于 总量的10% 则使用 总量的10% int validPoints = (int)(matches.size() * 1.0f); if(validPoints > count) { count = validPoints; } // 所有匹配点小于阈值,那么就取所有点 if(matches.size() < count) { count = matches.size(); } // 将筛选出的点当作较好的点 for(int index = 0; index < count; index++) { goodMatchs.push_back(matches.at(index)); } // 匹配结果 cv::Mat matchedMat; // 绘制结果, 注意顺序 cv::drawMatches(leftImageMat, leftKeyPoints, rightImageMat, rightKeyPoints, goodMatchs, matchedMat); #if 1 cv::namedWindow("matchedMat", cv::WINDOW_NORMAL); cv::resizeWindow("matchedMat", cv::Size(800, 300)); cv::imshow("matchedMat", matchedMat); #endif 

步骤五:计算变换矩阵

cpp 复制代码
// 准备匹配的点
std::vector<cv::Point2f> leftImagePoints; std::vector<cv::Point2f> rightImagePoints; for(int index = 0; index < goodMatchs.size(); index++) { leftImagePoints.push_back(rightKeyPoints.at(goodMatchs.at(index).trainIdx).pt); rightImagePoints.push_back(leftKeyPoints.at(goodMatchs.at(index).queryIdx).pt); } // 使用暴力匹配的点计算透视变换矩阵 cv::Mat m = cv::findHomography(leftImagePoints, rightImagePoints, CV_RANSAC); 

步骤六:计算第二张图变换后的图像的大小

cpp 复制代码
// 计算第二张图的变换大小
cv::Point2f leftTopPoint2f;
cv::Point2f leftBottomPoint2f; cv::Point2f rightTopPoint2f; cv::Point2f rightBottomPoint2f; cv::Mat H = m.clone(); cv::Mat src = leftImageMat.clone(); { cv::Mat V1; cv::Mat V2; // 左上角(0, 0, 1) double v2[3] = {0, 0, 1}; // 变换后的坐标值 double v1[3]; //列向量 V2 = cv::Mat(3, 1, CV_64FC1, v2); V1 = cv::Mat(3, 1, CV_64FC1, v1); V1 = H * V2; leftTopPoint2f.x = v1[0] / v1[2]; leftTopPoint2f.y = v1[1] / v1[2]; // 左下角(0, src.rows, 1) v2[0] = 0; v2[1] = src.rows; v2[2] = 1; V2 = cv::Mat(3, 1, CV_64FC1, v2); V1 = cv::Mat(3, 1, CV_64FC1, v1); V1 = H * V2; leftBottomPoint2f.x = v1[0] / v1[2]; leftBottomPoint2f.y = v1[1] / v1[2]; // 右上角(src.cols, 0, 1) v2[0] = src.cols; v2[1] = 0; v2[2] = 1; V2 = cv::Mat(3, 1, CV_64FC1, v2); V1 = cv::Mat(3, 1, CV_64FC1, v1); V1 = H * V2; rightTopPoint2f.x = v1[0] / v1[2]; rightTopPoint2f.y = v1[1] / v1[2]; // 右下角(src.cols,src.rows,1) v2[0] = src.cols; v2[1] = src.rows; v2[2] = 1; V2 = cv::Mat(3, 1, CV_64FC1, v2); V1 = cv::Mat(3, 1, CV_64FC1, v1); V1 = H * V2; rightBottomPoint2f.x = v1[0] / v1[2]; rightBottomPoint2f.y = v1[1] / v1[2]; } 

步骤七:对第二张图进行举证变化且变为目标大小

cpp 复制代码
// 图像变换
cv::Mat imageTransform1;
cv::warpPerspective(rightImageMat, // 源图 imageTransform1, // 变换后的输出图 m, // 变换矩阵 cv::Size(MAX(rightTopPoint2f.x, rightBottomPoint2f.x), // 输出图像宽度 leftImageMat.rows)); // 输出图像高度 cv::namedWindow("imageTransform1", cv::WINDOW_NORMAL); cv::resizeWindow("imageTransform1", cv::Size(400, 300)); cv::imshow("imageTransform1", imageTransform1); 

步骤八:进行融合


  

cpp 复制代码
// 创建拼接后的图
int resultWidth = imageTransform1.cols; int resultHeight = rightImageMat.rows; if(imageTransform1.cols < leftImageMat.cols) { resultWidth = leftImageMat.cols; } if(imageTransform1.rows < leftImageMat.rows) { resultHeight = leftImageMat.rows; } cv::Mat resultMat(resultHeight, resultWidth, CV_8UC3); resultMat.setTo(0); ··· // 开始拷贝 LOG << imageTransform1.cols << imageTransform1.rows; LOG << "copy to"; LOG << resultMat.cols << resultMat.rows; imageTransform1.copyTo(resultMat(cv::Rect(0, 0, imageTransform1.cols, imageTransform1.rows))); LOG << rightImageMat.cols << rightImageMat.rows; LOG << "copy to"; LOG << resultMat.cols << resultMat.rows; leftImageMat.copyTo(resultMat(cv::Rect(0, 0, leftImageMat.cols, leftImageMat.rows))); 

步骤九:对重叠区域进行渐进色过度

由于实际效果出现重影,查阅相关资料,这部分可以消除一部分,但是需要深入研究,而且消除一部分也只是达到较可以的水平,并不能完全消除。

cpp有兴趣读者,可以深入研究沟通这块。

比较明确的方法就是查看Stitcher的源码研究,将其投影矩阵部分重叠消除的代码抠出来。

匹配点数调参测试

强制40个点

匹配探测到的10%的点

匹配探测到的60%的点

匹配探测到的100%的点

函数原型

函数static Ptr create

cpp 复制代码
static Ptr<SURF> create(double hessianThreshold=100, int nOctaves = 4, int nOctaveLayers = 3, bool extended = false, bool upright = false); 
  • 参数一:double类型的hessianThreshold,默认值100,用于SURF的hessian关键点检测器的阈值;
  • 参数二:int类型的nOctaves,默认值4,关键点检测器将使用的金字塔倍频程数;
  • 参数三:int类型的nOctaveLayers,默认值3,每八度音阶的层数。3是D.Lowe纸张中使用的值。这个八度音阶数是根据图像分辨率自动计算出来的;
  • 参数四:bool类型的extended,扩展描述符标志(true-使用扩展的128个元素描述符;false-使用64个元素描述符),默认false;
  • 参数五:bool类型的upright,向上向右或旋转的特征标志(true-不计算特征的方向;faslse-计算方向),默认false;

函数xfeatures2d::SURT::detectAndCompute

cpp 复制代码
// 该函数结合了detect和compute,参照detect和compute函数参数
void xfeatures2d::SURT::detectAndCompute( InputArray image, InputArray mask, std::vector<KeyPoint>& keypoints, OutputArray descriptors, bool useProvidedKeypoints=false ); 

detectAndCompute函数是OpenCV库中用于特征检测与计算的函数,其原型根据不同的特征检测器(如SIFT、SURF、ORB等)可能略有不同,但大体上遵循相似的参数结构。

  • 参数一:InputArray类型的image,输入cv::Mat;
  • 参数二:InputArray类型的mask,输入cv::Mat,不需要时,直接可以cv::Mat()即可;
  • 参数三:std::Vector类型的keypoints,原图的关键点;
  • 参数四:OutputArray类型的descriptors,计算描述符;
  • 参数五:如果设置为true,则函数会假设keypoints向量中已经包含了一些关键点,函数将只计算这些点的描述子,而不是重新检测关键点。

函数cv::FlannBasedMatcher::create

cpp 复制代码
static Ptr<BFMatcher> create() 

初始化暴力匹配实例。

函数cv::FlannBasedMatcher::match

cpp 复制代码
void BFMatcher::match(InputArray queryDescriptors, InputArray trainDescriptors, std::vector<DMatch>& matches, InputArray mask=noArray() ) const; 
  • 参数一:InputArray类型的queryDescriptors,查询描述符集,一般cv::Mat,某个特征提取的描述符。
  • 参数二:InputArray类型的trainDescriptors,训练描述符集,此处输入的应该是没有加入到类对象集合种的(该类有训练的数据集合),一般cv::Mat,某个特征提取的描述符。
  • 参数三:std::vector类型的matches。如果在掩码中屏蔽了查询描述符,则不会为此添加匹配项描述符。因此,匹配项的大小可能小于查询描述符计数。
  • 参数四:InputArray类型的mask,指定输入查询和训练矩阵之间允许的匹配的掩码描述符。

函数cv::drawMatches

cpp 复制代码
void drawMatches( InputArray img1,
                  const std::vector<KeyPoint>& keypoints1, InputArray img2, const std::vector<KeyPoint>& keypoints2, const std::vector<DMatch>& matches1to2, InputOutputArray outImg, const Scalar& matchColor=Scalar::all(-1), const Scalar& singlePointColor=Scalar::all(-1), const std::vector<char>& matchesMask=std::vector<char>(), int flags=DrawMatchesFlags::DEFAULT ); 
  • 参数一:InputArray类型的img1,图像1。
  • 参数二:std::vector类型的keypoints1,图像1的关键点。
  • 参数三:InputArray类型的img2,图像2。
  • 参数四:std::vector类型的keypoints2,图像2的关键点。
  • 参数五:std::vector类型的matchers1to2,从第一个图像匹配到第二个图像,这意味着keypoints1[i]在keypoints2中有一个对应的点[matches[i]]。
  • 参数六:InputOutputArray类型的outImg,为空时,默认并排绘制输出图像以及连接关键点;若不为空,则在图像上绘制关系点。
  • 参数七:Scalar类型的matcherColor,匹配颜色匹配(线和连接的关键点)的颜色。如果颜色为cv::Scalar::all(-1),则为随机颜色。
  • 参数八:Scalar类型的singlePointColor,颜色单个关键点(圆)的颜色,这意味着关键点没有匹配到的则认是该颜色。
  • 参数九:std::vector类型的matchersMask,确定绘制的匹配项目,若是为空,则表示全部绘制。
  • 参数十:int类型的flags,查看枚举DrawMatchesFlags,如下:
      

函数cv::findHomography

cv::findHomography 是 OpenCV 库中的一个函数,用于在两组二维点之间寻找单应性矩阵(Homography Matrix)。单应性矩阵是一个 3x3 的矩阵,它将一个平面上的点映射到另一个平面(可能是同一个平面,但通常是在不同的视角或条件下)上的点。这在计算机视觉和图像处理中非常有用,比如图像拼接、增强现实、相机姿态估计等领域。

cpp 复制代码
cv::Mat cv::findHomography(InputArray srcPoints, InputArray dstPoints, int method = 0, double ransacReprojThresh = 3, OutputArray mask = noArray(), const int maxIters = 2000, const double confidence = 0.995); 
  • 参数一:srcPoints,源图像中的点集,通常是 std::vectorcv::Point2f 或 cv::Mat 类型,其中每行代表一个点的 x 和 y 坐标。
  • 参数二:dstPoints:目标图像中对应的点集,与 srcPoints 格式相同。
  • 参数三:method:单应性矩阵的计算方法。常见的值有 0(使用所有点,默认方法)、cv::RANSAC(RANdom SAmple Consensus,基于随机样本一致性的鲁棒方法)、cv::LMEDS(Least-Median of Squares,基于最小中值平方的方法,对噪声和异常值有较好的鲁棒性)等。
  • 参数四:ransacReprojThresh,当 method 设置为 cv::RANSAC 或 cv::RANSAC_FIXED_POINT 时,此参数指定了重投影误差的最大阈值,用于判断点是否为内点。
  • 参数五:mask,输出参数,一个与输入点集同样大小的掩码,用于指示哪些点被认为是内点(即,在计算单应性矩阵时使用的点)。
  • 参数六:maxIters,当 method 为 cv::RANSAC 或 cv::LMEDS 时,此参数指定了算法的最大迭代次数。
  • 参数七:confidence,当 method 为 cv::RANSAC 时,此参数指定了算法计算出的单应性矩阵的置信度(即在估计的模型是正确模型的概率)。

函数cv::warpPerspective

cpp 复制代码
void cv::warpPerspective(InputArray src, OutputArray dst, InputArray M, Size dsize, int flags = INTER_LINEAR, int borderMode = BORDER_CONSTANT, const Scalar& borderValue = Scalar()); 
  • 参数一:src,输入图像,即要进行透视变换的原始图像。
  • 参数二:dst,输出图像,即透视变换后的图像。这个图像将与 dsize 指定的尺寸相同。
  • 参数三:M,3x3 的透视变换矩阵。这个矩阵定义了如何将 src 中的点映射到 dst 中的新位置。
  • 参数四:dsize,输出图像的尺寸。这个参数是必需的,因为透视变换可能会改变图像的大小。
  • 参数五:flags,插值方法。默认为 INTER_LINEAR,表示双线性插值。其他选项包括 INTER_NEAREST(最近邻插值)、INTER_CUBIC(双三次插值)等。
  • 参数六:borderMode,边界像素的外推方法。默认为 BORDER_CONSTANT,表示用恒定的值(borderValue)填充边界外的像素。其他选项包括 BORDER_REPLICATE(复制边缘像素)、BORDER_REFLECT(反射边缘像素)等。
  • 参数七:borderValue,当 borderMode 为 BORDER_CONSTANT 时,用于填充边界像素的值。默认为 Scalar(),即黑色。

函数cv::Mat::copyTo

cpp 复制代码
void cv::Mat::copyTo(OutputArray m, InputArray mask=noArray()) const; 

cv::Mat::copyTo 是 OpenCV 库中 cv::Mat 类的一个成员函数,用于将一个矩阵(图像、数组等)的内容复制到另一个矩阵中。这个函数非常有用,因为它允许你复制矩阵的全部或部分数据,同时可以选择性地更新目标矩阵的大小和类型。

  • 参数一:输出矩阵m,即复制内容的目标矩阵。如果 m 的大小和类型与源矩阵(调用 copyTo 的矩阵)不同,m 会被重新分配以匹配源矩阵的大小和类型。
  • 参数二(可选):mask:一个与源矩阵同样大小的8位单通道矩阵,用于指定哪些元素应该被复制到目标矩阵中。如果 mask 的某个位置的值非零,则源矩阵对应位置的元素会被复制到目标矩阵;如果为零,则目标矩阵对应位置的元素不会被修改(保持原样或初始化为零,取决于目标矩阵的初始状态)。

Demo源码

cpp 复制代码
void OpenCVManager::testSplicingImages() { cv::Mat leftImageMat = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/30.jpg"); cv::Mat rightImageMat = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/31.jpg"); #if 0 // 对图片进行缩放,测试其拼接耗时 cv::resize(mat1, mat1, cv::Size(1920, 1080)); cv::resize(mat2, mat2, cv::Size(1920, 1080)); #endif //灰度图转换 cv::Mat leftImageGrayMat; cv::Mat rightImageGrayMat; cv::cvtColor(leftImageMat, leftImageGrayMat, CV_RGB2GRAY); cv::cvtColor(rightImageMat, rightImageGrayMat, CV_RGB2GRAY); // 提取特征点 cv::Ptr<cv::xfeatures2d::SurfFeatureDetector> pSurfFeatureDetector = cv::xfeatures2d::SurfFeatureDetector::create(); std::vector<cv::KeyPoint> leftKeyPoints; std::vector<cv::KeyPoint> rightKeyPoints; cv::Mat leftMatch; cv::Mat rightMatch; std::vector<cv::DMatch> matches; pSurfFeatureDetector->detectAndCompute(leftImageGrayMat, cv::Mat(), leftKeyPoints, leftMatch); pSurfFeatureDetector->detectAndCompute(rightImageGrayMat, cv::Mat(), rightKeyPoints, rightMatch); // 暴力匹配 cv::Ptr<cv::FlannBasedMatcher> pFlannBasedMatcher = cv::FlannBasedMatcher::create(); pFlannBasedMatcher->match(leftMatch, rightMatch, matches); // 筛选匹配点, 根据match的距离从小到大排序 std::sort(matches.begin(), matches.end()); // 筛选匹配点,根据排序留下最好的匹配点 std::vector<cv::DMatch> goodMatchs; // 阈值最小个数点 int count = 40; // 阈值点个数 小于 总量的10% 则使用 总量的10% int validPoints = (int)(matches.size() * 1.0f); if(validPoints > count) { count = validPoints; } // 所有匹配点小于阈值,那么就取所有点 if(matches.size() < count) { count = matches.size(); } // 将筛选出的点当作较好的点 for(int index = 0; index < count; index++) { goodMatchs.push_back(matches.at(index)); } // 匹配结果 cv::Mat matchedMat; // 绘制结果, 注意顺序 cv::drawMatches(leftImageMat, leftKeyPoints, rightImageMat, rightKeyPoints, goodMatchs, matchedMat); #if 1 cv::namedWindow("matchedMat", cv::WINDOW_NORMAL); cv::resizeWindow("matchedMat", cv::Size(800, 300)); cv::imshow("matchedMat", matchedMat); #endif // 准备匹配的点 std::vector<cv::Point2f> leftImagePoints; std::vector<cv::Point2f> rightImagePoints; for(int index = 0; index < goodMatchs.size(); index++) { leftImagePoints.push_back(rightKeyPoints.at(goodMatchs.at(index).trainIdx).pt); rightImagePoints.push_back(leftKeyPoints.at(goodMatchs.at(index).queryIdx).pt); } // 使用暴力匹配的点计算透视变换矩阵 cv::Mat m = cv::findHomography(leftImagePoints, rightImagePoints, CV_RANSAC); // 计算第二张图的变换大小 cv::Point2f leftTopPoint2f; cv::Point2f leftBottomPoint2f; cv::Point2f rightTopPoint2f; cv::Point2f rightBottomPoint2f; cv::Mat H = m.clone(); cv::Mat src = leftImageMat.clone(); { cv::Mat V1; cv::Mat V2; // 左上角(0, 0, 1) double v2[3] = {0, 0, 1}; // 变换后的坐标值 double v1[3]; //列向量 V2 = cv::Mat(3, 1, CV_64FC1, v2); V1 = cv::Mat(3, 1, CV_64FC1, v1); V1 = H * V2; leftTopPoint2f.x = v1[0] / v1[2]; leftTopPoint2f.y = v1[1] / v1[2]; // 左下角(0, src.rows, 1) v2[0] = 0; v2[1] = src.rows; v2[2] = 1; V2 = cv::Mat(3, 1, CV_64FC1, v2); V1 = cv::Mat(3, 1, CV_64FC1, v1); V1 = H * V2; leftBottomPoint2f.x = v1[0] / v1[2]; leftBottomPoint2f.y = v1[1] / v1[2]; // 右上角(src.cols, 0, 1) v2[0] = src.cols; v2[1] = 0; v2[2] = 1; V2 = cv::Mat(3, 1, CV_64FC1, v2); V1 = cv::Mat(3, 1, CV_64FC1, v1); V1 = H * V2; rightTopPoint2f.x = v1[0] / v1[2]; rightTopPoint2f.y = v1[1] / v1[2]; // 右下角(src.cols,src.rows,1) v2[0] = src.cols; v2[1] = src.rows; v2[2] = 1; V2 = cv::Mat(3, 1, CV_64FC1, v2); V1 = cv::Mat(3, 1, CV_64FC1, v1); V1 = H * V2; rightBottomPoint2f.x = v1[0] / v1[2]; rightBottomPoint2f.y = v1[1] / v1[2]; } // 图像变换 cv::Mat imageTransform1; cv::warpPerspective(rightImageMat, // 源图 imageTransform1, // 变换后的输出图 m, // 变换矩阵 cv::Size(MAX(rightTopPoint2f.x, rightBottomPoint2f.x), // 输出图像宽度 leftImageMat.rows)); // 输出图像高度 cv::namedWindow("imageTransform1", cv::WINDOW_NORMAL); cv::resizeWindow("imageTransform1", cv::Size(400, 300)); cv::imshow("imageTransform1", imageTransform1); // 创建拼接后的图 int resultWidth = imageTransform1.cols; int resultHeight = rightImageMat.rows; if(imageTransform1.cols < leftImageMat.cols) { resultWidth = leftImageMat.cols; } if(imageTransform1.rows < leftImageMat.rows) { resultHeight = leftImageMat.rows; } cv::Mat resultMat(resultHeight, resultWidth, CV_8UC3); resultMat.setTo(0); // 开始拷贝 LOG << imageTransform1.cols << imageTransform1.rows; LOG << "copy to"; LOG << resultMat.cols << resultMat.rows; imageTransform1.copyTo(resultMat(cv::Rect(0, 0, imageTransform1.cols, imageTransform1.rows))); LOG << rightImageMat.cols << rightImageMat.rows; LOG << "copy to"; LOG << resultMat.cols << resultMat.rows; leftImageMat.copyTo(resultMat(cv::Rect(0, 0, leftImageMat.cols, leftImageMat.rows))); cv::namedWindow("rightImageMat", cv::WINDOW_NORMAL); cv::resizeWindow("rightImageMat", cv::Size(400, 300)); cv::imshow("rightImageMat", rightImageMat); cv::namedWindow("leftImageMat", cv::WINDOW_NORMAL); cv::resizeWindow("leftImageMat", cv::Size(400, 300)); cv::imshow("leftImageMat", leftImageMat); cv::namedWindow("resultMat", cv::WINDOW_NORMAL); cv::resizeWindow("resultMat", cv::Size(400, 300)); cv::imshow("resultMat", resultMat); cv::waitKey(0); } 

入坑

入坑一:cv::Mat.copyTo函数崩溃

问题

cv::Mat的copyTo函数崩溃

原因

被复制的源文件mat和函数里面的目标mat都需要宽高,类型相等。

解决

是因为计算最终的图像错误导致的,纠正逻辑代码。