简介:这份C#人工智能项目实践资源包,基于Emgu.CV库实现模板匹配、行人检测与特征点识别等常见视觉任务,适合C#开发者、计算机视觉初学者或需要快速落地视觉方案的.NET工程师。资源共35个文件,约1MB,主要为jpg/jpeg图片样本、cs源码、xaml界面及工程配置文件,涵盖可直接运行的Demo工程与配套图像资源,便于对照学习与二次开发。项目中展示了模板匹配的MatchTemplate调用、基于Haar级联分类器的行人检测,以及SIFT/SURF特征点提取等完整代码逻辑;通过Visual Studio打开解决方案即可查看工程组织,适合理解Windows环境下C#结合Emgu.CV的集成方式。已有399人学习下载,对希望快速上手计算机视觉的C#用户而言,是一份轻量且具备参考价值的实践样例。
1. 用 C# 做视觉项目:Emgu.CV 的模板匹配与行人检测实战包
做 C# 上位机或者桌面工具的工程师,多少都翻过 Emgu.CV 的 Demo 包。这份压缩包内容很典型:一个控制台工程 Demo1,一个 WPF 工程 TemplateMatching,里面装着模板匹配、行人检测、特征点识别三块代码,还有一份完整的依赖清单。说白了,这就是 OpenCV 官方教程的 C# 落地版,把大多数人想抄的 MatchTemplate、DetectMultiScale、SIFT 这些名词,全部变成了可以直接运行的工程。适合两类人:一类是刚在 .NET 里引入计算机视觉,想找现成代码对照学习的;另一类是准备做智能监控、工业视觉、上位机图像处理项目的工程师,想拿这套代码当脚手架,换掉图片路径就能出效果。模板匹配的六种度量方式、Haar 级联分类器的五个参数、特征点匹配的滤除方法,这份资源里都涉及到了。先把它跑通,后面再谈精度和工程化。
2. 环境搭建与工程结构:NuGet、双工程与 OpenTK 依赖解析
2.1 为什么选 Emgu.CV:从 OpenCV 到 .NET 的桥
OpenCV 是 C++ 库,C# 要直接调用得靠 P/Invoke 手写一堆封装,不现实。Emgu.CV 把这些原生能力包成了托管 API,Image<TColor, TDepth>、Mat、CascadeClassifier这些类可以直接在 C# 里 new 出来用。和 AForge.NET、OpenCvSharp 相比,Emgu.CV 的 API 跟 OpenCV 官方文档对应关系最紧密,你在 OpenCV 官网查到的一个函数,往往能在 Emgu.CV 里找到同名或同名近似的封装,迁移成本低。而且它支持 WinForms 和 WPF 两套 UI 方案,工业视觉和上位机场景里的历史项目大概率就是它。
选型的时候还要考虑一个现实因素:网上能找到的旧工程代码,十有八九是 Emgu.CV 写的。你手里的这个包就是典型,packages.config说明它是 NuGet 早期的工程格式,大概率是 3.x 分支的版本。这类老工程的参考价值在于:你能看到当年官方推荐的写法,也能知道版本升级之后哪些 API 要改。后文我会专门讲 3.x 到 4.x 的迁移坑,遇到编译报错时对照着改就行。
2.2 工程文件逐个拆解:两个工程、packages.config 与 OpenTK 依赖
打开 EmguCV.sln 之前,先把文件结构看明白。管理大型视觉项目,最忌讳的就是"双击 sln 就编译"——你至少得知道哪个工程是入口,哪个是依赖,哪个文件删了会出事。
| 文件 / 目录 | 作用 |
|---|---|
| EmguCV.sln | 解决方案入口,包含 Demo1 和 TemplateMatching 两个工程 |
| Demo1 | 控制台工程,Program.cs 是唯一入口,适合快速验证算法 |
| TemplateMatching | WPF 工程,MainWindow.xaml + MainWindow.xaml.cs,带界面演示 |
| packages.config | NuGet 依赖清单,记录 Emgu.CV 和各原生库的版本号 |
| OpenTK.dll.config | OpenGL 绑定配置文件,老版本 Emgu.CV 的 3D/GPU 显示依赖它 |
| images | 测试图片目录,模板匹配和行人检测的素材都在里面 |
| haarcascade | 级联分类器 XML 文件,行人检测依赖它 |
两个工程的定位完全不同。Demo1 是控制台,适合跑批处理:读图、匹配、输出结果文件,全程没有界面,方便调试算法本身的正确性。TemplateMatching 是 WPF 工程,带MainWindow.xaml,界面上能显示图片和匹配框,适合做交互验证。
OpenTK.dll.config这个文件很多人会忽略。它是 OpenTK 库(OpenGL 的 C# 封装)的配置文件,老版本 Emgu.CV 在做CvInvoke.NamedWindow或者 GPU 相关操作时会用到。如果你在工程里没用到 3D 可视化,这个文件不用管;但删了它,某些老版本在启动时会报程序集加载失败,别问我怎么知道的。
2.3 把 NuGet 依赖还原:TargetFramework 与运行时库的坑
拿到工程第一件事不是写代码,而是把依赖还原到能编译。打开 packages.config,你会看到类似下面的条目:
<packages> <package id="Emgu.CV" version="3.4.3" targetFramework="net461" /> <package id="Emgu.CV.runtime.windows" version="3.4.3" targetFramework="net461" /> </packages>targetFramework="net461"这句话是关键。它表示这个工程当年是 .NET Framework 4.6.1 下创建的。如果你本机装的是 .NET 5 或 .NET 6,直接打开会提示目标框架不受支持。我一般按两个方案走:要么把 TargetFramework 改成你本机装的高版本 .NET Framework,要么在 Visual Studio Installer 里补装 4.6.1 开发组件。改完之后,右键解决方案,选择"还原 NuGet 包"。
还原完成后,还有一个隐藏坑:Emgu.CV.runtime.windows这个包会在 bin 目录下生成x86和x64两个子文件夹,里面是opencv_core340.dll、opencv_imgproc340.dll这些原生 DLL。程序运行时,CLR 会根据进程位数加载对应文件夹。千万别手贱把这些 DLL 拷出来放到根目录,一旦放乱,就会出现"编译通过但运行就崩"的经典问题。
提示:还原后如果在运行时报"未能加载文件或程序集 Emgu.CV",先检查项目属性里的平台目标是不是 x64 / x86,再检查 bin 目录下的原生 DLL 是否齐全。编译通过只代表托管层没问题,原生层的坑在运行时才暴露。
3. 把模板匹配跑通:MatchTemplate 方法选择与四个边界坑
3.1 模板匹配的原理:滑动窗口与六种相似度度量
模板匹配做的事情很简单:拿一个模板小图,在大图上从左到右、从上到下滑动,每滑到一个位置,计算模板和当前图像块的相似度,最后得到一张和原图尺寸相同的相似度矩阵,值最大的位置就是最佳匹配点。
Emgu.CV 的MatchTemplate提供了六种度量方式,这是第一个容易翻车的点——因为不同方法对应的"最佳值位置"不一样:
| 枚举值 | 说明 | 找最佳匹配看哪个值 |
|---|---|---|
| SqDiff | 平方差 | 最小值 |
| SqDiffNormed | 归一化平方差 | 最小值 |
| CCorr | 相关 | 最大值 |
| CCorrNormed | 归一化相关 | 最大值 |
| Ccoeff | 相关系数 | 最大值 |
| CcoeffNormed | 归一化相关系数 | 最大值 |
SqDiff系列对噪声敏感,归一化版本虽然抗光照变化,但计算量大。Ccoeff系列会先把图像块做去均值处理,对光照变化最鲁棒。实际项目里我基本上只用CcoeffNormed,它的输出范围是 [-1, 1],值越接近 1 说明越像,语义直观,阈值也好定。如果是红外热像图或者医学影像这种目标灰度特征明确的场景,SqDiffNormed也常用——它找最小值,最小值越接近 0 越像。
3.2 核心代码实现:MatchTemplate + MinMaxLoc 的完整链路
模板匹配的完整链路是:加载大图 → 转灰度 → 调 MatchTemplate 得到相似度矩阵 → 用 MinMaxLoc 找极值 → 在原始图上画框。下面是 Demo1 控制台工程的完整写法:
// 加载大图和模板图,路径按实际工程调整 Image<Bgr, byte> source = new Image<Bgr, byte>("images/scene.jpg"); Image<Bgr, byte> template = new Image<Bgr, byte>("images/template.jpg"); // 转灰度:模板匹配只依赖亮度纹理,灰度能减少约 2/3 的计算量 Image<Gray, byte> graySource = source.Convert<Gray, byte>(); Image<Gray, byte> grayTemplate = template.Convert<Gray, byte>(); // 计算相似度矩阵,result 的每个像素代表该位置的匹配度 using (Image<Gray, float> result = graySource.MatchTemplate(grayTemplate, TemplateMatchingType.CcoeffNormed)) { // 找出矩阵中的最小值和最大值,以及它们各自的位置 double minVal = 0, maxVal = 0; Point minLoc = new Point(), maxLoc = new Point(); result.MinMaxLoc(out minVal, out maxVal, out minLoc, out maxLoc); // CcoeffNormed 找最大值位置,maxLoc 就是最佳匹配的左上角 Rectangle matchRect = new Rectangle(maxLoc, template.Size); source.Draw(matchRect, new Bgr(0, 0, 255), 2); source.Save("output.jpg"); }Convert<Gray, byte>()这一步是必须的。我在不少帖子里看到有人直接拿彩色图调MatchTemplate,能跑,但结果不稳定。原因在于MatchTemplate对多通道图像的处理是按通道分别计算再合并,输出矩阵的语义会变得复杂。转灰度后输出就是单一相似度矩阵,结果明确。
MinMaxLoc返回四个值:最小值和最大值本身,以及它们的位置。CcoeffNormed对应的最佳匹配在最大值处,所以用maxLoc作为矩形的左上角,配合template.Size就能框出目标区域。矩形画在彩色原图上,注意Draw的颜色参数是Bgr而不是Bgra,这是个容易忽略的小细节。
3.3 多目标匹配与阈值选取:从相似度矩阵到 NMS
单目标匹配到maxLoc就结束了。但实际场景里往往一张图上有多个目标,比如货架上有多个同款商品。这时候不能只取一个最大值,而是要把相似度矩阵里所有高于阈值的位置都找出来:
// 阈值二值化:把相似度高于 0.8 的像素标记为 1,其余为 0 Image<Gray, byte> binary = result.ThresholdBinary(new Gray(0.8), new Gray(1.0)); // 找出所有高亮区域的轮廓 VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint(); Mat hierarchy = new Mat(); CvInvoke.FindContours(binary, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple); List<Rectangle> boxes = new List<Rectangle>(); for (int i = 0; i < contours.Size; i++) { Rectangle rect = CvInvoke.BoundingRectangle(contours[i]); boxes.Add(rect); source.Draw(rect, new Bgr(0, 255, 0), 2); }ThresholdBinary(new Gray(0.8), new Gray(1.0))表示把相似度大于 0.8 的位置置为 1,输出二值图。阈值定多少是模板匹配调参的核心,定太高漏检,定太低一堆误检。我的习惯是先输出原始相似度矩阵的热力图看看分布,再决定阈值放在哪个区间——这步别省,不然就是在赌。
FindContours拿到的是二值图中的连通域轮廓,BoundingRectangle把轮廓转成外接矩形。但这里有个衍生问题:同一个目标周围,高响应区域可能是连成一片的多个连通域,导致最终画出好几个重叠框。处理方法是非极大值抑制(NMS),核心逻辑是:把所有框按面积排序,逐个跟后面的框算 IoU,IoU 大于 0.5 就删掉分数低的。我一般会在多目标场景里必加这一步,不然图一亮出来就是框摞框的翻车现场。
注意:模板匹配有个天然边界——它只对平移敏感,对旋转、缩放基本无解。模板旋转 30 度,相似度会掉得没法看;模板被放缩 1.2 倍,匹配位置也会偏。如果业务场景里存在旋转缩放,别硬用模板匹配,直接跳到第 6 章用 SIFT。
4. 让行人检测落地:Haar 级联的分类器加载与 DetectMultiScale 调参
4.1 Haar 级联分类器:为什么检测行人用级联而不是模板匹配
行人检测如果还用模板匹配,结果一定是灾难。行人姿态多变,同一视角下不同人的高矮胖瘦、衣着颜色、动作幅度差异巨大,模板匹配的滑窗思路在这里完全失效。基于 OpenCV 的传统行人检测方案是 Haar 级联分类器:先用 Haar 特征描述图像局部区域的像素明暗对比关系,再通过 AdaBoost 算法把大量弱分类器级联成强分类器,最终得到一个能快速判定"这个区域是不是行人"的分类器。
OpenCV 官方用大量行人样本训练出了haarcascade_fullbody.xml、haarcascade_upperbody.xml、haarcascade_lowerbody.xml等现成的模型文件,这就是你搜"行人检测数据集"能找到的那批资源训练出来的成果。Emgu.CV 里加载这些 XML 文件不需要任何额外配置,比深度学习方案省去了一整套训练流程。论精度,Haar 比不上 YOLO 这类深度学习模型,但它的优势是 CPU 友好、部署简单、无 GPU 依赖,在 C# 上位机这种硬件条件有限的场景里仍然值得用。
4.2 加载分类器与 DetectMultiScale:五个参数逐项调
行人检测的核心代码非常短,但参数调起来能磨掉你一个下午。先看完整流程:
// 加载预训练的 Haar 级联分类器 CascadeClassifier cascade = new CascadeClassifier("haarcascade_fullbody.xml"); // 读入测试图片 using (Image<Bgr, byte> frame = new Image<Bgr, byte>("images/people.jpg")) { // 在图像中检测行人,返回一组矩形框 Rectangle[] humans = cascade.DetectMultiScale( frame, // 输入图像 1.1, // scaleFactor:每层图像缩放比例 3, // minNeighbors:候选框邻居数量阈值 new Size(64, 96), // minSize:行人最小尺寸 new Size(0, 0) // maxSize:最大尺寸,0 表示不限制 ); // 把每个检测到的行人用绿色框标出来 foreach (Rectangle rect in humans) { frame.Draw(rect, new Bgr(0, 255, 0), 2); } frame.Save("people_result.jpg"); }DetectMultiScale的返回类型在 3.x 和 4.x 里有差异。老版本返回MCvAvgComp[],新版本返回Rectangle[],遍历方式一样,但如果你的代码是从老工程拷的,编译报错就先查这个。
五个参数的含义和调参方向,我按经验列在下表:
| 参数 | 默认建议 | 对结果的影响 |
|---|---|---|
| scaleFactor | 1.1 | 每层缩放比例,越大速度越快但漏检越多 |
| minNeighbors | 3 | 越大误检越少、漏检越多;越小误检暴增 |
| minSize | 64x96 | 过滤更小目标,能显著降低误检 |
| maxSize | 0(不限) | 过滤过大区域,视频近景时常需要 |
| 输入图像 | 彩色直接传入 | 内部自动转灰度处理 |
scaleFactor的直觉理解是:分类器每次把图像缩小 1.1 倍,再扫一遍。1.1 意味着每层缩小 10%,扫描层数多、精度高、速度慢;改成 1.3,速度快了但小目标会漏。minNeighbors控制一个候选区域要经过多少个邻近检测确认才被采纳,值越大条件越苛刻。这两个参数是跷跷板,只能针对具体场景来回试。
4.3 视频序列中的行人检测:从单帧到实时
模板匹配和行人检测最常见的落地场景是视频流。Emgu.CV 的VideoCapture能同时处理本地视频文件、摄像头索引号和 RTSP 网络流:
// 打开本地视频文件;换成 0 就是读取第一个摄像头 VideoCapture capture = new VideoCapture("people.mp4"); Mat frameMat = new Mat(); // 循环读取每一帧,直到视频结束 while (true) { capture.Read(frameMat); if (frameMat.IsEmpty) break; // Mat 转成 Image 包装器,方便调用 Draw 画框 using (Image<Bgr, byte> frame = frameMat.ToImage<Bgr, byte>()) { Rectangle[] humans = cascade.DetectMultiScale( frame, 1.05, 4, new Size(64, 96), new Size(0, 0)); foreach (Rectangle rect in humans) frame.Draw(rect, new Bgr(0, 255, 0), 2); // 显示帧,按 ESC 退出 CvInvoke.Imshow("Pedestrian Detection", frame); if (CvInvoke.WaitKey(30) == 27) break; } }这段代码在网络摄像头或视频文件场景里能直接复用,前提是你把CascadeClassifier的初始化放到循环外。如果把new CascadeClassifier写进循环里,每帧重新加载一次 XML,性能会衰减到不可用的地步,这是新手最容易踩的性能坑。
CvInvoke.Imshow和CvInvoke.WaitKey是 OpenCV 经典的高层 GUI 函数,在控制台工程里能用。如果是在 WPF 工程里做界面集成,别用这两个函数——WPF 的消息循环会跟 OpenCV 的 GUI 线程冲突,表现为窗口卡死。WPF 项目要用ImageViewer控件绑定Image<Bgr, byte>对象,方式完全不同。上位机项目通常需要把检测结果叠加到自己的界面上,这时候直接用Image对象的Bitmap属性转成BitmapSource绑定到界面上最省事。
5. 常见问题与避坑记录:AccessViolation、图像格式与版本迁移
5.1 现象:C++ interop AccessViolationException 崩溃在 DetectMultiScale
现象:代码编译通过,但运行到DetectMultiScale时抛出 AccessViolationException,提示"尝试读取或写入受保护的内存",程序直接崩溃。
原因:这是 C# 调用 C++ 原生库的经典问题。Emgu.CV 的托管层通过 P/Invoke 调用原生 DLL,如果进程位数和原生 DLL 位数不匹配,或者原生库版本与托管程序集版本对不上,内存访问就出错。最常见的是平台目标选了 AnyCPU,系统按 64 位加载时原生库却只拷了 x86 的。
解决:把项目平台目标固定为 x64 或 x86,跟 packages.config 里Emgu.CV.runtime.windows的位数一致。然后检查 bin 目录下x64/x86子目录里的 DLL 是否齐全,缺失就重新还原 NuGet 包。如果还崩,换一个版本的原生 runtime 包再试。C# 调用 C++ 出现 access violation c0000005 是这类问题的典型搜索关键词,十次有八次是位数不一致。
5.2 现象:模板匹配结果偏到难以置信的位置
现象:maxLoc找到的最佳匹配位置明显不对,框没框在目标上,甚至框在空白处。
原因:大概率不是算法错了,而是图像通道或尺寸问题。比如大图和模板的通道不一致,或者模板虽然是从大图截取的,但中间经过了缩放,分辨率变了,匹配自然错位。还有一个常见操作:模板从系统截图工具里截出来,带了边框或阴影,跟原图的图像块有细微差异。
解决:模板必须从目标图像同一来源直接截取,不要从外部截图粘贴。两张图都要转成灰度后再匹配。如果模板确实来自不同分辨率的图像,先Resize统一尺寸再跑匹配。我在项目里遇到过一张从 PDF 里抠出来的模板图,匹配结果毫无规律,最后发现是模板带了一圈白边,裁掉后立刻正常。
5.3 现象:haarcascade 文件明明在根目录,却抛 FileNotFoundException
现象:new CascadeClassifier("haarcascade_fullbody.xml")抛文件不存在异常,但我确认 XML 文件就在工程根目录。
原因:程序运行时的当前工作目录不是工程根目录。用 Visual Studio 调试时,工作目录是项目根目录,而 exe 实际运行在bin\Debug下,相对路径是按工作目录解析的,文件根本不在那。
解决:把 XML 文件属性改成"如果较新则复制",让它每次编译自动拷到输出目录。或者用绝对路径加载,我一般写成Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "haarcascade_fullbody.xml"),这样无论从哪个目录启动都能找到。模型文件、测试图片、配置文件全部按这个规则处理,能省去一大半路径相关的诡异问题。
5.4 现象:x64 跑得好好的,换 AnyCPU 就莫名其妙闪退
现象:本机 x64 平台下程序稳定运行,改成 AnyCPU 后换到别的电脑上闪退,没有任何异常输出。
原因:AnyCPU 会在 64 位系统上以 64 位进程运行,如果目标机器缺少 Visual C++ 运行库,或者 Emgu.CV 的原生 DLL 位数匹配失败,进程直接退出。旧版本 Emgu.CV 依赖 VC++ 2015/2017 运行库,很多流水线工控机是精简系统,没装这些。
解决:发布前把平台目标定死,x64 就 x64,不要给用户留选择空间。部署包里附上 VC++ Redistributable 安装包,或者把所需运行库 DLL 拷贝到 exe 目录。原生依赖如opencv_core340.dll、opencv_objdetect340.dll等必须跟 exe 在同一目录,或者放在 x64/x86 子目录里。这种问题排查的时候最难受,因为编译不报错、运行不弹窗,直接闪退,只能靠排除法。
5.5 现象:多目标匹配时重复框包围同一个目标
现象:同一辆车或同一个人被框了三次,框的位置相近但大小略有差异。
原因:模板匹配的高响应区域在目标周围通常不止一个像素点,阈值二值化后形成多个连通的轮廓,每个轮廓都被当成独立目标画了框。
解决:加非极大值抑制。把所有框按面积降序排列,从面积最大的框开始,跟后面的框逐个计算 IoU,大于 0.5 就删除后面的框。IoU 的计算公式是交集面积除以并集面积,十行代码就能实现。我一般在多目标检测场景里把 NMS 封装成一个工具方法,所有检测流程共用,省得每次重写。这个方法同样适用于行人检测的多目标去重。
6. 特征点识别进阶:SIFT/SURF 匹配与可视化验证
6.1 从模板匹配到特征点识别:SIFT/SURF 的适用边界
模板匹配对旋转、缩放无能为力,这是它的物理边界。如果你的业务场景里目标会旋转,或者拍摄距离不固定导致尺度变化,SIFT 是更合适的选择。SIFT 检测的是图像中具有尺度不变性和旋转不变性的关键点,每个点附带 128 维描述子,通过描述子之间的欧氏距离判断匹配关系。SURF 是它的加速变体,速度更快但稳定性稍弱。需要注意 SIFT 和 SURF 都有专利约束,商业产品里要先确认授权情况,内部工具或学术项目则没有这个顾虑。
6.2 特征提取与 BFMatcher 匹配:代码实现与参数说明
// 读取两幅需要匹配的图像 Image<Gray, byte> img1 = new Image<Gray, byte>("images/scene1.jpg"); Image<Gray, byte> img2 = new Image<Gray, byte>("images/scene2.jpg"); // 创建 SIFT 特征检测器 SIFT sift = new SIFT(); // 分别检测关键点并计算描述子 VectorOfKeyPoint kp1 = new VectorOfKeyPoint(); VectorOfKeyPoint kp2 = new VectorOfKeyPoint(); Mat desc1 = new Mat(); Mat desc2 = new Mat(); sift.DetectAndCompute(img1, null, kp1, desc1, false); sift.DetectAndCompute(img2, null, kp2, desc2, false); // 用暴力匹配器做 KNN 匹配,k=2 是为了后续用比值法滤除误匹配 BFMatcher matcher = new BFMatcher(DistanceType.L2); VectorOfVectorOfDMatch matches = new VectorOfVectorOfDMatch(); matcher.KnnMatch(desc1, desc2, matches, 2);KnnMatch里 k=2 是个关键参数。它会给每个特征点返回两个最近的匹配对,然后用最近距离除以次近距离得到比值,比值小于 0.75 才认为是可靠匹配。这个比值过滤法能滤掉大量误匹配,是特征点匹配的经典手段。如果你直接取最近的一个匹配,误匹配率会高得没法看,匹配线满天飞。
6.3 可视化验证:把特征点连线画出来,别信输出数字
把匹配结果画到图上,是验证算法是否可靠的最快方式。代码里遍历过滤后的匹配对,用直线把两个关键点连起来:
// 遍历匹配对,筛选出可靠的匹配并绘制连线 for (int i = 0; i < matches.Size; i++) { // 取最近距离和次近距离的比值,小于 0.75 认为是可靠匹配 DMatch best = matches[i][0]; DMatch second = matches[i][1]; if (best.Distance < 0.75 * second.Distance) { // 把两幅图水平拼接后,画一条直线连接关键点 // 第二幅图的关键点横坐标要加上第一幅图的宽度 Point p1 = new Point((int)kp1[best.QueryIdx].Point.X, (int)kp1[best.QueryIdx].Point.Y); Point p2 = new Point((int)kp2[best.TrainIdx].Point.X + img1.Width, (int)kp2[best.TrainIdx].Point.Y); CvInvoke.Line(combined, p1, p2, new Bgr(0, 255, 0), 1); } }第一次跑特征点匹配的时候,我以为控制台输出的匹配数够多就万事大吉,结果把连线画出来一看,一堆线交叉乱飞,匹配质量完全不能用。从那以后我每次跑完特征点匹配都强制走一遍可视化流程,先看连线再谈参数。匹配线应该是平滑、大致的平行走向,交叉线多说明误匹配严重,调低比值阈值或者换 SURF 重试。希望帮到你。
本文还有配套的精品资源,点击获取