Compare commits
4 Commits
04da9cd798
...
1546aec567
| Author | SHA1 | Date | |
|---|---|---|---|
| 1546aec567 | |||
| eb6ee48a5e | |||
| 80c86e2ed7 | |||
| 3cfd115d72 |
@@ -671,6 +671,38 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
if (element != null && mainCanvas.Children.Contains(element))
|
||||
mainCanvas.Children.Remove(element);
|
||||
}
|
||||
|
||||
// ── 检测结果叠加层 ──
|
||||
private Canvas _detectionOverlay;
|
||||
|
||||
/// <summary>
|
||||
/// 设置检测结果叠加层 Canvas(由外部构建好后传入)。
|
||||
/// </summary>
|
||||
public void SetDetectionOverlayCanvas(Canvas overlayCanvas)
|
||||
{
|
||||
ClearDetectionOverlay();
|
||||
|
||||
if (overlayCanvas == null) return;
|
||||
|
||||
_detectionOverlay = overlayCanvas;
|
||||
_detectionOverlay.IsHitTestVisible = false;
|
||||
_detectionOverlay.SetBinding(Canvas.WidthProperty, new System.Windows.Data.Binding("CanvasWidth") { Source = this });
|
||||
_detectionOverlay.SetBinding(Canvas.HeightProperty, new System.Windows.Data.Binding("CanvasHeight") { Source = this });
|
||||
|
||||
// 插入到 backgroundImage 之后(索引1),在 ROI 和测量层之下
|
||||
int insertIndex = System.Math.Min(1, mainCanvas.Children.Count);
|
||||
mainCanvas.Children.Insert(insertIndex, _detectionOverlay);
|
||||
}
|
||||
|
||||
/// <summary>清除检测结果叠加层</summary>
|
||||
public void ClearDetectionOverlay()
|
||||
{
|
||||
if (_detectionOverlay != null)
|
||||
{
|
||||
mainCanvas.Children.Remove(_detectionOverlay);
|
||||
_detectionOverlay = null;
|
||||
}
|
||||
}
|
||||
public int MeasureCount => _ppGroups.Count + _ptlGroups.Count + _angleGroups.Count + _frGroups.Count + _bgaGroups.Count;
|
||||
|
||||
// ── 点击分发 ──
|
||||
|
||||
@@ -718,6 +718,16 @@ namespace XplorePlane.Services.Cnc
|
||||
nodeResult.Status = InspectionNodeStatus.Succeeded;
|
||||
_mainViewportService?.SetCncResultImage(resultImage, $"CNC \u8282\u70b9\u7ed3\u679c\uff1a{inspectionNode.Name}");
|
||||
}
|
||||
|
||||
// 推送检测结果叠加层数据(轮廓、标注信息)供 UI 分层绘制
|
||||
if (execResult.LastStepOutputData != null)
|
||||
{
|
||||
var lastOperatorKey = inspectionNode.Pipeline.Nodes
|
||||
.Where(n => n.IsEnabled)
|
||||
.OrderBy(n => n.Order)
|
||||
.LastOrDefault()?.OperatorKey ?? string.Empty;
|
||||
_mainViewportService?.PushDetectionOverlay(execResult.LastStepOutputData, lastOperatorKey);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using XP.ImageProcessing.Processors;
|
||||
|
||||
namespace XplorePlane.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 检测结果叠加层渲染器:根据算子输出数据构建 WPF Canvas 叠加层。
|
||||
/// 用于在 PolygonRoiCanvas 上分层绘制检测结果(轮廓、标注、半透明填充)。
|
||||
/// </summary>
|
||||
public static class DetectionOverlayRenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据算子输出数据构建叠加层 Canvas。
|
||||
/// </summary>
|
||||
public static Canvas BuildOverlay(IReadOnlyDictionary<string, object> outputData, string operatorKey)
|
||||
{
|
||||
if (outputData == null) return null;
|
||||
|
||||
var canvas = new Canvas
|
||||
{
|
||||
IsHitTestVisible = false,
|
||||
Background = Brushes.Transparent
|
||||
};
|
||||
|
||||
if (string.Equals(operatorKey, "BgaVoidRate", StringComparison.OrdinalIgnoreCase))
|
||||
RenderBgaOverlay(canvas, outputData);
|
||||
else if (string.Equals(operatorKey, "VoidMeasurement", StringComparison.OrdinalIgnoreCase))
|
||||
RenderVoidOverlay(canvas, outputData);
|
||||
|
||||
return canvas.Children.Count > 0 ? canvas : null;
|
||||
}
|
||||
|
||||
private static void RenderBgaOverlay(Canvas canvas, IReadOnlyDictionary<string, object> outputData)
|
||||
{
|
||||
if (!outputData.TryGetValue("BgaBalls", out var ballsObj)) return;
|
||||
if (ballsObj is not List<BgaBallInfo> bgaBalls) return;
|
||||
if (bgaBalls.Count == 0) return;
|
||||
|
||||
int ngCount = 0;
|
||||
foreach (var bga in bgaBalls)
|
||||
{
|
||||
bool isFail = bga.Classification != "PASS";
|
||||
if (isFail) ngCount++;
|
||||
|
||||
var contourBrush = isFail ? Brushes.Red : Brushes.Lime;
|
||||
|
||||
// 绘制焊球轮廓
|
||||
if (bga.ContourPoints != null && bga.ContourPoints.Length > 2)
|
||||
{
|
||||
var polygon = new Polygon
|
||||
{
|
||||
Stroke = contourBrush,
|
||||
StrokeThickness = 2,
|
||||
Fill = Brushes.Transparent,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
var points = new PointCollection();
|
||||
foreach (var pt in bga.ContourPoints)
|
||||
points.Add(new Point(pt.X, pt.Y));
|
||||
polygon.Points = points;
|
||||
canvas.Children.Add(polygon);
|
||||
}
|
||||
|
||||
// 绘制气泡填充(半透明)
|
||||
foreach (var v in bga.Voids)
|
||||
{
|
||||
if (v.ContourPoints != null && v.ContourPoints.Length > 2)
|
||||
{
|
||||
var voidPoly = new Polygon
|
||||
{
|
||||
Stroke = Brushes.Orange,
|
||||
StrokeThickness = 1,
|
||||
Fill = new SolidColorBrush(Color.FromArgb(100, 255, 200, 0)),
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
var voidPoints = new PointCollection();
|
||||
foreach (var pt in v.ContourPoints)
|
||||
voidPoints.Add(new Point(pt.X, pt.Y));
|
||||
voidPoly.Points = voidPoints;
|
||||
canvas.Children.Add(voidPoly);
|
||||
}
|
||||
}
|
||||
|
||||
// 编号标注
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = $"#{bga.Index}",
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = Brushes.Cyan,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
Canvas.SetLeft(label, bga.CenterX - 10);
|
||||
Canvas.SetTop(label, bga.CenterY - 8);
|
||||
canvas.Children.Add(label);
|
||||
}
|
||||
|
||||
// 总览标注
|
||||
int okCount = bgaBalls.Count - ngCount;
|
||||
var summaryLabel = new TextBlock
|
||||
{
|
||||
Text = $"Total: {bgaBalls.Count} | OK: {okCount} | NG: {ngCount}",
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = ngCount > 0 ? Brushes.Red : Brushes.Lime,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
Canvas.SetLeft(summaryLabel, 10);
|
||||
Canvas.SetTop(summaryLabel, 10);
|
||||
canvas.Children.Add(summaryLabel);
|
||||
}
|
||||
|
||||
private static void RenderVoidOverlay(Canvas canvas, IReadOnlyDictionary<string, object> outputData)
|
||||
{
|
||||
if (!outputData.TryGetValue("Voids", out var voidsObj)) return;
|
||||
if (voidsObj is not List<VoidRegionInfo> voids) return;
|
||||
if (voids.Count == 0) return;
|
||||
|
||||
double voidRate = outputData.TryGetValue("VoidRate", out var vrObj) && vrObj is double vr ? vr : 0;
|
||||
double voidLimit = outputData.TryGetValue("VoidLimit", out var vlObj) && vlObj is double vl ? vl : 25.0;
|
||||
string classification = outputData.TryGetValue("Classification", out var clsObj) && clsObj is string cls ? cls : "N/A";
|
||||
|
||||
foreach (var v in voids)
|
||||
{
|
||||
// 绘制空隙轮廓(半透明填充)
|
||||
if (v.ContourPoints != null && v.ContourPoints.Length > 2)
|
||||
{
|
||||
var voidPoly = new Polygon
|
||||
{
|
||||
Stroke = Brushes.Yellow,
|
||||
StrokeThickness = 1,
|
||||
Fill = new SolidColorBrush(Color.FromArgb(100, 255, 200, 0)),
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
var points = new PointCollection();
|
||||
foreach (var pt in v.ContourPoints)
|
||||
points.Add(new Point(pt.X, pt.Y));
|
||||
voidPoly.Points = points;
|
||||
canvas.Children.Add(voidPoly);
|
||||
}
|
||||
|
||||
// 编号标注
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = $"#{v.Index}",
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = Brushes.Cyan,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
Canvas.SetLeft(label, v.CenterX - 8);
|
||||
Canvas.SetTop(label, v.CenterY - 6);
|
||||
canvas.Children.Add(label);
|
||||
}
|
||||
|
||||
// 总览标注
|
||||
var overallColor = classification == "PASS" ? Brushes.Lime : Brushes.Red;
|
||||
var summaryLabel = new TextBlock
|
||||
{
|
||||
Text = $"Void: {voidRate:F1}% | Limit: {voidLimit:F0}% | {voids.Count} voids | {classification}",
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = overallColor,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
Canvas.SetLeft(summaryLabel, 10);
|
||||
Canvas.SetTop(summaryLabel, 10);
|
||||
canvas.Children.Add(summaryLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,5 +67,17 @@ namespace XplorePlane.Services
|
||||
|
||||
return BitmapSource.Create(width, height, 96, 96, PixelFormats.Gray8, null, pixels, stride);
|
||||
}
|
||||
|
||||
public static BitmapSource ToBitmapSourceFromBgr(Image<Bgr, byte> emguImage)
|
||||
{
|
||||
if (emguImage == null) throw new ArgumentNullException(nameof(emguImage));
|
||||
|
||||
int width = emguImage.Width;
|
||||
int height = emguImage.Height;
|
||||
byte[] pixels = emguImage.Bytes;
|
||||
int stride = pixels.Length / height;
|
||||
|
||||
return BitmapSource.Create(width, height, 96, 96, PixelFormats.Bgr24, null, pixels, stride);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,7 +264,11 @@ namespace XplorePlane.Services
|
||||
|
||||
var snapshot = new Dictionary<string, object>(processor.OutputData.Count);
|
||||
foreach (var kv in processor.OutputData)
|
||||
{
|
||||
// 不将大型 Emgu 图像对象序列化到快照中
|
||||
if (kv.Key == "RenderedResultImage" || kv.Key == "RoiMask") continue;
|
||||
snapshot[kv.Key] = kv.Value;
|
||||
}
|
||||
|
||||
return (result, snapshot);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace XplorePlane.Services.MainViewport
|
||||
@@ -34,5 +35,28 @@ namespace XplorePlane.Services.MainViewport
|
||||
/// 与 <see cref="SetManualImage"/> 不同,此方法在 CNC 运行期间不会被阻断。
|
||||
/// </summary>
|
||||
void SetCncResultImage(ImageSource image, string label);
|
||||
|
||||
/// <summary>
|
||||
/// 推送检测结果叠加层数据(轮廓、标注等),由 UI 分层绘制。
|
||||
/// </summary>
|
||||
event EventHandler<DetectionOverlayEventArgs> DetectionOverlayUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// 由 CNC 执行引擎调用,将检测算子的输出数据推送给 UI 叠加层。
|
||||
/// </summary>
|
||||
void PushDetectionOverlay(IReadOnlyDictionary<string, object> outputData, string operatorKey);
|
||||
}
|
||||
|
||||
/// <summary>检测结果叠加层事件参数</summary>
|
||||
public class DetectionOverlayEventArgs : EventArgs
|
||||
{
|
||||
public IReadOnlyDictionary<string, object> OutputData { get; }
|
||||
public string OperatorKey { get; }
|
||||
|
||||
public DetectionOverlayEventArgs(IReadOnlyDictionary<string, object> outputData, string operatorKey)
|
||||
{
|
||||
OutputData = outputData;
|
||||
OperatorKey = operatorKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.IO;
|
||||
using System.Windows.Media;
|
||||
@@ -208,6 +209,14 @@ namespace XplorePlane.Services.MainViewport
|
||||
RaiseStateChanged();
|
||||
}
|
||||
|
||||
public event EventHandler<DetectionOverlayEventArgs> DetectionOverlayUpdated;
|
||||
|
||||
public void PushDetectionOverlay(IReadOnlyDictionary<string, object> outputData, string operatorKey)
|
||||
{
|
||||
if (outputData == null) return;
|
||||
DetectionOverlayUpdated?.Invoke(this, new DetectionOverlayEventArgs(outputData, operatorKey));
|
||||
}
|
||||
|
||||
public void SetManualImage(ImageSource image, string filePath)
|
||||
{
|
||||
if (image == null)
|
||||
|
||||
@@ -11,9 +11,11 @@ namespace XplorePlane.Services
|
||||
|
||||
/// <param name="Image">流水线输出图像(始终为灰度预览路径下的结果)。</param>
|
||||
/// <param name="TemplateMatchOverlayData">当且仅当最后一步为旋转模板匹配且需绘制匹配框时,为 OutputData 的副本,供 UI 透明叠加层使用;否则为 null。</param>
|
||||
/// <param name="LastStepOutputData">最后一步算子的 OutputData 快照,供检测结果叠加层使用。</param>
|
||||
public sealed record PipelineExecutionResult(
|
||||
BitmapSource Image,
|
||||
IReadOnlyDictionary<string, object>? TemplateMatchOverlayData);
|
||||
IReadOnlyDictionary<string, object>? TemplateMatchOverlayData,
|
||||
IReadOnlyDictionary<string, object>? LastStepOutputData = null);
|
||||
|
||||
public interface IPipelineExecutionService
|
||||
{
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace XplorePlane.Services
|
||||
|
||||
var total = enabledNodes.Count;
|
||||
IReadOnlyDictionary<string, object>? templateOverlayData = null;
|
||||
IReadOnlyDictionary<string, object>? lastStepOutputData = null;
|
||||
|
||||
for (var step = 0; step < total; step++)
|
||||
{
|
||||
@@ -82,6 +83,7 @@ namespace XplorePlane.Services
|
||||
{
|
||||
templateOverlayData = TemplateMatchOverlayRenderer.TrySnapshotOutputForOverlay(
|
||||
node.OperatorKey, parameters, output);
|
||||
lastStepOutputData = output;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -107,7 +109,7 @@ namespace XplorePlane.Services
|
||||
if (!current.IsFrozen)
|
||||
current.Freeze();
|
||||
|
||||
return new PipelineExecutionResult(current, templateOverlayData);
|
||||
return new PipelineExecutionResult(current, templateOverlayData, lastStepOutputData);
|
||||
}
|
||||
|
||||
private static BitmapSource ScaleForPreview(BitmapSource source)
|
||||
|
||||
@@ -14,24 +14,35 @@ 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 BgaDetectionViewModel : BindableBase
|
||||
{
|
||||
private readonly IMainViewportService _viewportService;
|
||||
private CncEditorViewModel _cncEditorViewModel;
|
||||
private BitmapSource _originalImage;
|
||||
private System.Threading.CancellationTokenSource _debounceCts;
|
||||
private const int DebounceMs = 300;
|
||||
private const string BgaVoidRateOperatorKey = "BgaVoidRate";
|
||||
|
||||
public BgaDetectionViewModel(IMainViewportService viewportService)
|
||||
{
|
||||
_viewportService = viewportService;
|
||||
ExecuteCommand = new DelegateCommand(Execute);
|
||||
InsertToCncCommand = new DelegateCommand(ExecuteInsertToCnc);
|
||||
PropertyChanged += OnAnyPropertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>设置 CNC 编辑器 ViewModel 引用,用于插入参数到激活的 CNC 位置节点</summary>
|
||||
public void SetCncEditorViewModel(CncEditorViewModel cncEditorViewModel)
|
||||
{
|
||||
_cncEditorViewModel = cncEditorViewModel;
|
||||
}
|
||||
|
||||
private void OnAnyPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
// 排除结果属性和ROI开关,只监听参数变化
|
||||
@@ -265,6 +276,7 @@ namespace XplorePlane.ViewModels.ImageProcessing
|
||||
public System.Collections.ObjectModel.ObservableCollection<BgaResultItem> Results { get; } = new();
|
||||
|
||||
public DelegateCommand ExecuteCommand { get; }
|
||||
public DelegateCommand InsertToCncCommand { get; }
|
||||
|
||||
private void Execute()
|
||||
{
|
||||
@@ -361,6 +373,123 @@ namespace XplorePlane.ViewModels.ImageProcessing
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前 ROI 和算子参数插入到激活的 CNC 位置节点的检测模块中 BGA空洞模块的参数
|
||||
/// </summary>
|
||||
private void ExecuteInsertToCnc()
|
||||
{
|
||||
if (_cncEditorViewModel == null)
|
||||
{
|
||||
MessageBox.Show("CNC 编辑器未就绪,无法插入参数。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找当前激活的检测模块节点(SelectedNode 本身是 InspectionModule,或其父节点是 SavePosition)
|
||||
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
|
||||
{
|
||||
// 尝试在 Nodes 中找到当前选中节点所属的 SavePosition 的检测模块
|
||||
var allNodes = _cncEditorViewModel.Nodes;
|
||||
// 向前查找最近的 SavePosition
|
||||
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;
|
||||
}
|
||||
|
||||
// 获取或创建 Pipeline
|
||||
var pipeline = targetModuleNode.Pipeline ?? new PipelineModel { Name = targetModuleNode.Name };
|
||||
|
||||
// 查找已有的 BgaVoidRate 算子节点
|
||||
var bgaNode = pipeline.Nodes.FirstOrDefault(n =>
|
||||
string.Equals(n.OperatorKey, BgaVoidRateOperatorKey, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (bgaNode == null)
|
||||
{
|
||||
// 不存在则新建一个 BgaVoidRate 节点并添加到流水线末尾
|
||||
bgaNode = new PipelineNodeModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OperatorKey = BgaVoidRateOperatorKey,
|
||||
Order = pipeline.Nodes.Count,
|
||||
IsEnabled = true,
|
||||
Parameters = new Dictionary<string, object>()
|
||||
};
|
||||
pipeline.Nodes.Add(bgaNode);
|
||||
}
|
||||
|
||||
// 写入当前参数
|
||||
var parameters = bgaNode.Parameters;
|
||||
parameters["BgaMinArea"] = BgaMinArea;
|
||||
parameters["BgaMaxArea"] = BgaMaxArea;
|
||||
parameters["BgaBlurSize"] = BlurSize;
|
||||
parameters["BgaCircularity"] = Circularity;
|
||||
parameters["MinThreshold"] = MinThreshold;
|
||||
parameters["MaxThreshold"] = MaxThreshold;
|
||||
parameters["MinVoidArea"] = MinVoidArea;
|
||||
parameters["VoidLimit"] = VoidLimit;
|
||||
|
||||
// 写入 ROI 参数
|
||||
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 到节点
|
||||
pipeline.UpdatedAt = DateTime.UtcNow;
|
||||
targetModuleNode.Pipeline = pipeline;
|
||||
|
||||
// 强制刷新右侧检测模块面板:将选中节点切换到目标检测模块,触发重新加载
|
||||
_cncEditorViewModel.SelectedNode = null;
|
||||
_cncEditorViewModel.SelectedNode = targetModuleNode;
|
||||
|
||||
MessageBox.Show(
|
||||
$"已将 BGA 检测参数插入到检测模块「{targetModuleNode.Name}」。",
|
||||
"插入成功", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
private BitmapSource RenderResults(Image<Gray, byte> grayImage, IDictionary<string, object> output)
|
||||
{
|
||||
if (!output.ContainsKey("BgaVoidResult")) return null;
|
||||
|
||||
@@ -14,24 +14,35 @@ 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 VoidDetectionViewModel : BindableBase
|
||||
{
|
||||
private readonly IMainViewportService _viewportService;
|
||||
private CncEditorViewModel _cncEditorViewModel;
|
||||
private BitmapSource _originalImage;
|
||||
private System.Threading.CancellationTokenSource _debounceCts;
|
||||
private const int DebounceMs = 300;
|
||||
private const string VoidMeasurementOperatorKey = "VoidMeasurement";
|
||||
|
||||
public VoidDetectionViewModel(IMainViewportService viewportService)
|
||||
{
|
||||
_viewportService = viewportService;
|
||||
ExecuteCommand = new DelegateCommand(Execute);
|
||||
InsertToCncCommand = new DelegateCommand(ExecuteInsertToCnc);
|
||||
PropertyChanged += OnAnyPropertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>设置 CNC 编辑器 ViewModel 引用,用于插入参数到激活的 CNC 位置节点</summary>
|
||||
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))
|
||||
@@ -166,6 +177,7 @@ namespace XplorePlane.ViewModels.ImageProcessing
|
||||
public ObservableCollection<VoidResultItem> Results { get; } = new();
|
||||
|
||||
public DelegateCommand ExecuteCommand { get; }
|
||||
public DelegateCommand InsertToCncCommand { get; }
|
||||
|
||||
private void Execute()
|
||||
{
|
||||
@@ -235,6 +247,113 @@ namespace XplorePlane.ViewModels.ImageProcessing
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前 ROI 和算子参数插入到激活的 CNC 位置节点的检测模块中空隙检测模块的参数
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
// 获取或创建 Pipeline
|
||||
var pipeline = targetModuleNode.Pipeline ?? new PipelineModel { Name = targetModuleNode.Name };
|
||||
|
||||
// 查找已有的 VoidMeasurement 算子节点
|
||||
var voidNode = pipeline.Nodes.FirstOrDefault(n =>
|
||||
string.Equals(n.OperatorKey, VoidMeasurementOperatorKey, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (voidNode == null)
|
||||
{
|
||||
voidNode = new PipelineNodeModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OperatorKey = VoidMeasurementOperatorKey,
|
||||
Order = pipeline.Nodes.Count,
|
||||
IsEnabled = true,
|
||||
Parameters = new Dictionary<string, object>()
|
||||
};
|
||||
pipeline.Nodes.Add(voidNode);
|
||||
}
|
||||
|
||||
// 写入当前参数
|
||||
var parameters = voidNode.Parameters;
|
||||
parameters["MinThreshold"] = MinThreshold;
|
||||
parameters["MaxThreshold"] = MaxThreshold;
|
||||
parameters["MinVoidArea"] = MinVoidArea;
|
||||
parameters["MergeRadius"] = MergeRadius;
|
||||
parameters["BlurSize"] = BlurSize;
|
||||
parameters["VoidLimit"] = VoidLimit;
|
||||
|
||||
// 写入 ROI 参数
|
||||
if (RoiEnabled && _roiShape != null && _roiShape.Points.Count >= 3)
|
||||
{
|
||||
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["PolyCount"] = 0;
|
||||
}
|
||||
|
||||
// 更新 Pipeline 到节点
|
||||
pipeline.UpdatedAt = DateTime.UtcNow;
|
||||
targetModuleNode.Pipeline = pipeline;
|
||||
|
||||
// 强制刷新右侧检测模块面板
|
||||
_cncEditorViewModel.SelectedNode = null;
|
||||
_cncEditorViewModel.SelectedNode = targetModuleNode;
|
||||
|
||||
MessageBox.Show(
|
||||
$"已将空隙检测参数插入到检测模块「{targetModuleNode.Name}」。",
|
||||
"插入成功", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
private void ShowResultOnOverlay(BitmapSource resultBmp)
|
||||
{
|
||||
if (_canvas == null) return;
|
||||
|
||||
@@ -76,9 +76,12 @@
|
||||
<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="关闭">
|
||||
<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>
|
||||
|
||||
<!-- BGA定位参数卡片 -->
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
@@ -24,6 +25,17 @@ namespace XplorePlane.Views.ImageProcessing
|
||||
if (DataContext is BgaDetectionViewModel vm)
|
||||
vm.SetCanvas(canvas);
|
||||
}
|
||||
|
||||
// 从 MainViewModel 获取 CncEditorViewModel 引用
|
||||
if (DataContext is BgaDetectionViewModel bgaVm && 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)
|
||||
{
|
||||
bgaVm.SetCncEditorViewModel(cncEditor);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Closed += (s, e) =>
|
||||
|
||||
@@ -72,9 +72,13 @@
|
||||
<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="关闭">
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 参数卡片 -->
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
@@ -23,6 +24,17 @@ namespace XplorePlane.Views.ImageProcessing
|
||||
if (DataContext is VoidDetectionViewModel vm)
|
||||
vm.SetCanvas(canvas);
|
||||
}
|
||||
|
||||
// 从 MainViewModel 获取 CncEditorViewModel 引用
|
||||
if (DataContext is VoidDetectionViewModel voidVm && 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)
|
||||
{
|
||||
voidVm.SetCncEditorViewModel(cncEditor);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Closed += (s, e) =>
|
||||
|
||||
@@ -9,6 +9,7 @@ using Microsoft.Win32;
|
||||
using Prism.Ioc;
|
||||
using XP.ImageProcessing.RoiControl.Controls;
|
||||
using XplorePlane.Events;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.ViewModels;
|
||||
|
||||
namespace XplorePlane.Views
|
||||
@@ -126,6 +127,24 @@ namespace XplorePlane.Views
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 订阅检测结果叠加层事件
|
||||
try
|
||||
{
|
||||
var viewportService = ContainerLocator.Current?.Resolve<Services.MainViewport.IMainViewportService>();
|
||||
if (viewportService != null)
|
||||
{
|
||||
viewportService.DetectionOverlayUpdated += (s, args) =>
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
var overlay = DetectionOverlayRenderer.BuildOverlay(args.OutputData, args.OperatorKey);
|
||||
RoiCanvas.SetDetectionOverlayCanvas(overlay);
|
||||
}));
|
||||
};
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 光标信息:从 RoiCanvas.CursorInfo 同步到 MainViewModel
|
||||
var cursorInfoDesc = System.ComponentModel.DependencyPropertyDescriptor.FromProperty(
|
||||
PolygonRoiCanvas.CursorInfoProperty, typeof(PolygonRoiCanvas));
|
||||
|
||||
Reference in New Issue
Block a user