304 lines
12 KiB
C#
304 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 XP.ImageProcessing.Core;
|
||
using Serilog;
|
||
using System.Drawing;
|
||
|
||
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注入,最多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<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();
|
||
}
|
||
}
|