新增QFN检测面板及按钮,统一三个模块ROI颜色为Cyan
This commit is contained in:
@@ -121,7 +121,7 @@ namespace XplorePlane.ViewModels.ImageProcessing
|
||||
if (_canvas.ROIItems == null)
|
||||
_canvas.ROIItems = new ObservableCollection<ROIShape>();
|
||||
|
||||
_roiShape = new PolygonROI { Color = "Red", IsSelected = true };
|
||||
_roiShape = new PolygonROI { Color = "Cyan", IsSelected = true };
|
||||
_canvas.ROIItems.Add(_roiShape);
|
||||
_canvas.SelectedROI = _roiShape;
|
||||
|
||||
|
||||
@@ -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>();
|
||||
_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<QfnLeadPadResultItem> 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<QfnLeadPadInfo>;
|
||||
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<string, object>()
|
||||
};
|
||||
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<Gray, byte> grayImage, IDictionary<string, object> 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<QfnLeadPadInfo>;
|
||||
|
||||
var colorImage = new Image<Bgr, byte>(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<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 Image<Gray, byte> 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<Gray, byte>(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; } = "";
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
<Window
|
||||
x:Class="XplorePlane.Views.ImageProcessing.QfnLeadPadDetectionPanel"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="QFN引脚空洞检测" Width="780" Height="650"
|
||||
ResizeMode="CanResize" 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="8,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" />
|
||||
</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>
|
||||
<Grid Margin="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="290" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧:参数 -->
|
||||
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" Margin="0,0,8,0">
|
||||
<StackPanel>
|
||||
<!-- 工具栏 -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<ToggleButton IsChecked="{Binding RoiEnabled}" Style="{StaticResource IconBtnStyle}" ToolTip="启用ROI区域" Margin="0,0,6,0">
|
||||
<Image Source="/Assets/Icons/polygon.png" Width="20" Height="20" />
|
||||
</ToggleButton>
|
||||
<Button Style="{StaticResource IconBtnStyle}" Command="{Binding ExecuteCommand}" ToolTip="执行检测" Margin="0,0,6,0">
|
||||
<Image Source="/Assets/Icons/run32.png" Width="20" Height="20" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource IconBtnStyle}" Click="Close_Click" ToolTip="关闭" Margin="0,0,6,0">
|
||||
<Image Source="/Assets/Icons/ok.png" Width="20" Height="20" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource IconBtnStyle}" Command="{Binding InsertToCncCommand}" ToolTip="将当前参数插入到激活的CNC检测模块" Margin="0,0,6,0">
|
||||
<TextBlock Text="插入CNC" FontSize="11" VerticalAlignment="Center" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 引脚定位参数 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="引脚定位参数" FontWeight="SemiBold" FontSize="12" Margin="0,0,0,8" />
|
||||
<TextBlock Text="引脚阈值下限" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding PadThresholdLow, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="0" Maximum="255" Value="{Binding PadThresholdLow}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="引脚阈值上限" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding PadThresholdHigh, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="0" Maximum="255" Value="{Binding PadThresholdHigh}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="最小引脚面积" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding MinPadArea, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="10" Maximum="100000" Value="{Binding MinPadArea}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="最大引脚面积" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding MaxPadArea, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="100" Maximum="1000000" Value="{Binding MaxPadArea}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="最小长宽比" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding PadAspectRatioMin, StringFormat=F1, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="0.1" Maximum="20" Value="{Binding PadAspectRatioMin}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="模糊核大小" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding PadBlurSize, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="1" Maximum="31" Value="{Binding PadBlurSize}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="形态学核大小" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel>
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding PadMorphKernel, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="1" Maximum="31" Value="{Binding PadMorphKernel}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 空洞检测参数 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="空洞检测参数" FontWeight="SemiBold" FontSize="12" Margin="0,0,0,8" />
|
||||
<TextBlock Text="空洞阈值下限" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding VoidThresholdLow, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="0" Maximum="255" Value="{Binding VoidThresholdLow}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="空洞阈值上限" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding VoidThresholdHigh, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="0" Maximum="255" Value="{Binding VoidThresholdHigh}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="最小空洞面积" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding MinVoidArea, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="1" Maximum="10000" Value="{Binding MinVoidArea}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="合并半径" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding VoidMergeRadius, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="0" Maximum="20" Value="{Binding VoidMergeRadius}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="空洞率限值(%)" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding VoidRateLimit, StringFormat=F1, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="0" Maximum="100" Value="{Binding VoidRateLimit}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="引脚合格面积" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel>
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding MinQualifiedPadArea, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="0" Maximum="100000" Value="{Binding MinQualifiedPadArea}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 结果摘要 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<TextBlock Text="{Binding ResultText}" FontSize="13" FontWeight="SemiBold" Foreground="#333" TextWrapping="Wrap" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- 右侧:结果表格 -->
|
||||
<Border Grid.Column="1" Background="White" BorderBrush="#E8E8E8" BorderThickness="1" CornerRadius="8">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Text="引脚检测结果" FontWeight="SemiBold" FontSize="12" Margin="12,10,0,6" Foreground="#333" />
|
||||
<DataGrid Grid.Row="1" ItemsSource="{Binding Results}"
|
||||
AutoGenerateColumns="False" IsReadOnly="True"
|
||||
HeadersVisibility="Column" GridLinesVisibility="None"
|
||||
BorderThickness="0" Background="White"
|
||||
RowHeight="30" FontSize="11.5"
|
||||
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
|
||||
<DataGrid.ColumnHeaderStyle>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#F8F9FA" />
|
||||
<Setter Property="Foreground" Value="#666" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Padding" Value="8,6" />
|
||||
<Setter Property="BorderBrush" Value="#EEE" />
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1" />
|
||||
</Style>
|
||||
</DataGrid.ColumnHeaderStyle>
|
||||
<DataGrid.CellStyle>
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="Padding" Value="8,4" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
</DataGrid.CellStyle>
|
||||
<DataGrid.RowStyle>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="AlternationIndex" Value="1">
|
||||
<Setter Property="Background" Value="#FAFBFC" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#EDF4FC" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGrid.RowStyle>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="#" Binding="{Binding Index}" Width="35" />
|
||||
<DataGridTextColumn Header="中心X" Binding="{Binding CenterX}" Width="60" />
|
||||
<DataGridTextColumn Header="中心Y" Binding="{Binding CenterY}" Width="60" />
|
||||
<DataGridTextColumn Header="面积(px)" Binding="{Binding PadArea}" Width="70" />
|
||||
<DataGridTextColumn Header="空洞率" Binding="{Binding VoidRate}" Width="65" />
|
||||
<DataGridTextColumn Header="空洞数" Binding="{Binding VoidCount}" Width="55" />
|
||||
<DataGridTextColumn Header="判定" Binding="{Binding Classification}" Width="55" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -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<IMainViewportService>();
|
||||
DataContext = new QfnLeadPadDetectionViewModel(viewportService);
|
||||
|
||||
Loaded += (s, e) =>
|
||||
{
|
||||
var mainWin = Owner as MainWindow;
|
||||
if (mainWin != null)
|
||||
{
|
||||
var canvas = FindChild<PolygonRoiCanvas>(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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,11 +181,11 @@
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="灰度"
|
||||
telerik:ScreenTip.Title="线灰度"
|
||||
Command="{Binding GrayscaleCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/film-darken.png"
|
||||
Text="灰度" />
|
||||
Text="线灰度" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="黑底检测"
|
||||
Command="{Binding BlackBackgroundDetectionCommand}"
|
||||
@@ -415,6 +415,13 @@
|
||||
Content="孔隙检测"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/Pores.png" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="自动检测QFN引脚焊点并逐引脚测量空洞率"
|
||||
telerik:ScreenTip.Title="QFN引脚检测"
|
||||
Command="{Binding QfnLeadPadDetectionCommand}"
|
||||
Content="QFN检测"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/QFN.png" />
|
||||
</telerik:RadRibbonGroup>
|
||||
</telerik:RadRibbonTab>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user