2026-04-30 08:46:44 +08:00
|
|
|
|
|
|
|
|
|
|
using MathNet.Numerics.LinearAlgebra;
|
|
|
|
|
|
using MathNetMatrix = MathNet.Numerics.LinearAlgebra.Matrix<double>;
|
|
|
|
|
|
using MathNetVector = MathNet.Numerics.LinearAlgebra.Vector<double>;
|
|
|
|
|
|
using System.Drawing;
|
2026-04-27 16:45:06 +08:00
|
|
|
|
namespace 头罩视野.Services
|
|
|
|
|
|
{
|
|
|
|
|
|
class GetArea
|
|
|
|
|
|
{
|
2026-05-04 17:42:39 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 设备固定参数
|
|
|
|
|
|
private static double R = 330; // 半球半径
|
|
|
|
|
|
private static double angleStep = 10; // 每格角度
|
|
|
|
|
|
|
|
|
|
|
|
// 定义参数(和你代码里一致)
|
|
|
|
|
|
private const int totalLights = 81;
|
2026-04-27 16:45:06 +08:00
|
|
|
|
//public const double standardArea = 140;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>有效亮度阈值:区分有效视野和噪声/遮挡的门槛设备标定经验值,≥12判定为有效视野</summary>
|
|
|
|
|
|
public const int threshold = 12;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>异常值差值阈值:过滤孤立尖峰噪声当前点与前后点差值均>30时,判定为异常值并插值修正</summary>
|
|
|
|
|
|
public const int OutlierDiffThreshold = 30;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>灯条通道总数:360°圆周采样点数量对应每点5°(360° ÷ 72 = 5°/点),符合国标GB2890-2022要求</summary>
|
|
|
|
|
|
public const int lightNum = 72;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>设备最大检测半径:理论上的最大视野半径单位:mm,用来计算理论圆面积</summary> 这个是我们自己的设备值
|
|
|
|
|
|
public const int maxRadius_mm = 330;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>单眼标准标定面积:无面罩空标准头模的单眼实测面积 国标视野保存率计算的基准值,单位:cm²</summary>
|
2026-04-27 18:48:21 +08:00
|
|
|
|
public const double standardArea = 5180;
|
2026-04-27 16:45:06 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>双目标准标定总面积:无面罩空标准头模的双目总实测面积国标总视野保存率计算的基准值,单位:cm²</summary>
|
|
|
|
|
|
public const double StandardTotal = 10360;
|
|
|
|
|
|
|
|
|
|
|
|
// 补充:用半径计算的单眼理论圆面积(供参考) 公式:π × 半径²,单位:cm²
|
2026-04-27 18:48:21 +08:00
|
|
|
|
public static readonly double standardAreaOus = Math.PI * maxRadius_mm * maxRadius_mm / 100;
|
2026-04-27 16:45:06 +08:00
|
|
|
|
|
|
|
|
|
|
//双目重叠标准
|
|
|
|
|
|
public static double StandardBinocular = 4150;
|
|
|
|
|
|
|
2026-05-04 17:42:39 +08:00
|
|
|
|
//空白视野面积计算
|
|
|
|
|
|
public static readonly double _standardTotalArea = 2 * Math.PI * 330 * 330;
|
2026-04-27 16:45:06 +08:00
|
|
|
|
|
2026-05-04 17:42:39 +08:00
|
|
|
|
public static double GetBlankViewArea(double binocularTotalArea)
|
2026-04-27 16:45:06 +08:00
|
|
|
|
{
|
2026-05-04 17:42:39 +08:00
|
|
|
|
// 公式:空白 = 标准总面积 - 双目总视野
|
|
|
|
|
|
return _standardTotalArea - binocularTotalArea;
|
2026-04-27 16:45:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 17:42:39 +08:00
|
|
|
|
//下方视野
|
|
|
|
|
|
// 固定参数:每一格灯代表 10 度(你的设备标准)
|
|
|
|
|
|
|
2026-04-27 16:45:06 +08:00
|
|
|
|
/// <summary>
|
2026-05-04 17:42:39 +08:00
|
|
|
|
/// 计算 下方视野角度(单位:度 °)
|
2026-04-27 16:45:06 +08:00
|
|
|
|
/// </summary>
|
2026-05-04 17:42:39 +08:00
|
|
|
|
public static double CalculateBottomViewAngle(int[] lightData, List<(int m, int n)> lightPositions)
|
2026-04-27 16:45:06 +08:00
|
|
|
|
{
|
2026-05-04 17:42:39 +08:00
|
|
|
|
// 存所有亮灯的角度
|
|
|
|
|
|
List<double> angles = new List<double>();
|
2026-04-27 16:45:06 +08:00
|
|
|
|
|
2026-05-04 17:42:39 +08:00
|
|
|
|
for (int i = 0; i < lightData.Length; i++)
|
2026-04-27 16:45:06 +08:00
|
|
|
|
{
|
2026-05-04 17:42:39 +08:00
|
|
|
|
if (lightData[i] == 1)
|
2026-04-27 16:45:06 +08:00
|
|
|
|
{
|
2026-05-04 17:42:39 +08:00
|
|
|
|
var (m, n) = lightPositions[i];
|
|
|
|
|
|
double angle = m * angleStep;
|
|
|
|
|
|
angles.Add(angle);
|
2026-04-27 16:45:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 17:42:39 +08:00
|
|
|
|
// 没有亮灯返回 0
|
|
|
|
|
|
if (angles.Count == 0)
|
|
|
|
|
|
return 0;
|
2026-04-27 16:45:06 +08:00
|
|
|
|
|
2026-05-04 17:42:39 +08:00
|
|
|
|
// 最大角度 = 最下方视野
|
|
|
|
|
|
return angles.Max();
|
2026-04-27 16:45:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//视野保存率
|
2026-05-04 17:42:39 +08:00
|
|
|
|
public static double CalcVisionRate(double binocularRate)
|
2026-04-27 16:45:06 +08:00
|
|
|
|
{
|
2026-05-04 17:42:39 +08:00
|
|
|
|
|
2026-04-27 16:45:06 +08:00
|
|
|
|
// 1. 总视野保存率
|
2026-05-04 17:42:39 +08:00
|
|
|
|
double ratioTotal = binocularRate / StandardTotal;
|
2026-04-27 16:45:06 +08:00
|
|
|
|
double gammaTotal = GetVisionGamma(ratioTotal);
|
|
|
|
|
|
double totalRate = gammaTotal * ratioTotal * 100;
|
|
|
|
|
|
return (totalRate);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// GB2890-2022 自动获取 总视野/双目视野 校正系数γ
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="ratio">实测面积/标准面积 比值(0~1)</param>
|
|
|
|
|
|
/// <returns>校正系数 γ</returns>
|
|
|
|
|
|
public static double GetVisionGamma(double ratio)
|
|
|
|
|
|
{
|
|
|
|
|
|
// X:视野残存率 Si/S0
|
|
|
|
|
|
double[] xData = { 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 };
|
|
|
|
|
|
|
|
|
|
|
|
// 总视野 γ 对应值
|
|
|
|
|
|
double[] gammaTotal = { 1.22, 1.18, 1.14, 1.10, 1.06, 1.03, 1.02, 1.01, 1.00 };
|
|
|
|
|
|
|
|
|
|
|
|
double[] yData = gammaTotal;
|
|
|
|
|
|
|
|
|
|
|
|
// 边界限制
|
|
|
|
|
|
if (ratio <= xData[0]) return yData[0];
|
|
|
|
|
|
if (ratio >= xData.Last()) return 1.0;
|
|
|
|
|
|
|
|
|
|
|
|
// 线性插值
|
|
|
|
|
|
for (int i = 0; i < xData.Length - 1; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (ratio >= xData[i] && ratio <= xData[i + 1])
|
|
|
|
|
|
{
|
|
|
|
|
|
double t = (ratio - xData[i]) / (xData[i + 1] - xData[i]);
|
|
|
|
|
|
return yData[i] + t * (yData[i + 1] - yData[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return 1.0;
|
|
|
|
|
|
}
|
2026-04-30 08:46:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 传入:72个灯的亮灭数据(0=灭,1=亮)
|
|
|
|
|
|
// 返回:椭圆面积
|
|
|
|
|
|
public static double CalculateEllipseArea(int[] lightData, List<(int m, int n)> lightPositions)
|
|
|
|
|
|
{
|
2026-05-04 10:25:31 +08:00
|
|
|
|
//if (lightData.Length != totalLights || lightPositions.Count != totalLights)
|
|
|
|
|
|
// throw new Exception("必须是81个灯的数据");
|
2026-04-30 08:46:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 第一步:收集所有亮灯坐标
|
|
|
|
|
|
List<System.Drawing.Point> brightPoints = new List<System.Drawing.Point>();
|
2026-05-04 10:25:31 +08:00
|
|
|
|
for (int i = 0; i < totalLights; i++)
|
2026-04-30 08:46:44 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (lightData[i] == 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
var (m, n) = lightPositions[i];
|
|
|
|
|
|
System.Drawing.Point p = GetLightPoint(m, n);
|
|
|
|
|
|
brightPoints.Add(p);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第二步:用亮点拟合椭圆
|
|
|
|
|
|
var (cx, cy, a, b, area) = FitEllipse(brightPoints);
|
|
|
|
|
|
|
|
|
|
|
|
// 返回面积
|
|
|
|
|
|
return area;
|
2026-05-04 10:25:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
/// 生成设备全部243盏灯的(m,n)位置 上爪1条、下爪1条、左右共用1条,各81灯
|
2026-04-30 08:46:44 +08:00
|
|
|
|
private static System.Drawing.Point GetLightPoint(int m, int n)
|
|
|
|
|
|
{
|
2026-05-04 10:25:31 +08:00
|
|
|
|
double radH, radV;
|
|
|
|
|
|
|
|
|
|
|
|
// 上爪灯条(n=0):水平角固定,m控制垂直角
|
|
|
|
|
|
if (n == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
radH = 0;
|
|
|
|
|
|
radV = m * angleStep * Math.PI / 180;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 下爪灯条(n=1):水平角固定为180°,m控制垂直角
|
|
|
|
|
|
else if (n == 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
radH = Math.PI;
|
|
|
|
|
|
radV = m * angleStep * Math.PI / 180;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 左右共用灯条(n=2):垂直角固定,m控制水平角
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
radH = m * angleStep * Math.PI / 180;
|
|
|
|
|
|
radV = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保留你原来的投影公式
|
2026-04-30 08:46:44 +08:00
|
|
|
|
double x = R * Math.Tan(radH);
|
|
|
|
|
|
double y = R * Math.Tan(radV);
|
2026-05-04 10:25:31 +08:00
|
|
|
|
|
|
|
|
|
|
return new System.Drawing.Point((int)Math.Round(x), (int)Math.Round(y));
|
2026-04-30 08:46:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 10:25:31 +08:00
|
|
|
|
// 最小二乘法拟合椭圆(核心算法)cx:椭圆中心点的 X 坐标 cy:椭圆中心点的 Y 坐标 a:椭圆的长半轴长度(较大的那个半径)b:椭圆的短半轴长度(较小的那个半径)
|
2026-04-30 08:46:44 +08:00
|
|
|
|
|
|
|
|
|
|
private static (double cx, double cy, double a, double b, double area) FitEllipse(List<Point> points)
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = points.Count;
|
|
|
|
|
|
if (n < 5)
|
|
|
|
|
|
throw new Exception("至少需要5个点来拟合椭圆");
|
|
|
|
|
|
|
|
|
|
|
|
// 这里是正确写法
|
|
|
|
|
|
var M = MathNetMatrix.Build.Dense(n, 5);
|
|
|
|
|
|
var Y = MathNetVector.Build.Dense(n, i => -1.0);
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < n; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
double x = points[i].X;
|
|
|
|
|
|
double y = points[i].Y;
|
|
|
|
|
|
M[i, 0] = x * x;
|
|
|
|
|
|
M[i, 1] = x * y;
|
|
|
|
|
|
M[i, 2] = y * y;
|
|
|
|
|
|
M[i, 3] = x;
|
|
|
|
|
|
M[i, 4] = y;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 求解
|
|
|
|
|
|
Vector<double> sol = M.QR().Solve(Y);
|
|
|
|
|
|
double A = sol[0], B = sol[1], C = sol[2], D = sol[3], E = sol[4], F = 1;
|
|
|
|
|
|
|
|
|
|
|
|
// 椭圆中心
|
|
|
|
|
|
double cx = (2 * C * D - B * E) / (B * B - 4 * A * C);
|
|
|
|
|
|
double cy = (2 * A * E - B * D) / (B * B - 4 * A * C);
|
|
|
|
|
|
|
|
|
|
|
|
// 半轴
|
|
|
|
|
|
double term1 = 2 * (A * E * E + C * D * D - B * D * E + (B * B - 4 * A * C) * F);
|
|
|
|
|
|
double term2 = (A + C) + Math.Sqrt((A - C) * (A - C) + B * B);
|
|
|
|
|
|
double term3 = (A + C) - Math.Sqrt((A - C) * (A - C) + B * B);
|
|
|
|
|
|
|
|
|
|
|
|
double a = Math.Sqrt(Math.Abs(term1 / ((B * B - 4 * A * C) * term3)));
|
|
|
|
|
|
double b = Math.Sqrt(Math.Abs(term1 / ((B * B - 4 * A * C) * term2)));
|
|
|
|
|
|
|
|
|
|
|
|
if (a < b) (a, b) = (b, a);
|
|
|
|
|
|
double area = Math.PI * a * b;
|
|
|
|
|
|
|
2026-05-04 10:25:31 +08:00
|
|
|
|
return (cx, cy, a, b, area);
|
2026-04-30 08:46:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 10:25:31 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-27 16:45:06 +08:00
|
|
|
|
}
|