From 82465e6510640745f2d5bb8b82da6131a8363a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Fri, 15 May 2026 09:08:44 +0800 Subject: [PATCH] =?UTF-8?q?=E7=99=BD=E5=BA=95/=E9=BB=91=E5=BA=95=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=EF=BC=9A=E8=BD=AE=E5=BB=93=E4=B8=8E=E6=9C=80=E8=BF=9C?= =?UTF-8?q?=E5=BC=A6=E5=BA=A6=E9=87=8F=EF=BC=8CUI=20=E5=88=86=E8=89=B2?= =?UTF-8?q?=E4=B8=8E=E6=A0=87=E6=B3=A8=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 算子:输出轮廓顶点及顶点间最远弦(微米标定与原先一致) - 视图:实线轮廓;白底红/黑底绿;尺寸文字置于 ROI 外右侧垂直居中 - 事件与 MainViewModel 载荷改为 BackgroundDefectDetectionItem Co-authored-by: Cursor --- .../检测分析/BackgroundDefectAnalyzer.cs | 70 +++++++++++--- XplorePlane/Events/MeasurementToolEvent.cs | 18 +++- XplorePlane/ViewModels/Main/MainViewModel.cs | 13 ++- .../Views/Main/ViewportPanelView.xaml.cs | 91 ++++++++++++------- 4 files changed, 140 insertions(+), 52 deletions(-) diff --git a/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs b/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs index 8b9d79f..36a1776 100644 --- a/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs +++ b/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs @@ -2,10 +2,11 @@ // Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. // 文件名: BackgroundDefectAnalyzer.cs // 描述: 白底/黑底对比下的缺陷斑点分析(仅 ROI 内计算,不接入流水线算子) -// 算法: Otsu 二值化 → 形态学开运算 → 外轮廓 → 面积过滤 → 外接矩形等效圆 +// 算法: Otsu 二值化 → 形态学开运算 → 外轮廓 → 面积过滤 → 轮廓顶点最远弦(物理长度与历史等效直径同一标定:mm/px → μm) // 作者: 李伟 wei.lw.li@hexagon.com // ============================================================================ +using System.Collections.Generic; using System.Drawing; using Emgu.CV; using Emgu.CV.CvEnum; @@ -27,9 +28,15 @@ public enum BackgroundDefectMode } /// -/// 单个斑点,坐标相对于输入 ROI 左上角; 与主界面标注逻辑一致(等效直径,微米)。 +/// 单个斑点:轮廓顶点相对于 ROI 左上角; 为轮廓顶点间欧氏距离最大值(微米)。 /// -public readonly record struct BackgroundDefectBlob(Point CenterInRoi, int RadiusPixels, double SizeMicrometers); +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 上执行底色缺陷斑点检测。调用方负责构造与释放 。 @@ -42,7 +49,7 @@ public static class BackgroundDefectAnalyzer /// ROI 灰度图(单通道 8 位)。 /// 白底或黑底模式。 /// 轮廓最小面积(像素²),小于此值的轮廓丢弃。 - /// 像素物理尺寸(毫米/像素),用于等效直径换算。 + /// 像素物理尺寸(毫米/像素),用于轮廓最远弦换算为微米。 /// 形态学开运算核尺寸(奇数,默认 3)。 public static List DetectBlobs( Image roiGray, @@ -84,19 +91,56 @@ public static class BackgroundDefectAnalyzer 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); + int n = contours[i].Size; + if (n < 2) continue; - var centerInRoi = new Point((int)centerF.X, (int)centerF.Y); - int radiusPx = (int)radiusF; - double sizeMicrometers = radiusF * 2.0 * mmPerPixel * 1000.0; + var pts = new Point[n]; + for (int j = 0; j < n; j++) + pts[j] = contours[i][j]; - result.Add(new BackgroundDefectBlob(centerInRoi, radiusPx, sizeMicrometers)); + 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); + } } diff --git a/XplorePlane/Events/MeasurementToolEvent.cs b/XplorePlane/Events/MeasurementToolEvent.cs index cb561d3..526e189 100644 --- a/XplorePlane/Events/MeasurementToolEvent.cs +++ b/XplorePlane/Events/MeasurementToolEvent.cs @@ -1,7 +1,21 @@ +using System.Collections.Generic; +using System.Drawing; using Prism.Events; namespace XplorePlane.Events { + /// + /// 白底/黑底检测单条结果:全局图像坐标下的轮廓与最远弦(微米与既有展示规则一致)。 + /// + public class BackgroundDefectDetectionItem + { + public List Contour { get; set; } = new(); + /// 轮廓顶点间最远距离(微米)。 + public double SizeMicrometers { get; set; } + public Point ChordP1 { get; set; } + public Point ChordP2 { get; set; } + } + /// /// 测量工具模式 /// @@ -49,7 +63,7 @@ namespace XplorePlane.Events public class WhiteBackgroundResultPayload { public System.Drawing.Rectangle RoiRect { get; set; } - public System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)> Detections { get; set; } + public List Detections { get; set; } = new(); } /// @@ -70,6 +84,6 @@ namespace XplorePlane.Events public class BlackBackgroundResultPayload { public System.Drawing.Rectangle RoiRect { get; set; } - public System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)> Detections { get; set; } + public List Detections { get; set; } = new(); } } diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index c186f3e..965a9b6 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -1011,11 +1011,18 @@ namespace XplorePlane.ViewModels 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)>(blobs.Count); + var detections = new System.Collections.Generic.List(blobs.Count); foreach (var b in blobs) { - var globalCenter = new System.Drawing.Point(b.CenterInRoi.X + rx, b.CenterInRoi.Y + ry); - detections.Add((globalCenter, b.RadiusPixels, b.SizeMicrometers)); + var item = new BackgroundDefectDetectionItem + { + SizeMicrometers = b.MaxChordMicrometers, + ChordP1 = new System.Drawing.Point(b.MaxChordEndAInRoi.X + rx, b.MaxChordEndAInRoi.Y + ry), + ChordP2 = new System.Drawing.Point(b.MaxChordEndBInRoi.X + rx, b.MaxChordEndBInRoi.Y + ry) + }; + foreach (var p in b.ContourInRoi) + item.Contour.Add(new System.Drawing.Point(p.X + rx, p.Y + ry)); + detections.Add(item); } var roiRect = new System.Drawing.Rectangle(rx, ry, rw, rh); diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs index a8a023d..e92db09 100644 --- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs +++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs @@ -155,16 +155,16 @@ namespace XplorePlane.Views RegisterBackgroundDefectRoiMouseHandlers(); }, Prism.Events.ThreadOption.UIThread); - // 白底检测:渲染结果 + // 白底检测:渲染结果(红色标识) ea2?.GetEvent().Subscribe(payload => { - RenderBackgroundDefectResult(payload.RoiRect, payload.Detections); + RenderBackgroundDefectResult(payload.RoiRect, payload.Detections, isBlackBackground: false); }, Prism.Events.ThreadOption.UIThread); - // 黑底检测:渲染结果 + // 黑底检测:渲染结果(绿色标识) ea2?.GetEvent().Subscribe(payload => { - RenderBackgroundDefectResult(payload.RoiRect, payload.Detections); + RenderBackgroundDefectResult(payload.RoiRect, payload.Detections, isBlackBackground: true); }, Prism.Events.ThreadOption.UIThread); } catch { } @@ -542,12 +542,13 @@ namespace XplorePlane.Views private void RenderBackgroundDefectResult( System.Drawing.Rectangle roiRect, - System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)> detections) + System.Collections.Generic.IReadOnlyList detections, + bool isBlackBackground) { var canvas = FindChildByName(RoiCanvas, "mainCanvas"); if (canvas == null || detections == null) return; - // 绘制ROI矩形(蓝色实线) + // 绘制ROI矩形(蓝色实线,两种模式一致) var roiShape = new System.Windows.Shapes.Rectangle { Stroke = System.Windows.Media.Brushes.Blue, @@ -561,52 +562,74 @@ namespace XplorePlane.Views canvas.Children.Add(roiShape); _bgDefectOverlays.Add(roiShape); - // 绘制每个检测区域(白底为暗区、黑底为亮区,可视化相同) - foreach (var (center, radius, sizeMm) in detections) + var defectBrush = isBlackBackground + ? System.Windows.Media.Brushes.LimeGreen + : System.Windows.Media.Brushes.Red; + + const int labelPadRightOfRoi = 4; + const double labelLineHeight = 15; + int validCount = detections.Count(d => d.Contour != null && d.Contour.Count >= 2); + double roiMidY = roiRect.Y + roiRect.Height * 0.5; + double labelLeft = roiRect.X + roiRect.Width + labelPadRightOfRoi; + double labelStartY = roiMidY - validCount * labelLineHeight * 0.5; + int labelRow = 0; + + foreach (var d in detections) { - // 红色虚线圆 - var circle = new System.Windows.Shapes.Ellipse + if (d.Contour == null || d.Contour.Count < 2) continue; + + var fig = new PathFigure { - Stroke = System.Windows.Media.Brushes.Red, + StartPoint = new System.Windows.Point(d.Contour[0].X, d.Contour[0].Y), + IsClosed = true + }; + if (d.Contour.Count > 1) + { + fig.Segments.Add(new PolyLineSegment( + d.Contour.Skip(1).Select(p => new System.Windows.Point(p.X, p.Y)), true)); + } + + var geom = new PathGeometry(); + geom.Figures.Add(fig); + var contourPath = new System.Windows.Shapes.Path + { + Data = geom, + Stroke = defectBrush, StrokeThickness = 1, - StrokeDashArray = new System.Windows.Media.DoubleCollection { 4, 2 }, - Width = radius * 2, - Height = radius * 2, + Fill = System.Windows.Media.Brushes.Transparent, IsHitTestVisible = false }; - System.Windows.Controls.Canvas.SetLeft(circle, center.X - radius); - System.Windows.Controls.Canvas.SetTop(circle, center.Y - radius); - canvas.Children.Add(circle); - _bgDefectOverlays.Add(circle); + canvas.Children.Add(contourPath); + _bgDefectOverlays.Add(contourPath); - // 45°直径标注线(从圆心向左上到右下) - double offset = radius * 0.707; // cos(45°) * radius - var diamLine = new System.Windows.Shapes.Line + var chordLine = new System.Windows.Shapes.Line { - X1 = center.X - offset, - Y1 = center.Y - offset, - X2 = center.X + offset, - Y2 = center.Y + offset, - Stroke = System.Windows.Media.Brushes.Red, - StrokeThickness = 1, + X1 = d.ChordP1.X, + Y1 = d.ChordP1.Y, + X2 = d.ChordP2.X, + Y2 = d.ChordP2.Y, + Stroke = defectBrush, + StrokeThickness = 1.5, IsHitTestVisible = false }; - canvas.Children.Add(diamLine); - _bgDefectOverlays.Add(diamLine); + canvas.Children.Add(chordLine); + _bgDefectOverlays.Add(chordLine); + + double um = d.SizeMicrometers; + string label = um >= 1000 ? $"{um / 1000:F2} mm" : $"{um:F0} μm"; - // 尺寸标注(在斜线右上方) - string label = sizeMm >= 1000 ? $"{sizeMm / 1000:F2} mm" : $"{sizeMm:F0} μm"; var text = new System.Windows.Controls.TextBlock { Text = label, - Foreground = System.Windows.Media.Brushes.Red, + Foreground = defectBrush, FontSize = 11, IsHitTestVisible = false }; - System.Windows.Controls.Canvas.SetLeft(text, center.X + offset + 3); - System.Windows.Controls.Canvas.SetTop(text, center.Y - offset - 14); + System.Windows.Controls.Canvas.SetLeft(text, labelLeft); + System.Windows.Controls.Canvas.SetTop(text, labelStartY + labelRow * labelLineHeight); canvas.Children.Add(text); _bgDefectOverlays.Add(text); + labelRow++; } }