4 Commits

Author SHA1 Message Date
zhengxuan.zhang 1546aec567 优化高级模块CNC执行的可视化
CNC执行 → PipelineExecutionService(返回 LastStepOutputData)
                  → CncExecutionService(调用 PushDetectionOverlay)
                  → MainViewportService(触发 DetectionOverlayUpdated 事件)
                  → ViewportPanelView(订阅事件,调用 DetectionOverlayRenderer)
                  → PolygonRoiCanvas.SetDetectionOverlayCanvas(插入叠加层 Canvas)
2026-05-19 14:10:16 +08:00
zhengxuan.zhang eb6ee48a5e CNC高级模块的运行后的可视化 2026-05-19 13:11:47 +08:00
zhengxuan.zhang 80c86e2ed7 孔隙检测模块引入到CNC 2026-05-19 11:38:31 +08:00
zhengxuan.zhang 3cfd115d72 高级模块的CNC插入功能 2026-05-19 11:21:28 +08:00
16 changed files with 573 additions and 4 deletions
@@ -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));