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));
}
}