ThreatSourceLibaray/ThreatSource/src/Guidance/InfraredImageGenerator.cs

574 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using ThreatSource.Equipment;
using ThreatSource.Utils;
using AirTransmission;
using ThreatSource.Jammer;
using ThreatSource.Simulation;
using System.Diagnostics;
namespace ThreatSource.Guidance
{
/// <summary>
/// 红外图像生成器,用于生成目标的红外图像
/// </summary>
/// <remarks>
/// 该类实现红外图像的生成过程:
/// - 计算目标在图像平面上的投影
/// - 根据目标特性生成红外强度
/// - 添加背景噪声
/// - 考虑大气透过率影响
/// </remarks>
public class InfraredImageGenerator
{
/// <summary>
/// 图像宽度,单位:像素
/// </summary>
private readonly int imageWidth;
/// <summary>
/// 图像高度,单位:像素
/// </summary>
private readonly int imageHeight;
/// <summary>
/// 视场角,单位:弧度
/// </summary>
private readonly double fieldOfView;
/// <summary>
/// 背景辐射强度单位W/sr
/// </summary>
private readonly double backgroundIntensity;
/// <summary>
/// 红外波长,单位:微米
/// </summary>
private readonly double wavelength;
/// <summary>
/// 随机数生成器
/// </summary>
private readonly Random random = new();
/// <summary>
/// 烟幕温度转换为强度的比例因子 (简化 T^4 关系)
/// </summary>
private const double SMOKE_TEMPERATURE_FACTOR = 1e-11;
/// <summary>
/// 目标温度(K)转换为强度(W/sr)的比例因子 (基于简化 T^4) - **需要调整**
/// </summary>
private const double TEMPERATURE_TO_INTENSITY_FACTOR = 1e-6; // Initial guess 1e-11, changed to 1e-7, now 1e-6
private const double KELVIN_OFFSET = 273.15;
// 视线坐标系
private Vector3D forward; // 视线方向
private Vector3D right; // 图像平面右方向
private Vector3D up; // 图像平面上方向
/// <summary>
/// 初始化红外图像生成器的新实例
/// </summary>
/// <param name="imageWidth">图像宽度</param>
/// <param name="imageHeight">图像高度</param>
/// <param name="fieldOfView">视场角</param>
/// <param name="backgroundIntensity">背景辐射强度单位W/sr典型地表背景约0.01-0.1 W/sr</param>
/// <param name="wavelength">红外波长单位微米默认为3.0(中波红外)</param>
public InfraredImageGenerator(
int imageWidth = 640,
int imageHeight = 512,
double fieldOfView = Math.PI / 18,
double backgroundIntensity = 0.01,
double wavelength = 3.0)
{
this.imageWidth = imageWidth;
this.imageHeight = imageHeight;
this.fieldOfView = fieldOfView;
this.backgroundIntensity = backgroundIntensity;
this.wavelength = wavelength;
// Initialize coordinate system with default values
forward = Vector3D.UnitZ;
right = Vector3D.UnitX;
up = Vector3D.UnitY;
}
/// <summary>
/// 更新视线坐标系
/// </summary>
private void UpdateLineOfSightFrame(Vector3D missilePosition, Vector3D targetPosition)
{
// 计算视线方向
forward = (targetPosition - missilePosition).Normalize();
Debug.WriteLine($"视线方向: {forward}");
// 选择合适的上方向
Vector3D worldUp = Math.Abs(Vector3D.DotProduct(forward, Vector3D.UnitZ)) > 0.99
? Vector3D.UnitY
: Vector3D.UnitZ;
// 计算右方向和上方向
right = Vector3D.CrossProduct(forward, worldUp).Normalize();
up = Vector3D.CrossProduct(right, forward);
}
/// <summary>
/// 将世界坐标投影到图像平面
/// </summary>
private (double x, double y) ProjectToImagePlane(Vector3D worldPoint, Vector3D origin)
{
Vector3D relativePos = worldPoint - origin;
// 计算在图像平面上的投影
double x = Vector3D.DotProduct(relativePos, right);
double y = Vector3D.DotProduct(relativePos, up);
double z = Vector3D.DotProduct(relativePos, forward);
// 转换为角度
double angleX = Math.Atan2(x, z);
double angleY = Math.Atan2(y, z);
Debug.WriteLine($"投影角度: X={angleX:F6} 弧度, Y={angleY:F6} 弧度");
return (angleX, angleY);
}
/// <summary>
/// 将角度转换为像素坐标
/// </summary>
private (int x, int y) AngleToPixel(double angleX, double angleY)
{
double pixelSize = fieldOfView / imageWidth;
int pixelX = imageWidth/2 + (int)(angleX / pixelSize);
int pixelY = imageHeight/2 + (int)(angleY / pixelSize);
return (pixelX, pixelY);
}
/// <summary>
/// 计算大气透过率
/// </summary>
/// <param name="distance">距离(米)</param>
/// <param name="weather">天气条件</param>
/// <param name="smokeAttenuation">烟幕衰减</param>
/// <returns>大气透过率0-1之间</returns>
private double CalculateAtmosphericTransmittance(double distance, Weather weather, double smokeAttenuation)
{
double transmittance;
// 使用AtmosphereDllWrapper计算红外波段的透过率
transmittance = AtmosphereDllWrapper.CalculateTransmittance(
distance,
RadiationType.Infrared,
wavelength,
weather);
// 考虑烟幕衰减
transmittance*= smokeAttenuation;
return transmittance;
}
/// <summary>
/// 应用遮蔽型烟幕效果 (根据类型区分形状)
/// </summary>
/// <param name="smokeIntensityLayer">存储烟幕强度的图层</param>
/// <param name="missilePosition">导弹当前位置</param>
/// <param name="simulationManager">仿真管理器实例</param>
private void ApplyObscuringSmokeOverlay(double[,] smokeIntensityLayer, Vector3D missilePosition, ISimulationManager simulationManager)
{
const int minPixelSize = 3; // 最小像素尺寸 (直径或边长)
var smokeGrenades = simulationManager.GetEntitiesByType<SmokeGrenade>();
foreach (var smoke in smokeGrenades)
{
// 检查是否是遮蔽型烟幕 - 通过 config 访问
// 确保 config 和 RadiationTemperature 存在
if (smoke.IsActive ==false || smoke.config == null || !smoke.config.IsObscuring || smoke.config.RadiationTemperature <= 0) continue;
Vector3D toSmoke = smoke.KState.Position - missilePosition;
double distance = toSmoke.Magnitude();
// 粗略检查烟幕是否在前方视野内
if (Vector3D.DotProduct(toSmoke.Normalize(), forward) < Math.Cos(fieldOfView / 2.0)) continue; // Use half FOV
// 计算烟幕强度 (基于 T^4 简化模型) - 通过 config 访问
// 注意: 这里没有考虑从烟幕到导弹的大气衰减,可以根据需要添加
double smokeIntensity = SMOKE_TEMPERATURE_FACTOR * Math.Pow(smoke.config.RadiationTemperature, 4);
// 投影烟幕中心
var (angleX, angleY) = ProjectToImagePlane(smoke.KState.Position, missilePosition);
var (smokeCenterX, smokeCenterY) = AngleToPixel(angleX, angleY);
// 检查投影中心是否大致在图像内 (允许部分超出)
if (smokeCenterX < -imageWidth/4 || smokeCenterX >= imageWidth*1.25 ||
smokeCenterY < -imageHeight/4 || smokeCenterY >= imageHeight*1.25)
{
Debug.WriteLine($"烟幕 {smoke.Id} 投影中心 ({smokeCenterX}, {smokeCenterY}) 远离图像范围,跳过");
continue;
}
// 根据烟幕类型计算尺寸并填充 - 通过 config 访问
switch (smoke.config.SmokeType)
{
case SmokeScreenType.Wall:
// 墙状烟幕,考虑其朝向
// 获取烟幕墙朝向(法线方向)与视线方向的夹角
Vector3D smokeForward = smoke.KState.Orientation.ToVector().Normalize(); // 烟幕墙的朝向(法线方向)
double angleToViewLine = Math.Acos(Math.Abs(Vector3D.DotProduct(smokeForward, forward)));
// 计算墙在视线方向的有效投影尺寸
// 墙宽度在Z轴方向需要结合墙的朝向计算在视线中的投影
double wallWidth = smoke.config.WallWidth; // Z轴上的宽度
double wallHeight = smoke.config.WallHeight; // Y轴上的高度
double wallThickness = smoke.config.Thickness; // X轴上的厚度
Debug.WriteLine($"烟幕墙朝向: {smokeForward}, 与视线夹角: {angleToViewLine * 180 / Math.PI:F2}度");
// 计算墙面的有效投影宽度(考虑朝向)
// 当墙面垂直于视线时,宽度投影最大;平行时,宽度投影最小
double effectiveWidth = Math.Abs(wallWidth * Math.Sin(angleToViewLine)) + Math.Abs(wallThickness * Math.Cos(angleToViewLine));
// 计算像素尺寸(投影到图像平面)
int pixelWidth = (int)Math.Max(minPixelSize, (effectiveWidth / distance) * imageWidth / fieldOfView);
int pixelHeight = (int)Math.Max(minPixelSize, (wallHeight / distance) * imageHeight / fieldOfView);
Debug.WriteLine($"烟幕墙实际宽度: {wallWidth}m, 有效投影宽度: {effectiveWidth:F2}m");
int halfPixelWidth = pixelWidth / 2;
int halfPixelHeight = pixelHeight / 2;
// 确定绘制边界,确保在图像范围内
int minX_rect = Math.Max(0, smokeCenterX - halfPixelWidth);
int maxX_rect = Math.Min(imageWidth - 1, smokeCenterX + halfPixelWidth);
int minY_rect = Math.Max(0, smokeCenterY - halfPixelHeight);
int maxY_rect = Math.Min(imageHeight - 1, smokeCenterY + halfPixelHeight);
for (int y = minY_rect; y <= maxY_rect; y++)
{
for (int x = minX_rect; x <= maxX_rect; x++)
{
// 存储烟幕强度,处理重叠(取最大值)
smokeIntensityLayer[y, x] = Math.Max(smokeIntensityLayer[y, x], smokeIntensity);
}
}
Debug.WriteLine($"记录遮蔽烟幕(Wall): 中心({smokeCenterX},{smokeCenterY}), 尺寸{pixelWidth}x{pixelHeight}px, 强度{smokeIntensity:F6}");
break;
case SmokeScreenType.Cloud:
// 云状烟幕,近似为圆形
double cloudDiameter = smoke.config.CloudDiameter; // 通过 config 访问
int pixelDiameter = (int)Math.Max(minPixelSize, (cloudDiameter / distance) * imageWidth / fieldOfView);
int pixelRadius = pixelDiameter / 2;
int radiusSquared = pixelRadius * pixelRadius;
// 确定绘制边界,确保在图像范围内
int minX_circle = Math.Max(0, smokeCenterX - pixelRadius);
int maxX_circle = Math.Min(imageWidth - 1, smokeCenterX + pixelRadius);
int minY_circle = Math.Max(0, smokeCenterY - pixelRadius);
int maxY_circle = Math.Min(imageHeight - 1, smokeCenterY + pixelRadius);
for (int y = minY_circle; y <= maxY_circle; y++)
{
for (int x = minX_circle; x <= maxX_circle; x++)
{
int dx = x - smokeCenterX;
int dy = y - smokeCenterY;
if (dx * dx + dy * dy <= radiusSquared)
{
// 存储烟幕强度,处理重叠(取最大值)
smokeIntensityLayer[y, x] = Math.Max(smokeIntensityLayer[y, x], smokeIntensity);
}
}
}
Debug.WriteLine($"记录遮蔽烟幕(Cloud): 中心({smokeCenterX},{smokeCenterY}), 半径{pixelRadius}px, 强度{smokeIntensity:F6}");
break;
}
}
}
/// <summary>
/// 生成目标的红外图像
/// </summary>
/// <param name="target">目标对象</param>
/// <param name="missilePosition">导弹位置</param>
/// <param name="smokeAttenuation">衰减型烟幕透过率</param>
/// <param name="simulationManager">仿真管理器实例</param>
/// <returns>红外图像</returns>
public InfraredImage GenerateImage(BaseEquipment target, Vector3D missilePosition, double smokeAttenuation, ISimulationManager simulationManager)
{
// 更新视线坐标系
UpdateLineOfSightFrame(missilePosition, target.KState.Position);
// 创建并初始化烟幕强度图层 (-1表示无烟幕)
double[,] smokeIntensityLayer = new double[imageHeight, imageWidth];
for (int y = 0; y < imageHeight; y++)
{
for (int x = 0; x < imageWidth; x++)
{
smokeIntensityLayer[y, x] = -1.0;
}
}
// 应用(记录)遮蔽型烟幕效果到图层
ApplyObscuringSmokeOverlay(smokeIntensityLayer, missilePosition, simulationManager);
// --- 新增:根据烟幕强度图层创建烟幕覆盖掩码 ---
bool[,] smokeCoverageMask = new bool[imageHeight, imageWidth];
for (int y = 0; y < imageHeight; y++)
{
for (int x = 0; x < imageWidth; x++)
{
smokeCoverageMask[y, x] = smokeIntensityLayer[y, x] >= 0; // >= 0 表示被覆盖
}
}
// --- 结束新增 ---
// --- 新增:在此处创建 image 对象并传入掩码 ---
var image = new InfraredImage(imageWidth, imageHeight, fieldOfView / imageWidth, forward, smokeCoverageMask);
// --- 结束新增 ---
// 计算目标中心投影
var (angleX, angleY) = ProjectToImagePlane(target.KState.Position, missilePosition);
var (centerX, centerY) = AngleToPixel(angleX, angleY);
// 计算目标尺寸
double distance = (target.KState.Position - missilePosition).Magnitude();
double targetLengthAngle = Math.Atan2(target.Properties.Length, distance);
double targetWidthAngle = Math.Atan2(target.Properties.Width, distance);
int pixelLength = (int)(targetLengthAngle / (fieldOfView / imageWidth));
int pixelWidth = (int)(targetWidthAngle / (fieldOfView / imageWidth));
// 确保最小尺寸
pixelLength = Math.Max(1, pixelLength);
pixelWidth = Math.Max(1, pixelWidth);
Debug.WriteLine($"生成目标 {target.Id} 的图像,尺寸: {pixelLength}x{pixelWidth} 像素,目标中心: ({centerX}, {centerY})");
// 计算大气和衰减型烟幕的总透过率
double transmittance = CalculateAtmosphericTransmittance(distance, simulationManager.CurrentWeather, smokeAttenuation);
Debug.WriteLine($"大气及衰减烟幕透过率: {transmittance},烟幕衰减: {smokeAttenuation}");
// 生成目标图像强度,考虑遮蔽烟幕覆盖
GenerateTargetIntensity(image, smokeIntensityLayer, centerX, centerY, pixelLength, pixelWidth, target, distance, transmittance);
// 添加背景和噪声(这会影响到烟幕和目标区域)
AddBackgroundAndNoise(image, simulationManager.CurrentWeather);
return image;
}
/// <summary>
/// 生成目标的红外强度分布,考虑遮蔽烟幕覆盖
/// </summary>
/// <param name="image">红外图像对象</param>
/// <param name="smokeIntensityLayer">遮蔽烟幕强度图层</param>
/// <param name="centerX">目标中心X像素坐标</param>
/// <param name="centerY">目标中心Y像素坐标</param>
/// <param name="pixelLength">目标像素长度</param>
/// <param name="pixelWidth">目标像素宽度</param>
/// <param name="target">目标设备对象</param>
/// <param name="distance">目标距离</param>
/// <param name="transmittance">大气和衰减烟幕透过率</param>
private static void GenerateTargetIntensity(
InfraredImage image,
double[,] smokeIntensityLayer, // 新增参数
int centerX,
int centerY,
int pixelLength,
int pixelWidth,
BaseEquipment target,
double distance,
double transmittance)
{
// --- 开始: 使用 ThermalPattern 重构 ---
// 获取目标当前的 3x3 热成像模式 (摄氏度)
double[,] thermalPattern = target.GetCurrentThermalPattern();
if (thermalPattern == null)
{
Trace.TraceError($"警告: 目标 {target.Id} 未提供 ThermalPattern无法生成基于模式的强度。");
return;
}
int halfLength = pixelLength / 2;
int halfWidth = pixelWidth / 2;
int pixelsSet = 0;
double maxSetIntensity = 0;
// 计算目标像素总数和烟幕覆盖的像素数
int totalTargetPixels = 0;
int smokeCoveredPixels = 0;
// 确保映射的分母不为零
double patternPixelWidth = Math.Max(1.0, (double)pixelWidth / 3.0);
double patternPixelHeight = Math.Max(1.0, (double)pixelLength / 3.0); // Assuming Length maps to Cols (Height of pattern grid)
// 第一遍:计算烟幕覆盖率
// 遍历目标在图像中的包围盒,计算有多少像素被烟幕覆盖
for (int dy = -halfWidth; dy <= halfWidth; dy++)
{
int y = centerY + dy;
if (y < 0 || y >= image.Height) continue;
for (int dx = -halfLength; dx <= halfLength; dx++)
{
int x = centerX + dx;
if (x < 0 || x >= image.Width) continue;
// 只统计在目标有效区域内的像素
// 简化为椭圆形区域检查
double normalizedX = (double)dx / halfLength;
double normalizedY = (double)dy / halfWidth;
double distanceFromCenter = Math.Sqrt(normalizedX * normalizedX + normalizedY * normalizedY);
if (distanceFromCenter <= 1.0) // 在目标椭圆内
{
totalTargetPixels++;
if (smokeIntensityLayer[y, x] >= 0) // 被烟幕覆盖
{
smokeCoveredPixels++;
}
}
}
}
// 计算烟幕覆盖率
double smokeCoverageRatio = totalTargetPixels > 0 ? (double)smokeCoveredPixels / totalTargetPixels : 0.0;
Debug.WriteLine($"Blob {target.Id} 烟幕覆盖率: {smokeCoverageRatio:P2} ({smokeCoveredPixels}/{totalTargetPixels})");
// 第二遍:根据覆盖率调整目标亮度
// 遍历目标在图像中的包围盒
for (int dy = -halfWidth; dy <= halfWidth; dy++)
{
int y = centerY + dy;
if (y < 0 || y >= image.Height) continue;
for (int dx = -halfLength; dx <= halfLength; dx++)
{
int x = centerX + dx;
if (x < 0 || x >= image.Width) continue;
double finalIntensity;
double smokeIntensity = smokeIntensityLayer[y, x];
if (smokeIntensity >= 0) // 检查此像素是否被遮蔽烟幕覆盖
{
// 直接使用烟幕强度
finalIntensity = smokeIntensity;
}
else // 未被烟幕覆盖,基于 ThermalPattern 计算强度
{
// 1. 将像素坐标映射到 3x3 热成像模式的索引
int pixelYInBox = dy + halfWidth;
int pixelXInBox = dx + halfLength;
// Map to pattern indices [0, 2]
int patternRow = (int)Math.Floor(pixelYInBox / patternPixelWidth);
int patternCol = (int)Math.Floor(pixelXInBox / patternPixelHeight); // X maps to Col
// Clamp indices to [0, 2]
patternRow = Math.Max(0, Math.Min(2, patternRow));
patternCol = Math.Max(0, Math.Min(2, patternCol));
// 2. 获取对应模式单元的温度 (摄氏度)
double tempCelsius = thermalPattern[patternRow, patternCol];
// 3. 转换为开尔文
double tempKelvin = tempCelsius + KELVIN_OFFSET;
// 4. 使用简化的 T^4 定律计算基础强度 (W/sr)
double pixelBaseIntensity = tempKelvin > 0 ? TEMPERATURE_TO_INTENSITY_FACTOR * Math.Pow(tempKelvin, 4) : 0;
// 5. 应用大气透过率和距离衰减 (使用 distance^2)
finalIntensity = (distance > 1e-6) ? (pixelBaseIntensity * transmittance / (distance * distance)) : pixelBaseIntensity * transmittance;
// 6. 根据整体烟幕覆盖率调整非覆盖像素的亮度
// 烟幕覆盖率越高,剩余部分的可见度也应越低(烟雾散射效应)
if (smokeCoverageRatio > 0.3) // 超过30%覆盖时开始衰减
{
double visibilityFactor = 1.0 - (smokeCoverageRatio - 0.3) * 0.5; // 线性衰减最多降低50%
finalIntensity *= Math.Max(0.5, visibilityFactor);
}
}
// 设置最终计算出的强度到图像
// 确保强度非负
finalIntensity = Math.Max(0, finalIntensity);
image.SetIntensity(y, x, finalIntensity);
if (smokeIntensity < 0) // 只统计非烟幕覆盖的像素
{
pixelsSet++;
maxSetIntensity = Math.Max(maxSetIntensity, finalIntensity);
}
}
}
Debug.WriteLine($"目标基于ThermalPattern强度分布: 像素设置: {pixelsSet}, 最大强度: {maxSetIntensity:F6} W/sr");
}
/// <summary>
/// 添加背景辐射和噪声
/// </summary>
private void AddBackgroundAndNoise(InfraredImage image, Weather weather)
{
double maxIntensityBefore = double.MinValue;
double maxIntensityAfter = double.MinValue;
int pixelsModified = 0;
// 根据天气条件调整背景噪声水平
double noiseLevel = 0.2; // 默认噪声水平
double bgIntensity = backgroundIntensity;
// 雾、雨、雪等天气增加背景噪声
if (weather.Type == WeatherType.Fog)
{
noiseLevel = 0.5;
bgIntensity *= (1.0 + (1.0 - weather.Visibility / 10.0)); // 雾增加背景辐射
}
else if (weather.Type == WeatherType.Rain || weather.Type == WeatherType.Snow)
{
noiseLevel = 0.3;
// 降水增加噪声水平
if (weather.Precipitation > 0)
{
noiseLevel += 0.1 * Math.Min(1.0, weather.Precipitation / 10.0);
}
}
for (int y = 0; y < image.Height; y++)
{
for (int x = 0; x < image.Width; x++)
{
double currentIntensity = image.GetIntensity(y, x);
maxIntensityBefore = Math.Max(maxIntensityBefore, currentIntensity);
// 只在非目标区域添加背景
if (currentIntensity < bgIntensity)
{
currentIntensity = bgIntensity;
pixelsModified++;
}
// 添加高斯噪声 (降低噪声水平)
double noise = (random.NextDouble() - 0.5) * bgIntensity * noiseLevel;
currentIntensity += noise;
// 确保非负
currentIntensity = Math.Max(0, currentIntensity);
image.SetIntensity(y, x, currentIntensity);
maxIntensityAfter = Math.Max(maxIntensityAfter, currentIntensity);
}
}
}
}
}