bbb3bf5326
1. 资源泄漏:HighPass/LowPass滤波器Image对象未dispose(改用using+Clone) 2. 线程安全:GammaProcessor LUT从实例字段改为方法局部变量 3. 资源泄漏:BandPassFilter补充floatImage和mask的Dispose 4. 边界情况:ThresholdProcessor Otsu16初始阈值改为中值(防全黑/全白) 5. 异常安全:RemoveOutliers的medianImage用try-finally保证释放 6. 空引用:SuperResolution InputMetadata空检查
165 lines
6.4 KiB
C#
165 lines
6.4 KiB
C#
// ============================================================================
|
|
// 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;
|
|
|
|
/// <summary>
|
|
/// 阈值分割算子(支持 8 位和 16 位灰度图像)
|
|
/// </summary>
|
|
public class ThresholdProcessor<TDepth> : ImageProcessorBase<TDepth>
|
|
where TDepth : struct, IComparable
|
|
{
|
|
private static readonly ILogger _logger = Log.ForContext<ThresholdProcessor<TDepth>>();
|
|
|
|
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<Gray, TDepth> Process(Image<Gray, TDepth> inputImage)
|
|
{
|
|
int minThreshold = GetParameter<int>("MinThreshold");
|
|
int maxThreshold = GetParameter<int>("MaxThreshold");
|
|
bool useOtsu = GetParameter<bool>("UseOtsu");
|
|
|
|
int height = inputImage.Height;
|
|
int width = inputImage.Width;
|
|
int maxVal = MaxPixelValue;
|
|
|
|
var result = new Image<Gray, TDepth>(inputImage.Size);
|
|
|
|
if (useOtsu)
|
|
{
|
|
// 8 位直接用 OpenCV Otsu;16 位在完整位深直方图上原生计算 Otsu 阈值,避免降位
|
|
if (typeof(TDepth) == typeof(byte))
|
|
{
|
|
using var res8 = new Image<Gray, byte>(inputImage.Size);
|
|
CvInvoke.Threshold(inputImage as Image<Gray, byte> ?? inputImage.Convert<Gray, byte>(),
|
|
res8, minThreshold, 255, ThresholdType.Otsu);
|
|
_logger.Debug("Process: UseOtsu=true (8bit native)");
|
|
return res8 as Image<Gray, TDepth> ?? PixelDepthHelper.FromByteImage<TDepth>(res8);
|
|
}
|
|
else
|
|
{
|
|
int otsuThreshold = ComputeOtsuThreshold16((inputImage as Image<Gray, ushort>)!, 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 在完整 16 位直方图(65536 bin)上计算 Otsu 最优阈值(最大化类间方差)。
|
|
/// 返回值为灰度阈值:像素值 > 阈值 判为前景。
|
|
/// </summary>
|
|
private static int ComputeOtsuThreshold16(Image<Gray, ushort> 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;
|
|
}
|
|
}
|