using System.Drawing; using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using Emgu.CV.Util; namespace XP.Calibration.Core; /// /// 双球心检测结果 /// public record DualBallResult { /// 第一个球心 (面积较大的) public PointF Center1 { get; init; } /// 第二个球心 (面积较小的) public PointF Center2 { get; init; } /// 两球心距离 (像素) public double DistancePx { get; init; } /// 两球心距离 (mm), 需提供 pixelSize 才有效 public double DistanceMm { get; init; } /// 第一个球的轮廓面积 public double Area1 { get; init; } /// 第二个球的轮廓面积 public double Area2 { get; init; } } /// /// 双球心检测器: 从单帧图像中检测两个球心并计算距离 /// public static class DualBallDetector { /// /// 检测图像中两个最大球体的中心及距离 /// /// 输入图像 (CV_32F) /// 像素物理尺寸 (mm), 0 表示不计算物理距离 /// 最小轮廓面积阈值, 过滤噪声 /// 是否启用亚像素质心 public static DualBallResult? Detect( Mat srcFloat, double pixelSize = 0, double minArea = 50, bool enableSubPixel = true) { // 归一化 + 预处理 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); // 按面积排序, 取最大的两个 var candidates = new List<(int Index, double Area)>(); for (int i = 0; i < contours.Size; i++) { double area = CvInvoke.ContourArea(contours[i]); if (area >= minArea) candidates.Add((i, area)); } if (candidates.Count < 2) return null; candidates.Sort((a, b) => b.Area.CompareTo(a.Area)); var top2 = candidates.Take(2).ToList(); // 计算两个球心 var centers = new PointF[2]; var areas = new double[2]; for (int k = 0; k < 2; k++) { int idx = top2[k].Index; areas[k] = top2[k].Area; if (enableSubPixel) { using var mask = new Mat(srcFloat.Size, DepthType.Cv8U, 1); mask.SetTo(new MCvScalar(0)); CvInvoke.DrawContours(mask, contours, idx, new MCvScalar(255), -1); centers[k] = SubPixelCentroid(srcFloat, mask); } else { var moments = CvInvoke.Moments(contours[idx]); centers[k] = new PointF( (float)(moments.M10 / moments.M00), (float)(moments.M01 / moments.M00)); } } // 计算距离 double dx = centers[0].X - centers[1].X; double dy = centers[0].Y - centers[1].Y; double distPx = Math.Sqrt(dx * dx + dy * dy); double distMm = pixelSize > 0 ? distPx * pixelSize : 0; return new DualBallResult { Center1 = centers[0], Center2 = centers[1], DistancePx = distPx, DistanceMm = distMm, Area1 = areas[0], Area2 = areas[1] }; } /// /// 从 8bit 图像检测 (便捷重载, 支持 CV_8U 输入) /// public static DualBallResult? DetectFrom8U( Mat src8U, double pixelSize = 0, double minArea = 50, bool enableSubPixel = true) { using var floatMat = new Mat(); src8U.ConvertTo(floatMat, DepthType.Cv32F); return Detect(floatMat, pixelSize, minArea, enableSubPixel); } 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); for (int y = 0; y < imgFloat.Rows; y++) { for (int x = 0; x < imgFloat.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)); } }