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