BGA检测辅助面板: MVVM架构、参数调节自动检测、结果表格、ROI绘制、结果图像渲染

This commit is contained in:
李伟
2026-04-28 17:38:33 +08:00
parent 8b2cf01fe2
commit d8dc30b9b9
3 changed files with 639 additions and 0 deletions
@@ -0,0 +1,477 @@
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.Services.MainViewport;
namespace XplorePlane.ViewModels.ImageProcessing
{
public class BgaDetectionViewModel : BindableBase
{
private readonly IMainViewportService _viewportService;
private BitmapSource _originalImage;
private System.Threading.CancellationTokenSource _debounceCts;
private const int DebounceMs = 300;
public BgaDetectionViewModel(IMainViewportService viewportService)
{
_viewportService = viewportService;
ExecuteCommand = new DelegateCommand(Execute);
PropertyChanged += OnAnyPropertyChanged;
}
private void OnAnyPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
// 排除结果属性和ROI开关,只监听参数变化
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());
}
// BGA定位参数
private int _bgaMinArea = 500;
public int BgaMinArea { get => _bgaMinArea; set => SetProperty(ref _bgaMinArea, value); }
private int _bgaMaxArea = 500000;
public int BgaMaxArea { get => _bgaMaxArea; set => SetProperty(ref _bgaMaxArea, value); }
private int _blurSize = 5;
public int BlurSize { get => _blurSize; set => SetProperty(ref _blurSize, value); }
private double _circularity = 0.5;
public double Circularity { get => _circularity; set => SetProperty(ref _circularity, value); }
// 气泡检测参数
private int _minThreshold = 128;
public int MinThreshold { get => _minThreshold; set => SetProperty(ref _minThreshold, value); }
private int _maxThreshold = 255;
public int MaxThreshold { get => _maxThreshold; set => SetProperty(ref _maxThreshold, value); }
private int _minVoidArea = 10;
public int MinVoidArea { get => _minVoidArea; set => SetProperty(ref _minVoidArea, value); }
private double _voidLimit = 25.0;
public double VoidLimit { get => _voidLimit; set => SetProperty(ref _voidLimit, value); }
private double _maxSingleVoidLimit = 10.0;
public double MaxSingleVoidLimit { get => _maxSingleVoidLimit; set => SetProperty(ref _maxSingleVoidLimit, value); }
// ROI
private bool _roiEnabled;
public bool RoiEnabled
{
get => _roiEnabled;
set
{
if (SetProperty(ref _roiEnabled, value))
OnRoiEnabledChanged();
}
}
private PolygonRoiCanvas _canvas;
private PolygonROI _roiShape;
public void SetCanvas(PolygonRoiCanvas canvas)
{
_canvas = canvas;
}
private void OnRoiEnabledChanged()
{
if (_canvas == null) return;
if (RoiEnabled)
{
// 确保 ROIItems 存在
if (_canvas.ROIItems == null)
_canvas.ROIItems = new ObservableCollection<ROIShape>();
_roiShape = new PolygonROI { Color = "Red", IsSelected = true };
_canvas.ROIItems.Add(_roiShape);
_canvas.SelectedROI = _roiShape;
// 手动注册 CollectionChanged
_roiShape.Points.CollectionChanged += (s, e) =>
{
_canvas.SelectedROI = null;
_canvas.SelectedROI = _roiShape;
};
// 禁用右键菜单(参考点点距方式)
_canvas.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, false);
// 订阅画布点击事件来添加 ROI 顶点
_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;
}
}
/// <summary>智能插入顶点:找到距离新点最近的边,将新点插入到该边的两个顶点之间</summary>
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));
}
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 System.Collections.ObjectModel.ObservableCollection<BgaResultItem> Results { get; } = new();
public DelegateCommand ExecuteCommand { get; }
private void Execute()
{
// 首次执行时保存原图,后续始终用原图处理
if (_originalImage == null)
{
_originalImage = _viewportService?.CurrentDisplayImage as BitmapSource;
}
var image = _originalImage;
if (image == null)
{
ResultText = "请先加载图像";
return;
}
try
{
var processor = new BgaVoidRateProcessor();
processor.SetParameter("BgaMinArea", BgaMinArea);
processor.SetParameter("BgaMaxArea", BgaMaxArea);
processor.SetParameter("BgaBlurSize", BlurSize);
processor.SetParameter("BgaCircularity", Circularity);
processor.SetParameter("MinThreshold", MinThreshold);
processor.SetParameter("MaxThreshold", MaxThreshold);
processor.SetParameter("MinVoidArea", MinVoidArea);
processor.SetParameter("VoidLimit", VoidLimit);
processor.SetParameter("RoiMode", "None");
// 如果有 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() ?? "--"
: "未检测到BGA焊球";
// 填充结果表格
Results.Clear();
if (output.ContainsKey("BgaBalls"))
{
var bgaBalls = output["BgaBalls"] as List<BgaBallInfo>;
if (bgaBalls != null)
{
// 统一排序:按行分组(Y坐标相近归为一行),行内按X排序
var sorted = SortBgaBalls(bgaBalls);
foreach (var bga in sorted)
{
double maxVoid = bga.Voids.Count > 0 ? bga.Voids.Max(v => v.AreaPercent) : 0;
// 额外判定:最大单个气泡占比超限也为NG
string cls = bga.Classification;
if (cls == "PASS" && maxVoid > MaxSingleVoidLimit)
cls = "FAIL";
Results.Add(new BgaResultItem
{
Index = bga.Index,
Classification = cls,
CenterX = bga.CenterX.ToString("F1"),
CenterY = bga.CenterY.ToString("F1"),
BgaArea = bga.BgaArea.ToString(),
VoidRate = $"{bga.VoidRate:F1}%",
MaxVoidRate = $"{maxVoid:F1}%",
VoidCount = bga.Voids.Count.ToString(),
Circularity = bga.Circularity.ToString("F2")
});
}
}
}
// 绘制结果到图像
ResultImage = RenderResults(grayImage, output);
// 将结果图像推送到主界面显示
if (ResultImage != null)
_viewportService?.SetManualImage(ResultImage, "BGA检测结果");
grayImage.Dispose();
}
catch (Exception ex)
{
ResultText = $"错误: {ex.Message}";
}
}
private BitmapSource RenderResults(Image<Gray, byte> grayImage, IDictionary<string, object> output)
{
if (!output.ContainsKey("BgaVoidResult")) return null;
int bgaCount = (int)output["BgaCount"];
if (bgaCount == 0) return null;
double voidRate = (double)output["VoidRate"];
string classification = (string)output["Classification"];
double voidLimitVal = (double)output["VoidLimit"];
int thickness = (int)output["Thickness"];
var bgaBalls = output["BgaBalls"] as List<BgaBallInfo>;
var colorImage = new Image<Bgr, byte>(grayImage.Width, grayImage.Height);
CvInvoke.CvtColor(grayImage, colorImage, ColorConversion.Gray2Bgr);
if (bgaBalls != null && bgaBalls.Count > 0)
{
// 使用统一排序
var sorted = SortBgaBalls(bgaBalls);
// 半透明气泡填充
var overlay = colorImage.Clone();
foreach (var bga in sorted)
{
var fillColor = new MCvScalar(0, 200, 255);
foreach (var v in bga.Voids)
{
if (v.ContourPoints.Length > 0)
{
using var vop = new VectorOfPoint(v.ContourPoints);
using var vvop = new VectorOfVectorOfPoint(vop);
CvInvoke.DrawContours(overlay, vvop, 0, fillColor, -1);
}
}
}
CvInvoke.AddWeighted(overlay, 0.4, colorImage, 0.6, 0, colorImage);
overlay.Dispose();
// 绘制焊球轮廓 + 编号(蓝色,焊球下方)
foreach (var bga in sorted)
{
// 应用最大单气泡限值判定
double maxVoid = bga.Voids.Count > 0 ? bga.Voids.Max(v => v.AreaPercent) : 0;
string cls = bga.Classification;
if (cls == "PASS" && maxVoid > MaxSingleVoidLimit)
cls = "FAIL";
var bgaColor = cls == "PASS"
? new MCvScalar(0, 255, 0) : new MCvScalar(0, 0, 255);
if (bga.ContourPoints.Length > 0)
{
using var vop = new VectorOfPoint(bga.ContourPoints);
using var vvop = new VectorOfVectorOfPoint(vop);
CvInvoke.DrawContours(colorImage, vvop, 0, bgaColor, thickness);
}
// 编号标注在焊球下方,蓝色字体
var bbox = CvInvoke.BoundingRectangle(new VectorOfPoint(bga.ContourPoints));
CvInvoke.PutText(colorImage, $"#{bga.Index}",
new System.Drawing.Point(bbox.X + bbox.Width / 2 - 10, bbox.Bottom + 16),
FontFace.HersheySimplex, 0.45, new MCvScalar(255, 100, 0), 2);
}
// 左上角总览结果
int ngCount = sorted.Count(b =>
{
double mv = b.Voids.Count > 0 ? b.Voids.Max(v => v.AreaPercent) : 0;
return b.Classification == "FAIL" || mv > MaxSingleVoidLimit;
});
int okCount = sorted.Count - ngCount;
var overallColor = ngCount > 0
? new MCvScalar(0, 0, 255) : new MCvScalar(0, 255, 0);
CvInvoke.PutText(colorImage,
$"Total: {sorted.Count} | OK: {okCount} | NG: {ngCount}",
new System.Drawing.Point(10, 25),
FontFace.HersheySimplex, 0.55, 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;
}
/// <summary>按行分组排序(Y相近归为一行,行内按X从左到右),重新编号</summary>
private static List<BgaBallInfo> SortBgaBalls(List<BgaBallInfo> balls)
{
if (balls.Count == 0) return balls;
// 计算平均焊球半径作为行间距容差
var sortedByY = balls.OrderBy(b => b.CenterY).ToList();
double avgRadius = balls.Average(b => Math.Sqrt(b.BgaArea / Math.PI));
double rowTolerance = avgRadius * 0.8;
var rows = new List<List<BgaBallInfo>>();
var currentRow = new List<BgaBallInfo> { sortedByY[0] };
for (int i = 1; i < sortedByY.Count; i++)
{
if (sortedByY[i].CenterY - currentRow[0].CenterY > rowTolerance)
{
rows.Add(currentRow);
currentRow = new List<BgaBallInfo>();
}
currentRow.Add(sortedByY[i]);
}
rows.Add(currentRow);
// 每行内按X排序,汇总并重新编号
var result = new List<BgaBallInfo>();
foreach (var row in rows)
result.AddRange(row.OrderBy(b => b.CenterX));
for (int i = 0; i < result.Count; i++)
result[i].Index = i + 1;
return result;
}
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 BgaResultItem
{
public int Index { get; set; }
public string Classification { get; set; } = "";
public string CenterX { get; set; } = "";
public string CenterY { get; set; } = "";
public string BgaArea { get; set; } = "";
public string VoidRate { get; set; } = "";
public string MaxVoidRate { get; set; } = "";
public string VoidCount { get; set; } = "";
public string Circularity { get; set; } = "";
}
}
@@ -0,0 +1,105 @@
<Window
x:Class="XplorePlane.Views.ImageProcessing.BgaDetectionPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="BGA检测" Width="720" Height="520"
ResizeMode="CanResize" WindowStartupLocation="CenterOwner"
Topmost="True" ShowInTaskbar="False">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="280" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 左侧:参数面板 -->
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="10">
<CheckBox Content="启用ROI区域" IsChecked="{Binding RoiEnabled}" Margin="0,0,0,6" FontWeight="SemiBold" />
<TextBlock Text="BGA定位参数" FontWeight="SemiBold" Margin="0,0,0,6" />
<TextBlock Text="焊球最小面积" FontSize="11" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="60" Text="{Binding BgaMinArea, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="10" Maximum="100000" Value="{Binding BgaMinArea}" VerticalAlignment="Center" />
</DockPanel>
<TextBlock Text="焊球最大面积" FontSize="11" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="60" Text="{Binding BgaMaxArea, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="100" Maximum="10000000" Value="{Binding BgaMaxArea}" VerticalAlignment="Center" />
</DockPanel>
<TextBlock Text="模糊核大小" FontSize="11" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="60" Text="{Binding BlurSize, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="1" Maximum="31" Value="{Binding BlurSize}" VerticalAlignment="Center" />
</DockPanel>
<TextBlock Text="圆度阈值" FontSize="11" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="60" Text="{Binding Circularity, StringFormat=F2, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="0" Maximum="1" Value="{Binding Circularity}" SmallChange="0.01" LargeChange="0.1" VerticalAlignment="Center" />
</DockPanel>
<Separator Margin="0,4" />
<TextBlock Text="气泡检测参数" FontWeight="SemiBold" Margin="0,4,0,6" />
<TextBlock Text="最小灰度阈值" FontSize="11" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="60" Text="{Binding MinThreshold, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="0" Maximum="255" Value="{Binding MinThreshold}" VerticalAlignment="Center" />
</DockPanel>
<TextBlock Text="最大灰度阈值" FontSize="11" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="60" Text="{Binding MaxThreshold, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="0" Maximum="255" Value="{Binding MaxThreshold}" VerticalAlignment="Center" />
</DockPanel>
<TextBlock Text="最小气泡面积" FontSize="11" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="60" 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="VoidLimit(%)" FontSize="11" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="60" Text="{Binding VoidLimit, StringFormat=F1, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="0" Maximum="100" Value="{Binding VoidLimit}" VerticalAlignment="Center" />
</DockPanel>
<TextBlock Text="最大单气泡限值(%)" FontSize="11" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="60" Text="{Binding MaxSingleVoidLimit, StringFormat=F1, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="0" Maximum="100" Value="{Binding MaxSingleVoidLimit}" VerticalAlignment="Center" />
</DockPanel>
<Separator Margin="0,4" />
<TextBlock Text="{Binding ResultText}" FontSize="14" FontWeight="SemiBold" Margin="0,4,0,8" />
<WrapPanel HorizontalAlignment="Center">
<Button Content="执行检测" Padding="16,6" Margin="0,0,8,0" Command="{Binding ExecuteCommand}" />
<Button Content="关闭" Padding="16,6" Click="Close_Click" />
</WrapPanel>
</StackPanel>
</ScrollViewer>
<!-- 右侧:结果表格 -->
<GroupBox Grid.Column="1" Header="检测结果" Margin="4">
<DataGrid ItemsSource="{Binding Results}"
AutoGenerateColumns="False" IsReadOnly="True"
HeadersVisibility="Column" GridLinesVisibility="Horizontal"
BorderThickness="0" Background="White"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<DataTrigger Binding="{Binding Classification}" Value="FAIL">
<Setter Property="Background" Value="#FFDDDD" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="#" Binding="{Binding Index}" Width="30" />
<DataGridTextColumn Header="判定" Binding="{Binding Classification}" Width="50" />
<DataGridTextColumn Header="中心X" Binding="{Binding CenterX}" Width="65" />
<DataGridTextColumn Header="中心Y" Binding="{Binding CenterY}" Width="65" />
<DataGridTextColumn Header="焊球面积" Binding="{Binding BgaArea}" Width="70" />
<DataGridTextColumn Header="空隙率" Binding="{Binding VoidRate}" Width="65" />
<DataGridTextColumn Header="最大气泡" Binding="{Binding MaxVoidRate}" Width="65" />
<DataGridTextColumn Header="气泡数" Binding="{Binding VoidCount}" Width="55" />
<DataGridTextColumn Header="圆度" Binding="{Binding Circularity}" Width="55" />
</DataGrid.Columns>
</DataGrid>
</GroupBox>
</Grid>
</Window>
@@ -0,0 +1,57 @@
using System.Windows;
using Prism.Ioc;
using XP.ImageProcessing.RoiControl.Controls;
using XplorePlane.Services.MainViewport;
using XplorePlane.ViewModels.ImageProcessing;
namespace XplorePlane.Views.ImageProcessing
{
public partial class BgaDetectionPanel : Window
{
public BgaDetectionPanel()
{
InitializeComponent();
var viewportService = ContainerLocator.Current?.Resolve<IMainViewportService>();
DataContext = new BgaDetectionViewModel(viewportService);
Loaded += (s, e) =>
{
// 获取主界面的 RoiCanvas 传给 ViewModel
var mainWin = Owner as MainWindow;
if (mainWin != null)
{
var canvas = FindChild<PolygonRoiCanvas>(mainWin);
if (DataContext is BgaDetectionViewModel vm)
vm.SetCanvas(canvas);
}
};
Closed += (s, e) =>
{
if (DataContext is BgaDetectionViewModel vm)
{
// 恢复右键菜单,但保留 ROI
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;
}
}
}