// ============================================================================ // 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; } }