// ============================================================================ // Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. // 文件名: HDREnhancementProcessor.cs // 描述: 高动态范围(HDR)图像增强算子 // 功能: // - 局部色调映射(Local Tone Mapping) // - 自适应对数映射(Adaptive Logarithmic Mapping) // - Drago色调映射 // - 双边滤波色调映射 // - 增强图像暗部和亮部细节 // 算法: 基于色调映射的HDR增强 // 作者: 李伟 wei.lw.li@hexagon.com // ============================================================================ using Emgu.CV; using Emgu.CV.Structure; using XP.ImageProcessing.Core; using Serilog; namespace XP.ImageProcessing.Processors; /// /// 高动态范围图像增强算子 /// public class HDREnhancementProcessor : ImageProcessorBase { private static readonly ILogger _logger = Log.ForContext(); public HDREnhancementProcessor() { Name = LocalizationHelper.GetString("HDREnhancementProcessor_Name"); Description = LocalizationHelper.GetString("HDREnhancementProcessor_Description"); } protected override void InitializeParameters() { Parameters.Add("Method", new ProcessorParameter( "Method", LocalizationHelper.GetString("HDREnhancementProcessor_Method"), typeof(string), "LocalToneMap", null, null, LocalizationHelper.GetString("HDREnhancementProcessor_Method_Desc"), new string[] { "LocalToneMap", "AdaptiveLog", "Drago", "BilateralToneMap" })); Parameters.Add("Gamma", new ProcessorParameter( "Gamma", LocalizationHelper.GetString("HDREnhancementProcessor_Gamma"), typeof(double), 1.0, 0.1, 5.0, LocalizationHelper.GetString("HDREnhancementProcessor_Gamma_Desc"))); Parameters.Add("Saturation", new ProcessorParameter( "Saturation", LocalizationHelper.GetString("HDREnhancementProcessor_Saturation"), typeof(double), 1.0, 0.0, 3.0, LocalizationHelper.GetString("HDREnhancementProcessor_Saturation_Desc"))); Parameters.Add("DetailBoost", new ProcessorParameter( "DetailBoost", LocalizationHelper.GetString("HDREnhancementProcessor_DetailBoost"), typeof(double), 1.5, 0.0, 5.0, LocalizationHelper.GetString("HDREnhancementProcessor_DetailBoost_Desc"))); Parameters.Add("SigmaSpace", new ProcessorParameter( "SigmaSpace", LocalizationHelper.GetString("HDREnhancementProcessor_SigmaSpace"), typeof(double), 20.0, 1.0, 100.0, LocalizationHelper.GetString("HDREnhancementProcessor_SigmaSpace_Desc"))); Parameters.Add("SigmaColor", new ProcessorParameter( "SigmaColor", LocalizationHelper.GetString("HDREnhancementProcessor_SigmaColor"), typeof(double), 30.0, 1.0, 100.0, LocalizationHelper.GetString("HDREnhancementProcessor_SigmaColor_Desc"))); Parameters.Add("Bias", new ProcessorParameter( "Bias", LocalizationHelper.GetString("HDREnhancementProcessor_Bias"), typeof(double), 0.85, 0.0, 1.0, LocalizationHelper.GetString("HDREnhancementProcessor_Bias_Desc"))); _logger.Debug("InitializeParameters"); } public override Image Process(Image inputImage) { string method = GetParameter("Method"); double gamma = GetParameter("Gamma"); double saturation = GetParameter("Saturation"); double detailBoost = GetParameter("DetailBoost"); double sigmaSpace = GetParameter("SigmaSpace"); double sigmaColor = GetParameter("SigmaColor"); double bias = GetParameter("Bias"); Image result; switch (method) { case "AdaptiveLog": result = AdaptiveLogarithmicMapping(inputImage, gamma, bias); break; case "Drago": result = DragoToneMapping(inputImage, gamma, bias); break; case "BilateralToneMap": result = BilateralToneMapping(inputImage, gamma, sigmaSpace, sigmaColor, detailBoost); break; default: // LocalToneMap result = LocalToneMapping(inputImage, gamma, sigmaSpace, detailBoost, saturation); break; } _logger.Debug("Process: Method={Method}, Gamma={Gamma}, Saturation={Saturation}, DetailBoost={DetailBoost}, SigmaSpace={SigmaSpace}, SigmaColor={SigmaColor}, Bias={Bias}", method, gamma, saturation, detailBoost, sigmaSpace, sigmaColor, bias); return result; } /// /// 局部色调映射 /// 将图像分解为基础层(光照)和细节层,分别处理后合成 /// Base = GaussianBlur(log(I)) /// Detail = log(I) - Base /// Output = exp(Base_compressed + Detail * boost) /// private Image LocalToneMapping(Image inputImage, double gamma, double sigmaSpace, double detailBoost, double saturation) { int width = inputImage.Width; int height = inputImage.Height; // 转换为浮点并归一化到 (0, 1] var floatImage = inputImage.Convert(); for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) floatImage.Data[y, x, 0] = floatImage.Data[y, x, 0] / 255.0f + 0.001f; // 对数域 var logImage = new Image(width, height); for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) logImage.Data[y, x, 0] = (float)Math.Log(floatImage.Data[y, x, 0]); // 基础层:大尺度高斯模糊提取光照分量 int kernelSize = (int)(sigmaSpace * 6) | 1; if (kernelSize < 3) kernelSize = 3; var baseLayer = new Image(width, height); CvInvoke.GaussianBlur(logImage, baseLayer, new System.Drawing.Size(kernelSize, kernelSize), sigmaSpace); // 细节层 var detailLayer = logImage - baseLayer; // 压缩基础层的动态范围 double baseMin = double.MaxValue, baseMax = double.MinValue; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { float v = baseLayer.Data[y, x, 0]; if (v < baseMin) baseMin = v; if (v > baseMax) baseMax = v; } } double baseRange = baseMax - baseMin; if (baseRange < 0.001) baseRange = 0.001; // 目标动态范围(对数域) double targetRange = Math.Log(256.0); double compressionFactor = targetRange / baseRange; var compressedBase = new Image(width, height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { float normalized = (float)((baseLayer.Data[y, x, 0] - baseMin) / baseRange); compressedBase.Data[y, x, 0] = (float)(normalized * targetRange + Math.Log(0.01)); } } // 合成:压缩后的基础层 + 增强的细节层 var combined = new Image(width, height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { float val = compressedBase.Data[y, x, 0] + detailLayer.Data[y, x, 0] * (float)detailBoost; combined.Data[y, x, 0] = val; } } // 指数变换回线性域 var linearResult = new Image(width, height); for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) linearResult.Data[y, x, 0] = (float)Math.Exp(combined.Data[y, x, 0]); // Gamma校正 if (Math.Abs(gamma - 1.0) > 0.01) { double invGamma = 1.0 / gamma; double maxVal = 0; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) if (linearResult.Data[y, x, 0] > maxVal) maxVal = linearResult.Data[y, x, 0]; if (maxVal > 0) { for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) { double normalized = linearResult.Data[y, x, 0] / maxVal; linearResult.Data[y, x, 0] = (float)(Math.Pow(normalized, invGamma) * maxVal); } } } // 饱和度增强(对比度微调) if (Math.Abs(saturation - 1.0) > 0.01) { double mean = 0; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) mean += linearResult.Data[y, x, 0]; mean /= (width * height); for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) { double diff = linearResult.Data[y, x, 0] - mean; linearResult.Data[y, x, 0] = (float)(mean + diff * saturation); } } // 归一化到 [0, 255] var result = NormalizeToByteImage(linearResult); floatImage.Dispose(); logImage.Dispose(); baseLayer.Dispose(); detailLayer.Dispose(); compressedBase.Dispose(); combined.Dispose(); linearResult.Dispose(); return result; } /// /// 自适应对数映射 /// 根据场景的整体亮度自适应调整对数映射曲线 /// L_out = (log(1 + L_in) / log(1 + L_max)) ^ (1/gamma) /// 使用局部自适应:L_max 根据邻域计算 /// private Image AdaptiveLogarithmicMapping(Image inputImage, double gamma, double bias) { int width = inputImage.Width; int height = inputImage.Height; var floatImage = inputImage.Convert(); // 归一化到 [0, 1] for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) floatImage.Data[y, x, 0] /= 255.0f; // 计算全局最大亮度 float globalMax = 0; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) if (floatImage.Data[y, x, 0] > globalMax) globalMax = floatImage.Data[y, x, 0]; if (globalMax < 0.001f) globalMax = 0.001f; // 计算对数平均亮度 double logAvg = 0; int count = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { float v = floatImage.Data[y, x, 0]; if (v > 0.001f) { logAvg += Math.Log(v); count++; } } } logAvg = Math.Exp(logAvg / Math.Max(count, 1)); // 自适应对数映射 // bias 控制暗部和亮部的平衡 double logBase = Math.Log(2.0 + 8.0 * Math.Pow(logAvg / globalMax, Math.Log(bias) / Math.Log(0.5))); var result = new Image(width, height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { float lum = floatImage.Data[y, x, 0]; double mapped = Math.Log(1.0 + lum) / logBase; result.Data[y, x, 0] = (float)mapped; } } // Gamma校正 if (Math.Abs(gamma - 1.0) > 0.01) { double invGamma = 1.0 / gamma; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) result.Data[y, x, 0] = (float)Math.Pow(Math.Max(0, result.Data[y, x, 0]), invGamma); } var byteResult = NormalizeToByteImage(result); floatImage.Dispose(); result.Dispose(); return byteResult; } /// /// Drago色调映射 /// 使用自适应对数基底进行色调映射 /// L_out = log_base(1 + L_in) / log_base(1 + L_max) /// base = 2 + 8 * (L_in / L_max) ^ (ln(bias) / ln(0.5)) /// private Image DragoToneMapping(Image inputImage, double gamma, double bias) { int width = inputImage.Width; int height = inputImage.Height; var floatImage = inputImage.Convert(); // 归一化到 [0, 1] for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) floatImage.Data[y, x, 0] /= 255.0f; // 全局最大亮度 float maxLum = 0; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) if (floatImage.Data[y, x, 0] > maxLum) maxLum = floatImage.Data[y, x, 0]; if (maxLum < 0.001f) maxLum = 0.001f; double biasP = Math.Log(bias) / Math.Log(0.5); double divider = Math.Log10(1.0 + maxLum); if (divider < 0.001) divider = 0.001; var result = new Image(width, height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { float lum = floatImage.Data[y, x, 0]; // 自适应对数基底 double adaptBase = 2.0 + 8.0 * Math.Pow(lum / maxLum, biasP); double logAdapt = Math.Log(1.0 + lum) / Math.Log(adaptBase); double mapped = logAdapt / divider; result.Data[y, x, 0] = (float)Math.Max(0, Math.Min(1.0, mapped)); } } // Gamma校正 if (Math.Abs(gamma - 1.0) > 0.01) { double invGamma = 1.0 / gamma; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) result.Data[y, x, 0] = (float)Math.Pow(result.Data[y, x, 0], invGamma); } var byteResult = NormalizeToByteImage(result); floatImage.Dispose(); result.Dispose(); return byteResult; } /// /// 双边滤波色调映射 /// 使用双边滤波分离基础层和细节层 /// 双边滤波保边特性使得细节层更加精确 /// private Image BilateralToneMapping(Image inputImage, double gamma, double sigmaSpace, double sigmaColor, double detailBoost) { int width = inputImage.Width; int height = inputImage.Height; // 转换为浮点并取对数 var floatImage = inputImage.Convert(); var logImage = new Image(width, height); for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) logImage.Data[y, x, 0] = (float)Math.Log(floatImage.Data[y, x, 0] / 255.0f + 0.001); // 双边滤波提取基础层(保边平滑) int diameter = (int)(sigmaSpace * 2) | 1; if (diameter < 3) diameter = 3; if (diameter > 31) diameter = 31; var baseLayer = new Image(width, height); // 转换为 byte 进行双边滤波,再转回 float var logNorm = NormalizeToByteImage(logImage); var baseNorm = new Image(width, height); CvInvoke.BilateralFilter(logNorm, baseNorm, diameter, sigmaColor, sigmaSpace); // 将基础层转回浮点对数域 double logMin = double.MaxValue, logMax = double.MinValue; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) { float v = logImage.Data[y, x, 0]; if (v < logMin) logMin = v; if (v > logMax) logMax = v; } double logRange = logMax - logMin; if (logRange < 0.001) logRange = 0.001; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) baseLayer.Data[y, x, 0] = (float)(baseNorm.Data[y, x, 0] / 255.0 * logRange + logMin); // 细节层 = 对数图像 - 基础层 var detailLayer = logImage - baseLayer; // 压缩基础层 double baseMin = double.MaxValue, baseMax = double.MinValue; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) { float v = baseLayer.Data[y, x, 0]; if (v < baseMin) baseMin = v; if (v > baseMax) baseMax = v; } double bRange = baseMax - baseMin; if (bRange < 0.001) bRange = 0.001; double targetRange = Math.Log(256.0); double compression = targetRange / bRange; // 合成 var combined = new Image(width, height); for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) { float compBase = (float)((baseLayer.Data[y, x, 0] - baseMin) * compression + Math.Log(0.01)); combined.Data[y, x, 0] = compBase + detailLayer.Data[y, x, 0] * (float)detailBoost; } // 指数变换回线性域 var linearResult = new Image(width, height); for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) linearResult.Data[y, x, 0] = (float)Math.Exp(combined.Data[y, x, 0]); // Gamma校正 if (Math.Abs(gamma - 1.0) > 0.01) { double invGamma = 1.0 / gamma; double maxVal = 0; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) if (linearResult.Data[y, x, 0] > maxVal) maxVal = linearResult.Data[y, x, 0]; if (maxVal > 0) for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) linearResult.Data[y, x, 0] = (float)(Math.Pow(linearResult.Data[y, x, 0] / maxVal, invGamma) * maxVal); } var result = NormalizeToByteImage(linearResult); floatImage.Dispose(); logImage.Dispose(); logNorm.Dispose(); baseNorm.Dispose(); baseLayer.Dispose(); detailLayer.Dispose(); combined.Dispose(); linearResult.Dispose(); return result; } /// /// 归一化浮点图像到字节图像 /// private Image NormalizeToByteImage(Image floatImage) { double minVal = double.MaxValue; double maxVal = double.MinValue; for (int y = 0; y < floatImage.Height; y++) for (int x = 0; x < floatImage.Width; x++) { float val = floatImage.Data[y, x, 0]; if (val < minVal) minVal = val; if (val > maxVal) maxVal = val; } var result = new Image(floatImage.Size); double range = maxVal - minVal; if (range > 0) { for (int y = 0; y < floatImage.Height; y++) for (int x = 0; x < floatImage.Width; x++) { int normalized = (int)((floatImage.Data[y, x, 0] - minVal) / range * 255.0); result.Data[y, x, 0] = (byte)Math.Max(0, Math.Min(255, normalized)); } } return result; } }