Merge branch 'Develop/XP' into turbo-002-cnc
# Conflicts: # XplorePlane/Views/Main/MainWindow.xaml
@@ -3,7 +3,6 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
||||
xmlns:cal="clr-namespace:XP.Camera.Calibration"
|
||||
xmlns:controls="clr-namespace:XP.Camera.Calibration.Controls"
|
||||
mc:Ignorable="d"
|
||||
@@ -20,9 +19,9 @@
|
||||
<SolidColorBrush x:Key="TextSecondaryColor" Color="#666666" />
|
||||
|
||||
<Style x:Key="ToolbarButtonStyle" TargetType="Button">
|
||||
<Setter Property="Width" Value="90" />
|
||||
<Setter Property="Height" Value="70" />
|
||||
<Setter Property="Margin" Value="0,0,8,0" />
|
||||
<Setter Property="Width" Value="80" />
|
||||
<Setter Property="Height" Value="66" />
|
||||
<Setter Property="Margin" Value="0,0,6,0" />
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextColor}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}" />
|
||||
@@ -34,13 +33,13 @@
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="3">
|
||||
CornerRadius="6">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<ContentPresenter Content="{TemplateBinding Tag}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,4,0,4" />
|
||||
Margin="0,2,0,3" />
|
||||
<TextBlock Text="{TemplateBinding Content}"
|
||||
FontSize="12"
|
||||
FontSize="10.5"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
TextWrapping="Wrap"
|
||||
@@ -49,8 +48,8 @@
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#E5F3FF" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentColor}" />
|
||||
<Setter Property="Background" Value="#EAF2FB" />
|
||||
<Setter Property="BorderBrush" Value="#B0D4F1" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#CCE8FF" />
|
||||
@@ -72,43 +71,33 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" Background="{StaticResource PrimaryColor}" BorderBrush="{StaticResource BorderColor}"
|
||||
BorderThickness="0,0,0,1" Padding="15,10">
|
||||
<Border Grid.Row="0" Background="#FAFAFA" BorderBrush="{StaticResource BorderColor}"
|
||||
BorderThickness="0,0,0,1" Padding="12,8">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationLoadImage}"
|
||||
Command="{Binding LoadImageCommand}" FontFamily="Segoe UI"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag>
|
||||
<iconPacks:PackIconMaterial Kind="ImageOutline" Width="24" Height="24" />
|
||||
</Button.Tag>
|
||||
<Button.Tag><Image Source="/Calibration/Resources/添加图像.png" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationLoadCsv}"
|
||||
Command="{Binding LoadCsvCommand}" FontFamily="Segoe UI"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag>
|
||||
<iconPacks:PackIconMaterial Kind="FileDelimited" Width="24" Height="24" />
|
||||
</Button.Tag>
|
||||
<Button.Tag><Image Source="/Calibration/Resources/外部导入.png" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationExecute}"
|
||||
Command="{Binding CalibrateCommand}" FontFamily="Segoe UI"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag>
|
||||
<iconPacks:PackIconMaterial Kind="Crosshairs" Width="24" Height="24" />
|
||||
</Button.Tag>
|
||||
<Button.Tag><Image Source="/Calibration/Resources/执行.png" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationSave}"
|
||||
Command="{Binding SaveCalibrationCommand}" FontFamily="Segoe UI"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag>
|
||||
<iconPacks:PackIconMaterial Kind="ContentSave" Width="24" Height="24" />
|
||||
</Button.Tag>
|
||||
<Button.Tag><Image Source="/Calibration/Resources/保存结果.png" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationLoad}"
|
||||
Command="{Binding LoadCalibrationCommand}" FontFamily="Segoe UI"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag>
|
||||
<iconPacks:PackIconMaterial Kind="FolderOpen" Width="24" Height="24" />
|
||||
</Button.Tag>
|
||||
<Button.Tag><Image Source="/Calibration/Resources/加载数据.png" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<CheckBox Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.CalibrationShowWorld}"
|
||||
VerticalAlignment="Center" FontFamily="Segoe UI"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
||||
xmlns:cal="clr-namespace:XP.Camera.Calibration"
|
||||
xmlns:controls="clr-namespace:XP.Camera.Calibration.Controls"
|
||||
mc:Ignorable="d"
|
||||
@@ -21,9 +20,9 @@
|
||||
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||
|
||||
<Style x:Key="ToolbarButtonStyle" TargetType="Button">
|
||||
<Setter Property="Width" Value="90" />
|
||||
<Setter Property="Height" Value="70" />
|
||||
<Setter Property="Margin" Value="0,0,8,0" />
|
||||
<Setter Property="Width" Value="80" />
|
||||
<Setter Property="Height" Value="66" />
|
||||
<Setter Property="Margin" Value="0,0,6,0" />
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextColor}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}" />
|
||||
@@ -35,13 +34,13 @@
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="3">
|
||||
CornerRadius="6">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<ContentPresenter Content="{TemplateBinding Tag}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,4,0,4" />
|
||||
Margin="0,2,0,3" />
|
||||
<TextBlock Text="{TemplateBinding Content}"
|
||||
FontSize="12" FontFamily="Segoe UI"
|
||||
FontSize="10.5" FontFamily="Segoe UI"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
TextWrapping="Wrap"
|
||||
@@ -50,8 +49,8 @@
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#E5F3FF" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentColor}" />
|
||||
<Setter Property="Background" Value="#EAF2FB" />
|
||||
<Setter Property="BorderBrush" Value="#B0D4F1" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#CCE8FF" />
|
||||
@@ -72,32 +71,32 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" Background="{StaticResource PrimaryColor}" BorderBrush="{StaticResource BorderColor}"
|
||||
BorderThickness="0,0,0,1" Padding="15,10">
|
||||
<Border Grid.Row="0" Background="#FAFAFA" BorderBrush="{StaticResource BorderColor}"
|
||||
BorderThickness="0,0,0,1" Padding="12,8">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardAddImages}" Command="{Binding AddImagesCommand}"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag><iconPacks:PackIconMaterial Kind="ImageMultiple" Width="24" Height="24" /></Button.Tag>
|
||||
<Button.Tag><Image Source="/Calibration/Resources/添加图像.png" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardClearImages}" Command="{Binding ClearImagesCommand}"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag><iconPacks:PackIconMaterial Kind="DeleteSweep" Width="24" Height="24" /></Button.Tag>
|
||||
<Button.Tag><Image Source="/Calibration/Resources/清空列表.png" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardCalibrate}" Command="{Binding CalibrateCommand}"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag><iconPacks:PackIconMaterial Kind="GridLarge" Width="24" Height="24" /></Button.Tag>
|
||||
<Button.Tag><Image Source="/Calibration/Resources/校准.png" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardSave}" Command="{Binding SaveCalibrationCommand}"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag><iconPacks:PackIconMaterial Kind="ContentSave" Width="24" Height="24" /></Button.Tag>
|
||||
<Button.Tag><Image Source="/Calibration/Resources/保存结果.png" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardLoad}" Command="{Binding LoadCalibrationCommand}"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag><iconPacks:PackIconMaterial Kind="FolderOpen" Width="24" Height="24" /></Button.Tag>
|
||||
<Button.Tag><Image Source="/Calibration/Resources/加载数据.png" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=Resources.ChessboardUndistort}" Command="{Binding UndistortImageCommand}"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag><iconPacks:PackIconMaterial Kind="ImageEdit" Width="24" Height="24" /></Button.Tag>
|
||||
<Button.Tag><Image Source="/Calibration/Resources/执行.png" Width="24" Height="24" /></Button.Tag>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 759 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -15,7 +15,6 @@
|
||||
</Reference>
|
||||
<PackageReference Include="Emgu.CV" Version="4.10.0.5680" />
|
||||
<PackageReference Include="Emgu.CV.Bitmap" Version="4.10.0.5680" />
|
||||
<PackageReference Include="MahApps.Metro.IconPacks" Version="6.2.1" />
|
||||
<PackageReference Include="Prism.DryIoc" Version="9.0.537" />
|
||||
<PackageReference Include="Prism.Wpf" Version="9.0.537" />
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
@@ -32,4 +31,8 @@
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="Calibration\Resources\*.png" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -19,11 +19,6 @@
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MahApps.Metro" Version="2.4.11" />
|
||||
<PackageReference Include="MahApps.Metro.IconPacks" Version="6.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\XP.ImageProcessing.Core\XP.ImageProcessing.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -386,12 +386,14 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
private System.Windows.Media.Imaging.WriteableBitmap _bubbleMask;
|
||||
private int _bubbleThreshold = 128;
|
||||
private int _bubbleBrushSize = 5;
|
||||
private double _bubbleVoidLimit = 25.0;
|
||||
private bool _bubbleBrushDragging;
|
||||
private readonly System.Collections.Generic.Stack<byte[]> _bubbleUndoStack = new();
|
||||
|
||||
public void SetBubbleTool(BubbleSubTool tool) => _bubbleTool = tool;
|
||||
public void SetBubbleThreshold(int val) => _bubbleThreshold = val;
|
||||
public void SetBubbleBrushSize(int val) => _bubbleBrushSize = val;
|
||||
public void SetBubbleVoidLimit(double val) { _bubbleVoidLimit = val; UpdateBubbleResult(); }
|
||||
public Rect? BubbleRoi => _bubbleRoi;
|
||||
|
||||
/// <summary>设置 BGA 测量的气泡/焊球绘制模式</summary>
|
||||
@@ -467,9 +469,33 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
_angleTempVDot = _angleTempADot = null; _angleTempLineA = null;
|
||||
_angleTempV = _angleTempA = null; _angleClickCount = 0;
|
||||
_mDraggingDot = null; _mDraggingOwner = null;
|
||||
// 清理气泡测量状态
|
||||
_bubbleRoiRect = null; _bubbleRoiHandle = null; _bubbleResultLabel = null;
|
||||
_bubbleMaskImage = null; _bubbleMask = null;
|
||||
_bubbleRoi = null; _bubbleRoiStart = null;
|
||||
_bubbleRoiDragging = false; _bubbleRoiMoving = false;
|
||||
_bubbleRoiResizing = false; _bubbleBrushDragging = false;
|
||||
_bubbleTool = BubbleSubTool.Roi;
|
||||
_bubbleUndoStack.Clear();
|
||||
// 清理外部叠加的结果图层(IsHitTestVisible=false 的 Image,排除背景图)
|
||||
var toRemove = new System.Collections.Generic.List<UIElement>();
|
||||
foreach (UIElement child in mainCanvas.Children)
|
||||
{
|
||||
if (child is Image img && img != backgroundImage && !img.IsHitTestVisible)
|
||||
toRemove.Add(child);
|
||||
}
|
||||
foreach (var el in toRemove)
|
||||
mainCanvas.Children.Remove(el);
|
||||
}
|
||||
|
||||
public void ClearMeasurements() => RemoveMeasureOverlay();
|
||||
|
||||
/// <summary>从 mainCanvas 移除指定的 UI 元素(用于外部叠加层清理)</summary>
|
||||
public void RemoveFromCanvas(System.Windows.UIElement element)
|
||||
{
|
||||
if (element != null && mainCanvas.Children.Contains(element))
|
||||
mainCanvas.Children.Remove(element);
|
||||
}
|
||||
public int MeasureCount => _ppGroups.Count + _ptlGroups.Count + _angleGroups.Count + _frGroups.Count + _bgaGroups.Count;
|
||||
|
||||
// ── 点击分发 ──
|
||||
@@ -1287,8 +1313,8 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
// 更新 ROI 上方标签
|
||||
if (_bubbleResultLabel != null)
|
||||
{
|
||||
string cls = voidRate <= 25.0 ? "PASS" : "FAIL";
|
||||
_bubbleResultLabel.Text = $"Void: {voidRate:F1}% | {cls}";
|
||||
string cls = voidRate <= _bubbleVoidLimit ? "PASS" : "FAIL";
|
||||
_bubbleResultLabel.Text = $"Void: {voidRate:F1}% | Limit: {_bubbleVoidLimit:F1}% | {cls}";
|
||||
_bubbleResultLabel.Foreground = cls == "PASS" ? Brushes.Lime : Brushes.Red;
|
||||
Canvas.SetLeft(_bubbleResultLabel, roi.X);
|
||||
Canvas.SetTop(_bubbleResultLabel, roi.Y - 20);
|
||||
@@ -1937,14 +1963,44 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
// BGA 模式下阻止 ContextMenu 弹出
|
||||
if (CurrentMeasureMode == Models.MeasureMode.BgaVoid && _bgaCurrent != null)
|
||||
{
|
||||
SuppressContextMenu = true;
|
||||
e.Handled = true;
|
||||
}
|
||||
// 外部请求抑制右键菜单
|
||||
if (SuppressContextMenu)
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
// ROI 选中状态下,右键点击顶点附近时删除顶点并阻止菜单
|
||||
if (SelectedROI is PolygonROI poly && poly.IsSelected && poly.IsEditable && poly.Points.Count > 3)
|
||||
{
|
||||
var pos = e.GetPosition(mainCanvas);
|
||||
double threshold = 15;
|
||||
int nearestIndex = -1;
|
||||
double nearestDist = double.MaxValue;
|
||||
for (int i = 0; i < poly.Points.Count; i++)
|
||||
{
|
||||
var pt = poly.Points[i];
|
||||
double dx = pt.X - pos.X, dy = pt.Y - pos.Y;
|
||||
double dist = Math.Sqrt(dx * dx + dy * dy);
|
||||
if (dist < nearestDist) { nearestDist = dist; nearestIndex = i; }
|
||||
}
|
||||
if (nearestIndex >= 0 && nearestDist < threshold)
|
||||
{
|
||||
poly.Points.RemoveAt(nearestIndex);
|
||||
SuppressContextMenu = true;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>设置为 true 可抑制下一次右键菜单弹出,由外部(如 ViewportPanelView)在 ContextMenuOpening 中检查</summary>
|
||||
public bool SuppressContextMenu { get; set; }
|
||||
|
||||
private void ROI_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 选择ROI
|
||||
if (sender is FrameworkElement element && element.DataContext is ROIShape roi)
|
||||
// 选择ROI(仅可编辑时)
|
||||
if (sender is FrameworkElement element && element.DataContext is ROIShape roi && roi.IsEditable)
|
||||
{
|
||||
SelectedROI = roi;
|
||||
e.Handled = true;
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace XP.ImageProcessing.RoiControl.Models
|
||||
public abstract class ROIShape : INotifyPropertyChanged
|
||||
{
|
||||
private bool _isSelected;
|
||||
private bool _isEditable = true;
|
||||
private string _id = Guid.NewGuid().ToString();
|
||||
private string _color = "Red";
|
||||
|
||||
@@ -36,6 +37,13 @@ namespace XP.ImageProcessing.RoiControl.Models
|
||||
set { _isSelected = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
/// <summary>是否可编辑(拖拽顶点、删除顶点)</summary>
|
||||
public bool IsEditable
|
||||
{
|
||||
get => _isEditable;
|
||||
set { _isEditable = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public string Color
|
||||
{
|
||||
get => _color;
|
||||
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 568 B |
|
After Width: | Height: | Size: 609 B |
|
After Width: | Height: | Size: 676 B |
|
After Width: | Height: | Size: 801 B |
|
After Width: | Height: | Size: 697 B |
|
After Width: | Height: | Size: 516 B |
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 647 B |
|
After Width: | Height: | Size: 424 B |
|
After Width: | Height: | Size: 209 B |
|
After Width: | Height: | Size: 608 B |
@@ -0,0 +1,519 @@
|
||||
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;
|
||||
private System.Windows.Controls.Image _resultOverlayImage;
|
||||
|
||||
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(仅在添加/删除顶点时更新 Adorner,拖拽不触发)
|
||||
_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);
|
||||
|
||||
// 订阅画布点击事件来添加 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 void ShowResultOnOverlay(BitmapSource resultBmp)
|
||||
{
|
||||
if (_canvas == null) return;
|
||||
|
||||
// 移除旧的结果图层
|
||||
RemoveResultOverlay();
|
||||
|
||||
if (resultBmp == null) return;
|
||||
|
||||
// 创建新的结果图层叠加到 canvas 上(插入到背景图之后、ROI之前)
|
||||
_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)
|
||||
{
|
||||
// 插入到索引1(背景图是索引0),这样ROI和测量overlay在上面
|
||||
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 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);
|
||||
|
||||
// 将结果叠加到 canvas overlay 上
|
||||
ShowResultOnOverlay(ResultImage);
|
||||
|
||||
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,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; } = "";
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,8 @@ namespace XplorePlane.ViewModels
|
||||
public DelegateCommand AngleMeasureCommand { get; }
|
||||
public DelegateCommand ThroughHoleFillRateMeasureCommand { get; }
|
||||
public DelegateCommand BgaVoidMeasureCommand { get; }
|
||||
public DelegateCommand BgaDetectionCommand { get; }
|
||||
public DelegateCommand VoidDetectionCommand { get; }
|
||||
public DelegateCommand BubbleMeasureCommand { get; }
|
||||
|
||||
// 辅助线命令
|
||||
@@ -284,6 +286,8 @@ namespace XplorePlane.ViewModels
|
||||
AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure);
|
||||
ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure);
|
||||
BgaVoidMeasureCommand = new DelegateCommand(ExecuteBgaVoidMeasure);
|
||||
BgaDetectionCommand = new DelegateCommand(ExecuteBgaDetection);
|
||||
VoidDetectionCommand = new DelegateCommand(ExecuteVoidDetection);
|
||||
BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure);
|
||||
|
||||
// 辅助线命令
|
||||
@@ -758,6 +762,46 @@ namespace XplorePlane.ViewModels
|
||||
_bgaMeasurePanel.Show();
|
||||
}
|
||||
|
||||
private Window _bgaDetectionPanel;
|
||||
|
||||
private void ExecuteBgaDetection()
|
||||
{
|
||||
if (!CheckImageLoaded()) return;
|
||||
_logger.Info("BGA检测功能已触发");
|
||||
|
||||
if (_bgaDetectionPanel != null && _bgaDetectionPanel.IsVisible)
|
||||
{
|
||||
_bgaDetectionPanel.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
_bgaDetectionPanel = new Views.ImageProcessing.BgaDetectionPanel
|
||||
{
|
||||
Owner = System.Windows.Application.Current.MainWindow
|
||||
};
|
||||
_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,212 @@
|
||||
<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="840" Height="700"
|
||||
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>
|
||||
<!-- TextBox 样式 -->
|
||||
<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>
|
||||
<!-- 参数标签 -->
|
||||
<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>
|
||||
</Window.Resources>
|
||||
<Grid Margin="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="285" />
|
||||
<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>
|
||||
|
||||
<!-- BGA定位参数卡片 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="BGA定位参数" 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 BgaMinArea, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="10" Maximum="100000" Value="{Binding BgaMinArea}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="焊球最大面积" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="55" 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="模糊核大小" 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="圆度阈值" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel>
|
||||
<TextBox DockPanel.Dock="Right" Width="55" 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>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 气泡检测参数卡片 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="气泡检测参数" FontWeight="SemiBold" FontSize="12" Margin="0,0,0,8" />
|
||||
<TextBlock Text="最小灰度阈值" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<TextBox DockPanel.Dock="Right" Width="55" Text="{Binding 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="10000" Value="{Binding MinVoidArea}" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="VoidLimit(%)" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<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>
|
||||
<TextBlock Text="最大单气泡限值(%)" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel>
|
||||
<TextBox DockPanel.Dock="Right" Width="55" 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>
|
||||
</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" Padding="0">
|
||||
<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>
|
||||
<DataTrigger Binding="{Binding Classification}" Value="FAIL">
|
||||
<Setter Property="Background" Value="#FFF0F0" />
|
||||
</DataTrigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#EDF4FC" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGrid.RowStyle>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="#" Binding="{Binding Index}" Width="32" />
|
||||
<DataGridTextColumn Header="判定" Binding="{Binding Classification}" Width="50" />
|
||||
<DataGridTextColumn Header="中心X" Binding="{Binding CenterX}" Width="60" />
|
||||
<DataGridTextColumn Header="中心Y" Binding="{Binding CenterY}" Width="60" />
|
||||
<DataGridTextColumn Header="焊球面积" Binding="{Binding BgaArea}" Width="68" />
|
||||
<DataGridTextColumn Header="空隙率" Binding="{Binding VoidRate}" Width="60" />
|
||||
<DataGridTextColumn Header="最大气泡" Binding="{Binding MaxVoidRate}" Width="65" />
|
||||
<DataGridTextColumn Header="气泡数" Binding="{Binding VoidCount}" Width="52" />
|
||||
<DataGridTextColumn Header="圆度" Binding="{Binding Circularity}" Width="52" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,72 +3,94 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="BGA空隙测量"
|
||||
Width="240" Height="260"
|
||||
Width="280" Height="280"
|
||||
ResizeMode="NoResize"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Topmost="True"
|
||||
ShowInTaskbar="False">
|
||||
Topmost="True" ShowInTaskbar="False"
|
||||
Background="#F5F5F5" FontFamily="Microsoft YaHei UI">
|
||||
<Window.Resources>
|
||||
<Style x:Key="ToolToggleStyle" TargetType="RadioButton">
|
||||
<Style x:Key="IconBtnStyle" TargetType="ButtonBase">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<Border x:Name="Bd" Background="#EEEEEE" BorderBrush="#CCCCCC"
|
||||
BorderThickness="1" CornerRadius="3" Padding="14,5" Cursor="Hand">
|
||||
<ControlTemplate TargetType="ButtonBase">
|
||||
<Border x:Name="Bd" Background="#FFFFFF" BorderBrush="#E0E0E0"
|
||||
BorderThickness="1" CornerRadius="6" Padding="10,6">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#0078D7" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#005A9E" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#DDDDDD" />
|
||||
<Setter TargetName="Bd" Property="Background" Value="#EAF2FB" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#B0D4F1" />
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsMouseOver" Value="True" />
|
||||
<Condition Property="IsChecked" Value="True" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="Bd" Property="Background" Value="#006CBE" />
|
||||
</MultiTrigger>
|
||||
<DataTrigger Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#005FB8" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#004C99" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</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>
|
||||
<StackPanel Margin="10">
|
||||
<!-- 绘制模式 -->
|
||||
<TextBlock Text="绘制模式" FontWeight="SemiBold" Margin="0,0,0,4" />
|
||||
<WrapPanel>
|
||||
<RadioButton x:Name="RbVoid" Content="画气泡" IsChecked="True" Margin="0,0,6,4"
|
||||
Style="{StaticResource ToolToggleStyle}" />
|
||||
<RadioButton x:Name="RbBall" Content="画焊球" Margin="0,0,0,4"
|
||||
Style="{StaticResource ToolToggleStyle}" />
|
||||
</WrapPanel>
|
||||
<TextBlock Text="左键点两次画圆(圆心+半径),右键删除" FontSize="10" Foreground="Gray" Margin="0,2,0,0" />
|
||||
<!-- 工具栏 -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<RadioButton x:Name="RbVoid" IsChecked="True" Style="{StaticResource IconBtnStyle}" ToolTip="画气泡" Margin="0,0,4,0">
|
||||
<Image Source="/Assets/Icons/bubble.png" Width="20" Height="20" />
|
||||
</RadioButton>
|
||||
<RadioButton x:Name="RbBall" Style="{StaticResource IconBtnStyle}" ToolTip="画焊球" Margin="0,0,10,0">
|
||||
<TextBlock Text="焊球" FontSize="11" VerticalAlignment="Center" />
|
||||
</RadioButton>
|
||||
<Button Style="{StaticResource IconBtnStyle}" ToolTip="完成" Click="Finish_Click">
|
||||
<Image Source="/Assets/Icons/ok.png" Width="20" Height="20" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,8" />
|
||||
<!-- 提示 -->
|
||||
<TextBlock Text="左键点两次画圆(圆心+半径),右键删除" FontSize="10" Foreground="#999" Margin="0,0,0,8" />
|
||||
|
||||
<!-- VoidLimit -->
|
||||
<TextBlock Text="VoidLimit(%)" Margin="0,0,0,4" />
|
||||
<DockPanel>
|
||||
<TextBox x:Name="TbVoidLimit" DockPanel.Dock="Right" Width="50" Text="25.0"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider x:Name="SliderVoidLimit" Minimum="0" Maximum="100" Value="25"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<!-- 参数卡片 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="VoidLimit(%)" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel>
|
||||
<TextBox x:Name="TbVoidLimit" DockPanel.Dock="Right" Width="50" Text="25.0"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider x:Name="SliderVoidLimit" Minimum="0" Maximum="100" Value="25"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Separator Margin="0,8" />
|
||||
|
||||
<!-- 结果 -->
|
||||
<TextBlock x:Name="TbResult" Text="空隙率: --" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,8" />
|
||||
|
||||
<!-- 操作 -->
|
||||
<WrapPanel HorizontalAlignment="Center">
|
||||
<Button Content="完成" Padding="14,4" Click="Finish_Click" />
|
||||
</WrapPanel>
|
||||
<!-- 结果卡片 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<TextBlock x:Name="TbResult" Text="空隙率: --" FontSize="13" FontWeight="SemiBold" Foreground="#333" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
</Window>
|
||||
@@ -3,92 +3,116 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="气泡测量工具"
|
||||
Width="260" Height="340"
|
||||
Width="340" Height="380"
|
||||
ResizeMode="NoResize"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Topmost="True"
|
||||
ShowInTaskbar="False">
|
||||
Topmost="True" ShowInTaskbar="False"
|
||||
Background="#F5F5F5" FontFamily="Microsoft YaHei UI">
|
||||
<Window.Resources>
|
||||
<Style x:Key="ToolToggleStyle" TargetType="RadioButton">
|
||||
<Style x:Key="IconBtnStyle" TargetType="ButtonBase">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<Border x:Name="Bd" Background="#EEEEEE" BorderBrush="#CCCCCC"
|
||||
BorderThickness="1" CornerRadius="3" Padding="10,4" Cursor="Hand">
|
||||
<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="IsChecked" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#0078D7" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#005A9E" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#DDDDDD" />
|
||||
<Setter TargetName="Bd" Property="Background" Value="#EAF2FB" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#B0D4F1" />
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsMouseOver" Value="True" />
|
||||
<Condition Property="IsChecked" Value="True" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="Bd" Property="Background" Value="#006CBE" />
|
||||
</MultiTrigger>
|
||||
<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>
|
||||
<StackPanel Margin="10">
|
||||
<!-- 工具选择 -->
|
||||
<TextBlock Text="工具" FontWeight="SemiBold" Margin="0,0,0,4" />
|
||||
<WrapPanel>
|
||||
<RadioButton x:Name="RbRoi" Content="ROI" IsChecked="True" Margin="0,0,4,4"
|
||||
Style="{StaticResource ToolToggleStyle}" />
|
||||
<RadioButton x:Name="RbWand" Content="魔棒" Margin="0,0,4,4"
|
||||
Style="{StaticResource ToolToggleStyle}" />
|
||||
<RadioButton x:Name="RbBrush" Content="画笔" Margin="0,0,4,4"
|
||||
Style="{StaticResource ToolToggleStyle}" />
|
||||
<RadioButton x:Name="RbEraser" Content="橡皮擦" Margin="0,0,0,4"
|
||||
Style="{StaticResource ToolToggleStyle}" />
|
||||
</WrapPanel>
|
||||
<!-- 工具栏:工具选择 + 撤销/清除/完成 -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<RadioButton x:Name="RbRoi" IsChecked="True" Style="{StaticResource IconBtnStyle}" ToolTip="ROI" Margin="0,0,4,0">
|
||||
<Image Source="/Assets/Icons/rectangle32.png" Width="20" Height="20" />
|
||||
</RadioButton>
|
||||
<RadioButton x:Name="RbWand" Style="{StaticResource IconBtnStyle}" ToolTip="魔棒" Margin="0,0,4,0">
|
||||
<Image Source="/Assets/Icons/magic32.png" Width="20" Height="20" />
|
||||
</RadioButton>
|
||||
<RadioButton x:Name="RbBrush" Style="{StaticResource IconBtnStyle}" ToolTip="画笔" Margin="0,0,4,0">
|
||||
<Image Source="/Assets/Icons/brush32.png" Width="20" Height="20" />
|
||||
</RadioButton>
|
||||
<RadioButton x:Name="RbEraser" Style="{StaticResource IconBtnStyle}" ToolTip="橡皮擦" Margin="0,0,10,0">
|
||||
<Image Source="/Assets/Icons/eraser32.png" Width="20" Height="20" />
|
||||
</RadioButton>
|
||||
<Button Style="{StaticResource IconBtnStyle}" ToolTip="撤销" Click="Undo_Click" Margin="0,0,4,0">
|
||||
<Image Source="/Assets/Icons/Undo.png" Width="20" Height="20" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource IconBtnStyle}" ToolTip="清除标记" Click="ClearMask_Click" Margin="0,0,4,0">
|
||||
<Image Source="/Assets/Icons/Clear.png" Width="20" Height="20" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource IconBtnStyle}" ToolTip="完成" Click="Finish_Click">
|
||||
<Image Source="/Assets/Icons/ok.png" Width="20" Height="20" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,6" />
|
||||
<!-- 参数卡片 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="灰度阈值(与点击点的最大灰度差)" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,8">
|
||||
<TextBox x:Name="TbThreshold" DockPanel.Dock="Right" Width="50" Text="30"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider x:Name="SliderThreshold" Minimum="0" Maximum="60" Value="30"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="画笔/橡皮大小" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel>
|
||||
<TextBox x:Name="TbBrushSize" DockPanel.Dock="Right" Width="50" Text="5"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider x:Name="SliderBrushSize" Minimum="1" Maximum="30" Value="5"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 阈值 -->
|
||||
<TextBlock Text="灰度阈值(与点击点的最大灰度差)" Margin="0,0,0,4" FontSize="11" Foreground="Gray" />
|
||||
<DockPanel>
|
||||
<TextBox x:Name="TbThreshold" DockPanel.Dock="Right" Width="45" Text="128"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider x:Name="SliderThreshold" Minimum="0" Maximum="255" Value="128"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- 画笔大小 -->
|
||||
<TextBlock Text="画笔/橡皮大小" Margin="0,8,0,4" />
|
||||
<DockPanel>
|
||||
<TextBox x:Name="TbBrushSize" DockPanel.Dock="Right" Width="45" Text="5"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider x:Name="SliderBrushSize" Minimum="1" Maximum="30" Value="5"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
|
||||
<Separator Margin="0,8" />
|
||||
|
||||
<!-- 结果 -->
|
||||
<TextBlock Text="测量结果" FontWeight="SemiBold" Margin="0,0,0,4" />
|
||||
<TextBlock x:Name="TbResult" Text="空隙率: --" FontSize="14" Margin="0,0,0,4" />
|
||||
<DockPanel Margin="0,0,0,8">
|
||||
<TextBlock Text="VoidLimit(%):" VerticalAlignment="Center" />
|
||||
<TextBox x:Name="TbVoidLimit" Width="50" Text="25.0" Margin="6,0,0,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<WrapPanel HorizontalAlignment="Center">
|
||||
<Button Content="撤销" Padding="12,4" Margin="0,0,8,0" Click="Undo_Click" />
|
||||
<Button Content="清除标记" Padding="12,4" Margin="0,0,8,0" Click="ClearMask_Click" />
|
||||
<Button Content="完成" Padding="12,4" Click="Finish_Click" />
|
||||
</WrapPanel>
|
||||
<!-- 结果卡片 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock x:Name="TbResult" Text="空隙率: --" FontSize="13" FontWeight="SemiBold" Foreground="#333" Margin="0,0,0,6" />
|
||||
<DockPanel>
|
||||
<TextBlock Text="VoidLimit(%):" VerticalAlignment="Center" Style="{StaticResource ParamLabel}" />
|
||||
<TextBox x:Name="TbVoidLimit" Width="50" Text="25.0" Margin="6,0,0,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
|
||||
@@ -48,6 +48,20 @@ namespace XplorePlane.Views.ImageProcessing
|
||||
_canvas?.SetBubbleBrushSize((int)SliderBrushSize.Value);
|
||||
};
|
||||
|
||||
// VoidLimit 同步
|
||||
TbVoidLimit.TextChanged += (s, ev) =>
|
||||
{
|
||||
if (double.TryParse(TbVoidLimit.Text, out double val))
|
||||
_canvas?.SetBubbleVoidLimit(System.Math.Clamp(val, 0, 100));
|
||||
};
|
||||
|
||||
// 初始同步:确保面板默认值推送到 canvas
|
||||
_canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Roi);
|
||||
_canvas?.SetBubbleThreshold((int)SliderThreshold.Value);
|
||||
_canvas?.SetBubbleBrushSize((int)SliderBrushSize.Value);
|
||||
if (double.TryParse(TbVoidLimit.Text, out double initLimit))
|
||||
_canvas?.SetBubbleVoidLimit(System.Math.Clamp(initLimit, 0, 100));
|
||||
|
||||
// 监听 canvas 的工具切换事件(ROI 画完后自动切换时同步面板)
|
||||
if (_canvas != null)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,8 +195,6 @@
|
||||
</StackPanel>
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
|
||||
|
||||
<telerik:RadRibbonGroup Header="测量工具">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
@@ -209,14 +207,14 @@
|
||||
telerik:ScreenTip.Title="点点距测量"
|
||||
Command="{Binding PointDistanceMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/crosshair.png"
|
||||
SmallImage="/Assets/Icons/ptop.png"
|
||||
Text="点点距测量" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="测量点到直线的距离"
|
||||
telerik:ScreenTip.Title="点线距测量"
|
||||
Command="{Binding PointLineDistanceMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/mark.png"
|
||||
SmallImage="/Assets/Icons/ptol.png"
|
||||
Text="点线距测量" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -227,14 +225,14 @@
|
||||
telerik:ScreenTip.Title="角度测量"
|
||||
Command="{Binding AngleMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/dynamic-range.png"
|
||||
SmallImage="/Assets/Icons/angle.png"
|
||||
Text="角度测量" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="测量通孔填锡率"
|
||||
telerik:ScreenTip.Title="通孔填锡率测量"
|
||||
Command="{Binding ThroughHoleFillRateMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/pores.png"
|
||||
SmallImage="/Assets/Icons/cylinder.png"
|
||||
Text="通孔填锡率" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -367,19 +365,17 @@
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="Create a link in your document for quick access to webpages and files. Hyperlinks can also take you to places in your document."
|
||||
telerik:ScreenTip.Title="Add a Hyperlink"
|
||||
Command="{Binding Path=ShowHyperlinkDialog.Command}"
|
||||
telerik:ScreenTip.Description="自动检测BGA焊球并计算空隙率"
|
||||
telerik:ScreenTip.Title="BGA检测"
|
||||
Command="{Binding BgaDetectionCommand}"
|
||||
Content="BGA检测"
|
||||
IsEnabled="{Binding Path=ShowHyperlinkDialog.IsEnabled}"
|
||||
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. 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>
|
||||
@@ -405,7 +401,6 @@
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/spiral.png" />
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
</telerik:RadRibbonTab>
|
||||
|
||||
<telerik:RadRibbonTab Header="操作">
|
||||
@@ -520,8 +515,7 @@
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
|
||||
|
||||
|
||||
<telerik:RadRibbonButton
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/message.png"
|
||||
@@ -534,6 +528,7 @@
|
||||
Text="关于" />
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
|
||||
</telerik:RadRibbonTab>
|
||||
</telerik:RadRibbonView>
|
||||
|
||||
@@ -621,4 +616,4 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
</Window>
|
||||
@@ -33,18 +33,6 @@
|
||||
x:Name="RoiCanvas"
|
||||
Background="White"
|
||||
ImageSource="{Binding ImageSource}">
|
||||
<roi:PolygonRoiCanvas.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="放大" Click="ZoomIn_Click" />
|
||||
<MenuItem Header="缩小" Click="ZoomOut_Click" />
|
||||
<MenuItem Header="适应窗口" Click="ResetView_Click" />
|
||||
<Separator />
|
||||
<MenuItem Header="保存原始图像" Click="SaveOriginalImage_Click" />
|
||||
<MenuItem Header="保存结果图像" Click="SaveResultImage_Click" />
|
||||
<Separator />
|
||||
<MenuItem Header="清除所有测量" Click="ClearAllMeasurements_Click" />
|
||||
</ContextMenu>
|
||||
</roi:PolygonRoiCanvas.ContextMenu>
|
||||
</roi:PolygonRoiCanvas>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -31,6 +31,32 @@ namespace XplorePlane.Views
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
|
||||
// 动态创建右键菜单,支持条件性阻止弹出
|
||||
var menu = new System.Windows.Controls.ContextMenu();
|
||||
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "放大" });
|
||||
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "缩小" });
|
||||
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "适应窗口" });
|
||||
menu.Items.Add(new System.Windows.Controls.Separator());
|
||||
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "保存原始图像" });
|
||||
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "保存结果图像" });
|
||||
menu.Items.Add(new System.Windows.Controls.Separator());
|
||||
menu.Items.Add(new System.Windows.Controls.MenuItem { Header = "清除所有测量" });
|
||||
((System.Windows.Controls.MenuItem)menu.Items[0]).Click += ZoomIn_Click;
|
||||
((System.Windows.Controls.MenuItem)menu.Items[1]).Click += ZoomOut_Click;
|
||||
((System.Windows.Controls.MenuItem)menu.Items[2]).Click += ResetView_Click;
|
||||
((System.Windows.Controls.MenuItem)menu.Items[4]).Click += SaveOriginalImage_Click;
|
||||
((System.Windows.Controls.MenuItem)menu.Items[5]).Click += SaveResultImage_Click;
|
||||
((System.Windows.Controls.MenuItem)menu.Items[7]).Click += ClearAllMeasurements_Click;
|
||||
RoiCanvas.ContextMenu = menu;
|
||||
RoiCanvas.ContextMenuOpening += (s, e) =>
|
||||
{
|
||||
if (RoiCanvas.SuppressContextMenu)
|
||||
{
|
||||
RoiCanvas.SuppressContextMenu = false;
|
||||
e.Handled = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 测量事件 → 更新主界面状态栏
|
||||
RoiCanvas.MeasureCompleted += (s, e) =>
|
||||
{
|
||||
@@ -111,6 +137,8 @@ namespace XplorePlane.Views
|
||||
private void ClearAllMeasurements_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RoiCanvas.ClearMeasurements();
|
||||
RoiCanvas.ROIItems?.Clear();
|
||||
RoiCanvas.SelectedROI = null;
|
||||
if (DataContext is ViewportPanelViewModel vm)
|
||||
vm.ResetMeasurementState();
|
||||
SetStatus("已清除所有测量");
|
||||
|
||||
@@ -2,56 +2,149 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="相机参数设置"
|
||||
Width="320" Height="420"
|
||||
Width="340" Height="440"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ResizeMode="NoResize"
|
||||
ShowInTaskbar="False">
|
||||
<StackPanel Margin="15">
|
||||
ResizeMode="NoResize" ShowInTaskbar="False"
|
||||
Background="#F5F5F5" FontFamily="Microsoft YaHei UI">
|
||||
<Window.Resources>
|
||||
<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>
|
||||
<StackPanel Margin="12">
|
||||
<!-- 曝光和增益 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="曝光时间 (µs)" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,8">
|
||||
<TextBox DockPanel.Dock="Right" Width="65"
|
||||
Text="{Binding ExposureTime, UpdateSourceTrigger=PropertyChanged}"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="10" Maximum="1000000" Value="{Binding ExposureTime}"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="增益" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel>
|
||||
<TextBox DockPanel.Dock="Right" Width="65"
|
||||
Text="{Binding GainValue, UpdateSourceTrigger=PropertyChanged}"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="0" Maximum="24" Value="{Binding GainValue}"
|
||||
SmallChange="0.1" LargeChange="1" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="曝光时间 (µs)" FontSize="11" Foreground="#666" Margin="0,0,0,2" />
|
||||
<DockPanel Margin="0,0,0,10">
|
||||
<Button DockPanel.Dock="Right" Content="设置" Width="45" Height="26" FontSize="11"
|
||||
Margin="6,0,0,0" Command="{Binding ApplyExposureCommand}" />
|
||||
<TextBox Text="{Binding ExposureTime, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="26" FontSize="12" VerticalContentAlignment="Center" Padding="4,0" />
|
||||
</DockPanel>
|
||||
<!-- 分辨率 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="图像宽度 (px)" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel Margin="0,0,0,8">
|
||||
<TextBox DockPanel.Dock="Right" Width="65"
|
||||
Text="{Binding ImageWidth, UpdateSourceTrigger=PropertyChanged}"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="64" Maximum="8192" Value="{Binding ImageWidth}"
|
||||
SmallChange="1" LargeChange="100" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<TextBlock Text="图像高度 (px)" Style="{StaticResource ParamLabel}" />
|
||||
<DockPanel>
|
||||
<TextBox DockPanel.Dock="Right" Width="65"
|
||||
Text="{Binding ImageHeight, UpdateSourceTrigger=PropertyChanged}"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider Minimum="64" Maximum="8192" Value="{Binding ImageHeight}"
|
||||
SmallChange="1" LargeChange="100" VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="增益" FontSize="11" Foreground="#666" Margin="0,0,0,2" />
|
||||
<DockPanel Margin="0,0,0,10">
|
||||
<Button DockPanel.Dock="Right" Content="设置" Width="45" Height="26" FontSize="11"
|
||||
Margin="6,0,0,0" Command="{Binding ApplyGainCommand}" />
|
||||
<TextBox Text="{Binding GainValue, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="26" FontSize="12" VerticalContentAlignment="Center" Padding="4,0" />
|
||||
</DockPanel>
|
||||
<!-- 像素格式 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="像素格式" Style="{StaticResource ParamLabel}" />
|
||||
<ComboBox SelectedItem="{Binding SelectedPixelFormat}"
|
||||
ItemsSource="{Binding PixelFormatOptions}"
|
||||
Height="28" FontSize="11.5" VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="图像宽度 (px)" FontSize="11" Foreground="#666" Margin="0,0,0,2" />
|
||||
<DockPanel Margin="0,0,0,10">
|
||||
<Button DockPanel.Dock="Right" Content="设置" Width="45" Height="26" FontSize="11"
|
||||
Margin="6,0,0,0" Command="{Binding ApplyWidthCommand}" />
|
||||
<TextBox Text="{Binding ImageWidth, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="26" FontSize="12" VerticalContentAlignment="Center" Padding="4,0" />
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock Text="图像高度 (px)" FontSize="11" Foreground="#666" Margin="0,0,0,2" />
|
||||
<DockPanel Margin="0,0,0,10">
|
||||
<Button DockPanel.Dock="Right" Content="设置" Width="45" Height="26" FontSize="11"
|
||||
Margin="6,0,0,0" Command="{Binding ApplyHeightCommand}" />
|
||||
<TextBox Text="{Binding ImageHeight, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="26" FontSize="12" VerticalContentAlignment="Center" Padding="4,0" />
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock Text="像素格式" FontSize="11" Foreground="#666" Margin="0,0,0,2" />
|
||||
<DockPanel Margin="0,0,0,10">
|
||||
<Button DockPanel.Dock="Right" Content="设置" Width="45" Height="26" FontSize="11"
|
||||
Margin="6,0,0,0" Command="{Binding ApplyPixelFormatCommand}" />
|
||||
<ComboBox SelectedItem="{Binding SelectedPixelFormat}"
|
||||
ItemsSource="{Binding PixelFormatOptions}"
|
||||
Height="26" FontSize="12" VerticalContentAlignment="Center" />
|
||||
</DockPanel>
|
||||
|
||||
<Rectangle Height="1" Fill="#E0E0E0" Margin="0,2,0,10" />
|
||||
|
||||
<Button Content="读取当前参数" Height="30" FontSize="12"
|
||||
Command="{Binding RefreshCameraParamsCommand}" />
|
||||
<!-- 操作按钮 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,4,0,0">
|
||||
<Button Content="应用设置" Padding="20,6" FontSize="12" Cursor="Hand" Click="ApplyAll_Click">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#005FB8" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" Background="{TemplateBinding Background}"
|
||||
CornerRadius="6" Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#1A6FC4" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#004C99" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
<Button Content="读取参数" Padding="20,6" FontSize="12" Cursor="Hand" Margin="8,0,0,0"
|
||||
Command="{Binding RefreshCameraParamsCommand}">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="Foreground" Value="#333" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" Background="{TemplateBinding Background}"
|
||||
BorderBrush="#E0E0E0" BorderThickness="1"
|
||||
CornerRadius="6" Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#EAF2FB" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#CCE8FF" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
</Window>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace XplorePlane.Views
|
||||
{
|
||||
@@ -9,5 +10,24 @@ namespace XplorePlane.Views
|
||||
InitializeComponent();
|
||||
DataContext = viewModel;
|
||||
}
|
||||
|
||||
private void ApplyAll_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dc = DataContext;
|
||||
if (dc == null) return;
|
||||
var type = dc.GetType();
|
||||
ExecuteCommand(type, dc, "ApplyExposureCommand");
|
||||
ExecuteCommand(type, dc, "ApplyGainCommand");
|
||||
ExecuteCommand(type, dc, "ApplyWidthCommand");
|
||||
ExecuteCommand(type, dc, "ApplyHeightCommand");
|
||||
ExecuteCommand(type, dc, "ApplyPixelFormatCommand");
|
||||
}
|
||||
|
||||
private static void ExecuteCommand(System.Type type, object dc, string cmdName)
|
||||
{
|
||||
var prop = type.GetProperty(cmdName);
|
||||
if (prop?.GetValue(dc) is ICommand cmd && cmd.CanExecute(null))
|
||||
cmd.Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||