白底/黑底检测:轮廓与最远弦度量,UI 分色与标注优化
- 算子:输出轮廓顶点及顶点间最远弦(微米标定与原先一致) - 视图:实线轮廓;白底红/黑底绿;尺寸文字置于 ROI 外右侧垂直居中 - 事件与 MainViewModel 载荷改为 BackgroundDefectDetectionItem Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,10 +2,11 @@
|
|||||||
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
||||||
// 文件名: BackgroundDefectAnalyzer.cs
|
// 文件名: BackgroundDefectAnalyzer.cs
|
||||||
// 描述: 白底/黑底对比下的缺陷斑点分析(仅 ROI 内计算,不接入流水线算子)
|
// 描述: 白底/黑底对比下的缺陷斑点分析(仅 ROI 内计算,不接入流水线算子)
|
||||||
// 算法: Otsu 二值化 → 形态学开运算 → 外轮廓 → 面积过滤 → 外接矩形等效圆
|
// 算法: Otsu 二值化 → 形态学开运算 → 外轮廓 → 面积过滤 → 轮廓顶点最远弦(物理长度与历史等效直径同一标定:mm/px → μm)
|
||||||
// 作者: 李伟 wei.lw.li@hexagon.com
|
// 作者: 李伟 wei.lw.li@hexagon.com
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using Emgu.CV;
|
using Emgu.CV;
|
||||||
using Emgu.CV.CvEnum;
|
using Emgu.CV.CvEnum;
|
||||||
@@ -27,9 +28,15 @@ public enum BackgroundDefectMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 单个斑点,坐标相对于输入 ROI 左上角;<see cref="SizeMicrometers"/> 与主界面标注逻辑一致(等效直径,微米)。
|
/// 单个斑点:轮廓顶点相对于 ROI 左上角;<see cref="MaxChordMicrometers"/> 为轮廓顶点间欧氏距离最大值(微米)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly record struct BackgroundDefectBlob(Point CenterInRoi, int RadiusPixels, double SizeMicrometers);
|
public sealed class BackgroundDefectBlob
|
||||||
|
{
|
||||||
|
public Point[] ContourInRoi { get; init; } = Array.Empty<Point>();
|
||||||
|
public double MaxChordMicrometers { get; init; }
|
||||||
|
public Point MaxChordEndAInRoi { get; init; }
|
||||||
|
public Point MaxChordEndBInRoi { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 在灰度 ROI 上执行底色缺陷斑点检测。调用方负责构造与释放 <paramref name="roiGray"/>。
|
/// 在灰度 ROI 上执行底色缺陷斑点检测。调用方负责构造与释放 <paramref name="roiGray"/>。
|
||||||
@@ -42,7 +49,7 @@ public static class BackgroundDefectAnalyzer
|
|||||||
/// <param name="roiGray">ROI 灰度图(单通道 8 位)。</param>
|
/// <param name="roiGray">ROI 灰度图(单通道 8 位)。</param>
|
||||||
/// <param name="mode">白底或黑底模式。</param>
|
/// <param name="mode">白底或黑底模式。</param>
|
||||||
/// <param name="minAreaPixels">轮廓最小面积(像素²),小于此值的轮廓丢弃。</param>
|
/// <param name="minAreaPixels">轮廓最小面积(像素²),小于此值的轮廓丢弃。</param>
|
||||||
/// <param name="mmPerPixel">像素物理尺寸(毫米/像素),用于等效直径换算。</param>
|
/// <param name="mmPerPixel">像素物理尺寸(毫米/像素),用于轮廓最远弦换算为微米。</param>
|
||||||
/// <param name="morphKernelSize">形态学开运算核尺寸(奇数,默认 3)。</param>
|
/// <param name="morphKernelSize">形态学开运算核尺寸(奇数,默认 3)。</param>
|
||||||
public static List<BackgroundDefectBlob> DetectBlobs(
|
public static List<BackgroundDefectBlob> DetectBlobs(
|
||||||
Image<Gray, byte> roiGray,
|
Image<Gray, byte> roiGray,
|
||||||
@@ -84,19 +91,56 @@ public static class BackgroundDefectAnalyzer
|
|||||||
double area = CvInvoke.ContourArea(contours[i]);
|
double area = CvInvoke.ContourArea(contours[i]);
|
||||||
if (area < minAreaPixels) continue;
|
if (area < minAreaPixels) continue;
|
||||||
|
|
||||||
var boundRect = CvInvoke.BoundingRectangle(contours[i]);
|
int n = contours[i].Size;
|
||||||
double radiusF = Math.Max(boundRect.Width, boundRect.Height) / 2.0;
|
if (n < 2) continue;
|
||||||
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);
|
var pts = new Point[n];
|
||||||
int radiusPx = (int)radiusF;
|
for (int j = 0; j < n; j++)
|
||||||
double sizeMicrometers = radiusF * 2.0 * mmPerPixel * 1000.0;
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>轮廓顶点集合上的最远点对(欧氏距离,像素)。</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
using Prism.Events;
|
using Prism.Events;
|
||||||
|
|
||||||
namespace XplorePlane.Events
|
namespace XplorePlane.Events
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 白底/黑底检测单条结果:全局图像坐标下的轮廓与最远弦(微米与既有展示规则一致)。
|
||||||
|
/// </summary>
|
||||||
|
public class BackgroundDefectDetectionItem
|
||||||
|
{
|
||||||
|
public List<Point> Contour { get; set; } = new();
|
||||||
|
/// <summary>轮廓顶点间最远距离(微米)。</summary>
|
||||||
|
public double SizeMicrometers { get; set; }
|
||||||
|
public Point ChordP1 { get; set; }
|
||||||
|
public Point ChordP2 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 测量工具模式
|
/// 测量工具模式
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -49,7 +63,7 @@ namespace XplorePlane.Events
|
|||||||
public class WhiteBackgroundResultPayload
|
public class WhiteBackgroundResultPayload
|
||||||
{
|
{
|
||||||
public System.Drawing.Rectangle RoiRect { get; set; }
|
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<BackgroundDefectDetectionItem> Detections { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -70,6 +84,6 @@ namespace XplorePlane.Events
|
|||||||
public class BlackBackgroundResultPayload
|
public class BlackBackgroundResultPayload
|
||||||
{
|
{
|
||||||
public System.Drawing.Rectangle RoiRect { get; set; }
|
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<BackgroundDefectDetectionItem> Detections { get; set; } = new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1011,11 +1011,18 @@ namespace XplorePlane.ViewModels
|
|||||||
const double mmPerPixel = 0.139;
|
const double mmPerPixel = 0.139;
|
||||||
var blobs = BackgroundDefectAnalyzer.DetectBlobs(roiImage, mode, minArea, mmPerPixel);
|
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<BackgroundDefectDetectionItem>(blobs.Count);
|
||||||
foreach (var b in blobs)
|
foreach (var b in blobs)
|
||||||
{
|
{
|
||||||
var globalCenter = new System.Drawing.Point(b.CenterInRoi.X + rx, b.CenterInRoi.Y + ry);
|
var item = new BackgroundDefectDetectionItem
|
||||||
detections.Add((globalCenter, b.RadiusPixels, b.SizeMicrometers));
|
{
|
||||||
|
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);
|
var roiRect = new System.Drawing.Rectangle(rx, ry, rw, rh);
|
||||||
|
|||||||
@@ -155,16 +155,16 @@ namespace XplorePlane.Views
|
|||||||
RegisterBackgroundDefectRoiMouseHandlers();
|
RegisterBackgroundDefectRoiMouseHandlers();
|
||||||
}, Prism.Events.ThreadOption.UIThread);
|
}, Prism.Events.ThreadOption.UIThread);
|
||||||
|
|
||||||
// 白底检测:渲染结果
|
// 白底检测:渲染结果(红色标识)
|
||||||
ea2?.GetEvent<WhiteBackgroundResultEvent>().Subscribe(payload =>
|
ea2?.GetEvent<WhiteBackgroundResultEvent>().Subscribe(payload =>
|
||||||
{
|
{
|
||||||
RenderBackgroundDefectResult(payload.RoiRect, payload.Detections);
|
RenderBackgroundDefectResult(payload.RoiRect, payload.Detections, isBlackBackground: false);
|
||||||
}, Prism.Events.ThreadOption.UIThread);
|
}, Prism.Events.ThreadOption.UIThread);
|
||||||
|
|
||||||
// 黑底检测:渲染结果
|
// 黑底检测:渲染结果(绿色标识)
|
||||||
ea2?.GetEvent<BlackBackgroundResultEvent>().Subscribe(payload =>
|
ea2?.GetEvent<BlackBackgroundResultEvent>().Subscribe(payload =>
|
||||||
{
|
{
|
||||||
RenderBackgroundDefectResult(payload.RoiRect, payload.Detections);
|
RenderBackgroundDefectResult(payload.RoiRect, payload.Detections, isBlackBackground: true);
|
||||||
}, Prism.Events.ThreadOption.UIThread);
|
}, Prism.Events.ThreadOption.UIThread);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
@@ -542,12 +542,13 @@ namespace XplorePlane.Views
|
|||||||
|
|
||||||
private void RenderBackgroundDefectResult(
|
private void RenderBackgroundDefectResult(
|
||||||
System.Drawing.Rectangle roiRect,
|
System.Drawing.Rectangle roiRect,
|
||||||
System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)> detections)
|
System.Collections.Generic.IReadOnlyList<BackgroundDefectDetectionItem> detections,
|
||||||
|
bool isBlackBackground)
|
||||||
{
|
{
|
||||||
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
||||||
if (canvas == null || detections == null) return;
|
if (canvas == null || detections == null) return;
|
||||||
|
|
||||||
// 绘制ROI矩形(蓝色实线)
|
// 绘制ROI矩形(蓝色实线,两种模式一致)
|
||||||
var roiShape = new System.Windows.Shapes.Rectangle
|
var roiShape = new System.Windows.Shapes.Rectangle
|
||||||
{
|
{
|
||||||
Stroke = System.Windows.Media.Brushes.Blue,
|
Stroke = System.Windows.Media.Brushes.Blue,
|
||||||
@@ -561,52 +562,74 @@ namespace XplorePlane.Views
|
|||||||
canvas.Children.Add(roiShape);
|
canvas.Children.Add(roiShape);
|
||||||
_bgDefectOverlays.Add(roiShape);
|
_bgDefectOverlays.Add(roiShape);
|
||||||
|
|
||||||
// 绘制每个检测区域(白底为暗区、黑底为亮区,可视化相同)
|
var defectBrush = isBlackBackground
|
||||||
foreach (var (center, radius, sizeMm) in detections)
|
? 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)
|
||||||
{
|
{
|
||||||
// 红色虚线圆
|
if (d.Contour == null || d.Contour.Count < 2) continue;
|
||||||
var circle = new System.Windows.Shapes.Ellipse
|
|
||||||
|
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,
|
StrokeThickness = 1,
|
||||||
StrokeDashArray = new System.Windows.Media.DoubleCollection { 4, 2 },
|
Fill = System.Windows.Media.Brushes.Transparent,
|
||||||
Width = radius * 2,
|
|
||||||
Height = radius * 2,
|
|
||||||
IsHitTestVisible = false
|
IsHitTestVisible = false
|
||||||
};
|
};
|
||||||
System.Windows.Controls.Canvas.SetLeft(circle, center.X - radius);
|
canvas.Children.Add(contourPath);
|
||||||
System.Windows.Controls.Canvas.SetTop(circle, center.Y - radius);
|
_bgDefectOverlays.Add(contourPath);
|
||||||
canvas.Children.Add(circle);
|
|
||||||
_bgDefectOverlays.Add(circle);
|
|
||||||
|
|
||||||
// 45°直径标注线(从圆心向左上到右下)
|
var chordLine = new System.Windows.Shapes.Line
|
||||||
double offset = radius * 0.707; // cos(45°) * radius
|
|
||||||
var diamLine = new System.Windows.Shapes.Line
|
|
||||||
{
|
{
|
||||||
X1 = center.X - offset,
|
X1 = d.ChordP1.X,
|
||||||
Y1 = center.Y - offset,
|
Y1 = d.ChordP1.Y,
|
||||||
X2 = center.X + offset,
|
X2 = d.ChordP2.X,
|
||||||
Y2 = center.Y + offset,
|
Y2 = d.ChordP2.Y,
|
||||||
Stroke = System.Windows.Media.Brushes.Red,
|
Stroke = defectBrush,
|
||||||
StrokeThickness = 1,
|
StrokeThickness = 1.5,
|
||||||
IsHitTestVisible = false
|
IsHitTestVisible = false
|
||||||
};
|
};
|
||||||
canvas.Children.Add(diamLine);
|
canvas.Children.Add(chordLine);
|
||||||
_bgDefectOverlays.Add(diamLine);
|
_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
|
var text = new System.Windows.Controls.TextBlock
|
||||||
{
|
{
|
||||||
Text = label,
|
Text = label,
|
||||||
Foreground = System.Windows.Media.Brushes.Red,
|
Foreground = defectBrush,
|
||||||
FontSize = 11,
|
FontSize = 11,
|
||||||
IsHitTestVisible = false
|
IsHitTestVisible = false
|
||||||
};
|
};
|
||||||
System.Windows.Controls.Canvas.SetLeft(text, center.X + offset + 3);
|
System.Windows.Controls.Canvas.SetLeft(text, labelLeft);
|
||||||
System.Windows.Controls.Canvas.SetTop(text, center.Y - offset - 14);
|
System.Windows.Controls.Canvas.SetTop(text, labelStartY + labelRow * labelLineHeight);
|
||||||
canvas.Children.Add(text);
|
canvas.Children.Add(text);
|
||||||
_bgDefectOverlays.Add(text);
|
_bgDefectOverlays.Add(text);
|
||||||
|
labelRow++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user