孔隙率:主框架实现,添加辅助面板

This commit is contained in:
李伟
2026-04-29 13:24:37 +08:00
parent b287b83c9f
commit eeaa6b4c98
6 changed files with 638 additions and 5 deletions
@@ -125,7 +125,7 @@ namespace XplorePlane.ViewModels.ImageProcessing
}
};
// 禁用右键菜单(参考点点距方式)
// 禁用右键菜单
_canvas.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, false);
// 订阅画布点击事件来添加 ROI 顶点
@@ -0,0 +1,383 @@
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 VoidDetectionViewModel : BindableBase
{
private readonly IMainViewportService _viewportService;
private BitmapSource _originalImage;
private System.Threading.CancellationTokenSource _debounceCts;
private const int DebounceMs = 300;
public VoidDetectionViewModel(IMainViewportService viewportService)
{
_viewportService = viewportService;
ExecuteCommand = new DelegateCommand(Execute);
PropertyChanged += OnAnyPropertyChanged;
}
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 _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 int _mergeRadius = 3;
public int MergeRadius { get => _mergeRadius; set => SetProperty(ref _mergeRadius, value); }
private int _blurSize = 3;
public int BlurSize { get => _blurSize; set => SetProperty(ref _blurSize, value); }
private double _voidLimit = 25.0;
public double VoidLimit { get => _voidLimit; set => SetProperty(ref _voidLimit, 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<VoidResultItem> 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 VoidMeasurementProcessor();
processor.SetParameter("MinThreshold", MinThreshold);
processor.SetParameter("MaxThreshold", MaxThreshold);
processor.SetParameter("MinVoidArea", MinVoidArea);
processor.SetParameter("MergeRadius", MergeRadius);
processor.SetParameter("BlurSize", BlurSize);
processor.SetParameter("VoidLimit", VoidLimit);
// ROI 注入
if (RoiEnabled && _roiShape != null && _roiShape.Points.Count >= 3)
{
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("Voids"))
{
var voids = output["Voids"] as List<VoidRegionInfo>;
if (voids != null)
{
foreach (var v in voids)
{
Results.Add(new VoidResultItem
{
Index = v.Index,
CenterX = v.CenterX.ToString("F1"),
CenterY = v.CenterY.ToString("F1"),
Area = v.Area.ToString(),
AreaPercent = $"{v.AreaPercent:F2}%"
});
}
}
}
ResultImage = RenderResults(grayImage, output);
ShowResultOnOverlay(ResultImage);
grayImage.Dispose();
}
catch (Exception ex)
{
ResultText = $"错误: {ex.Message}";
}
}
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("VoidMeasurementResult")) return null;
int voidCount = (int)output["VoidCount"];
if (voidCount == 0) return null;
double voidRate = (double)output["VoidRate"];
string classification = (string)output["Classification"];
double voidLimitVal = (double)output["VoidLimit"];
var voids = output["Voids"] as List<VoidRegionInfo>;
var colorImage = new Image<Bgr, byte>(grayImage.Width, grayImage.Height);
CvInvoke.CvtColor(grayImage, colorImage, ColorConversion.Gray2Bgr);
if (voids != null && voids.Count > 0)
{
// 半透明气泡填充
var overlay = colorImage.Clone();
foreach (var v in voids)
{
if (v.ContourPoints.Length > 0)
{
using var vop = new VectorOfPoint(v.ContourPoints);
using var vvop = new VectorOfVectorOfPoint(vop);
CvInvoke.DrawContours(overlay, vvop, 0, new MCvScalar(0, 200, 255), -1);
}
}
CvInvoke.AddWeighted(overlay, 0.4, colorImage, 0.6, 0, colorImage);
overlay.Dispose();
// 绘制轮廓 + 编号
foreach (var v in 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, $"#{v.Index}",
new System.Drawing.Point((int)v.CenterX - 8, (int)v.CenterY + 5),
FontFace.HersheySimplex, 0.35, new MCvScalar(255, 100, 0), 1);
}
// 左上角总览
var overallColor = classification == "PASS"
? new MCvScalar(0, 255, 0) : new MCvScalar(0, 0, 255);
CvInvoke.PutText(colorImage,
$"Void: {voidRate:F1}% | Limit: {voidLimitVal:F0}% | {voidCount} voids | {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 VoidResultItem
{
public int Index { get; set; }
public string CenterX { get; set; } = "";
public string CenterY { get; set; } = "";
public string Area { get; set; } = "";
public string AreaPercent { get; set; } = "";
}
}
@@ -92,6 +92,7 @@ namespace XplorePlane.ViewModels
public DelegateCommand ThroughHoleFillRateMeasureCommand { get; }
public DelegateCommand BgaVoidMeasureCommand { get; }
public DelegateCommand BgaDetectionCommand { get; }
public DelegateCommand VoidDetectionCommand { get; }
public DelegateCommand BubbleMeasureCommand { get; }
// 辅助线命令
@@ -203,6 +204,7 @@ namespace XplorePlane.ViewModels
ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure);
BgaVoidMeasureCommand = new DelegateCommand(ExecuteBgaVoidMeasure);
BgaDetectionCommand = new DelegateCommand(ExecuteBgaDetection);
VoidDetectionCommand = new DelegateCommand(ExecuteVoidDetection);
BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure);
// 辅助线命令
@@ -553,6 +555,26 @@ namespace XplorePlane.ViewModels
_bgaDetectionPanel.Show();
}
private Window _voidDetectionPanel;
private void ExecuteVoidDetection()
{
if (!CheckImageLoaded()) return;
_logger.Info("空隙检测功能已触发");
if (_voidDetectionPanel != null && _voidDetectionPanel.IsVisible)
{
_voidDetectionPanel.Activate();
return;
}
_voidDetectionPanel = new Views.ImageProcessing.VoidDetectionPanel
{
Owner = System.Windows.Application.Current.MainWindow
};
_voidDetectionPanel.Show();
}
private Window _bubbleMeasurePanel;
private void ExecuteBubbleMeasure()
@@ -0,0 +1,179 @@
<Window
x:Class="XplorePlane.Views.ImageProcessing.VoidDetectionPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="空隙检测" Width="700" Height="580"
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="270" />
<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="关闭">
<Image Source="/Assets/Icons/ok.png" Width="20" Height="20" />
</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 MinThreshold, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="0" Maximum="255" Value="{Binding MinThreshold}" VerticalAlignment="Center" />
</DockPanel>
<TextBlock Text="最大灰度阈值" Style="{StaticResource ParamLabel}" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="55" 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="最小空隙面积" 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="100000" 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 MergeRadius, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
<Slider Minimum="0" Maximum="30" Value="{Binding MergeRadius}" VerticalAlignment="Center" />
</DockPanel>
<TextBlock Text="模糊核大小" Style="{StaticResource ParamLabel}" />
<DockPanel Margin="0,0,0,6">
<TextBox DockPanel.Dock="Right" Width="55" 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="VoidLimit(%)" Style="{StaticResource ParamLabel}" />
<DockPanel>
<TextBox DockPanel.Dock="Right" Width="55" 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>
</StackPanel>
</Border>
<!-- 结果摘要 -->
<Border Style="{StaticResource CardStyle}">
<TextBlock Text="{Binding ResultText}" FontSize="13" FontWeight="SemiBold" Foreground="#333" />
</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="65" />
<DataGridTextColumn Header="中心Y" Binding="{Binding CenterY}" Width="65" />
<DataGridTextColumn Header="面积(px)" Binding="{Binding Area}" Width="70" />
<DataGridTextColumn Header="占比" Binding="{Binding AreaPercent}" Width="65" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
</Grid>
</Window>
@@ -0,0 +1,50 @@
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 VoidDetectionPanel : Window
{
public VoidDetectionPanel()
{
InitializeComponent();
var viewportService = ContainerLocator.Current?.Resolve<IMainViewportService>();
DataContext = new VoidDetectionViewModel(viewportService);
Loaded += (s, e) =>
{
var mainWin = Owner as MainWindow;
if (mainWin != null)
{
var canvas = FindChild<PolygonRoiCanvas>(mainWin);
if (DataContext is VoidDetectionViewModel vm)
vm.SetCanvas(canvas);
}
};
Closed += (s, e) =>
{
if (DataContext is VoidDetectionViewModel 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;
}
}
}
+3 -4
View File
@@ -353,11 +353,10 @@
Size="Large"
SmallImage="/Assets/Icons/bga.png" />
<telerik:RadRibbonButton
telerik:ScreenTip.Description="Create a link in your document for quick access to webpages and files.&#13;&#13;Hyperlinks can also take you to places in your document."
telerik:ScreenTip.Title="Add a Hyperlink"
Command="{Binding Path=ShowHyperlinkDialog.Command}"
telerik:ScreenTip.Description="自动检测空隙区域并计算空隙率"
telerik:ScreenTip.Title="空隙检测"
Command="{Binding VoidDetectionCommand}"
Content="孔隙检测"
IsEnabled="{Binding Path=ShowHyperlinkDialog.IsEnabled}"
Size="Large"
SmallImage="/Assets/Icons/Pores.png" />
</telerik:RadRibbonGroup>