159 lines
4.6 KiB
C#
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));
|
|
}
|
|
}
|