Files
XplorePlane/XP.Calibration/Core/BallDetector.cs
T
2026-04-23 19:38:44 +08:00

159 lines
4.6 KiB
C#

using System.Drawing;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
using Emgu.CV.Util;
namespace XP.Calibration.Core;
/// <summary>
/// 球心检测器 | Ball center detector from projection images
/// </summary>
public static class BallDetector
{
/// <summary>
/// 是否启用亚像素质心 | Enable sub-pixel centroid
/// </summary>
public static bool EnableSubPixel { get; set; } = true;
/// <summary>
/// 从单帧投影中检测球心 (自适应阈值 + 亚像素质心法)
/// </summary>
public static bool DetectCenter(Mat srcFloat, out PointF center)
{
center = new PointF(-1, -1);
// 归一化到 0-255
using var img8 = new Mat();
CvInvoke.Normalize(srcFloat, img8, 0, 255, NormType.MinMax);
img8.ConvertTo(img8, DepthType.Cv8U);
CvInvoke.GaussianBlur(img8, img8, new Size(3, 3), 0.8);
// 自适应阈值
using var bin = new Mat();
CvInvoke.AdaptiveThreshold(img8, bin, 255,
AdaptiveThresholdType.MeanC, ThresholdType.Binary, 31, -5);
// 查找轮廓
using var contours = new VectorOfVectorOfPoint();
CvInvoke.FindContours(bin, contours, null,
RetrType.External, ChainApproxMethod.ChainApproxNone);
if (contours.Size == 0)
return false;
// 找最大轮廓
int maxIdx = 0;
double maxArea = 0;
for (int i = 0; i < contours.Size; i++)
{
double area = CvInvoke.ContourArea(contours[i]);
if (area > maxArea)
{
maxArea = area;
maxIdx = i;
}
}
// 创建掩膜
using var mask = new Mat(srcFloat.Size, DepthType.Cv8U, 1);
mask.SetTo(new MCvScalar(0));
CvInvoke.DrawContours(mask, contours, maxIdx, new MCvScalar(255), -1);
if (EnableSubPixel)
{
center = SubPixelCentroid(srcFloat, mask);
}
else
{
var moments = CvInvoke.Moments(contours[maxIdx]);
center = new PointF(
(float)(moments.M10 / moments.M00),
(float)(moments.M01 / moments.M00));
}
return center.X >= 0 && center.Y >= 0;
}
/// <summary>
/// 从单帧投影中检测球心 (Canny 边缘 + 椭圆拟合法)
/// </summary>
public static bool DetectCenterByEllipse(Mat srcFloat, out PointF center)
{
center = new PointF(-1, -1);
using var img8 = new Mat();
CvInvoke.Normalize(srcFloat, img8, 0, 255, NormType.MinMax);
img8.ConvertTo(img8, DepthType.Cv8U);
CvInvoke.GaussianBlur(img8, img8, new Size(3, 3), 0.8);
using var edges = new Mat();
CvInvoke.Canny(img8, edges, 30, 80);
using var contours = new VectorOfVectorOfPoint();
CvInvoke.FindContours(edges, contours, null,
RetrType.External, ChainApproxMethod.ChainApproxNone);
if (contours.Size == 0)
return false;
int maxIdx = 0;
double maxArea = 0;
for (int i = 0; i < contours.Size; i++)
{
double area = CvInvoke.ContourArea(contours[i]);
if (area > maxArea)
{
maxArea = area;
maxIdx = i;
}
}
if (contours[maxIdx].Size < 5)
return false;
using var points = new VectorOfPointF(
Array.ConvertAll(contours[maxIdx].ToArray(), p => new PointF(p.X, p.Y)));
var ellipse = CvInvoke.FitEllipse(points);
center = ellipse.Center;
return true;
}
/// <summary>
/// 亚像素质心计算 | Sub-pixel centroid using intensity weighting
/// </summary>
private static PointF SubPixelCentroid(Mat imgFloat, Mat mask)
{
double sumI = 0, sumX = 0, sumY = 0;
var imgData = imgFloat.GetData() as float[,];
var maskData = mask.GetData() as byte[,];
if (imgData == null || maskData == null)
return new PointF(-1, -1);
int rows = imgFloat.Rows;
int cols = imgFloat.Cols;
for (int y = 0; y < rows; y++)
{
for (int x = 0; x < cols; x++)
{
if (maskData[y, x] != 0)
{
double intensity = imgData[y, x];
sumI += intensity;
sumX += x * intensity;
sumY += y * intensity;
}
}
}
if (sumI == 0)
return new PointF(-1, -1);
return new PointF((float)(sumX / sumI), (float)(sumY / sumI));
}
}