Files
hoodFieldOfView/头罩视野slove/头罩视野/Services/GetArea.cs

368 lines
13 KiB
C#
Raw Normal View History

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
{
//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-04-27 17:41:15 +08:00
public static List<dynamic> RemoveOutliers(List<dynamic> data)
2026-04-27 16:45:06 +08:00
{
2026-04-28 15:08:34 +08:00
for (int i = 3; i < data.Count - 1; i++)
2026-04-27 16:45:06 +08:00
{
if (Math.Abs(data[i] - data[i - 1]) > OutlierDiffThreshold && Math.Abs(data[i] - data[i + 1]) > OutlierDiffThreshold)
{
2026-04-28 09:14:00 +08:00
data[i] = (dynamic)((data[i - 1] + data[i + 1]) / 2);
2026-04-27 16:45:06 +08:00
}
//过滤无效信号
if (data[i] < threshold)
{
data[i] = 0;
}
}
return data;
}
/// <summary>
/// 计算单眼视野面积
/// </summary>
/// <param name="groupData">20组数据每组lightNum个通道</param>
/// <param name="threshold">有效亮度阈值如12</param>
/// <param name="standardArea">如140</param>
/// <returns>计算好的面积</returns>
///
2026-04-27 17:41:15 +08:00
public static double CalculateEyeArea(List<dynamic> groupData)
2026-04-27 16:45:06 +08:00
{
double[] avg = new double[lightNum];
for (int c = 0; c < lightNum; c++)
{
double sum = 0;
foreach (var g in groupData) sum += g[c];
avg[c] = sum / groupData.Count;
}
int valid = avg.Count(v => v >= threshold);
return (valid / lightNum) * standardArea;
}
//计算双目视野面积
/// <summary>
/// 计算双目视野面积(左右眼同时可见)
/// </summary>
public static double CalcBinocularArea(
2026-04-27 17:41:15 +08:00
List<dynamic> leftGroups,
List<dynamic> rightGroups
2026-04-27 16:45:06 +08:00
)
{
// 1. 左眼平均数据
2026-04-27 18:48:21 +08:00
double[] leftAvg = GetEyeAvgArray(leftGroups);
2026-04-27 16:45:06 +08:00
// 2. 右眼平均数据
2026-04-27 18:48:21 +08:00
double[] rightAvg = GetEyeAvgArray(rightGroups);
2026-04-27 16:45:06 +08:00
// 3. 双目同时有效点数(左右都亮才算)
int biValid = 0;
for (int i = 0; i < lightNum; i++)
{
if (leftAvg[i] >= threshold && rightAvg[i] >= threshold)
biValid++;
}
// 4. 双目视野面积
return (biValid / lightNum) * standardArea;
}
//下方视野角度
/// <summary>
/// GB2890-2022 计算 单眼下方视野角度
/// eyeData单眼lightNum路平均数据数组
/// threshold有效亮度阈值
/// </summary>
public static double CalcLowerAngle(double[] eyeData, double perAngle)
{
// 总lightNum点 每点5°
int totalPoint = lightNum;
// 国标最下方起始点位第54号开始为正下方
int startDownIndex = 54;
int validCount = 0;
// 从最下方向上 连续检测有效点
for (int i = 0; i < lightNum / 2; i++)
{
int idx = (startDownIndex + i) % totalPoint;
if (eyeData[idx] >= threshold)
{
validCount++;
}
else
{
// 断开直接停止
break;
}
}
// 下方视野角度 = 有效点数 × 单步角度
return validCount * perAngle;
}
/// <summary>
/// 计算单眼lightNum点通道平均值数组
/// </summary>
/// <param name="eyeGroups">多组采样数据集合</param>
/// <returns>lightNum点通道平均值数组</returns>
2026-04-27 17:41:15 +08:00
public static double[] GetEyeAvgArray(List<dynamic> eyeGroups)
2026-04-27 16:45:06 +08:00
{
if (eyeGroups == null || eyeGroups.Count == 0)
return new double[lightNum]; // 无数据时返回全0数组
double[] avg = new double[lightNum];
for (int i = 0; i < lightNum; i++)
{
double sum = 0;
foreach (var group in eyeGroups)
{
sum += group[i];
}
avg[i] = sum / eyeGroups.Count;
}
return avg;
}
//视野保存率
//double totalSaveRate = (总视野面积 / 标准总视野面积) * 100;
public static class VisionCalculator
{
/// <summary>
/// 计算视野保存率
/// </summary>
/// <param name="actualArea">实测面积</param>
/// <param name="standardArea">标准面积</param>
/// <returns>保存率 %</returns>
public static double CalculateVisionSaveRate(double actualArea)
{
return (actualArea / standardArea) * 100;
}
}
//===== 2. 传入你采集的实测面积 =====
// leftArea左眼实测 rightArea右眼实测 binArea双目重叠实测
public static double CalcVisionRate(double leftArea, double rightArea)
{
// 总视野实测 = 左+右
double totalSi = leftArea + rightArea;
// 1. 总视野保存率
double ratioTotal = totalSi / StandardTotal;
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
2026-05-04 10:25:31 +08:00
2026-04-30 08:46:44 +08:00
// 设备固定参数
2026-05-04 09:06:59 +08:00
private static double R = 330; // 半球半径
private static double angleStep = 10; // 每格角度
2026-04-30 08:46:44 +08:00
2026-05-04 10:25:31 +08:00
// 定义参数(和你代码里一致)
private const int totalLights = 81;
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
//
//private static System.Drawing.Point GetLightPoint(int m, int n)
//{
// double radH = m * angleStep * Math.PI / 180;
// double radV = n * angleStep * Math.PI / 180;
// double x = R * Math.Tan(radH);
// double y = R * Math.Tan(radV);
// return new System.Drawing.Point((int)x, (int)y);
//}
}
2026-04-27 16:45:06 +08:00
}