Files
XplorePlane/XP.ImageProcessing.Processors/图像增强/HistogramOverlayProcessor.cs
T
2026-04-13 14:36:18 +08:00

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 Serilog;
using System.Drawing;
using System.Text;
using XP.ImageProcessing.Core;
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();
}
}