// ============================================================================ // Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. // 文件名: EllipseDetectionProcessor.cs // 描述: 椭圆检测算子,基于轮廓分析和椭圆拟合检测图像中的椭圆 // 功能: // - 阈值分割 + 轮廓提取 // - 椭圆拟合(FitEllipse) // - 面积/轴长/离心率/拟合误差多维过滤 // - 支持双阈值分割和 Otsu 自动阈值 // 算法: 阈值分割 + OpenCV FitEllipse // 作者: 李伟 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 EllipseInfo { /// 序号 public int Index { get; set; } /// 中心点X public float CenterX { get; set; } /// 中心点Y public float CenterY { get; set; } /// 长轴长度 public float MajorAxis { get; set; } /// 短轴长度 public float MinorAxis { get; set; } /// 旋转角度(度) public float Angle { get; set; } /// 面积 public double Area { get; set; } /// 周长 public double Perimeter { get; set; } /// 离心率 (0=圆, 接近1=扁椭圆) public double Eccentricity { get; set; } /// 拟合误差(像素) public double FitError { get; set; } /// 轮廓点集 public Point[] ContourPoints { get; set; } = Array.Empty(); /// 外接矩形 public Rectangle BoundingBox { get; set; } } /// /// 椭圆检测器 /// public class EllipseDetector { private static readonly ILogger _logger = Log.ForContext(); public int MinThreshold { get; set; } = 64; public int MaxThreshold { get; set; } = 192; public bool UseOtsu { get; set; } = false; public int MinContourPoints { get; set; } = 30; public double MinArea { get; set; } = 100; public double MaxArea { get; set; } = 1000000; public float MinMajorAxis { get; set; } = 10; public double MaxEccentricity { get; set; } = 0.95; public double MaxFitError { get; set; } = 5.0; public int Thickness { get; set; } = 2; /// 执行椭圆检测 public List Detect(Image inputImage, Image? roiMask = null) { _logger.Debug("Ellipse detection started: UseOtsu={UseOtsu}, MinThreshold={Min}, MaxThreshold={Max}", UseOtsu, MinThreshold, MaxThreshold); var results = new List(); using var binary = new Image(inputImage.Size); if (UseOtsu) { CvInvoke.Threshold(inputImage, binary, MinThreshold, 255, ThresholdType.Otsu); _logger.Debug("Using Otsu auto threshold"); } else { // 双阈值分割:介于MinThreshold和MaxThreshold之间的为前景(255),其他为背景(0) byte[,,] inputData = inputImage.Data; byte[,,] outputData = binary.Data; int height = inputImage.Height; int width = inputImage.Width; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { byte pixelValue = inputData[y, x, 0]; outputData[y, x, 0] = (pixelValue >= MinThreshold && pixelValue <= MaxThreshold) ? (byte)255 : (byte)0; } } _logger.Debug("Dual threshold segmentation: MinThreshold={Min}, MaxThreshold={Max}", MinThreshold, MaxThreshold); } // 应用ROI掩码 if (roiMask != null) { CvInvoke.BitwiseAnd(binary, roiMask, binary); } using var contours = new VectorOfVectorOfPoint(); using var hierarchy = new Mat(); CvInvoke.FindContours(binary, contours, hierarchy, RetrType.List, ChainApproxMethod.ChainApproxNone); _logger.Debug("Found {Count} contours", contours.Size); int index = 0; for (int i = 0; i < contours.Size; i++) { var contour = contours[i]; if (contour.Size < Math.Max(5, MinContourPoints)) continue; double area = CvInvoke.ContourArea(contour); if (area < MinArea || area > MaxArea) continue; RotatedRect ellipseRect = CvInvoke.FitEllipse(contour); float majorAxis = Math.Max(ellipseRect.Size.Width, ellipseRect.Size.Height); float minorAxis = Math.Min(ellipseRect.Size.Width, ellipseRect.Size.Height); if (majorAxis < MinMajorAxis) continue; double eccentricity = 0; if (majorAxis > 0) { double ratio = minorAxis / majorAxis; eccentricity = Math.Sqrt(1.0 - ratio * ratio); } if (eccentricity > MaxEccentricity) continue; double fitError = ComputeFitError(contour.ToArray(), ellipseRect); if (fitError > MaxFitError) continue; results.Add(new EllipseInfo { Index = index++, CenterX = ellipseRect.Center.X, CenterY = ellipseRect.Center.Y, MajorAxis = majorAxis, MinorAxis = minorAxis, Angle = ellipseRect.Angle, Area = area, Perimeter = CvInvoke.ArcLength(contour, true), Eccentricity = eccentricity, FitError = fitError, ContourPoints = contour.ToArray(), BoundingBox = CvInvoke.BoundingRectangle(contour) }); } _logger.Information("Ellipse detection completed: detected {Count} ellipses", results.Count); return results; } private static double ComputeFitError(Point[] contourPoints, RotatedRect ellipse) { double cx = ellipse.Center.X, cy = ellipse.Center.Y; double a = Math.Max(ellipse.Size.Width, ellipse.Size.Height) / 2.0; double b = Math.Min(ellipse.Size.Width, ellipse.Size.Height) / 2.0; double angleRad = ellipse.Angle * Math.PI / 180.0; double cosA = Math.Cos(angleRad), sinA = Math.Sin(angleRad); if (a < 1e-6) return double.MaxValue; double totalError = 0; foreach (var pt in contourPoints) { double dx = pt.X - cx, dy = pt.Y - cy; double localX = dx * cosA + dy * sinA; double localY = -dx * sinA + dy * cosA; double ellipseVal = (localX * localX) / (a * a) + (localY * localY) / (b * b); totalError += Math.Abs(Math.Sqrt(ellipseVal) - 1.0) * Math.Sqrt(a * b); } return totalError / contourPoints.Length; } } /// /// 椭圆检测算子 /// public class EllipseDetectionProcessor : ImageProcessorBase { private static readonly ILogger _logger = Log.ForContext(); public EllipseDetectionProcessor() { Name = LocalizationHelper.GetString("EllipseDetectionProcessor_Name"); Description = LocalizationHelper.GetString("EllipseDetectionProcessor_Description"); } protected override void InitializeParameters() { // ── 多边形ROI(由UI注入,最多32个点) ── Parameters.Add("PolyCount", new ProcessorParameter("PolyCount", "PolyCount", typeof(int), 0, null, null, "") { IsVisible = false }); for (int i = 0; i < 32; i++) { Parameters.Add($"PolyX{i}", new ProcessorParameter($"PolyX{i}", $"PolyX{i}", typeof(int), 0, null, null, "") { IsVisible = false }); Parameters.Add($"PolyY{i}", new ProcessorParameter($"PolyY{i}", $"PolyY{i}", typeof(int), 0, null, null, "") { IsVisible = false }); } Parameters.Add("MinThreshold", new ProcessorParameter( "MinThreshold", LocalizationHelper.GetString("EllipseDetectionProcessor_MinThreshold"), typeof(int), 64, 0, 255, LocalizationHelper.GetString("EllipseDetectionProcessor_MinThreshold_Desc"))); Parameters.Add("MaxThreshold", new ProcessorParameter( "MaxThreshold", LocalizationHelper.GetString("EllipseDetectionProcessor_MaxThreshold"), typeof(int), 192, 0, 255, LocalizationHelper.GetString("EllipseDetectionProcessor_MaxThreshold_Desc"))); Parameters.Add("UseOtsu", new ProcessorParameter( "UseOtsu", LocalizationHelper.GetString("EllipseDetectionProcessor_UseOtsu"), typeof(bool), false, null, null, LocalizationHelper.GetString("EllipseDetectionProcessor_UseOtsu_Desc"))); Parameters.Add("MinContourPoints", new ProcessorParameter( "MinContourPoints", LocalizationHelper.GetString("EllipseDetectionProcessor_MinContourPoints"), typeof(int), 30, 5, 1000, LocalizationHelper.GetString("EllipseDetectionProcessor_MinContourPoints_Desc"))); Parameters.Add("MinArea", new ProcessorParameter( "MinArea", LocalizationHelper.GetString("EllipseDetectionProcessor_MinArea"), typeof(double), 100.0, 0.0, 1000000.0, LocalizationHelper.GetString("EllipseDetectionProcessor_MinArea_Desc"))); Parameters.Add("MaxArea", new ProcessorParameter( "MaxArea", LocalizationHelper.GetString("EllipseDetectionProcessor_MaxArea"), typeof(double), 1000000.0, 0.0, 10000000.0, LocalizationHelper.GetString("EllipseDetectionProcessor_MaxArea_Desc"))); Parameters.Add("MaxEccentricity", new ProcessorParameter( "MaxEccentricity", LocalizationHelper.GetString("EllipseDetectionProcessor_MaxEccentricity"), typeof(double), 0.95, 0.0, 1.0, LocalizationHelper.GetString("EllipseDetectionProcessor_MaxEccentricity_Desc"))); Parameters.Add("MaxFitError", new ProcessorParameter( "MaxFitError", LocalizationHelper.GetString("EllipseDetectionProcessor_MaxFitError"), typeof(double), 5.0, 0.0, 50.0, LocalizationHelper.GetString("EllipseDetectionProcessor_MaxFitError_Desc"))); Parameters.Add("Thickness", new ProcessorParameter( "Thickness", LocalizationHelper.GetString("EllipseDetectionProcessor_Thickness"), typeof(int), 2, 1, 10, LocalizationHelper.GetString("EllipseDetectionProcessor_Thickness_Desc"))); _logger.Debug("InitializeParameters"); } public override Image Process(Image inputImage) { int thickness = GetParameter("Thickness"); _logger.Debug("Ellipse detection started"); OutputData.Clear(); // 构建多边形ROI掩码 int polyCount = GetParameter("PolyCount"); Image? roiMask = null; if (polyCount >= 3) { var pts = new Point[polyCount]; for (int i = 0; i < polyCount; i++) pts[i] = new Point(GetParameter($"PolyX{i}"), GetParameter($"PolyY{i}")); roiMask = new Image(inputImage.Width, inputImage.Height); using var vop = new VectorOfPoint(pts); using var vvop = new VectorOfVectorOfPoint(vop); CvInvoke.DrawContours(roiMask, vvop, 0, new MCvScalar(255), -1); } var detector = new EllipseDetector { MinThreshold = GetParameter("MinThreshold"), MaxThreshold = GetParameter("MaxThreshold"), UseOtsu = GetParameter("UseOtsu"), MinContourPoints = GetParameter("MinContourPoints"), MinArea = GetParameter("MinArea"), MaxArea = GetParameter("MaxArea"), MaxEccentricity = GetParameter("MaxEccentricity"), MaxFitError = GetParameter("MaxFitError"), Thickness = thickness }; var ellipses = detector.Detect(inputImage, roiMask); OutputData["Ellipses"] = ellipses; OutputData["EllipseCount"] = ellipses.Count; OutputData["Thickness"] = thickness; roiMask?.Dispose(); _logger.Information("Ellipse detection completed: detected {Count} ellipses", ellipses.Count); return inputImage.Clone(); } }