// ============================================================================ // Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. // 文件名: BackgroundDefectAnalyzer.cs // 描述: 白底/黑底对比下的缺陷斑点分析(仅 ROI 内计算,不接入流水线算子) // 算法: Otsu 二值化 → 形态学开运算 → 外轮廓 → 面积过滤 → 轮廓顶点最远弦(物理长度与历史等效直径同一标定:mm/px → μm) // 作者: 李伟 wei.lw.li@hexagon.com // ============================================================================ using System.Collections.Generic; using System.Drawing; using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using Emgu.CV.Util; namespace XP.ImageProcessing.Processors; /// /// 底色类型:决定 Otsu 后保留的前景是暗区还是亮区。 /// public enum BackgroundDefectMode { /// 白底图像上检测偏暗区域(BinaryInv + Otsu)。 WhiteBackground, /// 黑底图像上检测偏亮区域(Binary + Otsu)。 BlackBackground } /// /// 单个斑点:轮廓顶点相对于 ROI 左上角; 为轮廓顶点间欧氏距离最大值(微米)。 /// public sealed class BackgroundDefectBlob { public Point[] ContourInRoi { get; init; } = Array.Empty(); public double MaxChordMicrometers { get; init; } public Point MaxChordEndAInRoi { get; init; } public Point MaxChordEndBInRoi { get; init; } } /// /// 在灰度 ROI 上执行底色缺陷斑点检测。调用方负责构造与释放 。 /// public static class BackgroundDefectAnalyzer { /// /// 在 ROI 灰度图上检测斑点。 /// /// ROI 灰度图(单通道 8 位)。 /// 白底或黑底模式。 /// 轮廓最小面积(像素²),小于此值的轮廓丢弃。 /// 像素物理尺寸(毫米/像素),用于轮廓最远弦换算为微米。 /// 形态学开运算核尺寸(奇数,默认 3)。 public static List DetectBlobs( Image roiGray, BackgroundDefectMode mode, int minAreaPixels = 50, double mmPerPixel = 0.139, int morphKernelSize = 3) { if (roiGray == null) throw new ArgumentNullException(nameof(roiGray)); if (minAreaPixels < 1) minAreaPixels = 1; if (mmPerPixel <= 0) mmPerPixel = 0.139; if (morphKernelSize < 1) morphKernelSize = 1; if ((morphKernelSize & 1) == 0) morphKernelSize++; int rw = roiGray.Width; int rh = roiGray.Height; if (rw < 1 || rh < 1) return new List(); var thresholdType = mode == BackgroundDefectMode.WhiteBackground ? ThresholdType.BinaryInv | ThresholdType.Otsu : ThresholdType.Binary | ThresholdType.Otsu; using var binary = new Image(rw, rh); CvInvoke.Threshold(roiGray, binary, 0, 255, thresholdType); using var kernel = CvInvoke.GetStructuringElement( ElementShape.Ellipse, new Size(morphKernelSize, morphKernelSize), new Point(-1, -1)); CvInvoke.MorphologyEx(binary, binary, MorphOp.Open, kernel, new Point(-1, -1), 1, BorderType.Default, new MCvScalar(0)); using var contours = new VectorOfVectorOfPoint(); using var hierarchy = new Mat(); CvInvoke.FindContours(binary, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple); var result = new List(); for (int i = 0; i < contours.Size; i++) { double area = CvInvoke.ContourArea(contours[i]); if (area < minAreaPixels) continue; int n = contours[i].Size; if (n < 2) continue; var pts = new Point[n]; for (int j = 0; j < n; j++) pts[j] = contours[i][j]; MaxChordInPixelSpace(pts, out double maxChordPx, out Point pa, out Point pb); double maxChordMicrometers = maxChordPx * mmPerPixel * 1000.0; result.Add(new BackgroundDefectBlob { ContourInRoi = pts, MaxChordMicrometers = maxChordMicrometers, MaxChordEndAInRoi = pa, MaxChordEndBInRoi = pb }); } return result; } /// 轮廓顶点集合上的最远点对(欧氏距离,像素)。 private static void MaxChordInPixelSpace(Point[] pts, out double maxChordPx, out Point a, out Point b) { maxChordPx = 0; a = pts[0]; b = pts.Length > 1 ? pts[1] : pts[0]; long bestSq = 0; int bestI = 0, bestJ = 1; int n = pts.Length; for (int i = 0; i < n; i++) { int iX = pts[i].X, iY = pts[i].Y; for (int j = i + 1; j < n; j++) { long dx = iX - pts[j].X; long dy = iY - pts[j].Y; long sq = dx * dx + dy * dy; if (sq > bestSq) { bestSq = sq; bestI = i; bestJ = j; } } } a = pts[bestI]; b = pts[bestJ]; maxChordPx = Math.Sqrt(bestSq); } }