Compare commits
14 Commits
5e14fe3d9b
...
c80d3e2037
| Author | SHA1 | Date | |
|---|---|---|---|
| c80d3e2037 | |||
| cee89e4db0 | |||
| 1d8db8fa2d | |||
| 8d7fb4e0e3 | |||
| 6fc53c56c7 | |||
| 8dbc274e63 | |||
| 7846445b33 | |||
| 611b2ae147 | |||
| 6b35da4cc0 | |||
| 4d5fa04920 | |||
| 2663bda0ae | |||
| d5cdab294b | |||
| 64c22fc088 | |||
| 0b56010536 |
@@ -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;
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
MouseLeftButtonDown="Canvas_MouseLeftButtonDown"
|
||||
MouseLeftButtonUp="Canvas_MouseLeftButtonUp"
|
||||
MouseMove="Canvas_MouseMove"
|
||||
MouseLeave="Canvas_MouseLeave"
|
||||
MouseRightButtonDown="Canvas_MouseRightButtonDown"
|
||||
PreviewMouseRightButtonUp="Canvas_PreviewMouseRightButtonUp">
|
||||
|
||||
|
||||
@@ -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: --"));
|
||||
|
||||
/// <summary>鼠标在图像上的像素坐标和灰度值,格式: "X: 123 Y: 456 Gray: 128"</summary>
|
||||
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<Point> _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<byte[]> _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<Point> points)
|
||||
{
|
||||
_bubblePolyPoints.Clear();
|
||||
foreach (var p in points) _bubblePolyPoints.Add(p);
|
||||
if (_bubblePolyPoints.Count >= 3) InitBubbleMask();
|
||||
}
|
||||
public Rect? BubbleRoi => _bubbleRoi;
|
||||
|
||||
/// <summary>设置 BGA 测量的气泡/焊球绘制模式</summary>
|
||||
@@ -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
|
||||
|
||||
// ── 气泡测量辅助 ──
|
||||
|
||||
/// <summary>是否已有任何形状的气泡 ROI</summary>
|
||||
private bool HasBubbleRoi =>
|
||||
_bubbleRoi.HasValue || _bubbleCircleCenter.HasValue || _bubblePolyPoints.Count >= 3;
|
||||
|
||||
/// <summary>判断点是否在当前气泡 ROI 内</summary>
|
||||
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<Point> 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;
|
||||
}
|
||||
|
||||
/// <summary>获取当前 ROI 的外接矩形(用于掩码计算范围)</summary>
|
||||
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
|
||||
/// <summary>魔棒:在点击位置做 flood fill</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,21 @@ namespace XP.ImageProcessing.RoiControl.Models
|
||||
/// <summary>一次点点距测量的所有视觉元素</summary>
|
||||
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; }
|
||||
|
||||
/// <summary>垂直线段长度(像素)</summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>更新两条垂直线段的位置</summary>
|
||||
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)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 845 B |
@@ -44,6 +44,7 @@ namespace XplorePlane.ViewModels
|
||||
}
|
||||
|
||||
private string _statusMessage = "就绪";
|
||||
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
@@ -54,6 +55,7 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
// 导航命令
|
||||
public DelegateCommand NavigateHomeCommand { get; set; }
|
||||
|
||||
public DelegateCommand NavigateInspectCommand { get; set; }
|
||||
public DelegateCommand OpenFileCommand { get; set; }
|
||||
public DelegateCommand ExportCommand { get; set; }
|
||||
@@ -62,6 +64,7 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
// 窗口打开命令
|
||||
public DelegateCommand OpenImageProcessingCommand { get; }
|
||||
|
||||
public DelegateCommand LoadImageCommand { get; }
|
||||
public DelegateCommand OpenPipelineEditorCommand { get; }
|
||||
public DelegateCommand OpenCncEditorCommand { get; }
|
||||
@@ -91,6 +94,7 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
// 硬件命令
|
||||
public DelegateCommand AxisResetCommand { get; }
|
||||
|
||||
public DelegateCommand OpenDoorCommand { get; }
|
||||
public DelegateCommand CloseDoorCommand { get; }
|
||||
public DelegateCommand OpenDetectorConfigCommand { get; }
|
||||
@@ -101,10 +105,10 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
// 测量命令
|
||||
public DelegateCommand PointDistanceMeasureCommand { get; }
|
||||
|
||||
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; }
|
||||
@@ -114,6 +118,7 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
// 设置命令
|
||||
public DelegateCommand OpenLanguageSwitcherCommand { get; }
|
||||
|
||||
public DelegateCommand OpenRealTimeLogViewerCommand { get; }
|
||||
public DelegateCommand UseLiveDetectorSourceCommand { get; }
|
||||
|
||||
@@ -183,6 +188,7 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
// 窗口引用(单例窗口防止重复打开)
|
||||
private Window _motionDebugWindow;
|
||||
|
||||
private Window _detectorConfigWindow;
|
||||
private Window _plcAddrConfigWindow;
|
||||
private Window _realTimeLogViewerWindow;
|
||||
@@ -250,9 +256,8 @@ namespace XplorePlane.ViewModels
|
||||
ClearCommand = new DelegateCommand(OnClear);
|
||||
EditPropertiesCommand = new DelegateCommand(OnEditProperties);
|
||||
|
||||
|
||||
LoadImageCommand = new DelegateCommand(ExecuteLoadImage);
|
||||
|
||||
|
||||
OpenCncEditorCommand = new DelegateCommand(ExecuteOpenCncEditor);
|
||||
OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编辑"));
|
||||
OpenToolboxCommand = new DelegateCommand(ExecuteOpenToolbox);
|
||||
@@ -288,7 +293,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 +326,14 @@ 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;
|
||||
@@ -753,7 +765,6 @@ namespace XplorePlane.ViewModels
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private void ExecutePointDistanceMeasure()
|
||||
{
|
||||
if (!CheckImageLoaded()) return;
|
||||
@@ -782,31 +793,6 @@ namespace XplorePlane.ViewModels
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.ThroughHoleFillRate);
|
||||
}
|
||||
|
||||
private Window _bgaMeasurePanel;
|
||||
|
||||
private void ExecuteBgaVoidMeasure()
|
||||
{
|
||||
if (!CheckImageLoaded()) return;
|
||||
_logger.Info("BGA void measurement triggered.");
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.BgaVoid);
|
||||
|
||||
if (_bgaMeasurePanel != null && _bgaMeasurePanel.IsVisible)
|
||||
{
|
||||
_bgaMeasurePanel.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
_bgaMeasurePanel = new Views.ImageProcessing.BgaMeasurePanel
|
||||
{
|
||||
Owner = System.Windows.Application.Current.MainWindow
|
||||
};
|
||||
_bgaMeasurePanel.Closed += (s, e) =>
|
||||
{
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.None);
|
||||
};
|
||||
_bgaMeasurePanel.Show();
|
||||
}
|
||||
|
||||
private Window _bgaDetectionPanel;
|
||||
|
||||
private void ExecuteBgaDetection()
|
||||
@@ -874,7 +860,6 @@ namespace XplorePlane.ViewModels
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.None);
|
||||
};
|
||||
_bubbleMeasurePanel.Show();
|
||||
|
||||
}
|
||||
|
||||
private void ExecuteOpenLanguageSwitcher()
|
||||
@@ -956,8 +941,9 @@ namespace XplorePlane.ViewModels
|
||||
_logger.Info("[Image] OnPipelinePreviewUpdated received a pipeline preview image and pushed it to MainViewportService.");
|
||||
_mainViewportService.SetManualImage(payload.Image, string.Empty);
|
||||
}
|
||||
|
||||
public sealed record BuiltInInspectionModuleItem(string DisplayName, string FilePath);
|
||||
|
||||
#endregion
|
||||
#endregion 测量命令实现
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
<Window
|
||||
x:Class="XplorePlane.Views.ImageProcessing.BgaMeasurePanel"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="BGA空隙测量"
|
||||
Width="280" Height="280"
|
||||
ResizeMode="NoResize"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Topmost="True" ShowInTaskbar="False"
|
||||
Background="#F5F5F5" FontFamily="Microsoft YaHei UI">
|
||||
<Window.Resources>
|
||||
<Style x:Key="IconBtnStyle" TargetType="ButtonBase">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ButtonBase">
|
||||
<Border x:Name="Bd" Background="#FFFFFF" BorderBrush="#E0E0E0"
|
||||
BorderThickness="1" CornerRadius="6" Padding="10,6">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#EAF2FB" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#B0D4F1" />
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#005FB8" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#004C99" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</DataTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="CardStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="BorderBrush" Value="#E8E8E8" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="Padding" Value="12,10" />
|
||||
<Setter Property="Margin" Value="0,0,0,8" />
|
||||
</Style>
|
||||
<Style x:Key="ParamLabel" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Foreground" Value="#555" />
|
||||
<Setter Property="Margin" Value="0,0,0,3" />
|
||||
</Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="BorderBrush" Value="#D0D0D0" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="4,3" />
|
||||
<Setter Property="FontSize" Value="11.5" />
|
||||
<Style.Resources>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
</Style>
|
||||
</Style.Resources>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
<StackPanel Margin="10">
|
||||
<!-- 工具栏 -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<RadioButton x:Name="RbVoid" IsChecked="True" Style="{StaticResource IconBtnStyle}" ToolTip="画气泡" Margin="0,0,4,0">
|
||||
<Image Source="/Assets/Icons/bubble.png" Width="20" Height="20" />
|
||||
</RadioButton>
|
||||
<RadioButton x:Name="RbBall" Style="{StaticResource IconBtnStyle}" ToolTip="画焊球" Margin="0,0,10,0">
|
||||
<TextBlock Text="焊球" FontSize="11" VerticalAlignment="Center" />
|
||||
</RadioButton>
|
||||
<Button Style="{StaticResource IconBtnStyle}" ToolTip="完成" Click="Finish_Click">
|
||||
<Image Source="/Assets/Icons/ok.png" Width="20" Height="20" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 提示 -->
|
||||
<TextBlock Text="左键点两次画圆(圆心+半径),右键删除" FontSize="10" Foreground="#999" Margin="0,0,0,8" />
|
||||
|
||||
<!-- 参数卡片 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="VoidLimit(%)" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel>
|
||||
<TextBox x:Name="TbVoidLimit" DockPanel.Dock="Right" Width="50" Text="25.0"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider x:Name="SliderVoidLimit" Minimum="0" Maximum="100" Value="25"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 结果卡片 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<TextBlock x:Name="TbResult" Text="空隙率: --" FontSize="13" FontWeight="SemiBold" Foreground="#333" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
@@ -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<PolygonRoiCanvas>(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<T>(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<T>(child);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
<StackPanel Margin="10">
|
||||
<!-- 工具栏:工具选择 + 撤销/清除/完成 -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<RadioButton x:Name="RbRoi" IsChecked="True" Style="{StaticResource IconBtnStyle}" ToolTip="ROI" Margin="0,0,4,0">
|
||||
<RadioButton x:Name="RbRoi" IsChecked="True" Style="{StaticResource IconBtnStyle}" ToolTip="矩形ROI" Margin="0,0,4,0">
|
||||
<Image Source="/Assets/Icons/rectangle32.png" Width="20" Height="20" />
|
||||
</RadioButton>
|
||||
<RadioButton x:Name="RbRoiCircle" Style="{StaticResource IconBtnStyle}" ToolTip="圆形ROI" Margin="0,0,4,0">
|
||||
<Image Source="/Assets/Icons/circle32.png" Width="20" Height="20" />
|
||||
</RadioButton>
|
||||
<RadioButton x:Name="RbRoiPolygon" Style="{StaticResource IconBtnStyle}" ToolTip="多边形ROI" Margin="0,0,4,0">
|
||||
<Image Source="/Assets/Icons/polygon.png" Width="20" Height="20" />
|
||||
</RadioButton>
|
||||
<RadioButton x:Name="RbWand" Style="{StaticResource IconBtnStyle}" ToolTip="魔棒" Margin="0,0,4,0">
|
||||
<Image Source="/Assets/Icons/magic32.png" Width="20" Height="20" />
|
||||
</RadioButton>
|
||||
|
||||
@@ -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<PolygonRoiCanvas>(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 方式)──
|
||||
|
||||
/// <summary>清除所有类型的气泡 ROI(矩形/圆形/多边形)</summary>
|
||||
private void ClearAllBubbleRoi()
|
||||
{
|
||||
CleanupPolyRoi();
|
||||
_canvas?.ClearBubbleMeasure();
|
||||
// 确保多边形点也被清空
|
||||
_canvas?.SetBubblePolyPoints(new System.Collections.Generic.List<Point>());
|
||||
TbResult.Text = "空隙率: --";
|
||||
}
|
||||
|
||||
private void EnablePolyRoi()
|
||||
{
|
||||
if (_canvas == null) return;
|
||||
if (_polyRoiShape != null) return; // 已有
|
||||
|
||||
if (_canvas.ROIItems == null)
|
||||
_canvas.ROIItems = new ObservableCollection<ROIShape>();
|
||||
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>将 PolygonROI 的点同步到 canvas 内部的 _bubblePolyPoints</summary>
|
||||
private void SyncPolyToCanvas()
|
||||
{
|
||||
if (_canvas == null || _polyRoiShape == null) return;
|
||||
_canvas.SetBubblePolyPoints(_polyRoiShape.Points);
|
||||
}
|
||||
|
||||
/// <summary>切换到魔棒/画笔时,锁定多边形 ROI 不可编辑</summary>
|
||||
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<Point> 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<T>(DependencyObject parent) where T : DependencyObject
|
||||
{
|
||||
int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent);
|
||||
|
||||
@@ -238,15 +238,8 @@
|
||||
Text="通孔填锡率" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 第三列: BGA空隙测量 + 气泡测量 -->
|
||||
<!-- 第三列: 气泡测量 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="BGA焊球空隙率检测"
|
||||
telerik:ScreenTip.Title="BGA空隙测量"
|
||||
Command="{Binding BgaVoidMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/bga.png"
|
||||
Text="BGA空隙" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="手动气泡测量(魔棒+画笔)"
|
||||
telerik:ScreenTip.Title="气泡测量"
|
||||
@@ -614,7 +607,7 @@
|
||||
FontFamily="Consolas"
|
||||
FontSize="11"
|
||||
Foreground="White"
|
||||
Text="x: 0 y: 0" />
|
||||
Text="{Binding CursorInfoText}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
@@ -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<MainViewModel>(); } 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)
|
||||
|
||||
Reference in New Issue
Block a user