diff --git a/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs b/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs
new file mode 100644
index 0000000..8b9d79f
--- /dev/null
+++ b/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs
@@ -0,0 +1,102 @@
+// ============================================================================
+// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
+// 文件名: BackgroundDefectAnalyzer.cs
+// 描述: 白底/黑底对比下的缺陷斑点分析(仅 ROI 内计算,不接入流水线算子)
+// 算法: Otsu 二值化 → 形态学开运算 → 外轮廓 → 面积过滤 → 外接矩形等效圆
+// 作者: 李伟 wei.lw.li@hexagon.com
+// ============================================================================
+
+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 readonly record struct BackgroundDefectBlob(Point CenterInRoi, int RadiusPixels, double SizeMicrometers);
+
+///
+/// 在灰度 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;
+
+ var boundRect = CvInvoke.BoundingRectangle(contours[i]);
+ double radiusF = Math.Max(boundRect.Width, boundRect.Height) / 2.0;
+ var centerF = new PointF(
+ boundRect.X + boundRect.Width / 2.0f,
+ boundRect.Y + boundRect.Height / 2.0f);
+
+ var centerInRoi = new Point((int)centerF.X, (int)centerF.Y);
+ int radiusPx = (int)radiusF;
+ double sizeMicrometers = radiusF * 2.0 * mmPerPixel * 1000.0;
+
+ result.Add(new BackgroundDefectBlob(centerInRoi, radiusPx, sizeMicrometers));
+ }
+
+ return result;
+ }
+}
diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs
index da2e756..c186f3e 100644
--- a/XplorePlane/ViewModels/Main/MainViewModel.cs
+++ b/XplorePlane/ViewModels/Main/MainViewModel.cs
@@ -23,6 +23,7 @@ using XP.Hardware.MotionControl.Abstractions;
using XP.Hardware.MotionControl.Services;
using XplorePlane.Events;
using XplorePlane.Services.MainViewport;
+using XP.ImageProcessing.Processors;
using XplorePlane.Services.Storage;
using XplorePlane.ViewModels.Cnc;
using XplorePlane.Views;
@@ -958,102 +959,8 @@ namespace XplorePlane.ViewModels
StatusMessage = "白底检测:请在图像上拖拽绘制矩形ROI";
}
- private void OnWhiteBackgroundRoiDrawn(System.Windows.Int32Rect roi)
- {
- try
- {
- var viewportVm = _containerProvider.Resolve();
- var imageSource = viewportVm?.ImageSource as System.Windows.Media.Imaging.BitmapSource;
- if (imageSource == null) return;
-
- // 转为 Gray8
- System.Windows.Media.Imaging.BitmapSource gray8;
- if (imageSource.Format != System.Windows.Media.PixelFormats.Gray8)
- gray8 = new System.Windows.Media.Imaging.FormatConvertedBitmap(
- imageSource, System.Windows.Media.PixelFormats.Gray8, null, 0);
- else
- gray8 = imageSource;
-
- int imgW = gray8.PixelWidth;
- int imgH = gray8.PixelHeight;
-
- // 限制ROI在图像范围内
- int rx = Math.Clamp(roi.X, 0, imgW - 1);
- int ry = Math.Clamp(roi.Y, 0, imgH - 1);
- int rw = Math.Clamp(roi.Width, 1, imgW - rx);
- int rh = Math.Clamp(roi.Height, 1, imgH - ry);
-
- // 提取ROI区域像素
- byte[] roiPixels = new byte[rw * rh];
- gray8.CopyPixels(new System.Windows.Int32Rect(rx, ry, rw, rh), roiPixels, rw, 0);
-
- // 使用EmguCV处理
- using var roiImage = new Emgu.CV.Image(rw, rh);
- for (int y = 0; y < rh; y++)
- for (int x = 0; x < rw; x++)
- roiImage.Data[y, x, 0] = roiPixels[y * rw + x];
-
- // Otsu阈值分割(白底检测黑色区域:反转后黑色区域变白)
- using var binary = new Emgu.CV.Image(rw, rh);
- Emgu.CV.CvInvoke.Threshold(roiImage, binary, 0, 255,
- Emgu.CV.CvEnum.ThresholdType.BinaryInv | Emgu.CV.CvEnum.ThresholdType.Otsu);
-
- // 形态学开运算去噪
- using var kernel = Emgu.CV.CvInvoke.GetStructuringElement(
- Emgu.CV.CvEnum.ElementShape.Ellipse, new System.Drawing.Size(3, 3), new System.Drawing.Point(-1, -1));
- Emgu.CV.CvInvoke.MorphologyEx(binary, binary,
- Emgu.CV.CvEnum.MorphOp.Open, kernel, new System.Drawing.Point(-1, -1), 1,
- Emgu.CV.CvEnum.BorderType.Default, new Emgu.CV.Structure.MCvScalar(0));
-
- // 查找轮廓
- using var contours = new Emgu.CV.Util.VectorOfVectorOfPoint();
- using var hierarchy = new Emgu.CV.Mat();
- Emgu.CV.CvInvoke.FindContours(binary, contours, hierarchy,
- Emgu.CV.CvEnum.RetrType.External, Emgu.CV.CvEnum.ChainApproxMethod.ChainApproxSimple);
-
- // 过滤小区域(最小面积默认50像素²)
- const int minArea = 50;
- double pixelSize = 0.139; // mm/pixel,默认值(可从比例尺获取)
-
- var detections = new System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)>();
-
- for (int i = 0; i < contours.Size; i++)
- {
- double area = Emgu.CV.CvInvoke.ContourArea(contours[i]);
- if (area < minArea) continue;
-
- // 最小外接圆 - 使用外接矩形计算
- var boundRect = Emgu.CV.CvInvoke.BoundingRectangle(contours[i]);
- double radiusF = Math.Max(boundRect.Width, boundRect.Height) / 2.0;
- var centerF = new System.Drawing.PointF(
- boundRect.X + boundRect.Width / 2.0f,
- boundRect.Y + boundRect.Height / 2.0f);
-
- // 转换到全局坐标
- var globalCenter = new System.Drawing.Point((int)centerF.X + rx, (int)centerF.Y + ry);
- double diameterMm = radiusF * 2 * pixelSize * 1000; // 转μm
-
- detections.Add((globalCenter, (int)radiusF, diameterMm));
- }
-
- // 发布结果用于绘制(通过OutputData模式传递给ViewportPanelView)
- _eventAggregator.GetEvent().Publish(
- new WhiteBackgroundResultPayload
- {
- RoiRect = new System.Drawing.Rectangle(rx, ry, rw, rh),
- Detections = detections
- });
-
- StatusMessage = $"白底检测完成:检测到 {detections.Count} 个黑色区域";
- _logger.Info("White background detection: found {Count} dark regions in ROI ({X},{Y},{W},{H})",
- detections.Count, rx, ry, rw, rh);
- }
- catch (Exception ex)
- {
- _logger.Error(ex, "White background detection failed");
- StatusMessage = $"白底检测失败: {ex.Message}";
- }
- }
+ private void OnWhiteBackgroundRoiDrawn(System.Windows.Int32Rect roi) =>
+ RunBackgroundRoiDetection(roi, BackgroundDefectMode.WhiteBackground);
private void ExecuteBlackBackgroundDetection()
{
@@ -1063,7 +970,13 @@ namespace XplorePlane.ViewModels
StatusMessage = "黑底检测:请在图像上拖拽绘制矩形ROI";
}
- private void OnBlackBackgroundRoiDrawn(System.Windows.Int32Rect roi)
+ private void OnBlackBackgroundRoiDrawn(System.Windows.Int32Rect roi) =>
+ RunBackgroundRoiDetection(roi, BackgroundDefectMode.BlackBackground);
+
+ ///
+ /// 从视口灰度图取 ROI,调用 ,再发布结果事件(全局坐标)。
+ ///
+ private void RunBackgroundRoiDetection(System.Windows.Int32Rect roi, BackgroundDefectMode mode)
{
try
{
@@ -1089,64 +1002,45 @@ namespace XplorePlane.ViewModels
byte[] roiPixels = new byte[rw * rh];
gray8.CopyPixels(new System.Windows.Int32Rect(rx, ry, rw, rh), roiPixels, rw, 0);
- using var roiImage = new Emgu.CV.Image(rw, rh);
+ using var roiImage = new Image(rw, rh);
for (int y = 0; y < rh; y++)
for (int x = 0; x < rw; x++)
roiImage.Data[y, x, 0] = roiPixels[y * rw + x];
- // Otsu 二值化(黑底检测亮色区域:高于阈值为前景)
- using var binary = new Emgu.CV.Image(rw, rh);
- Emgu.CV.CvInvoke.Threshold(roiImage, binary, 0, 255,
- Emgu.CV.CvEnum.ThresholdType.Binary | Emgu.CV.CvEnum.ThresholdType.Otsu);
-
- using var kernel = Emgu.CV.CvInvoke.GetStructuringElement(
- Emgu.CV.CvEnum.ElementShape.Ellipse, new System.Drawing.Size(3, 3), new System.Drawing.Point(-1, -1));
- Emgu.CV.CvInvoke.MorphologyEx(binary, binary,
- Emgu.CV.CvEnum.MorphOp.Open, kernel, new System.Drawing.Point(-1, -1), 1,
- Emgu.CV.CvEnum.BorderType.Default, new Emgu.CV.Structure.MCvScalar(0));
-
- using var contours = new Emgu.CV.Util.VectorOfVectorOfPoint();
- using var hierarchy = new Emgu.CV.Mat();
- Emgu.CV.CvInvoke.FindContours(binary, contours, hierarchy,
- Emgu.CV.CvEnum.RetrType.External, Emgu.CV.CvEnum.ChainApproxMethod.ChainApproxSimple);
-
const int minArea = 50;
- double pixelSize = 0.139;
+ const double mmPerPixel = 0.139;
+ var blobs = BackgroundDefectAnalyzer.DetectBlobs(roiImage, mode, minArea, mmPerPixel);
- var detections = new System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)>();
-
- for (int i = 0; i < contours.Size; i++)
+ var detections = new System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)>(blobs.Count);
+ foreach (var b in blobs)
{
- double area = Emgu.CV.CvInvoke.ContourArea(contours[i]);
- if (area < minArea) continue;
-
- var boundRect = Emgu.CV.CvInvoke.BoundingRectangle(contours[i]);
- double radiusF = Math.Max(boundRect.Width, boundRect.Height) / 2.0;
- var centerF = new System.Drawing.PointF(
- boundRect.X + boundRect.Width / 2.0f,
- boundRect.Y + boundRect.Height / 2.0f);
-
- var globalCenter = new System.Drawing.Point((int)centerF.X + rx, (int)centerF.Y + ry);
- double diameterMm = radiusF * 2 * pixelSize * 1000;
-
- detections.Add((globalCenter, (int)radiusF, diameterMm));
+ var globalCenter = new System.Drawing.Point(b.CenterInRoi.X + rx, b.CenterInRoi.Y + ry);
+ detections.Add((globalCenter, b.RadiusPixels, b.SizeMicrometers));
}
- _eventAggregator.GetEvent().Publish(
- new BlackBackgroundResultPayload
- {
- RoiRect = new System.Drawing.Rectangle(rx, ry, rw, rh),
- Detections = detections
- });
-
- StatusMessage = $"黑底检测完成:检测到 {detections.Count} 个亮色区域";
- _logger.Info("Black background detection: found {Count} bright regions in ROI ({X},{Y},{W},{H})",
- detections.Count, rx, ry, rw, rh);
+ var roiRect = new System.Drawing.Rectangle(rx, ry, rw, rh);
+ if (mode == BackgroundDefectMode.WhiteBackground)
+ {
+ _eventAggregator.GetEvent().Publish(
+ new WhiteBackgroundResultPayload { RoiRect = roiRect, Detections = detections });
+ StatusMessage = $"白底检测完成:检测到 {detections.Count} 个黑色区域";
+ _logger.Info("White background detection: found {Count} dark regions in ROI ({X},{Y},{W},{H})",
+ detections.Count, rx, ry, rw, rh);
+ }
+ else
+ {
+ _eventAggregator.GetEvent().Publish(
+ new BlackBackgroundResultPayload { RoiRect = roiRect, Detections = detections });
+ StatusMessage = $"黑底检测完成:检测到 {detections.Count} 个亮色区域";
+ _logger.Info("Black background detection: found {Count} bright regions in ROI ({X},{Y},{W},{H})",
+ detections.Count, rx, ry, rw, rh);
+ }
}
catch (Exception ex)
{
- _logger.Error(ex, "Black background detection failed");
- StatusMessage = $"黑底检测失败: {ex.Message}";
+ string label = mode == BackgroundDefectMode.WhiteBackground ? "白底" : "黑底";
+ _logger.Error(ex, "{Label} background detection failed", label);
+ StatusMessage = $"{label}检测失败: {ex.Message}";
}
}