314 lines
12 KiB
C#
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();
|
|
}
|
|
} |