e0eec42a2f
- 新增模板助手/批量测试窗口、事件与 ViewModel,主窗口入口与 App 注册 - 运行匹配或批量测试前发布清除事件;视口通过 VisualTreeHelper 从父 Panel 移除持久虚线 ROI,避免 FindChild 失败时框残留 - TemplateMatchNative 等相关调整 Co-authored-by: Cursor <cursoragent@cursor.com>
799 lines
33 KiB
C#
799 lines
33 KiB
C#
using System;
|
|
using System.ComponentModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Imaging;
|
|
using Microsoft.Win32;
|
|
using Prism.Ioc;
|
|
using XP.ImageProcessing.RoiControl.Controls;
|
|
using XplorePlane.Events;
|
|
using XplorePlane.ViewModels;
|
|
|
|
namespace XplorePlane.Views
|
|
{
|
|
public partial class ViewportPanelView : UserControl
|
|
{
|
|
private MainViewModel _mainVm;
|
|
|
|
private MainViewModel GetMainVm()
|
|
{
|
|
if (_mainVm == null)
|
|
{
|
|
try { _mainVm = ContainerLocator.Current?.Resolve<MainViewModel>(); } catch { }
|
|
}
|
|
return _mainVm;
|
|
}
|
|
|
|
private void SetStatus(string msg)
|
|
{
|
|
var vm = GetMainVm();
|
|
if (vm != null) vm.StatusMessage = msg;
|
|
}
|
|
|
|
public ViewportPanelView()
|
|
{
|
|
InitializeComponent();
|
|
DataContextChanged += OnDataContextChanged;
|
|
|
|
// 动态创建右键菜单,支持条件性阻止弹出
|
|
var menu = new System.Windows.Controls.ContextMenu();
|
|
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "放大" });
|
|
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "缩小" });
|
|
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "适应窗口" });
|
|
menu.Items.Add(new System.Windows.Controls.Separator());
|
|
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "保存原始图像" });
|
|
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "保存结果图像" });
|
|
menu.Items.Add(new System.Windows.Controls.Separator());
|
|
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "清除所有测量" });
|
|
((System.Windows.Controls.MenuItem)menu.Items[0]).Click += ZoomIn_Click;
|
|
((System.Windows.Controls.MenuItem)menu.Items[1]).Click += ZoomOut_Click;
|
|
((System.Windows.Controls.MenuItem)menu.Items[2]).Click += ResetView_Click;
|
|
((System.Windows.Controls.MenuItem)menu.Items[4]).Click += SaveOriginalImage_Click;
|
|
((System.Windows.Controls.MenuItem)menu.Items[5]).Click += SaveResultImage_Click;
|
|
((System.Windows.Controls.MenuItem)menu.Items[7]).Click += ClearAllMeasurements_Click;
|
|
RoiCanvas.ContextMenu = menu;
|
|
RoiCanvas.ContextMenuOpening += (s, e) =>
|
|
{
|
|
if (RoiCanvas.SuppressContextMenu)
|
|
{
|
|
RoiCanvas.SuppressContextMenu = false;
|
|
e.Handled = true;
|
|
}
|
|
};
|
|
|
|
// 测量事件 → 更新主界面状态栏
|
|
RoiCanvas.MeasureCompleted += (s, e) =>
|
|
{
|
|
if (e is MeasureCompletedEventArgs args)
|
|
{
|
|
string typeLabel = args.MeasureType switch
|
|
{
|
|
"PointToLine" => "点线距",
|
|
"Angle" => "角度",
|
|
"FillRate" => "填锡率",
|
|
"BgaVoid" => "BGA空隙",
|
|
"BubbleVoid" => "气泡空隙",
|
|
_ => "点点距"
|
|
};
|
|
string valueText = args.MeasureType switch
|
|
{
|
|
"Angle" => $"{args.Distance:F2}°",
|
|
"FillRate" => $"{args.Distance:F1}%",
|
|
"BgaVoid" => $"{args.Distance:F1}%",
|
|
"BubbleVoid" => $"{args.Distance:F1}%",
|
|
_ => $"{args.Distance:F2} px"
|
|
};
|
|
SetStatus($"{typeLabel}: {valueText} | 共 {args.TotalCount} 条测量");
|
|
}
|
|
};
|
|
RoiCanvas.MeasureStatusChanged += (s, e) =>
|
|
{
|
|
if (e is MeasureStatusEventArgs args)
|
|
SetStatus(args.Message);
|
|
};
|
|
|
|
// 十字辅助线:直接订阅 Prism 事件
|
|
try
|
|
{
|
|
var ea = ContainerLocator.Current?.Resolve<Prism.Events.IEventAggregator>();
|
|
ea?.GetEvent<ToggleCrosshairEvent>().Subscribe(() =>
|
|
{
|
|
RoiCanvas.ShowCrosshair = !RoiCanvas.ShowCrosshair;
|
|
}, Prism.Events.ThreadOption.UIThread);
|
|
|
|
// 测量模式:直接订阅 Prism 事件
|
|
ea?.GetEvent<MeasurementToolEvent>().Subscribe(mode =>
|
|
{
|
|
RoiCanvas.CurrentMeasureMode = mode switch
|
|
{
|
|
MeasurementToolMode.PointDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointDistance,
|
|
MeasurementToolMode.PointLineDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointToLine,
|
|
MeasurementToolMode.Angle => XP.ImageProcessing.RoiControl.Models.MeasureMode.Angle,
|
|
MeasurementToolMode.ThroughHoleFillRate => XP.ImageProcessing.RoiControl.Models.MeasureMode.FillRate,
|
|
MeasurementToolMode.BgaVoid => XP.ImageProcessing.RoiControl.Models.MeasureMode.BgaVoid,
|
|
MeasurementToolMode.BubbleMeasure => XP.ImageProcessing.RoiControl.Models.MeasureMode.BubbleMeasure,
|
|
_ => XP.ImageProcessing.RoiControl.Models.MeasureMode.None
|
|
};
|
|
}, 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;
|
|
});
|
|
|
|
// 行灰度分布
|
|
try
|
|
{
|
|
var ea2 = ContainerLocator.Current?.Resolve<Prism.Events.IEventAggregator>();
|
|
ea2?.GetEvent<ToggleLineProfileEvent>().Subscribe(() =>
|
|
{
|
|
ToggleLineProfile();
|
|
}, Prism.Events.ThreadOption.UIThread);
|
|
|
|
// 白底检测:进入ROI绘制模式
|
|
ea2?.GetEvent<WhiteBackgroundDetectionEvent>().Subscribe(() =>
|
|
{
|
|
_bgDefectDrawing = false;
|
|
_bgDefectRoiMode = BackgroundDefectRoiMode.WhiteBackground;
|
|
RegisterBackgroundDefectRoiMouseHandlers();
|
|
}, Prism.Events.ThreadOption.UIThread);
|
|
|
|
// 黑底检测:进入ROI绘制模式
|
|
ea2?.GetEvent<BlackBackgroundDetectionEvent>().Subscribe(() =>
|
|
{
|
|
_bgDefectDrawing = false;
|
|
_bgDefectRoiMode = BackgroundDefectRoiMode.BlackBackground;
|
|
RegisterBackgroundDefectRoiMouseHandlers();
|
|
}, Prism.Events.ThreadOption.UIThread);
|
|
|
|
// 白底检测:渲染结果(红色标识)
|
|
ea2?.GetEvent<WhiteBackgroundResultEvent>().Subscribe(payload =>
|
|
{
|
|
RenderBackgroundDefectResult(payload.RoiRect, payload.Detections, isBlackBackground: false);
|
|
}, Prism.Events.ThreadOption.UIThread);
|
|
|
|
// 黑底检测:渲染结果(绿色标识)
|
|
ea2?.GetEvent<BlackBackgroundResultEvent>().Subscribe(payload =>
|
|
{
|
|
RenderBackgroundDefectResult(payload.RoiRect, payload.Detections, isBlackBackground: true);
|
|
}, Prism.Events.ThreadOption.UIThread);
|
|
|
|
ea2?.GetEvent<TemplateMatchEnterRoiModeEvent>().Subscribe(() =>
|
|
{
|
|
_bgDefectDrawing = false;
|
|
_bgDefectRoiMode = BackgroundDefectRoiMode.TemplateAssistant;
|
|
RegisterBackgroundDefectRoiMouseHandlers();
|
|
SetStatus("模板助手:请在图像上拖拽框选模板区域");
|
|
}, Prism.Events.ThreadOption.UIThread);
|
|
|
|
ea2?.GetEvent<TemplateMatchPreviewResultEvent>().Subscribe(payload =>
|
|
{
|
|
RenderTemplateMatchPreview(payload);
|
|
}, Prism.Events.ThreadOption.UIThread);
|
|
|
|
ea2?.GetEvent<TemplateMatchClearRoiOverlayEvent>().Subscribe(() =>
|
|
{
|
|
RemoveTemplateAssistantPersistRoi();
|
|
}, Prism.Events.ThreadOption.UIThread);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
#region 行灰度分布
|
|
|
|
private bool _lineProfileEnabled;
|
|
private System.Windows.Shapes.Line _profileRefLine; // 透明命中区域
|
|
private System.Windows.Shapes.Line _profileRefLineVisible; // 1px红线显示
|
|
private System.Windows.Shapes.Polyline _profileCurve;
|
|
private double _profileLineY;
|
|
private bool _profileDragging;
|
|
|
|
private void ToggleLineProfile()
|
|
{
|
|
_lineProfileEnabled = !_lineProfileEnabled;
|
|
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
|
if (canvas == null) return;
|
|
|
|
if (_lineProfileEnabled)
|
|
{
|
|
// 参考线默认在图像中间
|
|
_profileLineY = RoiCanvas.CanvasHeight / 2;
|
|
|
|
// 创建参考线(红色水平线,可拖动)
|
|
// 用透明粗线作为命中区域,叠加1px红线显示
|
|
_profileRefLine = new System.Windows.Shapes.Line
|
|
{
|
|
X1 = 0,
|
|
Y1 = _profileLineY,
|
|
X2 = RoiCanvas.CanvasWidth,
|
|
Y2 = _profileLineY,
|
|
Stroke = System.Windows.Media.Brushes.Transparent,
|
|
StrokeThickness = 7, // 上下3px命中区域
|
|
IsHitTestVisible = true,
|
|
Cursor = System.Windows.Input.Cursors.SizeNS
|
|
};
|
|
_profileRefLineVisible = new System.Windows.Shapes.Line
|
|
{
|
|
X1 = 0,
|
|
Y1 = _profileLineY,
|
|
X2 = RoiCanvas.CanvasWidth,
|
|
Y2 = _profileLineY,
|
|
Stroke = System.Windows.Media.Brushes.Red,
|
|
StrokeThickness = 1,
|
|
IsHitTestVisible = false
|
|
};
|
|
_profileRefLine.MouseLeftButtonDown += ProfileLine_MouseDown;
|
|
_profileRefLine.MouseMove += ProfileLine_MouseMove;
|
|
_profileRefLine.MouseLeftButtonUp += ProfileLine_MouseUp;
|
|
canvas.Children.Add(_profileRefLineVisible);
|
|
canvas.Children.Add(_profileRefLine);
|
|
|
|
// 创建灰度折线(固定显示在图像中间位置)
|
|
_profileCurve = new System.Windows.Shapes.Polyline
|
|
{
|
|
Stroke = System.Windows.Media.Brushes.Red,
|
|
StrokeThickness = 1,
|
|
IsHitTestVisible = false
|
|
};
|
|
canvas.Children.Add(_profileCurve);
|
|
|
|
UpdateLineProfile();
|
|
SetStatus("行灰度分布:拖动红线改变采样行,再次点击按钮关闭");
|
|
}
|
|
else
|
|
{
|
|
if (_profileRefLine != null)
|
|
{
|
|
canvas.Children.Remove(_profileRefLine);
|
|
_profileRefLine = null;
|
|
}
|
|
if (_profileRefLineVisible != null)
|
|
{
|
|
canvas.Children.Remove(_profileRefLineVisible);
|
|
_profileRefLineVisible = null;
|
|
}
|
|
if (_profileCurve != null)
|
|
{
|
|
canvas.Children.Remove(_profileCurve);
|
|
_profileCurve = null;
|
|
}
|
|
SetStatus("行灰度分布已关闭");
|
|
}
|
|
}
|
|
|
|
private void ProfileLine_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|
{
|
|
_profileDragging = true;
|
|
_profileRefLine?.CaptureMouse();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void ProfileLine_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
|
|
{
|
|
if (!_profileDragging || _profileRefLine == null) return;
|
|
|
|
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
|
if (canvas == null) return;
|
|
|
|
var pos = e.GetPosition(canvas);
|
|
_profileLineY = Math.Clamp(pos.Y, 0, RoiCanvas.CanvasHeight - 1);
|
|
|
|
_profileRefLine.Y1 = _profileLineY;
|
|
_profileRefLine.Y2 = _profileLineY;
|
|
_profileRefLineVisible.Y1 = _profileLineY;
|
|
_profileRefLineVisible.Y2 = _profileLineY;
|
|
|
|
UpdateLineProfile();
|
|
}
|
|
|
|
private void ProfileLine_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|
{
|
|
_profileDragging = false;
|
|
_profileRefLine?.ReleaseMouseCapture();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void UpdateLineProfile()
|
|
{
|
|
if (_profileCurve == null) return;
|
|
|
|
// 从当前显示图像获取像素数据
|
|
var viewportVm = DataContext as ViewportPanelViewModel;
|
|
var imageSource = viewportVm?.ImageSource as System.Windows.Media.Imaging.BitmapSource;
|
|
if (imageSource == null) return;
|
|
|
|
int imgWidth = imageSource.PixelWidth;
|
|
int imgHeight = imageSource.PixelHeight;
|
|
int row = (int)Math.Clamp(_profileLineY, 0, imgHeight - 1);
|
|
|
|
// 转为 Gray8 获取行像素
|
|
System.Windows.Media.Imaging.BitmapSource gray8;
|
|
if (imageSource.Format != System.Windows.Media.PixelFormats.Gray8)
|
|
gray8 = new System.Windows.Media.Imaging.FormatConvertedBitmap(
|
|
imageSource, System.Windows.Media.PixelFormats.Gray8, null, 0);
|
|
else
|
|
gray8 = imageSource;
|
|
|
|
byte[] rowPixels = new byte[imgWidth];
|
|
int stride = imgWidth;
|
|
gray8.CopyPixels(new System.Windows.Int32Rect(0, row, imgWidth, 1), rowPixels, stride, 0);
|
|
|
|
// 构建折线点集:折线固定显示在图像垂直中间位置
|
|
// 参考线位置决定采样哪一行,折线位置固定在画布中间
|
|
double canvasH = RoiCanvas.CanvasHeight;
|
|
double curveCenter = canvasH / 2.0; // 折线基线固定在图像中间
|
|
double displayHeight = canvasH * 0.25; // 折线振幅为画布高度的25%
|
|
|
|
var points = new System.Windows.Media.PointCollection(imgWidth);
|
|
for (int x = 0; x < imgWidth; x++)
|
|
{
|
|
double normalizedGray = rowPixels[x] / 255.0;
|
|
double y = curveCenter - normalizedGray * displayHeight;
|
|
points.Add(new System.Windows.Point(x, y));
|
|
}
|
|
_profileCurve.Points = points;
|
|
|
|
SetStatus($"行灰度分布 | Y={row} | 均值={rowPixels.Select(b => (double)b).Average():F1} | 最大={rowPixels.Max()} | 最小={rowPixels.Min()}");
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (e.OldValue is INotifyPropertyChanged oldVm)
|
|
oldVm.PropertyChanged -= OnVmPropertyChanged;
|
|
if (e.NewValue is INotifyPropertyChanged newVm)
|
|
newVm.PropertyChanged += OnVmPropertyChanged;
|
|
}
|
|
|
|
private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
|
|
{
|
|
// 测量模式和十字线通过 Prism 事件直接驱动,不再依赖 PropertyChanged
|
|
}
|
|
|
|
#region 右键菜单
|
|
|
|
private void ZoomIn_Click(object sender, RoutedEventArgs e) => RoiCanvas.ZoomScale = Math.Min(10.0, RoiCanvas.ZoomScale * 1.2);
|
|
private void ZoomOut_Click(object sender, RoutedEventArgs e) => RoiCanvas.ZoomScale = Math.Max(0.1, RoiCanvas.ZoomScale / 1.2);
|
|
private void ResetView_Click(object sender, RoutedEventArgs e) => RoiCanvas.ResetView();
|
|
|
|
private void ClearAllMeasurements_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
RoiCanvas.ClearMeasurements();
|
|
RoiCanvas.ROIItems?.Clear();
|
|
RoiCanvas.SelectedROI = null;
|
|
if (DataContext is ViewportPanelViewModel vm)
|
|
vm.ResetMeasurementState();
|
|
|
|
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
|
if (canvas != null)
|
|
{
|
|
if (_bgDefectPreview != null)
|
|
{
|
|
canvas.Children.Remove(_bgDefectPreview);
|
|
_bgDefectPreview = null;
|
|
}
|
|
ClearBackgroundDefectOverlays(canvas);
|
|
ClearTemplateMatchOverlays(canvas);
|
|
RemoveTemplateAssistantPersistRoi();
|
|
}
|
|
else
|
|
{
|
|
_bgDefectOverlays.Clear();
|
|
_tmMatchOverlays.Clear();
|
|
RemoveTemplateAssistantPersistRoi();
|
|
}
|
|
|
|
_bgDefectDrawing = false;
|
|
_bgDefectRoiMode = BackgroundDefectRoiMode.None;
|
|
try { RoiCanvas.ReleaseMouseCapture(); } catch { /* 未捕获则无影响 */ }
|
|
|
|
SetStatus("已清除所有测量、白底/黑底检测、模板匹配试跑叠加及模板助手 ROI");
|
|
}
|
|
|
|
private void SaveOriginalImage_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is not ViewportPanelViewModel vm || vm.ImageSource is not BitmapSource bitmap)
|
|
{
|
|
MessageBox.Show("No image available to save", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
SaveBitmapToFile(bitmap, "保存原始图像");
|
|
}
|
|
|
|
private void SaveResultImage_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var target = FindChildByName<Canvas>(RoiCanvas, "mainCanvas");
|
|
if (target == null)
|
|
{
|
|
MessageBox.Show("No image available to save", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
var width = (int)target.ActualWidth;
|
|
var height = (int)target.ActualHeight;
|
|
if (width == 0 || height == 0) return;
|
|
|
|
var rtb = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
|
|
rtb.Render(target);
|
|
SaveBitmapToFile(rtb, "保存结果图像");
|
|
}
|
|
|
|
private static void SaveBitmapToFile(BitmapSource bitmap, string title)
|
|
{
|
|
var dialog = new SaveFileDialog
|
|
{
|
|
Title = title,
|
|
Filter = "PNG 图像|*.png|BMP 图像|*.bmp|JPEG 图像|*.jpg",
|
|
DefaultExt = ".png"
|
|
};
|
|
if (dialog.ShowDialog() != true) return;
|
|
|
|
BitmapEncoder encoder = Path.GetExtension(dialog.FileName).ToLower() switch
|
|
{
|
|
".bmp" => new BmpBitmapEncoder(),
|
|
".jpg" or ".jpeg" => new JpegBitmapEncoder(),
|
|
_ => new PngBitmapEncoder()
|
|
};
|
|
encoder.Frames.Add(BitmapFrame.Create(bitmap));
|
|
using var fs = new FileStream(dialog.FileName, FileMode.Create);
|
|
encoder.Save(fs);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 白底/黑底检测
|
|
|
|
private enum BackgroundDefectRoiMode
|
|
{
|
|
None,
|
|
WhiteBackground,
|
|
BlackBackground,
|
|
TemplateAssistant
|
|
}
|
|
|
|
private BackgroundDefectRoiMode _bgDefectRoiMode;
|
|
private bool _bgDefectDrawing;
|
|
private System.Windows.Point _bgDefectStart;
|
|
private System.Windows.Shapes.Rectangle _bgDefectPreview;
|
|
private readonly System.Collections.Generic.List<System.Windows.UIElement> _bgDefectOverlays = new();
|
|
private readonly System.Collections.Generic.List<System.Windows.UIElement> _tmMatchOverlays = new();
|
|
private System.Windows.Shapes.Rectangle _templateAssistantRoiPersist;
|
|
private bool _bgDefectMouseHandlersRegistered;
|
|
|
|
private void RegisterBackgroundDefectRoiMouseHandlers()
|
|
{
|
|
if (_bgDefectMouseHandlersRegistered) return;
|
|
RoiCanvas.PreviewMouseLeftButtonDown += OnMainCanvasPreviewMouseDown;
|
|
RoiCanvas.PreviewMouseMove += OnMainCanvasPreviewMouseMove;
|
|
RoiCanvas.PreviewMouseLeftButtonUp += OnMainCanvasPreviewMouseUp;
|
|
_bgDefectMouseHandlersRegistered = true;
|
|
}
|
|
|
|
// 需要在 mainCanvas 的 MouseDown/Move/Up 中处理
|
|
// 由于 PolygonRoiCanvas 内部已经处理了鼠标事件,我们通过 PreviewMouse 事件来拦截
|
|
|
|
private void OnMainCanvasPreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|
{
|
|
if (_bgDefectRoiMode == BackgroundDefectRoiMode.None || e.LeftButton != System.Windows.Input.MouseButtonState.Pressed) return;
|
|
|
|
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
|
if (canvas == null) return;
|
|
|
|
_bgDefectStart = e.GetPosition(canvas);
|
|
_bgDefectDrawing = true;
|
|
|
|
// 创建预览矩形(不清除之前的检测结果)
|
|
_bgDefectPreview = new System.Windows.Shapes.Rectangle
|
|
{
|
|
Stroke = System.Windows.Media.Brushes.Blue,
|
|
StrokeThickness = 1,
|
|
StrokeDashArray = new System.Windows.Media.DoubleCollection { 4, 2 }
|
|
};
|
|
System.Windows.Controls.Canvas.SetLeft(_bgDefectPreview, _bgDefectStart.X);
|
|
System.Windows.Controls.Canvas.SetTop(_bgDefectPreview, _bgDefectStart.Y);
|
|
canvas.Children.Add(_bgDefectPreview);
|
|
|
|
RoiCanvas.CaptureMouse();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void OnMainCanvasPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
|
|
{
|
|
if (!_bgDefectDrawing || _bgDefectPreview == null) return;
|
|
|
|
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
|
if (canvas == null) return;
|
|
|
|
var current = e.GetPosition(canvas);
|
|
double x = Math.Min(_bgDefectStart.X, current.X);
|
|
double y = Math.Min(_bgDefectStart.Y, current.Y);
|
|
double w = Math.Abs(current.X - _bgDefectStart.X);
|
|
double h = Math.Abs(current.Y - _bgDefectStart.Y);
|
|
|
|
System.Windows.Controls.Canvas.SetLeft(_bgDefectPreview, x);
|
|
System.Windows.Controls.Canvas.SetTop(_bgDefectPreview, y);
|
|
_bgDefectPreview.Width = Math.Max(1, w);
|
|
_bgDefectPreview.Height = Math.Max(1, h);
|
|
}
|
|
|
|
private void OnMainCanvasPreviewMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|
{
|
|
if (!_bgDefectDrawing) return;
|
|
|
|
_bgDefectDrawing = false;
|
|
var completedMode = _bgDefectRoiMode;
|
|
_bgDefectRoiMode = BackgroundDefectRoiMode.None;
|
|
RoiCanvas.ReleaseMouseCapture();
|
|
|
|
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
|
if (canvas == null) return;
|
|
|
|
var end = e.GetPosition(canvas);
|
|
int x = (int)Math.Min(_bgDefectStart.X, end.X);
|
|
int y = (int)Math.Min(_bgDefectStart.Y, end.Y);
|
|
int w = (int)Math.Abs(end.X - _bgDefectStart.X);
|
|
int h = (int)Math.Abs(end.Y - _bgDefectStart.Y);
|
|
|
|
// 移除预览矩形
|
|
if (_bgDefectPreview != null)
|
|
{
|
|
canvas.Children.Remove(_bgDefectPreview);
|
|
_bgDefectPreview = null;
|
|
}
|
|
|
|
if (w < 10 || h < 10) return; // 太小忽略
|
|
|
|
// 模板助手:在画布上保留 ROI 矩形(与试跑匹配叠加分开管理)
|
|
if (completedMode == BackgroundDefectRoiMode.TemplateAssistant)
|
|
{
|
|
RemoveTemplateAssistantPersistRoi();
|
|
_templateAssistantRoiPersist = new System.Windows.Shapes.Rectangle
|
|
{
|
|
Stroke = System.Windows.Media.Brushes.DeepSkyBlue,
|
|
StrokeThickness = 1.5,
|
|
StrokeDashArray = new System.Windows.Media.DoubleCollection { 4, 2 },
|
|
Fill = System.Windows.Media.Brushes.Transparent,
|
|
Width = Math.Max(1, w),
|
|
Height = Math.Max(1, h),
|
|
IsHitTestVisible = false
|
|
};
|
|
System.Windows.Controls.Canvas.SetLeft(_templateAssistantRoiPersist, x);
|
|
System.Windows.Controls.Canvas.SetTop(_templateAssistantRoiPersist, y);
|
|
canvas.Children.Add(_templateAssistantRoiPersist);
|
|
}
|
|
|
|
// 发布ROI绘制完成事件
|
|
try
|
|
{
|
|
var ea = ContainerLocator.Current?.Resolve<Prism.Events.IEventAggregator>();
|
|
var rect = new System.Windows.Int32Rect(x, y, w, h);
|
|
if (completedMode == BackgroundDefectRoiMode.WhiteBackground)
|
|
ea?.GetEvent<WhiteBackgroundRoiDrawnEvent>().Publish(rect);
|
|
else if (completedMode == BackgroundDefectRoiMode.BlackBackground)
|
|
ea?.GetEvent<BlackBackgroundRoiDrawnEvent>().Publish(rect);
|
|
else if (completedMode == BackgroundDefectRoiMode.TemplateAssistant)
|
|
ea?.GetEvent<TemplateMatchRoiDrawnEvent>().Publish(rect);
|
|
}
|
|
catch { }
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void ClearTemplateMatchOverlays(System.Windows.Controls.Canvas canvas)
|
|
{
|
|
if (canvas != null)
|
|
{
|
|
foreach (var el in _tmMatchOverlays)
|
|
canvas.Children.Remove(el);
|
|
}
|
|
_tmMatchOverlays.Clear();
|
|
}
|
|
|
|
private void RemoveTemplateAssistantPersistRoi()
|
|
{
|
|
if (_templateAssistantRoiPersist == null) return;
|
|
var rect = _templateAssistantRoiPersist;
|
|
_templateAssistantRoiPersist = null;
|
|
if (VisualTreeHelper.GetParent(rect) is Panel p)
|
|
p.Children.Remove(rect);
|
|
}
|
|
|
|
private void RenderTemplateMatchPreview(TemplateMatchPreviewPayload payload)
|
|
{
|
|
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
|
if (canvas == null) return;
|
|
|
|
ClearTemplateMatchOverlays(canvas);
|
|
if (payload?.Hits == null || payload.Hits.Count == 0)
|
|
return;
|
|
|
|
var stroke = new SolidColorBrush(Color.FromRgb(255, 140, 0));
|
|
stroke.Freeze();
|
|
const int crossHalf = 8;
|
|
|
|
foreach (var h in payload.Hits)
|
|
{
|
|
var poly = new System.Windows.Shapes.Polygon
|
|
{
|
|
Stroke = stroke,
|
|
StrokeThickness = 2,
|
|
Fill = Brushes.Transparent,
|
|
IsHitTestVisible = false,
|
|
Points = new PointCollection
|
|
{
|
|
new System.Windows.Point(h.LtX, h.LtY),
|
|
new System.Windows.Point(h.RtX, h.RtY),
|
|
new System.Windows.Point(h.RbX, h.RbY),
|
|
new System.Windows.Point(h.LbX, h.LbY)
|
|
}
|
|
};
|
|
canvas.Children.Add(poly);
|
|
_tmMatchOverlays.Add(poly);
|
|
|
|
var cx = h.CenterX;
|
|
var cy = h.CenterY;
|
|
var hLine = new System.Windows.Shapes.Line
|
|
{
|
|
X1 = cx - crossHalf,
|
|
Y1 = cy,
|
|
X2 = cx + crossHalf,
|
|
Y2 = cy,
|
|
Stroke = stroke,
|
|
StrokeThickness = 1.5,
|
|
IsHitTestVisible = false
|
|
};
|
|
var vLine = new System.Windows.Shapes.Line
|
|
{
|
|
X1 = cx,
|
|
Y1 = cy - crossHalf,
|
|
X2 = cx,
|
|
Y2 = cy + crossHalf,
|
|
Stroke = stroke,
|
|
StrokeThickness = 1.5,
|
|
IsHitTestVisible = false
|
|
};
|
|
canvas.Children.Add(hLine);
|
|
canvas.Children.Add(vLine);
|
|
_tmMatchOverlays.Add(hLine);
|
|
_tmMatchOverlays.Add(vLine);
|
|
|
|
var tb = new System.Windows.Controls.TextBlock
|
|
{
|
|
Text = $"{h.Score:F2}",
|
|
Foreground = stroke,
|
|
FontSize = 10,
|
|
IsHitTestVisible = false
|
|
};
|
|
System.Windows.Controls.Canvas.SetLeft(tb, cx + crossHalf + 2);
|
|
System.Windows.Controls.Canvas.SetTop(tb, cy - 8);
|
|
canvas.Children.Add(tb);
|
|
_tmMatchOverlays.Add(tb);
|
|
}
|
|
}
|
|
|
|
private void RenderBackgroundDefectResult(
|
|
System.Drawing.Rectangle roiRect,
|
|
System.Collections.Generic.IReadOnlyList<BackgroundDefectDetectionItem> detections,
|
|
bool isBlackBackground)
|
|
{
|
|
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
|
if (canvas == null || detections == null) return;
|
|
|
|
// 绘制ROI矩形(蓝色实线,两种模式一致)
|
|
var roiShape = new System.Windows.Shapes.Rectangle
|
|
{
|
|
Stroke = System.Windows.Media.Brushes.Blue,
|
|
StrokeThickness = 1,
|
|
Width = roiRect.Width,
|
|
Height = roiRect.Height,
|
|
IsHitTestVisible = false
|
|
};
|
|
System.Windows.Controls.Canvas.SetLeft(roiShape, roiRect.X);
|
|
System.Windows.Controls.Canvas.SetTop(roiShape, roiRect.Y);
|
|
canvas.Children.Add(roiShape);
|
|
_bgDefectOverlays.Add(roiShape);
|
|
|
|
var defectBrush = isBlackBackground
|
|
? System.Windows.Media.Brushes.LimeGreen
|
|
: System.Windows.Media.Brushes.Red;
|
|
|
|
const int labelPadRightOfRoi = 4;
|
|
const double labelLineHeight = 15;
|
|
int validCount = detections.Count(d => d.Contour != null && d.Contour.Count >= 2);
|
|
double roiMidY = roiRect.Y + roiRect.Height * 0.5;
|
|
double labelLeft = roiRect.X + roiRect.Width + labelPadRightOfRoi;
|
|
double labelStartY = roiMidY - validCount * labelLineHeight * 0.5;
|
|
int labelRow = 0;
|
|
|
|
foreach (var d in detections)
|
|
{
|
|
if (d.Contour == null || d.Contour.Count < 2) continue;
|
|
|
|
var fig = new PathFigure
|
|
{
|
|
StartPoint = new System.Windows.Point(d.Contour[0].X, d.Contour[0].Y),
|
|
IsClosed = true
|
|
};
|
|
if (d.Contour.Count > 1)
|
|
{
|
|
fig.Segments.Add(new PolyLineSegment(
|
|
d.Contour.Skip(1).Select(p => new System.Windows.Point(p.X, p.Y)), true));
|
|
}
|
|
|
|
var geom = new PathGeometry();
|
|
geom.Figures.Add(fig);
|
|
var contourPath = new System.Windows.Shapes.Path
|
|
{
|
|
Data = geom,
|
|
Stroke = defectBrush,
|
|
StrokeThickness = 1,
|
|
Fill = System.Windows.Media.Brushes.Transparent,
|
|
IsHitTestVisible = false
|
|
};
|
|
canvas.Children.Add(contourPath);
|
|
_bgDefectOverlays.Add(contourPath);
|
|
|
|
var chordLine = new System.Windows.Shapes.Line
|
|
{
|
|
X1 = d.ChordP1.X,
|
|
Y1 = d.ChordP1.Y,
|
|
X2 = d.ChordP2.X,
|
|
Y2 = d.ChordP2.Y,
|
|
Stroke = defectBrush,
|
|
StrokeThickness = 1.5,
|
|
IsHitTestVisible = false
|
|
};
|
|
canvas.Children.Add(chordLine);
|
|
_bgDefectOverlays.Add(chordLine);
|
|
|
|
double um = d.SizeMicrometers;
|
|
string label = um >= 1000 ? $"{um / 1000:F2} mm" : $"{um:F0} μm";
|
|
|
|
var text = new System.Windows.Controls.TextBlock
|
|
{
|
|
Text = label,
|
|
Foreground = defectBrush,
|
|
FontSize = 11,
|
|
IsHitTestVisible = false
|
|
};
|
|
System.Windows.Controls.Canvas.SetLeft(text, labelLeft);
|
|
System.Windows.Controls.Canvas.SetTop(text, labelStartY + labelRow * labelLineHeight);
|
|
canvas.Children.Add(text);
|
|
_bgDefectOverlays.Add(text);
|
|
labelRow++;
|
|
}
|
|
}
|
|
|
|
private void ClearBackgroundDefectOverlays(System.Windows.Controls.Canvas canvas)
|
|
{
|
|
foreach (var el in _bgDefectOverlays)
|
|
canvas.Children.Remove(el);
|
|
_bgDefectOverlays.Clear();
|
|
}
|
|
|
|
#endregion
|
|
|
|
private static T FindChildByName<T>(DependencyObject parent, string name) where T : FrameworkElement
|
|
{
|
|
int count = VisualTreeHelper.GetChildrenCount(parent);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var child = VisualTreeHelper.GetChild(parent, i);
|
|
if (child is T t && t.Name == name) return t;
|
|
var result = FindChildByName<T>(child, name);
|
|
if (result != null) return result;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|