// ============================================================================ // Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. // 文件名: HistogramOverlayProcessor.cs // 描述: 直方图叠加算子,计算灰度直方图并以蓝色柱状图绘制到结果图像左上角 // 功能: // - 计算输入图像的灰度直方图 // - 将直方图绘制为蓝色半透明柱状图叠加到图像左上角 // - 输出直方图统计表格数据 // 算法: 灰度直方图统计 + 彩色图像叠加 // 作者: 李伟 wei.lw.li@hexagon.com // ============================================================================ using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using XP.ImageProcessing.Core; using Serilog; using System.Drawing; using System.Text; namespace XP.ImageProcessing.Processors; /// /// 直方图叠加算子,计算灰度直方图并以蓝色柱状图绘制到结果图像左上角,同时输出统计表格 /// public class HistogramOverlayProcessor : ImageProcessorBase { private static readonly ILogger _logger = Log.ForContext(); // 固定参数 private const int ChartWidth = 256; // 柱状图绘图区宽度 private const int ChartHeight = 200; // 柱状图绘图区高度 private const int AxisMarginLeft = 50; // Y轴标签预留宽度 private const int AxisMarginBottom = 25; // X轴标签预留高度 private const int Padding = 8; // 背景额外内边距 private const int PaddingRight = 25; // 右侧额外内边距(容纳X轴末尾刻度文字) private const int Margin = 10; // 距图像左上角边距 private const float BgAlpha = 0.6f; private const double FontScale = 0.35; private const int FontThickness = 1; public HistogramOverlayProcessor() { Name = LocalizationHelper.GetString("HistogramOverlayProcessor_Name"); Description = LocalizationHelper.GetString("HistogramOverlayProcessor_Description"); } protected override void InitializeParameters() { // 无可调参数 } public override Image Process(Image inputImage) { int h = inputImage.Height; int w = inputImage.Width; var srcData = inputImage.Data; // === 1. 计算灰度直方图 === var hist = new int[256]; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) hist[srcData[y, x, 0]]++; int maxCount = 0; long totalPixels = (long)h * w; for (int i = 0; i < 256; i++) if (hist[i] > maxCount) maxCount = hist[i]; // === 2. 计算统计信息 === double mean = 0, variance = 0; int minVal = 255, maxVal = 0; int modeVal = 0, modeCount = 0; long medianTarget = totalPixels / 2, cumulative = 0; int medianVal = 0; bool medianFound = false; for (int i = 0; i < 256; i++) { if (hist[i] > 0) { if (i < minVal) minVal = i; if (i > maxVal) maxVal = i; } if (hist[i] > modeCount) { modeCount = hist[i]; modeVal = i; } mean += (double)i * hist[i]; cumulative += hist[i]; if (!medianFound && cumulative >= medianTarget) { medianVal = i; medianFound = true; } } mean /= totalPixels; for (int i = 0; i < 256; i++) variance += hist[i] * (i - mean) * (i - mean); variance /= totalPixels; double stdDev = Math.Sqrt(variance); // === 3. 输出表格数据 === var sb = new StringBuilder(); sb.AppendLine("=== 灰度直方图统计 ==="); sb.AppendLine($"图像尺寸: {w} x {h}"); sb.AppendLine($"总像素数: {totalPixels}"); sb.AppendLine($"最小灰度: {minVal}"); sb.AppendLine($"最大灰度: {maxVal}"); sb.AppendLine($"平均灰度: {mean:F2}"); sb.AppendLine($"中位灰度: {medianVal}"); sb.AppendLine($"众数灰度: {modeVal} (出现 {modeCount} 次)"); sb.AppendLine($"标准差: {stdDev:F2}"); sb.AppendLine(); sb.AppendLine("灰度值\t像素数\t占比(%)"); for (int i = 0; i < 256; i++) { if (hist[i] > 0) sb.AppendLine($"{i}\t{hist[i]}\t{(double)hist[i] / totalPixels * 100.0:F4}"); } OutputData["HistogramTable"] = sb.ToString(); OutputData["Histogram"] = hist; // === 4. 生成彩色叠加图像(蓝色柱状图 + XY轴坐标) === var colorImage = inputImage.Convert(); var colorData = colorImage.Data; // 布局:背景区域包含 Padding + Y轴标签 + 绘图区 + Padding(水平) // Padding + 绘图区 + X轴标签 + Padding(垂直) int totalW = Padding + AxisMarginLeft + ChartWidth + PaddingRight; int totalH = Padding + ChartHeight + AxisMarginBottom + Padding; int bgW = Math.Min(totalW, w - Margin); int bgH = Math.Min(totalH, h - Margin); if (bgW > Padding + AxisMarginLeft && bgH > Padding + AxisMarginBottom) { int plotW = Math.Min(ChartWidth, bgW - Padding - AxisMarginLeft - PaddingRight); int plotH = Math.Min(ChartHeight, bgH - Padding - AxisMarginBottom - Padding); if (plotW <= 0 || plotH <= 0) goto SkipOverlay; // 绘图区左上角在图像中的坐标 int plotX0 = Margin + Padding + AxisMarginLeft; int plotY0 = Margin + Padding; // 计算每列柱高 double binWidth = (double)plotW / 256.0; var barHeights = new int[plotW]; for (int px = 0; px < plotW; px++) { int bin = Math.Min((int)(px / binWidth), 255); barHeights[px] = maxCount > 0 ? (int)((long)hist[bin] * (plotH - 1) / maxCount) : 0; } float alpha = BgAlpha; float inv = 1.0f - alpha; // 绘制半透明黑色背景(覆盖整个区域含坐标轴和内边距) Parallel.For(0, bgH, dy => { int imgY = Margin + dy; if (imgY >= h) return; for (int dx = 0; dx < bgW; dx++) { int imgX = Margin + dx; if (imgX >= w) break; colorData[imgY, imgX, 0] = (byte)(int)(colorData[imgY, imgX, 0] * inv); colorData[imgY, imgX, 1] = (byte)(int)(colorData[imgY, imgX, 1] * inv); colorData[imgY, imgX, 2] = (byte)(int)(colorData[imgY, imgX, 2] * inv); } }); // 绘制蓝色柱状图 Parallel.For(0, plotH, dy => { int imgY = plotY0 + dy; if (imgY >= h) return; int rowFromBottom = plotH - 1 - dy; for (int dx = 0; dx < plotW; dx++) { int imgX = plotX0 + dx; if (imgX >= w) break; if (rowFromBottom < barHeights[dx]) { byte curB = colorData[imgY, imgX, 0]; byte curG = colorData[imgY, imgX, 1]; byte curR = colorData[imgY, imgX, 2]; colorData[imgY, imgX, 0] = (byte)Math.Clamp(curB + (int)(255 * alpha), 0, 255); colorData[imgY, imgX, 1] = (byte)Math.Clamp(curG + (int)(50 * alpha), 0, 255); colorData[imgY, imgX, 2] = (byte)Math.Clamp(curR + (int)(50 * alpha), 0, 255); } } }); // === 5. 绘制坐标轴线和刻度标注 === var white = new MCvScalar(255, 255, 255); var gray = new MCvScalar(180, 180, 180); // Y轴线 CvInvoke.Line(colorImage, new Point(plotX0, plotY0), new Point(plotX0, plotY0 + plotH), white, 1); // X轴线 CvInvoke.Line(colorImage, new Point(plotX0, plotY0 + plotH), new Point(plotX0 + plotW, plotY0 + plotH), white, 1); // X轴刻度: 0, 64, 128, 192, 255 int[] xTicks = { 0, 64, 128, 192, 255 }; foreach (int tick in xTicks) { int tx = plotX0 + (int)(tick * binWidth); if (tx >= w) break; CvInvoke.Line(colorImage, new Point(tx, plotY0 + plotH), new Point(tx, plotY0 + plotH + 4), white, 1); string label = tick.ToString(); CvInvoke.PutText(colorImage, label, new Point(tx - 8, plotY0 + plotH + 18), FontFace.HersheySimplex, FontScale, white, FontThickness); } // Y轴刻度: 0%, 25%, 50%, 75%, 100% for (int i = 0; i <= 4; i++) { int val = maxCount * i / 4; int ty = plotY0 + plotH - (int)((long)plotH * i / 4); CvInvoke.Line(colorImage, new Point(plotX0 - 4, ty), new Point(plotX0, ty), white, 1); // 网格虚线 if (i > 0 && i < 4) { for (int gx = plotX0 + 2; gx < plotX0 + plotW; gx += 6) { int gxEnd = Math.Min(gx + 2, plotX0 + plotW); CvInvoke.Line(colorImage, new Point(gx, ty), new Point(gxEnd, ty), gray, 1); } } string label = FormatCount(val); CvInvoke.PutText(colorImage, label, new Point(Margin + Padding, ty + 4), FontFace.HersheySimplex, FontScale, white, FontThickness); } } SkipOverlay: OutputData["PseudoColorImage"] = colorImage; _logger.Debug("Process completed: histogram overlay, mean={Mean:F2}, stdDev={Std:F2}", mean, stdDev); return inputImage.Clone(); } /// /// 格式化像素计数为紧凑字符串(如 12345 → "12.3K") /// private static string FormatCount(int count) { if (count >= 1_000_000) return $"{count / 1_000_000.0:F1}M"; if (count >= 1_000) return $"{count / 1_000.0:F1}K"; return count.ToString(); } }