Files
XplorePlane/XP.ImageProcessing.Processors/图像增强/HDREnhancementProcessor.cs
T
wei.lw.li 7768c086ea feat: 图像增强/变换算子标记高级参数+默认值优化
高级参数标记: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默认开启
2026-08-12 09:25:34 +08:00

459 lines
18 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.
// ============================================================================
// 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;
}
}