7768c086ea
高级参数标记:Contrast(Contrast,Brightness,UseCLAHE,ClipLimit)、 Gamma(Gain)、Sharpen(Method,KernelSize)、Emboss(BlendRatio,GrayOffset)、 HistogramEq(Method,TileSize)、HDR(Method,Saturation,SigmaSpace,SigmaColor,Bias)、 Hierarchical(BaseGain,ClipLimit)、Retinex(Method,Sigma1/2/3,Offset)、 Rotate(ExpandCanvas,BackgroundValue,Interpolation) 默认值优化(适配平面CT DR图像): - Sharpen: Laplacian→UnsharpMask(对噪声更温和) - HistogramEq: Global→CLAHE(局部对比度更优) - Contrast: AutoContrast默认开启
459 lines
18 KiB
C#
459 lines
18 KiB
C#
// ============================================================================
|
||
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
||
// 文件名: HDREnhancementProcessor.cs
|
||
// 描述: 高动态范围(HDR)图像增强算子(支持 8 位 / 16 位)
|
||
// 功能:
|
||
// - 局部色调映射(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;
|
||
|
||
/// <summary>
|
||
/// 高动态范围图像增强算子(支持 8 位和 16 位灰度图像)。
|
||
/// 输入按当前位深归一化到 [0,1] 浮点域进行色调映射,输出经 min-max 归一化回目标位深,
|
||
/// 无中间 8 位降位损失(16 位输入的暗部/亮部细节得以保留)。
|
||
/// </summary>
|
||
public class HDREnhancementProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||
where TDepth : struct, IComparable
|
||
{
|
||
private static readonly ILogger _logger = Log.ForContext<HDREnhancementProcessor<TDepth>>();
|
||
|
||
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" }) { IsAdvanced = true });
|
||
|
||
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")) { IsAdvanced = true });
|
||
|
||
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")) { IsAdvanced = true });
|
||
|
||
Parameters.Add("SigmaColor", new ProcessorParameter(
|
||
"SigmaColor",
|
||
LocalizationHelper.GetString("HDREnhancementProcessor_SigmaColor"),
|
||
typeof(double),
|
||
30.0,
|
||
1.0,
|
||
100.0,
|
||
LocalizationHelper.GetString("HDREnhancementProcessor_SigmaColor_Desc")) { IsAdvanced = true });
|
||
|
||
Parameters.Add("Bias", new ProcessorParameter(
|
||
"Bias",
|
||
LocalizationHelper.GetString("HDREnhancementProcessor_Bias"),
|
||
typeof(double),
|
||
0.85,
|
||
0.0,
|
||
1.0,
|
||
LocalizationHelper.GetString("HDREnhancementProcessor_Bias_Desc")) { IsAdvanced = true });
|
||
|
||
_logger.Debug("InitializeParameters");
|
||
}
|
||
|
||
public override Image<Gray, TDepth> Process(Image<Gray, TDepth> inputImage)
|
||
{
|
||
string method = GetParameter<string>("Method");
|
||
double gamma = GetParameter<double>("Gamma");
|
||
double saturation = GetParameter<double>("Saturation");
|
||
double detailBoost = GetParameter<double>("DetailBoost");
|
||
double sigmaSpace = GetParameter<double>("SigmaSpace");
|
||
double sigmaColor = GetParameter<double>("SigmaColor");
|
||
double bias = GetParameter<double>("Bias");
|
||
|
||
Image<Gray, TDepth> result = method switch
|
||
{
|
||
"AdaptiveLog" => AdaptiveLogarithmicMapping(inputImage, gamma, bias),
|
||
"Drago" => DragoToneMapping(inputImage, gamma, bias),
|
||
"BilateralToneMap" => BilateralToneMapping(inputImage, gamma, sigmaSpace, sigmaColor, detailBoost),
|
||
_ => LocalToneMapping(inputImage, gamma, sigmaSpace, detailBoost, saturation),
|
||
};
|
||
|
||
_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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将输入图像归一化为 [0,1] 浮点图(按当前位深最大值)。
|
||
/// </summary>
|
||
private Image<Gray, float> ToNormalizedFloat(Image<Gray, TDepth> inputImage)
|
||
{
|
||
int w = inputImage.Width, h = inputImage.Height;
|
||
float maxVal = MaxPixelValue;
|
||
var floatImage = inputImage.Convert<Gray, float>();
|
||
for (int y = 0; y < h; y++)
|
||
for (int x = 0; x < w; x++)
|
||
floatImage.Data[y, x, 0] /= maxVal;
|
||
return floatImage;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 局部色调映射
|
||
/// 将图像分解为基础层(光照)和细节层,分别处理后合成
|
||
/// </summary>
|
||
private Image<Gray, TDepth> LocalToneMapping(Image<Gray, TDepth> inputImage,
|
||
double gamma, double sigmaSpace, double detailBoost, double saturation)
|
||
{
|
||
int width = inputImage.Width;
|
||
int height = inputImage.Height;
|
||
|
||
// 归一化到 (0, 1] 并加小常数避免 log(0)
|
||
var floatImage = ToNormalizedFloat(inputImage);
|
||
for (int y = 0; y < height; y++)
|
||
for (int x = 0; x < width; x++)
|
||
floatImage.Data[y, x, 0] += 0.001f;
|
||
|
||
// 对数域
|
||
var logImage = new Image<Gray, float>(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<Gray, float>(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);
|
||
|
||
var compressedBase = new Image<Gray, float>(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<Gray, float>(width, height);
|
||
for (int y = 0; y < height; y++)
|
||
for (int x = 0; x < width; x++)
|
||
combined.Data[y, x, 0] = compressedBase.Data[y, x, 0] + detailLayer.Data[y, x, 0] * (float)detailBoost;
|
||
|
||
// 指数变换回线性域
|
||
var linearResult = new Image<Gray, float>(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);
|
||
}
|
||
}
|
||
|
||
var result = PixelDepthHelper.FromFloatImage<TDepth>(linearResult);
|
||
|
||
floatImage.Dispose();
|
||
logImage.Dispose();
|
||
baseLayer.Dispose();
|
||
detailLayer.Dispose();
|
||
compressedBase.Dispose();
|
||
combined.Dispose();
|
||
linearResult.Dispose();
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 自适应对数映射
|
||
/// </summary>
|
||
private Image<Gray, TDepth> AdaptiveLogarithmicMapping(Image<Gray, TDepth> inputImage,
|
||
double gamma, double bias)
|
||
{
|
||
int width = inputImage.Width;
|
||
int height = inputImage.Height;
|
||
|
||
var floatImage = ToNormalizedFloat(inputImage);
|
||
|
||
// 计算全局最大亮度
|
||
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));
|
||
|
||
double logBase = Math.Log(2.0 + 8.0 * Math.Pow(logAvg / globalMax, Math.Log(bias) / Math.Log(0.5)));
|
||
|
||
var result = new Image<Gray, float>(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 depthResult = PixelDepthHelper.FromFloatImage<TDepth>(result);
|
||
|
||
floatImage.Dispose();
|
||
result.Dispose();
|
||
|
||
return depthResult;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Drago色调映射
|
||
/// </summary>
|
||
private Image<Gray, TDepth> DragoToneMapping(Image<Gray, TDepth> inputImage,
|
||
double gamma, double bias)
|
||
{
|
||
int width = inputImage.Width;
|
||
int height = inputImage.Height;
|
||
|
||
var floatImage = ToNormalizedFloat(inputImage);
|
||
|
||
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<Gray, float>(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 depthResult = PixelDepthHelper.FromFloatImage<TDepth>(result);
|
||
|
||
floatImage.Dispose();
|
||
result.Dispose();
|
||
|
||
return depthResult;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 双边滤波色调映射
|
||
/// 使用浮点双边滤波(CV_32F)分离基础层和细节层,全程保留位深精度。
|
||
/// </summary>
|
||
private Image<Gray, TDepth> BilateralToneMapping(Image<Gray, TDepth> inputImage,
|
||
double gamma, double sigmaSpace, double sigmaColor, double detailBoost)
|
||
{
|
||
int width = inputImage.Width;
|
||
int height = inputImage.Height;
|
||
|
||
// 归一化并取对数
|
||
var floatImage = ToNormalizedFloat(inputImage);
|
||
var logImage = new Image<Gray, float>(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] + 0.001);
|
||
|
||
// 浮点双边滤波提取基础层(保边平滑,无降位)
|
||
int diameter = (int)(sigmaSpace * 2) | 1;
|
||
if (diameter < 3) diameter = 3;
|
||
if (diameter > 31) diameter = 31;
|
||
|
||
var baseLayer = new Image<Gray, float>(width, height);
|
||
// sigmaColor 参数按 8 位语义定义,对数域需缩放到与 log 值域匹配的小尺度
|
||
double logSigmaColor = sigmaColor / 255.0;
|
||
CvInvoke.BilateralFilter(logImage, baseLayer, diameter, logSigmaColor, 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 bRange = baseMax - baseMin;
|
||
if (bRange < 0.001) bRange = 0.001;
|
||
double targetRange = Math.Log(256.0);
|
||
double compression = targetRange / bRange;
|
||
|
||
// 合成
|
||
var combined = new Image<Gray, float>(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<Gray, float>(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 = PixelDepthHelper.FromFloatImage<TDepth>(linearResult);
|
||
|
||
floatImage.Dispose();
|
||
logImage.Dispose();
|
||
baseLayer.Dispose();
|
||
detailLayer.Dispose();
|
||
combined.Dispose();
|
||
linearResult.Dispose();
|
||
|
||
return result;
|
||
}
|
||
}
|