384 lines
16 KiB
C#
384 lines
16 KiB
C#
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; } = "";
|
|
}
|
|
}
|