feat: 实现快捷工具栏白底检测功能
This commit is contained in:
@@ -30,4 +30,25 @@ namespace XplorePlane.Events
|
|||||||
/// 行灰度分布切换事件
|
/// 行灰度分布切换事件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ToggleLineProfileEvent : PubSubEvent { }
|
public class ToggleLineProfileEvent : PubSubEvent { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 白底检测事件(进入ROI绘制模式)
|
||||||
|
/// </summary>
|
||||||
|
public class WhiteBackgroundDetectionEvent : PubSubEvent { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 白底检测ROI绘制完成事件
|
||||||
|
/// </summary>
|
||||||
|
public class WhiteBackgroundRoiDrawnEvent : PubSubEvent<System.Windows.Int32Rect> { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 白底检测结果事件
|
||||||
|
/// </summary>
|
||||||
|
public class WhiteBackgroundResultEvent : PubSubEvent<WhiteBackgroundResultPayload> { }
|
||||||
|
|
||||||
|
public class WhiteBackgroundResultPayload
|
||||||
|
{
|
||||||
|
public System.Drawing.Rectangle RoiRect { get; set; }
|
||||||
|
public System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)> Detections { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,6 +275,9 @@ namespace XplorePlane.ViewModels
|
|||||||
_eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
|
_eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
|
||||||
.Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread);
|
.Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread);
|
||||||
|
|
||||||
|
_eventAggregator.GetEvent<WhiteBackgroundRoiDrawnEvent>()
|
||||||
|
.Subscribe(OnWhiteBackgroundRoiDrawn, ThreadOption.UIThread);
|
||||||
|
|
||||||
NavigationTree = new ObservableCollection<object>();
|
NavigationTree = new ObservableCollection<object>();
|
||||||
|
|
||||||
NavigateHomeCommand = new DelegateCommand(OnNavigateHome);
|
NavigateHomeCommand = new DelegateCommand(OnNavigateHome);
|
||||||
@@ -947,8 +950,106 @@ namespace XplorePlane.ViewModels
|
|||||||
private void ExecuteWhiteBackgroundDetection()
|
private void ExecuteWhiteBackgroundDetection()
|
||||||
{
|
{
|
||||||
if (!CheckImageLoaded()) return;
|
if (!CheckImageLoaded()) return;
|
||||||
_logger.Info("White background detection triggered.");
|
_logger.Info("White background detection: entering ROI draw mode.");
|
||||||
// TODO: 实现白底检测逻辑
|
_eventAggregator.GetEvent<WhiteBackgroundDetectionEvent>().Publish();
|
||||||
|
StatusMessage = "白底检测:请在图像上拖拽绘制矩形ROI";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWhiteBackgroundRoiDrawn(System.Windows.Int32Rect roi)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var viewportVm = _containerProvider.Resolve<ViewportPanelViewModel>();
|
||||||
|
var imageSource = viewportVm?.ImageSource as System.Windows.Media.Imaging.BitmapSource;
|
||||||
|
if (imageSource == null) return;
|
||||||
|
|
||||||
|
// 转为 Gray8
|
||||||
|
System.Windows.Media.Imaging.BitmapSource gray8;
|
||||||
|
if (imageSource.Format != System.Windows.Media.PixelFormats.Gray8)
|
||||||
|
gray8 = new System.Windows.Media.Imaging.FormatConvertedBitmap(
|
||||||
|
imageSource, System.Windows.Media.PixelFormats.Gray8, null, 0);
|
||||||
|
else
|
||||||
|
gray8 = imageSource;
|
||||||
|
|
||||||
|
int imgW = gray8.PixelWidth;
|
||||||
|
int imgH = gray8.PixelHeight;
|
||||||
|
|
||||||
|
// 限制ROI在图像范围内
|
||||||
|
int rx = Math.Clamp(roi.X, 0, imgW - 1);
|
||||||
|
int ry = Math.Clamp(roi.Y, 0, imgH - 1);
|
||||||
|
int rw = Math.Clamp(roi.Width, 1, imgW - rx);
|
||||||
|
int rh = Math.Clamp(roi.Height, 1, imgH - ry);
|
||||||
|
|
||||||
|
// 提取ROI区域像素
|
||||||
|
byte[] roiPixels = new byte[rw * rh];
|
||||||
|
gray8.CopyPixels(new System.Windows.Int32Rect(rx, ry, rw, rh), roiPixels, rw, 0);
|
||||||
|
|
||||||
|
// 使用EmguCV处理
|
||||||
|
using var roiImage = new Emgu.CV.Image<Emgu.CV.Structure.Gray, byte>(rw, rh);
|
||||||
|
for (int y = 0; y < rh; y++)
|
||||||
|
for (int x = 0; x < rw; x++)
|
||||||
|
roiImage.Data[y, x, 0] = roiPixels[y * rw + x];
|
||||||
|
|
||||||
|
// Otsu阈值分割(白底检测黑色区域:反转后黑色区域变白)
|
||||||
|
using var binary = new Emgu.CV.Image<Emgu.CV.Structure.Gray, byte>(rw, rh);
|
||||||
|
Emgu.CV.CvInvoke.Threshold(roiImage, binary, 0, 255,
|
||||||
|
Emgu.CV.CvEnum.ThresholdType.BinaryInv | Emgu.CV.CvEnum.ThresholdType.Otsu);
|
||||||
|
|
||||||
|
// 形态学开运算去噪
|
||||||
|
using var kernel = Emgu.CV.CvInvoke.GetStructuringElement(
|
||||||
|
Emgu.CV.CvEnum.ElementShape.Ellipse, new System.Drawing.Size(3, 3), new System.Drawing.Point(-1, -1));
|
||||||
|
Emgu.CV.CvInvoke.MorphologyEx(binary, binary,
|
||||||
|
Emgu.CV.CvEnum.MorphOp.Open, kernel, new System.Drawing.Point(-1, -1), 1,
|
||||||
|
Emgu.CV.CvEnum.BorderType.Default, new Emgu.CV.Structure.MCvScalar(0));
|
||||||
|
|
||||||
|
// 查找轮廓
|
||||||
|
using var contours = new Emgu.CV.Util.VectorOfVectorOfPoint();
|
||||||
|
using var hierarchy = new Emgu.CV.Mat();
|
||||||
|
Emgu.CV.CvInvoke.FindContours(binary, contours, hierarchy,
|
||||||
|
Emgu.CV.CvEnum.RetrType.External, Emgu.CV.CvEnum.ChainApproxMethod.ChainApproxSimple);
|
||||||
|
|
||||||
|
// 过滤小区域(最小面积默认50像素²)
|
||||||
|
const int minArea = 50;
|
||||||
|
double pixelSize = 0.139; // mm/pixel,默认值(可从比例尺获取)
|
||||||
|
|
||||||
|
var detections = new System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)>();
|
||||||
|
|
||||||
|
for (int i = 0; i < contours.Size; i++)
|
||||||
|
{
|
||||||
|
double area = Emgu.CV.CvInvoke.ContourArea(contours[i]);
|
||||||
|
if (area < minArea) continue;
|
||||||
|
|
||||||
|
// 最小外接圆 - 使用外接矩形计算
|
||||||
|
var boundRect = Emgu.CV.CvInvoke.BoundingRectangle(contours[i]);
|
||||||
|
double radiusF = Math.Max(boundRect.Width, boundRect.Height) / 2.0;
|
||||||
|
var centerF = new System.Drawing.PointF(
|
||||||
|
boundRect.X + boundRect.Width / 2.0f,
|
||||||
|
boundRect.Y + boundRect.Height / 2.0f);
|
||||||
|
|
||||||
|
// 转换到全局坐标
|
||||||
|
var globalCenter = new System.Drawing.Point((int)centerF.X + rx, (int)centerF.Y + ry);
|
||||||
|
double diameterMm = radiusF * 2 * pixelSize * 1000; // 转μm
|
||||||
|
|
||||||
|
detections.Add((globalCenter, (int)radiusF, diameterMm));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发布结果用于绘制(通过OutputData模式传递给ViewportPanelView)
|
||||||
|
_eventAggregator.GetEvent<WhiteBackgroundResultEvent>().Publish(
|
||||||
|
new WhiteBackgroundResultPayload
|
||||||
|
{
|
||||||
|
RoiRect = new System.Drawing.Rectangle(rx, ry, rw, rh),
|
||||||
|
Detections = detections
|
||||||
|
});
|
||||||
|
|
||||||
|
StatusMessage = $"白底检测完成:检测到 {detections.Count} 个黑色区域";
|
||||||
|
_logger.Info("White background detection: found {Count} dark regions in ROI ({X},{Y},{W},{H})",
|
||||||
|
detections.Count, rx, ry, rw, rh);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "White background detection failed");
|
||||||
|
StatusMessage = $"白底检测失败: {ex.Message}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteBlackBackgroundDetection()
|
private void ExecuteBlackBackgroundDetection()
|
||||||
|
|||||||
@@ -138,6 +138,26 @@ namespace XplorePlane.Views
|
|||||||
{
|
{
|
||||||
ToggleLineProfile();
|
ToggleLineProfile();
|
||||||
}, Prism.Events.ThreadOption.UIThread);
|
}, Prism.Events.ThreadOption.UIThread);
|
||||||
|
|
||||||
|
// 白底检测:进入ROI绘制模式
|
||||||
|
ea2?.GetEvent<WhiteBackgroundDetectionEvent>().Subscribe(() =>
|
||||||
|
{
|
||||||
|
_whiteDetectDrawing = false;
|
||||||
|
_whiteDetectMode = true;
|
||||||
|
// 注册鼠标事件(只注册一次)
|
||||||
|
RoiCanvas.PreviewMouseLeftButtonDown -= OnMainCanvasPreviewMouseDown;
|
||||||
|
RoiCanvas.PreviewMouseLeftButtonDown += OnMainCanvasPreviewMouseDown;
|
||||||
|
RoiCanvas.PreviewMouseMove -= OnMainCanvasPreviewMouseMove;
|
||||||
|
RoiCanvas.PreviewMouseMove += OnMainCanvasPreviewMouseMove;
|
||||||
|
RoiCanvas.PreviewMouseLeftButtonUp -= OnMainCanvasPreviewMouseUp;
|
||||||
|
RoiCanvas.PreviewMouseLeftButtonUp += OnMainCanvasPreviewMouseUp;
|
||||||
|
}, Prism.Events.ThreadOption.UIThread);
|
||||||
|
|
||||||
|
// 白底检测:渲染结果
|
||||||
|
ea2?.GetEvent<WhiteBackgroundResultEvent>().Subscribe(payload =>
|
||||||
|
{
|
||||||
|
RenderWhiteBackgroundResult(payload);
|
||||||
|
}, Prism.Events.ThreadOption.UIThread);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
@@ -380,6 +400,175 @@ namespace XplorePlane.Views
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region 白底检测
|
||||||
|
|
||||||
|
private bool _whiteDetectMode;
|
||||||
|
private bool _whiteDetectDrawing;
|
||||||
|
private System.Windows.Point _whiteDetectStart;
|
||||||
|
private System.Windows.Shapes.Rectangle _whiteDetectPreview;
|
||||||
|
private readonly System.Collections.Generic.List<System.Windows.UIElement> _whiteDetectOverlays = new();
|
||||||
|
|
||||||
|
// 需要在 mainCanvas 的 MouseDown/Move/Up 中处理
|
||||||
|
// 由于 PolygonRoiCanvas 内部已经处理了鼠标事件,我们通过 PreviewMouse 事件来拦截
|
||||||
|
|
||||||
|
private void OnMainCanvasPreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_whiteDetectMode || e.LeftButton != System.Windows.Input.MouseButtonState.Pressed) return;
|
||||||
|
|
||||||
|
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
||||||
|
if (canvas == null) return;
|
||||||
|
|
||||||
|
_whiteDetectStart = e.GetPosition(canvas);
|
||||||
|
_whiteDetectDrawing = true;
|
||||||
|
|
||||||
|
// 创建预览矩形(不清除之前的检测结果)
|
||||||
|
_whiteDetectPreview = new System.Windows.Shapes.Rectangle
|
||||||
|
{
|
||||||
|
Stroke = System.Windows.Media.Brushes.Blue,
|
||||||
|
StrokeThickness = 1,
|
||||||
|
StrokeDashArray = new System.Windows.Media.DoubleCollection { 4, 2 }
|
||||||
|
};
|
||||||
|
System.Windows.Controls.Canvas.SetLeft(_whiteDetectPreview, _whiteDetectStart.X);
|
||||||
|
System.Windows.Controls.Canvas.SetTop(_whiteDetectPreview, _whiteDetectStart.Y);
|
||||||
|
canvas.Children.Add(_whiteDetectPreview);
|
||||||
|
|
||||||
|
RoiCanvas.CaptureMouse();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMainCanvasPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_whiteDetectDrawing || _whiteDetectPreview == null) return;
|
||||||
|
|
||||||
|
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
||||||
|
if (canvas == null) return;
|
||||||
|
|
||||||
|
var current = e.GetPosition(canvas);
|
||||||
|
double x = Math.Min(_whiteDetectStart.X, current.X);
|
||||||
|
double y = Math.Min(_whiteDetectStart.Y, current.Y);
|
||||||
|
double w = Math.Abs(current.X - _whiteDetectStart.X);
|
||||||
|
double h = Math.Abs(current.Y - _whiteDetectStart.Y);
|
||||||
|
|
||||||
|
System.Windows.Controls.Canvas.SetLeft(_whiteDetectPreview, x);
|
||||||
|
System.Windows.Controls.Canvas.SetTop(_whiteDetectPreview, y);
|
||||||
|
_whiteDetectPreview.Width = Math.Max(1, w);
|
||||||
|
_whiteDetectPreview.Height = Math.Max(1, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMainCanvasPreviewMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_whiteDetectDrawing) return;
|
||||||
|
|
||||||
|
_whiteDetectDrawing = false;
|
||||||
|
_whiteDetectMode = false;
|
||||||
|
RoiCanvas.ReleaseMouseCapture();
|
||||||
|
|
||||||
|
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
||||||
|
if (canvas == null) return;
|
||||||
|
|
||||||
|
var end = e.GetPosition(canvas);
|
||||||
|
int x = (int)Math.Min(_whiteDetectStart.X, end.X);
|
||||||
|
int y = (int)Math.Min(_whiteDetectStart.Y, end.Y);
|
||||||
|
int w = (int)Math.Abs(end.X - _whiteDetectStart.X);
|
||||||
|
int h = (int)Math.Abs(end.Y - _whiteDetectStart.Y);
|
||||||
|
|
||||||
|
// 移除预览矩形
|
||||||
|
if (_whiteDetectPreview != null)
|
||||||
|
{
|
||||||
|
canvas.Children.Remove(_whiteDetectPreview);
|
||||||
|
_whiteDetectPreview = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w < 10 || h < 10) return; // 太小忽略
|
||||||
|
|
||||||
|
// 发布ROI绘制完成事件
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ea = ContainerLocator.Current?.Resolve<Prism.Events.IEventAggregator>();
|
||||||
|
ea?.GetEvent<WhiteBackgroundRoiDrawnEvent>().Publish(new System.Windows.Int32Rect(x, y, w, h));
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderWhiteBackgroundResult(WhiteBackgroundResultPayload payload)
|
||||||
|
{
|
||||||
|
var canvas = FindChildByName<System.Windows.Controls.Canvas>(RoiCanvas, "mainCanvas");
|
||||||
|
if (canvas == null || payload?.Detections == null) return;
|
||||||
|
|
||||||
|
// 绘制ROI矩形(蓝色实线)
|
||||||
|
var roiRect = new System.Windows.Shapes.Rectangle
|
||||||
|
{
|
||||||
|
Stroke = System.Windows.Media.Brushes.Blue,
|
||||||
|
StrokeThickness = 1,
|
||||||
|
Width = payload.RoiRect.Width,
|
||||||
|
Height = payload.RoiRect.Height,
|
||||||
|
IsHitTestVisible = false
|
||||||
|
};
|
||||||
|
System.Windows.Controls.Canvas.SetLeft(roiRect, payload.RoiRect.X);
|
||||||
|
System.Windows.Controls.Canvas.SetTop(roiRect, payload.RoiRect.Y);
|
||||||
|
canvas.Children.Add(roiRect);
|
||||||
|
_whiteDetectOverlays.Add(roiRect);
|
||||||
|
|
||||||
|
// 绘制每个检测到的黑色区域
|
||||||
|
foreach (var (center, radius, sizeMm) in payload.Detections)
|
||||||
|
{
|
||||||
|
// 红色虚线圆
|
||||||
|
var circle = new System.Windows.Shapes.Ellipse
|
||||||
|
{
|
||||||
|
Stroke = System.Windows.Media.Brushes.Red,
|
||||||
|
StrokeThickness = 1,
|
||||||
|
StrokeDashArray = new System.Windows.Media.DoubleCollection { 4, 2 },
|
||||||
|
Width = radius * 2,
|
||||||
|
Height = radius * 2,
|
||||||
|
IsHitTestVisible = false
|
||||||
|
};
|
||||||
|
System.Windows.Controls.Canvas.SetLeft(circle, center.X - radius);
|
||||||
|
System.Windows.Controls.Canvas.SetTop(circle, center.Y - radius);
|
||||||
|
canvas.Children.Add(circle);
|
||||||
|
_whiteDetectOverlays.Add(circle);
|
||||||
|
|
||||||
|
// 45°直径标注线(从圆心向左上到右下)
|
||||||
|
double offset = radius * 0.707; // cos(45°) * radius
|
||||||
|
var diamLine = new System.Windows.Shapes.Line
|
||||||
|
{
|
||||||
|
X1 = center.X - offset,
|
||||||
|
Y1 = center.Y - offset,
|
||||||
|
X2 = center.X + offset,
|
||||||
|
Y2 = center.Y + offset,
|
||||||
|
Stroke = System.Windows.Media.Brushes.Red,
|
||||||
|
StrokeThickness = 1,
|
||||||
|
IsHitTestVisible = false
|
||||||
|
};
|
||||||
|
canvas.Children.Add(diamLine);
|
||||||
|
_whiteDetectOverlays.Add(diamLine);
|
||||||
|
|
||||||
|
// 尺寸标注(在斜线右上方)
|
||||||
|
string label = sizeMm >= 1000 ? $"{sizeMm / 1000:F2} mm" : $"{sizeMm:F0} μm";
|
||||||
|
var text = new System.Windows.Controls.TextBlock
|
||||||
|
{
|
||||||
|
Text = label,
|
||||||
|
Foreground = System.Windows.Media.Brushes.Red,
|
||||||
|
FontSize = 11,
|
||||||
|
IsHitTestVisible = false
|
||||||
|
};
|
||||||
|
System.Windows.Controls.Canvas.SetLeft(text, center.X + offset + 3);
|
||||||
|
System.Windows.Controls.Canvas.SetTop(text, center.Y - offset - 14);
|
||||||
|
canvas.Children.Add(text);
|
||||||
|
_whiteDetectOverlays.Add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearWhiteDetectOverlays(System.Windows.Controls.Canvas canvas)
|
||||||
|
{
|
||||||
|
foreach (var el in _whiteDetectOverlays)
|
||||||
|
canvas.Children.Remove(el);
|
||||||
|
_whiteDetectOverlays.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
private static T FindChildByName<T>(DependencyObject parent, string name) where T : FrameworkElement
|
private static T FindChildByName<T>(DependencyObject parent, string name) where T : FrameworkElement
|
||||||
{
|
{
|
||||||
int count = VisualTreeHelper.GetChildrenCount(parent);
|
int count = VisualTreeHelper.GetChildrenCount(parent);
|
||||||
|
|||||||
Reference in New Issue
Block a user