编辑
2025-02-03
C# 应用
00
请注意,本文编写于 92 天前,最后修改于 92 天前,其中某些信息可能已经过时。

目录

环境准备
完整代码示例
调整图像大小
旋转目标
关键步骤
非极大值抑制(Non-Maximum Suppression, NMS)算法
代码关键点

使用OpenCvSharp在C#中进行模板匹配是一个相对直观的方法,但对于多角度的目标匹配和多个目标匹配,这需要一些额外的步骤和细节处理。在本文中,我们将详细介绍如何使用OpenCvSharp库实现多角度模板匹配,框选匹配目标并计数。

环境准备

在开始之前,请确保你已经安装了以下工具和库:

  1. Visual Studio 或 Rider 等 C# 开发环境
  2. .NET SDK
  3. OpenCvSharp 库

你可以通过 NuGet 包管理器安装 OpenCvSharp:

Bash
Install-Package OpenCvSharp4 Install-Package OpenCvSharp4.runtime.win

完整代码示例

下面是一个完整的示例代码,逐步讲解如何实现多角度模板匹配多个目标,并在匹配的目标上画红色框并计数:

C#
static void Main(string[] args) { // 加载库和图像 Mat sourceImage = Cv2.ImRead("clip.png", ImreadModes.Color); Mat templateImage = Cv2.ImRead("template1.png", ImreadModes.Color); const double threshold = 0.7; // 模板匹配的阈值 double rotationStep = 10; // 旋转角度步长 double minScale = 0.9; // 最小缩放比例 double maxScale = 1.1; // 最大缩放比例 double scaleStep = 0.1; // 缩放比例步长 double overlapThreshold = 0.3; // NMS的重叠阈值 // 转为灰度图像 Mat sourceGray = sourceImage.CvtColor(ColorConversionCodes.BGR2GRAY); Mat templateGray = templateImage.CvtColor(ColorConversionCodes.BGR2GRAY); List<Rect> possibleMatches = new List<Rect>(); // 循环多个角度和缩放比例 for (double scale = minScale; scale <= maxScale; scale += scaleStep) { Mat resizedTemplate = ResizeImage(templateGray, scale); for (int angle = 0; angle < 360; angle += (int)rotationStep) { Mat rotatedTemplate = RotateImage(resizedTemplate, angle); // 进行模板匹配 Mat result = new Mat(); Cv2.MatchTemplate(sourceGray, rotatedTemplate, result, TemplateMatchModes.CCoeffNormed); // 检测匹配位置 while (true) { double minVal, maxVal; Point minLoc, maxLoc; Cv2.MinMaxLoc(result, out minVal, out maxVal, out minLoc, out maxLoc); // 如果找到的最大匹配区域大于阈值 if (maxVal >= threshold) { // 创建匹配矩形区域 Rect matchRect = new Rect(maxLoc.X, maxLoc.Y, rotatedTemplate.Width, rotatedTemplate.Height); possibleMatches.Add(matchRect); // 将检测过的区域置为负值,防止重复检测 Cv2.FloodFill(result, maxLoc, new Scalar(-1)); } else { break; } } rotatedTemplate.Dispose(); } resizedTemplate.Dispose(); } // 使用NMS过滤结果 var filteredMatches = NonMaximumSuppression(possibleMatches, overlapThreshold); // 绘制结果 foreach (var match in filteredMatches) { Cv2.Rectangle(sourceImage, match, Scalar.Red, 2); } // 显示并保存结果 Cv2.ImShow("Result Image", sourceImage); Cv2.ImWrite("result.png", sourceImage); Cv2.WaitKey(); Console.WriteLine($"Matched objects count: {filteredMatches.Count}"); }

调整图像大小

C#
/// <summary> /// 调整图像大小 /// </summary> /// <param name="image">输入的Mat图像</param> /// <param name="scale">缩放比例</param> /// <returns>调整大小后的图像</returns> static Mat ResizeImage(Mat image, double scale) { Mat resized = new Mat(); Cv2.Resize(image, resized, new Size(), scale, scale, InterpolationFlags.Linear); return resized; }

旋转目标

C#
static Rect RotatedRectangleBoundingBox(Point2f center, Size2f size, double angle) { // 旋转后的各个角点 Point2f[] corners = new Point2f[] { new Point2f(-size.Width / 2, -size.Height / 2), // 左上角 new Point2f(size.Width / 2, -size.Height / 2), // 右上角 new Point2f(size.Width / 2, size.Height / 2), // 右下角 new Point2f(-size.Width / 2, size.Height / 2) // 左下角 }; // 将角度从度转换为弧度 double radians = angle * Math.PI / 180.0; // 旋转后的角点数组 Point2f[] rotatedCorners = new Point2f[4]; // 计算旋转后的角点位置 for (int i = 0; i < 4; i++) { rotatedCorners[i] = new Point2f( (float)(corners[i].X * Math.Cos(radians) - corners[i].Y * Math.Sin(radians) + center.X), // 旋转并平移到新的X坐标 (float)(corners[i].X * Math.Sin(radians) + corners[i].Y * Math.Cos(radians) + center.Y) // 旋转并平移到新的Y坐标 ); } // 初始化边界框的最小和最大坐标 float minX = rotatedCorners[0].X; float maxX = rotatedCorners[0].X; float minY = rotatedCorners[0].Y; float maxY = rotatedCorners[0].Y; // 找到旋转后的边界框 for (int i = 1; i < rotatedCorners.Length; i++) { if (rotatedCorners[i].X < minX) minX = rotatedCorners[i].X; // 更新最小X坐标 if (rotatedCorners[i].X > maxX) maxX = rotatedCorners[i].X; // 更新最大X坐标 if (rotatedCorners[i].Y < minY) minY = rotatedCorners[i].Y; // 更新最小Y坐标 if (rotatedCorners[i].Y > maxY) maxY = rotatedCorners[i].Y; // 更新最大Y坐标 } // 返回包含旋转后矩形的最小边界框 return new Rect((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); }
C#
static Rect RotatedRectangleBoundingBox(Point2f center, Size2f size, double angle) { // 旋转后的各个角点 Point2f[] corners = new Point2f[] { new Point2f(-size.Width / 2, -size.Height / 2), // 左上角 new Point2f(size.Width / 2, -size.Height / 2), // 右上角 new Point2f(size.Width / 2, size.Height / 2), // 右下角 new Point2f(-size.Width / 2, size.Height / 2) // 左下角 }; // 将角度从度转换为弧度 double radians = angle * Math.PI / 180.0; // 旋转后的角点数组 Point2f[] rotatedCorners = new Point2f[4]; // 计算旋转后的角点位置 for (int i = 0; i < 4; i++) { rotatedCorners[i] = new Point2f( (float)(corners[i].X * Math.Cos(radians) - corners[i].Y * Math.Sin(radians) + center.X), // 旋转并平移到新的X坐标 (float)(corners[i].X * Math.Sin(radians) + corners[i].Y * Math.Cos(radians) + center.Y) // 旋转并平移到新的Y坐标 ); } // 初始化边界框的最小和最大坐标 float minX = rotatedCorners[0].X; float maxX = rotatedCorners[0].X; float minY = rotatedCorners[0].Y; float maxY = rotatedCorners[0].Y; // 找到旋转后的边界框 for (int i = 1; i < rotatedCorners.Length; i++) { if (rotatedCorners[i].X < minX) minX = rotatedCorners[i].X; // 更新最小X坐标 if (rotatedCorners[i].X > maxX) maxX = rotatedCorners[i].X; // 更新最大X坐标 if (rotatedCorners[i].Y < minY) minY = rotatedCorners[i].Y; // 更新最小Y坐标 if (rotatedCorners[i].Y > maxY) maxY = rotatedCorners[i].Y; // 更新最大Y坐标 } // 返回包含旋转后矩形的最小边界框 return new Rect((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); }

关键步骤

  1. 定义原始角点:首先,定义一个矩形的四个原始角点,但它们是相对于矩形中心的,即以中心点 (0,0) 为基准。
  2. 转换角度为弧度:将输入的旋转角度从度数转换为弧度,因为在计算旋转矩阵时需要弧度制。
  3. 计算旋转后的角点位置:通过旋转矩阵公式将每个角点旋转,并平移到新的位置。
  4. 初始化边界框的最小和最大坐标:初始化边界框的最小和最大 X 和 Y 坐标为旋转后的第一个角点的坐标。
  5. 寻找最小边界框:遍历所有旋转后的角点,更新边界框的最小和最大坐标。
  6. 返回边界框:使用最小和最大坐标来构建并返回最终的边界框。

非极大值抑制(Non-Maximum Suppression, NMS)算法

C#
// 非极大值抑制算法实现 static List<Rect> NonMaximumSuppression(List<Rect> boxes, double overlapThreshold) { // 检查输入是否为空 if (boxes.Count == 0) { return new List<Rect>(); // 如果没有输入框,则返回空列表 } // 将矩形框根据其面积从小到大排序 boxes = boxes.OrderBy(box => box.Width * box.Height).ToList(); List<Rect> result = new List<Rect>(); // 存储最终保留的矩形框 // 循环处理每个框 while (boxes.Count > 0) { // 取出面积最大的矩形框 var box = boxes[boxes.Count - 1]; result.Add(box); // 将该框加入结果集 boxes.RemoveAt(boxes.Count - 1); // 移除该框 // 删除与当前框有较大重叠的框 boxes.RemoveAll(b => { // 计算两个矩形框的交集面积 double intersectionArea = (box & b).Area(); // 计算两个矩形框的并集面积 double unionArea = box.Area() + b.Area() - intersectionArea; // 计算交并比(Intersection over Union, IoU) double overlap = intersectionArea / unionArea; // 如果交并比大于等于设定的阈值,则删除该框 return overlap >= overlapThreshold; }); } return result; // 返回保留的矩形框列表 } static class RectExtensions { // 计算矩形框的面积 public static double Area(this Rect rect) { return rect.Width * rect.Height; } }

代码关键点

  1. 排序矩形框:首先,将输入的矩形框根据其面积进行升序排序。这意味着我们将会先处理面积较小的框,最后处理面积最大的框。
  2. 处理循环:在 while 循环中,我们每次取出面积最大的矩形框,将其添加到结果列表 result 中,并从 boxes 列表中删除。
  3. 删除重叠框:通过 boxes.RemoveAll 方法来删除与当前选中的框具有较大重叠的其他框。具体方法是计算每个框与当前选中框的交并比(IoU),如果IoU大于等于指定的 overlapThreshold,则删除该框。
  4. 计算交集面积和并集面积:使用扩展方法 Area 来计算矩形框的面积。交集面积可以通过两个矩形的交集部分计算得到,并集面积则是两个矩形面积之和减去交集面积。
  5. 返回结果:所有框处理完成后,返回结果列表 result,其中包含所有保留下来的矩形框。

image.png

本文作者:rick

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!