使用 C# 和 Emgu 库进行图像识别






4.93/5 (41投票s)
开发一个简单的应用程序,用于在图像列表中搜索包含原始图像较小一部分的图像,并以图形方式显示两者之间的交点。
摘要
接下来,我们将学习如何使用 C# 和 Emgu(Intel OpenCV 图像处理库的 .NET 包装器)来实现一个图像识别程序。
阅读完本文后,读者将能够开发一个简单的应用程序,该程序可以在图像列表中搜索包含原始图像较小一部分的图像,并以图形方式显示两者之间的交点。
下载并安装 Emgu
Emgu 库可以在 https://sourceforge.net/projects/emgucv/files/ 下载。该软件包的最新版本位于 https://sourceforge.net/projects/emgucv/files/latest/download"source=files,它以 EXE 安装程序的形式提供。为了获得该库的相当完整的参考,我们建议您访问 http://www.emgu.com/wiki/index.php/Main_Page,这是 Emgu 的官方 Wiki。在此,我们将仅考虑实现匹配两张图像以在可能的候选列表中找到较小图像所必需的方面和功能。
下载后,可以将软件包安装在您选择的文件夹中。安装完成后,安装文件夹将如下图所示,其中 \bin 文件夹包含该软件包的核心组件,即将在项目中引用的 DLL。
创建 Emgu 引用项目
让我们使用 Visual Studio 创建一个新项目(CTRL+N)。在本例中,请从“模板”菜单中选择“Visual C#”和“Winforms 应用程序”。现在,从 Visual Studio 顶部菜单,转到“项目”->“添加引用”。选择“浏览”,然后转到之前运行安装程序创建的 Emgu 路径,选择 Emgu.CV.World.dll 和 Emgu.CV.UI.dll(图 2)。使用 OK 按钮确认,Visual Studio 将继续将这些引用添加到项目中,如图 3 所示。
重要提示:要使编译后的解决方案正常工作,必须将 \emgucv-windesktop 3.1.0.2504\bin 中的 x86 和 x64 文件夹复制到编译后的可执行文件路径中。否则,使用 Emgu 的程序将无法引用 cvextern.dll、msvcp140.dll、opencv_ffmpeg310_64.dll、vcruntime140.dll,从而引发异常并阻止它们正常运行。
SURF 检测
正如维基百科关于加速鲁棒特征(https://en.wikipedia.org/wiki/Speeded_up_robust_features )页面所述,
"[...] 是一种专利的局部特征检测器和描述符。它可以用于对象识别、图像配准、分类或 3D 重建等任务。它部分受到尺度不变特征变换 (SIFT) 描述符的启发。标准版 SURF 比 SIFT 快几倍,并且其作者声称它比 SIFT 对不同的图像变换更鲁棒。为了检测兴趣点,SURF 使用了Hessian行列式斑点检测器的整数近似,该检测器可以使用预计算的积分图像通过 3 个整数运算来计算。其特征描述符基于兴趣点周围的Haar小波响应之和。这些也可以借助积分图像来计算。SURF 描述符已被用于定位和识别对象、人或面部,重建 3D 场景,跟踪对象以及提取兴趣点。"
特征匹配示例
在 \emgucv-windesktop 3.1.0.2504\Emgu.CV.Example\FeatureMatching 文件夹中,有一个示例,用于演示上述图像识别功能,因此它是进行进一步实现的一个很好的起点。让我们从 FeatureMatching.cs 文件开始:static
方法 Main()
中包含了几行代码。对于当前目的,我们感兴趣的是以下内容:
long matchTime;
using(Mat modelImage = CvInvoke.Imread("box.png", ImreadModes.Grayscale))
using (Mat observedImage = CvInvoke.Imread("box_in_scene.png", ImreadModes.Grayscale))
{
Mat result = DrawMatches.Draw(modelImage, observedImage, out matchTime);
ImageViewer.Show(result, String.Format("Matched in {0} milliseconds", matchTime));
}
Mat
类可以大致与 Image
类配对,它拥有一系列用于各种用途的方法和属性。如前所述,这里我们将仅限于基本功能。有关 Mat
类的完整参考,请访问此链接。
Imread
方法将从给定的路径和加载类型(灰度、彩色等)读取图像,并返回一个 Mat
对象供进一步使用。我们有两个来自不同源的 Mat
变量,即“box.png”和“box_in_scene.png”(两者都包含在示例文件夹中)。
正如我们在讨论此示例的页面(http://www.emgu.com/wiki/index.php/SURF_feature_detector_in_CSharp)上所见,处理的最终结果(即,显然,显示 DrawMatches.Draw
方法的结果)将如下所示:
因此,这里的工作很清楚:将加载两张图像,其中一张是另一张的子部分,然后将两者进行匹配以显示它们的共同点,从而确定相似性/包含关系。此匹配由 static
类 DrawMatches
的 Draw()
方法执行,该方法可以在示例文件夹中找到,或在上面列出的 SURF 检测器 URL 中找到。源代码如下:
//----------------------------------------------------------------------------
// Copyright (C) 2004-2016 by EMGU Corporation. All rights reserved.
//----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Features2D;
using Emgu.CV.Flann;
using Emgu.CV.Structure;
using Emgu.CV.Util;
namespace FeatureMatchingExample
{
public static class DrawMatches
{
public static void FindMatch(Mat modelImage, Mat observedImage,
out long matchTime, out VectorOfKeyPoint modelKeyPoints,
out VectorOfKeyPoint observedKeyPoints, VectorOfVectorOfDMatch matches,
out Mat mask, out Mat homography)
{
int k = 2;
double uniquenessThreshold = 0.80;
Stopwatch watch;
homography = null;
modelKeyPoints = new VectorOfKeyPoint();
observedKeyPoints = new VectorOfKeyPoint();
using (UMat uModelImage = modelImage.GetUMat(AccessType.Read))
using (UMat uObservedImage = observedImage.GetUMat(AccessType.Read))
{
KAZE featureDetector = new KAZE();
//extract features from the object image
Mat modelDescriptors = new Mat();
featureDetector.DetectAndCompute
(uModelImage, null, modelKeyPoints, modelDescriptors, false);
watch = Stopwatch.StartNew();
// extract features from the observed image
Mat observedDescriptors = new Mat();
featureDetector.DetectAndCompute(uObservedImage, null,
observedKeyPoints, observedDescriptors, false);
// Bruteforce, slower but more accurate
// You can use KDTree for faster matching with slight loss in accuracy
using (Emgu.CV.Flann.LinearIndexParams ip = new Emgu.CV.Flann.LinearIndexParams())
using (Emgu.CV.Flann.SearchParams sp = new SearchParams())
using (DescriptorMatcher matcher = new FlannBasedMatcher(ip, sp))
{
matcher.Add(modelDescriptors);
matcher.KnnMatch(observedDescriptors, matches, k, null);
mask = new Mat(matches.Size, 1, DepthType.Cv8U, 1);
mask.SetTo(new MCvScalar(255));
Features2DToolbox.VoteForUniqueness(matches, uniquenessThreshold, mask);
int nonZeroCount = CvInvoke.CountNonZero(mask);
if (nonZeroCount >= 4)
{
nonZeroCount = Features2DToolbox.VoteForSizeAndOrientation(modelKeyPoints, observedKeyPoints,
matches, mask, 1.5, 20);
if (nonZeroCount >= 4)
homography = Features2DToolbox.GetHomographyMatrixFromMatchedFeatures(modelKeyPoints,
observedKeyPoints, matches, mask, 2);
}
}
watch.Stop();
}
matchTime = watch.ElapsedMilliseconds;
}
/// <summary>
/// Draw the model image and observed image, the matched features and homography projection.
/// </summary>
/// <param name="modelImage">The model image</param>
/// <param name="observedImage">The observed image</param>
/// <param name="matchTime">The output total time for computing the homography matrix.
/// </param>
/// <returns>The model image and observed image,
/// the matched features and homography projection.</returns>
public static Mat Draw(Mat modelImage, Mat observedImage, out long matchTime)
{
Mat homography;
VectorOfKeyPoint modelKeyPoints;
VectorOfKeyPoint observedKeyPoints;
using (VectorOfVectorOfDMatch matches = new VectorOfVectorOfDMatch())
{
Mat mask;
FindMatch(modelImage, observedImage, out matchTime,
out modelKeyPoints, out observedKeyPoints, matches,
out mask, out homography);
//Draw the matched keypoints
Mat result = new Mat();
Features2DToolbox.DrawMatches(modelImage, modelKeyPoints, observedImage, observedKeyPoints,
matches, result, new MCvScalar(255, 255, 255), new MCvScalar(255, 255, 255), mask);
#region draw the projected region on the image
if (homography != null)
{
//draw a rectangle along the projected model
Rectangle rect = new Rectangle(Point.Empty, modelImage.Size);
PointF[] pts = new PointF[]
{
new PointF(rect.Left, rect.Bottom),
new PointF(rect.Right, rect.Bottom),
new PointF(rect.Right, rect.Top),
new PointF(rect.Left, rect.Top)
};
pts = CvInvoke.PerspectiveTransform(pts, homography);
#if NETFX_CORE
Point[] points = Extensions.ConvertAll<PointF, Point>(pts, Point.Round);
#else
Point[] points = Array.ConvertAll<PointF, Point>(pts, Point.Round);
#endif
using (VectorOfPoint vp = new VectorOfPoint(points))
{
CvInvoke.Polylines(result, vp, true, new MCvScalar(255, 0, 0, 255), 5);
}
}
#endregion
return result;
}
}
}
}
核心方法是 FindMatch()
,用于发现图像之间的相似性,并填充将在 Draw()
方法中用于图形化显示的数组。我们不会在此详细分析上述代码,但在接下来的内容中,我们将对其进行修改,使其不仅能够发现差异,还能告诉用户存在多少差异,从而使应用程序能够从图像列表中确定哪个图像最适合详细图像。
开发解决方案
为了分析任意数量的图像,以检测包含我们模型图像的图像,有必要为每个分析的图像提供相似性得分。这个想法很简单:给定一个包含图像的文件夹(或多个文件夹)和一个要定位的小图像,程序必须处理其中的每一个,确定一个值,该值表示在循环图像中发现了多少模型图像的关键点。在循环结束后,返回较高值的图像最有可能符合我们的预期结果(换句话说,得分越高,我们的模型包含在这些图像中的可能性就越高)。
确定相似性得分
FindMatch()
方法可以自定义以公开找到的图像匹配计数。
其签名可以修改为
public static void FindMatch(Mat modelImage, Mat observedImage, out long matchTime,
out VectorOfKeyPoint modelKeyPoints, out VectorOfKeyPoint observedKeyPoints,
VectorOfVectorOfDMatch matches, out Mat mask, out Mat homography, out long score)
将 score
变量添加为输出参数。在调用 VoteForUniqueness()
之后,通过在 VectorOfVectorOfDMatch
matches 之间循环来计算其值,每次遇到匹配时增加 score 值。
score = 0;
for (int i = 0; i < matches.Size; i++)
{
if (mask.GetData(i)[0] == 0) continue;
foreach (var e in matches[i].ToArray())
++score;
}
FindMatch()
方法因此变成如下:
public static void FindMatch(Mat modelImage, Mat observedImage, out long matchTime,
out VectorOfKeyPoint modelKeyPoints, out VectorOfKeyPoint observedKeyPoints,
VectorOfVectorOfDMatch matches, out Mat mask, out Mat homography, out long score)
{
int k = 2;
double uniquenessThreshold = 0.80;
Stopwatch watch;
homography = null;
modelKeyPoints = new VectorOfKeyPoint();
observedKeyPoints = new VectorOfKeyPoint();
using (UMat uModelImage = modelImage.GetUMat(AccessType.Read))
using (UMat uObservedImage = observedImage.GetUMat(AccessType.Read))
{
KAZE featureDetector = new KAZE();
Mat modelDescriptors = new Mat();
featureDetector.DetectAndCompute(uModelImage, null, modelKeyPoints, modelDescriptors, false);
watch = Stopwatch.StartNew();
Mat observedDescriptors = new Mat();
featureDetector.DetectAndCompute
(uObservedImage, null, observedKeyPoints, observedDescriptors, false);
// KdTree for faster results / less accuracy
using (var ip = new Emgu.CV.Flann.KdTreeIndexParams())
using (var sp = new SearchParams())
using (DescriptorMatcher matcher = new FlannBasedMatcher(ip, sp))
{
matcher.Add(modelDescriptors);
matcher.KnnMatch(observedDescriptors, matches, k, null);
mask = new Mat(matches.Size, 1, DepthType.Cv8U, 1);
mask.SetTo(new MCvScalar(255));
Features2DToolbox.VoteForUniqueness(matches, uniquenessThreshold, mask);
// Calculate score based on matches size
// ---------------------------------------------->
score = 0;
for (int i = 0; i < matches.Size; i++)
{
if (mask.GetData(i)[0] == 0) continue;
foreach (var e in matches[i].ToArray())
++score;
}
// <----------------------------------------------
int nonZeroCount = CvInvoke.CountNonZero(mask);
if (nonZeroCount >= 4)
{
nonZeroCount = Features2DToolbox.VoteForSizeAndOrientation
(modelKeyPoints, observedKeyPoints, matches, mask, 1.5, 20);
if (nonZeroCount >= 4)
homography = Features2DToolbox.GetHomographyMatrixFromMatchedFeatures
(modelKeyPoints, observedKeyPoints, matches, mask, 2);
}
}
watch.Stop();
}
matchTime = watch.ElapsedMilliseconds;
}
注意:为了在准确性上获得速度,在上述代码中,LinearIndexParams
已被 KdTreeIndexParams
替换。
显然,每个调用 FindMatch()
的方法都必须包含新的 score 变量。因此,Draw()
方法也必须相应地进行自定义。简而言之,每次执行 Draw()
方法时,都必须传递一个长整型变量,该变量将在传递给 FindMatch()
方法时计算 score。有关更多参考,请参阅文章末尾的源代码。
图像和得分查看器
为了快速查看任何图像的得分值,可以创建一个自定义图像查看器窗体。
声明一个新的窗体,命名为 emImageViewer
,并在其中添加一个 PictureBox
(示例中为 imgBox
),将其 Dock
属性设置为 Fill
。源代码如下:
using Emgu.CV;
using System.Drawing;
using System.Windows.Forms;
namespace ImgRecognitionEmGu
{
public partial class emImageViewer : Form
{
public emImageViewer(IImage image, long score = 0)
{
InitializeComponent();
this.Text = "Score: " + score.ToString();
if (image != null)
{
imgBox.Image = image.Bitmap;
Size size = image.Size;
size.Width += 12;
size.Height += 42;
if (!Size.Equals(size)) Size = size;
}
}
}
}
构造函数需要两个参数:第一个是 image
,类型为 IImage
,构成要在 imgBox
中可视化的图像(已转换为 Mat
对象)。第二个参数 score
是由上述方法计算的值。因此,如果我们使用以下代码比较两张图像:
long score;
long matchTime;
using (Mat modelImage = CvInvoke.Imread("box.png", ImreadModes.Grayscale))
using (Mat observedImage = CvInvoke.Imread("box_in_scene.png", ImreadModes.Grayscale))
{
Mat result = DrawMatches.Draw(modelImage, observedImage, out matchTime, out score);
var iv = new emImageViewer(result, score);
iv.Show();
}
结果将是。
显示得分为 144
。显然,更改模型图像将导致不同的得分值。例如,如果我们想将场景图像与自身进行比较,我们将得到:
处理图像列表
此时,计算图像列表的得分变得微不足道。
假设我们有一个给定的目录 images,其中包含其子文件夹结构,每个子文件夹可能包含图像。现在需要的是遍历整个目录树,并对找到的每个文件执行比较。计算出的得分可以保存在一个合适的内存结构中,并在处理结束时列出。
我们首先定义该结构/类:
class WeightedImages
{
public string ImagePath { get; set; } = "";
public long Score { get; set; } = 0;
}
WeightedImages
将用于记住完整图像的路径及其计算出的得分。该类将与 List<>
结合使用。
List<WeightedImages> imgList = new List<WeightedImages>();
一个简单的方法 ProcessFolder()
将递归地解析结构中每个目录的每个文件。对于其中的每一个,它将调用 ProcessImage()
,这是过程的核心。
private void ProcessFolder(string mainFolder, string detailImage)
{
foreach (var file in System.IO.Directory.GetFiles(mainFolder))
ProcessImage(file, detailImage);
foreach (var dir in System.IO.Directory.GetDirectories(mainFolder))
ProcessFolder(dir, detailImage);
}
private void ProcessImage(string completeImage, string detailImage)
{
if (completeImage == detailImage) return;
try
{
long score;
long matchTime;
using (Mat modelImage = CvInvoke.Imread(detailImage, ImreadModes.Color))
using (Mat observedImage = CvInvoke.Imread(completeImage, ImreadModes.Color))
{
Mat homography;
VectorOfKeyPoint modelKeyPoints;
VectorOfKeyPoint observedKeyPoints;
using (var matches = new VectorOfVectorOfDMatch())
{
Mat mask;
DrawMatches.FindMatch(modelImage, observedImage, out matchTime,
out modelKeyPoints, out observedKeyPoints, matches,
out mask, out homography, out score);
}
imgList.Add(new WeightedImages() { ImagePath = completeImage, Score = score });
}
} catch { }
}
ProcessImage()
不需要绘制结果对象,它可以在计算出得分后停止,并将该值(以及文件名)添加到 imgList
中。
一个按钮将启动该过程。
完成后,imgList
按得分值降序排序,并用作 DataGridView
的 DataSource
,以显示过程本身的结果。
在此示例中,搜索的图像是:
如您所见,已遍历了示例目录,并为找到的每个图像计算了得分,将搜索的图像与完整图像进行了比较。请注意,由于这些图像不是灰度图,而是彩色的,因此加载类型也相应设置为(ImreadModes.Color
)。
第二个按钮允许使用上面创建的 emImageViewer
打开当前选定网格行所代表的图像。
private void btnShow_Click(object sender, System.EventArgs e)
{
string imgPath = resultGrid.CurrentRow.Cells[0].Value.ToString();
long score;
long matchTime;
using (Mat modelImage = CvInvoke.Imread(_detailedImage, ImreadModes.Color))
using (Mat observedImage = CvInvoke.Imread(imgPath, ImreadModes.Color))
{
var result = DrawMatches.Draw(modelImage, observedImage, out matchTime, out score);
var iv = new emImageViewer(result, score);
iv.Show();
}
}
打开列表中的所有图像会得到以下结果:
我们可以看到正确图像是如何通过列表中最高的得分被发现的。
进一步实现
提出的代码仅在学习场景中有效。进一步的实现,例如管理大量图像(成千上万),可以通过使程序异步/多线程来实现,以更好地分配资源并减少处理时间。
参考文献
历史
- 2017年5月16日:初始版本