267 lines
10 KiB
C#
267 lines
10 KiB
C#
// ============================================================================
|
||
// 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;
|
||
|
||
/// <summary>
|
||
/// 直方图叠加算子,计算灰度直方图并以蓝色柱状图绘制到结果图像左上角,同时输出统计表格
|
||
/// </summary>
|
||
public class HistogramOverlayProcessor : ImageProcessorBase
|
||
{
|
||
private static readonly ILogger _logger = Log.ForContext<HistogramOverlayProcessor>();
|
||
|
||
// 固定参数
|
||
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<Gray, byte> Process(Image<Gray, byte> 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<Bgr, byte>();
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 格式化像素计数为紧凑字符串(如 12345 → "12.3K")
|
||
/// </summary>
|
||
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();
|
||
}
|
||
}
|