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(); } 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(); ea?.GetEvent().Subscribe(() => { RoiCanvas.ShowCrosshair = !RoiCanvas.ShowCrosshair; }, Prism.Events.ThreadOption.UIThread); // 测量模式:直接订阅 Prism 事件 ea?.GetEvent().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(); ea2?.GetEvent().Subscribe(() => { ToggleLineProfile(); }, Prism.Events.ThreadOption.UIThread); // 白底检测:进入ROI绘制模式 ea2?.GetEvent().Subscribe(() => { _bgDefectDrawing = false; _bgDefectRoiMode = BackgroundDefectRoiMode.WhiteBackground; RegisterBackgroundDefectRoiMouseHandlers(); }, Prism.Events.ThreadOption.UIThread); // 黑底检测:进入ROI绘制模式 ea2?.GetEvent().Subscribe(() => { _bgDefectDrawing = false; _bgDefectRoiMode = BackgroundDefectRoiMode.BlackBackground; RegisterBackgroundDefectRoiMouseHandlers(); }, Prism.Events.ThreadOption.UIThread); // 白底检测:渲染结果(红色标识) ea2?.GetEvent().Subscribe(payload => { RenderBackgroundDefectResult(payload.RoiRect, payload.Detections, isBlackBackground: false); }, Prism.Events.ThreadOption.UIThread); // 黑底检测:渲染结果(绿色标识) ea2?.GetEvent().Subscribe(payload => { RenderBackgroundDefectResult(payload.RoiRect, payload.Detections, isBlackBackground: true); }, Prism.Events.ThreadOption.UIThread); ea2?.GetEvent().Subscribe(() => { _bgDefectDrawing = false; _bgDefectRoiMode = BackgroundDefectRoiMode.TemplateAssistant; RegisterBackgroundDefectRoiMouseHandlers(); SetStatus("模板助手:请在图像上拖拽框选模板区域"); }, Prism.Events.ThreadOption.UIThread); ea2?.GetEvent().Subscribe(payload => { RenderTemplateMatchPreview(payload); }, Prism.Events.ThreadOption.UIThread); ea2?.GetEvent().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(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(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(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(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 _bgDefectOverlays = new(); private readonly System.Collections.Generic.List _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(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(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(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(); var rect = new System.Windows.Int32Rect(x, y, w, h); if (completedMode == BackgroundDefectRoiMode.WhiteBackground) ea?.GetEvent().Publish(rect); else if (completedMode == BackgroundDefectRoiMode.BlackBackground) ea?.GetEvent().Publish(rect); else if (completedMode == BackgroundDefectRoiMode.TemplateAssistant) ea?.GetEvent().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(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 detections, bool isBlackBackground) { var canvas = FindChildByName(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(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(child, name); if (result != null) return result; } return null; } } }