需求和效果预览
对于下图,需要检测左右两侧是否断开:
解决分析
设置左右2个ROI区域,找到ROI内面积最大的连通域,通过面积阈值和连通域宽高比判定是否断开。
可能遇到的问题:部分区域反光严重,二值化阈值不容易写死,所以可以用动态阈值自动调整阈值。
实现
- 基于OpenCvSharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using OpenCvSharp;namespace blob
{class Program{static void Main(string[] args){// 定义 二值化阈值 -- 使用动态阈值效果最佳int binary_threshold = 220;bool use_AdaptiveThreshold = true;// 定义 2个ROI区域int roi_w = 600, roi_h = 1570; // 左右两侧的ROI共用一个宽高int roi_x1_left = 220, roi_y1_left = 166; // 表示左上角的坐标int roi_x1_right = 900, roi_y1_right = 166;// 定义 blob的宽高比int hwRatio = 3;// 定义 blob面积上下限int minBlobArea = 140000;int maxBlobArea = 210000;string inputImage = "${input images path}";string saveResult = "${output images path}";foreach (string filePath in Directory.GetFiles(inputImage, "*.*", SearchOption.TopDirectoryOnly)){// 计算每张图片的计算时间double start = Cv2.GetTickCount() / Cv2.GetTickFrequency();// 加载图像Mat image = Cv2.ImRead(filePath, ImreadModes.Color);Mat image_raw = image.Clone(); // 用来可视化的// 图像预处理Cv2.CvtColor(image, image, ColorConversionCodes.BGR2GRAY);Size KernelBlur = new Size(3, 3);Cv2.GaussianBlur(image, image, KernelBlur, 0);// 二值化if (use_AdaptiveThreshold){// 动态阈值,通过计算像素点周围的k*k区域的加权平均,然后减去一个常数来得到自适应阈值; 11指窗口大小为11*11,2指减去的常数// k大一些效果更好,k=3的时候效果就不行,但k越大,速度越慢Cv2.AdaptiveThreshold(image, image, 255, AdaptiveThresholdTypes.MeanC, ThresholdTypes.Binary, 11, 2);}else{// 硬二值化Cv2.Threshold(image, image, binary_threshold, 255, ThresholdTypes.Binary);}// 执行膨胀和腐蚀操作Mat KernelSize = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3));Cv2.Erode(image, image, KernelSize, iterations:4);Cv2.Dilate(image, image, KernelSize, iterations:4);// 连通域分析Mat labels_32S = new Mat();Mat stats = new Mat();Mat centroids = new Mat();int num_labels = Cv2.ConnectedComponentsWithStats(image, labels_32S, stats, centroids); // 函数输出的labels_32S是MatType.CV_32S,32位系统可能导致内存分配失败Mat labels = new Mat(labels_32S.Size(), MatType.CV_8UC1); // 转为CV_8UC1labels_32S.ConvertTo(labels, MatType.CV_8UC1);// 提取 ROI 标签Mat roiLabelLeft = labels[new Rect(roi_x1_left, roi_y1_left, roi_w, roi_h)];Mat roiLabelRight = labels[new Rect(roi_x1_right, roi_y1_right, roi_w, roi_h)];// 初始化字典 -- maxAreaDict和maxAreaCoord的键都是"roi_left","roi_right",maxAreaDict的值是最大的blob面积,maxAreaCoord的值是最大面积对应的坐标Dictionary<string, int> maxAreaDict = new Dictionary<string, int>();Dictionary<string, List<int>> maxAreaCoord = new Dictionary<string, List<int>>();string[] roiNames = { "roi_left", "roi_right" };for (int i = 0; i < roiNames.Length; i++){string roiName = roiNames[i];Mat roiLabel = (i == 0) ? roiLabelLeft : roiLabelRight;int maxArea = 0;// ############### blob 分析 ###############// 创建与 roiLabel 相同大小的2个Mat// 注!new labelMask和labelValue必须在for外,不然会内存因不够而分配错误,而且运行很慢!Mat labelMask = new Mat(roiLabel.Size(), MatType.CV_8UC1);Mat labelValue = new Mat(roiLabel.Size(), MatType.CV_8UC1);for (int label = 1; label < num_labels; label++) // 从 1 开始,因为 0 是背景{labelValue.SetTo(new Scalar(label));Cv2.Compare(roiLabel, labelValue, labelMask, CmpType.EQ);// 计算面积int area = Cv2.CountNonZero(labelMask);// 获取坐标和宽高int x = (int)stats.At<int>(label, 0);int y = (int)stats.At<int>(label, 1);int width = (int)stats.At<int>(label, 2);int height = (int)stats.At<int>(label, 3);// 筛选连通域if (area > maxArea && (height / (double)width > hwRatio)) // 在满足宽高比的情况下,找到最大面积的连通域{var coor = new List<int> {x, y, width, height };maxAreaCoord[roiName] = coor;maxArea = area;}}maxAreaDict[roiName] = maxArea;}foreach (var blob in maxAreaCoord){string key = blob.Key;maxAreaDict.TryGetValue(key, out int max_area);if (max_area > minBlobArea && max_area < maxBlobArea) // 检查最大面积的连通域是否在设定的面积阈值内{List<int> coor_values = blob.Value;int x = coor_values[0];int y = coor_values[1];int width = coor_values[2];int height = coor_values[3];Rect rect = new Rect(x, y, width, height);Cv2.Rectangle(image_raw, rect, new Scalar(0, 255, 0), 4);string text = max_area.ToString();Point textLocation = new Point(rect.X, rect.Bottom + 60); // 显示在矩形框下面60个像素Cv2.PutText(image_raw, text, textLocation, HersheyFonts.HersheySimplex, fontScale:2.0, new Scalar(0, 255, 0), 3);} }double end = Cv2.GetTickCount() / Cv2.GetTickFrequency();string fileName = Path.GetFileName(filePath);string outputFilePath = Path.Combine(saveResult, fileName);// 保存处理后的图像Cv2.ImWrite(outputFilePath, image_raw);Console.WriteLine($"保存在: {outputFilePath}, 处理时间 = {end - start}");}}}
}
- 基于Emgu
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
using Emgu.CV.Util;namespace blob
{class Program{static void Main(string[] args){int binary_threshold = 220;bool use_AdaptiveThreshold = true;int roi_w = 600, roi_h = 1570;int roi_x1_left = 220, roi_y1_left = 166;int roi_x1_right = 900, roi_y1_right = 166;int hwRatio = 3;int minBlobArea = 140000;int maxBlobArea = 210000;string inputImage = "${input images path}";string saveResult = "${output images path}";foreach (string filePath in Directory.GetFiles(inputImage, "*.*", SearchOption.TopDirectoryOnly)){Mat image = CvInvoke.Imread(filePath, ImreadModes.Color);Mat image_raw = image.Clone();// 定义核大小,统一用3*3的System.Drawing.Size KernelSize = new System.Drawing.Size(3, 3);CvInvoke.CvtColor(image, image, ColorConversion.Bgr2Gray);CvInvoke.GaussianBlur(image, image, KernelSize, 0);if (use_AdaptiveThreshold){CvInvoke.AdaptiveThreshold(image, image, 255, AdaptiveThresholdType.MeanC, ThresholdType.Binary, 5, 2);}else{CvInvoke.Threshold(image, image, binary_threshold, 255, ThresholdType.Binary);}Mat kernel = CvInvoke.GetStructuringElement(ElementShape.Rectangle, KernelSize, new System.Drawing.Point(-1, -1));CvInvoke.Erode(image, image, kernel, new System.Drawing.Point(-1, -1), 4, BorderType.Default, new MCvScalar(0));CvInvoke.Dilate(image, image, kernel, new System.Drawing.Point(-1, -1), 4, BorderType.Default, new MCvScalar(0));Mat labels = new Mat();Mat stats = new Mat();Mat centroids = new Mat();int num_labels = CvInvoke.ConnectedComponentsWithStats(image, labels, stats, centroids, LineType.EightConnected);Mat roiLabelLeft = new Mat(labels, new System.Drawing.Rectangle(roi_x1_left, roi_y1_left, roi_w, roi_h));Mat roiLabelRight = new Mat(labels, new System.Drawing.Rectangle(roi_x1_right, roi_y1_right, roi_w, roi_h));Dictionary<string, int> maxLabelDict = new Dictionary<string, int>();Dictionary<string, int> maxAreaDict = new Dictionary<string, int>();Dictionary<string, List<int>> maxAreaCoord = new Dictionary<string, List<int>>();string[] roiNames = { "roi_left", "roi_right" };for (int i = 0; i < roiNames.Length; i++){string roiName = roiNames[i];Mat roiLabel = (i == 0) ? roiLabelLeft : roiLabelRight;int maxArea = 0;int maxLabel = 0;int[] statsData = new int[stats.Rows * stats.Cols]; // 处理连通域分析结果--stats,后面容易数据处理stats.CopyTo(statsData);Mat labelMask = new Mat(roiLabel.Size, DepthType.Cv32S, 1);Mat labelValue = new Mat(roiLabel.Size, DepthType.Cv32S, 1);for (int label = 1; label < num_labels; label++) {labelValue.SetTo(new MCvScalar(label));CvInvoke.Compare(roiLabel, labelValue, labelMask, CmpType.Equal);int area = CvInvoke.CountNonZero(labelMask);int x = statsData[label * stats.Cols + 0];int y = statsData[label * stats.Cols + 1];int width = statsData[label * stats.Cols + 2];int height = statsData[label * stats.Cols + 3];if (area > maxArea && (height / (double)width > hwRatio)){var coor = new List<int> { x, y, width, height };maxAreaCoord[roiName] = coor;maxArea = area;maxLabel = label;}}maxLabelDict[roiName] = maxLabel;maxAreaDict[roiName] = maxArea;}foreach (var blob in maxAreaCoord){string key = blob.Key;maxAreaDict.TryGetValue(key, out int max_area);if (max_area > minBlobArea && max_area < maxBlobArea){List<int> coor_values = blob.Value;int x = coor_values[0];int y = coor_values[1];int width = coor_values[2];int height = coor_values[3];System.Drawing.Rectangle rect = new System.Drawing.Rectangle(x, y, width, height);CvInvoke.Rectangle(image_raw, rect, new MCvScalar(0, 255, 0), 4);string text = max_area.ToString();System.Drawing.Point textLocation = new System.Drawing.Point(rect.X, rect.Bottom + 60); CvInvoke.PutText(image_raw, text, textLocation, FontFace.HersheySimplex, 1.0, new MCvScalar(0, 255, 0), 2);}}string fileName = Path.GetFileName(filePath);string outputFilePath = Path.Combine(saveResult, fileName);CvInvoke.Imwrite(outputFilePath, image_raw);Console.WriteLine($"保存在: {outputFilePath}");//CvInvoke.Imshow("show", image_raw);//CvInvoke.WaitKey(0);//CvInvoke.DestroyAllWindows();}}}
}
处理结果
附录
blob可视化分析(代码暂未公开)
上图中,两个红色框框是设定的ROI区域,不同色块表示不同的连通域,右侧白框表示ROI区域内面积最大的连通域。