From 7c06cd2def453e8815a086c890a1d24138576438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Tue, 26 May 2026 11:22:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EQFN=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E5=8F=8A=E6=8C=89=E9=92=AE=EF=BC=8C=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E4=B8=89=E4=B8=AA=E6=A8=A1=E5=9D=97ROI=E9=A2=9C?= =?UTF-8?q?=E8=89=B2=E4=B8=BACyan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ImageProcessing/BgaDetectionViewModel.cs | 2 +- .../QfnLeadPadDetectionViewModel.cs | 549 ++++++++++++++++++ XplorePlane/ViewModels/Main/MainViewModel.cs | 22 + .../QfnLeadPadDetectionPanel.xaml | 226 +++++++ .../QfnLeadPadDetectionPanel.xaml.cs | 59 ++ XplorePlane/Views/Main/MainWindow.xaml | 11 +- 6 files changed, 866 insertions(+), 3 deletions(-) create mode 100644 XplorePlane/ViewModels/ImageProcessing/QfnLeadPadDetectionViewModel.cs create mode 100644 XplorePlane/Views/ImageProcessing/QfnLeadPadDetectionPanel.xaml create mode 100644 XplorePlane/Views/ImageProcessing/QfnLeadPadDetectionPanel.xaml.cs diff --git a/XplorePlane/ViewModels/ImageProcessing/BgaDetectionViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/BgaDetectionViewModel.cs index f30ea24..691e805 100644 --- a/XplorePlane/ViewModels/ImageProcessing/BgaDetectionViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/BgaDetectionViewModel.cs @@ -121,7 +121,7 @@ namespace XplorePlane.ViewModels.ImageProcessing if (_canvas.ROIItems == null) _canvas.ROIItems = new ObservableCollection(); - _roiShape = new PolygonROI { Color = "Red", IsSelected = true }; + _roiShape = new PolygonROI { Color = "Cyan", IsSelected = true }; _canvas.ROIItems.Add(_roiShape); _canvas.SelectedROI = _roiShape; diff --git a/XplorePlane/ViewModels/ImageProcessing/QfnLeadPadDetectionViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/QfnLeadPadDetectionViewModel.cs new file mode 100644 index 0000000..b3b2dd8 --- /dev/null +++ b/XplorePlane/ViewModels/ImageProcessing/QfnLeadPadDetectionViewModel.cs @@ -0,0 +1,549 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using Emgu.CV.Util; +using Prism.Commands; +using Prism.Mvvm; +using XP.ImageProcessing.Processors; +using XP.ImageProcessing.RoiControl.Controls; +using XP.ImageProcessing.RoiControl.Models; +using XplorePlane.Models; +using XplorePlane.Services.MainViewport; +using XplorePlane.ViewModels.Cnc; + +namespace XplorePlane.ViewModels.ImageProcessing +{ + public class QfnLeadPadDetectionViewModel : BindableBase + { + private readonly IMainViewportService _viewportService; + private CncEditorViewModel _cncEditorViewModel; + private BitmapSource _originalImage; + private System.Threading.CancellationTokenSource _debounceCts; + private const int DebounceMs = 300; + private const string QfnLeadPadOperatorKey = "QfnLeadPadVoid"; + + public QfnLeadPadDetectionViewModel(IMainViewportService viewportService) + { + _viewportService = viewportService; + ExecuteCommand = new DelegateCommand(Execute); + InsertToCncCommand = new DelegateCommand(ExecuteInsertToCnc); + PropertyChanged += OnAnyPropertyChanged; + } + + public void SetCncEditorViewModel(CncEditorViewModel cncEditorViewModel) + { + _cncEditorViewModel = cncEditorViewModel; + } + + private void OnAnyPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ResultText) || e.PropertyName == nameof(ResultImage) || e.PropertyName == nameof(RoiEnabled)) + return; + TriggerDebouncedExecution(); + } + + private void TriggerDebouncedExecution() + { + _debounceCts?.Cancel(); + _debounceCts = new System.Threading.CancellationTokenSource(); + var token = _debounceCts.Token; + System.Threading.Tasks.Task.Delay(DebounceMs, token).ContinueWith(t => + { + if (!t.IsCanceled) Execute(); + }, System.Threading.Tasks.TaskScheduler.FromCurrentSynchronizationContext()); + } + + // ── 引脚定位参数 ── + private int _padBlurSize = 5; + public int PadBlurSize { get => _padBlurSize; set => SetProperty(ref _padBlurSize, value); } + + private int _padThresholdLow = 0; + public int PadThresholdLow { get => _padThresholdLow; set => SetProperty(ref _padThresholdLow, value); } + + private int _padThresholdHigh = 120; + public int PadThresholdHigh { get => _padThresholdHigh; set => SetProperty(ref _padThresholdHigh, value); } + + private int _padMorphKernel = 5; + public int PadMorphKernel { get => _padMorphKernel; set => SetProperty(ref _padMorphKernel, value); } + + private int _minPadArea = 200; + public int MinPadArea { get => _minPadArea; set => SetProperty(ref _minPadArea, value); } + + private int _maxPadArea = 100000; + public int MaxPadArea { get => _maxPadArea; set => SetProperty(ref _maxPadArea, value); } + + private double _padAspectRatioMin = 1.2; + public double PadAspectRatioMin { get => _padAspectRatioMin; set => SetProperty(ref _padAspectRatioMin, value); } + + // ── 空洞检测参数 ── + private int _voidThresholdLow = 128; + public int VoidThresholdLow { get => _voidThresholdLow; set => SetProperty(ref _voidThresholdLow, value); } + + private int _voidThresholdHigh = 255; + public int VoidThresholdHigh { get => _voidThresholdHigh; set => SetProperty(ref _voidThresholdHigh, value); } + + private int _minVoidArea = 5; + public int MinVoidArea { get => _minVoidArea; set => SetProperty(ref _minVoidArea, value); } + + private int _voidMergeRadius = 2; + public int VoidMergeRadius { get => _voidMergeRadius; set => SetProperty(ref _voidMergeRadius, value); } + + private double _voidRateLimit = 50.0; + public double VoidRateLimit { get => _voidRateLimit; set => SetProperty(ref _voidRateLimit, value); } + + private int _minQualifiedPadArea = 1000; + public int MinQualifiedPadArea { get => _minQualifiedPadArea; set => SetProperty(ref _minQualifiedPadArea, value); } + + // ── ROI ── + private bool _roiEnabled; + public bool RoiEnabled + { + get => _roiEnabled; + set + { + if (SetProperty(ref _roiEnabled, value)) + OnRoiEnabledChanged(); + } + } + + private PolygonRoiCanvas _canvas; + private PolygonROI _roiShape; + private System.Windows.Controls.Image _resultOverlayImage; + + public void SetCanvas(PolygonRoiCanvas canvas) => _canvas = canvas; + + private void OnRoiEnabledChanged() + { + if (_canvas == null) return; + if (RoiEnabled) + { + if (_canvas.ROIItems == null) + _canvas.ROIItems = new ObservableCollection(); + _roiShape = new PolygonROI { Color = "Cyan", IsSelected = true }; + _canvas.ROIItems.Add(_roiShape); + _canvas.SelectedROI = _roiShape; + _roiShape.Points.CollectionChanged += (s, e) => + { + if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add || + e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) + { + _canvas.SelectedROI = null; + _canvas.SelectedROI = _roiShape; + } + }; + _canvas.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, false); + _canvas.AddHandler(PolygonRoiCanvas.CanvasClickedEvent, new RoutedEventHandler(OnCanvasClickedForRoi)); + } + else + { + CleanupRoi(); + } + } + + private void OnCanvasClickedForRoi(object sender, RoutedEventArgs e) + { + if (!RoiEnabled || _roiShape == null) return; + if (e is CanvasClickedEventArgs args) + { + InsertPointToPolygon(args.Position, _roiShape.Points); + _roiShape.IsSelected = true; + _canvas.SelectedROI = _roiShape; + } + } + + public void CleanupRoi() + { + if (_canvas != null) + { + _canvas.RemoveHandler(PolygonRoiCanvas.CanvasClickedEvent, new RoutedEventHandler(OnCanvasClickedForRoi)); + _canvas.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, true); + } + if (_roiShape != null && _canvas?.ROIItems != null) + { + _canvas.ROIItems.Remove(_roiShape); + _canvas.SelectedROI = null; + _roiShape = null; + } + } + + public void RestoreContextMenu() + { + if (_canvas != null) + { + _canvas.RemoveHandler(PolygonRoiCanvas.CanvasClickedEvent, new RoutedEventHandler(OnCanvasClickedForRoi)); + _canvas.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, true); + if (_roiShape != null) + { + _roiShape.IsSelected = false; + _roiShape.IsEditable = false; + } + _canvas.SelectedROI = null; + } + } + + // ── 结果 ── + private string _resultText = "结果: --"; + public string ResultText { get => _resultText; set => SetProperty(ref _resultText, value); } + + private BitmapSource _resultImage; + public BitmapSource ResultImage { get => _resultImage; set => SetProperty(ref _resultImage, value); } + + public ObservableCollection Results { get; } = new(); + + public DelegateCommand ExecuteCommand { get; } + public DelegateCommand InsertToCncCommand { get; } + + private void Execute() + { + if (_originalImage == null) + _originalImage = _viewportService?.CurrentDisplayImage as BitmapSource; + var image = _originalImage; + if (image == null) { ResultText = "请先加载图像"; return; } + + try + { + var processor = new QfnLeadPadVoidProcessor(); + processor.SetParameter("PadBlurSize", PadBlurSize); + processor.SetParameter("PadThresholdLow", PadThresholdLow); + processor.SetParameter("PadThresholdHigh", PadThresholdHigh); + processor.SetParameter("PadMorphKernel", PadMorphKernel); + processor.SetParameter("MinPadArea", MinPadArea); + processor.SetParameter("MaxPadArea", MaxPadArea); + processor.SetParameter("PadAspectRatioMin", PadAspectRatioMin); + processor.SetParameter("VoidThresholdLow", VoidThresholdLow); + processor.SetParameter("VoidThresholdHigh", VoidThresholdHigh); + processor.SetParameter("MinVoidArea", MinVoidArea); + processor.SetParameter("VoidMergeRadius", VoidMergeRadius); + processor.SetParameter("VoidRateLimit", VoidRateLimit); + processor.SetParameter("MinQualifiedPadArea", MinQualifiedPadArea); + + // ROI 注入 + if (RoiEnabled && _roiShape != null && _roiShape.Points.Count >= 3) + { + processor.SetParameter("RoiMode", "Polygon"); + int count = Math.Min(_roiShape.Points.Count, 32); + processor.SetParameter("PolyCount", count); + for (int i = 0; i < count; i++) + { + processor.SetParameter($"PolyX{i}", (int)_roiShape.Points[i].X); + processor.SetParameter($"PolyY{i}", (int)_roiShape.Points[i].Y); + } + } + + var grayImage = BitmapSourceToGray(image); + processor.Process(grayImage); + var output = processor.OutputData; + + ResultText = output.ContainsKey("ResultText") + ? output["ResultText"]?.ToString() ?? "--" + : "未检测到引脚"; + + // 填充结果表格 + Results.Clear(); + if (output.ContainsKey("LeadPads")) + { + var pads = output["LeadPads"] as List; + if (pads != null) + { + foreach (var pad in pads) + { + Results.Add(new QfnLeadPadResultItem + { + Index = pad.Index, + CenterX = pad.CenterX.ToString("F1"), + CenterY = pad.CenterY.ToString("F1"), + PadArea = pad.PadArea.ToString(), + VoidRate = $"{pad.VoidRate:F1}%", + VoidCount = pad.Voids.Count, + Classification = pad.Classification + }); + } + } + } + + ResultImage = RenderResults(grayImage, output); + ShowResultOnOverlay(ResultImage); + grayImage.Dispose(); + } + catch (Exception ex) + { + ResultText = $"错误: {ex.Message}"; + } + } + + private void ExecuteInsertToCnc() + { + if (_cncEditorViewModel == null) + { + MessageBox.Show("CNC 编辑器未就绪,无法插入参数。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); + return; + } + + var selectedNode = _cncEditorViewModel.SelectedNode; + CncNodeViewModel targetModuleNode = null; + + if (selectedNode == null) + { + MessageBox.Show("请先在 CNC 编辑器中选择一个位置节点或检测模块。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); + return; + } + + if (selectedNode.IsInspectionModule) + targetModuleNode = selectedNode; + else if (selectedNode.IsSavePosition) + targetModuleNode = selectedNode.Children.FirstOrDefault(c => c.IsInspectionModule); + else + { + var allNodes = _cncEditorViewModel.Nodes; + CncNodeViewModel ownerPosition = null; + foreach (var node in allNodes) + { + if (node.IsSavePosition) ownerPosition = node; + if (node.Id == selectedNode.Id) break; + } + if (ownerPosition != null) + targetModuleNode = ownerPosition.Children.FirstOrDefault(c => c.IsInspectionModule); + } + + if (targetModuleNode == null) + { + MessageBox.Show("未找到激活的检测模块节点。\n请在 CNC 编辑器中选择一个包含检测模块的位置节点。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); + return; + } + + var pipeline = targetModuleNode.Pipeline ?? new PipelineModel { Name = targetModuleNode.Name }; + + var qfnNode = pipeline.Nodes.FirstOrDefault(n => + string.Equals(n.OperatorKey, QfnLeadPadOperatorKey, StringComparison.OrdinalIgnoreCase)); + + if (qfnNode == null) + { + qfnNode = new PipelineNodeModel + { + Id = Guid.NewGuid(), + OperatorKey = QfnLeadPadOperatorKey, + Order = pipeline.Nodes.Count, + IsEnabled = true, + Parameters = new Dictionary() + }; + pipeline.Nodes.Add(qfnNode); + } + + var parameters = qfnNode.Parameters; + parameters["PadBlurSize"] = PadBlurSize; + parameters["PadThresholdLow"] = PadThresholdLow; + parameters["PadThresholdHigh"] = PadThresholdHigh; + parameters["PadMorphKernel"] = PadMorphKernel; + parameters["MinPadArea"] = MinPadArea; + parameters["MaxPadArea"] = MaxPadArea; + parameters["PadAspectRatioMin"] = PadAspectRatioMin; + parameters["VoidThresholdLow"] = VoidThresholdLow; + parameters["VoidThresholdHigh"] = VoidThresholdHigh; + parameters["MinVoidArea"] = MinVoidArea; + parameters["VoidMergeRadius"] = VoidMergeRadius; + parameters["VoidRateLimit"] = VoidRateLimit; + parameters["MinQualifiedPadArea"] = MinQualifiedPadArea; + + if (RoiEnabled && _roiShape != null && _roiShape.Points.Count >= 3) + { + parameters["RoiMode"] = "Polygon"; + int count = Math.Min(_roiShape.Points.Count, 32); + parameters["PolyCount"] = count; + for (int i = 0; i < count; i++) + { + parameters[$"PolyX{i}"] = (int)_roiShape.Points[i].X; + parameters[$"PolyY{i}"] = (int)_roiShape.Points[i].Y; + } + } + else + { + parameters["RoiMode"] = "None"; + parameters["PolyCount"] = 0; + } + + pipeline.UpdatedAt = DateTime.UtcNow; + targetModuleNode.Pipeline = pipeline; + + _cncEditorViewModel.SelectedNode = null; + _cncEditorViewModel.SelectedNode = targetModuleNode; + + MessageBox.Show( + $"已将QFN引脚检测参数插入到检测模块「{targetModuleNode.Name}」。", + "插入成功", MessageBoxButton.OK, MessageBoxImage.Information); + } + + private void ShowResultOnOverlay(BitmapSource resultBmp) + { + if (_canvas == null) return; + RemoveResultOverlay(); + if (resultBmp == null) return; + + _resultOverlayImage = new System.Windows.Controls.Image + { + Source = resultBmp, + IsHitTestVisible = false, + Stretch = System.Windows.Media.Stretch.Fill + }; + _resultOverlayImage.SetBinding(System.Windows.FrameworkElement.WidthProperty, + new System.Windows.Data.Binding("CanvasWidth") { Source = _canvas }); + _resultOverlayImage.SetBinding(System.Windows.FrameworkElement.HeightProperty, + new System.Windows.Data.Binding("CanvasHeight") { Source = _canvas }); + + var mainCanvas = _canvas.FindName("mainCanvas") as System.Windows.Controls.Canvas; + if (mainCanvas != null) + { + int insertIndex = Math.Min(1, mainCanvas.Children.Count); + mainCanvas.Children.Insert(insertIndex, _resultOverlayImage); + } + } + + public void RemoveResultOverlay() + { + if (_resultOverlayImage == null || _canvas == null) return; + _canvas.RemoveFromCanvas(_resultOverlayImage); + _resultOverlayImage = null; + } + + private BitmapSource RenderResults(Image grayImage, IDictionary output) + { + if (!output.ContainsKey("QfnLeadResult")) return null; + int leadCount = (int)output["LeadCount"]; + if (leadCount == 0) return null; + + double voidRate = (double)output["VoidRate"]; + double voidRateLimitVal = (double)output["VoidRateLimit"]; + string classification = (string)output["Classification"]; + var pads = output["LeadPads"] as List; + + var colorImage = new Image(grayImage.Width, grayImage.Height); + CvInvoke.CvtColor(grayImage, colorImage, ColorConversion.Gray2Bgr); + + if (pads != null && pads.Count > 0) + { + // 半透明引脚填充 + var overlay = colorImage.Clone(); + foreach (var pad in pads) + { + if (pad.ContourPoints.Length > 0) + { + var padColor = pad.Classification == "PASS" + ? new MCvScalar(0, 200, 0) // 绿色 + : new MCvScalar(0, 0, 220); // 红色 + using var vop = new VectorOfPoint(pad.ContourPoints); + using var vvop = new VectorOfVectorOfPoint(vop); + CvInvoke.DrawContours(overlay, vvop, 0, padColor, -1); + } + } + CvInvoke.AddWeighted(overlay, 0.3, colorImage, 0.7, 0, colorImage); + overlay.Dispose(); + + // 绘制引脚轮廓 + 编号 + 空洞率 + foreach (var pad in pads) + { + var contourColor = pad.Classification == "PASS" + ? new MCvScalar(0, 255, 0) + : new MCvScalar(0, 0, 255); + + if (pad.ContourPoints.Length > 0) + { + using var vop = new VectorOfPoint(pad.ContourPoints); + using var vvop = new VectorOfVectorOfPoint(vop); + CvInvoke.DrawContours(colorImage, vvop, 0, contourColor, 2); + } + + // 绘制空洞轮廓 + foreach (var v in pad.Voids) + { + if (v.ContourPoints.Length > 0) + { + using var vop = new VectorOfPoint(v.ContourPoints); + using var vvop = new VectorOfVectorOfPoint(vop); + CvInvoke.DrawContours(colorImage, vvop, 0, new MCvScalar(0, 255, 255), 1); + } + } + + // 引脚编号和空洞率 + CvInvoke.PutText(colorImage, $"#{pad.Index} {pad.VoidRate:F1}%", + new System.Drawing.Point((int)pad.CenterX - 20, (int)pad.CenterY + 5), + FontFace.HersheySimplex, 0.35, contourColor, 1); + } + + // 左上角总览 + var overallColor = classification == "PASS" + ? new MCvScalar(0, 255, 0) : new MCvScalar(0, 0, 255); + CvInvoke.PutText(colorImage, + $"QFN Lead: {voidRate:F1}% | Limit: {voidRateLimitVal:F0}% | {leadCount} pads | {classification}", + new System.Drawing.Point(10, 25), + FontFace.HersheySimplex, 0.5, overallColor, 2); + } + + using var bitmap = colorImage.ToBitmap(); + var bmpSrc = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap( + bitmap.GetHbitmap(), IntPtr.Zero, System.Windows.Int32Rect.Empty, + BitmapSizeOptions.FromEmptyOptions()); + bmpSrc.Freeze(); + colorImage.Dispose(); + return bmpSrc; + } + + private static void InsertPointToPolygon(Point newPoint, ObservableCollection points) + { + if (points.Count < 2) { points.Add(newPoint); return; } + int insertIndex = 0; + double minDistance = double.MaxValue; + double d = PointToSegmentDistance(points[points.Count - 1], points[0], newPoint); + if (d < minDistance) { minDistance = d; insertIndex = 0; } + for (int i = 1; i < points.Count; i++) + { + d = PointToSegmentDistance(points[i - 1], points[i], newPoint); + if (d < minDistance) { minDistance = d; insertIndex = i; } + } + points.Insert(insertIndex, newPoint); + } + + private static double PointToSegmentDistance(Point a, Point b, Point p) + { + double dx = b.X - a.X, dy = b.Y - a.Y; + double lenSq = dx * dx + dy * dy; + if (lenSq < 1e-10) return Math.Sqrt((p.X - a.X) * (p.X - a.X) + (p.Y - a.Y) * (p.Y - a.Y)); + double t = Math.Clamp(((p.X - a.X) * dx + (p.Y - a.Y) * dy) / lenSq, 0, 1); + double projX = a.X + t * dx, projY = a.Y + t * dy; + return Math.Sqrt((p.X - projX) * (p.X - projX) + (p.Y - projY) * (p.Y - projY)); + } + + private static Image BitmapSourceToGray(BitmapSource bmp) + { + var converted = new FormatConvertedBitmap(bmp, PixelFormats.Bgra32, null, 0); + int w = converted.PixelWidth, h = converted.PixelHeight; + int stride = w * 4; + var pixels = new byte[stride * h]; + converted.CopyPixels(pixels, stride, 0); + var gray = new Image(w, h); + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + int idx = y * stride + x * 4; + gray.Data[y, x, 0] = (byte)(pixels[idx + 2] * 0.299 + pixels[idx + 1] * 0.587 + pixels[idx] * 0.114); + } + return gray; + } + } + + public class QfnLeadPadResultItem + { + public int Index { get; set; } + public string CenterX { get; set; } = ""; + public string CenterY { get; set; } = ""; + public string PadArea { get; set; } = ""; + public string VoidRate { get; set; } = ""; + public int VoidCount { get; set; } + public string Classification { get; set; } = ""; + } +} diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index b2648b8..cb537a3 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -171,6 +171,7 @@ namespace XplorePlane.ViewModels public DelegateCommand ThroughHoleFillRateMeasureCommand { get; } public DelegateCommand BgaDetectionCommand { get; } public DelegateCommand VoidDetectionCommand { get; } + public DelegateCommand QfnLeadPadDetectionCommand { get; } public DelegateCommand BubbleMeasureCommand { get; } private bool _isScaleBarVisible; @@ -402,6 +403,7 @@ namespace XplorePlane.ViewModels ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure); BgaDetectionCommand = new DelegateCommand(ExecuteBgaDetection); VoidDetectionCommand = new DelegateCommand(ExecuteVoidDetection); + QfnLeadPadDetectionCommand = new DelegateCommand(ExecuteQfnLeadPadDetection); BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure); // 辅助线命令 @@ -1106,6 +1108,26 @@ namespace XplorePlane.ViewModels _voidDetectionPanel.Show(); } + private Window _qfnLeadPadDetectionPanel; + + private void ExecuteQfnLeadPadDetection() + { + if (!CheckImageLoaded()) return; + _logger.Info("QFN引脚空洞检测功能已触发"); + + if (_qfnLeadPadDetectionPanel != null && _qfnLeadPadDetectionPanel.IsVisible) + { + _qfnLeadPadDetectionPanel.Activate(); + return; + } + + _qfnLeadPadDetectionPanel = new Views.ImageProcessing.QfnLeadPadDetectionPanel + { + Owner = System.Windows.Application.Current.MainWindow + }; + _qfnLeadPadDetectionPanel.Show(); + } + private Window _bubbleMeasurePanel; private void ExecuteBubbleMeasure() diff --git a/XplorePlane/Views/ImageProcessing/QfnLeadPadDetectionPanel.xaml b/XplorePlane/Views/ImageProcessing/QfnLeadPadDetectionPanel.xaml new file mode 100644 index 0000000..31177f7 --- /dev/null +++ b/XplorePlane/Views/ImageProcessing/QfnLeadPadDetectionPanel.xaml @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XplorePlane/Views/ImageProcessing/QfnLeadPadDetectionPanel.xaml.cs b/XplorePlane/Views/ImageProcessing/QfnLeadPadDetectionPanel.xaml.cs new file mode 100644 index 0000000..6931fe4 --- /dev/null +++ b/XplorePlane/Views/ImageProcessing/QfnLeadPadDetectionPanel.xaml.cs @@ -0,0 +1,59 @@ +using System.Windows; +using Prism.Ioc; +using XP.ImageProcessing.RoiControl.Controls; +using XplorePlane.Services.MainViewport; +using XplorePlane.ViewModels.Cnc; +using XplorePlane.ViewModels.ImageProcessing; + +namespace XplorePlane.Views.ImageProcessing +{ + public partial class QfnLeadPadDetectionPanel : Window + { + public QfnLeadPadDetectionPanel() + { + InitializeComponent(); + var viewportService = ContainerLocator.Current?.Resolve(); + DataContext = new QfnLeadPadDetectionViewModel(viewportService); + + Loaded += (s, e) => + { + var mainWin = Owner as MainWindow; + if (mainWin != null) + { + var canvas = FindChild(mainWin); + if (DataContext is QfnLeadPadDetectionViewModel vm) + vm.SetCanvas(canvas); + } + + if (DataContext is QfnLeadPadDetectionViewModel qfnVm && Owner?.DataContext is ViewModels.MainViewModel mainVm) + { + var cncEditorField = mainVm.GetType().GetField("_cncEditorViewModel", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (cncEditorField?.GetValue(mainVm) is CncEditorViewModel cncEditor) + qfnVm.SetCncEditorViewModel(cncEditor); + } + }; + + Closed += (s, e) => + { + if (DataContext is QfnLeadPadDetectionViewModel vm) + vm.RestoreContextMenu(); + }; + } + + private void Close_Click(object sender, RoutedEventArgs e) => Close(); + + private static T FindChild(DependencyObject parent) where T : DependencyObject + { + int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < count; i++) + { + var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, i); + if (child is T t) return t; + var result = FindChild(child); + if (result != null) return result; + } + return null; + } + } +} diff --git a/XplorePlane/Views/Main/MainWindow.xaml b/XplorePlane/Views/Main/MainWindow.xaml index f4c2ca3..9b30a1c 100644 --- a/XplorePlane/Views/Main/MainWindow.xaml +++ b/XplorePlane/Views/Main/MainWindow.xaml @@ -181,11 +181,11 @@ + Text="线灰度" /> +