From 7441526ed95f85907397e3e540ede8a791124fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Thu, 14 May 2026 13:48:56 +0800 Subject: [PATCH 01/12] =?UTF-8?q?=E8=A1=8C=E7=81=B0=E5=BA=A6=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XplorePlane/Events/MeasurementToolEvent.cs | 5 + XplorePlane/ViewModels/Main/MainViewModel.cs | 4 +- .../Views/Main/ViewportPanelView.xaml.cs | 171 ++++++++++++++++++ 3 files changed, 178 insertions(+), 2 deletions(-) diff --git a/XplorePlane/Events/MeasurementToolEvent.cs b/XplorePlane/Events/MeasurementToolEvent.cs index daa45a2..9964acb 100644 --- a/XplorePlane/Events/MeasurementToolEvent.cs +++ b/XplorePlane/Events/MeasurementToolEvent.cs @@ -25,4 +25,9 @@ namespace XplorePlane.Events /// 十字辅助线切换事件 /// public class ToggleCrosshairEvent : PubSubEvent { } + + /// + /// 行灰度分布切换事件 + /// + public class ToggleLineProfileEvent : PubSubEvent { } } diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index f71eeb9..151e895 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -961,8 +961,8 @@ namespace XplorePlane.ViewModels private void ExecuteGrayscale() { if (!CheckImageLoaded()) return; - _logger.Info("Grayscale conversion triggered."); - // TODO: 实现灰度转换逻辑 + _logger.Info("Line profile toggled."); + _eventAggregator.GetEvent().Publish(); } private void ExecuteSharpen() diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs index 763d2ab..50f21c6 100644 --- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs +++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.IO; +using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Media; @@ -128,8 +129,178 @@ namespace XplorePlane.Views var vm = GetMainVm(); if (vm != null) vm.CursorInfoText = RoiCanvas.CursorInfo; }); + + // 行灰度分布 + try + { + var ea2 = ContainerLocator.Current?.Resolve(); + ea2?.GetEvent().Subscribe(() => + { + ToggleLineProfile(); + }, Prism.Events.ThreadOption.UIThread); + } + catch { } } + #region 行灰度分布 + + private bool _lineProfileEnabled; + private System.Windows.Shapes.Line _profileRefLine; // 透明命中区域 + private System.Windows.Shapes.Line _profileRefLineVisible; // 1px红线显示 + private System.Windows.Shapes.Polyline _profileCurve; + private double _profileLineY; + private bool _profileDragging; + + private void ToggleLineProfile() + { + _lineProfileEnabled = !_lineProfileEnabled; + var canvas = FindChildByName(RoiCanvas, "mainCanvas"); + if (canvas == null) return; + + if (_lineProfileEnabled) + { + // 参考线默认在图像中间 + _profileLineY = RoiCanvas.CanvasHeight / 2; + + // 创建参考线(红色水平线,可拖动) + // 用透明粗线作为命中区域,叠加1px红线显示 + _profileRefLine = new System.Windows.Shapes.Line + { + X1 = 0, + Y1 = _profileLineY, + X2 = RoiCanvas.CanvasWidth, + Y2 = _profileLineY, + Stroke = System.Windows.Media.Brushes.Transparent, + StrokeThickness = 7, // 上下3px命中区域 + IsHitTestVisible = true, + Cursor = System.Windows.Input.Cursors.SizeNS + }; + _profileRefLineVisible = new System.Windows.Shapes.Line + { + X1 = 0, + Y1 = _profileLineY, + X2 = RoiCanvas.CanvasWidth, + Y2 = _profileLineY, + Stroke = System.Windows.Media.Brushes.Red, + StrokeThickness = 1, + IsHitTestVisible = false + }; + _profileRefLine.MouseLeftButtonDown += ProfileLine_MouseDown; + _profileRefLine.MouseMove += ProfileLine_MouseMove; + _profileRefLine.MouseLeftButtonUp += ProfileLine_MouseUp; + canvas.Children.Add(_profileRefLineVisible); + canvas.Children.Add(_profileRefLine); + + // 创建灰度折线(固定显示在图像中间位置) + _profileCurve = new System.Windows.Shapes.Polyline + { + Stroke = System.Windows.Media.Brushes.Red, + StrokeThickness = 1, + IsHitTestVisible = false + }; + canvas.Children.Add(_profileCurve); + + UpdateLineProfile(); + SetStatus("行灰度分布:拖动红线改变采样行,再次点击按钮关闭"); + } + else + { + if (_profileRefLine != null) + { + canvas.Children.Remove(_profileRefLine); + _profileRefLine = null; + } + if (_profileRefLineVisible != null) + { + canvas.Children.Remove(_profileRefLineVisible); + _profileRefLineVisible = null; + } + if (_profileCurve != null) + { + canvas.Children.Remove(_profileCurve); + _profileCurve = null; + } + SetStatus("行灰度分布已关闭"); + } + } + + private void ProfileLine_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + _profileDragging = true; + _profileRefLine?.CaptureMouse(); + e.Handled = true; + } + + private void ProfileLine_MouseMove(object sender, System.Windows.Input.MouseEventArgs e) + { + if (!_profileDragging || _profileRefLine == null) return; + + var canvas = FindChildByName(RoiCanvas, "mainCanvas"); + if (canvas == null) return; + + var pos = e.GetPosition(canvas); + _profileLineY = Math.Clamp(pos.Y, 0, RoiCanvas.CanvasHeight - 1); + + _profileRefLine.Y1 = _profileLineY; + _profileRefLine.Y2 = _profileLineY; + _profileRefLineVisible.Y1 = _profileLineY; + _profileRefLineVisible.Y2 = _profileLineY; + + UpdateLineProfile(); + } + + private void ProfileLine_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + _profileDragging = false; + _profileRefLine?.ReleaseMouseCapture(); + e.Handled = true; + } + + private void UpdateLineProfile() + { + if (_profileCurve == null) return; + + // 从当前显示图像获取像素数据 + var viewportVm = DataContext as ViewportPanelViewModel; + var imageSource = viewportVm?.ImageSource as System.Windows.Media.Imaging.BitmapSource; + if (imageSource == null) return; + + int imgWidth = imageSource.PixelWidth; + int imgHeight = imageSource.PixelHeight; + int row = (int)Math.Clamp(_profileLineY, 0, imgHeight - 1); + + // 转为 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; + + byte[] rowPixels = new byte[imgWidth]; + int stride = imgWidth; + gray8.CopyPixels(new System.Windows.Int32Rect(0, row, imgWidth, 1), rowPixels, stride, 0); + + // 构建折线点集:折线固定显示在图像垂直中间位置 + // 参考线位置决定采样哪一行,折线位置固定在画布中间 + double canvasH = RoiCanvas.CanvasHeight; + double curveCenter = canvasH / 2.0; // 折线基线固定在图像中间 + double displayHeight = canvasH * 0.25; // 折线振幅为画布高度的25% + + var points = new System.Windows.Media.PointCollection(imgWidth); + for (int x = 0; x < imgWidth; x++) + { + double normalizedGray = rowPixels[x] / 255.0; + double y = curveCenter - normalizedGray * displayHeight; + points.Add(new System.Windows.Point(x, y)); + } + _profileCurve.Points = points; + + SetStatus($"行灰度分布 | Y={row} | 均值={rowPixels.Select(b => (double)b).Average():F1} | 最大={rowPixels.Max()} | 最小={rowPixels.Min()}"); + } + + #endregion + private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { if (e.OldValue is INotifyPropertyChanged oldVm) 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 02/12] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=BF=AB?= =?UTF-8?q?=E6=8D=B7=E5=B7=A5=E5=85=B7=E6=A0=8F=E7=99=BD=E5=BA=95=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=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); From 1ad33cc3e6a5d5eed0caf8ca4137f5327b92c969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Thu, 14 May 2026 16:11:14 +0800 Subject: [PATCH 03/12] =?UTF-8?q?feat(viewport):=20=E9=BB=91=E5=BA=95?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E4=B8=8E=E7=99=BD/=E9=BB=91=E5=BA=95?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E9=9A=8F=E6=B8=85=E9=99=A4=E6=B5=8B=E9=87=8F?= =?UTF-8?q?=E4=B8=80=E5=B9=B6=E6=B8=85=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增黑底检测 Prism 事件与 MainViewModel 中 Otsu 二值化(Binary)流程,与白底(BinaryInv)对称。 - Viewport 统一 ROI 绘制与结果渲染;右键「清除所有测量」同时移除底色检测叠加层并复位 ROI 状态。 --- XplorePlane/Events/MeasurementToolEvent.cs | 21 +++ XplorePlane/ViewModels/Main/MainViewModel.cs | 95 +++++++++- .../Views/Main/ViewportPanelView.xaml.cs | 172 +++++++++++------- 3 files changed, 225 insertions(+), 63 deletions(-) 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 From baef619bd4d7b33f5a13bf4b873fabe3b2b53d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Thu, 14 May 2026 16:27:16 +0800 Subject: [PATCH 04/12] =?UTF-8?q?refactor:=20=E7=99=BD=E5=BA=95/=E9=BB=91?= =?UTF-8?q?=E5=BA=95=E6=A3=80=E6=B5=8B=E7=AE=97=E6=B3=95=E8=BF=81=E8=87=B3?= =?UTF-8?q?=20BackgroundDefectAnalyzer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 XP.ImageProcessing.Processors 新增静态分析类与 BackgroundDefectMode/BackgroundDefectBlob。 - MainViewModel 仅负责灰度 ROI 提取、坐标平移与 Prism 事件发布。 --- .../检测分析/BackgroundDefectAnalyzer.cs | 102 ++++++++++ XplorePlane/ViewModels/Main/MainViewModel.cs | 180 ++++-------------- 2 files changed, 139 insertions(+), 143 deletions(-) create mode 100644 XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs diff --git a/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs b/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs new file mode 100644 index 0000000..8b9d79f --- /dev/null +++ b/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs @@ -0,0 +1,102 @@ +// ============================================================================ +// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. +// 文件名: BackgroundDefectAnalyzer.cs +// 描述: 白底/黑底对比下的缺陷斑点分析(仅 ROI 内计算,不接入流水线算子) +// 算法: Otsu 二值化 → 形态学开运算 → 外轮廓 → 面积过滤 → 外接矩形等效圆 +// 作者: 李伟 wei.lw.li@hexagon.com +// ============================================================================ + +using System.Drawing; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using Emgu.CV.Util; + +namespace XP.ImageProcessing.Processors; + +/// +/// 底色类型:决定 Otsu 后保留的前景是暗区还是亮区。 +/// +public enum BackgroundDefectMode +{ + /// 白底图像上检测偏暗区域(BinaryInv + Otsu)。 + WhiteBackground, + + /// 黑底图像上检测偏亮区域(Binary + Otsu)。 + BlackBackground +} + +/// +/// 单个斑点,坐标相对于输入 ROI 左上角; 与主界面标注逻辑一致(等效直径,微米)。 +/// +public readonly record struct BackgroundDefectBlob(Point CenterInRoi, int RadiusPixels, double SizeMicrometers); + +/// +/// 在灰度 ROI 上执行底色缺陷斑点检测。调用方负责构造与释放 。 +/// +public static class BackgroundDefectAnalyzer +{ + /// + /// 在 ROI 灰度图上检测斑点。 + /// + /// ROI 灰度图(单通道 8 位)。 + /// 白底或黑底模式。 + /// 轮廓最小面积(像素²),小于此值的轮廓丢弃。 + /// 像素物理尺寸(毫米/像素),用于等效直径换算。 + /// 形态学开运算核尺寸(奇数,默认 3)。 + public static List DetectBlobs( + Image roiGray, + BackgroundDefectMode mode, + int minAreaPixels = 50, + double mmPerPixel = 0.139, + int morphKernelSize = 3) + { + if (roiGray == null) throw new ArgumentNullException(nameof(roiGray)); + if (minAreaPixels < 1) minAreaPixels = 1; + if (mmPerPixel <= 0) mmPerPixel = 0.139; + if (morphKernelSize < 1) morphKernelSize = 1; + if ((morphKernelSize & 1) == 0) morphKernelSize++; + + int rw = roiGray.Width; + int rh = roiGray.Height; + if (rw < 1 || rh < 1) return new List(); + + var thresholdType = mode == BackgroundDefectMode.WhiteBackground + ? ThresholdType.BinaryInv | ThresholdType.Otsu + : ThresholdType.Binary | ThresholdType.Otsu; + + using var binary = new Image(rw, rh); + CvInvoke.Threshold(roiGray, binary, 0, 255, thresholdType); + + using var kernel = CvInvoke.GetStructuringElement( + ElementShape.Ellipse, new Size(morphKernelSize, morphKernelSize), new Point(-1, -1)); + CvInvoke.MorphologyEx(binary, binary, MorphOp.Open, kernel, new Point(-1, -1), 1, + BorderType.Default, new MCvScalar(0)); + + using var contours = new VectorOfVectorOfPoint(); + using var hierarchy = new Mat(); + CvInvoke.FindContours(binary, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple); + + var result = new List(); + + for (int i = 0; i < contours.Size; i++) + { + double area = CvInvoke.ContourArea(contours[i]); + if (area < minAreaPixels) continue; + + var boundRect = CvInvoke.BoundingRectangle(contours[i]); + double radiusF = Math.Max(boundRect.Width, boundRect.Height) / 2.0; + var centerF = new PointF( + boundRect.X + boundRect.Width / 2.0f, + boundRect.Y + boundRect.Height / 2.0f); + + var centerInRoi = new Point((int)centerF.X, (int)centerF.Y); + int radiusPx = (int)radiusF; + double sizeMicrometers = radiusF * 2.0 * mmPerPixel * 1000.0; + + result.Add(new BackgroundDefectBlob(centerInRoi, radiusPx, sizeMicrometers)); + } + + return result; + } +} diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index da2e756..c186f3e 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -23,6 +23,7 @@ using XP.Hardware.MotionControl.Abstractions; using XP.Hardware.MotionControl.Services; using XplorePlane.Events; using XplorePlane.Services.MainViewport; +using XP.ImageProcessing.Processors; using XplorePlane.Services.Storage; using XplorePlane.ViewModels.Cnc; using XplorePlane.Views; @@ -958,102 +959,8 @@ namespace XplorePlane.ViewModels 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 OnWhiteBackgroundRoiDrawn(System.Windows.Int32Rect roi) => + RunBackgroundRoiDetection(roi, BackgroundDefectMode.WhiteBackground); private void ExecuteBlackBackgroundDetection() { @@ -1063,7 +970,13 @@ namespace XplorePlane.ViewModels StatusMessage = "黑底检测:请在图像上拖拽绘制矩形ROI"; } - private void OnBlackBackgroundRoiDrawn(System.Windows.Int32Rect roi) + private void OnBlackBackgroundRoiDrawn(System.Windows.Int32Rect roi) => + RunBackgroundRoiDetection(roi, BackgroundDefectMode.BlackBackground); + + /// + /// 从视口灰度图取 ROI,调用 ,再发布结果事件(全局坐标)。 + /// + private void RunBackgroundRoiDetection(System.Windows.Int32Rect roi, BackgroundDefectMode mode) { try { @@ -1089,64 +1002,45 @@ namespace XplorePlane.ViewModels 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); + using var roiImage = new 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; + const double mmPerPixel = 0.139; + var blobs = BackgroundDefectAnalyzer.DetectBlobs(roiImage, mode, minArea, mmPerPixel); - var detections = new System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)>(); - - for (int i = 0; i < contours.Size; i++) + var detections = new System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)>(blobs.Count); + foreach (var b in blobs) { - 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)); + var globalCenter = new System.Drawing.Point(b.CenterInRoi.X + rx, b.CenterInRoi.Y + ry); + detections.Add((globalCenter, b.RadiusPixels, b.SizeMicrometers)); } - _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); + var roiRect = new System.Drawing.Rectangle(rx, ry, rw, rh); + if (mode == BackgroundDefectMode.WhiteBackground) + { + _eventAggregator.GetEvent().Publish( + new WhiteBackgroundResultPayload { RoiRect = roiRect, 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); + } + else + { + _eventAggregator.GetEvent().Publish( + new BlackBackgroundResultPayload { RoiRect = roiRect, 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}"; + string label = mode == BackgroundDefectMode.WhiteBackground ? "白底" : "黑底"; + _logger.Error(ex, "{Label} background detection failed", label); + StatusMessage = $"{label}检测失败: {ex.Message}"; } } From 82465e6510640745f2d5bb8b82da6131a8363a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Fri, 15 May 2026 09:08:44 +0800 Subject: [PATCH 05/12] =?UTF-8?q?=E7=99=BD=E5=BA=95/=E9=BB=91=E5=BA=95?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=EF=BC=9A=E8=BD=AE=E5=BB=93=E4=B8=8E=E6=9C=80?= =?UTF-8?q?=E8=BF=9C=E5=BC=A6=E5=BA=A6=E9=87=8F=EF=BC=8CUI=20=E5=88=86?= =?UTF-8?q?=E8=89=B2=E4=B8=8E=E6=A0=87=E6=B3=A8=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 算子:输出轮廓顶点及顶点间最远弦(微米标定与原先一致) - 视图:实线轮廓;白底红/黑底绿;尺寸文字置于 ROI 外右侧垂直居中 - 事件与 MainViewModel 载荷改为 BackgroundDefectDetectionItem Co-authored-by: Cursor --- .../检测分析/BackgroundDefectAnalyzer.cs | 70 +++++++++++--- XplorePlane/Events/MeasurementToolEvent.cs | 18 +++- XplorePlane/ViewModels/Main/MainViewModel.cs | 13 ++- .../Views/Main/ViewportPanelView.xaml.cs | 91 ++++++++++++------- 4 files changed, 140 insertions(+), 52 deletions(-) diff --git a/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs b/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs index 8b9d79f..36a1776 100644 --- a/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs +++ b/XP.ImageProcessing.Processors/检测分析/BackgroundDefectAnalyzer.cs @@ -2,10 +2,11 @@ // Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. // 文件名: BackgroundDefectAnalyzer.cs // 描述: 白底/黑底对比下的缺陷斑点分析(仅 ROI 内计算,不接入流水线算子) -// 算法: Otsu 二值化 → 形态学开运算 → 外轮廓 → 面积过滤 → 外接矩形等效圆 +// 算法: Otsu 二值化 → 形态学开运算 → 外轮廓 → 面积过滤 → 轮廓顶点最远弦(物理长度与历史等效直径同一标定:mm/px → μm) // 作者: 李伟 wei.lw.li@hexagon.com // ============================================================================ +using System.Collections.Generic; using System.Drawing; using Emgu.CV; using Emgu.CV.CvEnum; @@ -27,9 +28,15 @@ public enum BackgroundDefectMode } /// -/// 单个斑点,坐标相对于输入 ROI 左上角; 与主界面标注逻辑一致(等效直径,微米)。 +/// 单个斑点:轮廓顶点相对于 ROI 左上角; 为轮廓顶点间欧氏距离最大值(微米)。 /// -public readonly record struct BackgroundDefectBlob(Point CenterInRoi, int RadiusPixels, double SizeMicrometers); +public sealed class BackgroundDefectBlob +{ + public Point[] ContourInRoi { get; init; } = Array.Empty(); + public double MaxChordMicrometers { get; init; } + public Point MaxChordEndAInRoi { get; init; } + public Point MaxChordEndBInRoi { get; init; } +} /// /// 在灰度 ROI 上执行底色缺陷斑点检测。调用方负责构造与释放 。 @@ -42,7 +49,7 @@ public static class BackgroundDefectAnalyzer /// ROI 灰度图(单通道 8 位)。 /// 白底或黑底模式。 /// 轮廓最小面积(像素²),小于此值的轮廓丢弃。 - /// 像素物理尺寸(毫米/像素),用于等效直径换算。 + /// 像素物理尺寸(毫米/像素),用于轮廓最远弦换算为微米。 /// 形态学开运算核尺寸(奇数,默认 3)。 public static List DetectBlobs( Image roiGray, @@ -84,19 +91,56 @@ public static class BackgroundDefectAnalyzer double area = CvInvoke.ContourArea(contours[i]); if (area < minAreaPixels) continue; - var boundRect = CvInvoke.BoundingRectangle(contours[i]); - double radiusF = Math.Max(boundRect.Width, boundRect.Height) / 2.0; - var centerF = new PointF( - boundRect.X + boundRect.Width / 2.0f, - boundRect.Y + boundRect.Height / 2.0f); + int n = contours[i].Size; + if (n < 2) continue; - var centerInRoi = new Point((int)centerF.X, (int)centerF.Y); - int radiusPx = (int)radiusF; - double sizeMicrometers = radiusF * 2.0 * mmPerPixel * 1000.0; + var pts = new Point[n]; + for (int j = 0; j < n; j++) + pts[j] = contours[i][j]; - result.Add(new BackgroundDefectBlob(centerInRoi, radiusPx, sizeMicrometers)); + MaxChordInPixelSpace(pts, out double maxChordPx, out Point pa, out Point pb); + double maxChordMicrometers = maxChordPx * mmPerPixel * 1000.0; + + result.Add(new BackgroundDefectBlob + { + ContourInRoi = pts, + MaxChordMicrometers = maxChordMicrometers, + MaxChordEndAInRoi = pa, + MaxChordEndBInRoi = pb + }); } return result; } + + /// 轮廓顶点集合上的最远点对(欧氏距离,像素)。 + private static void MaxChordInPixelSpace(Point[] pts, out double maxChordPx, out Point a, out Point b) + { + maxChordPx = 0; + a = pts[0]; + b = pts.Length > 1 ? pts[1] : pts[0]; + long bestSq = 0; + int bestI = 0, bestJ = 1; + int n = pts.Length; + for (int i = 0; i < n; i++) + { + int iX = pts[i].X, iY = pts[i].Y; + for (int j = i + 1; j < n; j++) + { + long dx = iX - pts[j].X; + long dy = iY - pts[j].Y; + long sq = dx * dx + dy * dy; + if (sq > bestSq) + { + bestSq = sq; + bestI = i; + bestJ = j; + } + } + } + + a = pts[bestI]; + b = pts[bestJ]; + maxChordPx = Math.Sqrt(bestSq); + } } diff --git a/XplorePlane/Events/MeasurementToolEvent.cs b/XplorePlane/Events/MeasurementToolEvent.cs index cb561d3..526e189 100644 --- a/XplorePlane/Events/MeasurementToolEvent.cs +++ b/XplorePlane/Events/MeasurementToolEvent.cs @@ -1,7 +1,21 @@ +using System.Collections.Generic; +using System.Drawing; using Prism.Events; namespace XplorePlane.Events { + /// + /// 白底/黑底检测单条结果:全局图像坐标下的轮廓与最远弦(微米与既有展示规则一致)。 + /// + public class BackgroundDefectDetectionItem + { + public List Contour { get; set; } = new(); + /// 轮廓顶点间最远距离(微米)。 + public double SizeMicrometers { get; set; } + public Point ChordP1 { get; set; } + public Point ChordP2 { get; set; } + } + /// /// 测量工具模式 /// @@ -49,7 +63,7 @@ namespace XplorePlane.Events 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; } + public List Detections { get; set; } = new(); } /// @@ -70,6 +84,6 @@ namespace XplorePlane.Events 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; } + public List Detections { get; set; } = new(); } } diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index c186f3e..965a9b6 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -1011,11 +1011,18 @@ namespace XplorePlane.ViewModels const double mmPerPixel = 0.139; var blobs = BackgroundDefectAnalyzer.DetectBlobs(roiImage, mode, minArea, mmPerPixel); - var detections = new System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)>(blobs.Count); + var detections = new System.Collections.Generic.List(blobs.Count); foreach (var b in blobs) { - var globalCenter = new System.Drawing.Point(b.CenterInRoi.X + rx, b.CenterInRoi.Y + ry); - detections.Add((globalCenter, b.RadiusPixels, b.SizeMicrometers)); + var item = new BackgroundDefectDetectionItem + { + SizeMicrometers = b.MaxChordMicrometers, + ChordP1 = new System.Drawing.Point(b.MaxChordEndAInRoi.X + rx, b.MaxChordEndAInRoi.Y + ry), + ChordP2 = new System.Drawing.Point(b.MaxChordEndBInRoi.X + rx, b.MaxChordEndBInRoi.Y + ry) + }; + foreach (var p in b.ContourInRoi) + item.Contour.Add(new System.Drawing.Point(p.X + rx, p.Y + ry)); + detections.Add(item); } var roiRect = new System.Drawing.Rectangle(rx, ry, rw, rh); diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs index a8a023d..e92db09 100644 --- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs +++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs @@ -155,16 +155,16 @@ namespace XplorePlane.Views RegisterBackgroundDefectRoiMouseHandlers(); }, Prism.Events.ThreadOption.UIThread); - // 白底检测:渲染结果 + // 白底检测:渲染结果(红色标识) ea2?.GetEvent().Subscribe(payload => { - RenderBackgroundDefectResult(payload.RoiRect, payload.Detections); + RenderBackgroundDefectResult(payload.RoiRect, payload.Detections, isBlackBackground: false); }, Prism.Events.ThreadOption.UIThread); - // 黑底检测:渲染结果 + // 黑底检测:渲染结果(绿色标识) ea2?.GetEvent().Subscribe(payload => { - RenderBackgroundDefectResult(payload.RoiRect, payload.Detections); + RenderBackgroundDefectResult(payload.RoiRect, payload.Detections, isBlackBackground: true); }, Prism.Events.ThreadOption.UIThread); } catch { } @@ -542,12 +542,13 @@ namespace XplorePlane.Views private void RenderBackgroundDefectResult( System.Drawing.Rectangle roiRect, - System.Collections.Generic.List<(System.Drawing.Point center, int radius, double sizeMm)> detections) + System.Collections.Generic.IReadOnlyList detections, + bool isBlackBackground) { var canvas = FindChildByName(RoiCanvas, "mainCanvas"); if (canvas == null || detections == null) return; - // 绘制ROI矩形(蓝色实线) + // 绘制ROI矩形(蓝色实线,两种模式一致) var roiShape = new System.Windows.Shapes.Rectangle { Stroke = System.Windows.Media.Brushes.Blue, @@ -561,52 +562,74 @@ namespace XplorePlane.Views canvas.Children.Add(roiShape); _bgDefectOverlays.Add(roiShape); - // 绘制每个检测区域(白底为暗区、黑底为亮区,可视化相同) - foreach (var (center, radius, sizeMm) in detections) + var defectBrush = isBlackBackground + ? System.Windows.Media.Brushes.LimeGreen + : System.Windows.Media.Brushes.Red; + + const int labelPadRightOfRoi = 4; + const double labelLineHeight = 15; + int validCount = detections.Count(d => d.Contour != null && d.Contour.Count >= 2); + double roiMidY = roiRect.Y + roiRect.Height * 0.5; + double labelLeft = roiRect.X + roiRect.Width + labelPadRightOfRoi; + double labelStartY = roiMidY - validCount * labelLineHeight * 0.5; + int labelRow = 0; + + foreach (var d in detections) { - // 红色虚线圆 - var circle = new System.Windows.Shapes.Ellipse + if (d.Contour == null || d.Contour.Count < 2) continue; + + var fig = new PathFigure { - Stroke = System.Windows.Media.Brushes.Red, + StartPoint = new System.Windows.Point(d.Contour[0].X, d.Contour[0].Y), + IsClosed = true + }; + if (d.Contour.Count > 1) + { + fig.Segments.Add(new PolyLineSegment( + d.Contour.Skip(1).Select(p => new System.Windows.Point(p.X, p.Y)), true)); + } + + var geom = new PathGeometry(); + geom.Figures.Add(fig); + var contourPath = new System.Windows.Shapes.Path + { + Data = geom, + Stroke = defectBrush, StrokeThickness = 1, - StrokeDashArray = new System.Windows.Media.DoubleCollection { 4, 2 }, - Width = radius * 2, - Height = radius * 2, + Fill = System.Windows.Media.Brushes.Transparent, IsHitTestVisible = false }; - System.Windows.Controls.Canvas.SetLeft(circle, center.X - radius); - System.Windows.Controls.Canvas.SetTop(circle, center.Y - radius); - canvas.Children.Add(circle); - _bgDefectOverlays.Add(circle); + canvas.Children.Add(contourPath); + _bgDefectOverlays.Add(contourPath); - // 45°直径标注线(从圆心向左上到右下) - double offset = radius * 0.707; // cos(45°) * radius - var diamLine = new System.Windows.Shapes.Line + var chordLine = 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, + X1 = d.ChordP1.X, + Y1 = d.ChordP1.Y, + X2 = d.ChordP2.X, + Y2 = d.ChordP2.Y, + Stroke = defectBrush, + StrokeThickness = 1.5, IsHitTestVisible = false }; - canvas.Children.Add(diamLine); - _bgDefectOverlays.Add(diamLine); + canvas.Children.Add(chordLine); + _bgDefectOverlays.Add(chordLine); + + double um = d.SizeMicrometers; + string label = um >= 1000 ? $"{um / 1000:F2} mm" : $"{um:F0} μm"; - // 尺寸标注(在斜线右上方) - 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, + Foreground = defectBrush, FontSize = 11, IsHitTestVisible = false }; - System.Windows.Controls.Canvas.SetLeft(text, center.X + offset + 3); - System.Windows.Controls.Canvas.SetTop(text, center.Y - offset - 14); + System.Windows.Controls.Canvas.SetLeft(text, labelLeft); + System.Windows.Controls.Canvas.SetTop(text, labelStartY + labelRow * labelLineHeight); canvas.Children.Add(text); _bgDefectOverlays.Add(text); + labelRow++; } } From e0eec42a2f6493cd57babcf5eb8f527f95452b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Fri, 15 May 2026 10:42:20 +0800 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20=E6=A8=A1=E6=9D=BF=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=E5=8A=A9=E6=89=8B=E7=AA=97=E5=8F=A3=E4=B8=8E=E4=B8=BB?= =?UTF-8?q?=E8=A7=86=E5=8F=A3=20ROI=20=E6=B8=85=E9=99=A4=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增模板助手/批量测试窗口、事件与 ViewModel,主窗口入口与 App 注册 - 运行匹配或批量测试前发布清除事件;视口通过 VisualTreeHelper 从父 Panel 移除持久虚线 ROI,避免 FindChild 失败时框残留 - TemplateMatchNative 等相关调整 Co-authored-by: Cursor --- .../定位识别/TemplateMatchNative.cs | 34 +- XplorePlane/App.xaml.cs | 2 + .../Events/TemplateMatchAssistantEvents.cs | 47 ++ .../TemplateMatchAssistantViewModel.cs | 507 ++++++++++++++++++ .../TemplateMatchBatchViewModel.cs | 340 ++++++++++++ XplorePlane/ViewModels/Main/MainViewModel.cs | 45 ++ .../TemplateMatchAssistantWindow.xaml | 274 ++++++++++ .../TemplateMatchAssistantWindow.xaml.cs | 28 + XplorePlane/Views/Main/MainWindow.xaml | 7 + .../Views/Main/ViewportPanelView.xaml.cs | 144 ++++- 10 files changed, 1424 insertions(+), 4 deletions(-) create mode 100644 XplorePlane/Events/TemplateMatchAssistantEvents.cs create mode 100644 XplorePlane/ViewModels/ImageProcessing/TemplateMatchAssistantViewModel.cs create mode 100644 XplorePlane/ViewModels/ImageProcessing/TemplateMatchBatchViewModel.cs create mode 100644 XplorePlane/Views/ImageProcessing/TemplateMatchAssistantWindow.xaml create mode 100644 XplorePlane/Views/ImageProcessing/TemplateMatchAssistantWindow.xaml.cs diff --git a/XP.ImageProcessing.Processors/定位识别/TemplateMatchNative.cs b/XP.ImageProcessing.Processors/定位识别/TemplateMatchNative.cs index 35e0364..043315d 100644 --- a/XP.ImageProcessing.Processors/定位识别/TemplateMatchNative.cs +++ b/XP.ImageProcessing.Processors/定位识别/TemplateMatchNative.cs @@ -30,6 +30,12 @@ public struct TM_Params /// 是否亚像素估计 (1=是, 0=否) public int UseSubPixel; + /// + /// 开启亚像素且角度容差绝对值超过该值时,托管封装会在调用原生库前关闭亚像素, + /// 以避免部分版本 TemplateMatchLib 在 Debug 下出现 vector 越界断言。 + /// + public const double SubPixelAngleSafetyLimitDegrees = 90.0; + /// /// 创建默认参数 /// @@ -168,9 +174,33 @@ public sealed class TemplateMatcherHandle : IDisposable public TM_Result[] Match(IntPtr srcData, int srcWidth, int srcHeight, int srcStep, TM_Params param) { ThrowIfDisposed(); - var results = new TM_Result[param.MaxCount]; + + // 与库默认一致并对齐已知崩溃组合:Debug 下亚像素 + 大角度容差易触发 vector 越界断言; + // 金字塔最小面积过小也可能与内部层级假设不一致。 + int tw = 0, th = 0, _pyramidLayers = 0; + _ = GetTemplateInfo(out tw, out th, out _pyramidLayers); + int templatePixels = Math.Max(0, tw) * Math.Max(0, th); + + int maxCount = Math.Clamp(param.MaxCount, 1, 100); + int minReduce = (int)Math.Clamp(param.MinReduceArea, 64, 4096); + if (templatePixels >= 512) + minReduce = Math.Max(256, minReduce); + if (templatePixels > 0) + minReduce = Math.Min(minReduce, templatePixels); + minReduce = Math.Max(64, minReduce); + + int useSubPixel = param.UseSubPixel; + if (useSubPixel != 0 && Math.Abs(param.ToleranceAngle) > TM_Params.SubPixelAngleSafetyLimitDegrees) + useSubPixel = 0; + + var p = param; + p.MaxCount = maxCount; + p.MinReduceArea = minReduce; + p.UseSubPixel = useSubPixel; + + var results = new TM_Result[p.MaxCount]; int count = TemplateMatchNative.TM_Match(_handle, srcData, srcWidth, srcHeight, srcStep, - ref param, results, param.MaxCount); + ref p, results, p.MaxCount); if (count <= 0) return Array.Empty(); diff --git a/XplorePlane/App.xaml.cs b/XplorePlane/App.xaml.cs index 73eb41c..258a6c0 100644 --- a/XplorePlane/App.xaml.cs +++ b/XplorePlane/App.xaml.cs @@ -47,6 +47,7 @@ using XplorePlane.Services.Recipe; using XplorePlane.Services.Storage; using XplorePlane.ViewModels; using XplorePlane.ViewModels.Cnc; +using XplorePlane.ViewModels.ImageProcessing; using XplorePlane.Views; using XplorePlane.Views.Cnc; @@ -422,6 +423,7 @@ namespace XplorePlane // 注册流水线 ViewModel(每次解析创建新实例) containerRegistry.Register(); containerRegistry.Register(); + containerRegistry.Register(); // 注册硬件库的 ViewModel(供 ViewModelLocator 自动装配) containerRegistry.Register(); diff --git a/XplorePlane/Events/TemplateMatchAssistantEvents.cs b/XplorePlane/Events/TemplateMatchAssistantEvents.cs new file mode 100644 index 0000000..f83747e --- /dev/null +++ b/XplorePlane/Events/TemplateMatchAssistantEvents.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Windows; +using Prism.Events; + +namespace XplorePlane.Events; + +/// +/// 进入「在视口上框选模板 ROI」模式(与主画布 Preview 鼠标逻辑配合)。 +/// +public class TemplateMatchEnterRoiModeEvent : PubSubEvent { } + +/// +/// 模板 ROI 框选完成(图像/画布像素坐标,与白底检测 ROI 约定一致)。仅表示区域已确定,不表示已训练。 +/// +public class TemplateMatchRoiDrawnEvent : PubSubEvent { } + +/// +/// 清除视口上的模板助手持久 ROI 框(例如加载模型后或重置时)。 +/// +public class TemplateMatchClearRoiOverlayEvent : PubSubEvent { } + +/// +/// 单次模板匹配试跑结果,供主视图叠加层绘制。 +/// +public class TemplateMatchHitDto +{ + public double CenterX { get; set; } + public double CenterY { get; set; } + public double Angle { get; set; } + public double Score { get; set; } + public double LtX { get; set; } + public double LtY { get; set; } + public double RtX { get; set; } + public double RtY { get; set; } + public double RbX { get; set; } + public double RbY { get; set; } + public double LbX { get; set; } + public double LbY { get; set; } +} + +public class TemplateMatchPreviewPayload +{ + public List Hits { get; set; } = new(); + public double MatchTimeMs { get; set; } +} + +public class TemplateMatchPreviewResultEvent : PubSubEvent { } diff --git a/XplorePlane/ViewModels/ImageProcessing/TemplateMatchAssistantViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/TemplateMatchAssistantViewModel.cs new file mode 100644 index 0000000..2cb5579 --- /dev/null +++ b/XplorePlane/ViewModels/ImageProcessing/TemplateMatchAssistantViewModel.cs @@ -0,0 +1,507 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Emgu.CV; +using Emgu.CV.Structure; +using Microsoft.Win32; +using Prism.Commands; +using Prism.Events; +using Prism.Ioc; +using Prism.Mvvm; +using XP.Common.Logging.Interfaces; +using XP.ImageProcessing.Processors; +using XplorePlane.Events; +using XplorePlane.Services.MainViewport; +using XplorePlane.ViewModels; +using Serilog; + +namespace XplorePlane.ViewModels.ImageProcessing; + +/// +/// 旋转模板匹配助手:框选 ROI、从 ROI 训练、参数、加载/保存模型、在当页试匹配;批量测试见 。 +/// +public class TemplateMatchAssistantViewModel : BindableBase, IDisposable +{ + private static readonly Serilog.ILogger Log = Serilog.Log.ForContext(); + + private readonly IEventAggregator _eventAggregator; + private readonly IContainerProvider _containerProvider; + private readonly ILoggerService _logger; + private readonly object _matcherLock = new(); + private readonly TemplateMatchBatchViewModel _batch; + private SubscriptionToken? _roiDrawnToken; + private TemplateMatcherHandle? _matcher; + private bool _disposed; + private Int32Rect _pendingTemplateRoi; + private bool _hasPendingTemplateRoi; + + private string _statusMessage = "可先加载模型,或点击「框选模板 ROI」后在主视图框选,再点「从 ROI 训练模板」。批量测试请切换到「批量测试」选项卡。"; + private double _matchThreshold = 0.75; + private double _toleranceAngle; + private double _maxMatchCount = 5; + private double _maxOverlap = 0.3; + private double _minReduceArea = 256; + private bool _useSimd = true; + private bool _useSubPixel; + private bool _isModelReady; + + public TemplateMatchAssistantViewModel( + IEventAggregator eventAggregator, + IContainerProvider containerProvider, + ILoggerService logger) + { + _eventAggregator = eventAggregator; + _containerProvider = containerProvider; + _logger = logger; + + SelectTemplateRoiCommand = new DelegateCommand(ExecuteSelectTemplateRoi); + LearnFromRoiCommand = new DelegateCommand(ExecuteLearnFromRoi, () => _hasPendingTemplateRoi); + LoadModelCommand = new DelegateCommand(ExecuteLoadModel); + SaveModelCommand = new DelegateCommand(ExecuteSaveModel, () => _isModelReady && _matcher != null); + RunMatchCommand = new DelegateCommand(ExecuteRunMatch, () => _isModelReady && _matcher != null); + + _batch = new TemplateMatchBatchViewModel( + this, + _eventAggregator, + _containerProvider.Resolve()); + + _roiDrawnToken = _eventAggregator.GetEvent() + .Subscribe(OnTemplateRoiDrawn, ThreadOption.UIThread); + } + + /// 批量测试子页(与助手共用同一模型与参数)。 + public TemplateMatchBatchViewModel Batch => _batch; + + public DelegateCommand SelectTemplateRoiCommand { get; } + public DelegateCommand LearnFromRoiCommand { get; } + public DelegateCommand LoadModelCommand { get; } + public DelegateCommand SaveModelCommand { get; } + public DelegateCommand RunMatchCommand { get; } + + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + + public double MatchThreshold + { + get => _matchThreshold; + set => SetProperty(ref _matchThreshold, Math.Clamp(value, 0, 1)); + } + + public double ToleranceAngle + { + get => _toleranceAngle; + set => SetProperty(ref _toleranceAngle, Math.Clamp(value, 0, 180)); + } + + /// 最大匹配数(滑块 1~100,运行匹配时取整)。 + public double MaxMatchCount + { + get => _maxMatchCount; + set => SetProperty(ref _maxMatchCount, Math.Clamp(value, 1, 100)); + } + + public double MaxOverlap + { + get => _maxOverlap; + set => SetProperty(ref _maxOverlap, Math.Clamp(value, 0, 1)); + } + + /// 金字塔最小面积(滑块 64~4096,运行匹配时取整)。 + public double MinReduceArea + { + get => _minReduceArea; + set => SetProperty(ref _minReduceArea, Math.Clamp(value, 64, 4096)); + } + + public bool UseSimd + { + get => _useSimd; + set => SetProperty(ref _useSimd, value); + } + + public bool UseSubPixel + { + get => _useSubPixel; + set => SetProperty(ref _useSubPixel, value); + } + + public bool IsModelReady + { + get => _isModelReady; + private set + { + if (SetProperty(ref _isModelReady, value)) + { + SaveModelCommand.RaiseCanExecuteChanged(); + RunMatchCommand.RaiseCanExecuteChanged(); + } + } + } + + /// 是否已有框选完成的模板 ROI(与是否已训练无关)。 + public bool HasPendingTemplateRoi + { + get => _hasPendingTemplateRoi; + private set + { + if (SetProperty(ref _hasPendingTemplateRoi, value)) + LearnFromRoiCommand.RaiseCanExecuteChanged(); + } + } + + /// + /// 清除主视图上框选的模板学习 ROI 叠加,并重置待训练 ROI 状态(运行匹配或开始批量测试前调用)。 + /// + public void ClearTemplateLearningRoiOnViewport() + { + _eventAggregator.GetEvent().Publish(); + HasPendingTemplateRoi = false; + } + + private void ExecuteSelectTemplateRoi() + { + _eventAggregator.GetEvent().Publish(); + StatusMessage = "请在主视图图像上拖拽框选模板区域;框选完成后 ROI 会保留在图上,再点击「从 ROI 训练模板」。"; + } + + private void OnTemplateRoiDrawn(Int32Rect roi) + { + var viewportVm = _containerProvider.Resolve(); + var imageSource = viewportVm?.ImageSource as BitmapSource; + if (imageSource == null) + { + StatusMessage = "当前无主视图图像,无法记录模板 ROI。"; + HasPendingTemplateRoi = false; + return; + } + + int imgW = imageSource.PixelWidth; + int imgH = imageSource.PixelHeight; + int rx = Math.Clamp(roi.X, 0, Math.Max(0, imgW - 1)); + int ry = Math.Clamp(roi.Y, 0, Math.Max(0, imgH - 1)); + int rw = Math.Clamp(roi.Width, 1, Math.Max(1, imgW - rx)); + int rh = Math.Clamp(roi.Height, 1, Math.Max(1, imgH - ry)); + + _pendingTemplateRoi = new Int32Rect(rx, ry, rw, rh); + HasPendingTemplateRoi = true; + StatusMessage = $"已框选模板区域 {rw}×{rh} 像素。请点击「从 ROI 训练模板」。"; + } + + private void ExecuteLearnFromRoi() + { + if (!HasPendingTemplateRoi) return; + + try + { + var viewportVm = _containerProvider.Resolve(); + var imageSource = viewportVm?.ImageSource as BitmapSource; + if (imageSource == null) + { + StatusMessage = "当前无主视图图像,无法学习模板。"; + return; + } + + BitmapSource gray = imageSource.Format == PixelFormats.Gray8 + ? imageSource + : new FormatConvertedBitmap(imageSource, PixelFormats.Gray8, null, 0); + + int imgW = gray.PixelWidth; + int imgH = gray.PixelHeight; + int rx = Math.Clamp(_pendingTemplateRoi.X, 0, imgW - 1); + int ry = Math.Clamp(_pendingTemplateRoi.Y, 0, imgH - 1); + int rw = Math.Clamp(_pendingTemplateRoi.Width, 1, imgW - rx); + int rh = Math.Clamp(_pendingTemplateRoi.Height, 1, imgH - ry); + + var pixels = new byte[rw * rh]; + gray.CopyPixels(new Int32Rect(rx, ry, rw, rh), pixels, rw, 0); + + using var roiImage = new Image(rw, rh); + for (int y = 0; y < rh; y++) + for (int x = 0; x < rw; x++) + roiImage.Data[y, x, 0] = pixels[y * rw + x]; + + lock (_matcherLock) + { + _matcher?.Dispose(); + _matcher = new TemplateMatcherHandle(); + IntPtr p = roiImage.Mat.DataPointer; + int step = (int)roiImage.Mat.Step; + if (!_matcher.LearnPattern(p, rw, rh, step)) + { + _matcher.Dispose(); + _matcher = null; + IsModelReady = false; + StatusMessage = "模板学习失败。"; + _logger.Warn("Template assistant: LearnPattern failed for ROI {0},{1},{2},{3}", rx, ry, rw, rh); + return; + } + } + + IsModelReady = true; + StatusMessage = $"模板已从 ROI 学习成功({rw}×{rh} 像素)。可保存模型或运行匹配。"; + _logger.Info("Template assistant: learned from ROI {0},{1},{2},{3}", rx, ry, rw, rh); + } + catch (Exception ex) + { + Log.Error(ex, "Template assistant ROI learn failed"); + StatusMessage = $"学习失败: {ex.Message}"; + IsModelReady = false; + } + } + + private void ExecuteLoadModel() + { + var dlg = new OpenFileDialog + { + Title = "加载模板模型", + Filter = "模板模型|*.tmmodel;*.tm|所有文件|*.*" + }; + if (dlg.ShowDialog() != true) return; + + try + { + lock (_matcherLock) + { + _matcher?.Dispose(); + _matcher = new TemplateMatcherHandle(); + if (!_matcher.LoadModel(dlg.FileName)) + { + _matcher.Dispose(); + _matcher = null; + IsModelReady = false; + StatusMessage = "模型加载失败。"; + return; + } + } + + HasPendingTemplateRoi = false; + _eventAggregator.GetEvent().Publish(); + + IsModelReady = true; + StatusMessage = $"已加载模型: {Path.GetFileName(dlg.FileName)}"; + } + catch (Exception ex) + { + Log.Error(ex, "LoadModel failed"); + StatusMessage = $"加载失败: {ex.Message}"; + IsModelReady = false; + } + } + + private void ExecuteSaveModel() + { + if (_matcher == null || !IsModelReady) return; + + var dlg = new SaveFileDialog + { + Title = "保存模板模型", + Filter = "模板模型|*.tmmodel|所有文件|*.*", + DefaultExt = ".tmmodel" + }; + if (dlg.ShowDialog() != true) return; + + try + { + bool ok; + lock (_matcherLock) + ok = _matcher != null && _matcher.SaveModel(dlg.FileName); + if (ok) + StatusMessage = $"模型已保存: {dlg.FileName}"; + else + StatusMessage = "模型保存失败。"; + } + catch (Exception ex) + { + Log.Error(ex, "SaveModel failed"); + StatusMessage = $"保存失败: {ex.Message}"; + } + } + + private void ExecuteRunMatch() + { + if (_matcher == null || !IsModelReady) return; + + ClearTemplateLearningRoiOnViewport(); + + try + { + var viewportVm = _containerProvider.Resolve(); + var imageSource = viewportVm?.ImageSource as BitmapSource; + if (imageSource == null) + { + StatusMessage = "当前无主视图图像。"; + return; + } + + using Image? full = BitmapSourceToGrayImage(imageSource); + if (full == null) return; + + bool forcedSubPixelOff = UseSubPixel && + Math.Abs(ToleranceAngle) > TM_Params.SubPixelAngleSafetyLimitDegrees; + int templatePixels = 0; + lock (_matcherLock) + { + if (_matcher != null && _matcher.GetTemplateInfo(out int tw, out int th, out _)) + templatePixels = Math.Max(0, tw) * Math.Max(0, th); + } + + bool bumpedMinReduce = templatePixels >= 512 && + (int)Math.Clamp(Math.Round(MinReduceArea), 64, 4096) < 256; + + if (!TryMatchGrayImage(full, out IReadOnlyList hitsReadonly, out double t, out string? matchErr)) + { + if (matchErr != null && matchErr.Contains("TemplateMatchLib", StringComparison.OrdinalIgnoreCase)) + StatusMessage = "未找到 TemplateMatchLib.dll,请确认已复制到输出目录。"; + else + StatusMessage = string.IsNullOrEmpty(matchErr) ? "匹配失败。" : matchErr; + _eventAggregator.GetEvent().Publish(new TemplateMatchPreviewPayload()); + return; + } + + var hits = new List(hitsReadonly); + + _eventAggregator.GetEvent().Publish( + new TemplateMatchPreviewPayload { Hits = hits, MatchTimeMs = t }); + + StatusMessage = hits.Count == 0 + ? $"未找到匹配(耗时 {t:F1} ms)。可调低阈值或角度范围。" + : $"匹配到 {hits.Count} 个目标,耗时 {t:F1} ms。"; + if (forcedSubPixelOff) + StatusMessage += $" 已自动关闭亚像素(角度容差>{TM_Params.SubPixelAngleSafetyLimitDegrees:F0}° 时匹配库易崩溃)。"; + if (bumpedMinReduce) + StatusMessage += " 已将金字塔最小面积提升至不低于 256(与库默认一致)。"; + } + catch (Exception ex) + { + Log.Error(ex, "RunMatch failed"); + StatusMessage = $"匹配失败: {ex.Message}"; + _eventAggregator.GetEvent().Publish(new TemplateMatchPreviewPayload()); + } + } + + private TM_Params BuildCurrentMatchParams() => new TM_Params + { + Score = MatchThreshold, + ToleranceAngle = ToleranceAngle, + MaxOverlap = MaxOverlap, + MaxCount = (int)Math.Clamp(Math.Round(MaxMatchCount), 1, 100), + MinReduceArea = (int)Math.Clamp(Math.Round(MinReduceArea), 64, 4096), + UseSIMD = UseSimd ? 1 : 0, + UseSubPixel = UseSubPixel ? 1 : 0 + }; + + /// + /// 使用当前助手参数对灰度图做模板匹配(与单张「运行匹配」一致)。供批量测试在后台线程调用,内部已加锁。 + /// + public bool TryMatchGrayImage(Image fullImage, out IReadOnlyList hits, + out double matchTimeMs, out string? errorMessage) + { + hits = Array.Empty(); + matchTimeMs = 0; + errorMessage = null; + if (fullImage == null) + { + errorMessage = "图像为空"; + return false; + } + + lock (_matcherLock) + { + if (_matcher == null || !IsModelReady) + { + errorMessage = "模型未就绪"; + return false; + } + + try + { + var param = BuildCurrentMatchParams(); + var results = _matcher.Match( + fullImage.Mat.DataPointer, + fullImage.Width, + fullImage.Height, + (int)fullImage.Mat.Step, + param); + + matchTimeMs = _matcher.LastMatchTime; + var list = new List(); + foreach (var r in results) + { + list.Add(new TemplateMatchHitDto + { + CenterX = r.CenterX, + CenterY = r.CenterY, + Angle = r.Angle, + Score = r.Score, + LtX = r.LtX, LtY = r.LtY, + RtX = r.RtX, RtY = r.RtY, + RbX = r.RbX, RbY = r.RbY, + LbX = r.LbX, LbY = r.LbY + }); + } + + hits = list; + return true; + } + catch (DllNotFoundException) + { + errorMessage = "TemplateMatchLib.dll 未找到"; + return false; + } + catch (Exception ex) + { + Log.Error(ex, "TryMatchGrayImage failed"); + errorMessage = ex.Message; + return false; + } + } + } + + internal static Image? BitmapSourceToGrayImage(BitmapSource bitmapSource) + { + BitmapSource source = bitmapSource.Format == PixelFormats.Gray8 + ? bitmapSource + : new FormatConvertedBitmap(bitmapSource, PixelFormats.Gray8, null, 0); + + int width = source.PixelWidth; + int height = source.PixelHeight; + if (width < 1 || height < 1) return null; + + int stride = width; + var pixels = new byte[width * height]; + source.CopyPixels(pixels, stride, 0); + + var image = new Image(width, height); + for (int y = 0; y < height; y++) + for (int x = 0; x < width; x++) + image.Data[y, x, 0] = pixels[y * stride + x]; + + return image; + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + if (_roiDrawnToken != null) + { + _eventAggregator.GetEvent().Unsubscribe(_roiDrawnToken); + _roiDrawnToken = null; + } + + _batch.Dispose(); + + lock (_matcherLock) + { + _matcher?.Dispose(); + _matcher = null; + } + } +} diff --git a/XplorePlane/ViewModels/ImageProcessing/TemplateMatchBatchViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/TemplateMatchBatchViewModel.cs new file mode 100644 index 0000000..fa70d43 --- /dev/null +++ b/XplorePlane/ViewModels/ImageProcessing/TemplateMatchBatchViewModel.cs @@ -0,0 +1,340 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using Microsoft.Win32; +using Prism.Commands; +using Prism.Events; +using Prism.Mvvm; +using XplorePlane.Events; +using XplorePlane.Services.MainViewport; + +namespace XplorePlane.ViewModels.ImageProcessing; + +/// 单张批量匹配结果行(绑定到 DataGrid)。 +public sealed class TemplateMatchBatchRow +{ + public string FileName { get; init; } = ""; + public string FullPath { get; init; } = ""; + /// 简要结果:命中 / 未找到 / 失败原因摘要。 + public string Result { get; init; } = ""; + public int MatchCount { get; init; } + public double BestScore { get; init; } + public double TimeMs { get; init; } + public string? ErrorDetail { get; init; } + public IReadOnlyList Hits { get; init; } = Array.Empty(); +} + +/// +/// 模板匹配批量测试:扫描文件夹、逐张匹配(与助手当前参数一致)、在主视口打开选中结果。 +/// +public class TemplateMatchBatchViewModel : BindableBase, IDisposable +{ + private static readonly Serilog.ILogger Log = Serilog.Log.ForContext(); + private static readonly HashSet ImageExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".bmp", ".png", ".jpg", ".jpeg", ".tif", ".tiff" + }; + + private readonly TemplateMatchAssistantViewModel _assistant; + private readonly IEventAggregator _eventAggregator; + private readonly IMainViewportService _mainViewportService; + private readonly List _imagePaths = new(); + private CancellationTokenSource? _batchCts; + private bool _disposed; + private bool _isRunning; + private string _folderPath = ""; + private string _batchStatusText = "请选择文件夹后点击「开始批量匹配」。"; + private TemplateMatchBatchRow? _selectedRow; + private int _imageFileCount; + + public TemplateMatchBatchViewModel( + TemplateMatchAssistantViewModel assistant, + IEventAggregator eventAggregator, + IMainViewportService mainViewportService) + { + _assistant = assistant ?? throw new ArgumentNullException(nameof(assistant)); + _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); + _mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService)); + + _assistant.PropertyChanged += OnAssistantPropertyChanged; + + PickFolderCommand = new DelegateCommand(ExecutePickFolder); + StartBatchCommand = new DelegateCommand(ExecuteStartBatch, CanStartBatch); + StopBatchCommand = new DelegateCommand(ExecuteStopBatch, () => _isRunning); + OpenSelectedInMainViewportCommand = new DelegateCommand(ExecuteOpenSelectedInMainViewport, () => SelectedRow != null); + } + + private void OnAssistantPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(TemplateMatchAssistantViewModel.IsModelReady)) + StartBatchCommand.RaiseCanExecuteChanged(); + } + + public ObservableCollection Rows { get; } = new(); + + public DelegateCommand PickFolderCommand { get; } + public DelegateCommand StartBatchCommand { get; } + public DelegateCommand StopBatchCommand { get; } + public DelegateCommand OpenSelectedInMainViewportCommand { get; } + + public string FolderPath + { + get => _folderPath; + private set => SetProperty(ref _folderPath, value); + } + + public string BatchStatusText + { + get => _batchStatusText; + private set => SetProperty(ref _batchStatusText, value); + } + + public int ImageFileCount + { + get => _imageFileCount; + private set => SetProperty(ref _imageFileCount, value); + } + + public bool IsRunning + { + get => _isRunning; + private set + { + if (SetProperty(ref _isRunning, value)) + { + StartBatchCommand.RaiseCanExecuteChanged(); + StopBatchCommand.RaiseCanExecuteChanged(); + } + } + } + + public TemplateMatchBatchRow? SelectedRow + { + get => _selectedRow; + set + { + if (SetProperty(ref _selectedRow, value)) + OpenSelectedInMainViewportCommand.RaiseCanExecuteChanged(); + } + } + + /// 供宿主窗口 DataGrid 双击调用。 + public void OpenSelectedFromDoubleClick() => OpenSelectedInMainViewportCommand.Execute(); + + private bool CanStartBatch() => + !_isRunning && _imagePaths.Count > 0 && _assistant.IsModelReady; + + private void ExecutePickFolder() + { + var dlg = new OpenFolderDialog + { + Title = "选择待测试图像所在文件夹(仅当前目录,不含子文件夹)", + InitialDirectory = Directory.Exists(_folderPath) ? _folderPath : Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) + }; + + if (dlg.ShowDialog() != true) + return; + + ScanFolder(dlg.FolderName); + } + + private void ScanFolder(string folder) + { + _imagePaths.Clear(); + if (!Directory.Exists(folder)) + { + FolderPath = ""; + ImageFileCount = 0; + BatchStatusText = "文件夹不存在。"; + StartBatchCommand.RaiseCanExecuteChanged(); + return; + } + + foreach (var path in Directory.EnumerateFiles(folder, "*.*", SearchOption.TopDirectoryOnly) + .OrderBy(p => p, StringComparer.OrdinalIgnoreCase)) + { + if (ImageExtensions.Contains(Path.GetExtension(path))) + _imagePaths.Add(path); + } + + FolderPath = folder; + ImageFileCount = _imagePaths.Count; + BatchStatusText = ImageFileCount == 0 + ? "该文件夹下没有支持的图像文件(bmp/png/jpg/tif…)。" + : $"已扫描 {ImageFileCount} 个图像文件,可开始批量匹配。"; + StartBatchCommand.RaiseCanExecuteChanged(); + } + + private async void ExecuteStartBatch() + { + if (!CanStartBatch()) return; + + _assistant.ClearTemplateLearningRoiOnViewport(); + + Rows.Clear(); + IsRunning = true; + _batchCts = new CancellationTokenSource(); + var token = _batchCts.Token; + int total = _imagePaths.Count; + int index = 0; + + try + { + foreach (var path in _imagePaths) + { + token.ThrowIfCancellationRequested(); + index++; + BatchStatusText = $"正在处理 {index}/{total}:{Path.GetFileName(path)}"; + + var row = await Task.Run(() => ProcessOneFile(path), token).ConfigureAwait(true); + Rows.Add(row); + } + + BatchStatusText = $"完成,共处理 {Rows.Count} 张。"; + } + catch (OperationCanceledException) + { + BatchStatusText = $"已停止(已处理 {Rows.Count}/{total} 张)。"; + } + catch (Exception ex) + { + Log.Error(ex, "Batch template match failed"); + BatchStatusText = $"批量过程异常:{ex.Message}"; + } + finally + { + IsRunning = false; + _batchCts?.Dispose(); + _batchCts = null; + } + } + + private void ExecuteStopBatch() => _batchCts?.Cancel(); + + private TemplateMatchBatchRow ProcessOneFile(string path) + { + var fileName = Path.GetFileName(path); + try + { + using var mat = CvInvoke.Imread(path, ImreadModes.Grayscale); + if (mat == null || mat.IsEmpty) + { + return new TemplateMatchBatchRow + { + FileName = fileName, + FullPath = path, + Result = "无法读取", + ErrorDetail = "Imread 为空或失败" + }; + } + + using var gray = mat.ToImage(); + if (!_assistant.TryMatchGrayImage(gray, out var hits, out var t, out var err)) + { + return new TemplateMatchBatchRow + { + FileName = fileName, + FullPath = path, + Result = "失败", + ErrorDetail = err, + TimeMs = t + }; + } + + int c = hits.Count; + double best = c == 0 ? 0 : hits.Max(h => h.Score); + return new TemplateMatchBatchRow + { + FileName = fileName, + FullPath = path, + Result = c > 0 ? "命中" : "未找到", + MatchCount = c, + BestScore = best, + TimeMs = t, + Hits = CloneHits(hits) + }; + } + catch (Exception ex) + { + Log.Warning(ex, "ProcessOneFile: {Path}", path); + return new TemplateMatchBatchRow + { + FileName = fileName, + FullPath = path, + Result = "异常", + ErrorDetail = ex.Message + }; + } + } + + private static List CloneHits(IReadOnlyList src) + { + var list = new List(src.Count); + foreach (var h in src) + { + list.Add(new TemplateMatchHitDto + { + CenterX = h.CenterX, + CenterY = h.CenterY, + Angle = h.Angle, + Score = h.Score, + LtX = h.LtX, LtY = h.LtY, + RtX = h.RtX, RtY = h.RtY, + RbX = h.RbX, RbY = h.RbY, + LbX = h.LbX, LbY = h.LbY + }); + } + + return list; + } + + private void ExecuteOpenSelectedInMainViewport() + { + if (SelectedRow == null || string.IsNullOrWhiteSpace(SelectedRow.FullPath) || !File.Exists(SelectedRow.FullPath)) + return; + + try + { + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.UriSource = new Uri(SelectedRow.FullPath, UriKind.Absolute); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + bitmap.Freeze(); + + _mainViewportService.SetManualImage(bitmap, SelectedRow.FullPath); + _eventAggregator.GetEvent() + .Publish(new ManualImageLoadedPayload(bitmap, SelectedRow.FullPath)); + + var hits = CloneHits(SelectedRow.Hits); + _eventAggregator.GetEvent().Publish( + new TemplateMatchPreviewPayload { Hits = hits, MatchTimeMs = SelectedRow.TimeMs }); + + BatchStatusText = $"已在主视图打开:{SelectedRow.FileName}"; + } + catch (Exception ex) + { + Log.Error(ex, "OpenSelectedInMainViewport"); + BatchStatusText = $"打开失败:{ex.Message}"; + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _assistant.PropertyChanged -= OnAssistantPropertyChanged; + _batchCts?.Cancel(); + _batchCts?.Dispose(); + _batchCts = null; + } +} diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index 965a9b6..e781ed2 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -26,8 +26,10 @@ using XplorePlane.Services.MainViewport; using XP.ImageProcessing.Processors; using XplorePlane.Services.Storage; using XplorePlane.ViewModels.Cnc; +using XplorePlane.ViewModels.ImageProcessing; using XplorePlane.Views; using XplorePlane.Views.Cnc; +using XplorePlane.Views.ImageProcessing; namespace XplorePlane.ViewModels { @@ -137,6 +139,7 @@ namespace XplorePlane.ViewModels public DelegateCommand WhiteBackgroundDetectionCommand { get; } public DelegateCommand BlackBackgroundDetectionCommand { get; } + public DelegateCommand OpenTemplateMatchAssistantCommand { get; } public DelegateCommand GrayscaleCommand { get; } public DelegateCommand SharpenCommand { get; } public DelegateCommand EnhanceCommand { get; } @@ -224,6 +227,7 @@ namespace XplorePlane.ViewModels private Window _settingsWindow; private Window _toolboxWindow; private Window _raySourceConfigWindow; + private Window _templateMatchAssistantWindow; private object _imagePanelContent; private GridLength _viewportPanelWidth = new(1, GridUnitType.Star); private GridLength _imagePanelWidth = new(320); @@ -342,6 +346,7 @@ namespace XplorePlane.ViewModels // 图像处理命令 WhiteBackgroundDetectionCommand = new DelegateCommand(ExecuteWhiteBackgroundDetection); BlackBackgroundDetectionCommand = new DelegateCommand(ExecuteBlackBackgroundDetection); + OpenTemplateMatchAssistantCommand = new DelegateCommand(ExecuteOpenTemplateMatchAssistant); GrayscaleCommand = new DelegateCommand(ExecuteGrayscale); SharpenCommand = new DelegateCommand(ExecuteSharpen); EnhanceCommand = new DelegateCommand(ExecuteEnhance); @@ -970,6 +975,46 @@ namespace XplorePlane.ViewModels StatusMessage = "黑底检测:请在图像上拖拽绘制矩形ROI"; } + private void ExecuteOpenTemplateMatchAssistant() + { + try + { + if (!CheckImageLoaded()) + { + StatusMessage = "请先加载图像再使用模板助手。"; + return; + } + + if (_templateMatchAssistantWindow != null) + { + if (_templateMatchAssistantWindow.IsLoaded) + { + _templateMatchAssistantWindow.Activate(); + return; + } + + _templateMatchAssistantWindow = null; + } + + var vm = _containerProvider.Resolve(); + var w = new TemplateMatchAssistantWindow + { + DataContext = vm, + Owner = Application.Current?.MainWindow + }; + w.Closed += (_, _) => { _templateMatchAssistantWindow = null; }; + _templateMatchAssistantWindow = w; + w.Show(); + _logger.Info("Template match assistant opened."); + StatusMessage = "已打开模板匹配助手"; + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to open template match assistant"); + StatusMessage = $"打开模板助手失败: {ex.Message}"; + } + } + private void OnBlackBackgroundRoiDrawn(System.Windows.Int32Rect roi) => RunBackgroundRoiDetection(roi, BackgroundDefectMode.BlackBackground); diff --git a/XplorePlane/Views/ImageProcessing/TemplateMatchAssistantWindow.xaml b/XplorePlane/Views/ImageProcessing/TemplateMatchAssistantWindow.xaml new file mode 100644 index 0000000..026f037 --- /dev/null +++ b/XplorePlane/Views/ImageProcessing/TemplateMatchAssistantWindow.xaml @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -179,13 +224,19 @@ - @@ -213,22 +264,42 @@ From 7447463c1a7339fb4a62f0a8f785bbf5a21c75b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Fri, 15 May 2026 14:31:48 +0800 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20ROI=20=E5=AF=B9=E9=BD=90=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E4=B8=8E=20TM=5FResult=20=E4=BD=8D=E5=A7=BF=E6=89=A9?= =?UTF-8?q?=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Core: Pose2D、Point2D、RoiAlignment、AlignmentRecipe(示教多边形→运行图刚体变换) - Processors: TemplateMatchAlignmentExtensions.ToPose2D / 四角与中心一致性校验 Co-authored-by: Cursor --- .../Alignment/AlignmentRecipe.cs | 21 ++++ XP.ImageProcessing.Core/Alignment/Point2D.cs | 4 + XP.ImageProcessing.Core/Alignment/Pose2D.cs | 18 ++++ .../Alignment/RoiAlignment.cs | 96 +++++++++++++++++++ .../TemplateMatchAlignmentExtensions.cs | 27 ++++++ 5 files changed, 166 insertions(+) create mode 100644 XP.ImageProcessing.Core/Alignment/AlignmentRecipe.cs create mode 100644 XP.ImageProcessing.Core/Alignment/Point2D.cs create mode 100644 XP.ImageProcessing.Core/Alignment/Pose2D.cs create mode 100644 XP.ImageProcessing.Core/Alignment/RoiAlignment.cs create mode 100644 XP.ImageProcessing.Processors/定位识别/TemplateMatchAlignmentExtensions.cs diff --git a/XP.ImageProcessing.Core/Alignment/AlignmentRecipe.cs b/XP.ImageProcessing.Core/Alignment/AlignmentRecipe.cs new file mode 100644 index 0000000..ff33afb --- /dev/null +++ b/XP.ImageProcessing.Core/Alignment/AlignmentRecipe.cs @@ -0,0 +1,21 @@ +namespace XP.ImageProcessing.Core.Alignment; + +/// +/// 示教阶段保存的对齐配方:基准位姿 + 示教图像素坐标下的检测 ROI。 +/// +public sealed class AlignmentRecipe +{ + /// 示教图上的基准位姿(建议示教图自匹配得到,或与模板 ROI 中心 + 角度 0 一致)。 + public Pose2D ReferencePose { get; set; } + + /// 示教图上的 ROI 多边形顶点(至少 3 点)。 + public List RoiPoints { get; set; } = new(); + + /// 将示教 ROI 变换到运行图坐标。 + public Point2D[] TransformRoi(Pose2D measuredPose) + => RoiAlignment.TransformPolygon(RoiPoints, ReferencePose, measuredPose); + + /// 变换为整型顶点,供检测算子注入。 + public (int X, int Y)[] TransformRoiToInt(Pose2D measuredPose) + => RoiAlignment.TransformPolygonToInt(RoiPoints, ReferencePose, measuredPose); +} diff --git a/XP.ImageProcessing.Core/Alignment/Point2D.cs b/XP.ImageProcessing.Core/Alignment/Point2D.cs new file mode 100644 index 0000000..26d1bb1 --- /dev/null +++ b/XP.ImageProcessing.Core/Alignment/Point2D.cs @@ -0,0 +1,4 @@ +namespace XP.ImageProcessing.Core.Alignment; + +/// 图像像素平面上的点(与 WPF/Emgu 解耦)。 +public readonly record struct Point2D(double X, double Y); diff --git a/XP.ImageProcessing.Core/Alignment/Pose2D.cs b/XP.ImageProcessing.Core/Alignment/Pose2D.cs new file mode 100644 index 0000000..5a80ae0 --- /dev/null +++ b/XP.ImageProcessing.Core/Alignment/Pose2D.cs @@ -0,0 +1,18 @@ +namespace XP.ImageProcessing.Core.Alignment; + +/// +/// 图像平面上的刚体位姿:绕 / 旋转 (度)。 +/// 与 TemplateMatchLib 的 CenterX/CenterY/Angle 约定一致。 +/// +public readonly record struct Pose2D(double X, double Y, double AngleDegrees) +{ + /// 示教/标准姿态(角度 0,中心由调用方指定)。 + public static Pose2D IdentityAt(double x, double y) => new(x, y, 0); + + /// + /// 由模板学习 ROI 矩形估计示教位姿中心(pattern 几何中心),角度默认 0。 + /// 更稳妥的做法是在示教图上自匹配得到 。 + /// + public static Pose2D FromTemplateRoiCenter(int roiX, int roiY, int roiWidth, int roiHeight, double angleDegrees = 0) + => new(roiX + roiWidth * 0.5, roiY + roiHeight * 0.5, angleDegrees); +} diff --git a/XP.ImageProcessing.Core/Alignment/RoiAlignment.cs b/XP.ImageProcessing.Core/Alignment/RoiAlignment.cs new file mode 100644 index 0000000..2770573 --- /dev/null +++ b/XP.ImageProcessing.Core/Alignment/RoiAlignment.cs @@ -0,0 +1,96 @@ +namespace XP.ImageProcessing.Core.Alignment; + +/// +/// 将示教图(模板坐标系)上的 ROI 点变换到运行图坐标。 +/// 旋转中心与模板匹配一致:绕 /(pattern 中心)。 +/// +public static class RoiAlignment +{ + /// + /// 刚体变换:示教图点 → 运行图点。 + /// :示教图上的基准位姿; + /// :运行图匹配位姿。 + /// + public static Point2D TransformPoint(Point2D point, Pose2D reference, Pose2D measured) + { + double dTheta = DegreesToRadians(measured.AngleDegrees - reference.AngleDegrees); + double cos = Math.Cos(dTheta); + double sin = Math.Sin(dTheta); + double dx = point.X - reference.X; + double dy = point.Y - reference.Y; + return new Point2D( + measured.X + cos * dx - sin * dy, + measured.Y + sin * dx + cos * dy); + } + + public static Point2D TransformPoint(double x, double y, Pose2D reference, Pose2D measured) + => TransformPoint(new Point2D(x, y), reference, measured); + + /// 变换多边形顶点(顺序不变)。 + public static Point2D[] TransformPolygon(IReadOnlyList templatePoints, Pose2D reference, Pose2D measured) + { + if (templatePoints == null || templatePoints.Count == 0) + return Array.Empty(); + + var result = new Point2D[templatePoints.Count]; + for (int i = 0; i < templatePoints.Count; i++) + result[i] = TransformPoint(templatePoints[i], reference, measured); + return result; + } + + /// 变换后四舍五入为整型顶点,供 BGA 等算子 PolyX/PolyY 注入。 + public static (int X, int Y)[] TransformPolygonToInt( + IReadOnlyList templatePoints, + Pose2D reference, + Pose2D measured) + { + var transformed = TransformPolygon(templatePoints, reference, measured); + var result = new (int X, int Y)[transformed.Length]; + for (int i = 0; i < transformed.Length; i++) + { + result[i] = ( + (int)Math.Round(transformed[i].X, MidpointRounding.AwayFromZero), + (int)Math.Round(transformed[i].Y, MidpointRounding.AwayFromZero)); + } + + return result; + } + + /// 变换轴对齐矩形为四个顶点(左上、右上、右下、左下)。 + public static Point2D[] TransformRect(double x, double y, double width, double height, Pose2D reference, Pose2D measured) + { + var corners = new[] + { + new Point2D(x, y), + new Point2D(x + width, y), + new Point2D(x + width, y + height), + new Point2D(x, y + height) + }; + return TransformPolygon(corners, reference, measured); + } + + /// + /// 校验匹配结果四角质心是否与 Center 一致(用于确认库的中心/角度约定)。 + /// + public static bool IsMatchCenterConsistentWithCorners( + double centerX, + double centerY, + double ltX, + double ltY, + double rtX, + double rtY, + double rbX, + double rbY, + double lbX, + double lbY, + double tolerancePixels = 1.0) + { + double cx = (ltX + rtX + rbX + lbX) * 0.25; + double cy = (ltY + rtY + rbY + lbY) * 0.25; + double dx = cx - centerX; + double dy = cy - centerY; + return dx * dx + dy * dy <= tolerancePixels * tolerancePixels; + } + + private static double DegreesToRadians(double degrees) => degrees * (Math.PI / 180.0); +} diff --git a/XP.ImageProcessing.Processors/定位识别/TemplateMatchAlignmentExtensions.cs b/XP.ImageProcessing.Processors/定位识别/TemplateMatchAlignmentExtensions.cs new file mode 100644 index 0000000..873650f --- /dev/null +++ b/XP.ImageProcessing.Processors/定位识别/TemplateMatchAlignmentExtensions.cs @@ -0,0 +1,27 @@ +using XP.ImageProcessing.Core.Alignment; + +namespace XP.ImageProcessing.Processors; + +/// +/// 将 TemplateMatchLib 匹配结果转换为对齐工具使用的 。 +/// +public static class TemplateMatchAlignmentExtensions +{ + public static Pose2D ToPose2D(this TM_Result result) + => new(result.CenterX, result.CenterY, result.Angle); + + /// 四角质心是否与 Center 一致(容差默认 1 像素)。 + public static bool IsCenterConsistentWithCorners(this TM_Result result, double tolerancePixels = 1.0) + => RoiAlignment.IsMatchCenterConsistentWithCorners( + result.CenterX, + result.CenterY, + result.LtX, + result.LtY, + result.RtX, + result.RtY, + result.RbX, + result.RbY, + result.LbX, + result.LbY, + tolerancePixels); +} From 12938764b19a44a62404f6463d7164e8ebb42a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Fri, 15 May 2026 15:44:18 +0800 Subject: [PATCH 09/12] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=BE=B9?= =?UTF-8?q?=E7=BC=98=E6=9F=A5=E6=89=BE=E6=8B=9F=E5=90=88=E7=9B=B4=E7=BA=BF?= =?UTF-8?q?=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 EdgeLineFitProcessor 算子(卡尺边缘检测 + 最小二乘/RANSAC直线拟合) - 新增 EdgeLineFitPanel 辅助面板(参数配置、交互绘制卡尺) - 支持任意角度旋转的卡尺区域,4个手柄控制长度/宽度 - 支持多次拟合累积显示,关闭面板后结果保留 - 极性箭头标识搜索方向(B→D / D→B / 双向) - 卡尺亮绿色1px,拟合直线蓝色2px - Ribbon快捷工具组新增「直线拟合」按钮 - 添加中英文本地化资源 --- XP.Common/Resources/Resources.en-US.resx | 56 ++ XP.Common/Resources/Resources.resx | 56 ++ XP.Common/Resources/Resources.zh-CN.resx | 56 ++ .../检测分析/EdgeLineFitProcessor.cs | 638 ++++++++++++++++++ .../Controls/PolygonRoiCanvas.xaml.cs | 51 ++ .../Models/MeasureMode.cs | 3 +- XplorePlane/Events/MeasurementToolEvent.cs | 3 +- .../ImageProcessing/EdgeLineFitViewModel.cs | 631 +++++++++++++++++ XplorePlane/ViewModels/Main/MainViewModel.cs | 22 + .../ImageProcessing/EdgeLineFitPanel.xaml | 127 ++++ .../ImageProcessing/EdgeLineFitPanel.xaml.cs | 53 ++ XplorePlane/Views/Main/MainWindow.xaml | 7 + .../Views/Main/ViewportPanelView.xaml.cs | 157 +++++ 13 files changed, 1858 insertions(+), 2 deletions(-) create mode 100644 XP.ImageProcessing.Processors/检测分析/EdgeLineFitProcessor.cs create mode 100644 XplorePlane/ViewModels/ImageProcessing/EdgeLineFitViewModel.cs create mode 100644 XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml create mode 100644 XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml.cs diff --git a/XP.Common/Resources/Resources.en-US.resx b/XP.Common/Resources/Resources.en-US.resx index 3154237..5885aaf 100644 --- a/XP.Common/Resources/Resources.en-US.resx +++ b/XP.Common/Resources/Resources.en-US.resx @@ -1887,4 +1887,60 @@ Reprojection error: {1:F4} pixels Image{0}: {1:F4} pixels + + + + Edge Find Line Fit + + + Place calipers along a search line to detect edge points and fit a line (supports Least Squares and RANSAC) + + + Caliper Count + + + Number of calipers placed evenly along the search line + + + Caliper Width + + + Search length of each caliper (pixels), perpendicular to the search line + + + Edge Polarity + + + Edge direction: BrightToDark, DarkToBright, or Both + + + Edge Threshold + + + Gradient strength threshold; edges below this value are ignored + + + Smoothing Sigma + + + Gaussian smoothing standard deviation for noise suppression (larger = smoother) + + + Fit Method + + + Line fitting algorithm: LeastSquares or RANSAC (robust, rejects outliers) + + + RANSAC Threshold + + + RANSAC inlier distance threshold (pixels); points closer than this to the line are inliers + + + Line Thickness + + + Drawing thickness for result visualization + \ No newline at end of file diff --git a/XP.Common/Resources/Resources.resx b/XP.Common/Resources/Resources.resx index 7f0cc41..eac5543 100644 --- a/XP.Common/Resources/Resources.resx +++ b/XP.Common/Resources/Resources.resx @@ -1920,4 +1920,60 @@ 图像{0}: {1:F4} 像素 + + + + 边缘查找拟合直线 + + + 沿搜索线放置卡尺检测边缘点,拟合直线(支持最小二乘和RANSAC) + + + 卡尺数量 + + + 沿搜索线等间距放置的卡尺数量 + + + 卡尺宽度 + + + 每个卡尺的搜索长度(像素),沿垂直于搜索线方向 + + + 边缘极性 + + + 边缘方向:BrightToDark(亮到暗)、DarkToBright(暗到亮)、Both(双向) + + + 边缘阈值 + + + 边缘梯度强度阈值,低于此值的边缘将被忽略 + + + 平滑Sigma + + + 高斯平滑的标准差,用于抑制噪声(越大越平滑) + + + 拟合方法 + + + 直线拟合算法:LeastSquares(最小二乘)、RANSAC(鲁棒拟合,可剔除异常点) + + + RANSAC阈值 + + + RANSAC内点判定距离阈值(像素),点到直线距离小于此值视为内点 + + + 线条粗细 + + + 绘制结果的线条粗细 + \ No newline at end of file diff --git a/XP.Common/Resources/Resources.zh-CN.resx b/XP.Common/Resources/Resources.zh-CN.resx index 7dc08f9..0d99bfa 100644 --- a/XP.Common/Resources/Resources.zh-CN.resx +++ b/XP.Common/Resources/Resources.zh-CN.resx @@ -1881,4 +1881,60 @@ 图像{0}: {1:F4} 像素 + + + + 边缘查找拟合直线 + + + 沿搜索线放置卡尺检测边缘点,拟合直线(支持最小二乘和RANSAC) + + + 卡尺数量 + + + 沿搜索线等间距放置的卡尺数量 + + + 卡尺宽度 + + + 每个卡尺的搜索长度(像素),沿垂直于搜索线方向 + + + 边缘极性 + + + 边缘方向:BrightToDark(亮到暗)、DarkToBright(暗到亮)、Both(双向) + + + 边缘阈值 + + + 边缘梯度强度阈值,低于此值的边缘将被忽略 + + + 平滑Sigma + + + 高斯平滑的标准差,用于抑制噪声(越大越平滑) + + + 拟合方法 + + + 直线拟合算法:LeastSquares(最小二乘)、RANSAC(鲁棒拟合,可剔除异常点) + + + RANSAC阈值 + + + RANSAC内点判定距离阈值(像素),点到直线距离小于此值视为内点 + + + 线条粗细 + + + 绘制结果的线条粗细 + \ No newline at end of file diff --git a/XP.ImageProcessing.Processors/检测分析/EdgeLineFitProcessor.cs b/XP.ImageProcessing.Processors/检测分析/EdgeLineFitProcessor.cs new file mode 100644 index 0000000..0fad857 --- /dev/null +++ b/XP.ImageProcessing.Processors/检测分析/EdgeLineFitProcessor.cs @@ -0,0 +1,638 @@ +// ============================================================================ +// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. +// 文件名: EdgeLineFitProcessor.cs +// 描述: 边缘查找拟合直线算子 +// 功能: +// - 沿用户定义的搜索线等间距放置多个卡尺(Caliper) +// - 在每个卡尺内沿垂直方向提取灰度投影并求导,定位边缘点 +// - 支持亚像素精度(抛物线插值) +// - 支持边缘极性选择(亮到暗/暗到亮/双向) +// - 使用最小二乘或RANSAC算法拟合直线 +// - 输出拟合直线参数、边缘点、内点/外点、拟合误差 +// 算法: 卡尺边缘检测 + 最小二乘/RANSAC直线拟合 +// 作者: 李伟 wei.lw.li@hexagon.com +// ============================================================================ + +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using Emgu.CV.Util; +using XP.ImageProcessing.Core; +using Serilog; +using System.Drawing; + +namespace XP.ImageProcessing.Processors; + +/// +/// 边缘点信息 +/// +public class EdgePointInfo +{ + /// 边缘点坐标(亚像素) + public PointF Position { get; set; } + + /// 边缘强度(梯度绝对值) + public double Strength { get; set; } + + /// 卡尺索引 + public int CaliperIndex { get; set; } + + /// 是否为拟合内点 + public bool IsInlier { get; set; } = true; +} + +/// +/// 直线拟合结果 +/// +public class LineFitResult +{ + /// 拟合是否成功 + public bool Success { get; set; } + + /// 直线方向向量 (vx, vy) + public PointF Direction { get; set; } + + /// 直线上一点 (x0, y0) + public PointF PointOnLine { get; set; } + + /// 直线角度(度,相对于X轴) + public double AngleDegrees { get; set; } + + /// 直线端点1(用于绘制) + public PointF Endpoint1 { get; set; } + + /// 直线端点2(用于绘制) + public PointF Endpoint2 { get; set; } + + /// 所有检测到的边缘点 + public List EdgePoints { get; set; } = new(); + + /// 内点列表 + public List Inliers { get; set; } = new(); + + /// 外点列表 + public List Outliers { get; set; } = new(); + + /// 平均拟合误差(像素) + public double FitError { get; set; } + + /// 有效边缘点数 + public int EdgePointCount { get; set; } +} + +/// +/// 边缘查找拟合直线算子 - 使用卡尺法检测边缘点并拟合直线 +/// +public class EdgeLineFitProcessor : ImageProcessorBase +{ + private static readonly ILogger _logger = Log.ForContext(); + private static readonly Random _random = new(); + + public EdgeLineFitProcessor() + { + Name = LocalizationHelper.GetString("EdgeLineFitProcessor_Name"); + Description = LocalizationHelper.GetString("EdgeLineFitProcessor_Description"); + } + + protected override void InitializeParameters() + { + // ── 搜索线起止点(由UI交互控件注入,不可见) ── + Parameters.Add("StartX", new ProcessorParameter( + "StartX", "StartX", typeof(int), 100, null, null, "") { IsVisible = false }); + Parameters.Add("StartY", new ProcessorParameter( + "StartY", "StartY", typeof(int), 200, null, null, "") { IsVisible = false }); + Parameters.Add("EndX", new ProcessorParameter( + "EndX", "EndX", typeof(int), 400, null, null, "") { IsVisible = false }); + Parameters.Add("EndY", new ProcessorParameter( + "EndY", "EndY", typeof(int), 200, null, null, "") { IsVisible = false }); + + // ── 卡尺参数 ── + Parameters.Add("CaliperCount", new ProcessorParameter( + "CaliperCount", + LocalizationHelper.GetString("EdgeLineFitProcessor_CaliperCount"), + typeof(int), 20, 3, 200, + LocalizationHelper.GetString("EdgeLineFitProcessor_CaliperCount_Desc"))); + + Parameters.Add("CaliperWidth", new ProcessorParameter( + "CaliperWidth", + LocalizationHelper.GetString("EdgeLineFitProcessor_CaliperWidth"), + typeof(int), 40, 5, 500, + LocalizationHelper.GetString("EdgeLineFitProcessor_CaliperWidth_Desc"))); + + // ── 边缘检测参数 ── + Parameters.Add("EdgePolarity", new ProcessorParameter( + "EdgePolarity", + LocalizationHelper.GetString("EdgeLineFitProcessor_EdgePolarity"), + typeof(string), "Both", null, null, + LocalizationHelper.GetString("EdgeLineFitProcessor_EdgePolarity_Desc"), + new string[] { "BrightToDark", "DarkToBright", "Both" })); + + Parameters.Add("EdgeThreshold", new ProcessorParameter( + "EdgeThreshold", + LocalizationHelper.GetString("EdgeLineFitProcessor_EdgeThreshold"), + typeof(int), 30, 1, 255, + LocalizationHelper.GetString("EdgeLineFitProcessor_EdgeThreshold_Desc"))); + + Parameters.Add("Sigma", new ProcessorParameter( + "Sigma", + LocalizationHelper.GetString("EdgeLineFitProcessor_Sigma"), + typeof(double), 1.0, 0.1, 10.0, + LocalizationHelper.GetString("EdgeLineFitProcessor_Sigma_Desc"))); + + // ── 拟合参数 ── + Parameters.Add("FitMethod", new ProcessorParameter( + "FitMethod", + LocalizationHelper.GetString("EdgeLineFitProcessor_FitMethod"), + typeof(string), "RANSAC", null, null, + LocalizationHelper.GetString("EdgeLineFitProcessor_FitMethod_Desc"), + new string[] { "LeastSquares", "RANSAC" })); + + Parameters.Add("RansacThreshold", new ProcessorParameter( + "RansacThreshold", + LocalizationHelper.GetString("EdgeLineFitProcessor_RansacThreshold"), + typeof(double), 2.0, 0.5, 20.0, + LocalizationHelper.GetString("EdgeLineFitProcessor_RansacThreshold_Desc"))); + + Parameters.Add("Thickness", new ProcessorParameter( + "Thickness", + LocalizationHelper.GetString("EdgeLineFitProcessor_Thickness"), + typeof(int), 2, 1, 10, + LocalizationHelper.GetString("EdgeLineFitProcessor_Thickness_Desc"))); + } + + public override Image Process(Image inputImage) + { + // 读取参数 + int startX = GetParameter("StartX"); + int startY = GetParameter("StartY"); + int endX = GetParameter("EndX"); + int endY = GetParameter("EndY"); + int caliperCount = GetParameter("CaliperCount"); + int caliperWidth = GetParameter("CaliperWidth"); + string edgePolarity = GetParameter("EdgePolarity"); + int edgeThreshold = GetParameter("EdgeThreshold"); + double sigma = GetParameter("Sigma"); + string fitMethod = GetParameter("FitMethod"); + double ransacThreshold = GetParameter("RansacThreshold"); + int thickness = GetParameter("Thickness"); + + OutputData.Clear(); + + _logger.Debug( + "EdgeLineFit started: Search({StartX},{StartY})->({EndX},{EndY}), Calipers={Count}, Width={Width}, Polarity={Polarity}", + startX, startY, endX, endY, caliperCount, caliperWidth, edgePolarity); + + // 计算搜索线方向和垂直方向 + double searchDx = endX - startX; + double searchDy = endY - startY; + double searchLen = Math.Sqrt(searchDx * searchDx + searchDy * searchDy); + + if (searchLen < 1.0) + { + _logger.Warning("Search line too short, cannot perform edge detection"); + OutputData["LineFitResult"] = new LineFitResult { Success = false }; + return inputImage.Clone(); + } + + // 搜索线单位方向 + double ux = searchDx / searchLen; + double uy = searchDy / searchLen; + + // 垂直于搜索线的方向(卡尺搜索方向) + double perpX = -uy; + double perpY = ux; + + // 沿搜索线等间距放置卡尺 + var edgePoints = new List(); + double step = searchLen / (caliperCount + 1); + + for (int i = 0; i < caliperCount; i++) + { + // 卡尺中心点 + double cx = startX + ux * step * (i + 1); + double cy = startY + uy * step * (i + 1); + + // 在卡尺内沿垂直方向提取灰度剖面 + var edgePoint = FindEdgeInCaliper( + inputImage, cx, cy, perpX, perpY, + caliperWidth, edgePolarity, edgeThreshold, sigma, i); + + if (edgePoint != null) + { + edgePoints.Add(edgePoint); + } + } + + _logger.Debug("Found {Count} edge points from {Total} calipers", edgePoints.Count, caliperCount); + + // 拟合直线 + var result = FitLine(edgePoints, fitMethod, ransacThreshold, inputImage.Size); + + // 存储输出数据 + OutputData["LineFitResult"] = result; + OutputData["EdgePoints"] = edgePoints.Select(p => p.Position).ToArray(); + OutputData["EdgePointCount"] = edgePoints.Count; + OutputData["Thickness"] = thickness; + + if (result.Success) + { + OutputData["FittedLineDirection"] = result.Direction; + OutputData["FittedLinePoint"] = result.PointOnLine; + OutputData["LineAngle"] = result.AngleDegrees; + OutputData["LineEndpoint1"] = result.Endpoint1; + OutputData["LineEndpoint2"] = result.Endpoint2; + OutputData["InlierPoints"] = result.Inliers.ToArray(); + OutputData["OutlierPoints"] = result.Outliers.ToArray(); + OutputData["FitError"] = result.FitError; + + _logger.Information( + "EdgeLineFit completed: Angle={Angle:F2}°, Inliers={Inliers}/{Total}, Error={Error:F3}px", + result.AngleDegrees, result.Inliers.Count, edgePoints.Count, result.FitError); + } + else + { + _logger.Warning("EdgeLineFit failed: insufficient edge points for line fitting"); + } + + // 搜索区域信息(供UI绘制) + OutputData["SearchStart"] = new PointF(startX, startY); + OutputData["SearchEnd"] = new PointF(endX, endY); + OutputData["CaliperWidth"] = caliperWidth; + OutputData["CaliperCount"] = caliperCount; + OutputData["PerpDirection"] = new PointF((float)perpX, (float)perpY); + + return inputImage.Clone(); + } + + /// + /// 在单个卡尺内查找边缘点 + /// + private EdgePointInfo? FindEdgeInCaliper( + Image image, + double centerX, double centerY, + double perpX, double perpY, + int caliperWidth, string polarity, + int threshold, double sigma, int caliperIndex) + { + int halfWidth = caliperWidth / 2; + int profileLength = caliperWidth; + + // 提取灰度剖面 + var profile = new double[profileLength]; + int validCount = 0; + + for (int i = 0; i < profileLength; i++) + { + double offset = i - halfWidth; + double px = centerX + perpX * offset; + double py = centerY + perpY * offset; + + int ix = (int)Math.Round(px); + int iy = (int)Math.Round(py); + + if (ix >= 0 && ix < image.Width && iy >= 0 && iy < image.Height) + { + profile[i] = image.Data[iy, ix, 0]; + validCount++; + } + else + { + profile[i] = 0; + } + } + + if (validCount < profileLength * 0.5) + return null; + + // 高斯平滑 + if (sigma > 0.1) + { + profile = GaussianSmooth1D(profile, sigma); + } + + // 求一阶导数 + var derivative = new double[profileLength]; + for (int i = 1; i < profileLength - 1; i++) + { + derivative[i] = (profile[i + 1] - profile[i - 1]) / 2.0; + } + + // 根据极性查找最强边缘 + int bestIdx = -1; + double bestStrength = 0; + + for (int i = 2; i < profileLength - 2; i++) + { + double strength = derivative[i]; + bool validPolarity = polarity switch + { + "BrightToDark" => strength < 0, // 亮到暗:导数为负 + "DarkToBright" => strength > 0, // 暗到亮:导数为正 + _ => true // Both:任意方向 + }; + + if (!validPolarity) continue; + + double absStrength = Math.Abs(strength); + if (absStrength >= threshold && absStrength > bestStrength) + { + bestStrength = absStrength; + bestIdx = i; + } + } + + if (bestIdx < 0) + return null; + + // 亚像素精度:抛物线插值 + double subPixelOffset = 0; + if (bestIdx > 0 && bestIdx < profileLength - 1) + { + double left = Math.Abs(derivative[bestIdx - 1]); + double center = Math.Abs(derivative[bestIdx]); + double right = Math.Abs(derivative[bestIdx + 1]); + double denom = 2.0 * (2.0 * center - left - right); + if (Math.Abs(denom) > 1e-6) + { + subPixelOffset = (left - right) / denom; + subPixelOffset = Math.Clamp(subPixelOffset, -0.5, 0.5); + } + } + + double edgeOffset = (bestIdx + subPixelOffset) - halfWidth; + float edgeX = (float)(centerX + perpX * edgeOffset); + float edgeY = (float)(centerY + perpY * edgeOffset); + + return new EdgePointInfo + { + Position = new PointF(edgeX, edgeY), + Strength = bestStrength, + CaliperIndex = caliperIndex, + IsInlier = true + }; + } + + /// + /// 一维高斯平滑 + /// + private static double[] GaussianSmooth1D(double[] data, double sigma) + { + int kernelRadius = (int)Math.Ceiling(sigma * 3); + int kernelSize = kernelRadius * 2 + 1; + var kernel = new double[kernelSize]; + double sum = 0; + + for (int i = 0; i < kernelSize; i++) + { + double x = i - kernelRadius; + kernel[i] = Math.Exp(-x * x / (2.0 * sigma * sigma)); + sum += kernel[i]; + } + for (int i = 0; i < kernelSize; i++) + kernel[i] /= sum; + + var result = new double[data.Length]; + for (int i = 0; i < data.Length; i++) + { + double val = 0; + double wSum = 0; + for (int k = 0; k < kernelSize; k++) + { + int idx = i + k - kernelRadius; + if (idx >= 0 && idx < data.Length) + { + val += data[idx] * kernel[k]; + wSum += kernel[k]; + } + } + result[i] = wSum > 0 ? val / wSum : data[i]; + } + return result; + } + + /// + /// 拟合直线 + /// + private LineFitResult FitLine(List edgePoints, string method, + double ransacThreshold, Size imageSize) + { + var result = new LineFitResult(); + + if (edgePoints.Count < 2) + { + result.Success = false; + return result; + } + + if (method == "RANSAC" && edgePoints.Count >= 3) + { + return FitLineRANSAC(edgePoints, ransacThreshold, imageSize); + } + else + { + return FitLineLeastSquares(edgePoints, imageSize); + } + } + + /// + /// 最小二乘直线拟合(使用OpenCV FitLine) + /// + private LineFitResult FitLineLeastSquares(List edgePoints, Size imageSize) + { + var result = new LineFitResult(); + var points = edgePoints.Select(p => p.Position).ToArray(); + + using var pointVector = new VectorOfPointF(points); + using var lineMat = new Mat(); + CvInvoke.FitLine(pointVector, lineMat, DistType.L2, 0, 0.01, 0.01); + var lineParams = new float[4]; + System.Runtime.InteropServices.Marshal.Copy(lineMat.DataPointer, lineParams, 0, 4); + + float vx = lineParams[0], vy = lineParams[1]; + float x0 = lineParams[2], y0 = lineParams[3]; + + result.Success = true; + result.Direction = new PointF(vx, vy); + result.PointOnLine = new PointF(x0, y0); + result.AngleDegrees = Math.Atan2(vy, vx) * 180.0 / Math.PI; + + // 计算端点(延伸到图像边界或搜索范围) + ComputeLineEndpoints(result, points, imageSize); + + // 所有点都是内点 + result.Inliers = points.ToList(); + result.Outliers = new List(); + foreach (var ep in edgePoints) + ep.IsInlier = true; + + // 计算拟合误差 + result.FitError = ComputeFitError(points, vx, vy, x0, y0); + result.EdgePointCount = edgePoints.Count; + result.EdgePoints = edgePoints; + + return result; + } + + /// + /// RANSAC直线拟合 + /// + private LineFitResult FitLineRANSAC(List edgePoints, double threshold, Size imageSize) + { + var result = new LineFitResult(); + var points = edgePoints.Select(p => p.Position).ToArray(); + int n = points.Length; + + // RANSAC参数 + int maxIterations = Math.Min(1000, n * (n - 1) / 2); + int bestInlierCount = 0; + float bestVx = 0, bestVy = 0, bestX0 = 0, bestY0 = 0; + List bestInlierIndices = new(); + + for (int iter = 0; iter < maxIterations; iter++) + { + // 随机选择2个点 + int idx1 = _random.Next(n); + int idx2 = _random.Next(n); + if (idx1 == idx2) continue; + + PointF p1 = points[idx1], p2 = points[idx2]; + float dx = p2.X - p1.X, dy = p2.Y - p1.Y; + float len = (float)Math.Sqrt(dx * dx + dy * dy); + if (len < 1e-6f) continue; + + float vx = dx / len, vy = dy / len; + + // 统计内点 + var inlierIndices = new List(); + for (int i = 0; i < n; i++) + { + double dist = PointToLineDistance(points[i], p1, vx, vy); + if (dist <= threshold) + { + inlierIndices.Add(i); + } + } + + if (inlierIndices.Count > bestInlierCount) + { + bestInlierCount = inlierIndices.Count; + bestInlierIndices = inlierIndices; + + // 用所有内点重新拟合 + var inlierPoints = inlierIndices.Select(i => points[i]).ToArray(); + using var pv = new VectorOfPointF(inlierPoints); + using var lpMat = new Mat(); + CvInvoke.FitLine(pv, lpMat, DistType.L2, 0, 0.01, 0.01); + var lp = new float[4]; + System.Runtime.InteropServices.Marshal.Copy(lpMat.DataPointer, lp, 0, 4); + bestVx = lp[0]; bestVy = lp[1]; bestX0 = lp[2]; bestY0 = lp[3]; + } + + // 如果内点比例已经很高,提前退出 + if (bestInlierCount > n * 0.95) + break; + } + + if (bestInlierCount < 2) + { + result.Success = false; + return result; + } + + result.Success = true; + result.Direction = new PointF(bestVx, bestVy); + result.PointOnLine = new PointF(bestX0, bestY0); + result.AngleDegrees = Math.Atan2(bestVy, bestVx) * 180.0 / Math.PI; + + // 分类内点/外点 + var inliers = new List(); + var outliers = new List(); + var inlierSet = new HashSet(bestInlierIndices); + + for (int i = 0; i < n; i++) + { + if (inlierSet.Contains(i)) + { + inliers.Add(points[i]); + edgePoints[i].IsInlier = true; + } + else + { + outliers.Add(points[i]); + edgePoints[i].IsInlier = false; + } + } + + result.Inliers = inliers; + result.Outliers = outliers; + + // 计算端点 + ComputeLineEndpoints(result, inliers.ToArray(), imageSize); + + // 计算拟合误差(仅内点) + result.FitError = ComputeFitError(inliers.ToArray(), bestVx, bestVy, bestX0, bestY0); + result.EdgePointCount = edgePoints.Count; + result.EdgePoints = edgePoints; + + return result; + } + + /// + /// 计算点到直线的距离 + /// + private static double PointToLineDistance(PointF point, PointF linePoint, float vx, float vy) + { + // 直线法向量 (-vy, vx) + double dx = point.X - linePoint.X; + double dy = point.Y - linePoint.Y; + return Math.Abs(-vy * dx + vx * dy); + } + + /// + /// 计算直线端点(基于边缘点的投影范围) + /// + private static void ComputeLineEndpoints(LineFitResult result, PointF[] points, Size imageSize) + { + float vx = result.Direction.X, vy = result.Direction.Y; + float x0 = result.PointOnLine.X, y0 = result.PointOnLine.Y; + + // 将所有点投影到直线方向上,找最小和最大投影值 + double minT = double.MaxValue, maxT = double.MinValue; + foreach (var p in points) + { + double t = (p.X - x0) * vx + (p.Y - y0) * vy; + if (t < minT) minT = t; + if (t > maxT) maxT = t; + } + + // 稍微延伸一点 + double extend = (maxT - minT) * 0.05; + minT -= extend; + maxT += extend; + + result.Endpoint1 = new PointF( + (float)(x0 + vx * minT), + (float)(y0 + vy * minT)); + result.Endpoint2 = new PointF( + (float)(x0 + vx * maxT), + (float)(y0 + vy * maxT)); + } + + /// + /// 计算平均拟合误差 + /// + private static double ComputeFitError(PointF[] points, float vx, float vy, float x0, float y0) + { + if (points.Length == 0) return 0; + + double totalError = 0; + foreach (var p in points) + { + double dx = p.X - x0; + double dy = p.Y - y0; + double dist = Math.Abs(-vy * dx + vx * dy); + totalError += dist; + } + return totalError / points.Length; + } +} diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index 8b101e1..0eaf82d 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -504,6 +504,12 @@ namespace XP.ImageProcessing.RoiControl.Controls private Point? _bgaPendingCenter; // 等待第二次点击定半径 private Ellipse _bgaPendingDot; + // 边缘查找拟合直线临时状态 + private int _elfClickCount; + private Ellipse _elfTempDot1; + private Line _elfTempLine; + private Point? _elfTempStart; + // 气泡测量状态 public enum BubbleSubTool { Roi, RoiCircle, RoiPolygon, Wand, Brush, Eraser } private BubbleSubTool _bubbleTool = BubbleSubTool.Roi; @@ -690,6 +696,8 @@ namespace XP.ImageProcessing.RoiControl.Controls HandleFillRateClick(pos); else if (CurrentMeasureMode == Models.MeasureMode.BgaVoid) HandleBgaVoidClick(pos); + else if (CurrentMeasureMode == Models.MeasureMode.EdgeLineFit) + HandleEdgeLineFitClick(pos); // BubbleMeasure 的点击在 MouseDown/Move/Up 中处理(拖拽画 ROI 和画笔) } @@ -870,6 +878,49 @@ namespace XP.ImageProcessing.RoiControl.Controls return g; } + // ── 边缘查找拟合直线 ── + + private void HandleEdgeLineFitClick(Point pos) + { + _elfClickCount++; + + if (_elfClickCount == 1) + { + _elfTempStart = pos; + _elfTempDot1 = CreateMDot(Brushes.Cyan); + _measureOverlay.Children.Add(_elfTempDot1); + SetDotPos(_elfTempDot1, pos); + RaiseMeasureStatusChanged($"直线拟合 - 搜索线起点: ({pos.X:F0}, {pos.Y:F0}),请点击搜索线终点"); + } + else if (_elfClickCount == 2) + { + // 绘制搜索线 + _elfTempLine = new Line + { + Stroke = Brushes.Cyan, + StrokeThickness = 1, + StrokeDashArray = new DoubleCollection { 4, 2 }, + IsHitTestVisible = false, + X1 = _elfTempStart.Value.X, + Y1 = _elfTempStart.Value.Y, + X2 = pos.X, + Y2 = pos.Y + }; + _measureOverlay.Children.Add(_elfTempLine); + + // 触发完成事件,传递搜索线起止点 + RaiseMeasureCompleted(_elfTempStart.Value, pos, 0, MeasureCount, "EdgeLineFit"); + RaiseMeasureStatusChanged($"直线拟合 - 搜索线已定义: ({_elfTempStart.Value.X:F0},{_elfTempStart.Value.Y:F0}) → ({pos.X:F0},{pos.Y:F0})"); + + // 清理临时状态 + if (_elfTempDot1 != null) _measureOverlay.Children.Remove(_elfTempDot1); + _elfTempDot1 = null; + _elfTempStart = null; + _elfClickCount = 0; + CurrentMeasureMode = Models.MeasureMode.None; + } + } + // ── 角度测量 ── private void HandleAngleClick(Point pos) diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs index 039b8d1..d726a79 100644 --- a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs +++ b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs @@ -8,6 +8,7 @@ namespace XP.ImageProcessing.RoiControl.Models Angle, FillRate, BgaVoid, - BubbleMeasure + BubbleMeasure, + EdgeLineFit } } diff --git a/XplorePlane/Events/MeasurementToolEvent.cs b/XplorePlane/Events/MeasurementToolEvent.cs index 526e189..2253239 100644 --- a/XplorePlane/Events/MeasurementToolEvent.cs +++ b/XplorePlane/Events/MeasurementToolEvent.cs @@ -27,7 +27,8 @@ namespace XplorePlane.Events Angle, ThroughHoleFillRate, BgaVoid, - BubbleMeasure + BubbleMeasure, + EdgeLineFit } /// diff --git a/XplorePlane/ViewModels/ImageProcessing/EdgeLineFitViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/EdgeLineFitViewModel.cs new file mode 100644 index 0000000..b540dc4 --- /dev/null +++ b/XplorePlane/ViewModels/ImageProcessing/EdgeLineFitViewModel.cs @@ -0,0 +1,631 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; +using Emgu.CV; +using Emgu.CV.Structure; +using Prism.Commands; +using Prism.Mvvm; +using XP.ImageProcessing.Processors; +using XP.ImageProcessing.RoiControl.Controls; +using XplorePlane.Services.MainViewport; +using Brushes = System.Windows.Media.Brushes; +using Ellipse = System.Windows.Shapes.Ellipse; +using Point = System.Windows.Point; + +namespace XplorePlane.ViewModels.ImageProcessing +{ + /// + /// 边缘查找拟合直线 ViewModel + /// 支持多次拟合,每次点击"画卡尺"开始一次新的测量,结果累积保留 + /// 关闭面板时保留所有结果,仅清除当前正在编辑的临时卡尺 + /// + public class EdgeLineFitViewModel : BindableBase + { + private readonly IMainViewportService _viewportService; + private PolygonRoiCanvas _canvas; + private Canvas _mainCanvas; + + // 当前正在编辑的搜索线 + private Point _lineStart; + private Point _lineEnd; + private double _halfWidth = 30; + private bool _lineDefined; + + // 当前编辑中的临时可视化(卡尺框+手柄,拟合前可调整) + private readonly List _tempOverlays = new(); + + // 已完成的拟合结果(永久保留在画布上) + // 不由本类管理生命周期,关闭面板后仍保留 + private readonly List _committedOverlays = new(); + + // 手柄位置 + private Point _handleStartPos, _handleEndPos, _handleTopPos, _handleBottomPos; + + // 交互状态 + private enum DragTarget { None, Start, End, Top, Bottom } + private DragTarget _dragging = DragTarget.None; + private bool _isDrawingLine; + private int _drawClickCount; + private int _fitCount; + + private const double HandleSize = 12; + private const double HitRadius = 10; + private static readonly SolidColorBrush CaliperStroke; + private static readonly SolidColorBrush CaliperFill; + private static readonly SolidColorBrush FitLineBrush; + private static readonly SolidColorBrush HandleFill; + + static EdgeLineFitViewModel() + { + CaliperStroke = new SolidColorBrush(Color.FromRgb(0, 255, 0)); + CaliperStroke.Freeze(); + CaliperFill = new SolidColorBrush(Color.FromArgb(20, 0, 255, 0)); + CaliperFill.Freeze(); + FitLineBrush = new SolidColorBrush(Color.FromRgb(30, 144, 255)); + FitLineBrush.Freeze(); + HandleFill = new SolidColorBrush(Color.FromArgb(220, 255, 255, 255)); + HandleFill.Freeze(); + } + + public EdgeLineFitViewModel(IMainViewportService viewportService) + { + _viewportService = viewportService; + FitCommand = new DelegateCommand(ExecuteFit, () => _lineDefined); + ClearAllCommand = new DelegateCommand(ExecuteClearAll); + DrawCaliperCommand = new DelegateCommand(ExecuteDrawCaliper); + } + + // ── 命令 ── + public DelegateCommand FitCommand { get; } + public DelegateCommand ClearAllCommand { get; } + public DelegateCommand DrawCaliperCommand { get; } + + // ── 参数 ── + private int _caliperCount = 20; + public int CaliperCount + { + get => _caliperCount; + set { if (SetProperty(ref _caliperCount, value)) RedrawTemp(); } + } + + private int _displayWidth = 60; + public int DisplayWidth + { + get => _displayWidth; + set + { + if (SetProperty(ref _displayWidth, Math.Max(10, value))) + { + _halfWidth = _displayWidth / 2.0; + RedrawTemp(); + } + } + } + + private string _edgePolarity = "Both"; + public string EdgePolarity + { + get => _edgePolarity; + set { if (SetProperty(ref _edgePolarity, value)) RedrawTemp(); } + } + + private int _edgeThreshold = 20; + public int EdgeThreshold { get => _edgeThreshold; set => SetProperty(ref _edgeThreshold, value); } + + private double _sigma = 1.0; + public double Sigma { get => _sigma; set => SetProperty(ref _sigma, value); } + + private string _fitMethod = "RANSAC"; + public string FitMethod { get => _fitMethod; set => SetProperty(ref _fitMethod, value); } + + private double _ransacThreshold = 2.0; + public double RansacThreshold { get => _ransacThreshold; set => SetProperty(ref _ransacThreshold, value); } + + private string _resultText = "就绪 - 点击「画卡尺」开始"; + public string ResultText { get => _resultText; set => SetProperty(ref _resultText, value); } + + // ── 初始化 ── + public void SetCanvas(PolygonRoiCanvas canvas) + { + _canvas = canvas; + _mainCanvas = FindChild(canvas, "mainCanvas"); + } + + /// 面板关闭时调用:仅清除临时编辑状态,保留已拟合结果 + public void OnPanelClosed() + { + UnregisterAll(); + ClearTempOverlays(); // 清除正在编辑的卡尺手柄 + // _committedOverlays 保留在画布上不清除 + } + + // ══════════════════════════════════════════════════════════════ + // 命令实现 + // ══════════════════════════════════════════════════════════════ + + /// 开始一次新的卡尺绘制(不影响已有结果) + private void ExecuteDrawCaliper() + { + // 清除当前临时编辑 + ClearTempOverlays(); + UnregisterAll(); + _lineDefined = false; + _dragging = DragTarget.None; + FitCommand.RaiseCanExecuteChanged(); + _drawClickCount = 0; + _isDrawingLine = true; + ResultText = "请在图像上点击搜索线起点"; + RegisterInteraction(); + } + + /// 执行拟合,将结果提交为永久显示 + private void ExecuteFit() + { + if (!_lineDefined) return; + + var imageSource = _viewportService?.CurrentDisplayImage as BitmapSource; + if (imageSource == null) { ResultText = "错误:无可用图像"; return; } + + try + { + BitmapSource source = imageSource; + if (imageSource.Format != PixelFormats.Gray8) + source = new FormatConvertedBitmap(imageSource, PixelFormats.Gray8, null, 0); + + int w = source.PixelWidth, h = source.PixelHeight; + int stride = w; + byte[] px = new byte[h * stride]; + source.CopyPixels(px, stride, 0); + + using var img = new Image(w, h); + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + img.Data[y, x, 0] = px[y * stride + x]; + + var proc = new EdgeLineFitProcessor(); + proc.SetParameter("StartX", (int)_lineStart.X); + proc.SetParameter("StartY", (int)_lineStart.Y); + proc.SetParameter("EndX", (int)_lineEnd.X); + proc.SetParameter("EndY", (int)_lineEnd.Y); + proc.SetParameter("CaliperCount", CaliperCount); + proc.SetParameter("CaliperWidth", (int)(_halfWidth * 2)); + proc.SetParameter("EdgePolarity", EdgePolarity); + proc.SetParameter("EdgeThreshold", EdgeThreshold); + proc.SetParameter("Sigma", Sigma); + proc.SetParameter("FitMethod", FitMethod); + proc.SetParameter("RansacThreshold", RansacThreshold); + + var result = proc.Process(img); + var od = proc.OutputData; + + if (od.ContainsKey("LineFitResult")) + { + var fr = od["LineFitResult"] as LineFitResult; + if (fr != null && fr.Success) + { + _fitCount++; + // 将当前卡尺从临时转为永久 + CommitCurrentCaliper(); + // 绘制拟合结果(永久) + DrawFitResult(fr, _fitCount); + ResultText = $"[#{_fitCount}] 拟合成功\n角度: {fr.AngleDegrees:F2}°\n" + + $"内点: {fr.Inliers.Count}/{fr.EdgePointCount}\n" + + $"误差: {fr.FitError:F3} px\n\n点击「画卡尺」继续下一条"; + // 拟合完成后清除编辑状态,准备下一次 + _lineDefined = false; + FitCommand.RaiseCanExecuteChanged(); + UnregisterAll(); + } + else + { + int ec = od.ContainsKey("EdgePointCount") ? (int)od["EdgePointCount"] : 0; + ResultText = $"拟合失败\n边缘点: {ec}\n请调整参数或拖拽手柄"; + } + } + result.Dispose(); + } + catch (Exception ex) { ResultText = $"异常: {ex.Message}"; } + } + + /// 清除所有(包括已拟合的结果) + private void ExecuteClearAll() + { + ClearTempOverlays(); + // 清除所有已提交的结果 + if (_mainCanvas != null) + { + foreach (var el in _committedOverlays) + _mainCanvas.Children.Remove(el); + } + _committedOverlays.Clear(); + _fitCount = 0; + UnregisterAll(); + _lineDefined = false; + _dragging = DragTarget.None; + FitCommand.RaiseCanExecuteChanged(); + ResultText = "已清除所有结果"; + } + + // ══════════════════════════════════════════════════════════════ + // 提交当前卡尺为永久显示 + // ══════════════════════════════════════════════════════════════ + + /// 将当前临时卡尺可视化转为永久(去掉手柄,保留边框和等分线) + private void CommitCurrentCaliper() + { + if (_mainCanvas == null) return; + + // 移除临时元素 + foreach (var el in _tempOverlays) + _mainCanvas.Children.Remove(el); + _tempOverlays.Clear(); + + // 重新绘制卡尺(无手柄,作为永久元素) + double dx = _lineEnd.X - _lineStart.X, dy = _lineEnd.Y - _lineStart.Y; + double len = Math.Sqrt(dx * dx + dy * dy); + if (len < 2) return; + + double ux = dx / len, uy = dy / len; + double px = -uy, py = ux; + double hw = _halfWidth; + + var c1 = new Point(_lineStart.X + px * hw, _lineStart.Y + py * hw); + var c2 = new Point(_lineEnd.X + px * hw, _lineEnd.Y + py * hw); + var c3 = new Point(_lineEnd.X - px * hw, _lineEnd.Y - py * hw); + var c4 = new Point(_lineStart.X - px * hw, _lineStart.Y - py * hw); + + // 矩形边框(半透明,不抢眼) + var border = new Polygon + { + Points = new System.Windows.Media.PointCollection { c1, c2, c3, c4 }, + Stroke = CaliperStroke, StrokeThickness = 1, Opacity = 0.5, + Fill = System.Windows.Media.Brushes.Transparent, IsHitTestVisible = false + }; + _mainCanvas.Children.Add(border); + _committedOverlays.Add(border); + + // 等分线 + int count = CaliperCount; + double step = len / (count + 1); + for (int i = 1; i <= count; i++) + { + double cx = _lineStart.X + ux * step * i, cy = _lineStart.Y + uy * step * i; + var line = new Line + { + X1 = cx + px * hw, Y1 = cy + py * hw, + X2 = cx - px * hw, Y2 = cy - py * hw, + Stroke = CaliperStroke, StrokeThickness = 1, Opacity = 0.3, IsHitTestVisible = false + }; + _mainCanvas.Children.Add(line); + _committedOverlays.Add(line); + } + } + + // ══════════════════════════════════════════════════════════════ + // 绘制拟合结果(永久) + // ══════════════════════════════════════════════════════════════ + + private void DrawFitResult(LineFitResult fr, int index) + { + if (_mainCanvas == null) return; + + // 拟合直线(蓝色) + AddCommitted(new Line + { + X1 = fr.Endpoint1.X, Y1 = fr.Endpoint1.Y, + X2 = fr.Endpoint2.X, Y2 = fr.Endpoint2.Y, + Stroke = FitLineBrush, StrokeThickness = 2, IsHitTestVisible = false + }); + + // 内点 + foreach (var pt in fr.Inliers) + AddCommitted(MakeDot(pt.X, pt.Y, Brushes.Lime)); + // 外点 + foreach (var pt in fr.Outliers) + AddCommitted(MakeDot(pt.X, pt.Y, Brushes.Red)); + + // 标注 + var lbl = new TextBlock + { + Text = $"#{index} ∠{fr.AngleDegrees:F2}° Err:{fr.FitError:F2}px", + Foreground = FitLineBrush, FontSize = 11, FontWeight = FontWeights.Bold, IsHitTestVisible = false + }; + Canvas.SetLeft(lbl, (fr.Endpoint1.X + fr.Endpoint2.X) / 2 + 5); + Canvas.SetTop(lbl, (fr.Endpoint1.Y + fr.Endpoint2.Y) / 2 - 18); + AddCommitted(lbl); + } + + private Ellipse MakeDot(double x, double y, SolidColorBrush fill) + { + var d = new Ellipse { Width = 5, Height = 5, Fill = fill, IsHitTestVisible = false }; + Canvas.SetLeft(d, x - 2.5); Canvas.SetTop(d, y - 2.5); + return d; + } + + private void AddCommitted(UIElement el) + { + _mainCanvas.Children.Add(el); + _committedOverlays.Add(el); + } + + // ══════════════════════════════════════════════════════════════ + // 临时卡尺可视化(编辑中,带手柄) + // ══════════════════════════════════════════════════════════════ + + private void RedrawTemp() + { + if (!_lineDefined || _mainCanvas == null) return; + ClearTempOverlays(); + DrawTempCaliper(); + } + + private void DrawTempCaliper() + { + if (_mainCanvas == null) return; + + double dx = _lineEnd.X - _lineStart.X, dy = _lineEnd.Y - _lineStart.Y; + double len = Math.Sqrt(dx * dx + dy * dy); + if (len < 2) return; + + double ux = dx / len, uy = dy / len; + double px = -uy, py = ux; + double hw = _halfWidth; + + var c1 = new Point(_lineStart.X + px * hw, _lineStart.Y + py * hw); + var c2 = new Point(_lineEnd.X + px * hw, _lineEnd.Y + py * hw); + var c3 = new Point(_lineEnd.X - px * hw, _lineEnd.Y - py * hw); + var c4 = new Point(_lineStart.X - px * hw, _lineStart.Y - py * hw); + + // 矩形 + AddTemp(new Polygon + { + Points = new System.Windows.Media.PointCollection { c1, c2, c3, c4 }, + Stroke = CaliperStroke, StrokeThickness = 1, Fill = CaliperFill, IsHitTestVisible = false + }); + + // 搜索线虚线 + AddTemp(new Line + { + X1 = _lineStart.X, Y1 = _lineStart.Y, X2 = _lineEnd.X, Y2 = _lineEnd.Y, + Stroke = CaliperStroke, StrokeThickness = 1, + StrokeDashArray = new DoubleCollection { 4, 3 }, IsHitTestVisible = false + }); + + // 等分线 + int count = CaliperCount; + double step = len / (count + 1); + for (int i = 1; i <= count; i++) + { + double cx = _lineStart.X + ux * step * i, cy = _lineStart.Y + uy * step * i; + AddTemp(new Line + { + X1 = cx + px * hw, Y1 = cy + py * hw, + X2 = cx - px * hw, Y2 = cy - py * hw, + Stroke = CaliperStroke, StrokeThickness = 1, Opacity = 0.4, IsHitTestVisible = false + }); + } + + // 极性箭头 + DrawPolarityArrow(px, py); + + // 手柄位置 + double midX = (_lineStart.X + _lineEnd.X) / 2, midY = (_lineStart.Y + _lineEnd.Y) / 2; + _handleStartPos = _lineStart; + _handleEndPos = _lineEnd; + _handleTopPos = new Point(midX + px * hw, midY + py * hw); + _handleBottomPos = new Point(midX - px * hw, midY - py * hw); + + // 绘制手柄 + AddTemp(MakeHandleVisual(_handleStartPos)); + AddTemp(MakeHandleVisual(_handleEndPos)); + AddTemp(MakeHandleVisual(_handleTopPos)); + AddTemp(MakeHandleVisual(_handleBottomPos)); + } + + private void DrawPolarityArrow(double px, double py) + { + double midX = (_lineStart.X + _lineEnd.X) / 2, midY = (_lineStart.Y + _lineEnd.Y) / 2; + double arrowLen = Math.Min(_halfWidth * 0.6, 16); + + if (EdgePolarity == "Both") + { + DrawArrow(midX, midY, px, py, arrowLen); + DrawArrow(midX, midY, -px, -py, arrowLen); + } + else if (EdgePolarity == "DarkToBright") + DrawArrow(midX, midY, px, py, arrowLen); + else + DrawArrow(midX, midY, -px, -py, arrowLen); + + string txt = EdgePolarity switch { "BrightToDark" => "B→D", "DarkToBright" => "D→B", _ => "↔" }; + var tb = new TextBlock { Text = txt, Foreground = CaliperStroke, FontSize = 10, IsHitTestVisible = false }; + Canvas.SetLeft(tb, midX + px * (_halfWidth + 12)); + Canvas.SetTop(tb, midY + py * (_halfWidth + 12) - 7); + AddTemp(tb); + } + + private void DrawArrow(double fx, double fy, double dx, double dy, double length) + { + double tx = fx + dx * length, ty = fy + dy * length; + AddTemp(new Line { X1 = fx, Y1 = fy, X2 = tx, Y2 = ty, Stroke = CaliperStroke, StrokeThickness = 1.5, IsHitTestVisible = false }); + double ang = Math.Atan2(dy, dx), hl = 5; + double a1 = ang + 2.5, a2 = ang - 2.5; + AddTemp(new Line { X1 = tx, Y1 = ty, X2 = tx + Math.Cos(a1) * hl, Y2 = ty + Math.Sin(a1) * hl, Stroke = CaliperStroke, StrokeThickness = 1.5, IsHitTestVisible = false }); + AddTemp(new Line { X1 = tx, Y1 = ty, X2 = tx + Math.Cos(a2) * hl, Y2 = ty + Math.Sin(a2) * hl, Stroke = CaliperStroke, StrokeThickness = 1.5, IsHitTestVisible = false }); + } + + private Ellipse MakeHandleVisual(Point pos) + { + var h = new Ellipse + { + Width = HandleSize, Height = HandleSize, + Fill = HandleFill, Stroke = CaliperStroke, StrokeThickness = 2, + IsHitTestVisible = false + }; + Canvas.SetLeft(h, pos.X - HandleSize / 2); + Canvas.SetTop(h, pos.Y - HandleSize / 2); + return h; + } + + private void AddTemp(UIElement el) { _mainCanvas.Children.Add(el); _tempOverlays.Add(el); } + private void ClearTempOverlays() + { + if (_mainCanvas == null) return; + foreach (var el in _tempOverlays) _mainCanvas.Children.Remove(el); + _tempOverlays.Clear(); + } + + // ══════════════════════════════════════════════════════════════ + // 统一鼠标交互 + // ══════════════════════════════════════════════════════════════ + + private bool _interactionRegistered; + + private void RegisterInteraction() + { + if (_canvas == null || _interactionRegistered) return; + _canvas.PreviewMouseLeftButtonDown += OnMouseDown; + _canvas.PreviewMouseMove += OnMouseMove; + _canvas.PreviewMouseLeftButtonUp += OnMouseUp; + _interactionRegistered = true; + } + + private void UnregisterAll() + { + if (_canvas == null || !_interactionRegistered) return; + _canvas.PreviewMouseLeftButtonDown -= OnMouseDown; + _canvas.PreviewMouseMove -= OnMouseMove; + _canvas.PreviewMouseLeftButtonUp -= OnMouseUp; + _interactionRegistered = false; + _isDrawingLine = false; + _dragging = DragTarget.None; + } + + private void OnMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + if (_mainCanvas == null) return; + var pos = e.GetPosition(_mainCanvas); + + // 绘制模式 + if (_isDrawingLine) + { + _drawClickCount++; + if (_drawClickCount == 1) + { + _lineStart = pos; + ResultText = "请点击搜索线终点"; + } + else if (_drawClickCount == 2) + { + _lineEnd = pos; + _isDrawingLine = false; + _lineDefined = true; + _halfWidth = DisplayWidth / 2.0; + FitCommand.RaiseCanExecuteChanged(); + RedrawTemp(); + ResultText = $"搜索线已定义 ({Len():F0}px)\n拖拽手柄调整,点击「拟合」执行"; + } + e.Handled = true; + return; + } + + // 拖拽模式 + if (_lineDefined) + { + var target = HitTestHandle(pos); + if (target != DragTarget.None) + { + _dragging = target; + _canvas.CaptureMouse(); + e.Handled = true; + } + } + } + + private void OnMouseMove(object sender, System.Windows.Input.MouseEventArgs e) + { + if (_dragging == DragTarget.None || _mainCanvas == null) return; + var pos = e.GetPosition(_mainCanvas); + + switch (_dragging) + { + case DragTarget.Start: + _lineStart = pos; + break; + case DragTarget.End: + _lineEnd = pos; + break; + case DragTarget.Top: + case DragTarget.Bottom: + double dist = PointToLineDist(pos, _lineStart, _lineEnd); + _halfWidth = Math.Max(5, dist); + SetProperty(ref _displayWidth, (int)(_halfWidth * 2), nameof(DisplayWidth)); + break; + } + + ClearTempOverlays(); + DrawTempCaliper(); + e.Handled = true; + } + + private void OnMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + if (_dragging == DragTarget.None) return; + _dragging = DragTarget.None; + _canvas.ReleaseMouseCapture(); + ResultText = $"搜索线: {Len():F0}px, 宽度: {(int)(_halfWidth * 2)}px\n点击「拟合」执行"; + e.Handled = true; + } + + private DragTarget HitTestHandle(Point pos) + { + if (Dist(pos, _handleStartPos) <= HitRadius) return DragTarget.Start; + if (Dist(pos, _handleEndPos) <= HitRadius) return DragTarget.End; + if (Dist(pos, _handleTopPos) <= HitRadius) return DragTarget.Top; + if (Dist(pos, _handleBottomPos) <= HitRadius) return DragTarget.Bottom; + return DragTarget.None; + } + + // ══════════════════════════════════════════════════════════════ + // 辅助 + // ══════════════════════════════════════════════════════════════ + + private double Len() + { + double dx = _lineEnd.X - _lineStart.X, dy = _lineEnd.Y - _lineStart.Y; + return Math.Sqrt(dx * dx + dy * dy); + } + + private static double Dist(Point a, Point b) + { + double dx = a.X - b.X, dy = a.Y - b.Y; + return Math.Sqrt(dx * dx + dy * dy); + } + + private static double PointToLineDist(Point p, Point a, Point b) + { + double abx = b.X - a.X, aby = b.Y - a.Y; + double len2 = abx * abx + aby * aby; + if (len2 < 1e-6) return Dist(p, a); + return Math.Abs(abx * (a.Y - p.Y) - aby * (a.X - p.X)) / Math.Sqrt(len2); + } + + private static T FindChild(DependencyObject parent, string name) where T : FrameworkElement + { + int count = VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < count; i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + if (child is T t && t.Name == name) return t; + var r = FindChild(child, name); + if (r != null) return r; + } + return null; + } + } +} diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index e781ed2..a62cb20 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -143,6 +143,7 @@ namespace XplorePlane.ViewModels public DelegateCommand GrayscaleCommand { get; } public DelegateCommand SharpenCommand { get; } public DelegateCommand EnhanceCommand { get; } + public DelegateCommand EdgeLineFitCommand { get; } // 设置命令 public DelegateCommand OpenLanguageSwitcherCommand { get; } @@ -350,6 +351,7 @@ namespace XplorePlane.ViewModels GrayscaleCommand = new DelegateCommand(ExecuteGrayscale); SharpenCommand = new DelegateCommand(ExecuteSharpen); EnhanceCommand = new DelegateCommand(ExecuteEnhance); + EdgeLineFitCommand = new DelegateCommand(ExecuteEdgeLineFit); AxisResetCommand = new DelegateCommand(ExecuteAxisReset); OpenDoorCommand = new DelegateCommand(ExecuteOpenDoor); @@ -888,6 +890,7 @@ namespace XplorePlane.ViewModels } private Window _bgaDetectionPanel; + private Window _edgeLineFitPanel; private void ExecuteBgaDetection() { @@ -1172,6 +1175,25 @@ namespace XplorePlane.ViewModels } } + private void ExecuteEdgeLineFit() + { + if (!CheckImageLoaded()) return; + _logger.Info("边缘查找拟合直线功能已触发"); + + if (_edgeLineFitPanel != null && _edgeLineFitPanel.IsVisible) + { + _edgeLineFitPanel.Activate(); + return; + } + + _edgeLineFitPanel = new Views.ImageProcessing.EdgeLineFitPanel + { + Owner = System.Windows.Application.Current.MainWindow + }; + _edgeLineFitPanel.Closed += (_, _) => { _edgeLineFitPanel = null; }; + _edgeLineFitPanel.Show(); + } + private Image? BitmapSourceToImage(BitmapSource bitmapSource) { // 转换为可用的图像格式 diff --git a/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml b/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml new file mode 100644 index 0000000..f3868a1 --- /dev/null +++ b/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Both + BrightToDark + DarkToBright + + + + + + + + + + + + + + + + + + + + + RANSAC + LeastSquares + + + + + + + + + + + + + + + + + + + + diff --git a/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml.cs b/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml.cs new file mode 100644 index 0000000..c2dc50b --- /dev/null +++ b/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml.cs @@ -0,0 +1,53 @@ +using System.Windows; +using Prism.Ioc; +using XP.ImageProcessing.RoiControl.Controls; +using XplorePlane.Services.MainViewport; +using XplorePlane.ViewModels.ImageProcessing; + +namespace XplorePlane.Views.ImageProcessing +{ + public partial class EdgeLineFitPanel : Window + { + public EdgeLineFitPanel() + { + InitializeComponent(); + + var viewportService = ContainerLocator.Current?.Resolve(); + DataContext = new EdgeLineFitViewModel(viewportService); + + Loaded += (s, e) => + { + var mainWin = Owner as MainWindow; + if (mainWin != null) + { + var canvas = FindChild(mainWin); + if (DataContext is EdgeLineFitViewModel vm) + { + vm.SetCanvas(canvas); + // 自动进入绘制模式 + vm.DrawCaliperCommand.Execute(); + } + } + }; + + Closed += (s, e) => + { + if (DataContext is EdgeLineFitViewModel vm) + vm.OnPanelClosed(); + }; + } + + private static T FindChild(DependencyObject parent) where T : DependencyObject + { + int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < count; i++) + { + var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, i); + if (child is T t) return t; + var result = FindChild(child); + if (result != null) return result; + } + return null; + } + } +} diff --git a/XplorePlane/Views/Main/MainWindow.xaml b/XplorePlane/Views/Main/MainWindow.xaml index b63c79e..ac6d80d 100644 --- a/XplorePlane/Views/Main/MainWindow.xaml +++ b/XplorePlane/Views/Main/MainWindow.xaml @@ -213,6 +213,13 @@ Size="Medium" SmallImage="/Assets/Icons/dynamic-range.png" Text="增强" /> + diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs index f7e90a3..652e5f6 100644 --- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs +++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs @@ -76,6 +76,7 @@ namespace XplorePlane.Views "FillRate" => "填锡率", "BgaVoid" => "BGA空隙", "BubbleVoid" => "气泡空隙", + "EdgeLineFit" => "直线拟合", _ => "点点距" }; string valueText = args.MeasureType switch @@ -84,9 +85,16 @@ namespace XplorePlane.Views "FillRate" => $"{args.Distance:F1}%", "BgaVoid" => $"{args.Distance:F1}%", "BubbleVoid" => $"{args.Distance:F1}%", + "EdgeLineFit" => "处理中...", _ => $"{args.Distance:F2} px" }; SetStatus($"{typeLabel}: {valueText} | 共 {args.TotalCount} 条测量"); + + // 边缘查找拟合直线:获取搜索线后执行算子 + if (args.MeasureType == "EdgeLineFit") + { + ExecuteEdgeLineFitProcessor(args.P1, args.P2); + } } }; RoiCanvas.MeasureStatusChanged += (s, e) => @@ -115,6 +123,7 @@ namespace XplorePlane.Views MeasurementToolMode.ThroughHoleFillRate => XP.ImageProcessing.RoiControl.Models.MeasureMode.FillRate, MeasurementToolMode.BgaVoid => XP.ImageProcessing.RoiControl.Models.MeasureMode.BgaVoid, MeasurementToolMode.BubbleMeasure => XP.ImageProcessing.RoiControl.Models.MeasureMode.BubbleMeasure, + MeasurementToolMode.EdgeLineFit => XP.ImageProcessing.RoiControl.Models.MeasureMode.EdgeLineFit, _ => XP.ImageProcessing.RoiControl.Models.MeasureMode.None }; }, Prism.Events.ThreadOption.UIThread); @@ -782,6 +791,154 @@ namespace XplorePlane.Views #endregion + #region 边缘查找拟合直线 + + private void ExecuteEdgeLineFitProcessor(Point startPoint, Point endPoint) + { + try + { + var vm = GetMainVm(); + if (vm == null) return; + + // 获取当前图像 + var viewportVm = ContainerLocator.Current?.Resolve(); + var imageSource = viewportVm?.ImageSource as BitmapSource; + if (imageSource == null) + { + SetStatus("直线拟合失败:无可用图像"); + return; + } + + // 转换为 Emgu.CV Image + BitmapSource source = imageSource; + if (imageSource.Format != System.Windows.Media.PixelFormats.Gray8) + source = new FormatConvertedBitmap(imageSource, System.Windows.Media.PixelFormats.Gray8, null, 0); + + int width = source.PixelWidth; + int height = source.PixelHeight; + int stride = width; + byte[] pixels = new byte[height * stride]; + source.CopyPixels(pixels, stride, 0); + + using var inputImage = new Emgu.CV.Image(width, height); + for (int y = 0; y < height; y++) + for (int x = 0; x < width; x++) + inputImage.Data[y, x, 0] = pixels[y * stride + x]; + + // 创建并配置算子 + var processor = new XP.ImageProcessing.Processors.EdgeLineFitProcessor(); + processor.SetParameter("StartX", (int)startPoint.X); + processor.SetParameter("StartY", (int)startPoint.Y); + processor.SetParameter("EndX", (int)endPoint.X); + processor.SetParameter("EndY", (int)endPoint.Y); + + // 执行处理 + var result = processor.Process(inputImage); + + // 获取输出数据并在画布上绘制结果 + var outputData = processor.OutputData; + if (outputData.ContainsKey("LineFitResult")) + { + var fitResult = outputData["LineFitResult"] as XP.ImageProcessing.Processors.LineFitResult; + if (fitResult != null && fitResult.Success) + { + DrawEdgeLineFitResult(fitResult, outputData); + SetStatus($"直线拟合完成: 角度={fitResult.AngleDegrees:F2}°, 内点={fitResult.Inliers.Count}/{fitResult.EdgePointCount}, 误差={fitResult.FitError:F3}px"); + } + else + { + SetStatus("直线拟合失败:未找到足够的边缘点"); + } + } + + result.Dispose(); + } + catch (Exception ex) + { + SetStatus($"直线拟合异常: {ex.Message}"); + } + } + + private readonly System.Collections.Generic.List _elfOverlays = new(); + + private void DrawEdgeLineFitResult( + XP.ImageProcessing.Processors.LineFitResult fitResult, + System.Collections.Generic.Dictionary outputData) + { + var canvas = FindChildByName(RoiCanvas, "mainCanvas"); + if (canvas == null) return; + + // 清除之前的拟合结果 + foreach (var el in _elfOverlays) + canvas.Children.Remove(el); + _elfOverlays.Clear(); + + // 绘制拟合直线(绿色) + var fitLine = new System.Windows.Shapes.Line + { + X1 = fitResult.Endpoint1.X, + Y1 = fitResult.Endpoint1.Y, + X2 = fitResult.Endpoint2.X, + Y2 = fitResult.Endpoint2.Y, + Stroke = System.Windows.Media.Brushes.Lime, + StrokeThickness = 2, + IsHitTestVisible = false + }; + canvas.Children.Add(fitLine); + _elfOverlays.Add(fitLine); + + // 绘制内点(绿色小圆点) + foreach (var pt in fitResult.Inliers) + { + var dot = new System.Windows.Shapes.Ellipse + { + Width = 6, + Height = 6, + Fill = System.Windows.Media.Brushes.Lime, + IsHitTestVisible = false + }; + System.Windows.Controls.Canvas.SetLeft(dot, pt.X - 3); + System.Windows.Controls.Canvas.SetTop(dot, pt.Y - 3); + canvas.Children.Add(dot); + _elfOverlays.Add(dot); + } + + // 绘制外点(红色小圆点) + foreach (var pt in fitResult.Outliers) + { + var dot = new System.Windows.Shapes.Ellipse + { + Width = 6, + Height = 6, + Fill = System.Windows.Media.Brushes.Red, + IsHitTestVisible = false + }; + System.Windows.Controls.Canvas.SetLeft(dot, pt.X - 3); + System.Windows.Controls.Canvas.SetTop(dot, pt.Y - 3); + canvas.Children.Add(dot); + _elfOverlays.Add(dot); + } + + // 绘制角度标注 + var labelText = $"∠{fitResult.AngleDegrees:F2}° | Err:{fitResult.FitError:F2}px"; + var label = new System.Windows.Controls.TextBlock + { + Text = labelText, + Foreground = System.Windows.Media.Brushes.Yellow, + FontSize = 12, + FontWeight = FontWeights.Bold, + IsHitTestVisible = false + }; + double labelX = (fitResult.Endpoint1.X + fitResult.Endpoint2.X) / 2 + 5; + double labelY = (fitResult.Endpoint1.Y + fitResult.Endpoint2.Y) / 2 - 20; + System.Windows.Controls.Canvas.SetLeft(label, labelX); + System.Windows.Controls.Canvas.SetTop(label, labelY); + canvas.Children.Add(label); + _elfOverlays.Add(label); + } + + #endregion + private static T FindChildByName(DependencyObject parent, string name) where T : FrameworkElement { int count = VisualTreeHelper.GetChildrenCount(parent); From 843c4d67a6417c6503e1dfd4b9bf41548bd41e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Mon, 18 May 2026 13:11:26 +0800 Subject: [PATCH 10/12] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90=E6=B5=B7?= =?UTF-8?q?=E5=BA=B7=E5=A8=81=E8=A7=86=E7=9B=B8=E6=9C=BA=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 HikvisionCameraController 实现 ICameraController - CameraFactory 支持 Basler/Hikvision 动态切换(config.json 配置) - PixelConverter 支持 Bayer RG/GR/GB/BG 8-bit 解码 - 修复采集链断裂问题(finally 中触发下一帧) - 相机设置面板:宽高和像素格式改为只读显示 - NavigationPropertyPanelViewModel 日志和状态文本改为英文 - 添加 MvCameraControl.Net.dll 到 ExternalLibraries --- ExternalLibraries/MvCameraControl.Net.XML | 18755 ++++++++++++++++ ExternalLibraries/MvCameraControl.Net.dll | Bin 0 -> 177152 bytes ExternalLibraries/config.json | 3 +- XP.Camera/Converters/PixelConverter.cs | 149 +- XP.Camera/Core/CameraFactory.cs | 4 +- .../Hikvision/HikvisionCameraController.cs | 535 + XP.Camera/XP.Camera.csproj | 7 + XplorePlane/App.xaml.cs | 49 +- .../Main/NavigationPropertyPanelViewModel.cs | 62 +- .../Views/Setting/CameraSettingsWindow.xaml | 24 +- .../Setting/CameraSettingsWindow.xaml.cs | 3 - XplorePlane/XplorePlane.csproj | 6 + 12 files changed, 19524 insertions(+), 73 deletions(-) create mode 100644 ExternalLibraries/MvCameraControl.Net.XML create mode 100644 ExternalLibraries/MvCameraControl.Net.dll create mode 100644 XP.Camera/Hikvision/HikvisionCameraController.cs diff --git a/ExternalLibraries/MvCameraControl.Net.XML b/ExternalLibraries/MvCameraControl.Net.XML new file mode 100644 index 0000000..e79ec14 --- /dev/null +++ b/ExternalLibraries/MvCameraControl.Net.XML @@ -0,0 +1,18755 @@ + + + + MvCameraControl.Net + + + + + 排序方式 + + + + + 按序列号排序 + + + + + 按用户自定义名字排序 + + + + + 按当前IP地址排序(升序) + + + + + 按当前IP地址排序(降序) + + + + + 动作命令信息 + + + + + 设备密钥 + + + + + 组键 + + + + + 组掩码 + + + + + 只有设置成1时Action Time才有效,非1时无效 + + + + + 预定的时间,和主频有关 + + + + + 广播包地址 + + + + + 等待ACK的超时时间,如果为0表示不需要ACK + + + + + 只有设置成1时指定的网卡IP才有效,非1时无效 + + + + + 指定的网卡IP + + + + + 动作命令返回信息 + + + + + 设备IP + + + + + 状态码 + + + + + 设备枚举类,支持枚举GigE Vision、USB3 Vision相机,及采集卡上的相机(GigE Vision、CameraLink、CoaXPress、XoFlink) + + + + + 枚举设备 + + 设备接口类型 + 设备列表 + 成功,返回MV_OK;失败,返回错误码 + + 设备接口类型为MV_GIGE_DEVICE时,枚举所有GigE设备,包含虚拟GigE设备和采集卡上的GigE设备。 + 设备接口类型为MV_USB_DEVICE时,枚举所有USB设备,包含虚拟USB设备。 + + 设备列表的内存是在SDK内部分配的,调用该接口时会进行设备列表内存的释放和申请,避免多线程枚举操作。 + + 枚举到设备后,通过创建设备实例。 + + + + + 枚举设备,支持枚举指定厂商的设备 + + 设备接口类型 + 厂商名称 + 设备列表 + 成功,返回MV_OK;失败,返回错误码 + + 设备接口类型为MV_GIGE_DEVICE时,枚举所有GigE设备,包含虚拟GigE设备和采集卡上的GigE设备。 + 设备接口类型为MV_USB_DEVICE时,枚举所有USB设备,包含虚拟USB设备。 + + 设备列表的内存是在SDK内部分配的,调用该接口时会进行设备列表内存的释放和申请,避免多线程枚举操作。 + + 枚举到设备后,通过创建设备实例。 + + + + + 枚举设备, 可指定排序方式枚举、根据厂商名字过滤 + + 设备接口类型 + 排序方式 + 厂商名称 + 设备列表 + 成功,返回MV_OK;失败,返回错误码 + + 设备接口类型为MV_GIGE_DEVICE时,仅枚举网络上的网口相机,不包含虚拟GigE相机和采集卡上的相机 + 设备接口类型为MV_USB_DEVICE时,枚举普通USB设备,不包含虚拟USB设备。 + + 设备列表的内存是在SDK内部分配的,调用该接口时会进行设备列表内存的释放和申请,避免多线程枚举操作。 + + 枚举到设备后,通过创建设备实例。 + + + + + 判断设备是否可达 + + 设备信息 + 访问权限 + 可达,返回true;不可达,返回false + + + + 设置GigE设备枚举超时时间,范围 1-UINT_MAX(包括1,不包括UINT_MAX) + + 超时时间 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置枚举命令的回复包类型 + + 回复包类型(默认广播),0-单播,1-广播 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取主机串口列表 + + 串口列表 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置在指定的串口上枚举设备 + + 串口列表 + 成功,返回MV_OK;失败,返回错误码 + + + + 发出动作命令 + + 动作命令信息 + 动作命令返回信息列表 + 成功,返回MV_OK;失败,返回错误码 + + 仅GigEVision相机支持 + + + + + 获取GigE设备组播状态 + + 设备信息 + 组播状态(true:组播状态;false:非组播) + 成功,返回MV_OK;失败,返回错误码 + + 仅GigEVision相机支持 + + + + + 将用户的设备信息格式转换为SDK的内部设备信息格式 + + + + + + + + 设备工厂类,用于创建设备实例 + + + + + 创建设备对象 + + 设备信息 + 成功-返回设备实例,失败-抛出异常 + + + + 通过设备IP地址创建设备,适用于GigE设备,不包含虚拟设备与采集卡设备 + + 设备IP地址 + 网口IP地址 + 成功-返回设备实例,失败-抛出异常 + + + + 通过GenTL设备信息创建设备句柄 + + 设备信息 + 成功-返回设备实例,失败-抛出异常 + + + + 设备接口类型 + + + + GigE Vision 设备 + + + USB3 Vision 设备 + + + Camera Link 设备(串口) + + + 虚拟 GigE Vision 设备 + + + 虚拟 USB3 Vision 设备 + + + 网口采集卡下GigE Vision设备 + + + Camera Link 设备 + + + CoaXPress设备 + + + XoFLink设备 + + + + 设备基本信息 + + + + + 设备接口类型 + + + + + 制造商信息 + + + + + 设备型号 + + + + + 设备版本 + + + + + 设备序列号 + + + + + 用户自定义名称 + + + + + 设备类型信息,7 - 0 bit: 预留,15 - 8 bit:产品子类别,23 - 16 bit:产品类型,31 - 24bit:产品线(如: 0x01 标准产品;0x02 3D产品;0x03 智能ID产品) + + + + + GigE相机信息 + + + + + GigE Vision协议主要版本 + + + + + GigE Vision协议次要版本 + + + + + 高MAC地址 + + + + + 低MAC地址 + + + + + IP配置选项 + + + + + 当前IP配置 + + + + + 当前IP地址 + + + + + 当前子网掩码 + + + + + 当前网关 + + + + + 网口IP地址 + + + + + 是否虚拟相机 + + + + + 是否采集卡上的相机 + + + + + USB相机信息 + + + + + 控制输入端点 + + + + + 控制输出端点 + + + + + 流端点 + + + + + 事件端点 + + + + + 供应商ID号 + + + + + 产品ID号 + + + + + 设备索引号 + + + + + 设备GUID号 + + + + + 家族名字 + + + + + 支持的USB协议 + + + + + 设备地址 + + + + + 是否虚拟相机 + + + + + Camera Link串口设备信息 + + + + + 端口号 + + + + + 名称 + + + + + CoaXPress设备信息 + + + + + 相机ID + + + + + 采集卡ID + + + + + Camera Link设备信息 + + + + + 相机ID + + + + + 采集卡ID + + + + + XoFLink设备信息 + + + + + 相机ID + + + + + 采集卡ID + + + + + 设备访问权限,只支持GigE设备 + + + + + 独占权限,其他APP只允许读CCP寄存器 + + + + + 可以从5模式下抢占权限,然后以独占权限打开 + + + + + 控制权限,其他APP允许读所有寄存器 + + + + + 可以从5模式下抢占权限,然后以控制权限打开 + + + + + 以可被抢占的控制权限打开 + + + + + 可以从5模式下抢占权限,然后以可被抢占的控制权限打开 + + + + + 读模式打开设备,适用于控制权限下 + + + + + IP配置类型 + + + + + 静态IP + + + + + DHCP自动获取IP + + + + + LLA(Link-local address),链路本地地址 + + + + + GigE设备传输模式 + + + + + 驱动模式 + + + + + Socket模式 + + + + + GigE设备网络传输的相关信息 + + + + + 已接收数据大小 [统计StartGrabbing和StopGrabbing之间的数据量] + + + + + 丢失的包数量 + + + + + 丢帧数量 + + + + + 已接收的帧数 + + + + + 请求重发包数 + + + + + 重发包数 + + + + + USB传输信息 + + + + + 已接收数据大小 [Open和Close之间] + + + + + 已收到的帧数 + + + + + 错误帧数 + + + + + GigE传输类型 + + + + + 单播 + + + + + 组播 + + + + + 局域网内广播 + + + + + 子网内广播 + + + + + 从相机获取 + + + + + 用户自定义应用端接收图像数据Port号 + + + + + 设置了单播,但本实例不接收图像数据 + + + + + 组播模式,但本实例不接收图像数据 + + + + + 设备异常类型 + + + + + 设备断开连接 + + + + + 设备异常消息 + + + + + 消息类型 + + + + + 提供设备通用的属性和接口 + + + + + 打开设备,默认以独占权限打开 + + 成功,返回MV_OK;失败,返回错误码 + + + + 以指定访问权限打开设备,只支持GigE设备 + + 访问权限 + 切换访问权限时的密钥 + 成功,返回MV_OK;失败,返回错误码 + + 目前设备暂不支持MV_ACCESS_ExclusiveWithSwitch、MV_ACCESS_ControlWithSwitch、MV_ACCESS_ControlSwitchEnable、MV_ACCESS_ControlSwitchEnableWithKey这四种抢占模式。 + + + + + 关闭设备 + + 成功,返回MV_OK;失败,返回错误码 + + + + 设备本地升级 + + 升级文件路径 + 成功,返回MV_OK;失败,返回错误码 + + 通过该接口可以将升级固件文件发送给设备进行升级。该接口需要等待升级固件文件成功传给设备端之后再返回,响应时间可能较长。 + + + + + 获取升级进度 + + 升级进度 + 成功,返回MV_OK;失败,返回错误码 + + + + 开启设备指定事件 + + 事件名称 + 成功,返回MV_OK;失败,返回错误码 + + + + 关闭设备指定事件 + + 事件名称 + 成功,返回MV_OK;失败,返回错误码 + + + + 设备异常事件 + + + + + 判断设备是否处于连接状态 + + + + + 获取设备对应的图像采集对象 + + + + + 获取设备对应的事件采集对象 + + + + + 获取设备信息 + + + + + 获取设备对应的参数配置对象 + + + + + 获取格式转换对象 + + + + + 获取用于图像处理的对象 + + + + + 获取用于图像保存的对象 + + + + + 获取用于解码图像的的对象 + + + + + 获取录像对象 + + + + + 获取图像和图形渲染对象 + + + + + GigE设备专用接口 + + + + + 获取最佳包大小 + + 最佳包大小 + 成功,返回MV_OK;失败,返回错误码 + + + + 强制设备IP + + IP地址 + 子网掩码 + 默认网关 + 成功,返回MV_OK;失败,返回错误码 + + 强制设置设备网络参数(包括IP、子网掩码、默认网关),强制设置之后将需要重新创建设备句柄,仅GigE设备支持。 + 如果设备为DHCP的状态,调用该接口强制设置设备网络参数之后设备将会重启。 + + + + + 配置IP方式 + + IP配置类型 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置传输模式,默认为Driver模式 + + 网络传输模式 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取网络传输信息 + + 网络传输信息 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置GVSP取流超时时间 + + 超时时间(MS),默认300ms,范围:>10ms + 成功,返回MV_OK;失败,返回错误码 + + + + 获取GVSP取流超时时间 + + 超时时间(MS) + 成功,返回MV_OK;失败,返回错误码 + + + + 设置GVCP命令超时时间 + + 超时时间(MS),默认500ms,范围:0-10000ms + 成功,返回MV_OK;失败,返回错误码 + + + + 获取GVCP命令超时时间 + + 超时时间(MS) + 成功,返回MV_OK;失败,返回错误码 + + + + 设置重传GVCP命令次数 + + 重传次数,范围:0-100 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取重传GVCP命令次数 + + 重传次数 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置是否打开重发包,及重发包参数 + + 是否支持重发包 + 最大重发比 + 重发超时时间 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置重传命令最大尝试次数 + + 重传命令最大尝试次数,默认值20 + 成功,返回MV_OK;失败,返回错误码 + + 该接口必须在调用开启重传包功能之后调用,否则失败且返回MV_E_CALLORDER。 + + + + + 获取重传命令最大尝试次数 + + 传命令最大尝试次数 + 成功,返回MV_OK;失败,返回错误码 + + 该接口必须在调用开启重传包功能之后调用,否则失败且返回MV_E_CALLORDER。 + + + + + 设置同一重传包多次请求之间的时间间隔 + + 同一重传包多次请求之间的时间间隔,默认10ms + 成功,返回MV_OK;失败,返回错误码 + + + + 获取同一重传包多次请求之间的时间间隔 + + 同一重传包多次请求之间的时间间隔(以毫秒为单位) + 成功,返回MV_OK;失败,返回错误码 + + + + 设置传输模式,可以为单播模式、组播模式等 + + 传输模式 + 组播地址,组播模式下有意义 + 组播端口,组播模式下有意义 + 成功,返回MV_OK;失败,返回错误码 + + + + USB设备专用接口 + + + + + 设置U3V的传输包大小 + + 传输的包大小,单位:Byte,默认为1M,范围:Windows[0x400, 0x400000], Linux[0x400, 0x200000] + 成功,返回MV_OK;失败,返回错误码 + + 增加传输包大小可以适当降低取流时的CPU占用率。但不同的PC和不同USB扩展卡存在不同的兼容性,如果该参数设置过大可能会出现取不到图像的风险。 + + + + + 获取U3V的传输包大小 + + 传输的包大小, 单位:Byte + 成功,返回MV_OK;失败,返回错误码 + + + + 设置U3V的传输通道个数 + + 传输通道个数,范围:1-10 + 成功,返回MV_OK;失败,返回错误码 + + 可根据PC的性能、设备出图帧率、图像大小和内存使用率等因素对该参数进行调节。但不同的PC和不同的USB扩展卡存在不同的兼容性。 + + + + + 获取U3V的传输通道个数 + + 传输通道个数 + 成功,返回MV_OK;失败,返回错误码 + + 该接口用于获取当前的U3V异步取流节点个数,2000W设备的MONO8默认为3个,YUV为默认2个,RGB为默认1个,其它情况默认8个节点。 + + + + + 设置U3V的事件缓存节点个数 + + 事件缓存节点个数,范围:1-64 + 成功,返回MV_OK;失败,返回错误码 + + 该接口用于设置当前的U3V事件缓存节点个数,默认情况下为5个。 + + + + + 设置U3V相机同步读写超时时间,范围为1000~UINT,默认1000 ms + + 同步读写超时时间 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取U3V相机同步读写超时时间 + + 同步读写超时时间 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取主机从USB设备接收的数据统计信息,如已接收字节数、帧数 + + USB传输信息 + 成功,返回MV_OK;失败,返回错误码 + + + + Camera Link波特率 + + + + + 9600 + + + + + 19200 + + + + + 38400 + + + + + 57600 + + + + + 115200 + + + + + 230400 + + + + + 460800 + + + + + 921600 + + + + + 最大值 + + + + + Camera Link串口设备专用接口 + + + + + 设置设备波特率 + + 波特率 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取设备波特率 + + 波特率 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取设备与主机间连接支持的波特率 + + 所支持波特率的或运算结果,单个波特率参考 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置串口操作等待时长 + + 串口操作的等待时长,单位为ms + 成功,返回MV_OK;失败,返回错误码 + + + + 错误码定义 + + + + 成功,无错误 + + + 错误或无效的句柄 + + + 不支持的功能 + + + 缓存已满 + + + 函数调用顺序错误 + + + 错误的参数 + + + 资源申请失败 + + + 无数据 + + + 前置条件有误,或运行环境已发生变化 + + + 版本不匹配 + + + 传入的内存空间不足 + + + 异常图像,可能是丢包导致图像不完整 + + + 动态导入DLL失败 + + + 没有可输出的缓存 + + + 加密错误 + + + 未知的错误 + + + 通用错误 + + + 参数非法 + + + 值超出范围 + + + 属性 + + + 运行环境有问题 + + + 逻辑错误 + + + 节点访问条件有误 + + + 超时 + + + 转换异常 + + + GenICam未知错误 + + + 命令不被设备支持 + + + 访问的目标地址不存在 + + + 目标地址不可写 + + + 设备无访问权限 + + + 设备忙,或网络断开 + + + 网络包数据错误 + + + 网络相关错误 + + + 设备IP冲突 + + + 读usb出错 + + + 写usb出错 + + + 设备异常 + + + GenICam相关错误 + + + 带宽不足 + + + 驱动不匹配或者未装驱动 + + + USB未知的错误 + + + 升级固件不匹配 + + + 升级固件语言不匹配 + + + 升级冲突(设备已经在升级了再次请求升级即返回此错误) + + + 升级时设备内部出现错误 + + + 升级时未知错误 + + + 处理正确 + + + 不确定类型错误 + + + 能力集中存在无效参数 + + + 内存地址为空 + + + 内存对齐不满足要求 + + + 内存空间大小不够 + + + 内存空间大小不满足对齐要求 + + + 内存地址不满足对齐要求 + + + 图像格式不正确或者不支持 + + + 图像宽高不正确或者超出范围 + + + 图像宽高与step参数不匹配 + + + 图像数据存储地址为空 + + + 设置或者获取参数类型不正确 + + + 设置或者获取参数的输入、输出结构体大小不正确 + + + 处理类型不正确 + + + 处理时输入、输出参数大小不正确 + + + 子处理类型不正确 + + + 子处理时输入、输出参数大小不正确 + + + index参数不正确 + + + value参数不正确或者超出范围 + + + param_num参数不正确 + + + 函数参数指针为空 + + + 超过限定的最大内存 + + + 回调函数出错 + + + 加密错误 + + + 算法库使用期限错误 + + + 参数范围不正确 + + + 数据大小不正确 + + + 数据step不正确 + + + cpu不支持优化代码中的指令集 + + + 警告 + + + 算法库超时 + + + 算法版本号出错 + + + 模型版本号出错 + + + GPU内存分配错误 + + + 文件不存在 + + + 字符串为空 + + + 图像解码器错误 + + + 打开文件错误 + + + 文件读取错误 + + + 文件写错误 + + + 文件读取大小错误 + + + 文件类型错误 + + + 模型类型错误 + + + 分配内存错误 + + + 线程绑核失败 + + + 噪声特性图像格式错误 + + + 噪声特性类型错误 + + + 噪声特性个数错误 + + + 噪声特性增益个数错误 + + + 噪声曲线增益值输入错误 + + + 噪声曲线柱数错误 + + + 噪声估计初始化增益设置错误 + + + 噪声估计未初始化 + + + 颜色空间模式错误 + + + 图像ROI个数错误 + + + 图像ROI原点错误 + + + 图像ROI大小错误 + + + 输入的相机增益不存在(增益个数已达上限) + + + 输入的相机增益不在范围内 + + + 输入的噪声特性内存大小错误 + + + + 设备event信息 + + + + + Event名称 + + + + + EventID + + + + + 流通道序号 + + + + + 帧号 (暂无固件支持) + + + + + 时间戳 + + + + + Event数据长度 (暂无固件支持) + + + + + Event数据,内部会进行一次拷贝,将非托管内存拷贝到托管内存 (暂无固件支持) + + + + + 设备事件 + + + + + 事件信息 + + + + + 提供设备事件订阅相关接口 + + + + + 订阅事件 + + 事件名称 + 成功,返回MV_OK;失败,返回错误码 + + + + 取消事件订阅 + + 事件名称 + 成功,返回MV_OK;失败,返回错误码 + + + + 订阅所有事件 + + 成功,返回MV_OK;失败,返回错误码 + + + + 取消订阅所有事件 + + 成功,返回MV_OK;失败,返回错误码 + + + + 设备事件 + + + + + 通过GenTL枚举到的接口信息 + + + + + GenTL接口ID + + + + + 传输层类型 + + + + + 显示名称 + + + + + GenTL的cti文件索引 + + + + + 通过GenTL枚举到的设备信息 + + + + + GenTL接口ID + + + + + 设备ID + + + + + 供应商名字 + + + + + 型号名字 + + + + + 传输层类型 + + + + + 设备显示名称 + + + + + 用户自定义名字 + + + + + 序列号 + + + + + 设备版本号 + + + + + GenTL的cti文件索引 + + + + + 提供GenTL相关接口 + + + + + 通过GenTL枚举Interfaces + + GenTL的cti文件路径 + Interfaces列表 + 成功,返回MV_OK;失败,返回错误码 + + + + 通过GenTL Interface枚举设备 + + Interface信息 + 设备列表 + 成功,返回MV_OK;失败,返回错误码 + + 枚举到设备后,通过创建设备实例。 + + + + + 卸载cti库 + + 枚举卡时加载的cti文件路径 + 成功,返回MV_OK;失败,返回错误码 + 卸载前需要保证通过该cti枚举出的相机已全部关闭,否则报错前置条件错误。 + + + + 提供获取图像缓存、图像属性的接口 + + + + + 转换为Bitmap + + Bitmap对象,失败返回null + + + + 图像数据指针(非托管内存) + + + + + 图像数据,内部会进行一次拷贝,将非托管内存拷贝到托管内存 + + + + + 图像宽度 + + + + + 图像高度 + + + + + 像素格式 + + + + + 图像大小 + + + + + 图像解码 + + + + + 无损解码 + + 输入图像及帧信息 + 输出图像及帧信息。图像使用完之后需调用Dispose方法及时释放内存,防止内存快速上涨。 + 成功,返回MV_OK;失败,返回错误码 + + 将从相机中取到的无损压缩码流解码成裸数据,同时支持解析当前相机实时图像的水印信息(如果输入的无损码流不是当前相机或者不是实时取流的,则水印解析可能异常)。 + 若解码失败,请检查以下情况:(1)需要CPU支持 SSE AVX指令集(2)若当前帧异常(丢包等),可能导致解码异常(3)相机出图异常,即使不丢包也会异常。 + + + + + 图像渲染模式 + + + + + 默认模式,Windows:GDI,Linux:OpenGL + + + + + Direct3D,只支持Windows + + + + + OPENGL,只支持Windows + + + + + 颜色 + + + + + 构造函数 + + 红色,根据像素颜色的相对深度,范围为[0.0 , 1.0],代表着[0, 255]的颜色深度 + 绿色,根据像素颜色的相对深度,范围为[0.0 , 1.0],代表着[0, 255]的颜色深度 + 蓝色,根据像素颜色的相对深度,范围为[0.0 , 1.0],代表着[0, 255]的颜色深度 + 透明度,根据像素颜色的相对透明度,范围为[0.0 , 1.0] (此参数功能暂不支持) + + + + 红色,根据像素颜色的相对深度,范围为[0.0 , 1.0],代表着[0, 255]的颜色深度 + + + + + 绿色,根据像素颜色的相对深度,范围为[0.0 , 1.0],代表着[0, 255]的颜色深度 + + + + + 蓝色,根据像素颜色的相对深度,范围为[0.0 , 1.0],代表着[0, 255]的颜色深度 + + + + + 透明度,根据像素颜色的相对透明度,范围为[0.0 , 1.0] (此参数功能暂不支持) + + + + + 矩形 + + + + + 构造函数 + + 矩形上边缘距离图像上边缘的距离,根据图像的相对位置,范围为[0.0 , 1.0] + 矩形下边缘距离图像下边缘的距离,根据图像的相对位置,范围为[0.0 , 1.0] + 矩形左边缘距离图像左边缘的距离,根据图像的相对位置,范围为[0.0 , 1.0] + 矩形右边缘距离图像右边缘的距离,根据图像的相对位置,范围为[0.0 , 1.0] + + + + 矩形上边缘距离图像上边缘的距离,根据图像的相对位置,范围为[0.0 , 1.0] + + + + + 矩形下边缘距离图像下边缘的距离,根据图像的相对位置,范围为[0.0 , 1.0] + + + + + 矩形左边缘距离图像左边缘的距离,根据图像的相对位置,范围为[0.0 , 1.0] + + + + + 矩形右边缘距离图像右边缘的距离,根据图像的相对位置,范围为[0.0 , 1.0] + + + + + 点 + + + + + 构造函数 + + 该点距离图像左边缘距离,根据图像的相对位置,范围为[0.0 , 1.0] + 该点距离图像上边缘距离,根据图像的相对位置,范围为[0.0 , 1.0] + + + + 该点距离图像左边缘距离,根据图像的相对位置,范围为[0.0 , 1.0] + + + + + 该点距离图像上边缘距离,根据图像的相对位置,范围为[0.0 , 1.0] + + + + + 圆形 + + + + + 构造函数 + + 圆心 + 宽向半径,根据图像的相对位置[0, 1.0],半径与圆心的位置有关,需保证画出的圆在显示框范围之内,否则报错 + 高向半径,根据图像的相对位置[0, 1.0],半径与圆心的位置有关,需保证画出的圆在显示框范围之内,否则报错 + + + + 圆心 + + + + + 宽向半径,根据图像的相对位置[0, 1.0],半径与圆心的位置有关,需保证画出的圆在显示框范围之内,否则报错 + + + + + 高向半径,根据图像的相对位置[0, 1.0],半径与圆心的位置有关,需保证画出的圆在显示框范围之内,否则报错 + + + + + 线条 + + + + + 线条的起始点坐标 + + + + + 线条的终点坐标 + + + + + 提供图像渲染、图形绘制接口 + + + + + 显示一帧图像 + + 窗口句柄 + 图像信息 + 渲染模式 + 成功,返回MV_OK;失败,返回错误码 + + 渲染方式可选择GDI或D3D,默认选择为GDI模式。渲染引擎仅在客户端不连接相机的情况下可以进行设置。GDI模式对电脑的显卡性能没有要求,适用于所有电脑。 D3D模式适用于安装显卡驱动且显卡内存大于1GB的电脑,该模式下客户端预览的图像效果会优于GDI模式下的图像效果。 + 渲染模式为RenderMode.OPENGL时支持4G以上超大图渲染 + + + + + 显示一帧图像 + + 窗口句柄 + 图像数据指针 + 图像数据长度 + 图像宽 + 图像高 + 像素格式 + 渲染模式 + 成功,返回MV_OK;失败,返回错误码 + + 渲染方式可选择GDI或D3D,默认选择为GDI模式。渲染引擎仅在客户端不连接相机的情况下可以进行设置。GDI模式对电脑的显卡性能没有要求,适用于所有电脑。 D3D模式适用于安装显卡驱动且显卡内存大于1GB的电脑,该模式下客户端预览的图像效果会优于GDI模式下的图像效果。 + + + + + 在图像上绘制矩形 + + 矩形框 + 线条颜色 + 线条宽度,只能是1或2 + 成功,返回MV_OK;失败,返回错误码 + + + + 在图像上绘制圆形 + + 圆形信息 + 线条颜色 + 线条宽度,只能是1或2 + 成功,返回MV_OK;失败,返回错误码 + + + + 在图像上绘制线条 + + 线条信息 + 线条颜色 + 线条宽度,只能是1或2 + 成功,返回MV_OK;失败,返回错误码 + + + + 图像格式 + + + + + BMP图像格式 + + + + + JPEG图像格式 + + + + + PNG图像格式 + + + + + TIFF图像格式 + + + + + 图像格式信息 + + + + + 图像格式 + + + + + JPEG编码质量(50-99],其他格式无效 + + + + + 提供保存图像数据到文件的接口,支持BMP、JPG、PNG、TIFF格式图像 + + + + + 保存图像到文件,支持BMP、JPG、PNG、TIFF格式图像 + + 文件路径 + 图像数据 + 图像格式信息 + 图像插值方法 + 成功,返回MV_OK;失败,返回错误码 + + + + 保存图像到缓存,支持BMP、JPG格式图像 + + 图像缓存 + 转换后的图像数据长度 + 图像数据 + 图像格式信息 + 图像插值方法 + 成功,返回MV_OK;失败,返回错误码 + + + + 提供采集卡属性和接口 + + + + + 打开采集卡 + + 成功,返回MV_OK;失败,返回错误码 + + + + 关闭采集卡 + + 成功,返回MV_OK;失败,返回错误码 + + + + 设备本地升级 + + 升级文件路径 + 成功,返回MV_OK;失败,返回错误码 + + 通过该接口可以将升级固件文件发送给设备进行升级。该接口需要等待升级固件文件成功传给设备端之后再返回,响应时间可能较长。 + + + + + 获取升级进度 + + 升级进度 + 成功,返回MV_OK;失败,返回错误码 + + + + 枚举采集卡上的相机 + + 相机列表 + 成功,返回MV_OK;失败,返回错误码 + + + + 开启设备指定事件 + + 事件名称 + 成功,返回MV_OK;失败,返回错误码 + + + + 关闭设备指定事件 + + 事件名称 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取采集卡对应的参数配置对象 + + + + + 获取采集卡对应的事件采集对象 + + + + + 提供枚举采集卡接口 + + + + + 枚举采集卡 + + 采集卡接口类型 + 采集卡列表 + 成功,返回MV_OK;失败,返回错误码 + + 枚举到采集卡后,通过创建采集卡实例。 + + + + + 采集卡工厂类,创建采集卡实例 + + + + + 根据采集卡信息创建采集卡实例 + + 采集卡信息 + 成功-返回采集卡实例,失败-抛出异常 + + + + 根据采集卡ID创建采集卡实例 + + 采集卡ID + 成功-返回采集卡实例,失败-抛出异常 + + + + 采集卡接口类型定义 + + + + + GigE Vision采集卡 + + + + + Camera Link采集卡 + + + + + CoaXPress采集卡 + + + + + XoFLink采集卡 + + + + + 采集卡信息 + + + + + 采集卡接口类型, + + + + + 采集卡的PCIE插槽信息 + + + + + 采集卡ID + + + + + 显示名称 + + + + + 序列号 + + + + + 型号 + + + + + 厂商 + + + + + 版本号 + + + + + 自定义名称 + + + + + 图像旋转角度 + + + + + 90度 + + + + + 180度 + + + + + 270度 + + + + + 图像翻转类型 + + + + + 垂直翻转 + + + + + 水平翻转 + + + + + 图像重构方式 + + + + + 源图像按行拆分成多张图像 + + + + + 图像拼接方式 + + + + + 垂直方向拼接 + + + + + 提供图像处理相关接口,比如旋转、翻转、对比度、饱和度等 + + + + + 图像旋转 + + 输入图像 + 输出图像。图像使用完之后需调用Dispose方法及时释放内存,防止内存快速上涨。 + 旋转角度 + 成功,返回MV_OK;失败,返回错误码 + + 该接口只支持MONO8/RGB24/BGR24格式数据的90/180/270度旋转。 + + + + + 图像翻转 + + 输入图像 + 输出图像。图像使用完之后需调用Dispose方法及时释放内存,防止内存快速上涨。 + 翻转类型 + 成功,返回MV_OK;失败,返回错误码 + + 该接口只支持MONO8/RGB24/BGR24格式数据的垂直和水平翻转。 + + + + + 图像对比度调节 + + 输入图像 + 输出图像。图像使用完之后需调用Dispose方法及时释放内存,防止内存快速上涨。 + 对比度值,[1, 10000] + 成功,返回MV_OK;失败,返回错误码 + + + + 重构图像(用于分时曝光功能) + + 输入图像 + 曝光个数(1-8] + 图像重构方式 + 输出图像列表。图像使用完之后需调用Dispose方法及时释放内存,防止内存快速上涨。 + 成功,返回MV_OK;失败,返回错误码 + + + + 重构图像(用于分时曝光功能,图像拆分后再拼接) + + 输入图像 + 曝光个数(1-8] + 图像重构的方式 + 图像拼接的方式 + 输出图像。图像使用完之后需调用Dispose方法及时释放内存,防止内存快速上涨。 + 成功,返回MV_OK;失败,返回错误码 + + + + 内存块 + + + + + 内存池 + + + + + 关闭内存池,清空内部缓存 + + + + + 归还内存块到内存池 + + 内存块 + 是否手动释放 + + + + 更新内存块列表,释放过期内存。方法内部不加锁 + + + + + + 更新内存块的空闲时间并删除过期内存块。 方法内部不加锁 + + + + + + + 打印统计信息 + + + + + 初始化属性值,在构造函数中使用 + + + + + 创建相机句柄 + + + 错误码 + + + + 判断设备是否处于连接状态 + + + + + 获取设备对应的图像采集对象 + + + + + 获取设备信息 + + + + + 获取设备对应的参数配置对象 + + + + + 设置设备波特率 + + 波特率 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取设备波特率 + + 波特率 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取设备与主机间连接支持的波特率 + + 所支持波特率的或运算结果,单个波特率参考 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置串口操作等待时长 + + 串口操作的等待时长,单位为ms + 成功,返回MV_OK;失败,返回错误码 + + + + Chunk数据 + + + + + Chunk数据指针(非托管内存) + + + + + Chunk数据,内部会进行一次拷贝,将非托管内存拷贝到托管内存 + + + + + ChunkID + + + + + Chunk数据长度 + + + + + 图像帧里面的chunk信息 + + + + + 通过ChunkID获取对应的ChunkData + + + + + + + 创建设备对象 + + + + + 创建设备对象 + + 设备信息 + 设备实例 + + + + 通过设备IP地址创建设备,适用于GigE设备 + + 设备IP地址 + 网口IP地址 + 设备实例 + + + + 通过GenTL设备信息创建设备句柄 + + 设备信息 + 成功,返回MV_OK;失败,返回错误码 + + + + 采集卡类型 + + + + + 未知采集卡 + + + + + 虚拟采集卡 + + + + + 自研采集卡 + + + + + gige设备信息实现 + + + + + 占用设备的主机ip + + + + + 组播ip + + + + + 组播port + + + + + 设备接口类型 + + + + + 制造商信息 + + + + + 制造商特殊信息 + + + + + 设备型号 + + + + + 设备版本 + + + + + 设备序列号 + + + + + 用户自定义名称 + + + + + 设备类型信息,7 - 0 bit: 预留,15 - 8 bit:产品子类别,23 - 16 bit:产品类型,31 - 24bit:产品线(如: 0x01 标准产品;0x02 3D产品;0x03 智能ID产品) + + + + + GigE Vision协议主要版本 + + + + + GigE Vision协议次要版本 + + + + + 高MAC地址 + + + + + 低MAC地址 + + + + + IP配置选项 + + + + + 当前IP配置 + + + + + 当前IP地址 + + + + + 当前子网掩码 + + + + + 当前网关 + + + + + 网口IP地址 + + + + + 是否虚拟相机 + + + + + 是否采集卡上的相机 + + + + + USB设备信息类 + + + + + 设备接口类型 + + + + + 制造商信息 + + + + + 设备型号 + + + + + 设备版本 + + + + + 设备序列号 + + + + + 用户自定义名称 + + + + + 设备类型信息,7 - 0 bit: 预留,15 - 8 bit:产品子类别,23 - 16 bit:产品类型,31 - 24bit:产品线(如: 0x01 标准产品;0x02 3D产品;0x03 智能ID产品) + + + + + 控制输入端点 + + + + + 控制输出端点 + + + + + 流端点 + + + + + 事件端点 + + + + + 供应商ID号 + + + + + 产品ID号 + + + + + 设备索引号 + + + + + 设备GUID号 + + + + + 家族名字 + + + + + 供应商名称 + + + + + 支持的USB协议 + + + + + 设备地址 + + + + + 是否虚拟相机 + + + + + Camera Link串口设备信息 + + + + + 设备接口类型 + + + + + 制造商信息 + + + + + 设备型号 + + + + + 设备版本 + + + + + 设备序列号 + + + + + 用户自定义名称 + + + + + 设备类型信息,7 - 0 bit: 预留,15 - 8 bit:产品子类别,23 - 16 bit:产品类型,31 - 24bit:产品线(如: 0x01 标准产品;0x02 3D产品;0x03 智能ID产品) + + + + + 端口号 + + + + + 名称 + + + + + CoaXPress设备信息 + + + + + 设备接口类型 + + + + + 制造商信息 + + + + + 设备型号 + + + + + 设备版本 + + + + + 设备序列号 + + + + + 用户自定义名称 + + + + + 设备类型信息,7 - 0 bit: 预留,15 - 8 bit:产品子类别,23 - 16 bit:产品类型,31 - 24bit:产品线(如: 0x01 标准产品;0x02 3D产品;0x03 智能ID产品) + + + + + 相机ID + + + + + 采集卡ID + + + + + Camera Link设备信息 + + + + + 设备接口类型 + + + + + 制造商信息 + + + + + 设备型号 + + + + + 设备版本 + + + + + 设备序列号 + + + + + 用户自定义名称 + + + + + 设备类型信息,7 - 0 bit: 预留,15 - 8 bit:产品子类别,23 - 16 bit:产品类型,31 - 24bit:产品线(如: 0x01 标准产品;0x02 3D产品;0x03 智能ID产品) + + + + + 相机ID + + + + + 采集卡ID + + + + + XoFLink设备信息 + + + + + 设备接口类型 + + + + + 制造商信息 + + + + + 设备型号 + + + + + 设备版本 + + + + + 设备序列号 + + + + + 用户自定义名称 + + + + + 设备类型信息,7 - 0 bit: 预留,15 - 8 bit:产品子类别,23 - 16 bit:产品类型,31 - 24bit:产品线(如: 0x01 标准产品;0x02 3D产品;0x03 智能ID产品) + + + + + 相机ID + + + + + 采集卡ID + + + + + Event名称 + + + + + EventID + + + + + 流通道序号 + + + + + 帧号 + + + + + 时间戳 + + + + + Event数据长度 + + + + + Event数据 + + + + + GenTL接口ID + + + + + 传输层类型 + + + + + 显示名称 + + + + + GenTL的cti文件索引 + + + + + GenTL接口ID + + + + + 设备ID + + + + + 供应商名字 + + + + + 型号名字 + + + + + 传输层类型 + + + + + 设备显示名称 + + + + + 用户自定义名字 + + + + + 序列号 + + + + + 设备版本号 + + + + + GenTL的cti文件索引 + + + + + 获取最佳包大小 + + + + + + + 强制配置ip + + + + + + + + + 设置ip方式 + + + + + + + 设置传输模式,默认为Driver模式 + + 网络传输模式 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取网络传输信息 + + 网络传输信息 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置GVSP取流超时时间 + + 超时时间(MS),默认300ms,范围:>10ms + 成功,返回MV_OK;失败,返回错误码 + + + + 获取GVSP取流超时时间 + + 超时时间(MS) + 成功,返回MV_OK;失败,返回错误码 + + + + 设置GVCP命令超时时间 + + 超时时间(MS),默认500ms,范围:0-10000ms + 成功,返回MV_OK;失败,返回错误码 + + + + 获取GVCP命令超时时间 + + 超时时间(MS) + 成功,返回MV_OK;失败,返回错误码 + + + + 设置重传GVCP命令次数 + + 重传次数,范围:0-100 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取重传GVCP命令次数 + + 重传次数 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置是否打开重发包,及重发包参数 + + 是否支持重发包 + 最大重发比 + 重发超时时间 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置重传命令最大尝试次数 + + 重传命令最大尝试次数,默认值20 + 成功,返回MV_OK;失败,返回错误码 + + 该接口必须在调用开启重传包功能之后调用,否则失败且返回MV_E_CALLORDER。 + + + + + 获取重传命令最大尝试次数 + + 传命令最大尝试次数 + 成功,返回MV_OK;失败,返回错误码 + + 该接口必须在调用开启重传包功能之后调用,否则失败且返回MV_E_CALLORDER。 + + + + + 设置同一重传包多次请求之间的时间间隔 + + 同一重传包多次请求之间的时间间隔,默认10ms + 成功,返回MV_OK;失败,返回错误码 + + + + 获取同一重传包多次请求之间的时间间隔 + + + 成功,返回MV_OK;失败,返回错误码 + + + + 设置传输模式,可以为单播模式、组播模式等 + + + 组播地址,组播模式下有意义 + 组播端口,组播模式下有意义 + 成功,返回MV_OK;失败,返回错误码 + + + + 解码功能实现类 + + + + + 判断像素是否为Mono格式 + + + + + + + 判断图像格式是否为彩色格式 + + + + + + + 获取图像大小 + + 图像宽度 + 图像高度 + 像素格式 + + + + + 内存拷贝 + + 目标缓存 + 源缓存 + 拷贝大小 + + + + 将FrameOut转成MvCCDll的帧结构体 + + + + + + + + 将Byte数组转为String,使用UTF-8编码,并去掉结尾的'\0' + + + + + + + String字符串拷贝到byte[] + + + + + + + 判断字符数组是否为utf-8 + + 字符数组 + + + + + 将枚举出来的设备信息转化为输出格式 + + + + + + + + 打开采集卡 + + 成功,返回MV_OK;失败,返回错误码 + + + + 关闭采集卡 + + 成功,返回MV_OK;失败,返回错误码 + + + + 设备本地升级 + + 升级文件路径 + 成功,返回MV_OK;失败,返回错误码 + + 通过该接口可以将升级固件文件发送给设备进行升级。该接口需要等待升级固件文件成功传给设备端之后再返回,响应时间可能较长。 + + + + + 获取升级进度 + + 升级进度 + 成功,返回MV_OK;失败,返回错误码 + + + + 枚举采集卡上的相机 + + 相机列表 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取采集卡对应的参数配置对象 + + + + + 采集卡接口类型, + + + + + 采集卡的PCIE插槽信息 + + + + + 采集卡ID + + + + + 显示名称 + + + + + 序列号 + + + + + 型号 + + + + + 厂商 + + + + + 版本号 + + + + + 自定义名称 + + + + + MyCamera + + + + ch GigE Vision采集卡 |en GigE Vision interface + + + ch Camera Link采集卡 |en Camera Link interface + + + ch CoaXPress采集卡 |en CoaXPress interface + + + ch XoFLink采集卡 |en XoFLink interface + + + Unknown Device Type, Reserved + + + GigE Device + + + 1394-a/b Device + + + USB3.0 Device + + + CameraLink Device + + + Virtual GigE Device + + + Virtual USB Device + + + GenTL GigE Device + + + GenTL CML Device + + + GenTL CXP Device + + + GenTL XOF Device + + + + ch:信息结构体的最大缓存 | en: Max buffer size of information structs + + + + + 最大的相机数量 + + + + + ch:最大Interface数量 | en:Max num of interfaces + + + + + ch:最大GenTL设备数量 | en:Max num of GenTL devices + + + + + XML节点描述最大长度 + + + + + XML节点最大长度 + + + + + XML节点最大数量 + + + + + XML节点显示名最大数量 + + + + + 枚举类型最大的EnumEntry个数 + + + + + string类型节点值的最大长度 + + + + + 最大父节点数 + + + + + 最大节点描述长度 + + + + + 参数导出允许配置的最大节点个数 + + + + + 参数导入时节点导入失败的最大错误个数 + + + + + 设备断开连接 + + + + + SDK与驱动版本不匹配 + + + + + 相机Event事件名称最大长度 + + + + 最大枚举条目对应的符号长度 + + + 分时曝光时最多将源图像拆分的个数 + + + 最大支持的串口数量 + + + + ch:最大支持的采集卡数量 | en:The maximum number of Frame Grabber interface supported + + + + + ch 静态 |en Static + + + + + ch DHCP |en DHCP + + + + + ch LLA |en LLA + + + + + 9600 + + + + + 19200 + + + + + 38400 + + + + + 57600 + + + + + 115200 + + + + + 230400 + + + + + 460800 + + + + + 921600 + + + + + ch 最大值 |en Auto Max + + + + + ch 网络流量和丢包信息 |en Network traffic and packet loss information + + + + + ch host接收到来自U3V设备的字节总数 |en The total number of bytes host received from U3V device + + + + + ch独占权限,其他APP只允许读CCP寄存器 |en Exclusive authority, other APP is only allowed to read the CCP register + + + + + ch 可以从5模式下抢占权限,然后以独占权限打开 |en You can seize the authority from the 5 mode, and then open with exclusive authority + + + + + ch 控制权限,其他APP允许读所有寄存器 |en Control authority, allows other APP reading all registers + + + + + ch 可以从5的模式下抢占权限,然后以控制权限打开 |en You can seize the authority from the 5 mode, and then open with control authority + + + + + ch 以可被抢占的控制权限打开 |en Open with seized control authority + + + + + ch 可以从5的模式下抢占权限,然后以可被抢占的控制权限打开 |en You can seize the authority from the 5 mode, and then open with seized control authority + + + + + ch 读模式打开设备,适用于控制权限下 |en Open with read mode and is available under control authority + + + + 成功,无错误 + + + 错误或无效的句柄 + + + 不支持的功能 + + + 缓存已满 + + + 函数调用顺序错误 + + + 错误的参数 + + + 资源申请失败 + + + 无数据 + + + 前置条件有误,或运行环境已发生变化 + + + 版本不匹配 + + + 传入的内存空间不足 + + + 异常图像,可能是丢包导致图像不完整 + + + 动态导入DLL失败 + + + 没有可输出的缓存 + + + 加密错误 + + + 打开文件出现错误 + + + 未知的错误 + + + 通用错误 + + + 参数非法 + + + 值超出范围 + + + 属性 + + + 运行环境有问题 + + + 逻辑错误 + + + 节点访问条件有误 + + + 超时 + + + 转换异常 + + + GenICam未知错误 + + + 命令不被设备支持 + + + 访问的目标地址不存在 + + + 目标地址不可写 + + + 设备无访问权限 + + + 设备忙,或网络断开 + + + 网络包数据错误 + + + 网络相关错误 + + + 设备IP冲突 + + + 读usb出错 + + + 写usb出错 + + + 设备异常 + + + GenICam相关错误 + + + 带宽不足 + + + 驱动不匹配或者未装驱动 + + + USB未知的错误 + + + 升级固件不匹配 + + + 升级固件语言不匹配 + + + 升级冲突(设备已经在升级了再次请求升级即返回此错误) + + + 升级时设备内部出现错误 + + + 升级时未知错误 + + + 处理正确 + + + 不确定类型错误 + + + 能力集中存在无效参数 + + + 内存地址为空 + + + 内存对齐不满足要求 + + + 内存空间大小不够 + + + 内存空间大小不满足对齐要求 + + + 内存地址不满足对齐要求 + + + 图像格式不正确或者不支持 + + + 图像宽高不正确或者超出范围 + + + 图像宽高与step参数不匹配 + + + 图像数据存储地址为空 + + + 设置或者获取参数类型不正确 + + + 设置或者获取参数的输入、输出结构体大小不正确 + + + 处理类型不正确 + + + 处理时输入、输出参数大小不正确 + + + 子处理类型不正确 + + + 子处理时输入、输出参数大小不正确 + + + index参数不正确 + + + value参数不正确或者超出范围 + + + param_num参数不正确 + + + 函数参数指针为空 + + + 超过限定的最大内存 + + + 回调函数出错 + + + 加密错误 + + + 算法库使用期限错误 + + + 参数范围不正确 + + + 数据大小不正确 + + + 数据step不正确 + + + cpu不支持优化代码中的指令集 + + + 警告 + + + 算法库超时 + + + 算法版本号出错 + + + 模型版本号出错 + + + GPU内存分配错误 + + + 文件不存在 + + + 字符串为空 + + + 图像解码器错误 + + + 打开文件错误 + + + 文件读取错误 + + + 文件写错误 + + + 文件读取大小错误 + + + 文件类型错误 + + + 模型类型错误 + + + 分配内存错误 + + + 线程绑核失败 + + + 噪声特性图像格式错误 + + + 噪声特性类型错误 + + + 噪声特性个数错误 + + + 噪声特性增益个数错误 + + + 噪声曲线增益值输入错误 + + + 噪声曲线柱数错误 + + + 噪声估计初始化增益设置错误 + + + 噪声估计未初始化 + + + 颜色空间模式错误 + + + 图像ROI个数错误 + + + 图像ROI原点错误 + + + 图像ROI大小错误 + + + 输入的相机增益不存在(增益个数已达上限) + + + 输入的相机增益不在范围内 + + + 输入的噪声特性内存大小错误 + + + + Byte array to struct + + Byte array + Struct type + Struct object + + + + Struct to Byte array + + Struct object + Byte + Bytes + + + + 判断字符数组是否为utf-8 + + 字符数组 + + + + + Write Error Message + + Message + ErrorNum + + + + 获取枚举类型节点(支持的枚举个数扩展到256) V4.4.1新增 + + + + + Grab callback + + Image data + Frame info + User defined variable + + + + Grab callback + + Image data + Frame info + User defined variable + + + + Xml Update callback(Interfaces not recommended) + + Node type + Current node feature structure + Nodes list + User defined variable + + + + Exception callback + + Msg type + User defined variable + + + + Event callback (Interfaces not recommended) + + User defined ID + User defined variable + + + + Event callback + + Event Info + User defined variable + + + + Stream Exception callback + + Msg type + User defined variable + + + + ch:采集卡信息列表 | en: Interface Information List + + + + + ch:在线设备数量 | en:Online Interface Number + + + + + ch:支持最多64个设备 | en:Support up to 64 Interfaces + + + + + ch:采集卡信息 | en: Interface information + + + + + ch: 采集卡类型; 低16位有效: bits(0~2)代表功能, bits(3~7)代表相机, bits(8-15)代表总线| en: Interface type + + + + + ch: 采集卡的PCIE插槽信息 | en: PCIe slot information of interface + + + + + ch: 采集卡ID | en: Interface ID + + + + + ch 显示名称 | en: Display name + + + + + ch 序列号 |en: Serial number + + + + + ch 型号 | en: model name + + + + + ch: 厂商 |en: manufacturer name + + + + + ch: 版本号| en: device version + + + + + ch: 自定义名称 |en: user defined name + + + + + ch 保留字段 | en Reserved + + + + + 排序方式 + + + + + 按序列号排序 + + + + + 按用户自定义名字排序 + + + + + 按当前IP地址排序(升序) + + + + + 按当前IP地址排序(降序) + + + + + ch: GigE设备信息 | en: GigE device information + + + + + IP 配置选项 + + + + + IP configuration:bit31-static bit30-dhcp bit29-lla + + + + + curtent ip + + + + + curtent subnet mask + + + + + current gateway + + + + + 制造商名 + + + + + 型号名 + + + + + 设备版本信息 + + + + + 制造商特殊信息 + + + + + 序列号 + + + + + 用户自定义名 + + + + + 网口IP地址 + + + + + 预留 + + + + + ch: GigE设备信息 | en: GigE device information + + + + + IP 配置选项 + + + + + IP configuration:bit31-static bit30-dhcp bit29-lla + + + + + curtent ip + + + + + curtent subnet mask + + + + + current gateway + + + + + 制造商名 + + + + + 型号名 + + + + + 设备版本信息 + + + + + 制造商特殊信息 + + + + + 序列号 + + + + + 用户自定义名 + + + + + 网口IP地址 + + + + + 预留 + + + + + ch:USB3 设备信息 | en:USB3 device information + + + + + 控制输入端点 + + + + + 控制输出端点 + + + + + 流端点 + + + + + 事件端点 + + + + + 供应商ID号 + + + + + 产品ID号 + + + + + 设备索引号 + + + + + 设备GUID号 + + + + + 供应商名字 + + + + + 型号名字 + + + + + 家族名字 + + + + + 设备版本号 + + + + + 制造商名字 + + + + + 序列号 + + + + + 用户自定义名字 + + + + + 支持的USB协议 + + + + + 设备地址 + + + + + 保留字节 + + + + + ch:USB3 设备信息 | en:USB3 device information + + + + + 控制输入端点 + + + + + 控制输出端点 + + + + + 流端点 + + + + + 事件端点 + + + + + 供应商ID号 + + + + + 产品ID号 + + + + + 设备索引号 + + + + + 设备GUID号 + + + + + 供应商名字 + + + + + 型号名字 + + + + + 家族名字 + + + + + 设备版本号 + + + + + 制造商名字 + + + + + 序列号 + + + + + 用户自定义名字 + + + + + 支持的USB协议 + + + + + 设备地址 + + + + + 保留字节 + + + + + ch:CamLink设备信息 | en:CamLink device information + + + + + 端口号ID + + + + + 模型名 + + + + + 家族名 + + + + + 设备版本信息 + + + + + 制造商名字 + + + + + 序列号 + + + + + 保留字节 + + + + + ch:采集卡Camera Link相机信息 | en:Camera Link device information on frame grabber + + + + + ch 采集卡ID |en Interface ID of Frame Grabber + + + + + ch 供应商名字 |en Vendor name + + + + + ch 型号名字 |en Model name + + + + + ch 厂商信息 |en Manufacturer information + + + + + ch 相机版本 |en Device version + + + + + ch 序列号 |en Serial number + + + + + ch 用户自定义名字 |en User defined name + + + + + ch 相机ID |en Device ID + + + + + ch 保留字段 |en Reserved + + + + + ch:CoaXPress相机信息 | en:CoaXPress device information + + + + + ch 采集卡ID |en Interface ID of Frame Grabber + + + + + ch 供应商名字 |en Vendor name + + + + + ch 型号名字 |en Model name + + + + + ch 厂商信息 |en Manufacturer information + + + + + ch 相机版本 |en Device version + + + + + ch 序列号 |en Serial number + + + + + ch 用户自定义名字 |en User defined name + + + + + ch 相机ID |en Device ID + + + + + ch 保留字段 |en Reserved + + + + + ch:XoFLink相机信息 | en:XoFLink device information + + + + + ch 采集卡ID |en Interface ID of Frame Grabber + + + + + ch 供应商名字 |en Vendor name + + + + + ch 型号名字 |en Model name + + + + + ch 厂商信息 |en Manufacturer information + + + + + ch 相机版本 |en Device version + + + + + ch 序列号 |en Serial number + + + + + ch 用户自定义名字 |en User defined name + + + + + ch 相机ID |en Device ID + + + + + ch 保留字段 |en Reserved + + + + + ch:设备信息 | en:Device information + + + + + 主版本号 + + + + + 次版本号 + + + + + MAC高地址 + + + + + MAC低地址 + + + + + 设备传输层协议类型,e.g. MV_GIGE_DEVICE + + + + + ch 设备类型信息 | en Device Type Info + + + + + 保留字节 + + + + + 设备类型 + + + + + 构造函数 + + 输入任意数,因为不接受无参构造函数 + + + + ch:特定类型的设备信息 | en:Special devcie information + + + + + GigE + + + + + Camera Link + + + + + Usb + + + + + CML + + + + + CXP + + + + + XOF + + + + + 相机列表 + + + + + 在线设备数量 + + + + + 支持最多256个设备 + + + + + ch:通过GenTL枚举到的Interface信息 | en:Interface Information with GenTL + + + + + GenTL接口ID + + + + + 传输层类型 + + + + + 设备显示名称 + + + + + GenTL的cti文件索引 + + + + + 保留字节 + + + + + ch:通过GenTL枚举到的设备信息列表 | en:Interface Information List with GenTL + + + + + ch:在线设备数量 | en:Online Interface Number + + + + + ch:支持最多256个设备 | en:Support up to 256 Interfaces + + + + + ch:通过GenTL枚举到的设备信息 | en:Device Information discovered by with GenTL + + + + + 采集卡ID + + + + + 设备ID + + + + + 供应商名字 + + + + + 模型名 + + + + + 传输类型 + + + + + 显示名 + + + + + 用户自定义名 + + + + + 序列号 + + + + + 设备版本信息 + + + + + cti文件序号 + + + + + 保留字节 + + + + + ch:通过GenTL枚举到的设备信息 | en:Device Information discovered by with GenTL + + + + + 采集卡ID + + + + + 设备ID + + + + + 供应商名字 + + + + + 模型名 + + + + + 传输类型 + + + + + 显示名 + + + + + 用户自定义名 + + + + + 序列号 + + + + + 设备版本信息 + + + + + cti文件序号 + + + + + 保留字节 + + + + + ch:GenTL设备列表 | en:GenTL devices list + + + + + 在线设备数量 + + + + + 支持最多256个设备 + + + + + Net Trans Info + + + + + 已接收数据大小 [统计StartGrabbing和StopGrabbing之间的数据量] + + + + + 丢帧数量 + + + + + 接收帧数 + + + + + 请求重发包数 + + + + + 重发包数 + + + + + Frame Out Info + + + + + 图像宽 + + + + + 图像高 + + + + + 像素格式 + + + + + 帧号 + + + + + 时间戳高32位 + + + + + 时间戳低32位 + + + + + 保留,8字节对齐 + + + + + 主机生成的时间戳 + + + + + 帧数据大小 + + + + + 丢包数量 + + + + + 保留字节 + + + + + Chunk数据信息 + + + + + Chunk数据 + + + + + ChunkID + + + + + Chunk大小 + + + + + 保留字节 + + + + + Frame Out Info Ex + + + + + 图像宽 + + + + + 图像高 + + + + + 像素格式 + + + + + 帧号 + + + + + 时间戳高32位 + + + + + 时间戳低32位 + + + + + 保留,8字节对齐 + + + + + 主机生成的时间戳 + + + + + Frame大小 + + + + + 秒数 + + + + + 周期数 + + + + + 周期偏移量 + + + + + 增益 + + + + + 曝光时间 + + + + + 平均亮度 + + + + + Red + + + + + Green + + + + + Blue + + + + + 帧计数器 + + + + + 触发计数 + + + + + 输入 + + + + + 输出 + + + + + 水平偏移量 + + + + + 垂直偏移量 + + + + + Chunk宽度 + + + + + Chunk高度 + + + + + 丢包数 + + + + + 为解析的Chunk数量 + + + + + 为解析的Chunk列表 + + + + + 图像宽扩展 + + + + + 图像高扩展 + + + + + 帧长度扩展 + + + + + 保留字节 + + + + + 为解析的Chunk列表 + + + + + 为解析的Chunk内容 + + + + + 对齐结构体,无实际用途 + + + + + 输出帧信息 + + + + + 帧数据地址 + + + + + 帧信息 + + + + + 保留字节 + + + + + 取流策略 + + + + + 从旧到新一帧一帧的获取图像(默认为该策略) + + + + + 获取列表中最新的一帧图像(同时清除列表中的其余图像) + + + + + 获取列表中最新的图像,个数由OutputQueueSize决定,范围为1-ImageNodeNum,设置成1等同于LatestImagesOnly,设置成ImageNodeNum等同于OneByOne + + + + + 等待下一帧图像 + + + + + 显示帧信息 + + + + + 显示窗口的句柄 + + + + + 显示的帧数据 + + + + + 显示的帧数据大小 + + + + + 图像宽 + + + + + 图像高 + + + + + 像素格式 + + + + + 保留字节 + + + + + 显示帧信息 + + + + + 图像宽 + + + + + 图像高 + + + + + 像素格式 + + + + + 显示的帧数据 + + + + + 显示的帧数据大小 + + + + + 图像渲染方式 0-默认模式(Windows GDI/Linux OPENGL), 1-D3D模式(Windows有效) + + + + + 保留字节 + + + + + 图像信息 + + + + + 图像宽 + + + + + 图像高 + + + + + 像素格式 + + + + + 图像缓存 + + + + + 图像缓存大小 + + + + + 图像长度 + + + + + 保留字节 + + + + + ch:保存3D数据格式 | en:Save 3D file + + + + + 未定义数据格式 + + + + + PLY数据格式 + + + + + CSV数据格式 + + + + + OBJ数据格式 + + + + + 保存的点阵参数 + + + + + [IN] 每一行点的数量 + + + + + [IN] 行数 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [OUT] 输出像素数据缓存 + + + + + [IN] 提供的输出缓冲区大小(nLinePntNum * nLineNum * (16*3 + 4) + 2048) + + + + + [OUT] 输出像素数据缓存长度 + + + + + 保存的点阵文件类型 + + + + + 保留字节 + + + + + 保存的图像格式 + + + + + 未定义类型 + + + + + Bmp图像格式 + + + + + Jpeg图像格式 + + + + + Png图像格式 + + + + + Tif图像格式 + + + + + 保存的图像参数 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [OUT] 输出图片缓存 + + + + + [OUT] 输出图片大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [IN] 输出图片格式 + + + + + 保存的图像参数 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [OUT] 输出图片缓存 + + + + + [OUT] 输出图片大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [IN] 输出图片格式 + + + + + [IN] 编码质量, (50-99] + + + + + [IN] Bayer的插值方法 0-快速 1-均衡 2-最优(如果传入其它值则默认为最优) + + + + + 保留字节 + + + + + 保存的图像信息扩展 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [OUT] 输出图片缓存 + + + + + [OUT] 输出图片大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [IN] 输出图片格式 + + + + + [IN] 编码质量, (50-99] + + + + + [IN] Bayer的插值方法 0-快速 1-均衡 2-最优(如果传入其它值则默认为最优) + + + + + 保留字节 + + + + + 保存图像到文件的参数 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [IN] 输入图片格式 + + + + + [IN] 编码质量, (0-100] + + + + + [IN] 输入文件路径 + + + + + [IN] Bayer的插值方法 0-快速 1-均衡 2-最优(如果传入其它值则默认为最优) + + + + + 保留字节 + + + + + 保存图像到文件信息扩展 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 输入图片格式 + + + + + [IN] 输入文件路径 + + + + + [IN] 编码质量, (0-100] + + + + + [IN] Bayer的插值方法 0-快速 1-均衡 2-最优(如果传入其它值则默认为最优) + + + + + 保留字节 + + + + + 保存图片所需参数 + + + + + [IN] 输入图片格式 + + + + + [IN] 编码质量, (0-100] + + + + + [IN] Bayer的插值方法 0-快速 1-均衡 2-最优(如果传入其它值则默认为最优) + + + + + 保留字节 + + + + + 旋转角度 + + + + + 旋转90度 + + + + + 旋转180度 + + + + + 旋转270度 + + + + + 旋转图像参数 + + + + + [IN] 像素格式(仅支持Mono8/RGB24/BGR24) + + + + + [IN][OUT] 图像宽 + + + + + [IN][OUT] 图像高 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [OUT] 输出图片缓存 + + + + + [OUT] 输出图片大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [IN] 旋转角度 + + + + + 保留字节 + + + + + 图像翻转类型 + + + + + 垂直方向翻转 + + + + + 水平方向翻转 + + + + + 翻转图像参数 + + + + + [IN] 像素格式(仅支持Mono8/RGB24/BGR24) + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [OUT] 输出图片缓存 + + + + + [OUT] 输出图片大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [IN] 翻转类型 + + + + + 保留字节 + + + + + 像素转换参数 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [IN] 源像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 目标像素格式 + + + + + [OUT] 输出数据缓存 + + + + + [OUT] 输出数据大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + 保留字节 + + + + + 图像像素转换信息扩展 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [IN] 源像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 目标像素格式 + + + + + [OUT] 输出数据缓存 + + + + + [OUT] 输出数据大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + 保留字节 + + + + + Gamma类型 + + + + + 不启用 + + + + + GAMMA值 + + + + + GAMMA曲线,8位需要的长度:256*sizeof(unsigned char) + 10位需要的长度:1024*sizeof(unsigned short) + 12位需要的长度:4096*sizeof(unsigned short) + 16位需要的长度:65536*sizeof(unsigned short) + + + + + 线性RGB转非线性RGB + + + + + 非线性RGB转线性RGB + + + + + Gamma参数 + + + + + [IN] Gamma类型 + + + + + [IN] Gamma值 + + + + + [IN] Gamma曲线缓存 + + + + + [IN] Gamma曲线长度 + + + + + 保留字节 + + + + + CCM参数 + + + + + [IN] 是否启用CCM + + + + + [IN] CCM矩阵(-8192~8192) + + + + + 保留字节 + + + + + CCM参数 + + + + + [IN] 是否启用CCM + + + + + [IN] 量化3x3矩阵 + + + + + [IN] 量化系数(2的整数幂) + + + + + 保留字节 + + + + + CLUT参数 + + + + + [IN] 是否启用CLUT + + + + + [IN] 量化系数(2的整数幂) + + + + + [IN] CLUT大小,建议值17 + + + + + [OUT] 量化CLUT + + + + + [IN] 量化CLUT缓存大小(nCLUTSize*nCLUTSize*nCLUTSize*sizeof(int)*3) + + + + + 保留字节 + + + + + 对比度调节参数 + + + + + [IN] 图像宽度(最小8) + + + + + [IN] 图像高度(最小8) + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [IN] 输入的像素格式 + + + + + [OUT] 输出像素数据缓存 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出像素数据缓存长度 + + + + + [IN] 对比度值,范围:[1, 10000] + + + + + 保留字节 + + + + + 锐化参数 + + + + + [IN] 图像宽度(最小8) + + + + + [IN] 图像高度(最小8) + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [IN] 输入的像素格式 + + + + + [OUT] 输出像素数据缓存 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出像素数据缓存长度 + + + + + [IN] 锐度调节强度,范围:[0, 500] + + + + + [IN] 锐度调节半径(半径越大,耗时越长),范围:[1, 21] + + + + + [IN] 锐度调节阈值,范围:[0, 255] + + + + + 保留字节 + + + + + 色彩校正参数(包括CCM和CLUT) + + + + + [IN] 图像宽度 + + + + + [IN] 图像高度 + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [IN] 输入的像素格式 + + + + + [OUT] 输出像素数据缓存 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出像素数据缓存长度 + + + + + [IN] 输入有效图像位数,8 or 10 or 12 or 16 + + + + + [IN] 输入Gamma信息 + + + + + [IN] 输入CCM信息 + + + + + [IN] 输入CLUT信息 + + + + + 保留字节 + + + + + 矩形ROI参数 + + + + + [IN] 矩形左上角X轴坐标 + + + + + [IN] 矩形左上角Y轴坐标 + + + + + [IN] 矩形宽度 + + + + + [IN] 矩形高度 + + + + + 噪声估计参数 + + + + + [IN] 图像宽度 + + + + + [IN] 图像高度 + + + + + [IN] 输入的像素格式 + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [IN] 图像ROI + + + + + [IN] ROI个数 + + + + + [IN] 噪声阈值[0-4095] + + + + + [OUT] 输出噪声特性 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出噪声特性长度 + + + + + 保留字节 + + + + + 空域降噪参数 + + + + + [IN] 图像宽度 + + + + + [IN] 图像高度 + + + + + [IN] 输入的像素格式 + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [OUT] 输出降噪后的数据 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出降噪后的数据长度 + + + + + [IN] 输入噪声特性 + + + + + [IN] 输入噪声特性长度 + + + + + [IN] 降噪强度(0-100) + + + + + [IN] 锐化强度(0-32) + + + + + [IN] 噪声校正系数(0-1280) + + + + + [IN] 亮度校正系数(1-2000) + + + + + [IN] 色调校正系数(1-2000) + + + + + [IN] 亮度降噪强度(0-100) + + + + + [IN] 色调降噪强度(0-100) + + + + + [IN] 锐化强度(1-1000) + + + + + 保留字节 + + + + + LSC标定参数 + + + + + [IN] 图像宽度(16~65536) + + + + + [IN] 图像高度(16~65536) + + + + + [IN] 输入的像素格式 + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [OUT] 输出标定表缓存 + + + + + [IN] 提供的标定表缓冲大小(nWidth*nHeight*sizeof(unsigned short)) + + + + + [OUT] 输出标定表缓存长度 + + + + + [IN] 宽度分块数 + + + + + [IN] 高度分块数 + + + + + [IN] 边缘填充系数,范围1~5 + + + + + [IN] 标定方式,0-中心为基准 + 1-最亮区域为基准 + 2-目标亮度 + + + + + [IN] 目标亮度(8bits,[0,255]) + (10bits,[0,1023]) + (12bits,[0,4095]) + (16bits,[0,65535]) + + + + + 保留字节 + + + + + LSC校正参数 + + + + + [IN] 图像宽度(16~65536) + + + + + [IN] 图像高度(16~65536) + + + + + [IN] 输入的像素格式 + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [OUT] 输出像素数据缓存 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出像素数据缓存长度 + + + + + [IN] 输入校正表缓存 + + + + + [IN] 输入校正表缓存长度 + + + + + 保留字节 + + + + + 噪声特性类型 + + + + + 无效 + + + + + 噪声曲线 + + + + + 噪声水平 + + + + + 默认值 + + + + + 噪声基本信息 + + + + + 版本 + + + + + 噪声特性类型 + + + + + 图像格式 + + + + + 平均噪声水平 + + + + + 曲线点数 + + + + + 噪声曲线 + + + + + 亮度曲线 + + + + + 保留字节 + + + + + 噪声估计参数 + + + + + [IN] 图像宽(大于等于8) + + + + + [IN] 图像高(大于等于8) + + + + + [IN] 像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 噪声阈值(0-4095) + + + + + [IN] 用于存储噪声曲线和亮度曲线(需要外部分配,缓存大小:4096 * sizeof(int) * 2) + + + + + [OUT] 降噪特性信息 + + + + + [IN] 线程数量,0表示算法库根据硬件自适应;1表示单线程(默认);大于1表示线程数目 + + + + + 保留字节 + + + + + 降噪参数 + + + + + [IN] 图像宽(大于等于8) + + + + + [IN] 图像高(大于等于8) + + + + + [IN] 像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [OUT] 输出降噪后的数据 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出降噪后的数据长度 + + + + + [IN] 降噪特性信息(来源于噪声估计) + + + + + [IN] 降噪强度(0-100) + + + + + [IN] 锐化强度(0-32) + + + + + [IN] 噪声校正系数(0-1280) + + + + + [IN] 线程数量,0表示算法库根据硬件自适应;1表示单线程(默认);大于1表示线程数目 + + + + + 保留字节 + + + + + 帧特殊信息 + + + + + [OUT] 秒数 + + + + + [OUT] 周期数 + + + + + [OUT] 周期偏移量 + + + + + [OUT] 增益 + + + + + [OUT] 曝光时间 + + + + + [OUT] 平均亮度 + + + + + [OUT] 红色 + + + + + [OUT] 绿色 + + + + + [OUT] 蓝色 + + + + + [OUT] 总帧数 + + + + + [OUT] 触发计数 + + + + + [OUT] 输入 + + + + + [OUT] 输出 + + + + + [OUT] 水平偏移量 + + + + + [OUT] 垂直偏移量 + + + + + [OUT] 水印宽 + + + + + [OUT] 水印高 + + + + + 保留字节 + + + + + HB解码参数 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [OUT] 图像宽 + + + + + [OUT] 图像高 + + + + + [OUT] 输出数据缓存 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出数据大小 + + + + + [OUT] 输出的像素格式 + + + + + [OUT] 水印信息 + + + + + 保留字节 + + + + + 录像格式定义 + + + + + 未定义格式 + + + + + AVI格式 + + + + + 录像参数 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 图像宽(指定目标参数时需为8的倍数) + + + + + [IN] 图像高(指定目标参数时需为8的倍数) + + + + + [IN] 帧率fps(大于1/16) + + + + + [IN] 码率kbps(128kbps-16Mbps) + + + + + [IN] 录像格式 + + + + + [IN] 录像文件存放路径 + + + + + 保留字节 + + + + + 输入帧信息 + + + + + [IN] 图像数据指针 + + + + + [IN] 图像大小 + + + + + 保留字节 + + + + + 采集模式 + + + + + 单帧模式 + + + + + 多帧模式 + + + + + 持续采集模式 + + + + + 增益模式 + + + + + 关闭 + + + + + 一次 + + + + + 连续 + + + + + 曝光模式 + + + + + Timed + + + + + TriggerWidth + + + + + 自动曝光模式 + + + + + 关闭 + + + + + 一次 + + + + + 连续 + + + + + 相机触发模式 + + + + + 关闭 + + + + + 打开 + + + + + Gamma选择器 + + + + + USER + + + + + SRGB + + + + + 自动白平衡 + + + + + 关闭自动白平衡 + + + + + 一次自动白平衡 + + + + + 连续自动白平衡 + + + + + 触发源 + + + + + LINE0 + + + + + LINE1 + + + + + LINE2 + + + + + LINE3 + + + + + COUNTER0 + + + + + SOFTWARE + + + + + FrequencyConverter + + + + + ALL MATHCH INFO + + + + + 需要输出的信息类型,e.g. MV_MATCH_TYPE_NET_DETECT + + + + + 输出的信息缓存,由调用者分配 + + + + + 信息缓存的大小 + + + + + + + + + + 已接收数据大小 [统计StartGrabbing和StopGrabbing之间的数据量] + + + + + 丢失的包数量 + + + + + 丢帧数量 + + + + + 帧数 + + + + + 请求重发包数 + + + + + 重发包数 + + + + + USB + + + + + 已接收数据大小 [统计OpenDevicce和CloseDevice之间的数据量] + + + + + 已收到的帧数 + + + + + 错误帧数 + + + + + 保留字节 + + + + + 图像的基本信息 + + + + + 宽度值 + + + + + 宽度最小值 + + + + + 宽度最大值 + + + + + Width Inc + + + + + 高度值 + + + + + 高度最小值 + + + + + 高度最大值 + + + + + Height Inc + + + + + 帧率 + + + + + 最小帧率 + + + + + 最大帧率 + + + + + 当前的像素格式 + + + + + 支持的像素格式种类 + + + + + 像素列表 + + + + + 保留字节 + + + + + 节点是否可见的权限等级 + + + + + Always visible + + + + + Visible for experts or Gurus + + + + + Visible for Gurus + + + + + Not Visible + + + + + Object is not yet initialized + + + + + 事件信息 + + + + + 事件名 + + + + + Event号 + + + + + 流通到序号 + + + + + 帧号高位 + + + + + 帧号低位 + + + + + 时间戳高位 + + + + + 时间戳低位 + + + + + Event数据 + + + + + Event数据长度 + + + + + 保留字节 + + + + + 节点错误类型 + + + + + 节点不存在 + + + + + 访问条件错误,通常是节点不可读写 + + + + + 写入越界,超出该节点支持的范围 + + + + + 校验失败,通常是写入的值与文件中的值不匹配 + + + + + 其它错误,可查阅日志 + + + + + 节点错误信息 + + + + + 节点名称 + + + + + 错误类型 + + + + + 保留字节 + + + + + 错误信息列表 + + + + + 错误个数 + + + + + 错误信息 + + + + + 保留字节 + + + + + 节点名称 + + + + + 节点名称 + + + + + 保留字节 + + + + + 节点列表 + + + + + 错误信息 + + + + + 节点个数 + + + + + 保留字节 + + + + + 文件存取 + + + + + 用户文件名 + + + + + 设备文件名 + + + + + 保留字节 + + + + + 文件存取 + + + + + 用户文件数据缓存空间 + + + + + 用户数据缓存大小 + + + + + 文件实际缓存大小 + + + + + 设备文件名 + + + + + 保留字节 + + + + + 文件存取进度 + + + + + 已完成的长度 + + + + + 总长度 + + + + + 保留字节 + + + + + GigE传输类型 + + + + + 表示单播(默认) + + + + + 表示组播 + + + + + 表示局域网内广播,暂不支持 + + + + + 表示子网内广播,暂不支持 + + + + + 表示从相机获取,暂不支持 + + + + + 表示用户自定义应用端接收图像数据Port号 + + + + + 表示设置了单播,但本实例不接收图像数据 + + + + + 表示组播模式,但本实例不接收图像数据 + + + + + 传输模式,可以为单播模式、组播模式等 + + + + + 传输模式 + + + + + 目标IP,组播模式下有意义 + + + + + 目标Port,组播模式下有意义 + + + + + 保留字节 + + + + + 动作命令信息 + + + + + 设备密钥 + + + + + 组键 + + + + + 组掩码 + + + + + 只有设置成1时Action Time才有效,非1时无效 + + + + + 预定的时间,和主频有关 + + + + + 广播包地址 + + + + + 等待ACK的超时时间,如果为0表示不需要ACK + + + + + 只有设置成1时指定的网卡IP才有效,非1时无效 + + + + + 指定的网卡IP + + + + + 保留字节 + + + + + 动作命令结果 + + + + + IP address of the device + + + + + status code returned by the device + + + + + 保留字节 + + + + + 动作命令结果列表 + + + + + 返回值个数 + + + + + 返回的结果 + + + + + 每个节点对应的接口类型 + + + + + IValue接口类型 + + + + + IBase接口类型 + + + + + IInteger接口类型 + + + + + IBoolean接口类型 + + + + + ICommand接口类型 + + + + + IFloat接口类型 + + + + + IString接口类型 + + + + + IRegister接口类型 + + + + + ICategory接口类型 + + + + + IEnumeration接口类型 + + + + + IEnumEntry接口类型 + + + + + IPort接口类型 + + + + + XML节点特点 + + + + + 节点类型 + + + + + 是否可见 + + + + + 节点描述 + + + + + 显示名称 + + + + + 节点名 + + + + + 提示 + + + + + 保留字节 + + + + + XML节点列表 + + + + + 节点个数 + + + + + 节点列表 + + + + + 整型节点值 + + + + + 当前值 + + + + + 最大值 + + + + + 最小值 + + + + + Inc + + + + + 保留字节 + + + + + 整型节点值 + + + + + 当前值 + + + + + 最大值 + + + + + 最小值 + + + + + Inc + + + + + 保留字节 + + + + + 浮点型节点值 + + + + + 当前值 + + + + + 最大值 + + + + + 最小值 + + + + + 保留字节 + + + + + 枚举型节点值 + + + + + 当前值 + + + + + 有效数据个数 + + + + + 保留字节 + + + + + 保留字节 + + + + + 枚举型节点值 + + + + + 当前值 + + + + + 有效数据个数 + + + + + 保留字节 + + + + + 保留字节 + + + + + 字符串型节点值 + + + + + 当前值 + + + + + 节点值的最大长度 + + + + + 保留字节 + + + + + 节点的读写性 + + + + + 未实现 + + + + + 不可获取 + + + + + 只写 + + + + + 只读 + + + + + 可读可写 + + + + + 未定义 + + + + + 内部用于AccessMode循环检测 + + + + + 整型节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + 是否可见 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 当前值 + + + + + 最小值 + + + + + 最大值 + + + + + 增量 + + + + + 保留字节 + + + + + 布尔型节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + 是否可见 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 当前值 + + + + + 保留字节 + + + + + 命令型节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + 是否可见 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 保留字节 + + + + + 浮点型节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + 是否可见 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 当前值 + + + + + 最小值 + + + + + 最大值 + + + + + 增量 + + + + + 保留字节 + + + + + 字符串类型节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + 是否可见 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 当前值 + + + + + 保留字节 + + + + + 寄存器型节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + 是否可见 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 当前值 + + + + + 保留字节 + + + + + 类别属性 + + + + + 节点描述 + + + + + 显示名称 + + + + + 节点名 + + + + + 提示 + + + + + 是否可见 + + + + + 保留字节 + + + + + EnumEntry属性节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + + + + + + 父节点数 + + + + + 父节点列表 + + + + + 是否可见 + + + + + 当前值 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 保留字节 + + + + + 节点描述 + + + + + 节点描述 + + + + + Enumeration属性节点 + + + + + 是否可见 + + + + + 节点描述 + + + + + 显示名称 + + + + + 节点名 + + + + + 提示 + + + + + Symbolic数 + + + + + 当前Symbolic索引 + + + + + Symbolic索引 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 当前值 + + + + + 保留字节 + + + + + Port属性节点 + + + + + 是否可见 + + + + + 节点描述 + + + + + 显示名称 + + + + + 节点名 + + + + + 提示 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 保留字节 + + + + 辅助线颜色 + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + 预留字节 + + + 自定义点坐标 + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + 预留字节 + + + 矩形框区域信息 + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + 辅助线颜色 + + + 辅助线宽度 + + + 预留字节 + + + 圆形框区域信息 + + + 圆心信息 + + + 宽向半径,根据图像的相对位置[0, 1.0] + + + 高向半径,根据图像的相对位置[0, 1.0] + + + 辅助线颜色信息 + + + 辅助线宽度 + + + 预留字节 + + + 线条辅助线信息 + + + 线条辅助线的起始点坐标 + + + 线条辅助线的终点坐标 + + + 辅助线颜色信息 + + + 辅助线宽度 + + + 预留字节 + + + 枚举类型指定条目信息 + + + 指定值 + + + 指定值对应的符号 + + + 预留字节 + + + U3V流异常类型 + + + 异常的图像,该帧被丢弃 + + + 缓存列表溢出,清除最旧的一帧 + + + 缓存列表为空,该帧被丢弃 + + + 断流恢复 + + + 断流,恢复失败,取流被中止 + + + 设备异常,取流被中止 + + + 重构后的图像列表 + + + 源图像宽 + + + 源图像高 + + + 像素格式 + + + 输出数据缓存 + + + 输出数据长度 + + + 提供的输出缓冲区大小 + + + 预留字节 + + + + 分时曝光的图像处理方式 + + + + + 源图像按行拆分成多张图像 + + + + + 源图像两行拆分成多张图像 + + + + 重构图像参数信息 + + + 源图像宽 + + + 源图像高 + + + 像素格式 + + + 输入数据缓存 + + + 输入数据长度 + + + 曝光个数(1-8] + + + 图像重构方式 + + + + 输出数据缓存信息 + + + + 预留字节 + + + 串口信息 + + + + 串口号 + + + + + 保留字节 + + + + 串口列表 + + + + 串口数量 + + + + + 串口信息 + + + + + 保留字节 + + + + + 像素格式定义 + + + + + 未定义像素格式 + + + + + Mono1p + + + + + Mono2p + + + + + Mono4p + + + + + Mono8 + + + + + Mono8_Signed + + + + + Mono10 + + + + + Mono10_Packed + + + + + Mono12 + + + + + Mono12_Packed + + + + + Mono14 + + + + + Mono16 + + + + + BayerGR8 + + + + + BayerRG8 + + + + + BayerGB8 + + + + + BayerBG8 + + + + + BayerRBGG8 + + + + + BayerGR10 + + + + + BayerRG10 + + + + + BayerGB10 + + + + + BayerBG10 + + + + + BayerGR12 + + + + + BayerRG12 + + + + + BayerGB12 + + + + + BayerBG12 + + + + + BayerGR10_Packed + + + + + BayerRG10_Packed + + + + + BayerGB10_Packed + + + + + BayerBG10_Packed + + + + + BayerGR12_Packed + + + + BayerRG12_Packed + + + BayerGB12_Packed + + + BayerBG12_Packed + + + BayerGR16 + + + BayerRG16 + + + BayerGB16 + + + BayerBG16 + + + RGB8_Packed + + + BGR8_Packed + + + RGBA8_Packed + + + BGRA8_Packed + + + RGB10_Packed + + + BGR10_Packed + + + RGB12_Packed + + + BGR12_Packed + + + RGB16_Packed + + + BGR16_Packed/// + + + RGBA16_Packed + + + BGRA16_Packed + + + RGB10V1_Packe + + + RGB10V2_Packed + + + RGB12V1_Packed + + + RGB565_Packed + + + BGR565_Packed + + + YUV411_Packed + + + YUV422_Packed + + + YUV422_YUYV_Packed + + + YUV444_Packed + + + YCBCR8_CBYCR + + + YCBCR422_8 + + + YCBCR422_8_CBYCRY + + + YCBCR411_8_CBYYCRYY + + + YCBCR601_8_CBYCR + + + YCBCR601_422_8 + + + YCBCR601_422_8_CBYCRY + + + YCBCR601_411_8_CBYYCRYY + + + YCBCR709_8_CBYCR + + + YCBCR709_422_8 + + + YCBCR709_422_8_CBYCRY + + + YCBCR709_411_8_CBYYCRYY + + + YUV420SP_NV12 + + + YUV420SP_NV21 + + + RGB8_Planar + + + RGB10_Planar + + + RGB12_Planar + + + RGB16_Planar + + + Jpeg + + + Coord3D_ABC32f + + + Coord3D_ABC32f_Planar + + + Coord3D_AC32f + + + COORD3D_DEPTH_PLUS_MASK + + + Coord3D_ABC32 + + + Coord3D_AB32f + + + Coord3D_AB32 + + + Coord3D_AC32f_64 + + + Coord3D_AC32f_Planar + + + Coord3D_AC32 + + + Coord3D_A32f + + + Coord3D_A32 + + + Coord3D_C32f + + + Coord3D_C32 + + + Coord3D_ABC16 + + + Coord3D_C16 + + + Float32 + + + HB_Mono8 + + + HB_Mono10 + + + HB_Mono10_Packed + + + HB_Mono12 + + + HB_Mono12_Packed + + + HB_Mono16 + + + HB_BayerGR8 + + + HB_BayerRG8 + + + HB_BayerGB8 + + + HB_BayerBG8 + + + HB_BayerRBGG8 + + + HB_BayerGR10 + + + HB_BayerRG10 + + + HB_BayerGB10 + + + HB_BayerBG10 + + + HB_BayerGR12 + + + HB_BayerRG12 + + + HB_BayerGB12 + + + HB_BayerBG12 + + + HB_BayerGR10_Packed + + + HB_BayerRG10_Packed + + + HB_BayerGB10_Packed + + + HB_BayerBG10_Packed + + + HB_BayerGR12_Packed + + + HB_BayerRG12_Packed + + + HB_BayerGB12_Packed + + + HB_BayerBG12_Packed + + + HB_YUV422_Packed + + + HB_YUV422_YUYV_Packed + + + HB_RGB8_Packed + + + HB_BGR8_Packed + + + HB_RGBA8_Packed + + + HB_BGRA8_Packed + + + HB_RGB16_Packed + + + HB_BGR16_Packed + + + HB_RGBA16_Packed + + + HB_BGRA16_Packed + + + + 图像类,实现基本的图像属性和接口,实现图像克隆、ToBitmap。作为其他图像类的基类 + + + + + 根据图像宽、高和像素格式创建对象,内部申请内存 + + 图像宽度 + 图像高度 + 像素格式 + 相机句柄,用于ToBitmap时像素格式转换 + + + + 根据图像宽、高、像素格式、图像大小创建对象,内部申请内存 + + 图像宽度 + 图像高度 + 像素格式 + 图像大小 + 相机句柄,用于ToBitmap时像素格式转换 + + + + 根据图像宽、高、像素格式、图像大小创建对象,外部传入内存 + + + + + + + + + + + 用于克隆 + + + + + + 构造空对象,内部变量由由子类初始化 + + + + + 相机句柄,用于内部做格式转换等图像处理 + + + + + 非托管内存指针 + + + + + 图像数据,内部会进行一次拷贝,将非托管内存拷贝到托管内存 + + + + + 从C库获取图像地址(GetImageBuffer或者回调),包装成Image,图像数据存放在非托管内存中 + + + + + 使用传入的非托管内存创建新的对象 + + + + + + + 设备句柄 + + + + 非托管内存指针 + + + + + 图像数据,内部会进行一次拷贝,将非托管内存拷贝到托管内存 + + + + + 基于内存池的Image对象,用于图像处理相关接口 + + + + + 根据图像宽、高和像素格式创建对象,内部申请内存 + + 图像宽度 + 图像高度 + 像素格式 + 相机句柄,用于ToBitmap时像素格式转换 + 内存池 + + + + 根据图像宽、高、像素格式、图像大小创建对象,内部申请内存 + + 图像宽度 + 图像高度 + 像素格式 + 相机句柄,用于ToBitmap时像素格式转换 + 图像大小 + 内存池 + + + + 根据图像宽、高、像素格式、图像大小创建对象,外部传入内存池内存 + + + + + + + + + + + + 整型参数实现类 + + + + + 整形参数 + + + + + 当前值 + + + + + 最大值 + + + + + 最小值 + + + + + Increment + + + + + 枚举项参数 + + + + + 枚举值 + + + + + 枚举符号 + + + + + 枚举类型实现类 + + + + + 枚举类型参数 + + + + + 当前枚举项 + + + + + 支持的枚举类型个数 + + + + + 支持的枚举项列表 + + + + + float类型实现类 + + + + + 浮点型参数 + + + + + 当前值 + + + + + 最大值 + + + + + 最小值 + + + + + String类型参数实现类 + + + + + 字符串类型参数 + + + + + 当前值 + + + + + 最大长度 + + + + + 参数导入时出错的节点信息 + + + + + 参数导入导出时的节点错误信息 + + + + + 节点名称 + + + + + 错误类型 + + + + + 节点名称 + + + + + 错误信息 + + + + + 参数实现类 + + + + + 提供设备参数配置相关接口 + + + + + 清除GenICam节点缓存 + + 成功,返回MV_OK;失败,返回错误码 + + 在加载工业相机节点时需要读取GenICam配置文件,该接口可以起到清除GenICam缓存的功能。 + + + + + 获取Integer属性值 + + 属性键值,如获取宽度信息则为"Width" + 属性值 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置Integer型属性值 + + 属性键值,如设置宽度信息则为"Width" + 属性值 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取Enumeration属性值 + + 属性键值,如获取像素格式信息则为"PixelFormat" + 属性值 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置Enumeration属性值 + + 属性键值,如设置像素格式信息则为"PixelFormat" + Enum型节点的值 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置Enumeration属性 + + 属性键值,如设置像素格式信息则为"PixelFormat" + EnumEntry的名称 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取Float属性值 + + 属性键值 + 属性值 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置Float型属性值 + + 属性键值 + 属性值 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取Boolean属性值 + + 属性键值 + 属性值 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置Boolean型属性值 + + 属性键值 + 属性值 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取String属性值 + + 属性键值 + 属性值 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置String型属性值 + + 属性键值 + 属性值 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置Command型属性值 + + 属性键值 + 成功,返回MV_OK;失败,返回错误码 + + + + 导入设备属性文件 + + 文件路径 + 成功,返回MV_OK;失败,返回错误码 + + + + 导入设备属性文件并保存错误信息列表 + + 文件路径 + 导入失败的节点信息 + 成功,返回MV_OK;失败,返回错误码 + 部分节点导入失败时也会返回MV_OK,通过nodeErrors返回导入失败的节点及错误原因 + + + + 保存设备属性到文件 + + 文件路径 + 成功,返回MV_OK;失败,返回错误码 + + + + 通过设备寄存器地址读取寄存器 + + 待读取的内存地址,该地址可以从设备的Camera.xml文件中获取,形如xxx_RegAddr的xml节点值 + 待读取的内存长度 + 存放读到的内存值(GEV设备内存值是按照大端模式存储的,其它协议设备按照小端存储) + 成功,返回MV_OK;失败,返回错误码 + + 访问设备,读取某段寄存器的数据。 + + + + + 通过设备寄存器地址写寄存器 + + 待写入的内存地址,该地址可以从设备的Camera.xml文件中获取,形如xxx_RegAddr的xml节点值 + 待写入的内存长度 + 待写入的内存值(注意GEV设备内存值要按照大端模式存储,其它协议设备按照小端存储) + 成功,返回MV_OK;失败,返回错误码 + + 访问设备,把一段数据写入某段寄存器。 + + + + + 获取设备属性树XML + + 设备xml + 成功,返回MV_OK;失败,返回错误码 + + + + 获得当前节点的访问模式 + + 节点名称 + 节点的访问模式 + 成功,返回MV_OK;失败,返回错误码 + + + + 获得当前节点的类型 + + 节点名称 + 节点类型 + 成功,返回MV_OK;失败,返回错误码 + + + + 从设备读取文件,保存为本地文件 + + 设备文件名 + 本地文件路径 + 成功,返回MV_OK;失败,返回错误码 + + + + 从设备读取文件,保存在内存 + + 设备文件名 + 缓存 + 成功,返回MV_OK;失败,返回错误码 + + + + 将文件写入设备 + + 设备文件名 + 本地文件路径 + 成功,返回MV_OK;失败,返回错误码 + + + + 将内存中的文件写入设备 + + 设备文件名 + 缓存 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取文件存取的进度 + + 已完成的长度 + 总长度 + 成功,返回MV_OK;失败,返回错误码 + + + + 设备句柄 + + + + + FileAccessRead到Byte数组时的默认文件大小 + + + + + 构造函数 + + + + + + 提供像素格式转换相关接口 + + + + + 设置图像插值算法类型 + + 图像插值算法 + 成功,返回MV_OK;失败,返回错误码 + + 设置内部图像转换接口的Bayer插值算法类型参数,使用的插值算法是该接口所设定的 + + + + + 插值算法平滑使能设置 + + 平滑使能(默认关闭) + 成功,返回MV_OK;失败,返回错误码 + + 设置内部图像转换接口的Bayer插值平滑使能参数,使用的插值算法是该接口所设定的。 + + + + + 设置Bayer格式的Gamma值 + + Gamma值:0.1 ~ 4.0 + 成功,返回MV_OK;失败,返回错误码 + + 设置该值后,将Bayer8/10/12/16格式转成RGB24/48, RGBA32/64,BGR24/48,BGRA32/64时起效。 + + + + + 设置Mono8/Bayer8/10/12/16格式的Gamma值 + + 像素格式 + Gamma值:0.1 ~ 4.0 + 成功,返回MV_OK;失败,返回错误码 + + 设置Mono8的gamma值后,再调用将Mono8转成Mono8时gamma值起效。 + 设置Bayer8/10/12/16的gamma值后,将Bayer8/10/12/16格式转成RGB24/48, RGBA32/64,BGR24/48,BGRA32/64时起效。 + 该接口兼容接口,新增支持Mono8像素格式 + + + + + 设置Bayer格式的Gamma信息 + + Gamma参数 + 成功,返回MV_OK;失败,返回错误码 + + 设置该信息后,在将Bayer8/10/12/16格式转成RGB24/48, RGBA32/64,BGR24/48,BGRA32/64时起效。 + + + + + 设置Bayer格式的CCM使能和矩阵,量化系数默认1024 + + CCM参数 + 成功,返回MV_OK;失败,返回错误码 + + 开启CCM并设置CCM矩阵后,在将Bayer8/10/12/16格式转成RGB24/48, RGBA32/64,BGR24/48,BGRA32/64时起效。 + + + + + 像素格式转换 + + 输入图像 + 输出图像。图像使用完之后需调用Dispose方法及时释放内存,防止内存快速上涨。 + 目标像素格式 + 成功,返回MV_OK;失败,返回错误码 + + + + 像素格式转换 + + 输入图像 + 输出图像缓存 + 输出图像长度 + 目标像素格式 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取像素格式转换所需的缓存大小 + + 目标像素格式 + 图像宽 + 图像高 + 缓存大小 + + + + 设备句柄, 用于内部做格式转换 + + + + + 构造函数,内部创建内存池,只在ToBitmap中使用 + + 设备句柄 + + + + 构造函数 + + 设备句柄 + + + + + 设置图像插值算法类型 + + 图像插值算法 + 成功,返回MV_OK;失败,返回错误码 + + + + 插值算法平滑使能设置 + + 平滑使能(默认关闭) + 成功,返回MV_OK;失败,返回错误码 + + + + 设置Bayer格式的Gamma值 + + Gamma值:0.1 ~ 4.0 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置Mono8/Bayer8/10/12/16格式的Gamma值 + + 像素格式 + Gamma值:0.1 ~ 4.0 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置Bayer格式的Gamma信息 + + Gamma参数 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置Bayer格式的CCM使能和矩阵,量化系数默认1024 + + CCM参数 + 成功,返回MV_OK;失败,返回错误码 + + + + 将图像转换为指定格式,输出IImage图像 + + 输入图像 + 输出图像 + 目的像素格式 + 成功,返回MV_OK;失败,返回错误码 + + + + 将图像转换为指定格式,输出Byte数组 + + 输入图像 + 输出图像缓存 + 输出图像长度 + 目的像素格式 + 成功,返回MV_OK;失败,返回错误码 + + outBuffer设置为null时, outDataLen会给出结果图像需要的缓存长度 + + + + + 获取像素格式转换所需的缓存大小 + + 目标像素格式 + 图像宽 + 图像高 + 缓存大小 + + + + SDk全局信息及操作接口 + + + + + 获取SDK版本信息 + + SDK版本号,格式x.y.z.a + + + + 初始化SDK + + 成功,返回MV_OK;失败,返回错误码 + + + + 反初始化SDK,释放资源 + + 成功,返回MV_OK;失败,返回错误码 + + + + 图像数据和帧信息 + + + + + 图像数据 + + + + + 帧号 + + + + + 设备时间戳 + + + + + 主机时间戳 + + + + + 帧长度 + + + + + 设备水印时标 + + + + + 周期数 + + + + + 周期偏移量 + + + + + 增益 + + + + + 曝光时间 + + + + + 平均亮度 + + + + + 白平衡红色通道 + + + + + 白平衡绿色通道 + + + + + 白平衡蓝色通道 + + + + + 总帧数 + + + + + 触发计数 + + + + + 输入 + + + + + 输出 + + + + + ROI区域,水平偏移量 + + + + + ROI区域,垂直偏移量 + + + + + 本帧丢包数 + + + + + Chunk数据 + + + + + 深拷贝 + + + + + + 相机句柄 + + + + + 图像类 + + + + + 提供取流相关接口 + + + + + 设置SDK内部图像缓存节点个数,大于等于1 + + 缓存节点个数 + 成功,返回MV_OK;失败,返回错误码 + + 调用该接口可以设置SDK内部图像缓存节点个数,在 IStreamGrabber.StartGrabbing() 前调用。 + 不同相机因为取流方式不同,不调用SetImageNodeNum方法的情况下,不同相机默认缓存节点的个数不同:比如 双U内部分配默认3个节点。 + SDK实际分配的节点个数 = SDK内部预分配的个数 + 用户分配的节点(SetImageNodeNum); + + + + + 获取当前图像缓存区的有效图像个数 + + 有效图像个数 + 成功,返回MV_OK;失败,返回错误码 + + + + 开始取流 + + 成功,返回MV_OK;失败,返回错误码 + + + + 开始取流,支持设置取流策略 + + 策略枚举值 + 成功,返回MV_OK;失败,返回错误码 + + + + 设置输出缓存个数(只有在 StreamGrabStrategy.LatestImages 策略下才有效,范围:1-ImageNodeNum) + + 输出缓存个数 + 成功,返回MV_OK;失败,返回错误码 + + + + 停止取流 + + 成功,返回MV_OK;失败,返回错误码 + + + + 获取一帧图像 + + 等待超时时间 + 图像数据和图像信息 + 成功,返回MV_OK;失败,返回错误码 + + + + 释放图像缓存(此接口用于释放不再使用的图像缓存,与 IStreamGrabber.FreeImageBuffer() 配套使用) + + 图像数据和图像数据 + 成功,返回MV_OK;失败,返回错误码 + + + + 清除取流数据缓存 + + 成功,返回MV_OK;失败,返回错误码 + + 该接口允许用户在不停止取流的时候,就能清除缓存中不需要的图像。 + 该接口在连续模式切触发模式后,可以清除历史数据。 + + + + + 设备流异常事件,只有USB设备支持 + + + 在StartGrabbing前调用 + + + + + 采集一帧图像的事件,获取图像数据 + + + 在StartGrabbing前调用 + + + + + 内部回调图像回调函数 + + + + + + + + 内部流异常回调函数 + + + + + + + 将MvCCDll中的帧结构体转为FrameOut类 + + + + + + + + USB流异常回调 + + + + + 设置U3V的传输包大小 + + 传输的包大小,单位:Byte,默认为1M,范围:Windows[0x400, 0x400000], Linux[0x400, 0x200000] + 成功,返回MV_OK;失败,返回错误码 + + 增加传输包大小可以适当降低取流时的CPU占用率。但不同的PC和不同USB扩展卡存在不同的兼容性,如果该参数设置过大可能会出现取不到图像的风险。 + + + + + 获取U3V的传输包大小 + + 传输的包大小, 单位:Byte + 成功,返回MV_OK;失败,返回错误码 + + + + 设置U3V的传输通道个数 + + 传输通道个数,范围:1-10 + 成功,返回MV_OK;失败,返回错误码 + + 可根据PC的性能、设备出图帧率、图像大小和内存使用率等因素对该参数进行调节。但不同的PC和不同的USB扩展卡存在不同的兼容性。 + + + + + 获取U3V的传输通道个数 + + 传输通道个数 + 成功,返回MV_OK;失败,返回错误码 + + 该接口用于获取当前的U3V异步取流节点个数,2000W设备的MONO8默认为3个,YUV为默认2个,RGB为默认1个,其它情况默认8个节点。 + + + + + 设置U3V的事件缓存节点个数 + + 事件缓存节点个数,范围:1-64 + 成功,返回MV_OK;失败,返回错误码 + + 该接口用于设置当前的U3V事件缓存节点个数,默认情况下为5个。 + + + + + 设置U3V相机同步读写超时时间,范围为1000~UINT,默认1000 ms + + 同步读写超时时间 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取U3V相机同步读写超时时间 + + 同步读写超时时间 + 成功,返回MV_OK;失败,返回错误码 + + + + 获取主机从USB设备接收的数据统计信息,如已接收字节数、帧数 + + USB传输信息 + 成功,返回MV_OK;失败,返回错误码 + + + + 录像功能实现类 + + + + + 录像,将图片录制成AVI格式视频 + + + + + 开始录像 + + 录像文件存放路径 + 录像参数 + 成功,返回MV_OK;失败,返回错误码 + + + + 输入录像数据 + + 图像数据 + 成功,返回MV_OK;失败,返回错误码 + + + + 停止录像 + + 成功,返回MV_OK;失败,返回错误码 + + + + 异常信息 + + + + + 错误码 + + + + + Xml节点访问模式 + + + + + 不可实现 + + + + + 不可用 + + + + + 只写 + + + + + 只读 + + + + + 读写 + + + + + 未定义 + + + + + 内部用于AccessMode循环检测 + + + + + 每个节点对应的接口类型 + + + + + Value + + + + + Base + + + + + Integer + + + + + Boolean + + + + + Command + + + + + Float + + + + + String + + + + + Register + + + + + Category + + + + + Enumeration + + + + + EnumEntry + + + + + Port + + + + + 节点错误信息的类型 + + + + + 节点不存在 + + + + + 访问条件错误,通常是节点不可读写 + + + + + 写入越界,超出该节点支持的范围 + + + + + 校验失败,通常是写入的值与文件中的值不匹配 + + + + + 其它错误,可查阅日志 + + + + + 图像插值算法类型 + + + + + 快速 + + + + + 均衡 + + + + + 最优 + + + + + 最优+ + + + + + Gamma类型 + + + + + 不启用 + + + + + Gamma值 + + + + + Gamma曲线 + 8位,长度:256*sizeof(unsigned char) + 10位,长度:1024*sizeof(unsigned short) + 12位,长度:4096*sizeof(unsigned short) + 16位,长度:65536*sizeof(unsigned short) + + + + + linear RGB to sRGB + + + + + sRGB to linear RGB(仅色彩插值时支持,色彩校正时无效) + + + + + Gamma参数 + + + + + Gamma类型 + + + + + Gamma值[0.1, 4.0] + + + + + Gamma曲线缓存 + + + + + Gamma曲线长度 + + + + + CCM参数 + + + + + 是否启用CCM + + + + + CCM矩阵[-65536~65536],必须是Int32[9] + + + + + 量化系数(2的整数幂,最大65536) + + + + + 流异常类型 + + + + + 异常的图像,该帧被丢弃 + + + + + 缓存列表溢出,清除最旧的一帧 + + + + + 缓存列表为空,该帧被丢弃 + + + + + 断流恢复 + + + + + 断流,恢复失败,取流被中止 + + + + + 设备异常,取流被中止 + + + + + 流异常事件 + + + + + 流异常类型 + + + + + 取流策略 + + + + + 从旧到新一帧一帧的获取图像(默认为该策略) + + + + + 获取列表中最新的一帧图像,同时清除列表中的其余图像 + + + + + 从输出缓存列表中获取最新的OutputQueueSize帧图像,其中OutputQueueSize范围为1-ImageNodeNum,可用 SetOutputQueueSize()接口设置。ImageNodeNum默认为1,可调用SetImageNodeNum()接口设置。 OutputQueueSize设置成1等同于LatestImagesOnly策略,OutputQueueSize设置成ImageNodeNum等同于OneByOne策略。 + + + + + 在调用取流接口时忽略输出缓存列表中所有图像,并等待设备即将生成的一帧图像。该策略只支持GigE设备,不支持U3V设备 + + + + + 采集一帧图像的事件,用于获取图像数据和帧信息 + + + + + 图像帧信息 + + + + + 视频格式 + + + + + AVI格式 + + + + + 录像参数 + + + + + 输入数据的像素格式 + + + + + 图像宽(指定目标参数时需为2的倍数) + + + + + 图像高(指定目标参数时需为2的倍数) + + + + + 帧率fps(大于1/16) + + + + + 码率kbps(128-16*1024) + + + + + 录像格式 + + + + + MyCamera + + + + ch GigE Vision采集卡 |en GigE Vision interface + + + ch Camera Link采集卡 |en Camera Link interface + + + ch CoaXPress采集卡 |en CoaXPress interface + + + ch XoFLink采集卡 |en XoFLink interface + + + Unknown Device Type, Reserved + + + GigE Device + + + 1394-a/b Device + + + USB3.0 Device + + + CameraLink Device + + + Virtual GigE Device + + + Virtual USB Device + + + GenTL GigE Device + + + GenTL CML Device + + + GenTL CXP Device + + + GenTL XOF Device + + + + ch:信息结构体的最大缓存 | en: Max buffer size of information structs + + + + + 最大的相机数量 + + + + + ch:最大Interface数量 | en:Max num of interfaces + + + + + ch:最大GenTL设备数量 | en:Max num of GenTL devices + + + + + XML节点描述最大长度 + + + + + XML节点最大长度 + + + + + XML节点最大数量 + + + + + XML节点显示名最大数量 + + + + + string类型节点值的最大长度 + + + + + 最大父节点数 + + + + + 最大节点描述长度 + + + + + 设备断开连接 + + + + + SDK与驱动版本不匹配 + + + + + 相机Event事件名称最大长度 + + + + 最大枚举条目对应的符号长度 + + + 分时曝光时最多将源图像拆分的个数 + + + + ch:最大支持的采集卡数量 | en:The maximum number of Frame Grabber interface supported + + + + + ch 静态 |en Static + + + + + ch DHCP |en DHCP + + + + + ch LLA |en LLA + + + + + 9600 + + + + + 19200 + + + + + 38400 + + + + + 57600 + + + + + 115200 + + + + + 230400 + + + + + 460800 + + + + + 921600 + + + + + ch 最大值 |en Auto Max + + + + + ch 网络流量和丢包信息 |en Network traffic and packet loss information + + + + + ch host接收到来自U3V设备的字节总数 |en The total number of bytes host received from U3V device + + + + + ch独占权限,其他APP只允许读CCP寄存器 |en Exclusive authority, other APP is only allowed to read the CCP register + + + + + ch 可以从5模式下抢占权限,然后以独占权限打开 |en You can seize the authority from the 5 mode, and then open with exclusive authority + + + + + ch 控制权限,其他APP允许读所有寄存器 |en Control authority, allows other APP reading all registers + + + + + ch 可以从5的模式下抢占权限,然后以控制权限打开 |en You can seize the authority from the 5 mode, and then open with control authority + + + + + ch 以可被抢占的控制权限打开 |en Open with seized control authority + + + + + ch 可以从5的模式下抢占权限,然后以可被抢占的控制权限打开 |en You can seize the authority from the 5 mode, and then open with seized control authority + + + + + ch 读模式打开设备,适用于控制权限下 |en Open with read mode and is available under control authority + + + + 成功,无错误 + + + 错误或无效的句柄 + + + 不支持的功能 + + + 缓存已满 + + + 函数调用顺序错误 + + + 错误的参数 + + + 资源申请失败 + + + 无数据 + + + 前置条件有误,或运行环境已发生变化 + + + 版本不匹配 + + + 传入的内存空间不足 + + + 异常图像,可能是丢包导致图像不完整 + + + 动态导入DLL失败 + + + 没有可输出的缓存 + + + 加密错误 + + + 打开文件出现错误 + + + 未知的错误 + + + 通用错误 + + + 参数非法 + + + 值超出范围 + + + 属性 + + + 运行环境有问题 + + + 逻辑错误 + + + 节点访问条件有误 + + + 超时 + + + 转换异常 + + + GenICam未知错误 + + + 命令不被设备支持 + + + 访问的目标地址不存在 + + + 目标地址不可写 + + + 设备无访问权限 + + + 设备忙,或网络断开 + + + 网络包数据错误 + + + 网络相关错误 + + + 设备IP冲突 + + + 读usb出错 + + + 写usb出错 + + + 设备异常 + + + GenICam相关错误 + + + 带宽不足 + + + 驱动不匹配或者未装驱动 + + + USB未知的错误 + + + 升级固件不匹配 + + + 升级固件语言不匹配 + + + 升级冲突(设备已经在升级了再次请求升级即返回此错误) + + + 升级时设备内部出现错误 + + + 升级时未知错误 + + + 处理正确 + + + 不确定类型错误 + + + 能力集中存在无效参数 + + + 内存地址为空 + + + 内存对齐不满足要求 + + + 内存空间大小不够 + + + 内存空间大小不满足对齐要求 + + + 内存地址不满足对齐要求 + + + 图像格式不正确或者不支持 + + + 图像宽高不正确或者超出范围 + + + 图像宽高与step参数不匹配 + + + 图像数据存储地址为空 + + + 设置或者获取参数类型不正确 + + + 设置或者获取参数的输入、输出结构体大小不正确 + + + 处理类型不正确 + + + 处理时输入、输出参数大小不正确 + + + 子处理类型不正确 + + + 子处理时输入、输出参数大小不正确 + + + index参数不正确 + + + value参数不正确或者超出范围 + + + param_num参数不正确 + + + 函数参数指针为空 + + + 超过限定的最大内存 + + + 回调函数出错 + + + 加密错误 + + + 算法库使用期限错误 + + + 参数范围不正确 + + + 数据大小不正确 + + + 数据step不正确 + + + cpu不支持优化代码中的指令集 + + + 警告 + + + 算法库超时 + + + 算法版本号出错 + + + 模型版本号出错 + + + GPU内存分配错误 + + + 文件不存在 + + + 字符串为空 + + + 图像解码器错误 + + + 打开文件错误 + + + 文件读取错误 + + + 文件写错误 + + + 文件读取大小错误 + + + 文件类型错误 + + + 模型类型错误 + + + 分配内存错误 + + + 线程绑核失败 + + + 噪声特性图像格式错误 + + + 噪声特性类型错误 + + + 噪声特性个数错误 + + + 噪声特性增益个数错误 + + + 噪声曲线增益值输入错误 + + + 噪声曲线柱数错误 + + + 噪声估计初始化增益设置错误 + + + 噪声估计未初始化 + + + 颜色空间模式错误 + + + 图像ROI个数错误 + + + 图像ROI原点错误 + + + 图像ROI大小错误 + + + 输入的相机增益不存在(增益个数已达上限) + + + 输入的相机增益不在范围内 + + + 输入的噪声特性内存大小错误 + + + + Constructor + + + + + Destructor + + + + + 设备句柄 + + + + + Initialize + + Success, return MV_OK. Failure, return error code + + + + Finalize + + Success, return MV_OK. Failure, return error code + + + + 枚举采集卡设备信息 + + 采集卡类型 + 设备信息 + Success, return MV_OK. Failure, return error code + + + + 创建采集卡设备句柄 + + 采集卡设备信息 + Success, return MV_OK. Failure, return error code + + + + 通过采集卡ID创建设备句柄 + + 采集卡ID + Success, return MV_OK. Failure, return error code + + + + 打开采集卡设备 + + 采集卡信息配置文件(目前不支持传配置文件) + + + + + 关闭采集卡 + + Success, return MV_OK. Failure, return error code + + + + 销毁采集卡句柄 + + Success, return MV_OK. Failure, return error code + + + + Get Camera Handle + + + + + + Get SDK Version + + Always return 4 Bytes of version number |Main |Sub |Rev |Test| + 8bits 8bits 8bits 8bits + + + + + Get supported Transport Layer + + Supported Transport Layer number + + + + Enumerate Device + + Enumerate TLs + Device List + Success, return MV_OK. Failure, return error code + + + + Enumerate device according to manufacture name + + Enumerate TLs + Device List + Manufacture Name + Success, return MV_OK. Failure, return error code + + + + Enumerate device according to the specified ordering + + Transmission layer of enumeration(All layer protocol type can input) + Device list + Manufacture Name + Sorting Method + Success, return MV_OK. Failure, return error code + + + + Is the device accessible + + Device Information + Access Right + Access, return true. Not access, return false + + + + Set SDK log path (Interfaces not recommended) + If the logging service MvLogServer is enabled, the interface is invalid and The logging service is enabled by default + + + + + + + Create Device + + Device Information + Success, return MV_OK. Failure, return error code + + + + Create Device without log + + Device Information + Success, return MV_OK. Failure, return error code + + + + Destroy Device + + Success, return MV_OK. Failure, return error code + + + + Open Device + + Success, return MV_OK. Failure, return error code + + + + Open Device + + Access Right + Switch key of access right + Success, return MV_OK. Failure, return error code + + + + Close Device + + Success, return MV_OK. Failure, return error code + + + + Is the device connected + + Connected, return true. Not Connected or DIsconnected, return false + + + + Register the image callback function + + Callback function pointer + User defined variable + Success, return MV_OK. Failure, return error code + + + + Register the RGB image callback function + + Callback function pointer + User defined variable + Success, return MV_OK. Failure, return error code + + + + Register the BGR image callback function + + Callback function pointer + User defined variable + Success, return MV_OK. Failure, return error code + + + + Start Grabbing + + Success, return MV_OK. Failure, return error code + + + + Stop Grabbing + + Success, return MV_OK. Failure, return error code + + + + Get one frame of RGB image, this function is using query to get data + query whether the internal cache has data, get data if there has, return error code if no data + + Image data receiving buffer + Buffer size + Image information + Waiting timeout + Success, return MV_OK. Failure, return error code + + + + Get one frame of BGR image, this function is using query to get data + query whether the internal cache has data, get data if there has, return error code if no data + + Image data receiving buffer + Buffer size + Image information + Waiting timeout + Success, return MV_OK. Failure, return error cod + + + + Get a frame of an image using an internal cache + + Image data and image information + Waiting timeout + Success, return MV_OK. Failure, return error code + + + + Free image buffer(used with MV_CC_GetImageBuffer) + + Image data and image information + Success, return MV_OK. Failure, return error code + + + + Get a frame of an image + + Image data receiving buffer + Buffer size + Image information + Waiting timeout + Success, return MV_OK. Failure, return error code + + + + Clear image Buffers to clear old data + + Success, return MV_OK. Failure, return error code + + + + Get the number of valid images in the current image buffer + + The number of valid images in the current image buffer + Success, return MV_OK. Failure, return error code + + + + Display one frame image + + Image information + Success, return MV_OK. Failure, return error code + + + + Display one frame image Ex + + dispaly Handle + Image information + Success, return MV_OK. Failure, return error code + + + + Set the number of the internal image cache nodes in SDK(Greater than or equal to 1, to be called before the capture) + + Number of cache nodes + Success, return MV_OK. Failure, return error code + + + + Set Grab Strategy + + The value of grab strategy + Success, return MV_OK. Failure, return error code + + + + Set The Size of Output Queue(Only work under the strategy of MV_GrabStrategy_LatestImages,rang:1-ImageNodeNum) + + The Size of Output Queue + Success, return MV_OK. Failure, return error code + + + + Get device information(Called before start grabbing) + + device information + Success, return MV_OK. Failure, return error code + + + + Get various type of information + + Various type of information + Success, return MV_OK. Failure, return error code + + + + Get Integer value + + Key value, for example, using "Width" to get width + Value of device features + Success, return MV_OK. Failure, return error code + + + + Set Integer value + + Key value, for example, using "Width" to set width + Feature value to set + Success, return MV_OK. Failure, return error code + + + + Get Enum value + + Key value, for example, using "PixelFormat" to get pixel format + Value of device features + Success, return MV_OK. Failure, return error code + + + + Set Enum value + + Key value, for example, using "PixelFormat" to set pixel format + Feature value to set + Success, return MV_OK. Failure, return error code + + + + Get the symbolic of the specified value of the Enum type node + + Key value, for example, using "PixelFormat" to set pixel format + Symbolic to get + Success, return MV_OK. Failure, return error code + + + + Set Enum value + + Key value, for example, using "PixelFormat" to set pixel format + Feature String to set + Success, return MV_OK. Failure, return error code + + + + Get Float value + + Key value + Value of device features + Success, return MV_OK. Failure, return error code + + + + Set float value + + Key value + Feature value to set + Success, return MV_OK. Failure, return error code + + + + Get Boolean value + + Key value + Value of device features + Success, return MV_OK. Failure, return error code + + + + Set Boolean value + + Key value + Feature value to set + Success, return MV_OK. Failure, return error code + + + + Get String value + + Key value + Value of device features + Success, return MV_OK. Failure, return error code + + + + Set String value + + Key value + Feature value to set + Success, return MV_OK. Failure, return error code + + + + Send Command + + Key value + Success, return MV_OK. Failure, return error code + + + + Read Memory + + Used as a return value, save the read-in memory value(Memory value is stored in accordance with the big end model) + Memory address to be read, which can be obtained from the Camera.xml file of the device, the form xml node value of xxx_RegAddr + Length of the memory to be read + Success, return MV_OK. Failure, return error code + + + + Write Memory + + Memory value to be written ( Note the memory value to be stored in accordance with the big end model) + Memory address to be written, which can be obtained from the Camera.xml file of the device, the form xml node value of xxx_RegAddr + Length of the memory to be written + Success, return MV_OK. Failure, return error code + + + + Invalidate GenICam Nodes + + Success, return MV_OK. Failure, return error code + + + + Get camera feature tree XML + + XML data receiving buffer + Buffer size + Actual data length + Success, return MV_OK. Failure, return error code + + + + Get Access mode of cur node + + Name of node + Access mode of the node + Success, return MV_OK. Failure, return error code + + + + Get Interface Type of cur node + + Name of node + Interface Type of the node + Success, return MV_OK. Failure, return error code + + + + Save camera feature + + File name + Success, return MV_OK. Failure, return error code + + + + Load camera feature + + File name + Success, return MV_OK. Failure, return error code + + + + Read the file from the camera + + File access structure + Success, return MV_OK. Failure, return error code + + + + Read the file from the camera + + File access structure + Success, return MV_OK. Failure, return error code + + + + Write the file to camera + + File access structure + Success, return MV_OK. Failure, return error code + + + + Write the file to camera + + File access structure + Success, return MV_OK. Failure, return error code + + + + Get File Access Progress + + File access Progress + Success, return MV_OK. Failure, return error code + + + + Device Local Upgrade + + File path and name + Success, return MV_OK. Failure, return error code + + + + Get Upgrade Progress + + Value of Progress + Success, return MV_OK. Failure, return error code + + + + Register Exception Message CallBack, call after open device + + Exception Message CallBack Function + User defined variable + Success, return MV_OK. Failure, return error code + + + + Register event callback, which is called after the device is opened + + Event CallBack Function + User defined variable + Success, return MV_OK. Failure, return error code + + + + Register single event callback, which is called after the device is opened + + Event name + Event CallBack Function + User defined variable + Success, return MV_OK. Failure, return error code + + + + Set enumerate device timeout + + time out,default 100ms + Success, return MV_OK. Failure, return error code + + + + Force IP + + IP to set + Subnet mask + Default gateway + Success, return MV_OK. Failure, return error code + + + + IP configuration method + + IP type, refer to MV_IP_CFG_x + Success, return MV_OK. Failure, return error code + + + + Set to use only one mode,type: MV_NET_TRANS_x. When do not set, priority is to use driver by default + + Net transmission mode, refer to MV_NET_TRANS_x + Success, return MV_OK. Failure, return error code + + + + Get net transmission information + + Transmission information + Success, return MV_OK. Failure, return error code + + + + Setting the ACK mode of devices Discovery + + ACK mode(Default-Broadcast),0-Unicast,1-Broadcast + Success, return MV_OK. Failure, return error code + + + + Set GVSP streaming timeout + + Timeout, default 300ms, range: >10ms + Success, return MV_OK. Failure, return error code + + + + Get GVSP streaming timeout + + Timeout, ms as unit + Success, return MV_OK. Failure, return error code + + + + Set GVCP cammand timeout + + Timeout, ms as unit, range: 0-10000 + Success, return MV_OK. Failure, return error code + + + + Get GVCP cammand timeout + + Timeout, ms as unit + Success, return MV_OK. Failure, return error code + + + + Set the number of retry GVCP cammand + + The number of retries,rang:0-100 + Success, return MV_OK. Failure, return error code + + + + Get the number of retry GVCP cammand + + The number of retries + Success, return MV_OK. Failure, return error code + + + + Get the optimal Packet Size, Only support GigE Camera + + Optimal packet size + + + + Set whethe to enable resend, and set resend + + Enable resend + Max resend persent + Resend timeout + Success, return MV_OK. Failure, return error code + + + + Set the max resend retry times + + The max times to retry resending lost packets,default 20 + Success, return MV_OK. Failure, return error code + + + + Get the max resend retry times + + the max times to retry resending lost packets + Success, return MV_OK. Failure, return error code + + + + Set time interval between same resend requests + + The time interval between same resend requests,default 10ms + Success, return MV_OK. Failure, return error code + + + + Get time interval between same resend requests + + The time interval between same resend requests + Success, return MV_OK. Failure, return error code + + + + Set transmission type,Unicast or Multicast + + Struct of transmission type + Success, return MV_OK. Failure, return error code + + + + Issue Action Command + + Action Command info + Action Command Result List + Success, return MV_OK. Failure, return error code + + + + Get Multicast Status + + Device Information + Status of Multicast + Success, return MV_OK. Failure, return error code + + + + Set device baudrate using one of the CL_BAUDRATE_XXXX value + + Baudrate to set. Refer to the 'CameraParams.h' for parameter definitions, for example, #define MV_CAML_BAUDRATE_9600 0x00000001 + Success, return MV_OK. Failure, return error code + + + + Get device baudrate, using one of the CL_BAUDRATE_XXXX value + + Return pointer of baud rate to user. + Refer to the 'CameraParams.h' for parameter definitions, for example, #define MV_CAML_BAUDRATE_9600 0x00000001 + Success, return MV_OK. Failure, return error code + + + + Get supported baudrates of the combined device and host interface + + Return pointer of the supported baudrates to user. 'OR' operation results of the supported baudrates. + Refer to the 'CameraParams.h' for single value definitions, for example, #define MV_CAML_BAUDRATE_9600 0x00000001 + Success, return MV_OK. Failure, return error code + + + + Sets the timeout for operations on the serial port + + Timeout in [ms] for operations on the serial port. + Success, return MV_OK. Failure, return error code + + + + Set transfer size of U3V device + + Transfer size,Byte,default:1M,rang:>=0x10000 + Success, return MV_OK. Failure, return error code + + + + Get transfer size of U3V device + + Transfer size,Byte + Success, return MV_OK. Failure, return error code + + + + Set transfer ways of U3V device + + Transfer ways,rang:1-10 + Success, return MV_OK. Failure, return error code + + + + Get transfer ways of U3V device + + Transfer ways + Success, return MV_OK. Failure, return error code + + + + Register Stream Exception Message CallBack + + Stream Exception Message CallBack Function + User defined variable + Success, return MV_OK. Failure, return error code + + + + Set the number of U3V device event cache nodes + + Event Node Number + Success, return MV_OK. Failure, return error code + + + + Set U3V Camera Synchronisation timeout + + Synchronisation time(ms), default 1000ms + Success, return MV_OK. Failure, return error code + + + + Get U3V Camera Synchronisation timeout + + Synchronisation time(ms), default 1000ms + Success, return MV_OK. Failure, return error code + + + + Enumerate interfaces by GenTL + + Interface information list + Path of GenTL's cti file + + + + + Unload cti library + + GenTL cti file path + Success, return MV_OK. Failure, return error code + + + + Enumerate Device Based On GenTL + + Interface information + Device List + Success, return MV_OK. Failure, return error code + + + + Create Device Handle Based On GenTL Device Info + + Device Information Structure + Success, return MV_OK. Failure, return error code + + + + Save image, support Bmp and Jpeg. + + Save image parameters structure + Success, return MV_OK. Failure, return error code + + + + Save the image file, support Bmp、 Jpeg、Png and Tiff. Encoding quality(50-99] + + Save the image file parameter structure + Success, return MV_OK. Failure, return error code + + + + Save 3D point data, support PLY、CSV and OBJ + + Save 3D point data parameters structure + Success, return MV_OK. Failure, return error code + + + + Rotate Image + + Rotate image parameter structure + Success, return MV_OK. Failure, return error code + + + + Flip Image + + Flip image parameter structure + Success, return MV_OK. Failure, return error code + + + + Pixel format conversion + + Convert Pixel Type parameter structure + Success, return MV_OK. Failure, return error code + + + + Interpolation algorithm type setting + + Bayer interpolation method 0-Fast 1-Equilibrium 2-Optimal + Success, return MV_OK. Failure, return error code + + + + Filter type of the bell interpolation quality algorithm setting + + Filter type enable + Success, return MV_OK. Failure, return error code + + + + Set Bayer Gamma value + + Gamma value[0.1,4.0] + Success, return MV_OK. Failure, return error code + + + + Set Mono8/Bayer Gamma value + + PixelType + Gamma value[0.1,4.0] + Success, return MV_OK. Failure, return error code + + + + Set Gamma param + + Gamma parameter structure + Success, return MV_OK. Failure, return error code + + + + Set CCM param + + CCM parameter structure + Success, return MV_OK. Failure, return error code + + + + Set CCM param + + CCM parameter structure + Success, return MV_OK. Failure, return error code + + + + Adjust image contrast + + Contrast parameter structure + Success, return MV_OK. Failure, return error code + + + + High Bandwidth Decode + + High Bandwidth Decode parameter structure + Success, return MV_OK. Failure, return error code + + + + Draw Rect Auxiliary Line + + Rect Auxiliary Line Info + Success, return MV_OK. Failure, return error code + + + + Draw Circle Auxiliary Line + + Circle Auxiliary Line Info + Success, return MV_OK. Failure, return error code + + + + Draw Line Auxiliary Line + + Linear Auxiliary Line Info + Success, return MV_OK. Failure, return error code + + + + Start Record + + Record param structure + Success, return MV_OK. Failure, return error code + + + + Input RAW data to Record + + Record data structure + Success, return MV_OK. Failure, return error code + + + + Stop Record + + Success, return MV_OK. Failure, return error code + + + + Open the GUI interface for getting or setting camera parameters + + Success, return MV_OK. Failure, return error code + + + + Reconstruct Image(For time-division exposure function) + + Reconstruct image parameters + Success, return MV_OK. Failure, return error code + + + + Byte array to struct + + Byte array + Struct type + Struct object + + + + 判断字符数组是否为utf-8 + + 字符数组 + + + + + Write Error Message + + Message + ErrorNum + + + + Save image, support Bmp and Jpeg. + + Save image parameters structure + Success, return MV_OK. Failure, return error code + + + + Save the image file, support Bmp、 Jpeg、Png and Tiff. Encoding quality(50-99] + + Save the image file parameter structure + Success, return MV_OK. Failure, return error code + + + + Pixel format conversion + + Convert Pixel Type parameter structure + Success, return MV_OK. Failure, return error code + + + + Get basic information of image (Interfaces not recommended) + + + + + + + Get GenICam proxy (Interfaces not recommended) + + + + + + Get root node (Interfaces not recommended) + + + + + + + Get all children node of specific node from xml, root node is Root (Interfaces not recommended) + + + + + + + + Get all children node of specific node from xml, root node is Root (Interfaces not recommended) + + + + + + + + Get current node feature (Interfaces not recommended) + + + + + + + + Update node (Interfaces not recommended) + + + + + + + + Register update callback (Interfaces not recommended) + + + + + + + + Noise estimate of Bayer format + + Noise estimate parameter structure + Success, return MV_OK. Failure, return error code + + + + Spatial Denoise of Bayer format + + Spatial Denoise parameter structure + Success, return MV_OK. Failure, return error code + + + + This interface is abandoned, it is recommended to use the MV_CC_DisplayOneFrame + + + + + + + This interface is abandoned, it is recommended to use the MV_CC_GetOneFrameTimeOut + + + + + + + + + This interface is abandoned, it is recommended to use the MV_CC_GetOneFrameTimeOut + + + + + + + + + This interface is abandoned, it is recommended to use the MV_CC_SaveImageEx + + + + + + + This interface is abandoned, it is recommended to use the MV_GIGE_ForceIpEx + + + + + + + This interface is abandoned, it is recommended to use the MV_CC_RegisterEventCallBackEx + + + + + + + + This interface is abandoned, it is recommended to use the MV_CC_GetIntValueEx + + + + + + + + This interface is abandoned, it is recommended to use the MV_CC_SetIntValueEx + + + + + + + + Set CLUT param + + CLUT parameter structure + Success, return MV_OK. Failure, return error code + + + + Image sharpen + + Sharpen parameter structure + Success, return MV_OK. Failure, return error code + + + + Color Correct(include CCM and CLUT) + + Color Correct parameter structure + Success, return MV_OK. Failure, return error code + + + + Noise Estimate + + Noise Estimate parameter structure + Success, return MV_OK. Failure, return error code + + + + Spatial Denoise + + Spatial Denoise parameter structure + Success, return MV_OK. Failure, return error code + + + + LSC Calib + + LSC Calib parameter structure + Success, return MV_OK. Failure, return error code + + + + LSC Correct + + LSC Correct parameter structure + Success, return MV_OK. Failure, return error code + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + Set PixelFormat + + PixelFormat + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + Get Trigger Source + + Trigger Source + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is replaced by general interface + + + + + + + This interface is abandoned, it is recommended to use the MV_CC_RegisterImageCallBackEx + + + + + + + + Grab callback + + Image data + Frame info + User defined variable + + + + Grab callback + + Image data + Frame info + User defined variable + + + + Xml Update callback(Interfaces not recommended) + + Node type + Current node feature structure + Nodes list + User defined variable + + + + Exception callback + + Msg type + User defined variable + + + + Event callback (Interfaces not recommended) + + User defined ID + User defined variable + + + + Event callback + + Event Info + User defined variable + + + + Stream Exception callback + + Msg type + User defined variable + + + + ch:采集卡信息列表 | en: Interface Information List + + + + + ch:在线设备数量 | en:Online Interface Number + + + + + ch:支持最多64个设备 | en:Support up to 64 Interfaces + + + + + ch:采集卡信息 | en: Interface information + + + + + ch: 采集卡类型; 低16位有效: bits(0~2)代表功能, bits(3~7)代表相机, bits(8-15)代表总线| en: Interface type + + + + + ch: 采集卡的PCIE插槽信息 | en: PCIe slot information of interface + + + + + ch: 采集卡ID | en: Interface ID + + + + + ch 显示名称 | en: Display name + + + + + ch 序列号 |en: Serial number + + + + + ch 型号 | en: model name + + + + + ch: 厂商 |en: manufacturer name + + + + + ch: 版本号| en: device version + + + + + ch: 自定义名称 |en: user defined name + + + + + ch 保留字段 | en Reserved + + + + + 排序方式 + + + + + 按序列号排序 + + + + + 按用户自定义名字排序 + + + + + 按当前IP地址排序(升序) + + + + + 按当前IP地址排序(降序) + + + + + ch: GigE设备信息 | en: GigE device information + + + + + IP 配置选项 + + + + + IP configuration:bit31-static bit30-dhcp bit29-lla + + + + + curtent ip + + + + + curtent subnet mask + + + + + current gateway + + + + + 制造商名 + + + + + 型号名 + + + + + 设备版本信息 + + + + + 制造商特殊信息 + + + + + 序列号 + + + + + 用户自定义名 + + + + + 网口IP地址 + + + + + 预留 + + + + + ch: GigE设备信息 | en: GigE device information + + + + + IP 配置选项 + + + + + IP configuration:bit31-static bit30-dhcp bit29-lla + + + + + curtent ip + + + + + curtent subnet mask + + + + + current gateway + + + + + 制造商名 + + + + + 型号名 + + + + + 设备版本信息 + + + + + 制造商特殊信息 + + + + + 序列号 + + + + + 用户自定义名 + + + + + 网口IP地址 + + + + + 预留 + + + + + ch:USB3 设备信息 | en:USB3 device information + + + + + 控制输入端点 + + + + + 控制输出端点 + + + + + 流端点 + + + + + 事件端点 + + + + + 供应商ID号 + + + + + 产品ID号 + + + + + 设备索引号 + + + + + 设备GUID号 + + + + + 供应商名字 + + + + + 型号名字 + + + + + 家族名字 + + + + + 设备版本号 + + + + + 制造商名字 + + + + + 序列号 + + + + + 用户自定义名字 + + + + + 支持的USB协议 + + + + + 保留字节 + + + + + ch:USB3 设备信息 | en:USB3 device information + + + + + 控制输入端点 + + + + + 控制输出端点 + + + + + 流端点 + + + + + 事件端点 + + + + + 供应商ID号 + + + + + 产品ID号 + + + + + 设备索引号 + + + + + 设备GUID号 + + + + + 供应商名字 + + + + + 型号名字 + + + + + 家族名字 + + + + + 设备版本号 + + + + + 制造商名字 + + + + + 序列号 + + + + + 用户自定义名字 + + + + + 支持的USB协议 + + + + + 保留字节 + + + + + ch:CamLink设备信息 | en:CamLink device information + + + + + 端口号ID + + + + + 模型名 + + + + + 家族名 + + + + + 设备版本信息 + + + + + 制造商名字 + + + + + 序列号 + + + + + 保留字节 + + + + + ch:采集卡Camera Link相机信息 | en:Camera Link device information on frame grabber + + + + + ch 采集卡ID |en Interface ID of Frame Grabber + + + + + ch 供应商名字 |en Vendor name + + + + + ch 型号名字 |en Model name + + + + + ch 厂商信息 |en Manufacturer information + + + + + ch 相机版本 |en Device version + + + + + ch 序列号 |en Serial number + + + + + ch 用户自定义名字 |en User defined name + + + + + ch 相机ID |en Device ID + + + + + ch 保留字段 |en Reserved + + + + + ch:CoaXPress相机信息 | en:CoaXPress device information + + + + + ch 采集卡ID |en Interface ID of Frame Grabber + + + + + ch 供应商名字 |en Vendor name + + + + + ch 型号名字 |en Model name + + + + + ch 厂商信息 |en Manufacturer information + + + + + ch 相机版本 |en Device version + + + + + ch 序列号 |en Serial number + + + + + ch 用户自定义名字 |en User defined name + + + + + ch 相机ID |en Device ID + + + + + ch 保留字段 |en Reserved + + + + + ch:XoFLink相机信息 | en:XoFLink device information + + + + + ch 采集卡ID |en Interface ID of Frame Grabber + + + + + ch 供应商名字 |en Vendor name + + + + + ch 型号名字 |en Model name + + + + + ch 厂商信息 |en Manufacturer information + + + + + ch 相机版本 |en Device version + + + + + ch 序列号 |en Serial number + + + + + ch 用户自定义名字 |en User defined name + + + + + ch 相机ID |en Device ID + + + + + ch 保留字段 |en Reserved + + + + + ch:设备信息 | en:Device information + + + + + 主版本号 + + + + + 次版本号 + + + + + MAC高地址 + + + + + MAC低地址 + + + + + 设备传输层协议类型,e.g. MV_GIGE_DEVICE + + + + + ch 设备类型信息 | en Device Type Info + + + + + 保留字节 + + + + + 设备类型 + + + + + 构造函数 + + 输入任意数,因为不接受无参构造函数 + + + + ch:特定类型的设备信息 | en:Special devcie information + + + + + GigE + + + + + Camera Link + + + + + Usb + + + + + CML + + + + + CXP + + + + + XOF + + + + + 相机列表 + + + + + 在线设备数量 + + + + + 支持最多256个设备 + + + + + ch:通过GenTL枚举到的Interface信息 | en:Interface Information with GenTL + + + + + GenTL接口ID + + + + + 传输层类型 + + + + + 设备显示名称 + + + + + GenTL的cti文件索引 + + + + + 保留字节 + + + + + ch:通过GenTL枚举到的设备信息列表 | en:Interface Information List with GenTL + + + + + ch:在线设备数量 | en:Online Interface Number + + + + + ch:支持最多256个设备 | en:Support up to 256 Interfaces + + + + + ch:通过GenTL枚举到的设备信息 | en:Device Information discovered by with GenTL + + + + + 采集卡ID + + + + + 设备ID + + + + + 供应商名字 + + + + + 模型名 + + + + + 传输类型 + + + + + 显示名 + + + + + 用户自定义名 + + + + + 序列号 + + + + + 设备版本信息 + + + + + cti文件序号 + + + + + 保留字节 + + + + + ch:通过GenTL枚举到的设备信息 | en:Device Information discovered by with GenTL + + + + + 采集卡ID + + + + + 设备ID + + + + + 供应商名字 + + + + + 模型名 + + + + + 传输类型 + + + + + 显示名 + + + + + 用户自定义名 + + + + + 序列号 + + + + + 设备版本信息 + + + + + cti文件序号 + + + + + 保留字节 + + + + + ch:GenTL设备列表 | en:GenTL devices list + + + + + 在线设备数量 + + + + + 支持最多256个设备 + + + + + Net Trans Info + + + + + 已接收数据大小 [统计StartGrabbing和StopGrabbing之间的数据量] + + + + + 丢帧数量 + + + + + 接收帧数 + + + + + 请求重发包数 + + + + + 重发包数 + + + + + Frame Out Info + + + + + 图像宽 + + + + + 图像高 + + + + + 像素格式 + + + + + 帧号 + + + + + 时间戳高32位 + + + + + 时间戳低32位 + + + + + 保留,8字节对齐 + + + + + 主机生成的时间戳 + + + + + 帧数据大小 + + + + + 丢包数量 + + + + + 保留字节 + + + + + Chunk数据信息 + + + + + Chunk数据 + + + + + ChunkID + + + + + Chunk大小 + + + + + 保留字节 + + + + + Frame Out Info Ex + + + + + 图像宽 + + + + + 图像高 + + + + + 像素格式 + + + + + 帧号 + + + + + 时间戳高32位 + + + + + 时间戳低32位 + + + + + 保留,8字节对齐 + + + + + 主机生成的时间戳 + + + + + Frame大小 + + + + + 秒数 + + + + + 周期数 + + + + + 周期偏移量 + + + + + 增益 + + + + + 曝光时间 + + + + + 平均亮度 + + + + + Red + + + + + Green + + + + + Blue + + + + + 帧计数器 + + + + + 触发计数 + + + + + 输入 + + + + + 输出 + + + + + 水平偏移量 + + + + + 垂直偏移量 + + + + + Chunk宽度 + + + + + Chunk高度 + + + + + 丢包数 + + + + + 为解析的Chunk数量 + + + + + 为解析的Chunk列表 + + + + + 图像宽扩展 + + + + + 图像高扩展 + + + + + 保留字节 + + + + + 为解析的Chunk列表 + + + + + 为解析的Chunk内容 + + + + + 对齐结构体,无实际用途 + + + + + 输出帧信息 + + + + + 帧数据地址 + + + + + 帧信息 + + + + + 保留字节 + + + + + 取流策略 + + + + + 从旧到新一帧一帧的获取图像(默认为该策略) + + + + + 获取列表中最新的一帧图像(同时清除列表中的其余图像) + + + + + 获取列表中最新的图像,个数由OutputQueueSize决定,范围为1-ImageNodeNum,设置成1等同于LatestImagesOnly,设置成ImageNodeNum等同于OneByOne + + + + + 等待下一帧图像 + + + + + 显示帧信息 + + + + + 显示窗口的句柄 + + + + + 显示的帧数据 + + + + + 显示的帧数据大小 + + + + + 图像宽 + + + + + 图像高 + + + + + 像素格式 + + + + + 保留字节 + + + + + 显示帧信息 + + + + + 图像宽 + + + + + 图像高 + + + + + 像素格式 + + + + + 显示的帧数据 + + + + + 显示的帧数据大小 + + + + + 保留字节 + + + + + ch:保存3D数据格式 | en:Save 3D file + + + + + 未定义数据格式 + + + + + PLY数据格式 + + + + + CSV数据格式 + + + + + OBJ数据格式 + + + + + 保存的点阵参数 + + + + + [IN] 每一行点的数量 + + + + + [IN] 行数 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [OUT] 输出像素数据缓存 + + + + + [IN] 提供的输出缓冲区大小(nLinePntNum * nLineNum * (16*3 + 4) + 2048) + + + + + [OUT] 输出像素数据缓存长度 + + + + + 保存的点阵文件类型 + + + + + 保留字节 + + + + + 保存的图像格式 + + + + + 未定义类型 + + + + + Bmp图像格式 + + + + + Jpeg图像格式 + + + + + Png图像格式 + + + + + Tif图像格式 + + + + + 保存的图像参数 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [OUT] 输出图片缓存 + + + + + [OUT] 输出图片大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [IN] 输出图片格式 + + + + + 保存的图像参数 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [OUT] 输出图片缓存 + + + + + [OUT] 输出图片大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [IN] 输出图片格式 + + + + + [IN] 编码质量, (50-99] + + + + + [IN] Bayer的插值方法 0-快速 1-均衡 2-最优(如果传入其它值则默认为最优) + + + + + 保留字节 + + + + + 保存的图像信息扩展 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [OUT] 输出图片缓存 + + + + + [OUT] 输出图片大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [IN] 输出图片格式 + + + + + [IN] 编码质量, (50-99] + + + + + [IN] Bayer的插值方法 0-快速 1-均衡 2-最优(如果传入其它值则默认为最优) + + + + + 保留字节 + + + + + 保存图像到文件的参数 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [IN] 输入图片格式 + + + + + [IN] 编码质量, (0-100] + + + + + [IN] 输入文件路径 + + + + + [IN] Bayer的插值方法 0-快速 1-均衡 2-最优(如果传入其它值则默认为最优) + + + + + 保留字节 + + + + + 保存图像到文件信息扩展 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 输入图片格式 + + + + + [IN] 输入文件路径 + + + + + [IN] 编码质量, (0-100] + + + + + [IN] Bayer的插值方法 0-快速 1-均衡 2-最优(如果传入其它值则默认为最优) + + + + + 保留字节 + + + + + 旋转角度 + + + + + 旋转90度 + + + + + 旋转180度 + + + + + 旋转270度 + + + + + 旋转图像参数 + + + + + [IN] 像素格式(仅支持Mono8/RGB24/BGR24) + + + + + [IN][OUT] 图像宽 + + + + + [IN][OUT] 图像高 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [OUT] 输出图片缓存 + + + + + [OUT] 输出图片大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [IN] 旋转角度 + + + + + 保留字节 + + + + + 图像翻转类型 + + + + + 垂直方向翻转 + + + + + 水平方向翻转 + + + + + 翻转图像参数 + + + + + [IN] 像素格式(仅支持Mono8/RGB24/BGR24) + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [OUT] 输出图片缓存 + + + + + [OUT] 输出图片大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [IN] 翻转类型 + + + + + 保留字节 + + + + + 像素转换参数 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [IN] 源像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 目标像素格式 + + + + + [OUT] 输出数据缓存 + + + + + [OUT] 输出数据大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + 保留字节 + + + + + 图像像素转换信息扩展 + + + + + [IN] 图像宽 + + + + + [IN] 图像高 + + + + + [IN] 源像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 目标像素格式 + + + + + [OUT] 输出数据缓存 + + + + + [OUT] 输出数据大小 + + + + + [IN] 提供的输出缓冲区大小 + + + + + 保留字节 + + + + + Gamma类型 + + + + + 不启用 + + + + + GAMMA值 + + + + + GAMMA曲线,8位需要的长度:256*sizeof(unsigned char) + 10位需要的长度:1024*sizeof(unsigned short) + 12位需要的长度:4096*sizeof(unsigned short) + 16位需要的长度:65536*sizeof(unsigned short) + + + + + 线性RGB转非线性RGB + + + + + 非线性RGB转线性RGB + + + + + Gamma参数 + + + + + [IN] Gamma类型 + + + + + [IN] Gamma值 + + + + + [IN] Gamma曲线缓存 + + + + + [IN] Gamma曲线长度 + + + + + 保留字节 + + + + + CCM参数 + + + + + [IN] 是否启用CCM + + + + + [IN] CCM矩阵(-8192~8192) + + + + + 保留字节 + + + + + CCM参数 + + + + + [IN] 是否启用CCM + + + + + [IN] 量化3x3矩阵 + + + + + [IN] 量化系数(2的整数幂) + + + + + 保留字节 + + + + + CLUT参数 + + + + + [IN] 是否启用CLUT + + + + + [IN] 量化系数(2的整数幂) + + + + + [IN] CLUT大小,建议值17 + + + + + [OUT] 量化CLUT + + + + + [IN] 量化CLUT缓存大小(nCLUTSize*nCLUTSize*nCLUTSize*sizeof(int)*3) + + + + + 保留字节 + + + + + 对比度调节参数 + + + + + [IN] 图像宽度(最小8) + + + + + [IN] 图像高度(最小8) + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [IN] 输入的像素格式 + + + + + [OUT] 输出像素数据缓存 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出像素数据缓存长度 + + + + + [IN] 对比度值,范围:[1, 10000] + + + + + 保留字节 + + + + + 锐化参数 + + + + + [IN] 图像宽度(最小8) + + + + + [IN] 图像高度(最小8) + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [IN] 输入的像素格式 + + + + + [OUT] 输出像素数据缓存 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出像素数据缓存长度 + + + + + [IN] 锐度调节强度,范围:[0, 500] + + + + + [IN] 锐度调节半径(半径越大,耗时越长),范围:[1, 21] + + + + + [IN] 锐度调节阈值,范围:[0, 255] + + + + + 保留字节 + + + + + 色彩校正参数(包括CCM和CLUT) + + + + + [IN] 图像宽度 + + + + + [IN] 图像高度 + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [IN] 输入的像素格式 + + + + + [OUT] 输出像素数据缓存 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出像素数据缓存长度 + + + + + [IN] 输入有效图像位数,8 or 10 or 12 or 16 + + + + + [IN] 输入Gamma信息 + + + + + [IN] 输入CCM信息 + + + + + [IN] 输入CLUT信息 + + + + + 保留字节 + + + + + 矩形ROI参数 + + + + + [IN] 矩形左上角X轴坐标 + + + + + [IN] 矩形左上角Y轴坐标 + + + + + [IN] 矩形宽度 + + + + + [IN] 矩形高度 + + + + + 噪声估计参数 + + + + + [IN] 图像宽度 + + + + + [IN] 图像高度 + + + + + [IN] 输入的像素格式 + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [IN] 图像ROI + + + + + [IN] ROI个数 + + + + + [IN] 噪声阈值[0-4095] + + + + + [OUT] 输出噪声特性 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出噪声特性长度 + + + + + 保留字节 + + + + + 空域降噪参数 + + + + + [IN] 图像宽度 + + + + + [IN] 图像高度 + + + + + [IN] 输入的像素格式 + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [OUT] 输出降噪后的数据 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出降噪后的数据长度 + + + + + [IN] 输入噪声特性 + + + + + [IN] 输入噪声特性长度 + + + + + [IN] 降噪强度(0-100) + + + + + [IN] 锐化强度(0-32) + + + + + [IN] 噪声校正系数(0-1280) + + + + + [IN] 亮度校正系数(1-2000) + + + + + [IN] 色调校正系数(1-2000) + + + + + [IN] 亮度降噪强度(0-100) + + + + + [IN] 色调降噪强度(0-100) + + + + + [IN] 锐化强度(1-1000) + + + + + 保留字节 + + + + + LSC标定参数 + + + + + [IN] 图像宽度(16~65536) + + + + + [IN] 图像高度(16~65536) + + + + + [IN] 输入的像素格式 + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [OUT] 输出标定表缓存 + + + + + [IN] 提供的标定表缓冲大小(nWidth*nHeight*sizeof(unsigned short)) + + + + + [OUT] 输出标定表缓存长度 + + + + + [IN] 宽度分块数 + + + + + [IN] 高度分块数 + + + + + [IN] 边缘填充系数,范围1~5 + + + + + [IN] 标定方式,0-中心为基准 + 1-最亮区域为基准 + 2-目标亮度 + + + + + [IN] 目标亮度(8bits,[0,255]) + (10bits,[0,1023]) + (12bits,[0,4095]) + (16bits,[0,65535]) + + + + + 保留字节 + + + + + LSC校正参数 + + + + + [IN] 图像宽度(16~65536) + + + + + [IN] 图像高度(16~65536) + + + + + [IN] 输入的像素格式 + + + + + [IN] 输入图像缓存 + + + + + [IN] 输入图像缓存长度 + + + + + [OUT] 输出像素数据缓存 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出像素数据缓存长度 + + + + + [IN] 输入校正表缓存 + + + + + [IN] 输入校正表缓存长度 + + + + + 保留字节 + + + + + 噪声特性类型 + + + + + 无效 + + + + + 噪声曲线 + + + + + 噪声水平 + + + + + 默认值 + + + + + 噪声基本信息 + + + + + 版本 + + + + + 噪声特性类型 + + + + + 图像格式 + + + + + 平均噪声水平 + + + + + 曲线点数 + + + + + 噪声曲线 + + + + + 亮度曲线 + + + + + 保留字节 + + + + + 噪声估计参数 + + + + + [IN] 图像宽(大于等于8) + + + + + [IN] 图像高(大于等于8) + + + + + [IN] 像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [IN] 噪声阈值(0-4095) + + + + + [IN] 用于存储噪声曲线和亮度曲线(需要外部分配,缓存大小:4096 * sizeof(int) * 2) + + + + + [OUT] 降噪特性信息 + + + + + [IN] 线程数量,0表示算法库根据硬件自适应;1表示单线程(默认);大于1表示线程数目 + + + + + 保留字节 + + + + + 降噪参数 + + + + + [IN] 图像宽(大于等于8) + + + + + [IN] 图像高(大于等于8) + + + + + [IN] 像素格式 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [OUT] 输出降噪后的数据 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出降噪后的数据长度 + + + + + [IN] 降噪特性信息(来源于噪声估计) + + + + + [IN] 降噪强度(0-100) + + + + + [IN] 锐化强度(0-32) + + + + + [IN] 噪声校正系数(0-1280) + + + + + [IN] 线程数量,0表示算法库根据硬件自适应;1表示单线程(默认);大于1表示线程数目 + + + + + 保留字节 + + + + + 帧特殊信息 + + + + + [OUT] 秒数 + + + + + [OUT] 周期数 + + + + + [OUT] 周期偏移量 + + + + + [OUT] 增益 + + + + + [OUT] 曝光时间 + + + + + [OUT] 平均亮度 + + + + + [OUT] 红色 + + + + + [OUT] 绿色 + + + + + [OUT] 蓝色 + + + + + [OUT] 总帧数 + + + + + [OUT] 触发计数 + + + + + [OUT] 输入 + + + + + [OUT] 输出 + + + + + [OUT] 水平偏移量 + + + + + [OUT] 垂直偏移量 + + + + + [OUT] 水印宽 + + + + + [OUT] 水印高 + + + + + 保留字节 + + + + + HB解码参数 + + + + + [IN] 输入数据缓存 + + + + + [IN] 输入数据大小 + + + + + [OUT] 图像宽 + + + + + [OUT] 图像高 + + + + + [OUT] 输出数据缓存 + + + + + [IN] 提供的输出缓冲区大小 + + + + + [OUT] 输出数据大小 + + + + + [OUT] 输出的像素格式 + + + + + [OUT] 水印信息 + + + + + 保留字节 + + + + + 录像格式定义 + + + + + 未定义格式 + + + + + AVI格式 + + + + + 录像参数 + + + + + [IN] 输入数据的像素格式 + + + + + [IN] 图像宽(指定目标参数时需为8的倍数) + + + + + [IN] 图像高(指定目标参数时需为8的倍数) + + + + + [IN] 帧率fps(大于1/16) + + + + + [IN] 码率kbps(128kbps-16Mbps) + + + + + [IN] 录像格式 + + + + + [IN] 录像文件存放路径 + + + + + 保留字节 + + + + + 输入帧信息 + + + + + [IN] 图像数据指针 + + + + + [IN] 图像大小 + + + + + 保留字节 + + + + + 采集模式 + + + + + 单帧模式 + + + + + 多帧模式 + + + + + 持续采集模式 + + + + + 增益模式 + + + + + 关闭 + + + + + 一次 + + + + + 连续 + + + + + 曝光模式 + + + + + Timed + + + + + TriggerWidth + + + + + 自动曝光模式 + + + + + 关闭 + + + + + 一次 + + + + + 连续 + + + + + 相机触发模式 + + + + + 关闭 + + + + + 打开 + + + + + Gamma选择器 + + + + + USER + + + + + SRGB + + + + + 自动白平衡 + + + + + 关闭自动白平衡 + + + + + 一次自动白平衡 + + + + + 连续自动白平衡 + + + + + 触发源 + + + + + LINE0 + + + + + LINE1 + + + + + LINE2 + + + + + LINE3 + + + + + COUNTER0 + + + + + SOFTWARE + + + + + FrequencyConverter + + + + + ALL MATHCH INFO + + + + + 需要输出的信息类型,e.g. MV_MATCH_TYPE_NET_DETECT + + + + + 输出的信息缓存,由调用者分配 + + + + + 信息缓存的大小 + + + + + + + + + + 已接收数据大小 [统计StartGrabbing和StopGrabbing之间的数据量] + + + + + 丢失的包数量 + + + + + 丢帧数量 + + + + + 帧数 + + + + + 请求重发包数 + + + + + 重发包数 + + + + + USB + + + + + 已接收数据大小 [统计OpenDevicce和CloseDevice之间的数据量] + + + + + 已收到的帧数 + + + + + 错误帧数 + + + + + 保留字节 + + + + + 图像的基本信息 + + + + + 宽度值 + + + + + 宽度最小值 + + + + + 宽度最大值 + + + + + Width Inc + + + + + 高度值 + + + + + 高度最小值 + + + + + 高度最大值 + + + + + Height Inc + + + + + 帧率 + + + + + 最小帧率 + + + + + 最大帧率 + + + + + 当前的像素格式 + + + + + 支持的像素格式种类 + + + + + 像素列表 + + + + + 保留字节 + + + + + 节点是否可见的权限等级 + + + + + Always visible + + + + + Visible for experts or Gurus + + + + + Visible for Gurus + + + + + Not Visible + + + + + Object is not yet initialized + + + + + 事件信息 + + + + + 事件名 + + + + + Event号 + + + + + 流通到序号 + + + + + 帧号高位 + + + + + 帧号低位 + + + + + 时间戳高位 + + + + + 时间戳低位 + + + + + Event数据 + + + + + Event数据长度 + + + + + 保留字节 + + + + + 文件存取 + + + + + 用户文件名 + + + + + 设备文件名 + + + + + 保留字节 + + + + + 文件存取 + + + + + 用户文件数据缓存空间 + + + + + 用户数据缓存大小 + + + + + 文件实际缓存大小 + + + + + 设备文件名 + + + + + 保留字节 + + + + + 文件存取进度 + + + + + 已完成的长度 + + + + + 总长度 + + + + + 保留字节 + + + + + GigE传输类型 + + + + + 表示单播(默认) + + + + + 表示组播 + + + + + 表示局域网内广播,暂不支持 + + + + + 表示子网内广播,暂不支持 + + + + + 表示从相机获取,暂不支持 + + + + + 表示用户自定义应用端接收图像数据Port号 + + + + + 表示设置了单播,但本实例不接收图像数据 + + + + + 表示组播模式,但本实例不接收图像数据 + + + + + 传输模式,可以为单播模式、组播模式等 + + + + + 传输模式 + + + + + 目标IP,组播模式下有意义 + + + + + 目标Port,组播模式下有意义 + + + + + 保留字节 + + + + + 动作命令信息 + + + + + 设备密钥 + + + + + 组键 + + + + + 组掩码 + + + + + 只有设置成1时Action Time才有效,非1时无效 + + + + + 预定的时间,和主频有关 + + + + + 广播包地址 + + + + + 等待ACK的超时时间,如果为0表示不需要ACK + + + + + 保留字节 + + + + + 动作命令结果 + + + + + IP address of the device + + + + + status code returned by the device + + + + + 保留字节 + + + + + 动作命令结果列表 + + + + + 返回值个数 + + + + + 返回的结果 + + + + + 每个节点对应的接口类型 + + + + + IValue接口类型 + + + + + IBase接口类型 + + + + + IInteger接口类型 + + + + + IBoolean接口类型 + + + + + ICommand接口类型 + + + + + IFloat接口类型 + + + + + IString接口类型 + + + + + IRegister接口类型 + + + + + ICategory接口类型 + + + + + IEnumeration接口类型 + + + + + IEnumEntry接口类型 + + + + + IPort接口类型 + + + + + XML节点特点 + + + + + 节点类型 + + + + + 是否可见 + + + + + 节点描述 + + + + + 显示名称 + + + + + 节点名 + + + + + 提示 + + + + + 保留字节 + + + + + XML节点列表 + + + + + 节点个数 + + + + + 节点列表 + + + + + 整型节点值 + + + + + 当前值 + + + + + 最大值 + + + + + 最小值 + + + + + Inc + + + + + 保留字节 + + + + + 整型节点值 + + + + + 当前值 + + + + + 最大值 + + + + + 最小值 + + + + + Inc + + + + + 保留字节 + + + + + 浮点型节点值 + + + + + 当前值 + + + + + 最大值 + + + + + 最小值 + + + + + 保留字节 + + + + + 枚举型节点值 + + + + + 当前值 + + + + + 有效数据个数 + + + + + 保留字节 + + + + + 保留字节 + + + + + 字符串型节点值 + + + + + 当前值 + + + + + 节点值的最大长度 + + + + + 保留字节 + + + + + 节点的读写性 + + + + + 未实现 + + + + + 不可获取 + + + + + 只写 + + + + + 只读 + + + + + 可读可写 + + + + + 未定义 + + + + + 内部用于AccessMode循环检测 + + + + + 整型节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + 是否可见 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 当前值 + + + + + 最小值 + + + + + 最大值 + + + + + 增量 + + + + + 保留字节 + + + + + 布尔型节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + 是否可见 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 当前值 + + + + + 保留字节 + + + + + 命令型节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + 是否可见 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 保留字节 + + + + + 浮点型节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + 是否可见 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 当前值 + + + + + 最小值 + + + + + 最大值 + + + + + 增量 + + + + + 保留字节 + + + + + 字符串类型节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + 是否可见 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 当前值 + + + + + 保留字节 + + + + + 寄存器型节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + 是否可见 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 当前值 + + + + + 保留字节 + + + + + 类别属性 + + + + + 节点描述 + + + + + 显示名称 + + + + + 节点名 + + + + + 提示 + + + + + 是否可见 + + + + + 保留字节 + + + + + EnumEntry属性节点 + + + + + 节点名 + + + + + 显示名称 + + + + + 节点描述 + + + + + 提示 + + + + + + + + + + 父节点数 + + + + + 父节点列表 + + + + + 是否可见 + + + + + 当前值 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 保留字节 + + + + + 节点描述 + + + + + 节点描述 + + + + + Enumeration属性节点 + + + + + 是否可见 + + + + + 节点描述 + + + + + 显示名称 + + + + + 节点名 + + + + + 提示 + + + + + Symbolic数 + + + + + 当前Symbolic索引 + + + + + Symbolic索引 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 当前值 + + + + + 保留字节 + + + + + Port属性节点 + + + + + 是否可见 + + + + + 节点描述 + + + + + 显示名称 + + + + + 节点名 + + + + + 提示 + + + + + 访问模式 + + + + + 是否锁定。0-否;1-是 + + + + + 保留字节 + + + + 辅助线颜色 + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + 预留字节 + + + 自定义点坐标 + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + 预留字节 + + + 矩形框区域信息 + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + [0.0 , 1.0] + + + 辅助线颜色 + + + 辅助线宽度 + + + 预留字节 + + + 圆形框区域信息 + + + 圆心信息 + + + 宽向半径,根据图像的相对位置[0, 1.0] + + + 高向半径,根据图像的相对位置[0, 1.0] + + + 辅助线颜色信息 + + + 辅助线宽度 + + + 预留字节 + + + 线条辅助线信息 + + + 线条辅助线的起始点坐标 + + + 线条辅助线的终点坐标 + + + 辅助线颜色信息 + + + 辅助线宽度 + + + 预留字节 + + + 枚举类型指定条目信息 + + + 指定值 + + + 指定值对应的符号 + + + 预留字节 + + + U3V流异常类型 + + + 异常的图像,该帧被丢弃 + + + 缓存列表溢出,清除最旧的一帧 + + + 缓存列表为空,该帧被丢弃 + + + 断流恢复 + + + 断流,恢复失败,取流被中止 + + + 设备异常,取流被中止 + + + 重构后的图像列表 + + + 源图像宽 + + + 源图像高 + + + 像素格式 + + + 输出数据缓存 + + + 输出数据长度 + + + 提供的输出缓冲区大小 + + + 预留字节 + + + + 分时曝光的图像处理方式 + + + + + 源图像按行拆分成多张图像 + + + + 重构图像参数信息 + + + 源图像宽 + + + 源图像高 + + + 像素格式 + + + 输入数据缓存 + + + 输入数据长度 + + + 曝光个数(1-8] + + + 图像重构方式 + + + + 输出数据缓存信息 + + + + 预留字节 + + + + 像素格式定义 + + + + + 未定义像素格式 + + + + + Mono1p + + + + + Mono2p + + + + + Mono4p + + + + + Mono8 + + + + + Mono8_Signed + + + + + Mono10 + + + + + Mono10_Packed + + + + + Mono12 + + + + + Mono12_Packed + + + + + Mono14 + + + + + Mono16 + + + + + BayerGR8 + + + + + BayerRG8 + + + + + BayerGB8 + + + + + BayerBG8 + + + + + BayerRBGG8 + + + + + BayerGR10 + + + + + BayerRG10 + + + + + BayerGB10 + + + + + BayerBG10 + + + + + BayerGR12 + + + + + BayerRG12 + + + + + BayerGB12 + + + + + BayerBG12 + + + + + BayerGR10_Packed + + + + + BayerRG10_Packed + + + + + BayerGB10_Packed + + + + + BayerBG10_Packed + + + + + BayerGR12_Packed + + + + BayerRG12_Packed + + + BayerGB12_Packed + + + BayerBG12_Packed + + + BayerGR16 + + + BayerRG16 + + + BayerGB16 + + + BayerBG16 + + + RGB8_Packed + + + BGR8_Packed + + + RGBA8_Packed + + + BGRA8_Packed + + + RGB10_Packed + + + BGR10_Packed + + + RGB12_Packed + + + BGR12_Packed + + + RGB16_Packed + + + BGR16_Packed/// + + + RGBA16_Packed + + + BGRA16_Packed + + + RGB10V1_Packe + + + RGB10V2_Packed + + + RGB12V1_Packed + + + RGB565_Packed + + + BGR565_Packed + + + YUV411_Packed + + + YUV422_Packed + + + YUV422_YUYV_Packed + + + YUV444_Packed + + + YCBCR8_CBYCR + + + YCBCR422_8 + + + YCBCR422_8_CBYCRY + + + YCBCR411_8_CBYYCRYY + + + YCBCR601_8_CBYCR + + + YCBCR601_422_8 + + + YCBCR601_422_8_CBYCRY + + + YCBCR601_411_8_CBYYCRYY + + + YCBCR709_8_CBYCR + + + YCBCR709_422_8 + + + YCBCR709_422_8_CBYCRY + + + YCBCR709_411_8_CBYYCRYY + + + YUV420SP_NV12 + + + YUV420SP_NV21 + + + RGB8_Planar + + + RGB10_Planar + + + RGB12_Planar + + + RGB16_Planar + + + Jpeg + + + Coord3D_ABC32f + + + Coord3D_ABC32f_Planar + + + Coord3D_AC32f + + + COORD3D_DEPTH_PLUS_MASK + + + Coord3D_ABC32 + + + Coord3D_AB32f + + + Coord3D_AB32 + + + Coord3D_AC32f_64 + + + Coord3D_AC32f_Planar + + + Coord3D_AC32 + + + Coord3D_A32f + + + Coord3D_A32 + + + Coord3D_C32f + + + Coord3D_C32 + + + Coord3D_ABC16 + + + Coord3D_C16 + + + Float32 + + + HB_Mono8 + + + HB_Mono10 + + + HB_Mono10_Packed + + + HB_Mono12 + + + HB_Mono12_Packed + + + HB_Mono16 + + + HB_BayerGR8 + + + HB_BayerRG8 + + + HB_BayerGB8 + + + HB_BayerBG8 + + + HB_BayerRBGG8 + + + HB_BayerGR10 + + + HB_BayerRG10 + + + HB_BayerGB10 + + + HB_BayerBG10 + + + HB_BayerGR12 + + + HB_BayerRG12 + + + HB_BayerGB12 + + + HB_BayerBG12 + + + HB_BayerGR10_Packed + + + HB_BayerRG10_Packed + + + HB_BayerGB10_Packed + + + HB_BayerBG10_Packed + + + HB_BayerGR12_Packed + + + HB_BayerRG12_Packed + + + HB_BayerGB12_Packed + + + HB_BayerBG12_Packed + + + HB_YUV422_Packed + + + HB_YUV422_YUYV_Packed + + + HB_RGB8_Packed + + + HB_BGR8_Packed + + + HB_RGBA8_Packed + + + HB_BGRA8_Packed + + + HB_RGB16_Packed + + + HB_BGR16_Packed + + + HB_RGBA16_Packed + + + HB_BGRA16_Packed + + + + 像素格式 + + + + + 未定义像素格式 + + + + + Mono1p + + + + + Mono2p + + + + + Mono4p + + + + + Mono8 + + + + + Mono8_Signed + + + + + Mono10 + + + + + Mono10_Packed + + + + + Mono12 + + + + + Mono12_Packed + + + + + Mono14 + + + + + Mono16 + + + + + BayerGR8 + + + + + BayerRG8 + + + + + BayerGB8 + + + + + BayerBG8 + + + + + BayerRBGG8 + + + + + BayerGR10 + + + + + BayerRG10 + + + + + BayerGB10 + + + + + BayerBG10 + + + + + BayerGR12 + + + + + BayerRG12 + + + + + BayerGB12 + + + + + BayerBG12 + + + + + BayerGR10_Packed + + + + + BayerRG10_Packed + + + + + BayerGB10_Packed + + + + + BayerBG10_Packed + + + + + BayerGR12_Packed + + + + BayerRG12_Packed + + + BayerGB12_Packed + + + BayerBG12_Packed + + + BayerGR16 + + + BayerRG16 + + + BayerGB16 + + + BayerBG16 + + + RGB8_Packed + + + BGR8_Packed + + + RGBA8_Packed + + + BGRA8_Packed + + + RGB10_Packed + + + BGR10_Packed + + + RGB12_Packed + + + BGR12_Packed + + + RGB16_Packed + + + BGR16_Packed/// + + + RGBA16_Packed + + + BGRA16_Packed + + + RGB10V1_Packe + + + RGB10V2_Packed + + + RGB12V1_Packed + + + RGB565_Packed + + + BGR565_Packed + + + YUV411_Packed + + + YUV422_Packed + + + YUV422_YUYV_Packed + + + YUV444_Packed + + + YCBCR8_CBYCR + + + YCBCR422_8 + + + YCBCR422_8_CBYCRY + + + YCBCR411_8_CBYYCRYY + + + YCBCR601_8_CBYCR + + + YCBCR601_422_8 + + + YCBCR601_422_8_CBYCRY + + + YCBCR601_411_8_CBYYCRYY + + + YCBCR709_8_CBYCR + + + YCBCR709_422_8 + + + YCBCR709_422_8_CBYCRY + + + YCBCR709_411_8_CBYYCRYY + + + YUV420SP_NV12 + + + YUV420SP_NV21 + + + RGB8_Planar + + + RGB10_Planar + + + RGB12_Planar + + + RGB16_Planar + + + Jpeg + + + Coord3D_ABC32f + + + Coord3D_ABC32f_Planar + + + Coord3D_AC32f + + + COORD3D_DEPTH_PLUS_MASK + + + Coord3D_ABC32 + + + Coord3D_AB32f + + + Coord3D_AB32 + + + Coord3D_AC32f_64 + + + Coord3D_AC32f_Planar + + + Coord3D_AC32 + + + Coord3D_A32f + + + Coord3D_A32 + + + Coord3D_C32f + + + Coord3D_C32 + + + Coord3D_ABC16 + + + Coord3D_C16 + + + Float32 + + + HB_Mono8 + + + HB_Mono10 + + + HB_Mono10_Packed + + + HB_Mono12 + + + HB_Mono12_Packed + + + HB_Mono16 + + + HB_BayerGR8 + + + HB_BayerRG8 + + + HB_BayerGB8 + + + HB_BayerBG8 + + + HB_BayerRBGG8 + + + HB_BayerGR10 + + + HB_BayerRG10 + + + HB_BayerGB10 + + + HB_BayerBG10 + + + HB_BayerGR12 + + + HB_BayerRG12 + + + HB_BayerGB12 + + + HB_BayerBG12 + + + HB_BayerGR10_Packed + + + HB_BayerRG10_Packed + + + HB_BayerGB10_Packed + + + HB_BayerBG10_Packed + + + HB_BayerGR12_Packed + + + HB_BayerRG12_Packed + + + HB_BayerGB12_Packed + + + HB_BayerBG12_Packed + + + HB_YUV422_Packed + + + HB_YUV422_YUYV_Packed + + + HB_RGB8_Packed + + + HB_BGR8_Packed + + + HB_RGBA8_Packed + + + HB_BGRA8_Packed + + + HB_RGB16_Packed + + + HB_BGR16_Packed + + + HB_RGBA16_Packed + + + HB_BGRA16_Packed + + + diff --git a/ExternalLibraries/MvCameraControl.Net.dll b/ExternalLibraries/MvCameraControl.Net.dll new file mode 100644 index 0000000000000000000000000000000000000000..a2e4c8b00de6776f31506185c3c062934a3e2698 GIT binary patch literal 177152 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 l&zHAzRkcKLAT|i**11 literal 0 HcmV?d00001 diff --git a/ExternalLibraries/config.json b/ExternalLibraries/config.json index 28f9fdf..0200def 100644 --- a/ExternalLibraries/config.json +++ b/ExternalLibraries/config.json @@ -1,4 +1,5 @@ { "Language": "zh-CN", - "LogLevel": "Debug" + "LogLevel": "Debug", + "CameraType": "Hikvision" } \ No newline at end of file diff --git a/XP.Camera/Converters/PixelConverter.cs b/XP.Camera/Converters/PixelConverter.cs index 2c965b5..601f13a 100644 --- a/XP.Camera/Converters/PixelConverter.cs +++ b/XP.Camera/Converters/PixelConverter.cs @@ -10,6 +10,7 @@ public static class PixelConverter { /// /// 将原始像素数据转换为 WPF 的 BitmapSource 对象。 + /// 支持 Mono8、BGR8、RGB8、BGRA8 以及 Bayer 8-bit 格式(自动解码为 BGR24)。 /// 返回的 BitmapSource 已调用 Freeze(),可跨线程访问。 /// public static BitmapSource ToBitmapSource(byte[] pixelData, int width, int height, string pixelFormat) @@ -19,11 +20,23 @@ public static class PixelConverter if (height <= 0) throw new ArgumentException("Height must be a positive integer.", nameof(height)); ArgumentNullException.ThrowIfNull(pixelFormat); - var (format, stride) = pixelFormat switch + string normalized = NormalizePixelFormat(pixelFormat); + + // Bayer 格式需要解码 + if (normalized.StartsWith("Bayer")) + { + byte[] bgrData = DemosaicBayer(pixelData, width, height, normalized); + var bmp = BitmapSource.Create(width, height, 96, 96, PixelFormats.Bgr24, null, bgrData, width * 3); + bmp.Freeze(); + return bmp; + } + + var (format, stride) = normalized switch { "Mono8" => (PixelFormats.Gray8, width), "BGR8" => (PixelFormats.Bgr24, width * 3), "BGRA8" => (PixelFormats.Bgra32, width * 4), + "RGB8" => (PixelFormats.Rgb24, width * 3), _ => throw new NotSupportedException($"Pixel format '{pixelFormat}' is not supported.") }; @@ -31,4 +44,136 @@ public static class PixelConverter bitmap.Freeze(); return bitmap; } -} \ No newline at end of file + + /// + /// 将不同 SDK 的像素格式名称统一为标准名称。 + /// + private static string NormalizePixelFormat(string pixelFormat) + { + if (pixelFormat is "Mono8" or "BGR8" or "BGRA8" or "RGB8") + return pixelFormat; + + var upper = pixelFormat.ToUpperInvariant(); + + if (upper.Contains("MONO8")) return "Mono8"; + if (upper.Contains("BGR8")) return "BGR8"; + if (upper.Contains("BGRA8")) return "BGRA8"; + if (upper.Contains("RGB8") && !upper.Contains("BAYER")) return "RGB8"; + + // Bayer 格式 + if (upper.Contains("BAYERRG8") || upper.Contains("BAYER_RG8")) return "BayerRG8"; + if (upper.Contains("BAYERGR8") || upper.Contains("BAYER_GR8")) return "BayerGR8"; + if (upper.Contains("BAYERGB8") || upper.Contains("BAYER_GB8")) return "BayerGB8"; + if (upper.Contains("BAYERBG8") || upper.Contains("BAYER_BG8")) return "BayerBG8"; + + return pixelFormat; + } + + /// + /// 简单 Bayer 解码(双线性插值),输出 BGR24。 + /// + private static byte[] DemosaicBayer(byte[] bayer, int width, int height, string pattern) + { + // pattern: BayerRG8, BayerGR8, BayerGB8, BayerBG8 + // RG: R G GR: G R GB: G B BG: B G + // G B B G R G G R + + int rRow, rCol; // 红色像素在2x2块中的位置 + switch (pattern) + { + case "BayerRG8": rRow = 0; rCol = 0; break; + case "BayerGR8": rRow = 0; rCol = 1; break; + case "BayerGB8": rRow = 1; rCol = 0; break; + case "BayerBG8": rRow = 1; rCol = 1; break; + default: rRow = 0; rCol = 0; break; + } + + byte[] bgr = new byte[width * height * 3]; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int srcIdx = y * width + x; + int dstIdx = (y * width + x) * 3; + + // 确定当前像素在 Bayer 模式中的角色 + int py = (y + rRow) % 2; // 0=红行, 1=蓝行 + int px = (x + rCol) % 2; // 0=红列/蓝列, 1=绿列 + + byte r, g, b; + + if (py == 0 && px == 0) + { + // 红色像素位置 + r = bayer[srcIdx]; + g = AvgNeighbors4(bayer, width, height, x, y); + b = AvgDiagonal(bayer, width, height, x, y); + } + else if (py == 1 && px == 1) + { + // 蓝色像素位置 + b = bayer[srcIdx]; + g = AvgNeighbors4(bayer, width, height, x, y); + r = AvgDiagonal(bayer, width, height, x, y); + } + else if (py == 0 && px == 1) + { + // 绿色像素(红行) + g = bayer[srcIdx]; + r = AvgHorizontal(bayer, width, x, y); + b = AvgVertical(bayer, width, height, x, y); + } + else + { + // 绿色像素(蓝行) + g = bayer[srcIdx]; + b = AvgHorizontal(bayer, width, x, y); + r = AvgVertical(bayer, width, height, x, y); + } + + bgr[dstIdx] = b; + bgr[dstIdx + 1] = g; + bgr[dstIdx + 2] = r; + } + } + + return bgr; + } + + private static byte AvgNeighbors4(byte[] data, int w, int h, int x, int y) + { + int sum = 0, count = 0; + if (x > 0) { sum += data[y * w + x - 1]; count++; } + if (x < w - 1) { sum += data[y * w + x + 1]; count++; } + if (y > 0) { sum += data[(y - 1) * w + x]; count++; } + if (y < h - 1) { sum += data[(y + 1) * w + x]; count++; } + return count > 0 ? (byte)(sum / count) : (byte)0; + } + + private static byte AvgDiagonal(byte[] data, int w, int h, int x, int y) + { + int sum = 0, count = 0; + if (x > 0 && y > 0) { sum += data[(y - 1) * w + x - 1]; count++; } + if (x < w - 1 && y > 0) { sum += data[(y - 1) * w + x + 1]; count++; } + if (x > 0 && y < h - 1) { sum += data[(y + 1) * w + x - 1]; count++; } + if (x < w - 1 && y < h - 1) { sum += data[(y + 1) * w + x + 1]; count++; } + return count > 0 ? (byte)(sum / count) : (byte)0; + } + + private static byte AvgHorizontal(byte[] data, int w, int x, int y) + { + int sum = 0, count = 0; + if (x > 0) { sum += data[y * w + x - 1]; count++; } + if (x < w - 1) { sum += data[y * w + x + 1]; count++; } + return count > 0 ? (byte)(sum / count) : (byte)0; + } + + private static byte AvgVertical(byte[] data, int w, int h, int x, int y) + { + int sum = 0, count = 0; + if (y > 0) { sum += data[(y - 1) * w + x]; count++; } + if (y < h - 1) { sum += data[(y + 1) * w + x]; count++; } + return count > 0 ? (byte)(sum / count) : (byte)0; + } +} diff --git a/XP.Camera/Core/CameraFactory.cs b/XP.Camera/Core/CameraFactory.cs index de85f89..e8baf3b 100644 --- a/XP.Camera/Core/CameraFactory.cs +++ b/XP.Camera/Core/CameraFactory.cs @@ -11,8 +11,8 @@ public class CameraFactory : ICameraFactory return cameraType switch { "Basler" => new BaslerCameraController(), - // "Hikvision" => new HikvisionCameraController(), - _ => throw new NotSupportedException($"不支持的相机品牌: {cameraType}") + "Hikvision" => new HikvisionCameraController(), + _ => throw new NotSupportedException($"Unsupported Camera Type: {cameraType}") }; } } \ No newline at end of file diff --git a/XP.Camera/Hikvision/HikvisionCameraController.cs b/XP.Camera/Hikvision/HikvisionCameraController.cs new file mode 100644 index 0000000..5e0a33c --- /dev/null +++ b/XP.Camera/Hikvision/HikvisionCameraController.cs @@ -0,0 +1,535 @@ +using MvCameraControl; +using Serilog; + +namespace XP.Camera; + +/// +/// 海康威视相机控制器,封装 MvCameraControl.Net SDK 实现 。 +/// +/// +/// 所有公共方法通过内部 _syncLock 对象进行 lock 同步,保证线程安全。 +/// 事件回调(ImageGrabbed、GrabError)在 SDK 回调线程上触发,不持有 _syncLock,避免死锁。 +/// +public class HikvisionCameraController : ICameraController +{ + private static readonly ILogger _logger = Log.ForContext(); + private static bool _sdkInitialized; + private static readonly object _sdkInitLock = new(); + + private readonly object _syncLock = new(); + private IDevice? _device; + private CameraInfo? _cachedCameraInfo; + private bool _isConnected; + private bool _isGrabbing; + + public HikvisionCameraController() + { + // SDK 初始化延迟到 Open() 中执行 + } + + /// + public bool IsConnected + { + get { lock (_syncLock) { return _isConnected; } } + } + + /// + public bool IsGrabbing + { + get { lock (_syncLock) { return _isGrabbing; } } + } + + /// + public event EventHandler? ImageGrabbed; + + /// + public event EventHandler? GrabError; + + /// + public event EventHandler? ConnectionLost; + + /// + public CameraInfo Open() + { + lock (_syncLock) + { + if (_isConnected && _cachedCameraInfo != null) + { + _logger.Information("Hikvision camera already connected, returning cached info."); + return _cachedCameraInfo; + } + + try + { + _logger.Information("Opening Hikvision camera connection..."); + + // 确保 SDK 初始化 + EnsureSdkInitialized(); + + // 枚举设备 + DeviceTLayerType layerType = DeviceTLayerType.MvGigEDevice + | DeviceTLayerType.MvUsbDevice; + + List deviceInfoList; + int ret = DeviceEnumerator.EnumDevices(layerType, out deviceInfoList); + _logger.Information("EnumDevices(GigE|USB) returned: 0x{RetCode:X8}, device count: {Count}", + ret, deviceInfoList?.Count ?? 0); + + // 如果没找到,分别尝试 + if (ret == MvError.MV_OK && (deviceInfoList == null || deviceInfoList.Count == 0)) + { + // 单独尝试 GigE + List gigeList; + int retGige = DeviceEnumerator.EnumDevices(DeviceTLayerType.MvGigEDevice, out gigeList); + _logger.Information("EnumDevices(GigE only) returned: 0x{RetCode:X8}, count: {Count}", + retGige, gigeList?.Count ?? 0); + + // 单独尝试 USB + List usbList; + int retUsb = DeviceEnumerator.EnumDevices(DeviceTLayerType.MvUsbDevice, out usbList); + _logger.Information("EnumDevices(USB only) returned: 0x{RetCode:X8}, count: {Count}", + retUsb, usbList?.Count ?? 0); + + // 合并结果 + deviceInfoList = new List(); + if (gigeList != null) deviceInfoList.AddRange(gigeList); + if (usbList != null) deviceInfoList.AddRange(usbList); + } + + if (ret != MvError.MV_OK) + { + throw new CameraException($"Enumerate Hikvision devices failed: 0x{ret:X8}"); + } + + if (deviceInfoList == null || deviceInfoList.Count == 0) + { + throw new DeviceNotFoundException("No Hikvision camera device found."); + } + + // 选择第一个设备 + IDeviceInfo deviceInfo = deviceInfoList[0]; + _logger.Information("Found Hikvision device: {Model} (SN: {Serial})", + deviceInfo.ModelName, deviceInfo.SerialNumber); + + // 创建设备 + _device = DeviceFactory.CreateDevice(deviceInfo); + + // 打开设备 + ret = _device.Open(); + if (ret != MvError.MV_OK) + { + _device.Dispose(); + _device = null; + throw new CameraException($"Open Hikvision device failed: 0x{ret:X8}"); + } + + // GigE 设备优化包大小 + if (_device is IGigEDevice gigEDevice) + { + int packetSize; + ret = gigEDevice.GetOptimalPacketSize(out packetSize); + if (ret == MvError.MV_OK && packetSize > 0) + { + _device.Parameters.SetIntValue("GevSCPSPacketSize", packetSize); + _logger.Debug("Set GigE packet size to {PacketSize}", packetSize); + } + } + + // 配置软件触发模式 + _device.Parameters.SetEnumValueByString("TriggerMode", "On"); + _device.Parameters.SetEnumValueByString("TriggerSource", "Software"); + + // 彩色相机:尝试设置输出为 BGR8 以便直接显示 + // 如果相机不支持 BGR8(如只支持 Bayer),则保持默认 + int fmtRet = _device.Parameters.SetEnumValueByString("PixelFormat", "BGR8Packed"); + if (fmtRet != MvError.MV_OK) + { + // 尝试 Mono8(黑白相机) + fmtRet = _device.Parameters.SetEnumValueByString("PixelFormat", "Mono8"); + } + _logger.Debug("Set PixelFormat result: 0x{Ret:X8}", fmtRet); + + _cachedCameraInfo = new CameraInfo( + ModelName: deviceInfo.ModelName ?? "", + SerialNumber: deviceInfo.SerialNumber ?? "", + VendorName: deviceInfo.ManufacturerName ?? "", + DeviceType: deviceInfo.TLayerType.ToString() + ); + + _isConnected = true; + _logger.Information("Hikvision camera connected: {ModelName} (SN: {SerialNumber})", + _cachedCameraInfo.ModelName, _cachedCameraInfo.SerialNumber); + + return _cachedCameraInfo; + } + catch (Exception ex) when (ex is not CameraException) + { + _device?.Dispose(); + _device = null; + _logger.Error(ex, "Failed to open Hikvision camera."); + throw new CameraException("Failed to open Hikvision camera device.", ex); + } + } + } + + /// + public void Close() + { + lock (_syncLock) + { + if (!_isConnected) + { + _logger.Information("Hikvision camera not connected, Close() ignored."); + return; + } + + try + { + if (_isGrabbing) + { + StopGrabbingInternal(); + } + + _logger.Information("Closing Hikvision camera connection..."); + _device?.Close(); + _device?.Dispose(); + _device = null; + _isConnected = false; + _cachedCameraInfo = null; + _logger.Information("Hikvision camera connection closed."); + } + catch (Exception ex) when (ex is not CameraException) + { + _device = null; + _isConnected = false; + _isGrabbing = false; + _cachedCameraInfo = null; + _logger.Error(ex, "Error while closing Hikvision camera."); + throw new CameraException("Failed to close Hikvision camera device.", ex); + } + } + } + + /// + public void StartGrabbing() + { + lock (_syncLock) + { + EnsureConnected(); + + if (_isGrabbing) + { + _logger.Information("Already grabbing, StartGrabbing() ignored."); + return; + } + + try + { + _logger.Information("Starting Hikvision grabbing with software trigger..."); + + // 设置缓存节点数 + _device!.StreamGrabber.SetImageNodeNum(5); + + // 注册回调 + _device.StreamGrabber.FrameGrabedEvent += OnFrameGrabbed; + + // 开始采集 + int ret = _device.StreamGrabber.StartGrabbing(); + if (ret != MvError.MV_OK) + { + _device.StreamGrabber.FrameGrabedEvent -= OnFrameGrabbed; + throw new CameraException($"Start grabbing failed: 0x{ret:X8}"); + } + + _isGrabbing = true; + _logger.Information("Hikvision grabbing started."); + } + catch (Exception ex) when (ex is not CameraException) + { + _logger.Error(ex, "Failed to start Hikvision grabbing."); + throw new CameraException("Failed to start grabbing.", ex); + } + } + } + + /// + public void ExecuteSoftwareTrigger() + { + lock (_syncLock) + { + if (!_isGrabbing) + { + throw new InvalidOperationException("Cannot execute software trigger: camera is not grabbing."); + } + + try + { + int ret = _device!.Parameters.SetCommandValue("TriggerSoftware"); + if (ret != MvError.MV_OK) + { + throw new CameraException($"Execute software trigger failed: 0x{ret:X8}"); + } + } + catch (Exception ex) when (ex is not CameraException and not InvalidOperationException) + { + _logger.Error(ex, "Failed to execute software trigger."); + throw new CameraException("Failed to execute software trigger.", ex); + } + } + } + + /// + public void StopGrabbing() + { + lock (_syncLock) + { + if (!_isGrabbing) return; + StopGrabbingInternal(); + } + } + + /// + public double GetExposureTime() + { + lock (_syncLock) + { + EnsureConnected(); + IFloatValue floatValue; + int ret = _device!.Parameters.GetFloatValue("ExposureTime", out floatValue); + if (ret != MvError.MV_OK) + throw new CameraException($"Get ExposureTime failed: 0x{ret:X8}"); + return floatValue.CurValue; + } + } + + /// + public void SetExposureTime(double microseconds) + { + lock (_syncLock) + { + EnsureConnected(); + // 关闭自动曝光 + _device!.Parameters.SetEnumValueByString("ExposureAuto", "Off"); + int ret = _device.Parameters.SetFloatValue("ExposureTime", (float)microseconds); + if (ret != MvError.MV_OK) + throw new CameraException($"Set ExposureTime failed: 0x{ret:X8}"); + _logger.Information("Hikvision exposure time set to {Microseconds} µs.", microseconds); + } + } + + /// + public double GetGain() + { + lock (_syncLock) + { + EnsureConnected(); + IFloatValue floatValue; + int ret = _device!.Parameters.GetFloatValue("Gain", out floatValue); + if (ret != MvError.MV_OK) + throw new CameraException($"Get Gain failed: 0x{ret:X8}"); + return floatValue.CurValue; + } + } + + /// + public void SetGain(double value) + { + lock (_syncLock) + { + EnsureConnected(); + _device!.Parameters.SetEnumValueByString("GainAuto", "Off"); + int ret = _device.Parameters.SetFloatValue("Gain", (float)value); + if (ret != MvError.MV_OK) + throw new CameraException($"Set Gain failed: 0x{ret:X8}"); + _logger.Information("Hikvision gain set to {Value}.", value); + } + } + + /// + public int GetWidth() + { + lock (_syncLock) + { + EnsureConnected(); + IIntValue intValue; + int ret = _device!.Parameters.GetIntValue("Width", out intValue); + if (ret != MvError.MV_OK) + throw new CameraException($"Get Width failed: 0x{ret:X8}"); + return (int)intValue.CurValue; + } + } + + /// + public void SetWidth(int value) + { + lock (_syncLock) + { + EnsureConnected(); + int ret = _device!.Parameters.SetIntValue("Width", value); + if (ret != MvError.MV_OK) + throw new CameraException($"Set Width failed: 0x{ret:X8}"); + _logger.Information("Hikvision width set to {Value}.", value); + } + } + + /// + public int GetHeight() + { + lock (_syncLock) + { + EnsureConnected(); + IIntValue intValue; + int ret = _device!.Parameters.GetIntValue("Height", out intValue); + if (ret != MvError.MV_OK) + throw new CameraException($"Get Height failed: 0x{ret:X8}"); + return (int)intValue.CurValue; + } + } + + /// + public void SetHeight(int value) + { + lock (_syncLock) + { + EnsureConnected(); + int ret = _device!.Parameters.SetIntValue("Height", value); + if (ret != MvError.MV_OK) + throw new CameraException($"Set Height failed: 0x{ret:X8}"); + _logger.Information("Hikvision height set to {Value}.", value); + } + } + + /// + public string GetPixelFormat() + { + lock (_syncLock) + { + EnsureConnected(); + IEnumValue enumValue; + int ret = _device!.Parameters.GetEnumValue("PixelFormat", out enumValue); + if (ret != MvError.MV_OK) + throw new CameraException($"Get PixelFormat failed: 0x{ret:X8}"); + return enumValue.CurEnumEntry.Symbolic; + } + } + + /// + public void SetPixelFormat(string format) + { + lock (_syncLock) + { + EnsureConnected(); + int ret = _device!.Parameters.SetEnumValueByString("PixelFormat", format); + if (ret != MvError.MV_OK) + throw new CameraException($"Set PixelFormat failed: 0x{ret:X8}"); + _logger.Information("Hikvision pixel format set to {Format}.", format); + } + } + + /// + public void Dispose() + { + Close(); + GC.SuppressFinalize(this); + } + + // ══════════════════════════════════════════════════════════════ + // 私有方法 + // ══════════════════════════════════════════════════════════════ + + /// + /// SDK 回调:图像采集完成 + /// + private void OnFrameGrabbed(object? sender, FrameGrabbedEventArgs e) + { + try + { + var frameOut = e.FrameOut; + if (frameOut == null || frameOut.Image == null) + { + _logger.Warning("Hikvision OnFrameGrabbed: FrameOut or Image is null"); + GrabError?.Invoke(this, new GrabErrorEventArgs(-1, "FrameOut or Image is null.")); + return; + } + + var image = frameOut.Image; + int width = (int)image.Width; + int height = (int)image.Height; + int imageSize = (int)image.ImageSize; + string pixelFormat = image.PixelType.ToString(); + + // 提取像素数据 + byte[] pixelData = image.PixelData ?? Array.Empty(); + + _logger.Debug("Hikvision frame: {Width}x{Height}, format={Format}, dataLen={Len}", + width, height, pixelFormat, pixelData.Length); + + if (pixelData.Length == 0) + { + _logger.Warning("Hikvision OnFrameGrabbed: PixelData is empty"); + return; + } + + var args = new ImageGrabbedEventArgs(pixelData, width, height, pixelFormat); + ImageGrabbed?.Invoke(this, args); + } + catch (Exception ex) + { + _logger.Error(ex, "Exception in Hikvision OnFrameGrabbed handler."); + } + } + + private void StopGrabbingInternal() + { + if (!_isGrabbing) return; + + try + { + _device?.StreamGrabber.StopGrabbing(); + if (_device != null) + _device.StreamGrabber.FrameGrabedEvent -= OnFrameGrabbed; + _isGrabbing = false; + _logger.Information("Hikvision grabbing stopped."); + } + catch (Exception ex) when (ex is not CameraException) + { + _isGrabbing = false; + _logger.Error(ex, "Error while stopping Hikvision grabbing."); + throw new CameraException("Failed to stop grabbing.", ex); + } + } + + private void EnsureConnected() + { + if (!_isConnected) + throw new InvalidOperationException("Hikvision camera is not connected. Call Open() first."); + } + + /// + /// 确保 SDK 全局初始化(只调用一次) + /// + private static void EnsureSdkInitialized() + { + if (_sdkInitialized) return; + lock (_sdkInitLock) + { + if (_sdkInitialized) return; + try + { + int ret = SDKSystem.Initialize(); + if (ret != MvError.MV_OK) + { + _logger.Error("Hikvision SDK Initialize failed: 0x{ErrorCode:X8}", ret); + throw new CameraException($"Hikvision SDK Initialize failed: 0x{ret:X8}"); + } + _sdkInitialized = true; + _logger.Information("Hikvision SDK initialized successfully."); + } + catch (Exception ex) when (ex is not CameraException) + { + _logger.Error(ex, "Failed to initialize Hikvision SDK."); + throw new CameraException("Failed to initialize Hikvision SDK.", ex); + } + } + } +} diff --git a/XP.Camera/XP.Camera.csproj b/XP.Camera/XP.Camera.csproj index 1510592..0a4b749 100644 --- a/XP.Camera/XP.Camera.csproj +++ b/XP.Camera/XP.Camera.csproj @@ -7,12 +7,19 @@ enable XP.Camera XP.Camera + true + true ..\ExternalLibraries\Basler.Pylon.dll + + ..\ExternalLibraries\MvCameraControl.Net.dll + true + true + diff --git a/XplorePlane/App.xaml.cs b/XplorePlane/App.xaml.cs index 258a6c0..696c467 100644 --- a/XplorePlane/App.xaml.cs +++ b/XplorePlane/App.xaml.cs @@ -321,7 +321,7 @@ namespace XplorePlane shell.Loaded += (s, e) => { // [DEV] 导航相机连接已屏蔽,开发阶段跳过以加快启动速度 - // TryConnectCamera(); + TryConnectCamera(); // 初始化主界面探测器帧流水线,开始接收探测器图像事件 try @@ -334,20 +334,20 @@ namespace XplorePlane } // [DEV] 相机状态通知已屏蔽 - // try - // { - // var cameraVm = Container.Resolve(); - // cameraVm.OnCameraReady(); - // } - // catch (Exception ex) - // { - // Log.Error(ex, "通知相机 ViewModel 失败"); - // } + try + { + var cameraVm = Container.Resolve(); + cameraVm.OnCameraReady(); + } + catch (Exception ex) + { + Log.Error(ex, "通知相机 ViewModel 失败"); + } - // if (_cameraError != null) - // { - // HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error); - // } + if (_cameraError != null) + { + HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error); + } }; return shell; @@ -462,7 +462,26 @@ namespace XplorePlane // ── 导航相机服务(单例)── containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(() => - new CameraFactory().CreateController("Basler")); + { + string cameraType = "Hikvision"; // 默认值 + try + { + var configPath = Path.Combine(AppContext.BaseDirectory, "config.json"); + if (File.Exists(configPath)) + { + var json = File.ReadAllText(configPath); + var match = System.Text.RegularExpressions.Regex.Match(json, "\"CameraType\"\\s*:\\s*\"([^\"]+)\""); + if (match.Success) + cameraType = match.Groups[1].Value; + } + } + catch (Exception ex) + { + Log.Warning(ex, "读取 CameraType 配置失败,使用默认值 Hikvision"); + } + Log.Information("相机类型: {CameraType}", cameraType); + return new CameraFactory().CreateController(cameraType); + }); containerRegistry.RegisterSingleton(); Log.Information("依赖注入容器配置完成"); diff --git a/XplorePlane/ViewModels/Main/NavigationPropertyPanelViewModel.cs b/XplorePlane/ViewModels/Main/NavigationPropertyPanelViewModel.cs index 586e99f..1c5d728 100644 --- a/XplorePlane/ViewModels/Main/NavigationPropertyPanelViewModel.cs +++ b/XplorePlane/ViewModels/Main/NavigationPropertyPanelViewModel.cs @@ -55,9 +55,6 @@ namespace XplorePlane.ViewModels StopGrabCommand.RaiseCanExecuteChanged(); ApplyExposureCommand.RaiseCanExecuteChanged(); ApplyGainCommand.RaiseCanExecuteChanged(); - ApplyWidthCommand.RaiseCanExecuteChanged(); - ApplyHeightCommand.RaiseCanExecuteChanged(); - ApplyPixelFormatCommand.RaiseCanExecuteChanged(); RefreshCameraParamsCommand.RaiseCanExecuteChanged(); OpenCameraSettingsCommand.RaiseCanExecuteChanged(); } @@ -79,7 +76,7 @@ namespace XplorePlane.ViewModels } } - private string _cameraStatusText = "未连接"; + private string _cameraStatusText = "Disconnected"; public string CameraStatusText { @@ -152,8 +149,6 @@ namespace XplorePlane.ViewModels set => SetProperty(ref _selectedPixelFormat, value); } - public ObservableCollection PixelFormatOptions { get; } = new() { "Mono8", "BGR8", "BGRA8" }; - #endregion Properties #region Commands @@ -164,9 +159,6 @@ namespace XplorePlane.ViewModels public DelegateCommand StopGrabCommand { get; } public DelegateCommand ApplyExposureCommand { get; } public DelegateCommand ApplyGainCommand { get; } - public DelegateCommand ApplyWidthCommand { get; } - public DelegateCommand ApplyHeightCommand { get; } - public DelegateCommand ApplyPixelFormatCommand { get; } public DelegateCommand RefreshCameraParamsCommand { get; } public DelegateCommand OpenCameraSettingsCommand { get; } @@ -183,9 +175,6 @@ namespace XplorePlane.ViewModels StopGrabCommand = new DelegateCommand(StopGrab, () => IsCameraGrabbing); ApplyExposureCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetExposureTime(ExposureTime)), () => IsCameraConnected); ApplyGainCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetGain(GainValue)), () => IsCameraConnected); - ApplyWidthCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetWidth(ImageWidth)), () => IsCameraConnected); - ApplyHeightCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetHeight(ImageHeight)), () => IsCameraConnected); - ApplyPixelFormatCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetPixelFormat(SelectedPixelFormat)), () => IsCameraConnected); RefreshCameraParamsCommand = new DelegateCommand(RefreshCameraParams, () => IsCameraConnected); OpenCameraSettingsCommand = new DelegateCommand(OpenCameraSettings, () => IsCameraConnected); @@ -193,7 +182,7 @@ namespace XplorePlane.ViewModels _defaultImageSource = new BitmapImage(new Uri("pack://application:,,,/Assets/Icons/NoCamera.png")); _cameraImageSource = _defaultImageSource; - CameraStatusText = "正在检索相机..."; + CameraStatusText = "Searching camera..."; } /// @@ -203,7 +192,7 @@ namespace XplorePlane.ViewModels { if (!_camera.IsConnected) { - CameraStatusText = "未检测到相机"; + CameraStatusText = "No camera detected"; return; } @@ -212,7 +201,7 @@ namespace XplorePlane.ViewModels _camera.ConnectionLost += OnCameraConnectionLost; IsCameraConnected = true; - CameraStatusText = "已连接"; + CameraStatusText = "Connected"; RefreshCameraParams(); SyncCameraStateToAppState(); StartGrab(); @@ -231,7 +220,7 @@ namespace XplorePlane.ViewModels var info = _camera.Open(); IsCameraConnected = true; - CameraStatusText = $"已连接: {info.ModelName} (SN: {info.SerialNumber})"; + CameraStatusText = $"Connected: {info.ModelName} (SN: {info.SerialNumber})"; _logger.Information("Camera connected: {ModelName}", info.ModelName); RefreshCameraParams(); SyncCameraStateToAppState(); @@ -239,7 +228,7 @@ namespace XplorePlane.ViewModels catch (Exception ex) { _logger.Error(ex, "Failed to connect camera"); - CameraStatusText = $"连接失败: {ex.Message}"; + CameraStatusText = $"Connection failed: {ex.Message}"; IsCameraConnected = false; SyncCameraStateToAppState(); } @@ -263,7 +252,7 @@ namespace XplorePlane.ViewModels _camera.ConnectionLost -= OnCameraConnectionLost; IsCameraConnected = false; IsCameraGrabbing = false; - CameraStatusText = "未连接"; + CameraStatusText = "Disconnected"; CameraImageSource = null; SyncCameraStateToAppState(); _logger.Information("Camera disconnected"); @@ -276,7 +265,7 @@ namespace XplorePlane.ViewModels { _camera.StartGrabbing(); IsCameraGrabbing = true; - CameraStatusText = "采集中..."; + CameraStatusText = "Grabbing..."; SyncCameraStateToAppState(); // 如果已勾选实时,自动启动 Live View @@ -288,7 +277,7 @@ namespace XplorePlane.ViewModels catch (Exception ex) { _logger.Error(ex, "Failed to start grabbing"); - CameraStatusText = $"采集失败: {ex.Message}"; + CameraStatusText = $"Grab failed: {ex.Message}"; } } @@ -299,7 +288,7 @@ namespace XplorePlane.ViewModels IsLiveViewEnabled = false; _camera.StopGrabbing(); IsCameraGrabbing = false; - CameraStatusText = "已停止采集"; + CameraStatusText = "Grab stopped"; SyncCameraStateToAppState(); } catch (Exception ex) @@ -313,7 +302,7 @@ namespace XplorePlane.ViewModels if (!IsCameraGrabbing) return; _liveViewRunning = true; - CameraStatusText = "实时采集中..."; + CameraStatusText = "Live..."; try { _camera.ExecuteSoftwareTrigger(); } catch (Exception ex) { _logger.Error(ex, "Live view trigger failed"); } @@ -323,7 +312,7 @@ namespace XplorePlane.ViewModels { _liveViewRunning = false; if (IsCameraGrabbing) - CameraStatusText = "采集中..."; + CameraStatusText = "Grabbing..."; } private void RefreshCameraParams() @@ -334,13 +323,16 @@ namespace XplorePlane.ViewModels GainValue = _camera.GetGain(); ImageWidth = _camera.GetWidth(); ImageHeight = _camera.GetHeight(); - SelectedPixelFormat = _camera.GetPixelFormat(); + + var currentFormat = _camera.GetPixelFormat(); + SelectedPixelFormat = currentFormat; + _logger.Information("Camera parameters refreshed"); } catch (Exception ex) { _logger.Error(ex, "Failed to read camera parameters"); - CameraStatusText = $"读取参数失败: {ex.Message}"; + CameraStatusText = $"Read params failed: {ex.Message}"; } } @@ -354,7 +346,7 @@ namespace XplorePlane.ViewModels catch (Exception ex) { _logger.Error(ex, "Failed to apply camera parameter"); - CameraStatusText = $"设置参数失败: {ex.Message}"; + CameraStatusText = $"Set param failed: {ex.Message}"; } } @@ -385,17 +377,21 @@ namespace XplorePlane.ViewModels if (!_disposed) CameraImageSource = bitmap; }); - - if (_liveViewRunning) - { - _camera.ExecuteSoftwareTrigger(); - } } catch (Exception ex) { if (!_disposed) _logger.Error(ex, "Failed to process camera image"); } + finally + { + // 无论图像处理是否成功,都继续触发下一帧,保持采集链不断 + if (_liveViewRunning && !_disposed) + { + try { _camera.ExecuteSoftwareTrigger(); } + catch { /* 忽略触发失败 */ } + } + } } private void OnCameraGrabError(object? sender, GrabErrorEventArgs e) @@ -407,7 +403,7 @@ namespace XplorePlane.ViewModels app.Dispatcher.BeginInvoke(() => { if (!_disposed) - CameraStatusText = $"采集错误: {e.ErrorDescription}"; + CameraStatusText = $"Grab error: {e.ErrorDescription}"; }); } @@ -422,7 +418,7 @@ namespace XplorePlane.ViewModels if (_disposed) return; IsCameraConnected = false; IsCameraGrabbing = false; - CameraStatusText = "连接已断开"; + CameraStatusText = "Connection lost"; CameraImageSource = null; SyncCameraStateToAppState(); }); diff --git a/XplorePlane/Views/Setting/CameraSettingsWindow.xaml b/XplorePlane/Views/Setting/CameraSettingsWindow.xaml index a0e9ddc..b4e0414 100644 --- a/XplorePlane/Views/Setting/CameraSettingsWindow.xaml +++ b/XplorePlane/Views/Setting/CameraSettingsWindow.xaml @@ -59,21 +59,11 @@ - - - - + - - - - + @@ -81,9 +71,9 @@ - + diff --git a/XplorePlane/Views/Setting/CameraSettingsWindow.xaml.cs b/XplorePlane/Views/Setting/CameraSettingsWindow.xaml.cs index 338c524..b2f6537 100644 --- a/XplorePlane/Views/Setting/CameraSettingsWindow.xaml.cs +++ b/XplorePlane/Views/Setting/CameraSettingsWindow.xaml.cs @@ -18,9 +18,6 @@ namespace XplorePlane.Views var type = dc.GetType(); ExecuteCommand(type, dc, "ApplyExposureCommand"); ExecuteCommand(type, dc, "ApplyGainCommand"); - ExecuteCommand(type, dc, "ApplyWidthCommand"); - ExecuteCommand(type, dc, "ApplyHeightCommand"); - ExecuteCommand(type, dc, "ApplyPixelFormatCommand"); } private static void ExecuteCommand(System.Type type, object dc, string cmdName) diff --git a/XplorePlane/XplorePlane.csproj b/XplorePlane/XplorePlane.csproj index ce47455..76771ac 100644 --- a/XplorePlane/XplorePlane.csproj +++ b/XplorePlane/XplorePlane.csproj @@ -49,6 +49,12 @@ Libs\Native\BR.AN.PviServices.dll True + + + + ..\ExternalLibraries\MvCameraControl.Net.dll + true + From 9c639f27cd9fba7af742007161e4ec4e04cda19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Mon, 18 May 2026 13:54:56 +0800 Subject: [PATCH 11/12] =?UTF-8?q?=E5=AF=BC=E8=88=AA=E7=9B=B8=E6=9C=BA?= =?UTF-8?q?=E7=9B=B8=E5=85=B3Log=E6=94=B9=E4=B8=BA=E8=8B=B1=E6=96=87?= =?UTF-8?q?=EF=BC=9B=E6=B7=BB=E5=8A=A0=E4=B8=80=E4=BA=9B=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XplorePlane/App.xaml.cs | 57 ++++++++++------------ XplorePlane/Assets/Icons/FittedCircle.png | Bin 0 -> 778 bytes XplorePlane/Assets/Icons/FittedLine.png | Bin 0 -> 583 bytes XplorePlane/Assets/Icons/Matching.png | Bin 0 -> 1171 bytes XplorePlane/Views/Main/MainWindow.xaml | 55 +++++++++++++++------ 5 files changed, 67 insertions(+), 45 deletions(-) create mode 100644 XplorePlane/Assets/Icons/FittedCircle.png create mode 100644 XplorePlane/Assets/Icons/FittedLine.png create mode 100644 XplorePlane/Assets/Icons/Matching.png diff --git a/XplorePlane/App.xaml.cs b/XplorePlane/App.xaml.cs index 696c467..e2ed480 100644 --- a/XplorePlane/App.xaml.cs +++ b/XplorePlane/App.xaml.cs @@ -166,12 +166,12 @@ namespace XplorePlane { var cameraVm = bootstrapper.Container.Resolve(); cameraVm?.Dispose(); - Log.Information("导航相机 ViewModel 已释放"); + Log.Information("Navigation camera ViewModel has been released"); } } catch (Exception ex) { - Log.Error(ex, "导航相机 ViewModel 释放失败"); + Log.Error(ex, "Navigation camera ViewModel release failed"); } // 释放导航相机服务资源 @@ -182,12 +182,12 @@ namespace XplorePlane { var cameraService = bootstrapper.Container.Resolve(); cameraService?.Dispose(); - Log.Information("导航相机服务资源已释放"); + Log.Information("Navigation camera service resources have been released"); } } catch (Exception ex) { - Log.Error(ex, "导航相机服务资源释放失败"); + Log.Error(ex, "Navigation camera service resource release failed"); } // 释放主界面探测器帧流水线资源 @@ -320,9 +320,6 @@ namespace XplorePlane // 主窗口加载完成后再连接相机,确保所有模块和原生 DLL 已完成初始化 shell.Loaded += (s, e) => { - // [DEV] 导航相机连接已屏蔽,开发阶段跳过以加快启动速度 - TryConnectCamera(); - // 初始化主界面探测器帧流水线,开始接收探测器图像事件 try { @@ -333,21 +330,22 @@ namespace XplorePlane Log.Error(ex, "初始化主界面探测器帧流水线失败"); } - // [DEV] 相机状态通知已屏蔽 - try - { - var cameraVm = Container.Resolve(); - cameraVm.OnCameraReady(); - } - catch (Exception ex) - { - Log.Error(ex, "通知相机 ViewModel 失败"); - } + // [DEV] 导航相机连接已屏蔽,开发阶段跳过以加快启动速度 + //TryConnectCamera(); + //try + //{ + // var cameraVm = Container.Resolve(); + // cameraVm.OnCameraReady(); + //} + //catch (Exception ex) + //{ + // Log.Error(ex, "Failed to notify the camera ViewModel"); + //} - if (_cameraError != null) - { - HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error); - } + //if (_cameraError != null) + //{ + // HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error); + //} }; return shell; @@ -364,17 +362,17 @@ namespace XplorePlane try { var info = camera.Open(); - Log.Information("导航相机已连接: {ModelName} (SN: {SerialNumber})", info.ModelName, info.SerialNumber); + Log.Information("Navigation camera connected: {ModelName} (SN: {SerialNumber})", info.ModelName, info.SerialNumber); } catch (DeviceNotFoundException) { - Log.Warning("未检测到导航相机"); - _cameraError = "未检测到导航相机,请检查连接后重启软件。"; + Log.Warning("Navigation camera not detected"); + _cameraError = "Navigation camera not detected,Please check the connection and restart the software.。"; } catch (Exception ex) { - Log.Warning(ex, "导航相机自动连接失败: {Message}", ex.Message); - _cameraError = $"导航相机连接失败: {ex.Message}"; + Log.Warning(ex, "Automatic connection of navigation camera failed: {Message}", ex.Message); + _cameraError = $"Navigation camera connection failed: {ex.Message}"; } } @@ -408,7 +406,6 @@ namespace XplorePlane containerRegistry.RegisterSingleton(); containerRegistry.Register(); - // 注册流水线服务(单例,共享 IImageProcessingService) containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); @@ -477,9 +474,9 @@ namespace XplorePlane } catch (Exception ex) { - Log.Warning(ex, "读取 CameraType 配置失败,使用默认值 Hikvision"); + Log.Warning(ex, "Failed to read CameraType configuration, using default value Hikvision"); } - Log.Information("相机类型: {CameraType}", cameraType); + Log.Information("Camera Type: {CameraType}", cameraType); return new CameraFactory().CreateController(cameraType); }); containerRegistry.RegisterSingleton(); @@ -505,4 +502,4 @@ namespace XplorePlane base.ConfigureModuleCatalog(moduleCatalog); } } -} +} \ No newline at end of file diff --git a/XplorePlane/Assets/Icons/FittedCircle.png b/XplorePlane/Assets/Icons/FittedCircle.png new file mode 100644 index 0000000000000000000000000000000000000000..13ac0d8da26a5d42706c9bfaa95c787649f19131 GIT binary patch literal 778 zcmV+l1NHogP)5! z!e=lTERe$&mWZArFdB`f!{Km_tdMJ?(U>A#k)XYzTxiZSgdP&;_xoKI7wDDGk96_) zbUO8@ujxz9=ktX2tl4b-xm+$kA##w8beU87EhWGr<954kF5Ygp4J1xwz2EN>>XY4W z*Hc}x*=&}yXCfrjR;wjX%~iiAcL@MK$GQcU^#F?REl|`iGSU$d*08V@%~M|?0l-%% zcCp5+)5KRqL_|Wmgk_=fR&oSO__LpTBf29gwaotAGe9K%Zi1Ol*<>usxPpb8ujDSurono3$lf-!8m#~ZgmGSX-ti=OhMeGPAV^?)2 z5Xm*_s7vs`2LWPYjk>^PtYex7VB=e`E)J1n8iCqSGeG}&fQHWLab~>IYI;wwr2iHV zbUK~!Z3y&#pZlk($_+Yq@Vfq-{@n?+FJV;6PLTTnpK>gJgh1Ou)^a>vk;*)wBR{?3 z8MrV*4#3JhP^BkW+m2s=L&9eX%$Tsu4pM*nH^m4So(%T5&DmJ4o>z=e^O904UHH(71V2*rc4HmmSVUAh>uk`7h}x2x@vBcK71 zKq2Oi5c}goi)rjLqPYOirOW)d$a4DaTS&l0_)%Wq4P(ys(L>bUek^WTjEAg`sgZ*+ z=D-oadenq1r+K$hNS32yrl0>7ESNfQ1-b3%Z!Bh6-r`~bU#6&it2gfi3v_l1bylpO@7NX0AIvVe{O z)L{`~kqR&@v_=Z#;LLNA>_j6e79rGv0mq_g2~aX|k*;4AJm0G;Li8yFa8?UQ7THMD z1t*Zn6aqL)J}lbxAQ!;yaoe}$)h4?(fM!x5z;lY@cwmt14lyo4VCDiGu{sI?PU^rZ zJAC_4=oJK?(Rb3c1mwsBXR7hGL!o0GL3vNraVY^2+?DJiIV*v-!AZnO;5WdH1n{bz z=Vz{y9jO@D>I4Hfs literal 0 HcmV?d00001 diff --git a/XplorePlane/Assets/Icons/Matching.png b/XplorePlane/Assets/Icons/Matching.png new file mode 100644 index 0000000000000000000000000000000000000000..bcf4ca20badbca0c2de5420349fd53ce64cc08bc GIT binary patch literal 1171 zcmV;E1Z?|>P)44K6vwapv9x^%c{5-e$Qx8zTNLQjsYRusK!FZ@1X(YU06_pbnF3_Ul&n&Kyn$`N zo4w@)viU>#{b-UQudXd8OAv!cJmmTM^N@#^-e|t@-}ePzcXzkHzrR1(+uNJb&*{&l z$j=TA4o2#HNm3t_0i7CTYisKq=ZC#sZ?s%4`|u5X>YbaZ6G07>ucLbbl!0TwLmUoI zPELBKr>8sc3#ef{yt=yD0n8Y^Bj+G9G^4+_-3h?pt_JIYP7O$)DHMV-U$wKdvy0>7 z<0JIO*VorWgdtOALtl(LTPFb0JuoWg@FaVumAc+3v=H~n^5Gw#wBydO_ zfp>7!xni8JZP-rT8Wja$-=-A+tbE2^daz7mTR)5i>4|6X2x{cbntYqg#>U3RRG;X~ z+wuwk3ZC4s=b&F%xq;D{1M=J~;WPk{$8k7x>cCgcvw@C>bAY`uvb+Mo>;%ONQa^^T znuZ2uQU|#jOtHkg`#mz%W`uFoIG9s?GdY2B%K#$sf$7T9$VAy(2L>ZMxf)Edo2uk?s__f> zEBgOI{{Z-pG;dFaf}i%ei84Ri=`en-f++OC5zsr9;(ji=YMWI*hMuXH$V>ssvRxEg z5yVu?NQS#6EGqE+L`svnMxECQAjYoBTh0K^cagGmXK2%<<6N1UyalAueQ_HCN);0c z0st8Vdt6;I26m3%zL?D5P@80*dNSi(Q)MOtP)XDT>Qhpt(5Xxuyd&hkf}A@f;1azL z_A#(0e4$QUjCz^S6SUxbC*=7GfT%&~Efx+@){2aBuUdv)w|=lvc^cfOTIb3MZeVq+ zGBb^f+g1T^iU1;%9IlWcT%j33_#Uz%^2f<>n^@Egx*$7(uEZ+b zq0SH43V^OyseHO*cg+#pJx?znx2H;dvRey4N2G7&O5~GaSD&guUF3(@`MA0FUAYzj z`|W$q67=>;r{DjGtP~HIA16OF%c2^>HOwWIss=pWZ7l%JJa7OB|9mhGr9t0pci3fDb%KXT - - - @@ -517,6 +503,45 @@ Text="坐标标定" /> + + + + + + + + + + + + + + + From e233f0fd96b7222f7ba6372ed8f29b22f4e24009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Mon, 18 May 2026 15:03:34 +0800 Subject: [PATCH 12/12] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=BE=B9?= =?UTF-8?q?=E7=BC=98=E6=9F=A5=E6=89=BE=E6=8B=9F=E5=90=88=E5=9C=86=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=20+=20=E4=BC=98=E5=8C=96=E6=8B=9F=E5=90=88=E4=BA=A4?= =?UTF-8?q?=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 EdgeCircleFitProcessor 算子(卡尺径向边缘检测 + Kasa/RANSAC圆拟合) - 新增 EdgeCircleFitPanel 辅助面板(拖拽画圆交互) - Ribbon快捷工具组新增「圆拟合」按钮 - 拟合后卡尺保持可编辑状态,支持调整后重新拟合 - 每次拟合自动清除上一次结果 - 拟合方法固定RANSAC,UI不暴露选择 - 结果标注简化:直线显示角度,圆显示半径和圆心坐标 - 不再显示内点/外点小圆点 - 添加中英文本地化资源 --- XP.Common/Resources/Resources.en-US.resx | 62 ++ XP.Common/Resources/Resources.resx | 62 ++ XP.Common/Resources/Resources.zh-CN.resx | 62 ++ .../检测分析/EdgeCircleFitProcessor.cs | 582 ++++++++++++++++++ .../ImageProcessing/EdgeCircleFitViewModel.cs | 497 +++++++++++++++ .../ImageProcessing/EdgeLineFitViewModel.cs | 39 +- XplorePlane/ViewModels/Main/MainViewModel.cs | 22 + .../ImageProcessing/EdgeCircleFitPanel.xaml | 128 ++++ .../EdgeCircleFitPanel.xaml.cs | 52 ++ .../ImageProcessing/EdgeLineFitPanel.xaml | 5 - XplorePlane/Views/Main/MainWindow.xaml | 1 + 11 files changed, 1482 insertions(+), 30 deletions(-) create mode 100644 XP.ImageProcessing.Processors/检测分析/EdgeCircleFitProcessor.cs create mode 100644 XplorePlane/ViewModels/ImageProcessing/EdgeCircleFitViewModel.cs create mode 100644 XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml create mode 100644 XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml.cs diff --git a/XP.Common/Resources/Resources.en-US.resx b/XP.Common/Resources/Resources.en-US.resx index 5885aaf..75a2e64 100644 --- a/XP.Common/Resources/Resources.en-US.resx +++ b/XP.Common/Resources/Resources.en-US.resx @@ -1943,4 +1943,66 @@ Reprojection error: {1:F4} pixels Drawing thickness for result visualization + + + + Edge Find Circle Fit + + + Place calipers along estimated circle to detect edge points and fit a circle (supports Least Squares and RANSAC) + + + Caliper Count + + + Number of calipers placed evenly around the circle + + + Caliper Width + + + Search length of each caliper along radial direction (pixels) + + + Edge Polarity + + + Edge direction: BrightToDark, DarkToBright, or Both + + + Edge Threshold + + + Gradient strength threshold; edges below this value are ignored + + + Smoothing Sigma + + + Gaussian smoothing standard deviation for noise suppression + + + Search Direction + + + Caliper search direction: Inward (toward center), Outward (away from center), Both + + + Fit Method + + + Circle fitting algorithm: LeastSquares or RANSAC (robust, rejects outliers) + + + RANSAC Threshold + + + RANSAC inlier distance threshold (pixels); points closer than this to the circle are inliers + + + Line Thickness + + + Drawing thickness for result visualization + \ No newline at end of file diff --git a/XP.Common/Resources/Resources.resx b/XP.Common/Resources/Resources.resx index eac5543..e102321 100644 --- a/XP.Common/Resources/Resources.resx +++ b/XP.Common/Resources/Resources.resx @@ -1976,4 +1976,66 @@ 绘制结果的线条粗细 + + + + 边缘查找拟合圆 + + + 沿预估圆周放置卡尺检测边缘点,拟合圆(支持最小二乘和RANSAC) + + + 卡尺数量 + + + 沿圆周等角度放置的卡尺数量 + + + 卡尺宽度 + + + 每个卡尺沿径向的搜索长度(像素) + + + 边缘极性 + + + 边缘方向:BrightToDark(亮到暗)、DarkToBright(暗到亮)、Both(双向) + + + 边缘阈值 + + + 边缘梯度强度阈值,低于此值的边缘将被忽略 + + + 平滑Sigma + + + 高斯平滑的标准差,用于抑制噪声 + + + 搜索方向 + + + 卡尺搜索方向:Inward(向圆心)、Outward(背离圆心)、Both(双向) + + + 拟合方法 + + + 圆拟合算法:LeastSquares(最小二乘)、RANSAC(鲁棒拟合) + + + RANSAC阈值 + + + RANSAC内点判定距离阈值(像素),点到圆周距离小于此值视为内点 + + + 线条粗细 + + + 绘制结果的线条粗细 + \ No newline at end of file diff --git a/XP.Common/Resources/Resources.zh-CN.resx b/XP.Common/Resources/Resources.zh-CN.resx index 0d99bfa..f3ce870 100644 --- a/XP.Common/Resources/Resources.zh-CN.resx +++ b/XP.Common/Resources/Resources.zh-CN.resx @@ -1937,4 +1937,66 @@ 绘制结果的线条粗细 + + + + 边缘查找拟合圆 + + + 沿预估圆周放置卡尺检测边缘点,拟合圆(支持最小二乘和RANSAC) + + + 卡尺数量 + + + 沿圆周等角度放置的卡尺数量 + + + 卡尺宽度 + + + 每个卡尺沿径向的搜索长度(像素) + + + 边缘极性 + + + 边缘方向:BrightToDark(亮到暗)、DarkToBright(暗到亮)、Both(双向) + + + 边缘阈值 + + + 边缘梯度强度阈值,低于此值的边缘将被忽略 + + + 平滑Sigma + + + 高斯平滑的标准差,用于抑制噪声 + + + 搜索方向 + + + 卡尺搜索方向:Inward(向圆心)、Outward(背离圆心)、Both(双向) + + + 拟合方法 + + + 圆拟合算法:LeastSquares(最小二乘)、RANSAC(鲁棒拟合) + + + RANSAC阈值 + + + RANSAC内点判定距离阈值(像素),点到圆周距离小于此值视为内点 + + + 线条粗细 + + + 绘制结果的线条粗细 + \ No newline at end of file diff --git a/XP.ImageProcessing.Processors/检测分析/EdgeCircleFitProcessor.cs b/XP.ImageProcessing.Processors/检测分析/EdgeCircleFitProcessor.cs new file mode 100644 index 0000000..2386c81 --- /dev/null +++ b/XP.ImageProcessing.Processors/检测分析/EdgeCircleFitProcessor.cs @@ -0,0 +1,582 @@ +// ============================================================================ +// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. +// 文件名: EdgeCircleFitProcessor.cs +// 描述: 边缘查找拟合圆算子 +// 功能: +// - 沿预估圆周等角度放置卡尺,每个卡尺沿径向搜索边缘点 +// - 支持亚像素精度(抛物线插值) +// - 支持边缘极性选择和搜索方向(向内/向外) +// - 使用最小二乘或RANSAC算法拟合圆 +// - 输出拟合圆参数、边缘点、内点/外点、拟合误差 +// 算法: 卡尺边缘检测 + 最小二乘/RANSAC圆拟合 +// 作者: 李伟 wei.lw.li@hexagon.com +// ============================================================================ + +using Emgu.CV; +using Emgu.CV.Structure; +using XP.ImageProcessing.Core; +using Serilog; +using System.Drawing; + +namespace XP.ImageProcessing.Processors; + +/// +/// 圆拟合结果 +/// +public class CircleFitResult +{ + /// 拟合是否成功 + public bool Success { get; set; } + + /// 拟合圆心X + public double CenterX { get; set; } + + /// 拟合圆心Y + public double CenterY { get; set; } + + /// 拟合半径 + public double Radius { get; set; } + + /// 所有检测到的边缘点 + public List EdgePoints { get; set; } = new(); + + /// 内点列表 + public List Inliers { get; set; } = new(); + + /// 外点列表 + public List Outliers { get; set; } = new(); + + /// 平均拟合误差(像素) + public double FitError { get; set; } + + /// 有效边缘点数 + public int EdgePointCount { get; set; } +} + +/// +/// 边缘查找拟合圆算子 - 沿预估圆周放置卡尺检测边缘点并拟合圆 +/// +public class EdgeCircleFitProcessor : ImageProcessorBase +{ + private static readonly ILogger _logger = Log.ForContext(); + private static readonly Random _random = new(); + + public EdgeCircleFitProcessor() + { + Name = LocalizationHelper.GetString("EdgeCircleFitProcessor_Name"); + Description = LocalizationHelper.GetString("EdgeCircleFitProcessor_Description"); + } + + protected override void InitializeParameters() + { + // ── 预估圆参数(由UI交互注入,不可见) ── + Parameters.Add("CenterX", new ProcessorParameter( + "CenterX", "CenterX", typeof(int), 200, null, null, "") { IsVisible = false }); + Parameters.Add("CenterY", new ProcessorParameter( + "CenterY", "CenterY", typeof(int), 200, null, null, "") { IsVisible = false }); + Parameters.Add("Radius", new ProcessorParameter( + "Radius", "Radius", typeof(int), 100, null, null, "") { IsVisible = false }); + + // ── 卡尺参数 ── + Parameters.Add("CaliperCount", new ProcessorParameter( + "CaliperCount", + LocalizationHelper.GetString("EdgeCircleFitProcessor_CaliperCount"), + typeof(int), 36, 3, 360, + LocalizationHelper.GetString("EdgeCircleFitProcessor_CaliperCount_Desc"))); + + Parameters.Add("CaliperWidth", new ProcessorParameter( + "CaliperWidth", + LocalizationHelper.GetString("EdgeCircleFitProcessor_CaliperWidth"), + typeof(int), 40, 5, 500, + LocalizationHelper.GetString("EdgeCircleFitProcessor_CaliperWidth_Desc"))); + + // ── 边缘检测参数 ── + Parameters.Add("EdgePolarity", new ProcessorParameter( + "EdgePolarity", + LocalizationHelper.GetString("EdgeCircleFitProcessor_EdgePolarity"), + typeof(string), "Both", null, null, + LocalizationHelper.GetString("EdgeCircleFitProcessor_EdgePolarity_Desc"), + new string[] { "BrightToDark", "DarkToBright", "Both" })); + + Parameters.Add("EdgeThreshold", new ProcessorParameter( + "EdgeThreshold", + LocalizationHelper.GetString("EdgeCircleFitProcessor_EdgeThreshold"), + typeof(int), 20, 1, 255, + LocalizationHelper.GetString("EdgeCircleFitProcessor_EdgeThreshold_Desc"))); + + Parameters.Add("Sigma", new ProcessorParameter( + "Sigma", + LocalizationHelper.GetString("EdgeCircleFitProcessor_Sigma"), + typeof(double), 1.0, 0.1, 10.0, + LocalizationHelper.GetString("EdgeCircleFitProcessor_Sigma_Desc"))); + + Parameters.Add("SearchDirection", new ProcessorParameter( + "SearchDirection", + LocalizationHelper.GetString("EdgeCircleFitProcessor_SearchDirection"), + typeof(string), "Both", null, null, + LocalizationHelper.GetString("EdgeCircleFitProcessor_SearchDirection_Desc"), + new string[] { "Inward", "Outward", "Both" })); + + // ── 拟合参数 ── + Parameters.Add("FitMethod", new ProcessorParameter( + "FitMethod", + LocalizationHelper.GetString("EdgeCircleFitProcessor_FitMethod"), + typeof(string), "RANSAC", null, null, + LocalizationHelper.GetString("EdgeCircleFitProcessor_FitMethod_Desc"), + new string[] { "LeastSquares", "RANSAC" })); + + Parameters.Add("RansacThreshold", new ProcessorParameter( + "RansacThreshold", + LocalizationHelper.GetString("EdgeCircleFitProcessor_RansacThreshold"), + typeof(double), 2.0, 0.5, 20.0, + LocalizationHelper.GetString("EdgeCircleFitProcessor_RansacThreshold_Desc"))); + + Parameters.Add("Thickness", new ProcessorParameter( + "Thickness", + LocalizationHelper.GetString("EdgeCircleFitProcessor_Thickness"), + typeof(int), 2, 1, 10, + LocalizationHelper.GetString("EdgeCircleFitProcessor_Thickness_Desc"))); + } + + public override Image Process(Image inputImage) + { + int centerX = GetParameter("CenterX"); + int centerY = GetParameter("CenterY"); + int radius = GetParameter("Radius"); + int caliperCount = GetParameter("CaliperCount"); + int caliperWidth = GetParameter("CaliperWidth"); + string edgePolarity = GetParameter("EdgePolarity"); + int edgeThreshold = GetParameter("EdgeThreshold"); + double sigma = GetParameter("Sigma"); + string searchDirection = GetParameter("SearchDirection"); + string fitMethod = GetParameter("FitMethod"); + double ransacThreshold = GetParameter("RansacThreshold"); + + OutputData.Clear(); + + _logger.Debug( + "EdgeCircleFit started: Center=({CX},{CY}), R={R}, Calipers={Count}, Width={Width}", + centerX, centerY, radius, caliperCount, caliperWidth); + + if (radius < 5) + { + _logger.Warning("Radius too small for circle fitting"); + OutputData["CircleFitResult"] = new CircleFitResult { Success = false }; + return inputImage.Clone(); + } + + // 沿圆周等角度放置卡尺 + var edgePoints = new List(); + double angleStep = 2.0 * Math.PI / caliperCount; + + for (int i = 0; i < caliperCount; i++) + { + double angle = angleStep * i; + // 圆周上的采样点 + double sampleX = centerX + radius * Math.Cos(angle); + double sampleY = centerY + radius * Math.Sin(angle); + + // 径向方向(从圆心指向外) + double dirX = Math.Cos(angle); + double dirY = Math.Sin(angle); + + // 根据搜索方向确定卡尺搜索方向 + double searchDirX, searchDirY; + if (searchDirection == "Inward") + { + searchDirX = -dirX; + searchDirY = -dirY; + } + else if (searchDirection == "Outward") + { + searchDirX = dirX; + searchDirY = dirY; + } + else // Both: 搜索方向为径向(从内到外),卡尺中心在圆周上 + { + searchDirX = dirX; + searchDirY = dirY; + } + + var edgePoint = FindEdgeInCaliper( + inputImage, sampleX, sampleY, searchDirX, searchDirY, + caliperWidth, edgePolarity, edgeThreshold, sigma, i); + + if (edgePoint != null) + { + edgePoints.Add(edgePoint); + } + } + + _logger.Debug("Found {Count} edge points from {Total} calipers", edgePoints.Count, caliperCount); + + // 拟合圆 + var result = FitCircle(edgePoints, fitMethod, ransacThreshold); + + // 存储输出 + OutputData["CircleFitResult"] = result; + OutputData["EdgePoints"] = edgePoints.Select(p => p.Position).ToArray(); + OutputData["EdgePointCount"] = edgePoints.Count; + OutputData["Thickness"] = GetParameter("Thickness"); + + if (result.Success) + { + OutputData["FittedCenterX"] = result.CenterX; + OutputData["FittedCenterY"] = result.CenterY; + OutputData["FittedRadius"] = result.Radius; + OutputData["InlierPoints"] = result.Inliers.ToArray(); + OutputData["OutlierPoints"] = result.Outliers.ToArray(); + OutputData["FitError"] = result.FitError; + + _logger.Information( + "EdgeCircleFit completed: Center=({CX:F2},{CY:F2}), R={R:F2}, Inliers={Inliers}/{Total}, Error={Error:F3}px", + result.CenterX, result.CenterY, result.Radius, + result.Inliers.Count, edgePoints.Count, result.FitError); + } + else + { + _logger.Warning("EdgeCircleFit failed: insufficient edge points"); + } + + return inputImage.Clone(); + } + + // ══════════════════════════════════════════════════════════════ + // 卡尺边缘检测(复用直线拟合中的逻辑) + // ══════════════════════════════════════════════════════════════ + + private EdgePointInfo? FindEdgeInCaliper( + Image image, + double centerX, double centerY, + double dirX, double dirY, + int caliperWidth, string polarity, + int threshold, double sigma, int caliperIndex) + { + int halfWidth = caliperWidth / 2; + int profileLength = caliperWidth; + + var profile = new double[profileLength]; + int validCount = 0; + + for (int i = 0; i < profileLength; i++) + { + double offset = i - halfWidth; + double px = centerX + dirX * offset; + double py = centerY + dirY * offset; + + int ix = (int)Math.Round(px); + int iy = (int)Math.Round(py); + + if (ix >= 0 && ix < image.Width && iy >= 0 && iy < image.Height) + { + profile[i] = image.Data[iy, ix, 0]; + validCount++; + } + else + { + profile[i] = 0; + } + } + + if (validCount < profileLength * 0.5) + return null; + + if (sigma > 0.1) + profile = GaussianSmooth1D(profile, sigma); + + var derivative = new double[profileLength]; + for (int i = 1; i < profileLength - 1; i++) + derivative[i] = (profile[i + 1] - profile[i - 1]) / 2.0; + + int bestIdx = -1; + double bestStrength = 0; + + for (int i = 2; i < profileLength - 2; i++) + { + double strength = derivative[i]; + bool validPolarity = polarity switch + { + "BrightToDark" => strength < 0, + "DarkToBright" => strength > 0, + _ => true + }; + + if (!validPolarity) continue; + + double absStrength = Math.Abs(strength); + if (absStrength >= threshold && absStrength > bestStrength) + { + bestStrength = absStrength; + bestIdx = i; + } + } + + if (bestIdx < 0) + return null; + + // 亚像素插值 + double subPixelOffset = 0; + if (bestIdx > 0 && bestIdx < profileLength - 1) + { + double left = Math.Abs(derivative[bestIdx - 1]); + double center = Math.Abs(derivative[bestIdx]); + double right = Math.Abs(derivative[bestIdx + 1]); + double denom = 2.0 * (2.0 * center - left - right); + if (Math.Abs(denom) > 1e-6) + { + subPixelOffset = (left - right) / denom; + subPixelOffset = Math.Clamp(subPixelOffset, -0.5, 0.5); + } + } + + double edgeOffset = (bestIdx + subPixelOffset) - halfWidth; + float edgeX = (float)(centerX + dirX * edgeOffset); + float edgeY = (float)(centerY + dirY * edgeOffset); + + return new EdgePointInfo + { + Position = new PointF(edgeX, edgeY), + Strength = bestStrength, + CaliperIndex = caliperIndex, + IsInlier = true + }; + } + + private static double[] GaussianSmooth1D(double[] data, double sigma) + { + int kernelRadius = (int)Math.Ceiling(sigma * 3); + int kernelSize = kernelRadius * 2 + 1; + var kernel = new double[kernelSize]; + double sum = 0; + + for (int i = 0; i < kernelSize; i++) + { + double x = i - kernelRadius; + kernel[i] = Math.Exp(-x * x / (2.0 * sigma * sigma)); + sum += kernel[i]; + } + for (int i = 0; i < kernelSize; i++) + kernel[i] /= sum; + + var result = new double[data.Length]; + for (int i = 0; i < data.Length; i++) + { + double val = 0, wSum = 0; + for (int k = 0; k < kernelSize; k++) + { + int idx = i + k - kernelRadius; + if (idx >= 0 && idx < data.Length) + { + val += data[idx] * kernel[k]; + wSum += kernel[k]; + } + } + result[i] = wSum > 0 ? val / wSum : data[i]; + } + return result; + } + + // ══════════════════════════════════════════════════════════════ + // 圆拟合 + // ══════════════════════════════════════════════════════════════ + + private CircleFitResult FitCircle(List edgePoints, string method, double ransacThreshold) + { + var result = new CircleFitResult(); + + if (edgePoints.Count < 3) + { + result.Success = false; + return result; + } + + if (method == "RANSAC" && edgePoints.Count >= 4) + return FitCircleRANSAC(edgePoints, ransacThreshold); + else + return FitCircleLeastSquares(edgePoints); + } + + /// + /// 最小二乘拟合圆(Kasa方法) + /// 将 (x-a)² + (y-b)² = r² 展开为: x² + y² = 2ax + 2by + (r²-a²-b²) + /// 令 c = r²-a²-b², 线性方程: 2ax + 2by + c = x² + y² + /// + private CircleFitResult FitCircleLeastSquares(List edgePoints) + { + var points = edgePoints.Select(p => p.Position).ToArray(); + var (cx, cy, r) = KasaFit(points); + + var result = new CircleFitResult + { + Success = true, + CenterX = cx, + CenterY = cy, + Radius = r, + Inliers = points.ToList(), + Outliers = new List(), + EdgePointCount = edgePoints.Count, + EdgePoints = edgePoints + }; + + foreach (var ep in edgePoints) + ep.IsInlier = true; + + result.FitError = ComputeCircleFitError(points, cx, cy, r); + return result; + } + + /// + /// RANSAC 圆拟合 + /// + private CircleFitResult FitCircleRANSAC(List edgePoints, double threshold) + { + var result = new CircleFitResult(); + var points = edgePoints.Select(p => p.Position).ToArray(); + int n = points.Length; + + int maxIterations = Math.Min(2000, n * (n - 1) * (n - 2) / 6); + int bestInlierCount = 0; + double bestCx = 0, bestCy = 0, bestR = 0; + List bestInlierIndices = new(); + + for (int iter = 0; iter < maxIterations; iter++) + { + // 随机选3个点 + int i1 = _random.Next(n), i2 = _random.Next(n), i3 = _random.Next(n); + if (i1 == i2 || i1 == i3 || i2 == i3) continue; + + var (cx, cy, r) = FitCircleFrom3Points(points[i1], points[i2], points[i3]); + if (r <= 0 || double.IsNaN(r)) continue; + + // 统计内点 + var inlierIndices = new List(); + for (int i = 0; i < n; i++) + { + double dist = Math.Abs(Distance(points[i], cx, cy) - r); + if (dist <= threshold) + inlierIndices.Add(i); + } + + if (inlierIndices.Count > bestInlierCount) + { + bestInlierCount = inlierIndices.Count; + bestInlierIndices = inlierIndices; + + // 用所有内点重新拟合 + var inlierPoints = inlierIndices.Select(i => points[i]).ToArray(); + (bestCx, bestCy, bestR) = KasaFit(inlierPoints); + } + + if (bestInlierCount > n * 0.95) + break; + } + + if (bestInlierCount < 3) + { + result.Success = false; + return result; + } + + result.Success = true; + result.CenterX = bestCx; + result.CenterY = bestCy; + result.Radius = bestR; + + var inlierSet = new HashSet(bestInlierIndices); + for (int i = 0; i < n; i++) + { + if (inlierSet.Contains(i)) + { + result.Inliers.Add(points[i]); + edgePoints[i].IsInlier = true; + } + else + { + result.Outliers.Add(points[i]); + edgePoints[i].IsInlier = false; + } + } + + result.FitError = ComputeCircleFitError(result.Inliers.ToArray(), bestCx, bestCy, bestR); + result.EdgePointCount = edgePoints.Count; + result.EdgePoints = edgePoints; + + return result; + } + + /// + /// Kasa 最小二乘圆拟合 + /// + private static (double cx, double cy, double r) KasaFit(PointF[] points) + { + int n = points.Length; + if (n < 3) return (0, 0, 0); + + // 构建线性方程组: A * [a, b, c]^T = B + // 其中 2*a*xi + 2*b*yi + c = xi² + yi² + double sumX = 0, sumY = 0, sumX2 = 0, sumY2 = 0; + double sumXY = 0, sumX3 = 0, sumY3 = 0, sumX2Y = 0, sumXY2 = 0; + + for (int i = 0; i < n; i++) + { + double x = points[i].X, y = points[i].Y; + double x2 = x * x, y2 = y * y; + sumX += x; sumY += y; + sumX2 += x2; sumY2 += y2; + sumXY += x * y; + sumX3 += x2 * x; sumY3 += y2 * y; + sumX2Y += x2 * y; sumXY2 += x * y2; + } + + double A = n * sumX2 - sumX * sumX; + double B = n * sumXY - sumX * sumY; + double C = n * sumY2 - sumY * sumY; + double D = 0.5 * (n * (sumX3 + sumXY2) - sumX * (sumX2 + sumY2)); + double E = 0.5 * (n * (sumX2Y + sumY3) - sumY * (sumX2 + sumY2)); + + double denom = A * C - B * B; + if (Math.Abs(denom) < 1e-10) + return (0, 0, 0); + + double cx = (D * C - B * E) / denom; + double cy = (A * E - B * D) / denom; + double r = Math.Sqrt((sumX2 + sumY2 - 2 * cx * sumX - 2 * cy * sumY) / n + cx * cx + cy * cy); + + return (cx, cy, r); + } + + /// + /// 3点拟合圆 + /// + private static (double cx, double cy, double r) FitCircleFrom3Points(PointF p1, PointF p2, PointF p3) + { + double ax = p1.X, ay = p1.Y; + double bx = p2.X, by = p2.Y; + double cx = p3.X, cy = p3.Y; + + double d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)); + if (Math.Abs(d) < 1e-10) + return (0, 0, -1); + + double ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d; + double uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d; + double r = Math.Sqrt((ax - ux) * (ax - ux) + (ay - uy) * (ay - uy)); + + return (ux, uy, r); + } + + private static double Distance(PointF p, double cx, double cy) + { + double dx = p.X - cx, dy = p.Y - cy; + return Math.Sqrt(dx * dx + dy * dy); + } + + private static double ComputeCircleFitError(PointF[] points, double cx, double cy, double r) + { + if (points.Length == 0) return 0; + double total = 0; + foreach (var p in points) + total += Math.Abs(Distance(p, cx, cy) - r); + return total / points.Length; + } +} diff --git a/XplorePlane/ViewModels/ImageProcessing/EdgeCircleFitViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/EdgeCircleFitViewModel.cs new file mode 100644 index 0000000..2bee916 --- /dev/null +++ b/XplorePlane/ViewModels/ImageProcessing/EdgeCircleFitViewModel.cs @@ -0,0 +1,497 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; +using Emgu.CV; +using Emgu.CV.Structure; +using Prism.Commands; +using Prism.Mvvm; +using XP.ImageProcessing.Processors; +using XP.ImageProcessing.RoiControl.Controls; +using XplorePlane.Services.MainViewport; +using Brushes = System.Windows.Media.Brushes; +using Ellipse = System.Windows.Shapes.Ellipse; +using Point = System.Windows.Point; + +namespace XplorePlane.ViewModels.ImageProcessing +{ + /// + /// 边缘查找拟合圆 ViewModel + /// 交互:3点定义预估圆,手柄可调整圆心和半径,点击拟合执行 + /// + public class EdgeCircleFitViewModel : BindableBase + { + private readonly IMainViewportService _viewportService; + private PolygonRoiCanvas _canvas; + private Canvas _mainCanvas; + + // 预估圆 + private Point _center; + private double _radius; + private bool _circleDefined; + + // 可视化 + private readonly List _tempOverlays = new(); + private readonly List _committedOverlays = new(); + + // 手柄位置 + private Point _handleCenterPos; + private Point _handleRadiusPos; // 圆周上0°位置 + + // 交互 + private enum DragTarget { None, Center, Radius } + private DragTarget _dragging = DragTarget.None; + private bool _isDrawing; + private int _fitCount; + + private const double HandleSize = 12; + private const double HitRadius = 10; + private static readonly SolidColorBrush CaliperStroke; + private static readonly SolidColorBrush CaliperFill; + private static readonly SolidColorBrush FitCircleBrush; + private static readonly SolidColorBrush HandleFill; + + static EdgeCircleFitViewModel() + { + CaliperStroke = new SolidColorBrush(Color.FromRgb(0, 255, 0)); + CaliperStroke.Freeze(); + CaliperFill = new SolidColorBrush(Color.FromArgb(15, 0, 255, 0)); + CaliperFill.Freeze(); + FitCircleBrush = new SolidColorBrush(Color.FromRgb(30, 144, 255)); + FitCircleBrush.Freeze(); + HandleFill = new SolidColorBrush(Color.FromArgb(220, 255, 255, 255)); + HandleFill.Freeze(); + } + + public EdgeCircleFitViewModel(IMainViewportService viewportService) + { + _viewportService = viewportService; + FitCommand = new DelegateCommand(ExecuteFit, () => _circleDefined); + ClearAllCommand = new DelegateCommand(ExecuteClearAll); + DrawCircleCommand = new DelegateCommand(ExecuteDrawCircle); + } + + // ── 命令 ── + public DelegateCommand FitCommand { get; } + public DelegateCommand ClearAllCommand { get; } + public DelegateCommand DrawCircleCommand { get; } + + // ── 参数 ── + private int _caliperCount = 36; + public int CaliperCount { get => _caliperCount; set { if (SetProperty(ref _caliperCount, value)) RedrawTemp(); } } + + private int _caliperWidth = 40; + public int CaliperWidth { get => _caliperWidth; set { if (SetProperty(ref _caliperWidth, value)) RedrawTemp(); } } + + private string _edgePolarity = "Both"; + public string EdgePolarity { get => _edgePolarity; set { if (SetProperty(ref _edgePolarity, value)) RedrawTemp(); } } + + private int _edgeThreshold = 20; + public int EdgeThreshold { get => _edgeThreshold; set => SetProperty(ref _edgeThreshold, value); } + + private double _sigma = 1.0; + public double Sigma { get => _sigma; set => SetProperty(ref _sigma, value); } + + private string _searchDirection = "Both"; + public string SearchDirection { get => _searchDirection; set => SetProperty(ref _searchDirection, value); } + + private string _fitMethod = "RANSAC"; + public string FitMethod { get => _fitMethod; set => SetProperty(ref _fitMethod, value); } + + private double _ransacThreshold = 2.0; + public double RansacThreshold { get => _ransacThreshold; set => SetProperty(ref _ransacThreshold, value); } + + private string _resultText = "Ready - click Draw Circle"; + public string ResultText { get => _resultText; set => SetProperty(ref _resultText, value); } + + // ── 初始化 ── + public void SetCanvas(PolygonRoiCanvas canvas) + { + _canvas = canvas; + _mainCanvas = FindChild(canvas, "mainCanvas"); + } + + public void OnPanelClosed() + { + UnregisterAll(); + ClearTempOverlays(); + } + + // ══════════════════════════════════════════════════════════════ + // 命令 + // ══════════════════════════════════════════════════════════════ + + private void ExecuteDrawCircle() + { + ClearTempOverlays(); + UnregisterAll(); + _circleDefined = false; + _dragging = DragTarget.None; + FitCommand.RaiseCanExecuteChanged(); + _isDrawing = true; + ResultText = "Press and drag to define circle (center → radius)"; + RegisterInteraction(); + } + + private void ExecuteFit() + { + if (!_circleDefined) return; + + // 清除上一次拟合结果 + ClearCommitted(); + + var imageSource = _viewportService?.CurrentDisplayImage as BitmapSource; + if (imageSource == null) { ResultText = "Error: no image"; return; } + + try + { + BitmapSource source = imageSource; + if (imageSource.Format != PixelFormats.Gray8) + source = new FormatConvertedBitmap(imageSource, PixelFormats.Gray8, null, 0); + + int w = source.PixelWidth, h = source.PixelHeight; + int stride = w; + byte[] px = new byte[h * stride]; + source.CopyPixels(px, stride, 0); + + using var img = new Image(w, h); + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + img.Data[y, x, 0] = px[y * stride + x]; + + var proc = new EdgeCircleFitProcessor(); + proc.SetParameter("CenterX", (int)_center.X); + proc.SetParameter("CenterY", (int)_center.Y); + proc.SetParameter("Radius", (int)_radius); + proc.SetParameter("CaliperCount", CaliperCount); + proc.SetParameter("CaliperWidth", CaliperWidth); + proc.SetParameter("EdgePolarity", EdgePolarity); + proc.SetParameter("EdgeThreshold", EdgeThreshold); + proc.SetParameter("Sigma", Sigma); + proc.SetParameter("SearchDirection", SearchDirection); + proc.SetParameter("FitMethod", FitMethod); + proc.SetParameter("RansacThreshold", RansacThreshold); + + var result = proc.Process(img); + var od = proc.OutputData; + + if (od.ContainsKey("CircleFitResult")) + { + var fr = od["CircleFitResult"] as CircleFitResult; + if (fr != null && fr.Success) + { + _fitCount++; + DrawFitResult(fr); + ResultText = $"[#{_fitCount}] Fit OK\nCenter: ({fr.CenterX:F1}, {fr.CenterY:F1})\n" + + $"Radius: {fr.Radius:F2} px\n" + + $"Inliers: {fr.Inliers.Count}/{fr.EdgePointCount}\n" + + $"Error: {fr.FitError:F3} px\n\nAdjust and fit again, or draw new"; + } + else + { + int ec = od.ContainsKey("EdgePointCount") ? (int)od["EdgePointCount"] : 0; + ResultText = $"Fit failed\nEdge points: {ec}\nAdjust params or circle"; + } + } + result.Dispose(); + } + catch (Exception ex) { ResultText = $"Exception: {ex.Message}"; } + } + + private void ExecuteClearAll() + { + ClearTempOverlays(); + if (_mainCanvas != null) + foreach (var el in _committedOverlays) _mainCanvas.Children.Remove(el); + _committedOverlays.Clear(); + _fitCount = 0; + UnregisterAll(); + _circleDefined = false; + FitCommand.RaiseCanExecuteChanged(); + ResultText = "Cleared"; + } + + // ══════════════════════════════════════════════════════════════ + // 拟合结果绘制(永久) + // ══════════════════════════════════════════════════════════════ + + private void DrawFitResult(CircleFitResult fr) + { + if (_mainCanvas == null) return; + + // 拟合圆(蓝色) + var circle = new Ellipse + { + Width = fr.Radius * 2, Height = fr.Radius * 2, + Stroke = FitCircleBrush, StrokeThickness = 2, Fill = Brushes.Transparent, + IsHitTestVisible = false + }; + Canvas.SetLeft(circle, fr.CenterX - fr.Radius); + Canvas.SetTop(circle, fr.CenterY - fr.Radius); + AddCommitted(circle); + + // 圆心十字 + double cs = 6; + AddCommitted(new Line { X1 = fr.CenterX - cs, Y1 = fr.CenterY, X2 = fr.CenterX + cs, Y2 = fr.CenterY, Stroke = FitCircleBrush, StrokeThickness = 1.5, IsHitTestVisible = false }); + AddCommitted(new Line { X1 = fr.CenterX, Y1 = fr.CenterY - cs, X2 = fr.CenterX, Y2 = fr.CenterY + cs, Stroke = FitCircleBrush, StrokeThickness = 1.5, IsHitTestVisible = false }); + + // 标注 + var lbl = new TextBlock + { + Text = $"R:{fr.Radius:F1} C:({fr.CenterX:F1},{fr.CenterY:F1})", + Foreground = FitCircleBrush, FontSize = 11, FontWeight = FontWeights.Bold, IsHitTestVisible = false + }; + Canvas.SetLeft(lbl, fr.CenterX + 5); Canvas.SetTop(lbl, fr.CenterY - fr.Radius - 18); + AddCommitted(lbl); + } + + private void AddCommitted(UIElement el) { _mainCanvas.Children.Add(el); _committedOverlays.Add(el); } + + private void ClearCommitted() + { + if (_mainCanvas == null) return; + foreach (var el in _committedOverlays) _mainCanvas.Children.Remove(el); + _committedOverlays.Clear(); + } + + // ══════════════════════════════════════════════════════════════ + // 临时卡尺可视化 + // ══════════════════════════════════════════════════════════════ + + private void RedrawTemp() + { + if (!_circleDefined || _mainCanvas == null) return; + ClearTempOverlays(); + DrawTempCaliper(); + } + + private void DrawTempCaliper() + { + if (_mainCanvas == null || _radius < 5) return; + + // 预估圆(虚线) + var previewCircle = new Ellipse + { + Width = _radius * 2, Height = _radius * 2, + Stroke = CaliperStroke, StrokeThickness = 1, + StrokeDashArray = new DoubleCollection { 4, 3 }, + Fill = CaliperFill, IsHitTestVisible = false + }; + Canvas.SetLeft(previewCircle, _center.X - _radius); + Canvas.SetTop(previewCircle, _center.Y - _radius); + AddTemp(previewCircle); + + // 卡尺径向线 + int count = CaliperCount; + double halfW = CaliperWidth / 2.0; + double angleStep = 2.0 * Math.PI / count; + + for (int i = 0; i < count; i++) + { + double angle = angleStep * i; + double dirX = Math.Cos(angle), dirY = Math.Sin(angle); + double cx = _center.X + _radius * dirX; + double cy = _center.Y + _radius * dirY; + + AddTemp(new Line + { + X1 = cx - dirX * halfW, Y1 = cy - dirY * halfW, + X2 = cx + dirX * halfW, Y2 = cy + dirY * halfW, + Stroke = CaliperStroke, StrokeThickness = 1, Opacity = 0.4, IsHitTestVisible = false + }); + } + + // 手柄 + _handleCenterPos = _center; + _handleRadiusPos = new Point(_center.X + _radius, _center.Y); + + AddTemp(MakeHandle(_handleCenterPos)); + AddTemp(MakeHandle(_handleRadiusPos)); + } + + private void CommitCurrentCaliper() + { + if (_mainCanvas == null) return; + foreach (var el in _tempOverlays) _mainCanvas.Children.Remove(el); + _tempOverlays.Clear(); + + // 绘制永久卡尺(半透明) + var circle = new Ellipse + { + Width = _radius * 2, Height = _radius * 2, + Stroke = CaliperStroke, StrokeThickness = 1, Opacity = 0.4, + Fill = Brushes.Transparent, IsHitTestVisible = false + }; + Canvas.SetLeft(circle, _center.X - _radius); + Canvas.SetTop(circle, _center.Y - _radius); + AddCommitted(circle); + } + + private Ellipse MakeHandle(Point pos) + { + var h = new Ellipse + { + Width = HandleSize, Height = HandleSize, + Fill = HandleFill, Stroke = CaliperStroke, StrokeThickness = 2, + IsHitTestVisible = false + }; + Canvas.SetLeft(h, pos.X - HandleSize / 2); + Canvas.SetTop(h, pos.Y - HandleSize / 2); + return h; + } + + private void AddTemp(UIElement el) { _mainCanvas.Children.Add(el); _tempOverlays.Add(el); } + private void ClearTempOverlays() + { + if (_mainCanvas == null) return; + foreach (var el in _tempOverlays) _mainCanvas.Children.Remove(el); + _tempOverlays.Clear(); + } + + // ══════════════════════════════════════════════════════════════ + // 鼠标交互 + // ══════════════════════════════════════════════════════════════ + + private bool _interactionRegistered; + + private void RegisterInteraction() + { + if (_canvas == null || _interactionRegistered) return; + _canvas.PreviewMouseLeftButtonDown += OnMouseDown; + _canvas.PreviewMouseMove += OnMouseMove; + _canvas.PreviewMouseLeftButtonUp += OnMouseUp; + _interactionRegistered = true; + } + + private void UnregisterAll() + { + if (_canvas == null || !_interactionRegistered) return; + _canvas.PreviewMouseLeftButtonDown -= OnMouseDown; + _canvas.PreviewMouseMove -= OnMouseMove; + _canvas.PreviewMouseLeftButtonUp -= OnMouseUp; + _interactionRegistered = false; + _isDrawing = false; + _dragging = DragTarget.None; + } + + private void OnMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + if (_mainCanvas == null) return; + var pos = e.GetPosition(_mainCanvas); + + // 绘制模式:按下鼠标确定圆心,拖拽确定半径 + if (_isDrawing) + { + _center = pos; + _radius = 0; + _dragging = DragTarget.Radius; // 复用 Radius 拖拽逻辑 + _canvas.CaptureMouse(); + e.Handled = true; + return; + } + + // 拖拽手柄 + if (_circleDefined) + { + var target = HitTest(pos); + if (target != DragTarget.None) + { + _dragging = target; + _canvas.CaptureMouse(); + e.Handled = true; + } + } + } + + private void OnMouseMove(object sender, System.Windows.Input.MouseEventArgs e) + { + if (_dragging == DragTarget.None || _mainCanvas == null) return; + var pos = e.GetPosition(_mainCanvas); + + if (_dragging == DragTarget.Center) + { + _center = pos; + } + else if (_dragging == DragTarget.Radius) + { + _radius = Math.Max(5, Dist(pos, _center)); + } + + // 实时预览 + if (_radius >= 5) + { + _circleDefined = true; + ClearTempOverlays(); + DrawTempCaliper(); + } + e.Handled = true; + } + + private void OnMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + if (_dragging == DragTarget.None) return; + + // 绘制模式完成 + if (_isDrawing) + { + _isDrawing = false; + _dragging = DragTarget.None; + _canvas.ReleaseMouseCapture(); + + if (_radius >= 5) + { + _circleDefined = true; + FitCommand.RaiseCanExecuteChanged(); + RedrawTemp(); + ResultText = $"Circle defined: R={_radius:F0}px\nDrag handles to adjust\nClick Fit to execute"; + } + else + { + _circleDefined = false; + ResultText = "Circle too small, try again"; + } + e.Handled = true; + return; + } + + _dragging = DragTarget.None; + _canvas.ReleaseMouseCapture(); + ResultText = $"Circle: R={_radius:F0}px\nClick Fit to execute"; + e.Handled = true; + } + + private DragTarget HitTest(Point pos) + { + if (Dist(pos, _handleCenterPos) <= HitRadius) return DragTarget.Center; + if (Dist(pos, _handleRadiusPos) <= HitRadius) return DragTarget.Radius; + return DragTarget.None; + } + + // ══════════════════════════════════════════════════════════════ + // 辅助 + // ══════════════════════════════════════════════════════════════ + + private static double Dist(Point a, Point b) + { + double dx = a.X - b.X, dy = a.Y - b.Y; + return Math.Sqrt(dx * dx + dy * dy); + } + + private static T FindChild(DependencyObject parent, string name) where T : FrameworkElement + { + int count = VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < count; i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + if (child is T t && t.Name == name) return t; + var r = FindChild(child, name); + if (r != null) return r; + } + return null; + } + } +} diff --git a/XplorePlane/ViewModels/ImageProcessing/EdgeLineFitViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/EdgeLineFitViewModel.cs index b540dc4..f7fcd8b 100644 --- a/XplorePlane/ViewModels/ImageProcessing/EdgeLineFitViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/EdgeLineFitViewModel.cs @@ -168,6 +168,9 @@ namespace XplorePlane.ViewModels.ImageProcessing { if (!_lineDefined) return; + // 清除上一次拟合结果 + ClearCommitted(); + var imageSource = _viewportService?.CurrentDisplayImage as BitmapSource; if (imageSource == null) { ResultText = "错误:无可用图像"; return; } @@ -209,17 +212,10 @@ namespace XplorePlane.ViewModels.ImageProcessing if (fr != null && fr.Success) { _fitCount++; - // 将当前卡尺从临时转为永久 - CommitCurrentCaliper(); - // 绘制拟合结果(永久) - DrawFitResult(fr, _fitCount); + DrawFitResult(fr); ResultText = $"[#{_fitCount}] 拟合成功\n角度: {fr.AngleDegrees:F2}°\n" + $"内点: {fr.Inliers.Count}/{fr.EdgePointCount}\n" + - $"误差: {fr.FitError:F3} px\n\n点击「画卡尺」继续下一条"; - // 拟合完成后清除编辑状态,准备下一次 - _lineDefined = false; - FitCommand.RaiseCanExecuteChanged(); - UnregisterAll(); + $"误差: {fr.FitError:F3} px\n\n可继续调整后再次拟合"; } else { @@ -310,7 +306,7 @@ namespace XplorePlane.ViewModels.ImageProcessing // 绘制拟合结果(永久) // ══════════════════════════════════════════════════════════════ - private void DrawFitResult(LineFitResult fr, int index) + private void DrawFitResult(LineFitResult fr) { if (_mainCanvas == null) return; @@ -322,17 +318,10 @@ namespace XplorePlane.ViewModels.ImageProcessing Stroke = FitLineBrush, StrokeThickness = 2, IsHitTestVisible = false }); - // 内点 - foreach (var pt in fr.Inliers) - AddCommitted(MakeDot(pt.X, pt.Y, Brushes.Lime)); - // 外点 - foreach (var pt in fr.Outliers) - AddCommitted(MakeDot(pt.X, pt.Y, Brushes.Red)); - // 标注 var lbl = new TextBlock { - Text = $"#{index} ∠{fr.AngleDegrees:F2}° Err:{fr.FitError:F2}px", + Text = $"∠{fr.AngleDegrees:F2}°", Foreground = FitLineBrush, FontSize = 11, FontWeight = FontWeights.Bold, IsHitTestVisible = false }; Canvas.SetLeft(lbl, (fr.Endpoint1.X + fr.Endpoint2.X) / 2 + 5); @@ -340,19 +329,19 @@ namespace XplorePlane.ViewModels.ImageProcessing AddCommitted(lbl); } - private Ellipse MakeDot(double x, double y, SolidColorBrush fill) - { - var d = new Ellipse { Width = 5, Height = 5, Fill = fill, IsHitTestVisible = false }; - Canvas.SetLeft(d, x - 2.5); Canvas.SetTop(d, y - 2.5); - return d; - } - private void AddCommitted(UIElement el) { _mainCanvas.Children.Add(el); _committedOverlays.Add(el); } + private void ClearCommitted() + { + if (_mainCanvas == null) return; + foreach (var el in _committedOverlays) _mainCanvas.Children.Remove(el); + _committedOverlays.Clear(); + } + // ══════════════════════════════════════════════════════════════ // 临时卡尺可视化(编辑中,带手柄) // ══════════════════════════════════════════════════════════════ diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index a62cb20..60f3c59 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -144,6 +144,7 @@ namespace XplorePlane.ViewModels public DelegateCommand SharpenCommand { get; } public DelegateCommand EnhanceCommand { get; } public DelegateCommand EdgeLineFitCommand { get; } + public DelegateCommand EdgeCircleFitCommand { get; } // 设置命令 public DelegateCommand OpenLanguageSwitcherCommand { get; } @@ -352,6 +353,7 @@ namespace XplorePlane.ViewModels SharpenCommand = new DelegateCommand(ExecuteSharpen); EnhanceCommand = new DelegateCommand(ExecuteEnhance); EdgeLineFitCommand = new DelegateCommand(ExecuteEdgeLineFit); + EdgeCircleFitCommand = new DelegateCommand(ExecuteEdgeCircleFit); AxisResetCommand = new DelegateCommand(ExecuteAxisReset); OpenDoorCommand = new DelegateCommand(ExecuteOpenDoor); @@ -891,6 +893,7 @@ namespace XplorePlane.ViewModels private Window _bgaDetectionPanel; private Window _edgeLineFitPanel; + private Window _edgeCircleFitPanel; private void ExecuteBgaDetection() { @@ -1194,6 +1197,25 @@ namespace XplorePlane.ViewModels _edgeLineFitPanel.Show(); } + private void ExecuteEdgeCircleFit() + { + if (!CheckImageLoaded()) return; + _logger.Info("边缘查找拟合圆功能已触发"); + + if (_edgeCircleFitPanel != null && _edgeCircleFitPanel.IsVisible) + { + _edgeCircleFitPanel.Activate(); + return; + } + + _edgeCircleFitPanel = new Views.ImageProcessing.EdgeCircleFitPanel + { + Owner = System.Windows.Application.Current.MainWindow + }; + _edgeCircleFitPanel.Closed += (_, _) => { _edgeCircleFitPanel = null; }; + _edgeCircleFitPanel.Show(); + } + private Image? BitmapSourceToImage(BitmapSource bitmapSource) { // 转换为可用的图像格式 diff --git a/XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml b/XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml new file mode 100644 index 0000000..c13d41d --- /dev/null +++ b/XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Both + BrightToDark + DarkToBright + + + + Both + Inward + Outward + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml.cs b/XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml.cs new file mode 100644 index 0000000..5f2bb09 --- /dev/null +++ b/XplorePlane/Views/ImageProcessing/EdgeCircleFitPanel.xaml.cs @@ -0,0 +1,52 @@ +using System.Windows; +using Prism.Ioc; +using XP.ImageProcessing.RoiControl.Controls; +using XplorePlane.Services.MainViewport; +using XplorePlane.ViewModels.ImageProcessing; + +namespace XplorePlane.Views.ImageProcessing +{ + public partial class EdgeCircleFitPanel : Window + { + public EdgeCircleFitPanel() + { + InitializeComponent(); + + var viewportService = ContainerLocator.Current?.Resolve(); + DataContext = new EdgeCircleFitViewModel(viewportService); + + Loaded += (s, e) => + { + var mainWin = Owner as MainWindow; + if (mainWin != null) + { + var canvas = FindChild(mainWin); + if (DataContext is EdgeCircleFitViewModel vm) + { + vm.SetCanvas(canvas); + vm.DrawCircleCommand.Execute(); + } + } + }; + + Closed += (s, e) => + { + if (DataContext is EdgeCircleFitViewModel vm) + vm.OnPanelClosed(); + }; + } + + private static T FindChild(DependencyObject parent) where T : DependencyObject + { + int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < count; i++) + { + var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, i); + if (child is T t) return t; + var result = FindChild(child); + if (result != null) return result; + } + return null; + } + } +} diff --git a/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml b/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml index f3868a1..e530710 100644 --- a/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml +++ b/XplorePlane/Views/ImageProcessing/EdgeLineFitPanel.xaml @@ -101,11 +101,6 @@ - - - RANSAC - LeastSquares - diff --git a/XplorePlane/Views/Main/MainWindow.xaml b/XplorePlane/Views/Main/MainWindow.xaml index 49122a0..bfa1636 100644 --- a/XplorePlane/Views/Main/MainWindow.xaml +++ b/XplorePlane/Views/Main/MainWindow.xaml @@ -537,6 +537,7 @@