diff --git a/XP.ImageProcessing.Processors/检测分析/BgaVoidRateProcessor.cs b/XP.ImageProcessing.Processors/检测分析/BgaVoidRateProcessor.cs
index d78fae9..450d0f1 100644
--- a/XP.ImageProcessing.Processors/检测分析/BgaVoidRateProcessor.cs
+++ b/XP.ImageProcessing.Processors/检测分析/BgaVoidRateProcessor.cs
@@ -333,14 +333,13 @@ public class BgaVoidRateProcessor : ImageProcessorBase
}
int voidPixels = CvInvoke.CountNonZero(voidImg);
- bga.VoidPixels = voidPixels;
- bga.VoidRate = bgaPixels > 0 ? (double)voidPixels / bgaPixels * 100.0 : 0;
// 检测每个气泡的轮廓
using var contours = new VectorOfVectorOfPoint();
using var hierarchy = new Mat();
CvInvoke.FindContours(voidImg, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple);
+ int filteredVoidArea = 0;
for (int i = 0; i < contours.Size; i++)
{
double area = CvInvoke.ContourArea(contours[i]);
@@ -349,6 +348,7 @@ public class BgaVoidRateProcessor : ImageProcessorBase
var moments = CvInvoke.Moments(contours[i]);
if (moments.M00 < 1) continue;
+ filteredVoidArea += (int)Math.Round(area);
bga.Voids.Add(new VoidInfo
{
Index = bga.Voids.Count + 1,
@@ -361,6 +361,10 @@ public class BgaVoidRateProcessor : ImageProcessorBase
});
}
+ // 空隙率基于过滤后的轮廓面积计算
+ bga.VoidPixels = filteredVoidArea;
+ bga.VoidRate = bgaPixels > 0 ? (double)filteredVoidArea / bgaPixels * 100.0 : 0;
+
// 按面积从大到小排序
bga.Voids.Sort((a, b) => b.Area.CompareTo(a.Area));
for (int i = 0; i < bga.Voids.Count; i++) bga.Voids[i].Index = i + 1;
diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml
index 0546e65..bc732a8 100644
--- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml
+++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml
@@ -41,6 +41,7 @@
MouseLeftButtonDown="Canvas_MouseLeftButtonDown"
MouseLeftButtonUp="Canvas_MouseLeftButtonUp"
MouseMove="Canvas_MouseMove"
+ MouseLeave="Canvas_MouseLeave"
MouseRightButtonDown="Canvas_MouseRightButtonDown"
PreviewMouseRightButtonUp="Canvas_PreviewMouseRightButtonUp">
diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs
index 8c1d4b3..56e7f05 100644
--- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs
+++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs
@@ -123,7 +123,6 @@ namespace XP.ImageProcessing.RoiControl.Controls
var control = (PolygonRoiCanvas)d;
if (e.NewValue is BitmapSource bitmap)
{
- // 使用像素尺寸,避免 DPI 不同导致 DIP 尺寸与实际像素不一致
control.CanvasWidth = bitmap.PixelWidth;
control.CanvasHeight = bitmap.PixelHeight;
control.ResetView();
@@ -135,6 +134,11 @@ namespace XP.ImageProcessing.RoiControl.Controls
control.ResetView();
}
+ // 图像切换时清除测量、叠加层和ROI
+ control.ClearMeasurements();
+ control.ROIItems?.Clear();
+ control.SelectedROI = null;
+
// 图像尺寸变化后刷新十字线
if (control.ShowCrosshair)
control.AddCrosshair();
@@ -295,6 +299,19 @@ namespace XP.ImageProcessing.RoiControl.Controls
set => SetValue(ShowCrosshairProperty, value);
}
+ // ── 光标信息(像素坐标 + 灰度值)──
+
+ public static readonly DependencyProperty CursorInfoProperty =
+ DependencyProperty.Register(nameof(CursorInfo), typeof(string), typeof(PolygonRoiCanvas),
+ new PropertyMetadata("X: -- Y: -- Gray: --"));
+
+ /// 鼠标在图像上的像素坐标和灰度值,格式: "X: 123 Y: 456 Gray: 128"
+ public string CursorInfo
+ {
+ get => (string)GetValue(CursorInfoProperty);
+ set => SetValue(CursorInfoProperty, value);
+ }
+
private Line _crosshairH, _crosshairV;
private static void OnShowCrosshairChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -371,7 +388,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
private Ellipse _bgaPendingDot;
// 气泡测量状态
- public enum BubbleSubTool { Roi, Wand, Brush, Eraser }
+ public enum BubbleSubTool { Roi, RoiCircle, RoiPolygon, Wand, Brush, Eraser }
private BubbleSubTool _bubbleTool = BubbleSubTool.Roi;
private Rectangle _bubbleRoiRect;
private Ellipse _bubbleRoiHandle; // 右下角调整手柄
@@ -381,6 +398,17 @@ namespace XP.ImageProcessing.RoiControl.Controls
private bool _bubbleRoiDragging;
private bool _bubbleRoiMoving; // 拖动整个 ROI
private bool _bubbleRoiResizing; // 右下角调整大小
+ // 圆形 ROI
+ private Ellipse _bubbleCircleShape;
+ private Point? _bubbleCircleCenter;
+ private double _bubbleCircleRadius;
+ private bool _bubbleCircleDragging;
+ private bool _bubbleCircleMoving; // 拖动整个圆形 ROI
+ private bool _bubbleCircleResizing; // 调整圆形 ROI 大小
+ private Point _bubbleCircleDragOffset;
+ // 多边形 ROI
+ private Polygon _bubblePolyShape;
+ private System.Collections.ObjectModel.ObservableCollection _bubblePolyPoints = new();
private Point _bubbleRoiDragOffset;
private Image _bubbleMaskImage;
private System.Windows.Media.Imaging.WriteableBitmap _bubbleMask;
@@ -390,10 +418,39 @@ namespace XP.ImageProcessing.RoiControl.Controls
private bool _bubbleBrushDragging;
private readonly System.Collections.Generic.Stack _bubbleUndoStack = new();
- public void SetBubbleTool(BubbleSubTool tool) => _bubbleTool = tool;
+ public void SetBubbleTool(BubbleSubTool tool)
+ {
+ _bubbleTool = tool;
+ // 根据工具设置光标
+ UpdateBubbleToolCursor();
+ }
+
+ private void UpdateBubbleToolCursor()
+ {
+ if (mainCanvas == null) return;
+ if (CurrentMeasureMode != Models.MeasureMode.BubbleMeasure)
+ {
+ mainCanvas.Cursor = Cursors.Arrow;
+ return;
+ }
+ mainCanvas.Cursor = _bubbleTool switch
+ {
+ BubbleSubTool.Wand => Cursors.Cross,
+ BubbleSubTool.Brush => Cursors.Pen,
+ BubbleSubTool.Eraser => Cursors.No,
+ _ => Cursors.Arrow
+ };
+ }
+
public void SetBubbleThreshold(int val) => _bubbleThreshold = val;
public void SetBubbleBrushSize(int val) => _bubbleBrushSize = val;
public void SetBubbleVoidLimit(double val) { _bubbleVoidLimit = val; UpdateBubbleResult(); }
+ public void SetBubblePolyPoints(System.Collections.Generic.IList points)
+ {
+ _bubblePolyPoints.Clear();
+ foreach (var p in points) _bubblePolyPoints.Add(p);
+ if (_bubblePolyPoints.Count >= 3) InitBubbleMask();
+ }
public Rect? BubbleRoi => _bubbleRoi;
/// 设置 BGA 测量的气泡/焊球绘制模式
@@ -475,6 +532,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
_bubbleRoi = null; _bubbleRoiStart = null;
_bubbleRoiDragging = false; _bubbleRoiMoving = false;
_bubbleRoiResizing = false; _bubbleBrushDragging = false;
+ _bubbleCircleDragging = false; _bubbleCircleMoving = false; _bubbleCircleResizing = false;
_bubbleTool = BubbleSubTool.Roi;
_bubbleUndoStack.Clear();
// 清理外部叠加的结果图层(IsHitTestVisible=false 的 Image,排除背景图)
@@ -545,13 +603,89 @@ namespace XP.ImageProcessing.RoiControl.Controls
private Models.MeasureGroup CreatePPGroup(Point p1, Point p2)
{
var g = new Models.MeasureGroup { P1 = p1, P2 = p2 };
- g.Line = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false };
+ g.Line = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false };
g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false };
+
+ // 使用垂直线段代替圆点
+ g.PerpLine1 = new Line { Stroke = Brushes.Yellow, StrokeThickness = 1, IsHitTestVisible = true, Cursor = Cursors.Hand };
+ g.PerpLine2 = new Line { Stroke = Brushes.Yellow, StrokeThickness = 1, IsHitTestVisible = true, Cursor = Cursors.Hand };
+
+ // 保留圆点以兼容拖拽逻辑(但设为不可见)
g.Dot1 = CreateMDot(Brushes.Red);
g.Dot2 = CreateMDot(Brushes.Blue);
- foreach (UIElement el in new UIElement[] { g.Line, g.Label, g.Dot1, g.Dot2 })
+ g.Dot1.Visibility = Visibility.Collapsed;
+ g.Dot2.Visibility = Visibility.Collapsed;
+
+ foreach (UIElement el in new UIElement[] { g.Line, g.Label, g.PerpLine1, g.PerpLine2, g.Dot1, g.Dot2 })
_measureOverlay.Children.Add(el);
+
+ // 设置圆点位置(用于拖拽计算)
SetDotPos(g.Dot1, p1); SetDotPos(g.Dot2, p2);
+
+ // 垂直线段1的拖拽处理 - 直接设置拖拽状态
+ g.PerpLine1.MouseLeftButtonDown += (s, e) =>
+ {
+ _mDraggingOwner = g;
+ _mDraggingRole = "Dot1";
+ _mDraggingDot = g.Dot1;
+ g.PerpLine1.CaptureMouse();
+ e.Handled = true;
+ };
+ g.PerpLine1.MouseMove += (s, e) =>
+ {
+ if (_mDraggingOwner != g || _mDraggingRole != "Dot1" || _measureOverlay == null) return;
+ if (e.LeftButton != MouseButtonState.Pressed) return;
+ var pos = e.GetPosition(_measureOverlay);
+ SetDotPos(g.Dot1, pos);
+ g.P1 = pos;
+ g.UpdateLine();
+ g.UpdateLabel(FormatDistance(g.Distance));
+ RaiseMeasureCompleted(g.P1, g.P2, g.Distance, MeasureCount, "PointDistance");
+ };
+ g.PerpLine1.MouseLeftButtonUp += (s, e) =>
+ {
+ if (_mDraggingOwner == g && _mDraggingRole == "Dot1")
+ {
+ _mDraggingOwner = null;
+ _mDraggingRole = null;
+ _mDraggingDot = null;
+ g.PerpLine1.ReleaseMouseCapture();
+ }
+ e.Handled = true;
+ };
+
+ // 垂直线段2的拖拽处理
+ g.PerpLine2.MouseLeftButtonDown += (s, e) =>
+ {
+ _mDraggingOwner = g;
+ _mDraggingRole = "Dot2";
+ _mDraggingDot = g.Dot2;
+ g.PerpLine2.CaptureMouse();
+ e.Handled = true;
+ };
+ g.PerpLine2.MouseMove += (s, e) =>
+ {
+ if (_mDraggingOwner != g || _mDraggingRole != "Dot2" || _measureOverlay == null) return;
+ if (e.LeftButton != MouseButtonState.Pressed) return;
+ var pos = e.GetPosition(_measureOverlay);
+ SetDotPos(g.Dot2, pos);
+ g.P2 = pos;
+ g.UpdateLine();
+ g.UpdateLabel(FormatDistance(g.Distance));
+ RaiseMeasureCompleted(g.P1, g.P2, g.Distance, MeasureCount, "PointDistance");
+ };
+ g.PerpLine2.MouseLeftButtonUp += (s, e) =>
+ {
+ if (_mDraggingOwner == g && _mDraggingRole == "Dot2")
+ {
+ _mDraggingOwner = null;
+ _mDraggingRole = null;
+ _mDraggingDot = null;
+ g.PerpLine2.ReleaseMouseCapture();
+ }
+ e.Handled = true;
+ };
+
g.UpdateLine(); g.UpdateLabel(FormatDistance(g.Distance));
return g;
}
@@ -576,7 +710,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
_ptlTempDot2 = CreateMDot(Brushes.Lime);
_measureOverlay.Children.Add(_ptlTempDot2);
SetDotPos(_ptlTempDot2, pos);
- _ptlTempLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false,
+ _ptlTempLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false,
X1 = _ptlTempL1.Value.X, Y1 = _ptlTempL1.Value.Y, X2 = pos.X, Y2 = pos.Y };
_measureOverlay.Children.Add(_ptlTempLine);
RaiseMeasureStatusChanged($"点线距 - 直线已定义,请点击测量点");
@@ -602,12 +736,12 @@ namespace XP.ImageProcessing.RoiControl.Controls
private Models.PointToLineGroup CreatePTLGroup(Point l1, Point l2, Point p)
{
var g = new Models.PointToLineGroup { L1 = l1, L2 = l2, P = p };
- g.MainLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false };
+ g.MainLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false };
g.ExtLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false,
StrokeDashArray = new DoubleCollection { 4, 2 }, Visibility = Visibility.Collapsed };
g.PerpLine = new Line { Stroke = Brushes.Yellow, StrokeThickness = 1, IsHitTestVisible = false,
StrokeDashArray = new DoubleCollection { 4, 2 } };
- g.FootDot = new Ellipse { Width = 8, Height = 8, Fill = Brushes.Cyan, Stroke = Brushes.White, StrokeThickness = 1, IsHitTestVisible = false };
+ g.FootDot = new Ellipse { Width = 2, Height = 2, Fill = Brushes.Cyan, Stroke = Brushes.White, StrokeThickness = 0.5, IsHitTestVisible = false };
g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false };
g.DotL1 = CreateMDot(Brushes.Lime);
g.DotL2 = CreateMDot(Brushes.Lime);
@@ -709,7 +843,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
g.PathE4 = CreateEllipsePath(Brushes.Lime, false);
g.FullLine = new Line { Stroke = Brushes.Red, StrokeThickness = 1, IsHitTestVisible = false };
- g.FillLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false };
+ g.FillLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false };
g.Label = new TextBlock { FontSize = 13, FontWeight = FontWeights.Bold, Cursor = Cursors.Hand };
g.Label.SetValue(ContextMenuService.IsEnabledProperty, false);
@@ -737,7 +871,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
private Ellipse CreateAxisHandle(Brush fill)
{
- var h = new Ellipse { Width = 8, Height = 8, Fill = fill, Stroke = Brushes.White, StrokeThickness = 1, Cursor = Cursors.SizeAll };
+ var h = new Ellipse { Width = 2, Height = 2, Fill = fill, Stroke = Brushes.White, StrokeThickness = 0.5, Cursor = Cursors.SizeAll };
h.SetValue(ContextMenuService.IsEnabledProperty, false);
h.MouseLeftButtonDown += MDot_Down;
h.MouseMove += MDot_Move;
@@ -838,7 +972,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
};
c.CenterDot = CreateMDot(isBall ? Brushes.Lime : Brushes.Orange);
c.EdgeDot = CreateMDot(isBall ? Brushes.Lime : Brushes.Orange);
- c.EdgeDot.Width = 8; c.EdgeDot.Height = 8;
+ c.EdgeDot.Width = 2; c.EdgeDot.Height = 2;
c.EdgeDot.Cursor = System.Windows.Input.Cursors.SizeAll;
_measureOverlay.Children.Add(c.Shape);
@@ -902,7 +1036,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
private Ellipse CreateMDot(Brush fill)
{
- var dot = new Ellipse { Width = 12, Height = 12, Fill = fill, Stroke = Brushes.White, StrokeThickness = 1.5, Cursor = Cursors.Hand };
+ var dot = new Ellipse { Width = 2, Height = 2, Fill = fill, Stroke = Brushes.White, StrokeThickness = 0.5, Cursor = Cursors.Hand };
dot.SetValue(ContextMenuService.IsEnabledProperty, false);
dot.MouseLeftButtonDown += MDot_Down;
dot.MouseMove += MDot_Move;
@@ -1154,6 +1288,64 @@ namespace XP.ImageProcessing.RoiControl.Controls
// ── 气泡测量辅助 ──
+ /// 是否已有任何形状的气泡 ROI
+ private bool HasBubbleRoi =>
+ _bubbleRoi.HasValue || _bubbleCircleCenter.HasValue || _bubblePolyPoints.Count >= 3;
+
+ /// 判断点是否在当前气泡 ROI 内
+ private bool IsInBubbleRoi(Point pos)
+ {
+ if (_bubbleRoi.HasValue)
+ return _bubbleRoi.Value.Contains(pos);
+ if (_bubbleCircleCenter.HasValue)
+ {
+ double dx = pos.X - _bubbleCircleCenter.Value.X;
+ double dy = pos.Y - _bubbleCircleCenter.Value.Y;
+ return dx * dx + dy * dy <= _bubbleCircleRadius * _bubbleCircleRadius;
+ }
+ if (_bubblePolyPoints.Count >= 3)
+ return IsPointInPolygon(pos, _bubblePolyPoints);
+ return false;
+ }
+
+ private static bool IsPointInPolygon(Point p, System.Collections.Generic.IList polygon)
+ {
+ bool inside = false;
+ int n = polygon.Count;
+ for (int i = 0, j = n - 1; i < n; j = i++)
+ {
+ if ((polygon[i].Y > p.Y) != (polygon[j].Y > p.Y) &&
+ p.X < (polygon[j].X - polygon[i].X) * (p.Y - polygon[i].Y) / (polygon[j].Y - polygon[i].Y) + polygon[i].X)
+ inside = !inside;
+ }
+ return inside;
+ }
+
+ /// 获取当前 ROI 的外接矩形(用于掩码计算范围)
+ private Rect GetBubbleRoiBounds()
+ {
+ if (_bubbleRoi.HasValue) return _bubbleRoi.Value;
+ if (_bubbleCircleCenter.HasValue)
+ {
+ var c = _bubbleCircleCenter.Value;
+ var r = _bubbleCircleRadius;
+ return new Rect(c.X - r, c.Y - r, r * 2, r * 2);
+ }
+ if (_bubblePolyPoints.Count >= 3)
+ {
+ double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue;
+ foreach (var pt in _bubblePolyPoints)
+ {
+ if (pt.X < minX) minX = pt.X;
+ if (pt.Y < minY) minY = pt.Y;
+ if (pt.X > maxX) maxX = pt.X;
+ if (pt.Y > maxY) maxY = pt.Y;
+ }
+ return new Rect(minX, minY, maxX - minX, maxY - minY);
+ }
+ return Rect.Empty;
+ }
+
private void EnsureBubbleRoiVisuals()
{
EnsureMeasureOverlay();
@@ -1162,7 +1354,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
_bubbleRoiRect = new Rectangle
{
Stroke = Brushes.Red,
- StrokeThickness = 1.5,
+ StrokeThickness = 1,
Fill = Brushes.Transparent,
Visibility = Visibility.Collapsed,
IsHitTestVisible = false
@@ -1211,9 +1403,55 @@ namespace XP.ImageProcessing.RoiControl.Controls
}
}
+ private void EnsureBubbleCircleVisuals()
+ {
+ EnsureMeasureOverlay();
+ if (_bubbleCircleShape == null)
+ {
+ _bubbleCircleShape = new Ellipse
+ {
+ Stroke = Brushes.Red,
+ StrokeThickness = 1,
+ Fill = Brushes.Transparent,
+ Visibility = Visibility.Collapsed,
+ IsHitTestVisible = false
+ };
+ _measureOverlay.Children.Add(_bubbleCircleShape);
+ }
+ }
+
+ private void UpdateBubbleCircleVisuals()
+ {
+ if (_bubbleCircleShape == null || !_bubbleCircleCenter.HasValue) return;
+ var c = _bubbleCircleCenter.Value;
+ var r = _bubbleCircleRadius;
+ _bubbleCircleShape.Width = r * 2;
+ _bubbleCircleShape.Height = r * 2;
+ Canvas.SetLeft(_bubbleCircleShape, c.X - r);
+ Canvas.SetTop(_bubbleCircleShape, c.Y - r);
+ _bubbleCircleShape.Visibility = Visibility.Visible;
+ }
+
+ private void UpdateBubblePolyVisuals()
+ {
+ EnsureMeasureOverlay();
+ if (_bubblePolyShape == null)
+ {
+ _bubblePolyShape = new Polygon
+ {
+ Stroke = Brushes.Red,
+ StrokeThickness = 1,
+ Fill = Brushes.Transparent,
+ IsHitTestVisible = false
+ };
+ _measureOverlay.Children.Add(_bubblePolyShape);
+ }
+ _bubblePolyShape.Points = new PointCollection(_bubblePolyPoints);
+ }
+
private void InitBubbleMask()
{
- if (!_bubbleRoi.HasValue) return;
+ if (!HasBubbleRoi) return;
int w = (int)CanvasWidth, h = (int)CanvasHeight;
if (w <= 0 || h <= 0) return;
@@ -1238,9 +1476,9 @@ namespace XP.ImageProcessing.RoiControl.Controls
private void ApplyBrushAt(Point pos)
{
- if (_bubbleMask == null || !_bubbleRoi.HasValue) return;
+ if (_bubbleMask == null || !HasBubbleRoi) return;
- var roi = _bubbleRoi.Value;
+ var roi = GetBubbleRoiBounds();
int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight;
int cx = (int)pos.X, cy = (int)pos.Y;
int r = _bubbleBrushSize;
@@ -1249,15 +1487,12 @@ namespace XP.ImageProcessing.RoiControl.Controls
int roiX0 = Math.Max(0, (int)roi.X), roiY0 = Math.Max(0, (int)roi.Y);
int roiX1 = Math.Min(w, (int)roi.Right), roiY1 = Math.Min(h, (int)roi.Bottom);
- // 计算笔刷影响的矩形区域
int x0 = Math.Max(roiX0, cx - r), y0 = Math.Max(roiY0, cy - r);
int x1 = Math.Min(roiX1, cx + r + 1), y1 = Math.Min(roiY1, cy + r + 1);
if (x0 >= x1 || y0 >= y1) return;
int regionW = x1 - x0, regionH = y1 - y0;
- int stride = w * 4;
- // 读取整行范围的像素
var pixels = new byte[regionW * regionH * 4];
_bubbleMask.CopyPixels(new System.Windows.Int32Rect(x0, y0, regionW, regionH), pixels, regionW * 4, 0);
@@ -1268,6 +1503,8 @@ namespace XP.ImageProcessing.RoiControl.Controls
{
int dx = px2 - cx, dy = py2 - cy;
if (dx * dx + dy * dy > r2) continue;
+ // 检查是否在 ROI 内
+ if (!IsInBubbleRoi(new Point(px2, py2))) continue;
int idx = ((py2 - y0) * regionW + (px2 - x0)) * 4;
if (erase)
@@ -1290,8 +1527,8 @@ namespace XP.ImageProcessing.RoiControl.Controls
private void UpdateBubbleResult()
{
- if (_bubbleMask == null || !_bubbleRoi.HasValue) return;
- var roi = _bubbleRoi.Value;
+ if (_bubbleMask == null || !HasBubbleRoi) return;
+ var roi = GetBubbleRoiBounds();
int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight;
int stride = w * 4;
var pixels = new byte[stride * h];
@@ -1304,13 +1541,13 @@ namespace XP.ImageProcessing.RoiControl.Controls
for (int y = roiY0; y < roiY1; y++)
for (int x = roiX0; x < roiX1; x++)
{
+ if (!IsInBubbleRoi(new Point(x, y))) continue;
roiArea++;
int idx = (y * w + x) * 4;
- if (pixels[idx + 3] > 0) voidArea++; // alpha > 0 表示已标记
+ if (pixels[idx + 3] > 0) voidArea++;
}
double voidRate = roiArea > 0 ? voidArea * 100.0 / roiArea : 0;
- // 更新 ROI 上方标签
if (_bubbleResultLabel != null)
{
string cls = voidRate <= _bubbleVoidLimit ? "PASS" : "FAIL";
@@ -1327,14 +1564,15 @@ namespace XP.ImageProcessing.RoiControl.Controls
/// 魔棒:在点击位置做 flood fill
public void WandFloodFill(Point pos)
{
- if (_bubbleMask == null || !_bubbleRoi.HasValue || ImageSource == null) return;
+ if (_bubbleMask == null || !HasBubbleRoi || ImageSource == null) return;
// 保存快照用于撤销
SaveMaskSnapshot();
- var roi = _bubbleRoi.Value;
+ if (!IsInBubbleRoi(pos)) return;
+
+ var roi = GetBubbleRoiBounds();
int px = (int)pos.X, py = (int)pos.Y;
- if (!roi.Contains(pos)) return;
// 获取灰度像素
var gray = GetGrayscalePixels();
@@ -1344,9 +1582,8 @@ namespace XP.ImageProcessing.RoiControl.Controls
if (px < 0 || px >= w || py < 0 || py >= h) return;
int seedVal = gray[py * w + px];
- int lo = _bubbleThreshold, hi = _bubbleThreshold;
+ int lo = _bubbleThreshold;
- // BFS flood fill
var visited = new bool[w * h];
var queue = new System.Collections.Generic.Queue<(int x, int y)>();
queue.Enqueue((px, py));
@@ -1362,19 +1599,16 @@ namespace XP.ImageProcessing.RoiControl.Controls
var (cx, cy) = queue.Dequeue();
int val = gray[cy * w + cx];
- // 阈值判断:与种子点灰度差在阈值范围内
if (Math.Abs(val - seedVal) > lo) continue;
- // 必须在 ROI 内
- if (cx < roiX0 || cx >= roiX1 || cy < roiY0 || cy >= roiY1) continue;
+ if (!IsInBubbleRoi(new Point(cx, cy))) continue;
filled.Add((cx, cy));
- // 四邻域
int[] dx = { -1, 1, 0, 0 }, dy = { 0, 0, -1, 1 };
for (int d = 0; d < 4; d++)
{
int nx = cx + dx[d], ny = cy + dy[d];
- if (nx >= roiX0 && nx < roiX1 && ny >= roiY0 && ny < roiY1 && !visited[ny * w + nx])
+ if (nx >= 0 && nx < w && ny >= 0 && ny < h && !visited[ny * w + nx] && IsInBubbleRoi(new Point(nx, ny)))
{
visited[ny * w + nx] = true;
queue.Enqueue((nx, ny));
@@ -1455,8 +1689,16 @@ namespace XP.ImageProcessing.RoiControl.Controls
if (_bubbleRoiHandle != null) { _measureOverlay.Children.Remove(_bubbleRoiHandle); _bubbleRoiHandle = null; }
if (_bubbleResultLabel != null) { _measureOverlay.Children.Remove(_bubbleResultLabel); _bubbleResultLabel = null; }
if (_bubbleMaskImage != null) { _measureOverlay.Children.Remove(_bubbleMaskImage); _bubbleMaskImage = null; }
+ if (_bubbleCircleShape != null) { _measureOverlay.Children.Remove(_bubbleCircleShape); _bubbleCircleShape = null; }
+ if (_bubblePolyShape != null) { _measureOverlay.Children.Remove(_bubblePolyShape); _bubblePolyShape = null; }
}
_bubbleRoi = null;
+ _bubbleCircleCenter = null;
+ _bubbleCircleRadius = 0;
+ _bubbleCircleDragging = false;
+ _bubbleCircleMoving = false;
+ _bubbleCircleResizing = false;
+ _bubblePolyPoints.Clear();
_bubbleMask = null;
_bubbleRoiStart = null;
_bubbleRoiDragging = false;
@@ -1735,6 +1977,38 @@ namespace XP.ImageProcessing.RoiControl.Controls
#region Mouse Events
+ private void UpdateCursorInfo(Point pos)
+ {
+ if (ImageSource == null)
+ {
+ CursorInfo = "X: -- Y: -- Gray: --";
+ return;
+ }
+
+ int px = (int)pos.X, py = (int)pos.Y;
+ string grayText = "--";
+
+ if (ImageSource is BitmapSource bmp && px >= 0 && py >= 0 && px < bmp.PixelWidth && py < bmp.PixelHeight)
+ {
+ try
+ {
+ var pixel = new byte[4];
+ var converted = new FormatConvertedBitmap(bmp, PixelFormats.Bgra32, null, 0);
+ converted.CopyPixels(new System.Windows.Int32Rect(px, py, 1, 1), pixel, 4, 0);
+ int gray = (int)(pixel[2] * 0.299 + pixel[1] * 0.587 + pixel[0] * 0.114);
+ grayText = gray.ToString();
+ }
+ catch { }
+ }
+
+ CursorInfo = $"X: {px} Y: {py} Gray: {grayText}";
+ }
+
+ private void Canvas_MouseLeave(object sender, MouseEventArgs e)
+ {
+ CursorInfo = "X: -- Y: -- Gray: --";
+ }
+
private void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
double oldZoom = ZoomScale;
@@ -1781,7 +2055,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
e.Handled = true;
return;
}
- // 没有 ROI 时才画新的,有 ROI 时点击外部不拦截(让图像正常拖动)
+ // 没有 ROI 时才画新的
if (!_bubbleRoi.HasValue)
{
_bubbleRoiStart = pos;
@@ -1791,12 +2065,52 @@ namespace XP.ImageProcessing.RoiControl.Controls
e.Handled = true;
return;
}
- // 有 ROI 但点击了外部 → 不拦截,走正常拖动逻辑
}
- if ((_bubbleTool == BubbleSubTool.Brush || _bubbleTool == BubbleSubTool.Eraser) && _bubbleRoi.HasValue)
+ if (_bubbleTool == BubbleSubTool.RoiCircle)
{
- // 只在 ROI 内才启动画笔,ROI 外不拦截,让图像正常拖动
- if (_bubbleRoi.Value.Contains(pos))
+ // 检查是否点击了圆形 ROI
+ if (_bubbleCircleCenter.HasValue)
+ {
+ double dx = pos.X - _bubbleCircleCenter.Value.X;
+ double dy = pos.Y - _bubbleCircleCenter.Value.Y;
+ double dist = Math.Sqrt(dx * dx + dy * dy);
+ // 先检查是否点击圆内部(拖动),排除边缘附近区域
+ if (dist < _bubbleCircleRadius - 10)
+ {
+ _bubbleCircleMoving = true;
+ _bubbleCircleDragOffset = new Point(pos.X - _bubbleCircleCenter.Value.X, pos.Y - _bubbleCircleCenter.Value.Y);
+ mainCanvas.CaptureMouse();
+ e.Handled = true;
+ return;
+ }
+ // 再检查是否点击边缘附近(调整大小)
+ if (Math.Abs(dist - _bubbleCircleRadius) < 15)
+ {
+ _bubbleCircleResizing = true;
+ _bubbleRoiStart = pos;
+ mainCanvas.CaptureMouse();
+ e.Handled = true;
+ return;
+ }
+ }
+ // 没有圆形 ROI 时开始画
+ if (!_bubbleCircleCenter.HasValue)
+ {
+ _bubbleRoiStart = pos;
+ _bubbleCircleDragging = true;
+ EnsureBubbleCircleVisuals();
+ mainCanvas.CaptureMouse();
+ e.Handled = true;
+ return;
+ }
+ }
+ if (_bubbleTool == BubbleSubTool.RoiPolygon)
+ {
+ // 多边形 ROI 由外部面板通过 CanvasClickedEvent 处理,这里不拦截
+ }
+ if ((_bubbleTool == BubbleSubTool.Brush || _bubbleTool == BubbleSubTool.Eraser) && HasBubbleRoi)
+ {
+ if (IsInBubbleRoi(pos))
{
SaveMaskSnapshot();
_bubbleBrushDragging = true;
@@ -1855,8 +2169,47 @@ namespace XP.ImageProcessing.RoiControl.Controls
e.Handled = true;
return;
}
+ // 气泡测量:圆形 ROI 拖拽
+ if (_bubbleCircleDragging && _bubbleRoiStart.HasValue)
+ {
+ var pos = e.GetPosition(mainCanvas);
+ double dx = pos.X - _bubbleRoiStart.Value.X;
+ double dy = pos.Y - _bubbleRoiStart.Value.Y;
+ double r = Math.Sqrt(dx * dx + dy * dy);
+ if (_bubbleCircleShape != null)
+ {
+ _bubbleCircleShape.Width = r * 2;
+ _bubbleCircleShape.Height = r * 2;
+ Canvas.SetLeft(_bubbleCircleShape, _bubbleRoiStart.Value.X - r);
+ Canvas.SetTop(_bubbleCircleShape, _bubbleRoiStart.Value.Y - r);
+ _bubbleCircleShape.Visibility = Visibility.Visible;
+ }
+ e.Handled = true;
+ return;
+ }
+ // 气泡测量:圆形 ROI 拖动
+ if (_bubbleCircleMoving && _bubbleCircleCenter.HasValue)
+ {
+ var pos = e.GetPosition(mainCanvas);
+ _bubbleCircleCenter = new Point(pos.X - _bubbleCircleDragOffset.X, pos.Y - _bubbleCircleDragOffset.Y);
+ UpdateBubbleCircleVisuals();
+ e.Handled = true;
+ return;
+ }
+ // 气泡测量:圆形 ROI 调整大小
+ if (_bubbleCircleResizing && _bubbleCircleCenter.HasValue)
+ {
+ var pos = e.GetPosition(mainCanvas);
+ double dx = pos.X - _bubbleCircleCenter.Value.X;
+ double dy = pos.Y - _bubbleCircleCenter.Value.Y;
+ _bubbleCircleRadius = Math.Max(5, Math.Sqrt(dx * dx + dy * dy));
+ UpdateBubbleCircleVisuals();
+ InitBubbleMask();
+ e.Handled = true;
+ return;
+ }
// 气泡测量:画笔/橡皮拖拽
- if (_bubbleBrushDragging && _bubbleRoi.HasValue)
+ if (_bubbleBrushDragging && HasBubbleRoi)
{
var pos = e.GetPosition(mainCanvas);
ApplyBrushAt(pos);
@@ -1877,6 +2230,9 @@ namespace XP.ImageProcessing.RoiControl.Controls
lastMousePosition = currentPosition;
}
}
+
+ // 更新光标信息(像素坐标 + 灰度值)
+ UpdateCursorInfo(e.GetPosition(mainCanvas));
}
private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
@@ -1914,6 +2270,47 @@ namespace XP.ImageProcessing.RoiControl.Controls
e.Handled = true;
return;
}
+ // 气泡测量:圆形 ROI 拖拽完成
+ if (_bubbleCircleDragging && _bubbleRoiStart.HasValue)
+ {
+ var pos = e.GetPosition(mainCanvas);
+ double dx = pos.X - _bubbleRoiStart.Value.X;
+ double dy = pos.Y - _bubbleRoiStart.Value.Y;
+ double r = Math.Sqrt(dx * dx + dy * dy);
+ _bubbleCircleDragging = false;
+ mainCanvas.ReleaseMouseCapture();
+
+ if (r > 5)
+ {
+ _bubbleCircleCenter = _bubbleRoiStart.Value;
+ _bubbleCircleRadius = r;
+ _bubbleRoiStart = null;
+ InitBubbleMask();
+ RaiseMeasureStatusChanged($"圆形ROI 已设置: 半径={r:F0}px");
+ }
+ e.Handled = true;
+ return;
+ }
+ // 气泡测量:圆形 ROI 拖动完成
+ if (_bubbleCircleMoving)
+ {
+ _bubbleCircleMoving = false;
+ mainCanvas.ReleaseMouseCapture();
+ InitBubbleMask();
+ e.Handled = true;
+ return;
+ }
+ // 气泡测量:圆形 ROI 调整大小完成
+ if (_bubbleCircleResizing)
+ {
+ _bubbleCircleResizing = false;
+ _bubbleRoiStart = null;
+ mainCanvas.ReleaseMouseCapture();
+ InitBubbleMask();
+ RaiseMeasureStatusChanged($"圆形ROI 已调整: 半径={_bubbleCircleRadius:F0}px");
+ e.Handled = true;
+ return;
+ }
// 气泡测量:画笔/橡皮松开
if (_bubbleBrushDragging)
{
@@ -1933,7 +2330,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
{
HandleMeasureClick(clickPosition);
// 魔棒点击
- if (CurrentMeasureMode == Models.MeasureMode.BubbleMeasure && _bubbleTool == BubbleSubTool.Wand && _bubbleRoi.HasValue)
+ if (CurrentMeasureMode == Models.MeasureMode.BubbleMeasure && _bubbleTool == BubbleSubTool.Wand && HasBubbleRoi)
{
WandFloodFill(clickPosition);
}
diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs b/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs
index 32ba2aa..0c93043 100644
--- a/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs
+++ b/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs
@@ -9,12 +9,21 @@ namespace XP.ImageProcessing.RoiControl.Models
/// 一次点点距测量的所有视觉元素
public class MeasureGroup
{
+ // 保留原有圆点属性以兼容其他代码
public Ellipse Dot1 { get; set; }
public Ellipse Dot2 { get; set; }
+
+ // 新增:垂直线段(替代圆点的视觉表现)
+ public Line PerpLine1 { get; set; }
+ public Line PerpLine2 { get; set; }
+
public Line Line { get; set; }
public TextBlock Label { get; set; }
public Point P1 { get; set; }
public Point P2 { get; set; }
+
+ /// 垂直线段长度(像素)
+ public double PerpLineLength { get; set; } = 10.0;
public double Distance
{
@@ -30,6 +39,46 @@ namespace XP.ImageProcessing.RoiControl.Models
Line.X1 = P1.X; Line.Y1 = P1.Y;
Line.X2 = P2.X; Line.Y2 = P2.Y;
Line.Visibility = Visibility.Visible;
+
+ // 更新垂直线段位置
+ UpdatePerpLines();
+ }
+
+ /// 更新两条垂直线段的位置
+ public void UpdatePerpLines()
+ {
+ if (PerpLine1 == null || PerpLine2 == null) return;
+
+ // 计算两点连线的方向向量
+ double dx = P2.X - P1.X;
+ double dy = P2.Y - P1.Y;
+ double len = Math.Sqrt(dx * dx + dy * dy);
+
+ if (len < 0.001) return; // 避免除零
+
+ // 归一化方向向量
+ double ux = dx / len;
+ double uy = dy / len;
+
+ // 垂直方向向量 (-uy, ux)
+ double vx = -uy;
+ double vy = ux;
+
+ // P1处的垂直线段
+ double halfLen1 = PerpLineLength / 2;
+ PerpLine1.X1 = P1.X + vx * halfLen1;
+ PerpLine1.Y1 = P1.Y + vy * halfLen1;
+ PerpLine1.X2 = P1.X - vx * halfLen1;
+ PerpLine1.Y2 = P1.Y - vy * halfLen1;
+ PerpLine1.Visibility = Visibility.Visible;
+
+ // P2处的垂直线段
+ double halfLen2 = PerpLineLength / 2;
+ PerpLine2.X1 = P2.X + vx * halfLen2;
+ PerpLine2.Y1 = P2.Y + vy * halfLen2;
+ PerpLine2.X2 = P2.X - vx * halfLen2;
+ PerpLine2.Y2 = P2.Y - vy * halfLen2;
+ PerpLine2.Visibility = Visibility.Visible;
}
public void UpdateLabel(string distanceText = null)
diff --git a/XplorePlane/Assets/Icons/circle32.png b/XplorePlane/Assets/Icons/circle32.png
new file mode 100644
index 0000000..88c1158
Binary files /dev/null and b/XplorePlane/Assets/Icons/circle32.png differ
diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs
index 26e6403..be7eefe 100644
--- a/XplorePlane/ViewModels/Main/MainViewModel.cs
+++ b/XplorePlane/ViewModels/Main/MainViewModel.cs
@@ -104,7 +104,6 @@ namespace XplorePlane.ViewModels
public DelegateCommand PointLineDistanceMeasureCommand { get; }
public DelegateCommand AngleMeasureCommand { get; }
public DelegateCommand ThroughHoleFillRateMeasureCommand { get; }
- public DelegateCommand BgaVoidMeasureCommand { get; }
public DelegateCommand BgaDetectionCommand { get; }
public DelegateCommand VoidDetectionCommand { get; }
public DelegateCommand BubbleMeasureCommand { get; }
@@ -288,7 +287,6 @@ namespace XplorePlane.ViewModels
PointLineDistanceMeasureCommand = new DelegateCommand(ExecutePointLineDistanceMeasure);
AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure);
ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure);
- BgaVoidMeasureCommand = new DelegateCommand(ExecuteBgaVoidMeasure);
BgaDetectionCommand = new DelegateCommand(ExecuteBgaDetection);
VoidDetectionCommand = new DelegateCommand(ExecuteVoidDetection);
BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure);
@@ -322,6 +320,13 @@ namespace XplorePlane.ViewModels
public string CncStatusMessage => _cncEditorViewModel.StatusMessage;
public bool CncHasExecutionError => _cncEditorViewModel.HasExecutionError;
+ private string _cursorInfoText = "X: -- Y: -- Gray: --";
+ public string CursorInfoText
+ {
+ get => _cursorInfoText;
+ set => SetProperty(ref _cursorInfoText, value);
+ }
+
private void ShowWindow(Window window, string name)
{
window.Owner = Application.Current.MainWindow;
diff --git a/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml b/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml
deleted file mode 100644
index 87aad82..0000000
--- a/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml
+++ /dev/null
@@ -1,96 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml.cs b/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml.cs
deleted file mode 100644
index ee34e2f..0000000
--- a/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-using System.Windows;
-using XP.ImageProcessing.RoiControl.Controls;
-
-namespace XplorePlane.Views.ImageProcessing
-{
- public partial class BgaMeasurePanel : Window
- {
- private PolygonRoiCanvas _canvas;
-
- public BgaMeasurePanel()
- {
- InitializeComponent();
- Loaded += OnLoaded;
- }
-
- private void OnLoaded(object sender, RoutedEventArgs e)
- {
- try
- {
- var mainWin = Owner as MainWindow;
- if (mainWin != null)
- _canvas = FindChild(mainWin);
- }
- catch { }
-
- // 模式切换:通知 canvas 切换气泡/焊球
- RbVoid.Checked += (s, ev) =>
- {
- if (_canvas != null) _canvas.SetBgaDrawBall(false);
- };
- RbBall.Checked += (s, ev) =>
- {
- if (_canvas != null) _canvas.SetBgaDrawBall(true);
- };
-
- // VoidLimit 同步
- SliderVoidLimit.ValueChanged += (s, ev) =>
- {
- TbVoidLimit.Text = SliderVoidLimit.Value.ToString("F1");
- _canvas?.SetBgaVoidLimit(SliderVoidLimit.Value);
- };
-
- // 监听测量完成事件更新结果
- if (_canvas != null)
- {
- _canvas.MeasureCompleted += (s, ev) =>
- {
- if (ev is MeasureCompletedEventArgs args && args.MeasureType == "BgaVoid")
- {
- TbResult.Text = $"空隙率: {args.Distance:F1}%";
- }
- };
- }
- }
-
- private void Finish_Click(object sender, RoutedEventArgs e)
- {
- Close();
- }
-
- private static T FindChild(DependencyObject parent) where T : DependencyObject
- {
- int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent);
- for (int i = 0; i < count; i++)
- {
- var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, i);
- if (child is T t) return t;
- var result = FindChild(child);
- if (result != null) return result;
- }
- return null;
- }
- }
-}
diff --git a/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml
index e4fc177..8989ad2 100644
--- a/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml
+++ b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml
@@ -3,7 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="气泡测量工具"
- Width="340" Height="380"
+ Width="420" Height="380"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner"
Topmost="True" ShowInTaskbar="False"
@@ -60,9 +60,15 @@
-
+
+
+
+
+
+
+
diff --git a/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs
index 99fa7aa..8a5c022 100644
--- a/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs
+++ b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs
@@ -1,6 +1,9 @@
+using System;
+using System.Collections.ObjectModel;
using System.Windows;
using Prism.Ioc;
using XP.ImageProcessing.RoiControl.Controls;
+using XP.ImageProcessing.RoiControl.Models;
using XplorePlane.ViewModels;
namespace XplorePlane.Views.ImageProcessing
@@ -8,31 +11,33 @@ namespace XplorePlane.Views.ImageProcessing
public partial class BubbleMeasurePanel : Window
{
private PolygonRoiCanvas _canvas;
+ private PolygonROI _polyRoiShape;
+ private bool _polyRoiActive; // 标记多边形 ROI 是否激活
public BubbleMeasurePanel()
{
InitializeComponent();
Loaded += OnLoaded;
+ Closed += OnClosed;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
- // 获取主界面的 RoiCanvas
try
{
var mainWin = Owner as MainWindow;
if (mainWin != null)
- {
_canvas = FindChild(mainWin);
- }
}
catch { }
// 工具切换
- RbRoi.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Roi);
- RbWand.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Wand);
- RbBrush.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Brush);
- RbEraser.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Eraser);
+ RbRoi.Checked += (s, ev) => { ClearAllBubbleRoi(); _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Roi); };
+ RbRoiCircle.Checked += (s, ev) => { ClearAllBubbleRoi(); _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.RoiCircle); };
+ RbRoiPolygon.Checked += (s, ev) => { ClearAllBubbleRoi(); _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.RoiPolygon); EnablePolyRoi(); };
+ RbWand.Checked += (s, ev) => { _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Wand); FinalizePolyRoi(); };
+ RbBrush.Checked += (s, ev) => { _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Brush); FinalizePolyRoi(); };
+ RbEraser.Checked += (s, ev) => { _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Eraser); FinalizePolyRoi(); };
// 阈值同步
SliderThreshold.ValueChanged += (s, ev) =>
@@ -52,36 +57,136 @@ namespace XplorePlane.Views.ImageProcessing
TbVoidLimit.TextChanged += (s, ev) =>
{
if (double.TryParse(TbVoidLimit.Text, out double val))
- _canvas?.SetBubbleVoidLimit(System.Math.Clamp(val, 0, 100));
+ _canvas?.SetBubbleVoidLimit(Math.Clamp(val, 0, 100));
};
- // 初始同步:确保面板默认值推送到 canvas
+ // 初始同步
_canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Roi);
_canvas?.SetBubbleThreshold((int)SliderThreshold.Value);
_canvas?.SetBubbleBrushSize((int)SliderBrushSize.Value);
if (double.TryParse(TbVoidLimit.Text, out double initLimit))
- _canvas?.SetBubbleVoidLimit(System.Math.Clamp(initLimit, 0, 100));
+ _canvas?.SetBubbleVoidLimit(Math.Clamp(initLimit, 0, 100));
- // 监听 canvas 的工具切换事件(ROI 画完后自动切换时同步面板)
if (_canvas != null)
{
- _canvas.BubbleToolChanged += (s, ev) =>
- {
- // 同步面板 radio button
- };
-
+ _canvas.BubbleToolChanged += (s, ev) => { };
_canvas.MeasureCompleted += (s, ev) =>
{
if (ev is MeasureCompletedEventArgs args && args.MeasureType == "BubbleVoid")
- {
TbResult.Text = $"空隙率: {args.Distance:F1}%";
- }
};
}
}
+ // ── 多边形 ROI(复用 ROIItems 方式)──
+
+ /// 清除所有类型的气泡 ROI(矩形/圆形/多边形)
+ private void ClearAllBubbleRoi()
+ {
+ CleanupPolyRoi();
+ _canvas?.ClearBubbleMeasure();
+ // 确保多边形点也被清空
+ _canvas?.SetBubblePolyPoints(new System.Collections.Generic.List());
+ TbResult.Text = "空隙率: --";
+ }
+
+ private void EnablePolyRoi()
+ {
+ if (_canvas == null) return;
+ if (_polyRoiShape != null) return; // 已有
+
+ if (_canvas.ROIItems == null)
+ _canvas.ROIItems = new ObservableCollection();
+
+ _polyRoiShape = new PolygonROI { Color = "Red", IsSelected = true };
+ _polyRoiActive = true;
+ _canvas.ROIItems.Add(_polyRoiShape);
+ _canvas.SelectedROI = _polyRoiShape;
+
+ _polyRoiShape.Points.CollectionChanged += (s, ev) =>
+ {
+ if (!_polyRoiActive) return; // 已失效的 handler 不处理
+ if (ev.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add ||
+ ev.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
+ {
+ _canvas.SelectedROI = null;
+ _canvas.SelectedROI = _polyRoiShape;
+ SyncPolyToCanvas();
+ }
+ };
+
+ _canvas.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, false);
+ _canvas.AddHandler(PolygonRoiCanvas.CanvasClickedEvent, new RoutedEventHandler(OnCanvasClickedForPoly));
+ }
+
+ private void OnCanvasClickedForPoly(object sender, RoutedEventArgs e)
+ {
+ if (_polyRoiShape == null) return;
+ if (e is CanvasClickedEventArgs args)
+ {
+ InsertPointToPolygon(args.Position, _polyRoiShape.Points);
+ _polyRoiShape.IsSelected = true;
+ _canvas.SelectedROI = _polyRoiShape;
+ }
+ }
+
+ /// 将 PolygonROI 的点同步到 canvas 内部的 _bubblePolyPoints
+ private void SyncPolyToCanvas()
+ {
+ if (_canvas == null || _polyRoiShape == null) return;
+ _canvas.SetBubblePolyPoints(_polyRoiShape.Points);
+ }
+
+ /// 切换到魔棒/画笔时,锁定多边形 ROI 不可编辑
+ private void FinalizePolyRoi()
+ {
+ if (_polyRoiShape == null) return;
+ _polyRoiShape.IsSelected = false;
+ _polyRoiShape.IsEditable = false;
+ _canvas?.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, true);
+ if (_canvas != null)
+ _canvas.RemoveHandler(PolygonRoiCanvas.CanvasClickedEvent, new RoutedEventHandler(OnCanvasClickedForPoly));
+ _canvas.SelectedROI = null;
+ }
+
+ private void CleanupPolyRoi()
+ {
+ _polyRoiActive = false;
+ if (_canvas != null)
+ {
+ _canvas.RemoveHandler(PolygonRoiCanvas.CanvasClickedEvent, new RoutedEventHandler(OnCanvasClickedForPoly));
+ _canvas.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, true);
+ }
+ if (_polyRoiShape != null && _canvas?.ROIItems != null)
+ {
+ _canvas.ROIItems.Remove(_polyRoiShape);
+ _canvas.SelectedROI = null;
+ _polyRoiShape = null;
+ }
+ }
+
+ private void OnClosed(object sender, EventArgs e)
+ {
+ // 关闭面板时保留 ROI 但设为不可编辑
+ _polyRoiActive = false;
+ if (_canvas != null)
+ {
+ _canvas.RemoveHandler(PolygonRoiCanvas.CanvasClickedEvent, new RoutedEventHandler(OnCanvasClickedForPoly));
+ _canvas.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, true);
+ }
+ if (_polyRoiShape != null)
+ {
+ _polyRoiShape.IsSelected = false;
+ _polyRoiShape.IsEditable = false;
+ _canvas.SelectedROI = null;
+ }
+ }
+
+ // ── 通用操作 ──
+
private void ClearMask_Click(object sender, RoutedEventArgs e)
{
+ CleanupPolyRoi();
_canvas?.ClearBubbleMeasure();
TbResult.Text = "空隙率: --";
}
@@ -96,6 +201,34 @@ namespace XplorePlane.Views.ImageProcessing
Close();
}
+ // ── 工具方法 ──
+
+ private static void InsertPointToPolygon(Point newPoint, ObservableCollection points)
+ {
+ if (points.Count < 2) { points.Add(newPoint); return; }
+
+ int insertIndex = 0;
+ double minDistance = double.MaxValue;
+ double d = PointToSegmentDistance(points[points.Count - 1], points[0], newPoint);
+ if (d < minDistance) { minDistance = d; insertIndex = 0; }
+ for (int i = 1; i < points.Count; i++)
+ {
+ d = PointToSegmentDistance(points[i - 1], points[i], newPoint);
+ if (d < minDistance) { minDistance = d; insertIndex = i; }
+ }
+ points.Insert(insertIndex, newPoint);
+ }
+
+ private static double PointToSegmentDistance(Point a, Point b, Point p)
+ {
+ double dx = b.X - a.X, dy = b.Y - a.Y;
+ double lenSq = dx * dx + dy * dy;
+ if (lenSq < 1e-10) return Math.Sqrt((p.X - a.X) * (p.X - a.X) + (p.Y - a.Y) * (p.Y - a.Y));
+ double t = Math.Clamp(((p.X - a.X) * dx + (p.Y - a.Y) * dy) / lenSq, 0, 1);
+ double projX = a.X + t * dx, projY = a.Y + t * dy;
+ return Math.Sqrt((p.X - projX) * (p.X - projX) + (p.Y - projY) * (p.Y - projY));
+ }
+
private static T FindChild(DependencyObject parent) where T : DependencyObject
{
int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent);
diff --git a/XplorePlane/Views/Main/MainWindow.xaml b/XplorePlane/Views/Main/MainWindow.xaml
index 53f9bd0..7f8e53f 100644
--- a/XplorePlane/Views/Main/MainWindow.xaml
+++ b/XplorePlane/Views/Main/MainWindow.xaml
@@ -238,15 +238,8 @@
Text="通孔填锡率" />
-
+
-
+ Text="{Binding CursorInfoText}" />
diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs
index cecf8f8..763d2ab 100644
--- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs
+++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs
@@ -17,13 +17,19 @@ namespace XplorePlane.Views
{
private MainViewModel _mainVm;
- private void SetStatus(string msg)
+ private MainViewModel GetMainVm()
{
if (_mainVm == null)
{
try { _mainVm = ContainerLocator.Current?.Resolve(); } catch { }
}
- if (_mainVm != null) _mainVm.StatusMessage = msg;
+ return _mainVm;
+ }
+
+ private void SetStatus(string msg)
+ {
+ var vm = GetMainVm();
+ if (vm != null) vm.StatusMessage = msg;
}
public ViewportPanelView()
@@ -113,6 +119,15 @@ namespace XplorePlane.Views
}, Prism.Events.ThreadOption.UIThread);
}
catch { }
+
+ // 光标信息:从 RoiCanvas.CursorInfo 同步到 MainViewModel
+ var cursorInfoDesc = System.ComponentModel.DependencyPropertyDescriptor.FromProperty(
+ PolygonRoiCanvas.CursorInfoProperty, typeof(PolygonRoiCanvas));
+ cursorInfoDesc?.AddValueChanged(RoiCanvas, (s, e) =>
+ {
+ var vm = GetMainVm();
+ if (vm != null) vm.CursorInfoText = RoiCanvas.CursorInfo;
+ });
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)