// ============================================================================
// 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();
}
}