using System.Drawing; using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using Emgu.CV.Util; namespace XP.Calibration.Core; /// /// 球心检测器 | Ball center detector from projection images /// public static class BallDetector { /// /// 是否启用亚像素质心 | Enable sub-pixel centroid /// public static bool EnableSubPixel { get; set; } = true; /// /// 从单帧投影中检测球心 (自适应阈值 + 亚像素质心法) /// 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; } /// /// 从单帧投影中检测球心 (Canny 边缘 + 椭圆拟合法) /// 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; } /// /// 亚像素质心计算 | Sub-pixel centroid using intensity weighting /// 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)); } }