2025-11-17
C#
00

目录

1. 简介
2. 环境准备
4. 简单对象计数示例
4.1 基础计数
4.2 带面积筛选的计数
5. 高级应用示例
5.1 基于形状特征的计数
三角型检测
找出圆形
6. 注意事项
7. 总结

1. 简介

本文将详细介绍如何使用C#和OpenCvSharp来实现基于轮廓检测的对象计数。我们将通过多个实例来说明不同场景下的轮廓检测和计数方法。

2. 环境准备

首先需要安装必要的NuGet包:

Bash
Install-Package OpenCvSharp4 Install-Package OpenCvSharp4.Windows

4. 简单对象计数示例

4.1 基础计数

C#
static void Main(string[] args) { CountClips("clip.png"); Cv2.WaitKey(0); Cv2.DestroyAllWindows(); } public static int CountClips(string imagePath) { using (var src = new Mat(imagePath)) using (var gray = new Mat()) using (var blur = new Mat()) using (var binary = new Mat()) { // 转换为灰度图 Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); // 高斯模糊 Cv2.GaussianBlur(gray, blur, new Size(5, 5), 0); // 使用自适应阈值 Cv2.AdaptiveThreshold( blur, binary, 255, AdaptiveThresholdTypes.GaussianC, ThresholdTypes.Binary, 11, // 邻域大小 2 // 常数差值 ); // 查找轮廓,包括内部轮廓 Point[][] contours; HierarchyIndex[] hierarchy; Cv2.FindContours( binary, out contours, out hierarchy, RetrievalModes.Tree, // 改用Tree模式 ContourApproximationModes.ApproxSimple); // 过滤轮廓 int clipCount = 0; for (int i = 0; i < contours.Length; i++) { // 计算轮廓周长 double perimeter = Cv2.ArcLength(contours[i], true); Cv2.DrawContours( src, contours, i, Scalar.Red, 2); clipCount++; } Cv2.ImShow("Result", src); return clipCount; } }

image.png

Python
Cv2.AdaptiveThreshold( blur, // 输入图像(通常是灰度图或经过模糊处理的图像) binary, // 输出图像(二值化结果) 255, // 最大值(像素被判定为白色时的值) AdaptiveThresholdTypes.GaussianC, // 自适应方法类型 ThresholdTypes.Binary, // 阈值类型 11, // 邻域大小(块大小) 2 // 常数差值(C) );

4.2 带面积筛选的计数

C#
for (int i = 0; i < contours.Length; i++) { // 计算轮廓面积 double area = Cv2.ContourArea(contours[i]); // 计算轮廓周长 double perimeter = Cv2.ArcLength(contours[i], true); //这个先不用 // 基于面积和周长比例筛选曲别针 if (area > 2000 && area < 7000) // 面积阈值需要根据实际图像调整 { Cv2.DrawContours( src, contours, i, Scalar.Red, 2); clipCount++; } }

image.png

5. 高级应用示例

5.1 基于形状特征的计数

三角型检测

C#
static void Main(string[] args) { CountTriangles("a.png"); } public static void CountTriangles(string imagePath) { using (var src = new Mat(imagePath)) using (var gray = new Mat()) using (var binary = new Mat()) { Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); Cv2.AdaptiveThreshold(gray, binary, 255, AdaptiveThresholdTypes.GaussianC, ThresholdTypes.BinaryInv, 11, 2); Point[][] contours; HierarchyIndex[] hierarchy; Cv2.FindContours( binary, out contours, out hierarchy, RetrievalModes.List, ContourApproximationModes.ApproxSimple); var triangles = new List<Triangle>(); Mat result = src.Clone(); foreach (var contour in contours) { double area = Cv2.ContourArea(contour); if (area < 100) continue; double perimeter = Cv2.ArcLength(contour, true); Point[] approx = Cv2.ApproxPolyDP(contour, 0.03 * perimeter, true); if (approx.Length == 3 && IsValidTriangle(approx)) { var newTriangle = new Triangle(approx); // 检查是否与现有三角形重叠 bool isOverlapping = false; foreach (var existingTriangle in triangles) { if (IsOverlapping(newTriangle, existingTriangle)) { isOverlapping = true; break; } } if (!isOverlapping) { triangles.Add(newTriangle); } } } // 绘制通过验证的三角形 for (int i = 0; i < triangles.Count; i++) { var triangle = triangles[i]; Cv2.DrawContours(result, new[] { triangle.Points }, -1, new Scalar(0, 0, 255), 2); // 在三角形中心显示序号 Point center = triangle.Center; Cv2.PutText(result, (i + 1).ToString(), center, HersheyFonts.HersheySimplex, 0.8, new Scalar(0, 0, 255), 2); } Console.WriteLine($"检测到的三角形数量: {triangles.Count}"); Cv2.ImWrite("detected_triangles.jpg", result); Cv2.ImShow("Detected Triangles", result); Cv2.WaitKey(0); Cv2.DestroyAllWindows(); } } private class Triangle { public Point[] Points { get; } public Point Center { get; } public double Area { get; } public Triangle(Point[] points) { Points = points; Center = new Point( (points[0].X + points[1].X + points[2].X) / 3, (points[0].Y + points[1].Y + points[2].Y) / 3 ); Area = Math.Abs((points[1].X - points[0].X) * (points[2].Y - points[0].Y) - (points[2].X - points[0].X) * (points[1].Y - points[0].Y)) / 2; } } private static bool IsOverlapping(Triangle t1, Triangle t2) { // 检查中心点距离 double centerDistance = Math.Sqrt( Math.Pow(t1.Center.X - t2.Center.X, 2) + Math.Pow(t1.Center.Y - t2.Center.Y, 2) ); // 如果中心点距离小于某个阈值,认为是重叠的 double threshold = Math.Min(Math.Sqrt(t1.Area), Math.Sqrt(t2.Area)) * 0.5; return centerDistance < threshold; } private static bool IsValidTriangle(Point[] points) { if (points.Length != 3) return false; double side1 = Distance(points[0], points[1]); double side2 = Distance(points[1], points[2]); double side3 = Distance(points[2], points[0]); if (side1 + side2 <= side3 || side2 + side3 <= side1 || side1 + side3 <= side2) return false; double minSide = Math.Min(Math.Min(side1, side2), side3); double maxSide = Math.Max(Math.Max(side1, side2), side3); double ratio = minSide / maxSide; return ratio > 0.2; } private static double Distance(Point p1, Point p2) { return Math.Sqrt(Math.Pow(p2.X - p1.X, 2) + Math.Pow(p2.Y - p1.Y, 2)); }

image.png

找出圆形

C#
static void Main(string[] args) { CountCircles("southeast.jpg"); } public static void CountCircles(string imagePath) { using (var src = new Mat(imagePath)) using (var gray = new Mat()) { // 转换为灰度图 Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); // 使用高斯模糊减少噪声 Cv2.GaussianBlur(gray, gray, new Size(9, 9), 2, 2); // 使用Hough圆变换检测圆 CircleSegment[] circles = Cv2.HoughCircles( gray, HoughModes.Gradient, dp: 1, // 累加器分辨率 minDist: 150, // 增加圆心之间的最小距离,避免重复检测 param1: 50, // 调整Canny边缘检测的阈值 param2: 25, // 降低累加器阈值,使检测更敏感 minRadius: 50, // 调整最小半径 maxRadius: 100 // 调整最大半径 ); Mat result = src.Clone(); var validCircles = new List<CircleSegment>(); // 过滤重叠的圆 foreach (var circle in circles) { bool isOverlapping = false; foreach (var existingCircle in validCircles) { if (IsCirclesOverlapping(circle, existingCircle)) { isOverlapping = true; break; } } if (!isOverlapping) { validCircles.Add(circle); } } // 绘制检测到的圆 for (int i = 0; i < validCircles.Count; i++) { var circle = validCircles[i]; // 绘制圆形轮廓 Cv2.Circle( result, (int)circle.Center.X, (int)circle.Center.Y, (int)circle.Radius, new Scalar(0, 0, 255), // 红色 2 ); // 绘制圆心 Cv2.Circle( result, (int)circle.Center.X, (int)circle.Center.Y, 3, new Scalar(0, 255, 0), // 绿色 -1 ); // 添加序号 Cv2.PutText( result, (i + 1).ToString(), new Point(circle.Center.X - 10, circle.Center.Y - 10), HersheyFonts.HersheySimplex, 0.8, new Scalar(0, 0, 255), 2 ); } Console.WriteLine($"检测到的圆形数量: {validCircles.Count}"); Cv2.ImWrite("detected_circles.jpg", result); Cv2.ImShow("Detected Circles", result); Cv2.WaitKey(0); Cv2.DestroyAllWindows(); } } private static bool IsCirclesOverlapping(CircleSegment c1, CircleSegment c2) { // 计算两个圆心之间的距离 double centerDistance = Math.Sqrt( Math.Pow(c1.Center.X - c2.Center.X, 2) + Math.Pow(c1.Center.Y - c2.Center.Y, 2) ); // 如果圆心距离小于两个圆半径之和的一定比例,认为是重叠的 double threshold = (c1.Radius + c2.Radius) * 0.5; return centerDistance < threshold; }

image.png

6. 注意事项

  1. 图像预处理的重要性
    • 适当的模糊处理可以减少噪声
    • 合适的二值化阈值对结果有重要影响
  2. 轮廓筛选
    • 面积阈值可以过滤掉噪声
    • 形状识别需要考虑轮廓近似的精度

7. 总结

本文详细介绍了使用C#和OpenCvSharp实现基于轮廓检测的对象计数方法,从基础的轮廓检测到复杂的形状和颜色分析都有详细的示例代码。这些方法可以根据实际需求进行组合和优化,实现更复杂的图像分析任务。

本文作者:技术老小子

本文链接:

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