// ============================================================================
// 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);
}
}