feat(viewport): 黑底检测与白/黑底结果随清除测量一并清除

- 新增黑底检测 Prism 事件与 MainViewModel 中 Otsu 二值化(Binary)流程,与白底(BinaryInv)对称。

- Viewport 统一 ROI 绘制与结果渲染;右键「清除所有测量」同时移除底色检测叠加层并复位 ROI 状态。
This commit is contained in:
李伟
2026-05-14 16:11:14 +08:00
parent 1fb789190c
commit 1ad33cc3e6
3 changed files with 225 additions and 63 deletions
@@ -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; }
}
/// <summary>
/// 黑底检测事件(进入ROI绘制模式)
/// </summary>
public class BlackBackgroundDetectionEvent : PubSubEvent { }
/// <summary>
/// 黑底检测ROI绘制完成事件
/// </summary>
public class BlackBackgroundRoiDrawnEvent : PubSubEvent<System.Windows.Int32Rect> { }
/// <summary>
/// 黑底检测结果事件
/// </summary>
public class BlackBackgroundResultEvent : PubSubEvent<BlackBackgroundResultPayload> { }
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; }
}
}
+93 -2
View File
@@ -278,6 +278,9 @@ namespace XplorePlane.ViewModels
_eventAggregator.GetEvent<WhiteBackgroundRoiDrawnEvent>()
.Subscribe(OnWhiteBackgroundRoiDrawn, ThreadOption.UIThread);
_eventAggregator.GetEvent<BlackBackgroundRoiDrawnEvent>()
.Subscribe(OnBlackBackgroundRoiDrawn, ThreadOption.UIThread);
NavigationTree = new ObservableCollection<object>();
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<BlackBackgroundDetectionEvent>().Publish();
StatusMessage = "黑底检测:请在图像上拖拽绘制矩形ROI";
}
private void OnBlackBackgroundRoiDrawn(System.Windows.Int32Rect roi)
{
try
{
var viewportVm = _containerProvider.Resolve<ViewportPanelViewModel>();
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<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.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<BlackBackgroundResultEvent>().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()
+111 -61
View File
@@ -142,21 +142,29 @@ namespace XplorePlane.Views
// 白底检测:进入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;
_bgDefectDrawing = false;
_bgDefectRoiMode = BackgroundDefectRoiMode.WhiteBackground;
RegisterBackgroundDefectRoiMouseHandlers();
}, Prism.Events.ThreadOption.UIThread);
// 黑底检测:进入ROI绘制模式
ea2?.GetEvent<BlackBackgroundDetectionEvent>().Subscribe(() =>
{
_bgDefectDrawing = false;
_bgDefectRoiMode = BackgroundDefectRoiMode.BlackBackground;
RegisterBackgroundDefectRoiMouseHandlers();
}, Prism.Events.ThreadOption.UIThread);
// 白底检测:渲染结果
ea2?.GetEvent<WhiteBackgroundResultEvent>().Subscribe(payload =>
{
RenderWhiteBackgroundResult(payload);
RenderBackgroundDefectResult(payload.RoiRect, payload.Detections);
}, Prism.Events.ThreadOption.UIThread);
// 黑底检测:渲染结果
ea2?.GetEvent<BlackBackgroundResultEvent>().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<System.Windows.Controls.Canvas>(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<System.Windows.UIElement> _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<System.Windows.UIElement> _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<System.Windows.Controls.Canvas>(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<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);
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<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);
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<Prism.Events.IEventAggregator>();
ea?.GetEvent<WhiteBackgroundRoiDrawnEvent>().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<WhiteBackgroundRoiDrawnEvent>().Publish(rect);
else if (completedMode == BackgroundDefectRoiMode.BlackBackground)
ea?.GetEvent<BlackBackgroundRoiDrawnEvent>().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<System.Windows.Controls.Canvas>(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