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}"; } }