14 Commits

Author SHA1 Message Date
LI Wei.lw c80d3e2037 已合并 PR 64: 修改手动测量相关工具等
1.气泡测量工具:修改鼠标光标 - 魔棒/画笔/橡皮工具选中时显示对应的系统光标(魔棒=Cross,画笔=Pen,橡皮=No)
2.气泡测量工具:增加圆形ROI - 圆形 ROI 支持创建、拖动移动、边缘拖动调整大小
3.点点距测量工具:端点由点改为线段 - 将测量点标记从圆形改为垂直于连线的线段
4.气泡测量工具:打开面板时先清除ROIItems里的残留 - 修复残留数据问题
5.圆形ROI图标 - 添加圆形 ROI 工具图标
6.状态栏右下角显示图像像素坐标和灰度 - 显示当前鼠标位置的 X/Y 坐标和灰度值
7.手动测量工具:控制点改为2*2、线宽改为1 - 统一调整测量工具的视觉样式
8.删除BGA手动测量工具 - 移除手动 BGA 测量功能
2026-05-07 14:27:28 +08:00
李伟 cee89e4db0 删除BGA测量相关代码 2026-05-07 14:10:04 +08:00
李伟 1d8db8fa2d 解决合并冲突:合并TURBO-596分支 2026-05-07 13:51:59 +08:00
李伟 8d7fb4e0e3 气泡测量工具:修改鼠标光标 2026-05-06 16:16:22 +08:00
李伟 6fc53c56c7 气泡测量工具:增加圆形ROI 2026-05-06 14:01:56 +08:00
李伟 8dbc274e63 点点距测量工具:端点由点改为线段 2026-05-06 13:28:25 +08:00
李伟 7846445b33 气泡测量工具:打开面板时先清除ROIItems里的残留 2026-05-06 09:10:35 +08:00
李伟 611b2ae147 圆形ROI图标 2026-04-30 16:51:26 +08:00
李伟 6b35da4cc0 状态栏右下角显示图像像素坐标和灰度 2026-04-30 15:52:01 +08:00
李伟 4d5fa04920 手动测量工具:控制点改为2*2、线宽改为1 2026-04-30 14:01:56 +08:00
李伟 2663bda0ae 删除BGA手动测量工具 2026-04-30 13:46:18 +08:00
LI Wei.lw d5cdab294b 已合并 PR 59: 更改气泡筛选逻辑
1.加载新图像时会自动清除旧的测量结果、叠加层和ROI
2.更改气泡筛选逻辑:按照面积筛选,去除小于设定面积的气泡重新计算空隙率更新结果图像
2026-04-30 13:24:08 +08:00
李伟 64c22fc088 加载新图像时会自动清除旧的测量结果、叠加层和ROI 2026-04-30 09:25:22 +08:00
李伟 0b56010536 更改气泡筛选逻辑:按照面积筛选,去除小于设定面积的气泡重新计算空隙率更新结果图像 2026-04-30 09:24:34 +08:00
12 changed files with 690 additions and 276 deletions
@@ -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

+19 -33
View File
@@ -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);
+2 -9
View File
@@ -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)