Files
XplorePlane/XP.ImageProcessing.Processors/检测分析/EllipseDetectionProcessor.cs
T
2026-04-13 14:36:18 +08:00

314 lines
12 KiB
C#

// ============================================================================
// 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 Serilog;
using System.Drawing;
using XP.ImageProcessing.Core;
namespace XP.ImageProcessing.Processors;
/// <summary>
/// 椭圆检测结�
/// </summary>
public class EllipseInfo
{
/// <summary>åºå·</summary>
public int Index { get; set; }
/// <summary>中心点X</summary>
public float CenterX { get; set; }
/// <summary>中心点Y</summary>
public float CenterY { get; set; }
/// <summary>长轴长度</summary>
public float MajorAxis { get; set; }
/// <summary>短轴长度</summary>
public float MinorAxis { get; set; }
/// <summary>旋转角度(度�/summary>
public float Angle { get; set; }
/// <summary>é¢ç§¯</summary>
public double Area { get; set; }
/// <summary>周长</summary>
public double Perimeter { get; set; }
/// <summary>离心çŽ?(0=åœ? 接近1=æ‰æ¤­åœ?</summary>
public double Eccentricity { get; set; }
/// <summary>拟åˆè¯¯å·®ï¼ˆåƒç´ ï¼‰</summary>
public double FitError { get; set; }
/// <summary>轮廓点集</summary>
public Point[] ContourPoints { get; set; } = Array.Empty<Point>();
/// <summary>外接矩形</summary>
public Rectangle BoundingBox { get; set; }
}
/// <summary>
/// 椭圆检测器
/// </summary>
public class EllipseDetector
{
private static readonly ILogger _logger = Log.ForContext<EllipseDetector>();
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;
/// <summary>执行椭圆检�/summary>
public List<EllipseInfo> Detect(Image<Gray, byte> inputImage, Image<Gray, byte>? roiMask = null)
{
_logger.Debug("Ellipse detection started: UseOtsu={UseOtsu}, MinThreshold={Min}, MaxThreshold={Max}",
UseOtsu, MinThreshold, MaxThreshold);
var results = new List<EllipseInfo>();
using var binary = new Image<Gray, byte>(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;
}
}
/// <summary>
/// 椭圆检测算�
/// </summary>
public class EllipseDetectionProcessor : ImageProcessorBase
{
private static readonly ILogger _logger = Log.ForContext<EllipseDetectionProcessor>();
public EllipseDetectionProcessor()
{
Name = LocalizationHelper.GetString("EllipseDetectionProcessor_Name");
Description = LocalizationHelper.GetString("EllipseDetectionProcessor_Description");
}
protected override void InitializeParameters()
{
// ── 多边形ROI(由UI注入,最�2个点�──
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<Gray, byte> Process(Image<Gray, byte> inputImage)
{
int thickness = GetParameter<int>("Thickness");
_logger.Debug("Ellipse detection started");
OutputData.Clear();
// 构建多边形ROI掩ç 
int polyCount = GetParameter<int>("PolyCount");
Image<Gray, byte>? roiMask = null;
if (polyCount >= 3)
{
var pts = new Point[polyCount];
for (int i = 0; i < polyCount; i++)
pts[i] = new Point(GetParameter<int>($"PolyX{i}"), GetParameter<int>($"PolyY{i}"));
roiMask = new Image<Gray, byte>(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<int>("MinThreshold"),
MaxThreshold = GetParameter<int>("MaxThreshold"),
UseOtsu = GetParameter<bool>("UseOtsu"),
MinContourPoints = GetParameter<int>("MinContourPoints"),
MinArea = GetParameter<double>("MinArea"),
MaxArea = GetParameter<double>("MaxArea"),
MaxEccentricity = GetParameter<double>("MaxEccentricity"),
MaxFitError = GetParameter<double>("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();
}
}