// ============================================================================ // 文件名: VoidMeasurementProcessor.cs // 描述: 空隙测量算子 // // 处理流程: // 1. 构建多边形ROI掩码,计算ROI面积 // 2. 在ROI内进行双阈值分割提取气泡区域 // 3. 形态学膨胀合并相邻气泡 // 4. 轮廓检测,计算每个气泡面积 // 5. 计算空隙率 = 总气泡面积 / ROI面积 // ============================================================================ using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using Emgu.CV.Util; using XP.ImageProcessing.Core; using Serilog; using System.Drawing; namespace XP.ImageProcessing.Processors; public class VoidMeasurementProcessor : ImageProcessorBase { private static readonly ILogger _logger = Log.ForContext(); public VoidMeasurementProcessor() { Name = LocalizationHelper.GetString("VoidMeasurementProcessor_Name"); Description = LocalizationHelper.GetString("VoidMeasurementProcessor_Description"); } protected override void InitializeParameters() { // ── 多边形ROI(由UI注入,最多32个点) ── Parameters.Add("PolyCount", new ProcessorParameter("PolyCount", "PolyCount", typeof(int), 0, null, null, "") { IsVisible = false }); for (int i = 0; i < 32; i++) { Parameters.Add($"PolyX{i}", new ProcessorParameter($"PolyX{i}", $"PolyX{i}", typeof(int), 0, null, null, "") { IsVisible = false }); Parameters.Add($"PolyY{i}", new ProcessorParameter($"PolyY{i}", $"PolyY{i}", typeof(int), 0, null, null, "") { IsVisible = false }); } // ── 气泡检测参数 ── Parameters.Add("MinThreshold", new ProcessorParameter( "MinThreshold", LocalizationHelper.GetString("VoidMeasurementProcessor_MinThreshold"), typeof(int), 128, 0, 255, LocalizationHelper.GetString("VoidMeasurementProcessor_MinThreshold_Desc"))); Parameters.Add("MaxThreshold", new ProcessorParameter( "MaxThreshold", LocalizationHelper.GetString("VoidMeasurementProcessor_MaxThreshold"), typeof(int), 255, 0, 255, LocalizationHelper.GetString("VoidMeasurementProcessor_MaxThreshold_Desc"))); Parameters.Add("MinVoidArea", new ProcessorParameter( "MinVoidArea", LocalizationHelper.GetString("VoidMeasurementProcessor_MinVoidArea"), typeof(int), 10, 1, 100000, LocalizationHelper.GetString("VoidMeasurementProcessor_MinVoidArea_Desc"))); Parameters.Add("MergeRadius", new ProcessorParameter( "MergeRadius", LocalizationHelper.GetString("VoidMeasurementProcessor_MergeRadius"), typeof(int), 3, 0, 30, LocalizationHelper.GetString("VoidMeasurementProcessor_MergeRadius_Desc"))); Parameters.Add("BlurSize", new ProcessorParameter( "BlurSize", LocalizationHelper.GetString("VoidMeasurementProcessor_BlurSize"), typeof(int), 3, 1, 31, LocalizationHelper.GetString("VoidMeasurementProcessor_BlurSize_Desc"))); Parameters.Add("VoidLimit", new ProcessorParameter( "VoidLimit", LocalizationHelper.GetString("VoidMeasurementProcessor_VoidLimit"), typeof(double), 25.0, 0.0, 100.0, LocalizationHelper.GetString("VoidMeasurementProcessor_VoidLimit_Desc"))); } public override Image Process(Image inputImage) { int minThresh = GetParameter("MinThreshold"); int maxThresh = GetParameter("MaxThreshold"); int minVoidArea = GetParameter("MinVoidArea"); int mergeRadius = GetParameter("MergeRadius"); int blurSize = GetParameter("BlurSize"); double voidLimit = GetParameter("VoidLimit"); if (blurSize % 2 == 0) blurSize++; OutputData.Clear(); int w = inputImage.Width, h = inputImage.Height; // ── 构建多边形ROI掩码 ── int polyCount = GetParameter("PolyCount"); Image? roiMask = null; Point[]? roiPoints = null; if (polyCount >= 3) { roiPoints = new Point[polyCount]; for (int i = 0; i < polyCount; i++) roiPoints[i] = new Point(GetParameter($"PolyX{i}"), GetParameter($"PolyY{i}")); roiMask = new Image(w, h); using var vop = new VectorOfPoint(roiPoints); using var vvop = new VectorOfVectorOfPoint(vop); CvInvoke.DrawContours(roiMask, vvop, 0, new MCvScalar(255), -1); } else { // 无ROI时使用全图 roiMask = new Image(w, h); roiMask.SetValue(new Gray(255)); } int roiArea = CvInvoke.CountNonZero(roiMask); _logger.Debug("VoidMeasurement: ROI area={Area}, Thresh=[{Min},{Max}], MergeR={MR}", roiArea, minThresh, maxThresh, mergeRadius); // ── 高斯模糊降噪 ── var blurred = new Image(w, h); CvInvoke.GaussianBlur(inputImage, blurred, new Size(blurSize, blurSize), 0); // ── 双阈值分割提取气泡(亮区域) ── var voidImg = new Image(w, h); byte[,,] srcData = blurred.Data; byte[,,] dstData = voidImg.Data; byte[,,] maskData = roiMask.Data; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { if (maskData[y, x, 0] > 0) { byte val = srcData[y, x, 0]; dstData[y, x, 0] = (val >= minThresh && val <= maxThresh) ? (byte)255 : (byte)0; } } } // ── 形态学膨胀合并相邻气泡 ── if (mergeRadius > 0) { int kernelSize = mergeRadius * 2 + 1; using var kernel = CvInvoke.GetStructuringElement(ElementShape.Ellipse, new Size(kernelSize, kernelSize), new Point(-1, -1)); CvInvoke.Dilate(voidImg, voidImg, kernel, new Point(-1, -1), 1, BorderType.Default, new MCvScalar(0)); // 与ROI掩码取交集,防止膨胀超出ROI CvInvoke.BitwiseAnd(voidImg, roiMask, voidImg); } // ── 轮廓检测 ── using var contours = new VectorOfVectorOfPoint(); using var hierarchy = new Mat(); CvInvoke.FindContours(voidImg, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple); var voids = new List(); int totalVoidArea = 0; for (int i = 0; i < contours.Size; i++) { double area = CvInvoke.ContourArea(contours[i]); if (area < minVoidArea) continue; var moments = CvInvoke.Moments(contours[i]); if (moments.M00 < 1) continue; int intArea = (int)Math.Round(area); totalVoidArea += intArea; voids.Add(new VoidRegionInfo { Index = voids.Count + 1, CenterX = moments.M10 / moments.M00, CenterY = moments.M01 / moments.M00, Area = intArea, AreaPercent = roiArea > 0 ? area / roiArea * 100.0 : 0, BoundingBox = CvInvoke.BoundingRectangle(contours[i]), ContourPoints = contours[i].ToArray() }); } // 按面积从大到小排序 voids.Sort((a, b) => b.Area.CompareTo(a.Area)); for (int i = 0; i < voids.Count; i++) voids[i].Index = i + 1; double voidRate = roiArea > 0 ? (double)totalVoidArea / roiArea * 100.0 : 0; string classification = voidRate <= voidLimit ? "PASS" : "FAIL"; int maxVoidArea = voids.Count > 0 ? voids[0].Area : 0; _logger.Information("VoidMeasurement: VoidRate={Rate:F1}%, Voids={Count}, MaxArea={Max}, {Class}", voidRate, voids.Count, maxVoidArea, classification); // ── 输出数据 ── OutputData["VoidMeasurementResult"] = true; OutputData["RoiArea"] = roiArea; OutputData["RoiPoints"] = roiPoints; OutputData["TotalVoidArea"] = totalVoidArea; OutputData["VoidRate"] = voidRate; OutputData["VoidLimit"] = voidLimit; OutputData["VoidCount"] = voids.Count; OutputData["MaxVoidArea"] = maxVoidArea; OutputData["Classification"] = classification; OutputData["Voids"] = voids; OutputData["ResultText"] = $"Void: {voidRate:F1}% | {classification} | {voids.Count} voids | ROI: {roiArea}px"; blurred.Dispose(); voidImg.Dispose(); roiMask.Dispose(); return inputImage.Clone(); } } /// /// 单个空隙区域信息 /// public class VoidRegionInfo { public int Index { get; set; } public double CenterX { get; set; } public double CenterY { get; set; } public int Area { get; set; } public double AreaPercent { get; set; } public Rectangle BoundingBox { get; set; } public Point[] ContourPoints { get; set; } = Array.Empty(); }