// ============================================================================ // 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; /// /// 对比度调整算子 /// public class ContrastProcessor : ImageProcessorBase { private static readonly ILogger _logger = Log.ForContext(); 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 Process(Image inputImage) { double contrast = GetParameter("Contrast"); int brightness = GetParameter("Brightness"); bool autoContrast = GetParameter("AutoContrast"); bool useCLAHE = GetParameter("UseCLAHE"); double clipLimit = GetParameter("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 AutoContrastStretch(Image 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(); if (maxVal > minVal) { floatImage = (floatImage - minVal) * (255.0 / (maxVal - minVal)); } _logger.Debug("AutoContrastStretch"); return floatImage.Convert(); } private Image ApplyCLAHE(Image 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(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; } }