From 1fb789190ce25381ecb1ac369613912c7e603223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Thu, 14 May 2026 15:54:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=BF=AB=E6=8D=B7?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=A0=8F=E7=99=BD=E5=BA=95=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XplorePlane/Events/MeasurementToolEvent.cs | 21 ++ XplorePlane/ViewModels/Main/MainViewModel.cs | 105 +++++++++- .../Views/Main/ViewportPanelView.xaml.cs | 189 ++++++++++++++++++ 3 files changed, 313 insertions(+), 2 deletions(-) diff --git a/XplorePlane/Events/MeasurementToolEvent.cs b/XplorePlane/Events/MeasurementToolEvent.cs index 9964acb..4356e45 100644 --- a/XplorePlane/Events/MeasurementToolEvent.cs +++ b/XplorePlane/Events/MeasurementToolEvent.cs @@ -30,4 +30,25 @@ namespace XplorePlane.Events /// 行灰度分布切换事件 /// public class ToggleLineProfileEvent : PubSubEvent { } + + /// + /// 白底检测事件(进入ROI绘制模式) + /// + public class WhiteBackgroundDetectionEvent : PubSubEvent { } + + /// + /// 白底检测ROI绘制完成事件 + /// + public class WhiteBackgroundRoiDrawnEvent : PubSubEvent { } + + /// + /// 白底检测结果事件 + /// + public class WhiteBackgroundResultEvent : PubSubEvent { } + + 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; } + } } diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index 151e895..6f99e44 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -275,6 +275,9 @@ namespace XplorePlane.ViewModels _eventAggregator.GetEvent() .Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread); + _eventAggregator.GetEvent() + .Subscribe(OnWhiteBackgroundRoiDrawn, ThreadOption.UIThread); + NavigationTree = new ObservableCollection(); NavigateHomeCommand = new DelegateCommand(OnNavigateHome); @@ -947,8 +950,106 @@ namespace XplorePlane.ViewModels private void ExecuteWhiteBackgroundDetection() { if (!CheckImageLoaded()) return; - _logger.Info("White background detection triggered."); - // TODO: 实现白底检测逻辑 + _logger.Info("White background detection: entering ROI draw mode."); + _eventAggregator.GetEvent().Publish(); + StatusMessage = "白底检测:请在图像上拖拽绘制矩形ROI"; + } + + private void OnWhiteBackgroundRoiDrawn(System.Windows.Int32Rect roi) + { + try + { + var viewportVm = _containerProvider.Resolve(); + 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(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(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().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() diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs index 50f21c6..9be9e3d 100644 --- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs +++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs @@ -138,6 +138,26 @@ namespace XplorePlane.Views { ToggleLineProfile(); }, Prism.Events.ThreadOption.UIThread); + + // 白底检测:进入ROI绘制模式 + ea2?.GetEvent().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().Subscribe(payload => + { + RenderWhiteBackgroundResult(payload); + }, Prism.Events.ThreadOption.UIThread); } catch { } } @@ -380,6 +400,175 @@ namespace XplorePlane.Views #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 _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(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(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(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(); + ea?.GetEvent().Publish(new System.Windows.Int32Rect(x, y, w, h)); + } + catch { } + + e.Handled = true; + } + + private void RenderWhiteBackgroundResult(WhiteBackgroundResultPayload payload) + { + var canvas = FindChildByName(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(DependencyObject parent, string name) where T : FrameworkElement { int count = VisualTreeHelper.GetChildrenCount(parent);