需求和效果预览
对于下图,需要检测左右两侧是否断开:
解决分析
设置左右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_8UC1
labels_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区域内面积最大的连通域。