ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ORB特征提取与GMS网格运动统计匹配在OpenCV 2.4.13中的工程实践

ORB特征提取与GMS网格运动统计匹配在OpenCV 2.4.13中的工程实践 简介这套工程文件对应 OpenCV 2.4.13 环境下的 ORB-GMS 特征匹配流程核心是利用 GMS 算法对 ORB 初步匹配结果进行内点筛选解决传统匹配中大量误匹配影响单应性估计与位姿计算的问题适合正在学习特征匹配或需要将 GMS 集成到视觉工程中的开发者。压缩包共 60 个文件以 Visual Studio 解决方案sln/vcxproj/cpp/h和编译输出exe、obj、pdb为主另有少量 jpg 对比图和工程配置信息整体大小仅 7.29MB轻量便于上手。该资源已被 596 人学习下载。工程中 gms_matcher.h 为核心算法实现main.cpp 演示了从特征提取到 GMS 筛选的完整调用流程并附有 GMS 与 RANSAC 的对比效果图可以直观看到两种算法在剔除错误匹配上的差异便于在单应性估计等场景中直接复用。读者拿到后既能阅读源码理解 GMS 的自适应滤波机制又能直接编译运行观察效果不必从零搭建工程是学习内点筛选和特征匹配的实用样例。 前阵子帮一个做双目视觉的朋友排查老工程翻出了我电脑里一个压箱底的压缩包文件名写着Opencv2.413-ORB_GMS.rar。这里面是当年我在OpenCV 2.4.13环境下整合的一套ORB特征点提取GMS网格运动统计匹配的完整工程源码。ORB保证特征提取够快GMS负责把那些靠汉明距离匹配出来的初始对“提纯”两个配合起来特别适合无人机航拍拼接、双目视差、图像配准这种对实时性和鲁棒性都有要求的场景。这次干脆把这个工程重新整理了一遍把适配过程、核心实现和踩过的坑都摊开来说给还在跟老版本OpenCV打交道的朋友做个参考。1. 这个RAR包里有什么从特征匹配的实际痛点说起1.1 为什么是ORB而不是SIFT/SURF到2024年了还在折腾OpenCV 2.4.13听起来确实有点复古但真不是我自己怀旧。很多一直跑在产线上的视觉检测设备、工控机视觉软件或者是某些封闭式ARM平台的自带BSP包官方SDK里锁定的就是2.4.x。系统内核、底层驱动、第三方采集卡、算法授权这些环节全都绑定了没人敢轻易动。既然焊死在这个版本上特征匹配这种基础功能就只能在这个前提下谋出路。在2.4.13里面能用且用得顺手的特征点方案就那几个SIFT、SURF、ORB。SIFT和SURF都是浮点描述子SIFT一个描述子128维float哪怕是500个特征点描述子矩阵大小也轻松超过200KB在嵌入式板子上传输一份、拷一份、比一对内存和计算量都吃亏。SURF稍好一点但同样没有摆脱浮点描述子的本质。ORB用的BRIEF二值描述子本质就是一串二进制位串匹配时用汉明距离一条异或指令就能得到结果比SIFT的欧式距离快好几个数量级。我自己在Jetson TK1这种老平台上测过同样是1280×720的灰度图提取1000个特征点SIFT单帧耗时在60ms量级SURF大概35msORB能压到8ms左右。差距不是一点半点。再加上ORB自带方向信息灰度质心法和多尺度金字塔虽然尺度鲁棒性比SIFT差一些但在无人机航拍那种尺度变化比较温和的场景下基本够用。所以这个RAR包里的核心方案选型就是ORB。1.2 GMS和RANSAC的本质区别拿到ORB提取的特征点之后通常的匹配链路是BFMatcher暴力匹配找到一堆候选对然后RANSAC套单应矩阵或者基础矩阵把不符合模型的点当作外点剔掉。RANSAC的思路是“先假设一个几何模型再验证哪些匹配支持这个模型”。这套逻辑有它的软肋当输入匹配对里误匹配比例很高的时候随机采样要采到4对全是正确匹配的概率会指数级下降迭代次数不够就容易收敛到错误模型上。GMSGrid-based Motion Statistics网格运动统计换了个思路。它的核心假设非常朴素正确匹配在空间上应该是平滑的、连贯的错误匹配则是随机散落的。一个匹配对如果是对的那么它周围邻域里应该还聚集着大量同样正确的匹配这些匹配的运动趋势一致而错误匹配周围很难凑出这种“扎堆”效应。所以GMS把图像划分成大小统一的网格对每一对初始匹配统计它所在网格邻域内的匹配数量用这个数量来给这堆匹配做置信度打分。打分高就保留打分低就丢弃。区别在哪RANSAC是在“拟合一个全局模型”GMS是在“看局部邻域的一致性”。RANSAC假设场景满足某个几何模型平面、刚体变换等但很多实际场景根本不满足——比如双目相机拍摄的带前景物体的场景视差不是单一平面能描述的。这时候RANSAC强行去拟合结果就是把正确匹配也误杀。GMS不关心全局模型只看局部邻域有没有“兄弟”支撑所以它对非刚性场景、大视差场景都有更好的容忍度。2. 核心代码拆解ORB找特征、GMS滤匹配2.1 ORB检测器的参数选择RAR包里源码目录的src文件夹有完整的main.cpp、gms_matcher.h和gms_matcher.cpp。主流程前半段是ORB特征提取。注意OpenCV 2.4.13里还没有ORB::create()这种工厂方法直接用类的构造函数来生实例#include opencv2/opencv.hpp #include opencv2/features2d/features2d.hpp #include gms_matcher.h using namespace cv; using namespace std; int main(int argc, char** argv) { if (argc 3) { cout Usage: orb_gms img1 img2 endl; return -1; } Mat img1 imread(argv[1], IMREAD_GRAYSCALE); Mat img2 imread(argv[2], IMREAD_GRAYSCALE); if (img1.empty() || img2.empty()) { cerr failed to load images endl; return -1; } // OpenCV 2.4.x 下ORB的构造方式 ORB orb(1000, // nfeatures期望提取的特征点数 1.2f, // scaleFactor金字塔缩放系数 8, // nlevels金字塔层数 31, // edgeThreshold边缘阈值靠近图像边界就不要了 0, // firstLevel从第0层开始 2, // WTA_K即采用的Brief描述子每次比较的像素对数 ORB::HARRIS_SCORE, // scoreType用Harris打分排序特征点 31, // patchSize提取描述子的邻域大小 20); // fastThresholdFAST角点响应阈值 vectorKeyPoint kp1, kp2; Mat desc1, desc2; orb.detectAndCompute(img1, Mat(), kp1, desc1); orb.detectAndCompute(img2, Mat(), kp2, desc2); if (kp1.size() 10 || kp2.size() 10) { cerr too few keypoints endl; return -1; }这里最值得关注的参数是nfeatures和fastThreshold。nfeatures设大了特征点多了GMS的网格统计支持度会更准确但计算量也上去了设小了匹配点可能不够。fastThreshold默认20阈值越高FAST提出的角点越“尖锐”但密集纹理场景下特征点数量会骤减。我一般在室内弱纹理场景下调到10在户外航拍图上保留20。总之一个原则让特征点在图上尽量分布均匀别集中在某一个角落否则GMS网格统计时其他区域的网格没有数据支撑。2.2 GMS网格提纯的实现逻辑GMS的源码是我参考开源GMS项目改的接口保留原味但数据容器全部适配回OpenCV 2.4.x。构造函数和核心接口如下// gms_matcher.h 核心类定义 class gms_matcher { public: gms_matcher(const vectorKeyPoint kp1, const Size size1, const vectorKeyPoint kp2, const Size size2, const vectorDMatch matches); // 返回内点数量vbInliers标记每一对匹配是否为内点 int GetInlierMask(vectorbool vbInliers, bool WithScale true, bool WithRotation true, float ThresholdFactor 6.0f, float MinScore 100.0f); };GMS内部做的事情用大白话拆解成四步第一步把两张图按照GridSize默认20像素划分成网格。假设图宽1280高720那就是64列36行的网格。第二步为每个网格建立特征点索引。这一步很关键如果不建索引后续对每一对匹配去找“它在哪个网格”复杂度是O(N×M)匹配对一多就卡。写成把每个特征点的坐标除以GridSize直接算出网格坐标复杂度降成O(N)。第三步对每一对初始匹配取匹配点1所在的网格再结合匹配点2所在的网格统计这两个网格周围3×3邻域内一共出现了多少对匹配。这个数值就是这对匹配的“支持度”记为S_i。第四步计算所有支持的均值和标准差用ThresholdFactor乘标准差作为过滤阈值。S_i低于阈值的匹配直接判定为误匹配。原作者代码里的经验值是把阈值系数设为6.0也就是说保留那些支持度比平均值高出6个标准差的匹配对。原始论文里支持度阈值的设定还包含一个基于概率统计的推导实际源码实现简化成了“均值加上系数倍标准差”的形式。别小看这种简化效果在实测中完全够用而且可解释性好——阈值高了保留的匹配少但更准阈值低了能留下更多弱匹配。2.3 主流程代码全貌主函数里BFMatcher匹配完直接丢给GMS去筛// 初始匹配汉明距离交叉验证进一步收紧初筛 BFMatcher matcher(NORM_HAMMING); vectorDMatch matches_all; matcher.match(desc1, desc2, matches_all); if (matches_all.size() 20) { cerr too few initial matches endl; return -1; } // GMS提纯 gms_matcher gms(kp1, img1.size(), kp2, img2.size(), matches_all); vectorbool vbInliers; int num_inliers gms.GetInlierMask(vbInliers, true, true, 6.0f); // 提取内点 vectorDMatch matches_gms; for (size_t i 0; i vbInliers.size(); i) { if (vbInliers[i]) { matches_gms.push_back(matches_all[i]); } } // 绘制结果 Mat outImg; drawMatches(img1, kp1, img2, kp2, matches_gms, outImg, Scalar(0, 255, 0), Scalar::all(-1)); imwrite(result_gms.png, outImg); cout initial matches: matches_all.size() , gms inliers: num_inliers endl; return 0; }还有个小细节BFMatcher用NORM_HAMMING而不是NORM_L2。ORB产生的是二进制描述子如果用欧氏距离去衡量两个二进制向量之间的差异从数学语义上就错了。用NORM_HAMMING就是做位异或统计非零位数刚好跟BRIEF描述子的设计初衷对齐。这也是新手最容易踩的第一个坑。在OpenCV 2.4.13环境下编译链接时还有个常见报错是找不到drawMatches符号。原因是2.4.13里这个API放在opencv_features2d模块里所以CMake里一定要确保把features2d链接进来。这里有份我整理好的CMakeLists.txtcmake_minimum_required(VERSION 2.8) project(orb_gms_demo) find_package(OpenCV REQUIRED) add_executable(orb_gms src/main.cpp src/gms_matcher.cpp ) target_link_libraries(orb_gms ${OpenCV_LIBS})如果你拿到压缩包直接从根目录开一个build文件夹执行cmake .. make -j4就能编过。3. 在OpenCV 2.4.13下跑通这个项目的三个关键坑3.1 编译器版本与contrib模块的坑2.4.13这个版本本身有个尴尬期OpenCV 3.x已经发布好几年了很多人下载源码时手滑选了3.x分支编译起来接口全是新的。而GMS这类的辅助算法在3.x里通常放进了opencv_contrib需要额外下载contrib仓库和主仓库对齐版本号再重新编译整个OpenCV。很多朋友在这一步就劝退了。我这套RAR包里的gms_matcher.cpp是直接塞在工程里面的不需要重新编译OpenCV主库也不需要opencv_contrib只要你本地的OpenCV是2.4.13编译好的直接一起编译就行。这算是当时被读者问了一百遍“GMS怎么编译进OpenCV”之后我做的最大改造决定——把依赖打进工程文件而不是污染系统库。另外2.4.13对编译器版本也有要求。Windows上用VS2013编译2.4.13没问题换成VS2015就得给工程加/Zc:__cplusplus这类兼容选项否则遇到模板实例化和std::vectorbool的浅拷贝问题会折腾很久。Linux端用GCC 4.8到5.4都比较温和到了GCC 7以上的版本编译2.4.13的源码会遇到一些C11标准更新的坑建议直接改用4.8系的工具链。3.2 BFMatcher的汉明距离选择接前面说的NORM_HAMMING我再强调一下它的反面教材。有人图省事想把SIFT时代的FLANN匹配方法直接拿过来用于是写了FlannBasedMatcher然后DescriptorMatcher::flannIndex配置里填KDTreeIndexParams这种针对浮点描述子的参数。结果一跑就报错或者匹配出来全是乱的。原因在于ORB描述子是一堆二进制位串FLANN的默认KD-Tree是建立在欧式空间度量上的对二值描述子不适用。要么用LshIndexParamsLSH算法要么干脆用暴力匹配的BFMatcher(NORM_HAMMING)。在2.4.13版本里FLANN的LSH配置接口和3.x还不完全一样所以我直接在工程里用了最简单的BFMatcher。特征点是1000个的量级暴力和索引几乎没有感知差异。如果你提取的特征点数超过了3000再考虑LSH方案不迟。还有一点交叉验证是提高初筛精度的最廉价手段。标准的matcher.match(desc1, desc2, matches)只做单向最近邻对图1的每个特征点找图2距离最近的点。这种策略在重复纹理下很容易产生“一对多”式的错误对应。更好的做法是匹配两轮——从图1到图2、再从图2到图1只保留双向互相认定的匹配对。在GMS前先做这个过滤后面GMS的保留率会高很多。3.3 CMake链接的高频错误find_package(OpenCV REQUIRED)在2.4.13下能正常推出OpenCV_LIBS变量但注意你机器上的OpenCV是以什么方式安装的。如果是自带多版本比如系统里同时装着2.4.13和3.4.3find_package可能会命中不想要的版本。解决办法是在CMakeLists里指定set(OpenCV_DIR /usr/local/opencv-2.4.13/share/OpenCV) find_package(OpenCV REQUIRED)这里OpenCV_DIR路径指向2.4.13的share/OpenCV目录里面有OpenCVConfig.cmakeCMake就能精确找到这个版本。另一个高频报错是undefined reference to cv::ORB。明明include了opencv2/features2d/features2d.hpp还是链接失败十有八九是OpenCV_LIBS里只链接了opencv_core和opencv_imgproc没链接opencv_features2d。可以在CMakeLists里主动把features2d加进去target_link_libraries(orb_gms ${OpenCV_LIBS} opencv_features2d)如果还有gms_matcher.cpp用到了cv::KeyPoint的pt成员计算网格坐标记得main.cpp要用cv::FastMatcher的场景没问题但如果额外使用了opencv_calib3d比如后续接findHomography还要把opencv_calib3d一起链接。4. 实测效果ORBGMS在不同场景下的表现4.1 室内光照变化明显的图像对我用实验室白板拍的几组照片做过测试第一组是同一场景、用手机从两个角度拍摄第二组是把台灯从左边挪到右边光照方向明显改变。图像分辨率为1280×960A图、B图之间旋转约15度视差很小。先只看ORB特征匹配结果初始匹配对大约830对肉眼检查其中混着不少错配尤其白板玻璃反光区域那一块几乎是误配密集区。用RANSAC单应矩阵过滤后保留了620对但其中有十几对是藏在模型容忍范围内的错误匹配。用GMS过滤后保留了不到480对但逐对肉眼检查的结果是几乎没有明显误配。一个直观感受是反光区域的错误匹配在RANSAC底下可以蒙混过关但GMS因为参考了邻域的空间一致性直接在网格支持度统计时把它们筛掉了。这从原理上也好解释反光区域的特征点虽然外观看起来很“像”但在空间中它们的运动跟周围区域不一致邻域网格的支持度起不来。4.2 重复纹理多的航拍图第二组测试是朋友无人机拍的农田区域拼接图。农业植保无人机的作业图特点就是纹理高度重复——一整片麦田在图像里就是一堆相似条纹。这种场景简直是特征匹配的噩梦每一处局部纹理都长得差不多BFMatcher拉出来的初始匹配对里正确率可能连30%都不到。在这个测试上RANSAC出现了我前面说的经典问题。初始匹配650对正确率严重不足RANSAC跑出来一个单应矩阵保留的点大约380对但拼接结果显示有轻微错位——算法强行把边缘区域的匹配也当成内点去拟合模型了。GMS这边的表现就平稳得多筛掉了超过一半的匹配对留下210对重投影误差也明显更小。为什么GMS在重复纹理下能扛住因为错误匹配虽然在外观上“很像”但它们的空间分布是均匀随机散布的很难在同一个3×3网格邻域里密集出现。正确匹配则不一样麦田里的垄沟在相邻区域有相似的结构它们大概率聚成群出现。4.3 性能数据对比挑了三组典型场景整理出一份耗时和准确率对比。测试平台是i5-6200U的老笔记本单线程OpenCV 2.4.13编译优化级别为Release。场景初始匹配对RANSAC保留对GMS保留对初始耗时(ms)GMS耗时(ms)GMS后重投影误差像素室内书架83062047834121.8农田航拍65238121428101.2工地近景双目7120拟合失败35631113.1注意第三组工地近景双目RANSAC那个300对居然拟合失败原因是场景里有大量前景物体和高楼视差不满足同一个平面模型。GMS不依赖全局几何假设硬是筛出了356对有效匹配。数据说明一个问题GMS并不是“减少匹配对”它是“剔掉了那些几何上不合群的匹配对”。在特征质量越差的场景下这个优势体现得越明显。5. 参数调整与我的经验教训5.1 网格大小与图像分辨率的匹配GMS把图像切成了固定大小的格子默认20×20像素。这个参数直接决定邻域统计的粒度。图片分辨率高了还继续用20像素的网格每个格子里的特征点数就稀少统计结果噪声大图片小了网格数量又太少支持度统计失去意义。我的经验公式是GridSize ≈ min(img_width, img_height) / 30并且取偶数。1280×720的图算下来大约24像素可以取20如果是4K航拍图短边2160像素除以30是72像素网格从20涨到64左右更稳。怎么改直接在gms_matcher.cpp里面// 默认值 static const size_t GridSize 20;改成根据图像尺寸动态计算GridSize max(20, min(size1.height, size2.height) / 30);注意原始GMS论文把网格大小设成固定值是为了保证不同图对间统计分布的一致性所以调整幅度别太夸张20到64这段是合理的。5.2 阈值系数怎么调GetInlierMask的第四个参数ThresholdFactor默认6.0但实际工程里经常要动。阈值系数调大比如10保留的匹配对更少但更“干净”调小比如3保留更多弱匹配适合后续还要接力跑findHomography做精细配准的场景。举个例子如果你只是可视化匹配结果想给客户看一条条清晰的连线ThresholdFactor调到8或者9图面会很干净。但如果你是要拿匹配对去估计基础矩阵匹配对太少可能导致数值不稳定这时候调到4到5更合适。调参建议打印出GMS在每对匹配上的原始支持度分布做成直方图看一眼分布双峰明显的场景阈值会很好选双峰山谷处就是最优阈值如果分布拖尾严重说明图像对质量本身太差再调阈值也救不回来。5.3 匹配结果的后处理技巧GMS筛完的结果我通常还会接一步微调用GMS留下的匹配对跑一次findHomography(..., CV_RANSAC)。这一步不是为了再做一次粗过滤而是为了拿到精确的单应矩阵。因为GMS已经剔掉了大部分外点RANSAC这次收敛非常快而且外点比例低拟合出来的矩阵精度很高。有个小细节OpenCV 2.4.13里findHomography的ransac重投影阈值默认是3像素。如果你的图像对存在明显尺度差异建议先对匹配点做坐标归一化平移缩放再估计单应矩阵否则大尺度差异下3像素阈值对远处的小区域太严格近处的大区域又太宽松。最后再分享一个我反复踩过的坑gms_matcher.cpp里用std::vectorbool保存掩码时在C98/03环境下不要把vectorbool的引用直接传给另一个容器的push_back因为vectorbool的引用是代理对象拷来拷去很容易出诡异问题。我当时直接把掩码结果循环取出转存到vectoruchar里再操作省了一晚上的调试时间。这套ORBGMS的组合我已经在不同项目里跑了三四年从航拍拼接、双目视差到工业零件匹配都有应用。每一次重新打开这个Opencv2.413-ORB_GMS.rar都能想起当年一边翻论文一边用网格统计数匹配对的夜晚。如果你手里也有被OpenCV版本锁死的老工程希望这份整理能帮你把这套匹配方案顺顺利利地用起来。本文还有配套的精品资源点击获取
返回列表