// ============================================================================
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
// 文件名: ThresholdProcessor.cs
// 描述: 阈值分割算子,用于图像二值化处理
// 功能:
// - 固定阈值二值化
// - Otsu自动阈值计算
// - 可调节阈值和最大值
// - 将灰度图像转换为二值图像
// 算法: 阈值分割、Otsu算法
// 作者: 李伟 wei.lw.li@hexagon.com
// ============================================================================
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
using XP.ImageProcessing.Core;
using Serilog;
namespace XP.ImageProcessing.Processors;
///
/// 阈值分割算子(支持 8 位和 16 位灰度图像)
///
public class ThresholdProcessor : ImageProcessorBase
where TDepth : struct, IComparable
{
private static readonly ILogger _logger = Log.ForContext>();
public ThresholdProcessor()
{
Name = LocalizationHelper.GetString("ThresholdProcessor_Name");
Description = LocalizationHelper.GetString("ThresholdProcessor_Description");
}
protected override void InitializeParameters()
{
// 参数范围必须跟随当前算子的位深。主流程使用 ushort,因此默认阈值也按
// 16 位满量程计算,避免把 8 位的 64/192 直接套用到 0~65535。
int quarterValue = MaxPixelValue / 4;
int threeQuarterValue = MaxPixelValue * 3 / 4;
Parameters.Add("MinThreshold", new ProcessorParameter(
"MinThreshold",
LocalizationHelper.GetString("ThresholdProcessor_MinThreshold"),
typeof(int), quarterValue, 0, MaxPixelValue,
LocalizationHelper.GetString("ThresholdProcessor_MinThreshold_Desc")));
Parameters.Add("MaxThreshold", new ProcessorParameter(
"MaxThreshold",
LocalizationHelper.GetString("ThresholdProcessor_MaxThreshold"),
typeof(int), threeQuarterValue, 0, MaxPixelValue,
LocalizationHelper.GetString("ThresholdProcessor_MaxThreshold_Desc")));
Parameters.Add("UseOtsu", new ProcessorParameter(
"UseOtsu",
LocalizationHelper.GetString("ThresholdProcessor_UseOtsu"),
typeof(bool), false, null, null,
LocalizationHelper.GetString("ThresholdProcessor_UseOtsu_Desc")));
_logger.Debug("InitializeParameters");
}
public override Image Process(Image inputImage)
{
int minThreshold = GetParameter("MinThreshold");
int maxThreshold = GetParameter("MaxThreshold");
bool useOtsu = GetParameter("UseOtsu");
int height = inputImage.Height;
int width = inputImage.Width;
int maxVal = MaxPixelValue;
var result = new Image(inputImage.Size);
if (useOtsu)
{
// 8 位直接用 OpenCV Otsu;16 位在完整位深直方图上原生计算 Otsu 阈值,避免降位
if (typeof(TDepth) == typeof(byte))
{
using var res8 = new Image(inputImage.Size);
CvInvoke.Threshold(inputImage as Image ?? inputImage.Convert(),
res8, minThreshold, 255, ThresholdType.Otsu);
_logger.Debug("Process: UseOtsu=true (8bit native)");
return res8 as Image ?? PixelDepthHelper.FromByteImage(res8);
}
else
{
int otsuThreshold = ComputeOtsuThreshold16((inputImage as Image)!, maxVal);
Parallel.For(0, height, y =>
{
for (int x = 0; x < width; x++)
{
int val = PixelDepthHelper.ReadPixel(inputImage, y, x);
PixelDepthHelper.WritePixel(result, y, x, val > otsuThreshold ? maxVal : 0);
}
});
_logger.Debug("Process: UseOtsu=true (16bit native), threshold={Threshold}", otsuThreshold);
return result;
}
}
// 手工双阈值分割(支持全位深)
Parallel.For(0, height, y =>
{
for (int x = 0; x < width; x++)
{
int val = PixelDepthHelper.ReadPixel(inputImage, y, x);
PixelDepthHelper.WritePixel(result, y, x,
(val >= minThreshold && val <= maxThreshold) ? maxVal : 0);
}
});
_logger.Debug("Process: MinThreshold={Min}, MaxThreshold={Max}", minThreshold, maxThreshold);
return result;
}
///
/// 在完整 16 位直方图(65536 bin)上计算 Otsu 最优阈值(最大化类间方差)。
/// 返回值为灰度阈值:像素值 > 阈值 判为前景。
///
private static int ComputeOtsuThreshold16(Image image, int maxVal)
{
int levels = maxVal + 1;
var histogram = new long[levels];
int h = image.Height, w = image.Width;
var data = image.Data;
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
histogram[data[y, x, 0]]++;
long totalPixels = (long)w * h;
if (totalPixels == 0) return maxVal / 2;
double totalSum = 0;
for (int i = 0; i < levels; i++)
totalSum += (double)i * histogram[i];
double bgSum = 0;
long bgPixels = 0;
double maxVariance = -1;
int bestThreshold = maxVal / 2; // Default to midpoint if no valid threshold found
for (int t = 0; t < levels; t++)
{
bgPixels += histogram[t];
if (bgPixels == 0) continue;
long fgPixels = totalPixels - bgPixels;
if (fgPixels == 0) break;
bgSum += (double)t * histogram[t];
double bgMean = bgSum / bgPixels;
double fgMean = (totalSum - bgSum) / fgPixels;
double variance = (double)bgPixels * fgPixels * (bgMean - fgMean) * (bgMean - fgMean);
if (variance > maxVariance)
{
maxVariance = variance;
bestThreshold = t;
}
}
return bestThreshold;
}
}