diff --git a/XplorePlane/Events/MeasurementToolEvent.cs b/XplorePlane/Events/MeasurementToolEvent.cs index 4356e45..cb561d3 100644 --- a/XplorePlane/Events/MeasurementToolEvent.cs +++ b/XplorePlane/Events/MeasurementToolEvent.cs @@ -51,4 +51,25 @@ namespace XplorePlane.Events public System.Drawing.Rectangle RoiRect { get; set; } public System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)> Detections { get; set; } } + + /// + /// 黑底检测事件(进入ROI绘制模式) + /// + public class BlackBackgroundDetectionEvent : PubSubEvent { } + + /// + /// 黑底检测ROI绘制完成事件 + /// + public class BlackBackgroundRoiDrawnEvent : PubSubEvent { } + + /// + /// 黑底检测结果事件 + /// + public class BlackBackgroundResultEvent : PubSubEvent { } + + public class BlackBackgroundResultPayload + { + 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 6f99e44..da2e756 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -278,6 +278,9 @@ namespace XplorePlane.ViewModels _eventAggregator.GetEvent() .Subscribe(OnWhiteBackgroundRoiDrawn, ThreadOption.UIThread); + _eventAggregator.GetEvent() + .Subscribe(OnBlackBackgroundRoiDrawn, ThreadOption.UIThread); + NavigationTree = new ObservableCollection(); NavigateHomeCommand = new DelegateCommand(OnNavigateHome); @@ -1055,8 +1058,96 @@ namespace XplorePlane.ViewModels private void ExecuteBlackBackgroundDetection() { if (!CheckImageLoaded()) return; - _logger.Info("Black background detection triggered."); - // TODO: 实现黑底检测逻辑 + _logger.Info("Black background detection: entering ROI draw mode."); + _eventAggregator.GetEvent().Publish(); + StatusMessage = "黑底检测:请在图像上拖拽绘制矩形ROI"; + } + + private void OnBlackBackgroundRoiDrawn(System.Windows.Int32Rect roi) + { + try + { + var viewportVm = _containerProvider.Resolve(); + var imageSource = viewportVm?.ImageSource as System.Windows.Media.Imaging.BitmapSource; + if (imageSource == null) return; + + 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; + + 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); + + byte[] roiPixels = new byte[rw * rh]; + gray8.CopyPixels(new System.Windows.Int32Rect(rx, ry, rw, rh), roiPixels, rw, 0); + + 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.Binary | 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); + + const int minArea = 50; + double pixelSize = 0.139; + + 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; + + detections.Add((globalCenter, (int)radiusF, diameterMm)); + } + + _eventAggregator.GetEvent().Publish( + new BlackBackgroundResultPayload + { + RoiRect = new System.Drawing.Rectangle(rx, ry, rw, rh), + Detections = detections + }); + + StatusMessage = $"黑底检测完成:检测到 {detections.Count} 个亮色区域"; + _logger.Info("Black background detection: found {Count} bright regions in ROI ({X},{Y},{W},{H})", + detections.Count, rx, ry, rw, rh); + } + catch (Exception ex) + { + _logger.Error(ex, "Black background detection failed"); + StatusMessage = $"黑底检测失败: {ex.Message}"; + } } private void ExecuteGrayscale() diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs index 9be9e3d..a8a023d 100644 --- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs +++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs @@ -142,21 +142,29 @@ namespace XplorePlane.Views // 白底检测:进入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; + _bgDefectDrawing = false; + _bgDefectRoiMode = BackgroundDefectRoiMode.WhiteBackground; + RegisterBackgroundDefectRoiMouseHandlers(); + }, Prism.Events.ThreadOption.UIThread); + + // 黑底检测:进入ROI绘制模式 + ea2?.GetEvent().Subscribe(() => + { + _bgDefectDrawing = false; + _bgDefectRoiMode = BackgroundDefectRoiMode.BlackBackground; + RegisterBackgroundDefectRoiMouseHandlers(); }, Prism.Events.ThreadOption.UIThread); // 白底检测:渲染结果 ea2?.GetEvent().Subscribe(payload => { - RenderWhiteBackgroundResult(payload); + RenderBackgroundDefectResult(payload.RoiRect, payload.Detections); + }, Prism.Events.ThreadOption.UIThread); + + // 黑底检测:渲染结果 + ea2?.GetEvent().Subscribe(payload => + { + RenderBackgroundDefectResult(payload.RoiRect, payload.Detections); }, Prism.Events.ThreadOption.UIThread); } catch { } @@ -347,7 +355,25 @@ namespace XplorePlane.Views RoiCanvas.SelectedROI = null; if (DataContext is ViewportPanelViewModel vm) vm.ResetMeasurementState(); - SetStatus("已清除所有测量"); + + var canvas = FindChildByName(RoiCanvas, "mainCanvas"); + if (canvas != null) + { + if (_bgDefectPreview != null) + { + canvas.Children.Remove(_bgDefectPreview); + _bgDefectPreview = null; + } + ClearBackgroundDefectOverlays(canvas); + } + else + _bgDefectOverlays.Clear(); + + _bgDefectDrawing = false; + _bgDefectRoiMode = BackgroundDefectRoiMode.None; + try { RoiCanvas.ReleaseMouseCapture(); } catch { /* 未捕获则无影响 */ } + + SetStatus("已清除所有测量及白底/黑底检测结果"); } private void SaveOriginalImage_Click(object sender, RoutedEventArgs e) @@ -400,37 +426,54 @@ namespace XplorePlane.Views #endregion - #region 白底检测 + #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(); + private enum BackgroundDefectRoiMode + { + None, + WhiteBackground, + BlackBackground + } + + private BackgroundDefectRoiMode _bgDefectRoiMode; + private bool _bgDefectDrawing; + private System.Windows.Point _bgDefectStart; + private System.Windows.Shapes.Rectangle _bgDefectPreview; + private readonly System.Collections.Generic.List _bgDefectOverlays = new(); + private bool _bgDefectMouseHandlersRegistered; + + private void RegisterBackgroundDefectRoiMouseHandlers() + { + if (_bgDefectMouseHandlersRegistered) return; + RoiCanvas.PreviewMouseLeftButtonDown += OnMainCanvasPreviewMouseDown; + RoiCanvas.PreviewMouseMove += OnMainCanvasPreviewMouseMove; + RoiCanvas.PreviewMouseLeftButtonUp += OnMainCanvasPreviewMouseUp; + _bgDefectMouseHandlersRegistered = true; + } // 需要在 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; + if (_bgDefectRoiMode == BackgroundDefectRoiMode.None || e.LeftButton != System.Windows.Input.MouseButtonState.Pressed) return; var canvas = FindChildByName(RoiCanvas, "mainCanvas"); if (canvas == null) return; - _whiteDetectStart = e.GetPosition(canvas); - _whiteDetectDrawing = true; + _bgDefectStart = e.GetPosition(canvas); + _bgDefectDrawing = true; // 创建预览矩形(不清除之前的检测结果) - _whiteDetectPreview = new System.Windows.Shapes.Rectangle + _bgDefectPreview = 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); + System.Windows.Controls.Canvas.SetLeft(_bgDefectPreview, _bgDefectStart.X); + System.Windows.Controls.Canvas.SetTop(_bgDefectPreview, _bgDefectStart.Y); + canvas.Children.Add(_bgDefectPreview); RoiCanvas.CaptureMouse(); e.Handled = true; @@ -438,45 +481,46 @@ namespace XplorePlane.Views private void OnMainCanvasPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e) { - if (!_whiteDetectDrawing || _whiteDetectPreview == null) return; + if (!_bgDefectDrawing || _bgDefectPreview == 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); + double x = Math.Min(_bgDefectStart.X, current.X); + double y = Math.Min(_bgDefectStart.Y, current.Y); + double w = Math.Abs(current.X - _bgDefectStart.X); + double h = Math.Abs(current.Y - _bgDefectStart.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); + System.Windows.Controls.Canvas.SetLeft(_bgDefectPreview, x); + System.Windows.Controls.Canvas.SetTop(_bgDefectPreview, y); + _bgDefectPreview.Width = Math.Max(1, w); + _bgDefectPreview.Height = Math.Max(1, h); } private void OnMainCanvasPreviewMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e) { - if (!_whiteDetectDrawing) return; + if (!_bgDefectDrawing) return; - _whiteDetectDrawing = false; - _whiteDetectMode = false; + _bgDefectDrawing = false; + var completedMode = _bgDefectRoiMode; + _bgDefectRoiMode = BackgroundDefectRoiMode.None; 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); + int x = (int)Math.Min(_bgDefectStart.X, end.X); + int y = (int)Math.Min(_bgDefectStart.Y, end.Y); + int w = (int)Math.Abs(end.X - _bgDefectStart.X); + int h = (int)Math.Abs(end.Y - _bgDefectStart.Y); // 移除预览矩形 - if (_whiteDetectPreview != null) + if (_bgDefectPreview != null) { - canvas.Children.Remove(_whiteDetectPreview); - _whiteDetectPreview = null; + canvas.Children.Remove(_bgDefectPreview); + _bgDefectPreview = null; } if (w < 10 || h < 10) return; // 太小忽略 @@ -485,34 +529,40 @@ namespace XplorePlane.Views try { var ea = ContainerLocator.Current?.Resolve(); - ea?.GetEvent().Publish(new System.Windows.Int32Rect(x, y, w, h)); + var rect = new System.Windows.Int32Rect(x, y, w, h); + if (completedMode == BackgroundDefectRoiMode.WhiteBackground) + ea?.GetEvent().Publish(rect); + else if (completedMode == BackgroundDefectRoiMode.BlackBackground) + ea?.GetEvent().Publish(rect); } catch { } e.Handled = true; } - private void RenderWhiteBackgroundResult(WhiteBackgroundResultPayload payload) + private void RenderBackgroundDefectResult( + System.Drawing.Rectangle roiRect, + System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)> detections) { var canvas = FindChildByName(RoiCanvas, "mainCanvas"); - if (canvas == null || payload?.Detections == null) return; + if (canvas == null || detections == null) return; // 绘制ROI矩形(蓝色实线) - var roiRect = new System.Windows.Shapes.Rectangle + var roiShape = new System.Windows.Shapes.Rectangle { Stroke = System.Windows.Media.Brushes.Blue, StrokeThickness = 1, - Width = payload.RoiRect.Width, - Height = payload.RoiRect.Height, + Width = roiRect.Width, + Height = 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); + System.Windows.Controls.Canvas.SetLeft(roiShape, roiRect.X); + System.Windows.Controls.Canvas.SetTop(roiShape, roiRect.Y); + canvas.Children.Add(roiShape); + _bgDefectOverlays.Add(roiShape); - // 绘制每个检测到的黑色区域 - foreach (var (center, radius, sizeMm) in payload.Detections) + // 绘制每个检测区域(白底为暗区、黑底为亮区,可视化相同) + foreach (var (center, radius, sizeMm) in detections) { // 红色虚线圆 var circle = new System.Windows.Shapes.Ellipse @@ -527,7 +577,7 @@ namespace XplorePlane.Views 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); + _bgDefectOverlays.Add(circle); // 45°直径标注线(从圆心向左上到右下) double offset = radius * 0.707; // cos(45°) * radius @@ -542,7 +592,7 @@ namespace XplorePlane.Views IsHitTestVisible = false }; canvas.Children.Add(diamLine); - _whiteDetectOverlays.Add(diamLine); + _bgDefectOverlays.Add(diamLine); // 尺寸标注(在斜线右上方) string label = sizeMm >= 1000 ? $"{sizeMm / 1000:F2} mm" : $"{sizeMm:F0} μm"; @@ -556,15 +606,15 @@ namespace XplorePlane.Views 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); + _bgDefectOverlays.Add(text); } } - private void ClearWhiteDetectOverlays(System.Windows.Controls.Canvas canvas) + private void ClearBackgroundDefectOverlays(System.Windows.Controls.Canvas canvas) { - foreach (var el in _whiteDetectOverlays) + foreach (var el in _bgDefectOverlays) canvas.Children.Remove(el); - _whiteDetectOverlays.Clear(); + _bgDefectOverlays.Clear(); } #endregion