228 lines
8.1 KiB
C#
228 lines
8.1 KiB
C#
// ============================================================================
|
|
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
|
// 文件名: ContrastProcessor.cs
|
|
// 描述: 对比度调整算子,用于增强图像对比度
|
|
// 功能:
|
|
// - 线性对比度和亮度调整
|
|
// - 自动对比度拉伸
|
|
// - CLAHE(对比度受限自适应直方图均衡化)
|
|
// - 支持多种对比度增强方法
|
|
// 算法: 线性变换、直方图均衡化、CLAHE
|
|
// 作者: 李伟 wei.lw.li@hexagon.com
|
|
// ============================================================================
|
|
|
|
using Emgu.CV;
|
|
using Emgu.CV.Structure;
|
|
using XP.ImageProcessing.Core;
|
|
using Serilog;
|
|
using System.Drawing;
|
|
|
|
namespace XP.ImageProcessing.Processors;
|
|
|
|
/// <summary>
|
|
/// 对比度调整算子
|
|
/// </summary>
|
|
public class ContrastProcessor : ImageProcessorBase
|
|
{
|
|
private static readonly ILogger _logger = Log.ForContext<ContrastProcessor>();
|
|
|
|
public ContrastProcessor()
|
|
{
|
|
Name = LocalizationHelper.GetString("ContrastProcessor_Name");
|
|
Description = LocalizationHelper.GetString("ContrastProcessor_Description");
|
|
}
|
|
|
|
protected override void InitializeParameters()
|
|
{
|
|
Parameters.Add("Contrast", new ProcessorParameter(
|
|
"Contrast",
|
|
LocalizationHelper.GetString("ContrastProcessor_Contrast"),
|
|
typeof(double),
|
|
1.0,
|
|
0.1,
|
|
3.0,
|
|
LocalizationHelper.GetString("ContrastProcessor_Contrast_Desc")));
|
|
|
|
Parameters.Add("Brightness", new ProcessorParameter(
|
|
"Brightness",
|
|
LocalizationHelper.GetString("ContrastProcessor_Brightness"),
|
|
typeof(int),
|
|
0,
|
|
-100,
|
|
100,
|
|
LocalizationHelper.GetString("ContrastProcessor_Brightness_Desc")));
|
|
|
|
Parameters.Add("AutoContrast", new ProcessorParameter(
|
|
"AutoContrast",
|
|
LocalizationHelper.GetString("ContrastProcessor_AutoContrast"),
|
|
typeof(bool),
|
|
false,
|
|
null,
|
|
null,
|
|
LocalizationHelper.GetString("ContrastProcessor_AutoContrast_Desc")));
|
|
|
|
Parameters.Add("UseCLAHE", new ProcessorParameter(
|
|
"UseCLAHE",
|
|
LocalizationHelper.GetString("ContrastProcessor_UseCLAHE"),
|
|
typeof(bool),
|
|
false,
|
|
null,
|
|
null,
|
|
LocalizationHelper.GetString("ContrastProcessor_UseCLAHE_Desc")));
|
|
|
|
Parameters.Add("ClipLimit", new ProcessorParameter(
|
|
"ClipLimit",
|
|
LocalizationHelper.GetString("ContrastProcessor_ClipLimit"),
|
|
typeof(double),
|
|
2.0,
|
|
1.0,
|
|
10.0,
|
|
LocalizationHelper.GetString("ContrastProcessor_ClipLimit_Desc")));
|
|
_logger.Debug("InitializeParameters");
|
|
}
|
|
|
|
public override Image<Gray, byte> Process(Image<Gray, byte> inputImage)
|
|
{
|
|
double contrast = GetParameter<double>("Contrast");
|
|
int brightness = GetParameter<int>("Brightness");
|
|
bool autoContrast = GetParameter<bool>("AutoContrast");
|
|
bool useCLAHE = GetParameter<bool>("UseCLAHE");
|
|
double clipLimit = GetParameter<double>("ClipLimit");
|
|
|
|
var result = inputImage.Clone();
|
|
|
|
if (useCLAHE)
|
|
{
|
|
result = ApplyCLAHE(inputImage, clipLimit);
|
|
}
|
|
else if (autoContrast)
|
|
{
|
|
result = AutoContrastStretch(inputImage);
|
|
}
|
|
else
|
|
{
|
|
result = inputImage * contrast + brightness;
|
|
}
|
|
_logger.Debug("Process: Contrast = {contrast},Brightness = {brightness}," +
|
|
"AutoContrast = {autoContrast},UseCLAHE = {useCLAHE}, ClipLimit = {clipLimit}", contrast, brightness, autoContrast, useCLAHE, clipLimit);
|
|
return result;
|
|
}
|
|
|
|
private Image<Gray, byte> AutoContrastStretch(Image<Gray, byte> inputImage)
|
|
{
|
|
double minVal = 0, maxVal = 0;
|
|
Point minLoc = new Point();
|
|
Point maxLoc = new Point();
|
|
CvInvoke.MinMaxLoc(inputImage, ref minVal, ref maxVal, ref minLoc, ref maxLoc);
|
|
|
|
if (minVal == 0 && maxVal == 255)
|
|
{
|
|
return inputImage.Clone();
|
|
}
|
|
|
|
var floatImage = inputImage.Convert<Gray, float>();
|
|
|
|
if (maxVal > minVal)
|
|
{
|
|
floatImage = (floatImage - minVal) * (255.0 / (maxVal - minVal));
|
|
}
|
|
_logger.Debug("AutoContrastStretch");
|
|
return floatImage.Convert<Gray, byte>();
|
|
}
|
|
|
|
private Image<Gray, byte> ApplyCLAHE(Image<Gray, byte> inputImage, double clipLimit)
|
|
{
|
|
int tileSize = 8;
|
|
int width = inputImage.Width;
|
|
int height = inputImage.Height;
|
|
byte[,,] srcData = inputImage.Data;
|
|
|
|
// 计算分块数
|
|
int tilesX = (width + tileSize - 1) / tileSize;
|
|
int tilesY = (height + tileSize - 1) / tileSize;
|
|
int actualTileW = (width + tilesX - 1) / tilesX;
|
|
int actualTileH = (height + tilesY - 1) / tilesY;
|
|
|
|
// 为每个 tile 计算带 clip limit 的均衡化映射表
|
|
var luts = new byte[tilesY, tilesX, 256];
|
|
for (int ty = 0; ty < tilesY; ty++)
|
|
{
|
|
for (int tx = 0; tx < tilesX; tx++)
|
|
{
|
|
int x0 = tx * actualTileW;
|
|
int y0 = ty * actualTileH;
|
|
int x1 = Math.Min(x0 + actualTileW, width);
|
|
int y1 = Math.Min(y0 + actualTileH, height);
|
|
int tilePixels = (x1 - x0) * (y1 - y0);
|
|
|
|
// 构建直方图
|
|
var hist = new int[256];
|
|
for (int y = y0; y < y1; y++)
|
|
for (int x = x0; x < x1; x++)
|
|
hist[srcData[y, x, 0]]++;
|
|
|
|
// Clip limit 裁剪并重新分配
|
|
int clipThreshold = (int)(clipLimit * tilePixels / 256);
|
|
if (clipThreshold > 0)
|
|
{
|
|
int excess = 0;
|
|
for (int i = 0; i < 256; i++)
|
|
{
|
|
if (hist[i] > clipThreshold)
|
|
{
|
|
excess += hist[i] - clipThreshold;
|
|
hist[i] = clipThreshold;
|
|
}
|
|
}
|
|
int avgInc = excess / 256;
|
|
int remainder = excess - avgInc * 256;
|
|
for (int i = 0; i < 256; i++)
|
|
hist[i] += avgInc + (i < remainder ? 1 : 0);
|
|
}
|
|
|
|
// 构建 CDF 映射表
|
|
int sum = 0;
|
|
for (int i = 0; i < 256; i++)
|
|
{
|
|
sum += hist[i];
|
|
luts[ty, tx, i] = (byte)Math.Clamp(sum * 255 / tilePixels, 0, 255);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 双线性插值生成结果
|
|
var result = new Image<Gray, byte>(width, height);
|
|
byte[,,] dstData = result.Data;
|
|
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
// 计算当前像素所在 tile 的中心坐标
|
|
double fx = (double)x / actualTileW - 0.5;
|
|
double fy = (double)y / actualTileH - 0.5;
|
|
int tx0 = Math.Max(0, (int)fx);
|
|
int ty0 = Math.Max(0, (int)fy);
|
|
int tx1 = Math.Min(tx0 + 1, tilesX - 1);
|
|
int ty1 = Math.Min(ty0 + 1, tilesY - 1);
|
|
double ax = fx - tx0;
|
|
double ay = fy - ty0;
|
|
ax = Math.Clamp(ax, 0, 1);
|
|
ay = Math.Clamp(ay, 0, 1);
|
|
|
|
byte val = srcData[y, x, 0];
|
|
double v00 = luts[ty0, tx0, val];
|
|
double v10 = luts[ty0, tx1, val];
|
|
double v01 = luts[ty1, tx0, val];
|
|
double v11 = luts[ty1, tx1, val];
|
|
|
|
double interpolated = v00 * (1 - ax) * (1 - ay) + v10 * ax * (1 - ay)
|
|
+ v01 * (1 - ax) * ay + v11 * ax * ay;
|
|
dstData[y, x, 0] = (byte)Math.Clamp((int)(interpolated + 0.5), 0, 255);
|
|
}
|
|
}
|
|
|
|
_logger.Debug("ApplyCLAHE: ClipLimit={ClipLimit}, TileSize={TileSize}", clipLimit, tileSize);
|
|
return result;
|
|
}
|
|
} |