已合并 PR 238: 图像处理算子参数拆分出常用和高级参数
1. Bug 修复 — OpenCV 类型限制 双边滤波:OpenCV BilateralFilter 不支持 CV_16U,16 位图像改为先转 32F 再执行滤波后转回 中值滤波 / 去离群点:OpenCV MedianBlur 不支持 CV_16U,16 位图像转 32F 处理;KernelSize 最大值限制为 5(CV_32F 仅支持 kernel 3 和 5) 2. 高级参数折叠面板 ProcessorParameter 新增 IsAdvanced 属性,标记非核心参数 UI 层新增"高级参数" Expander(默认折叠),仅在有高级参数时显示 约 42 个参数标记为高级,用户日常操作只需关注核心参数 修复 Expander 内 FontWeight 继承导致的字体样式不一致 3. 默认值优化(适配平面 CT DR 图像) 锐化方法:Laplacian → UnsharpMask(对噪声更温和) 直方图均衡化:Global → CLAHE(局部对比度更优) 对比度调整:AutoContrast 默认开启,其余参数移入高级 算子方法下拉框统一移入高级参数 FilmEffect 窗宽窗位默认值按 16 位配置(32768 / 65535) 4. 代码质量修复 资源泄漏:HighPass/LowPass 滤波器 Image 对象改用 using + Clone();BandPassFilter 补充 floatImage 和 mask 的释放 线程安全:GammaProcessor LUT 从实例字段改为方法局部变量,消除并发竞态 边界情况:Otsu16 阈值初始值改为 maxVal/2,防止全黑/全白图返回 0 异常安全:RemoveOutliers 的 medianImage 用 try-finally 保证释放 空引用防护:SuperResolution InputMetadata 添加空检查 目录整理:移除"其他"分类,FilmEffect → 图像增强,PseudoColor → 图像变换
This commit is contained in:
@@ -45,6 +45,9 @@ public class ProcessorParameter
|
||||
/// <summary>参数是否可见</summary>
|
||||
public bool IsVisible { get; set; } = true;
|
||||
|
||||
/// <summary>是否为高级参数(默认折叠隐藏,展开后可见)</summary>
|
||||
public bool IsAdvanced { get; set; } = false;
|
||||
|
||||
public ProcessorParameter(string name, string displayName, Type valueType, object defaultValue,
|
||||
object? minValue = null, object? maxValue = null, string description = "", string[]? options = null)
|
||||
{
|
||||
|
||||
+154
-154
@@ -1,155 +1,155 @@
|
||||
// ============================================================================
|
||||
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
||||
// 文件名: PseudoColorProcessor.cs
|
||||
// 描述: 伪色彩渲染算子,将灰度图像映射为彩色图像
|
||||
// 功能:
|
||||
// - 支持多种 OpenCV 内置色彩映射表(Jet、Hot、Cool、Rainbow 等)
|
||||
// - 可选灰度范围裁剪,突出感兴趣的灰度区间
|
||||
// - 可选反转色彩映射方向
|
||||
// 算法: 查找表(LUT)色彩映射
|
||||
// 作者: 李伟 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 位灰度图像)。
|
||||
/// 灰度主图按原位深透传(无损失);彩色叠加图因 OpenCV ApplyColorMap 仅支持 CV_8U,
|
||||
/// 会将灰度归一化到 8 位后映射(彩色显示固有限制,不影响主数据链路精度)。
|
||||
/// </summary>
|
||||
public class PseudoColorProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
where TDepth : struct, IComparable
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<PseudoColorProcessor<TDepth>>();
|
||||
|
||||
public PseudoColorProcessor()
|
||||
{
|
||||
Name = LocalizationHelper.GetString("PseudoColorProcessor_Name");
|
||||
Description = LocalizationHelper.GetString("PseudoColorProcessor_Description");
|
||||
}
|
||||
|
||||
protected override void InitializeParameters()
|
||||
{
|
||||
Parameters.Add("ColorMap", new ProcessorParameter(
|
||||
"ColorMap",
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_ColorMap"),
|
||||
typeof(string),
|
||||
"Jet",
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_ColorMap_Desc"),
|
||||
new string[] { "Jet", "Hot", "Cool", "Rainbow", "HSV", "Turbo", "Inferno", "Magma", "Plasma", "Bone", "Ocean", "Spring", "Summer", "Autumn", "Winter" }));
|
||||
|
||||
Parameters.Add("MinValue", new ProcessorParameter(
|
||||
"MinValue",
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_MinValue"),
|
||||
typeof(int),
|
||||
0,
|
||||
0,
|
||||
255,
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_MinValue_Desc")));
|
||||
|
||||
Parameters.Add("MaxValue", new ProcessorParameter(
|
||||
"MaxValue",
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_MaxValue"),
|
||||
typeof(int),
|
||||
255,
|
||||
0,
|
||||
255,
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_MaxValue_Desc")));
|
||||
|
||||
Parameters.Add("InvertMap", new ProcessorParameter(
|
||||
"InvertMap",
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_InvertMap"),
|
||||
typeof(bool),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_InvertMap_Desc")));
|
||||
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
public override Image<Gray, TDepth> Process(Image<Gray, TDepth> inputImage)
|
||||
{
|
||||
string colorMapName = GetParameter<string>("ColorMap");
|
||||
int minValue = GetParameter<int>("MinValue");
|
||||
int maxValue = GetParameter<int>("MaxValue");
|
||||
bool invertMap = GetParameter<bool>("InvertMap");
|
||||
|
||||
OutputData.Clear();
|
||||
|
||||
// 彩色映射只能作用于 8 位:先将输入降到 8 位(彩色显示固有限制)
|
||||
using var input8 = PixelDepthHelper.ToByteImage(inputImage);
|
||||
|
||||
// 灰度范围裁剪与归一化(MinValue/MaxValue 按 8 位语义 0-255)
|
||||
Image<Gray, byte> normalized;
|
||||
if (minValue > 0 || maxValue < 255)
|
||||
{
|
||||
// 将 [minValue, maxValue] 映射到 [0, 255]
|
||||
normalized = input8.Clone();
|
||||
double scale = 255.0 / Math.Max(maxValue - minValue, 1);
|
||||
for (int y = 0; y < normalized.Height; y++)
|
||||
{
|
||||
for (int x = 0; x < normalized.Width; x++)
|
||||
{
|
||||
int val = normalized.Data[y, x, 0];
|
||||
val = Math.Clamp(val, minValue, maxValue);
|
||||
normalized.Data[y, x, 0] = (byte)((val - minValue) * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
normalized = input8.Clone();
|
||||
}
|
||||
|
||||
// 反转灰度(反转色彩映射方向)
|
||||
if (invertMap)
|
||||
{
|
||||
CvInvoke.BitwiseNot(normalized, normalized);
|
||||
}
|
||||
|
||||
// 应用色彩映射
|
||||
ColorMapType cmType = colorMapName switch
|
||||
{
|
||||
"Hot" => ColorMapType.Hot,
|
||||
"Cool" => ColorMapType.Cool,
|
||||
"Rainbow" => ColorMapType.Rainbow,
|
||||
"HSV" => ColorMapType.Hsv,
|
||||
"Turbo" => ColorMapType.Turbo,
|
||||
"Inferno" => ColorMapType.Inferno,
|
||||
"Magma" => ColorMapType.Magma,
|
||||
"Plasma" => ColorMapType.Plasma,
|
||||
"Bone" => ColorMapType.Bone,
|
||||
"Ocean" => ColorMapType.Ocean,
|
||||
"Spring" => ColorMapType.Spring,
|
||||
"Summer" => ColorMapType.Summer,
|
||||
"Autumn" => ColorMapType.Autumn,
|
||||
"Winter" => ColorMapType.Winter,
|
||||
_ => ColorMapType.Jet
|
||||
};
|
||||
|
||||
using var colorMat = new Mat();
|
||||
CvInvoke.ApplyColorMap(normalized.Mat, colorMat, cmType);
|
||||
|
||||
var colorImage = colorMat.ToImage<Bgr, byte>();
|
||||
|
||||
// 将彩色图像存入 OutputData,供 UI 显示
|
||||
OutputData["PseudoColorImage"] = colorImage;
|
||||
|
||||
_logger.Debug("Process: ColorMap={ColorMap}, MinValue={Min}, MaxValue={Max}, InvertMap={Invert}",
|
||||
colorMapName, minValue, maxValue, invertMap);
|
||||
|
||||
normalized.Dispose();
|
||||
|
||||
// 返回原始灰度图像(彩色图像通过 OutputData 传递)
|
||||
return inputImage.Clone();
|
||||
}
|
||||
// ============================================================================
|
||||
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
||||
// 文件名: PseudoColorProcessor.cs
|
||||
// 描述: 伪色彩渲染算子,将灰度图像映射为彩色图像
|
||||
// 功能:
|
||||
// - 支持多种 OpenCV 内置色彩映射表(Jet、Hot、Cool、Rainbow 等)
|
||||
// - 可选灰度范围裁剪,突出感兴趣的灰度区间
|
||||
// - 可选反转色彩映射方向
|
||||
// 算法: 查找表(LUT)色彩映射
|
||||
// 作者: 李伟 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 位灰度图像)。
|
||||
/// 灰度主图按原位深透传(无损失);彩色叠加图因 OpenCV ApplyColorMap 仅支持 CV_8U,
|
||||
/// 会将灰度归一化到 8 位后映射(彩色显示固有限制,不影响主数据链路精度)。
|
||||
/// </summary>
|
||||
public class PseudoColorProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
where TDepth : struct, IComparable
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<PseudoColorProcessor<TDepth>>();
|
||||
|
||||
public PseudoColorProcessor()
|
||||
{
|
||||
Name = LocalizationHelper.GetString("PseudoColorProcessor_Name");
|
||||
Description = LocalizationHelper.GetString("PseudoColorProcessor_Description");
|
||||
}
|
||||
|
||||
protected override void InitializeParameters()
|
||||
{
|
||||
Parameters.Add("ColorMap", new ProcessorParameter(
|
||||
"ColorMap",
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_ColorMap"),
|
||||
typeof(string),
|
||||
"Jet",
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_ColorMap_Desc"),
|
||||
new string[] { "Jet", "Hot", "Cool", "Rainbow", "HSV", "Turbo", "Inferno", "Magma", "Plasma", "Bone", "Ocean", "Spring", "Summer", "Autumn", "Winter" }));
|
||||
|
||||
Parameters.Add("MinValue", new ProcessorParameter(
|
||||
"MinValue",
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_MinValue"),
|
||||
typeof(int),
|
||||
0,
|
||||
0,
|
||||
255,
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_MinValue_Desc")));
|
||||
|
||||
Parameters.Add("MaxValue", new ProcessorParameter(
|
||||
"MaxValue",
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_MaxValue"),
|
||||
typeof(int),
|
||||
255,
|
||||
0,
|
||||
255,
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_MaxValue_Desc")));
|
||||
|
||||
Parameters.Add("InvertMap", new ProcessorParameter(
|
||||
"InvertMap",
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_InvertMap"),
|
||||
typeof(bool),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("PseudoColorProcessor_InvertMap_Desc")));
|
||||
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
public override Image<Gray, TDepth> Process(Image<Gray, TDepth> inputImage)
|
||||
{
|
||||
string colorMapName = GetParameter<string>("ColorMap");
|
||||
int minValue = GetParameter<int>("MinValue");
|
||||
int maxValue = GetParameter<int>("MaxValue");
|
||||
bool invertMap = GetParameter<bool>("InvertMap");
|
||||
|
||||
OutputData.Clear();
|
||||
|
||||
// 彩色映射只能作用于 8 位:先将输入降到 8 位(彩色显示固有限制)
|
||||
using var input8 = PixelDepthHelper.ToByteImage(inputImage);
|
||||
|
||||
// 灰度范围裁剪与归一化(MinValue/MaxValue 按 8 位语义 0-255)
|
||||
Image<Gray, byte> normalized;
|
||||
if (minValue > 0 || maxValue < 255)
|
||||
{
|
||||
// 将 [minValue, maxValue] 映射到 [0, 255]
|
||||
normalized = input8.Clone();
|
||||
double scale = 255.0 / Math.Max(maxValue - minValue, 1);
|
||||
for (int y = 0; y < normalized.Height; y++)
|
||||
{
|
||||
for (int x = 0; x < normalized.Width; x++)
|
||||
{
|
||||
int val = normalized.Data[y, x, 0];
|
||||
val = Math.Clamp(val, minValue, maxValue);
|
||||
normalized.Data[y, x, 0] = (byte)((val - minValue) * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
normalized = input8.Clone();
|
||||
}
|
||||
|
||||
// 反转灰度(反转色彩映射方向)
|
||||
if (invertMap)
|
||||
{
|
||||
CvInvoke.BitwiseNot(normalized, normalized);
|
||||
}
|
||||
|
||||
// 应用色彩映射
|
||||
ColorMapType cmType = colorMapName switch
|
||||
{
|
||||
"Hot" => ColorMapType.Hot,
|
||||
"Cool" => ColorMapType.Cool,
|
||||
"Rainbow" => ColorMapType.Rainbow,
|
||||
"HSV" => ColorMapType.Hsv,
|
||||
"Turbo" => ColorMapType.Turbo,
|
||||
"Inferno" => ColorMapType.Inferno,
|
||||
"Magma" => ColorMapType.Magma,
|
||||
"Plasma" => ColorMapType.Plasma,
|
||||
"Bone" => ColorMapType.Bone,
|
||||
"Ocean" => ColorMapType.Ocean,
|
||||
"Spring" => ColorMapType.Spring,
|
||||
"Summer" => ColorMapType.Summer,
|
||||
"Autumn" => ColorMapType.Autumn,
|
||||
"Winter" => ColorMapType.Winter,
|
||||
_ => ColorMapType.Jet
|
||||
};
|
||||
|
||||
using var colorMat = new Mat();
|
||||
CvInvoke.ApplyColorMap(normalized.Mat, colorMat, cmType);
|
||||
|
||||
var colorImage = colorMat.ToImage<Bgr, byte>();
|
||||
|
||||
// 将彩色图像存入 OutputData,供 UI 显示
|
||||
OutputData["PseudoColorImage"] = colorImage;
|
||||
|
||||
_logger.Debug("Process: ColorMap={ColorMap}, MinValue={Min}, MaxValue={Max}, InvertMap={Invert}",
|
||||
colorMapName, minValue, maxValue, invertMap);
|
||||
|
||||
normalized.Dispose();
|
||||
|
||||
// 返回原始灰度图像(彩色图像通过 OutputData 传递)
|
||||
return inputImage.Clone();
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ public class RotateProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("RotateProcessor_ExpandCanvas_Desc")));
|
||||
LocalizationHelper.GetString("RotateProcessor_ExpandCanvas_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("BackgroundValue", new ProcessorParameter(
|
||||
"BackgroundValue",
|
||||
@@ -61,7 +61,7 @@ public class RotateProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
0,
|
||||
0,
|
||||
255,
|
||||
LocalizationHelper.GetString("RotateProcessor_BackgroundValue_Desc")));
|
||||
LocalizationHelper.GetString("RotateProcessor_BackgroundValue_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("Interpolation", new ProcessorParameter(
|
||||
"Interpolation",
|
||||
@@ -71,7 +71,7 @@ public class RotateProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("RotateProcessor_Interpolation_Desc"),
|
||||
new string[] { "Nearest", "Bilinear", "Bicubic" }));
|
||||
new string[] { "Nearest", "Bilinear", "Bicubic" }) { IsAdvanced = true });
|
||||
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
@@ -35,20 +35,20 @@ public class ThresholdProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
|
||||
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(
|
||||
// 参数范围必须跟随当前算子的位深。主流程使用 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,
|
||||
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,
|
||||
typeof(int), threeQuarterValue, 0, MaxPixelValue,
|
||||
LocalizationHelper.GetString("ThresholdProcessor_MaxThreshold_Desc")));
|
||||
|
||||
Parameters.Add("UseOtsu", new ProcessorParameter(
|
||||
@@ -128,6 +128,8 @@ public class ThresholdProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
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];
|
||||
@@ -135,7 +137,7 @@ public class ThresholdProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
double bgSum = 0;
|
||||
long bgPixels = 0;
|
||||
double maxVariance = -1;
|
||||
int bestThreshold = 0;
|
||||
int bestThreshold = maxVal / 2; // Default to midpoint if no valid threshold found
|
||||
|
||||
for (int t = 0; t < levels; t++)
|
||||
{
|
||||
@@ -159,4 +161,4 @@ public class ThresholdProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
|
||||
return bestThreshold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public class ContrastProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
1.0,
|
||||
0.1,
|
||||
3.0,
|
||||
LocalizationHelper.GetString("ContrastProcessor_Contrast_Desc")));
|
||||
LocalizationHelper.GetString("ContrastProcessor_Contrast_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("Brightness", new ProcessorParameter(
|
||||
"Brightness",
|
||||
@@ -51,13 +51,13 @@ public class ContrastProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
0,
|
||||
-100,
|
||||
100,
|
||||
LocalizationHelper.GetString("ContrastProcessor_Brightness_Desc")));
|
||||
LocalizationHelper.GetString("ContrastProcessor_Brightness_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("AutoContrast", new ProcessorParameter(
|
||||
"AutoContrast",
|
||||
LocalizationHelper.GetString("ContrastProcessor_AutoContrast"),
|
||||
typeof(bool),
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("ContrastProcessor_AutoContrast_Desc")));
|
||||
@@ -69,7 +69,7 @@ public class ContrastProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("ContrastProcessor_UseCLAHE_Desc")));
|
||||
LocalizationHelper.GetString("ContrastProcessor_UseCLAHE_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("ClipLimit", new ProcessorParameter(
|
||||
"ClipLimit",
|
||||
@@ -78,7 +78,7 @@ public class ContrastProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
2.0,
|
||||
1.0,
|
||||
10.0,
|
||||
LocalizationHelper.GetString("ContrastProcessor_ClipLimit_Desc")));
|
||||
LocalizationHelper.GetString("ContrastProcessor_ClipLimit_Desc")) { IsAdvanced = true });
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ public class EmbossProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
0.5,
|
||||
0.0,
|
||||
1.0,
|
||||
LocalizationHelper.GetString("EmbossProcessor_BlendRatio_Desc")));
|
||||
LocalizationHelper.GetString("EmbossProcessor_BlendRatio_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("GrayOffset", new ProcessorParameter(
|
||||
"GrayOffset",
|
||||
@@ -73,7 +73,7 @@ public class EmbossProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
128,
|
||||
0,
|
||||
255,
|
||||
LocalizationHelper.GetString("EmbossProcessor_GrayOffset_Desc")));
|
||||
LocalizationHelper.GetString("EmbossProcessor_GrayOffset_Desc")) { IsAdvanced = true });
|
||||
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
+213
-213
@@ -1,214 +1,214 @@
|
||||
// ============================================================================
|
||||
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
||||
// 文件名: FilmEffectProcessor.cs
|
||||
// 描述: 电子胶片效果算子,模拟传统X射线胶片的显示效果
|
||||
// 功能:
|
||||
// - 窗宽窗位(Window/Level)调整
|
||||
// - 胶片反转(正片/负片)
|
||||
// - 多种胶片特性曲线(线性、S曲线、对数、指数)
|
||||
// - 边缘增强(模拟胶片锐化效果)
|
||||
// - 使用查找表(LUT)加速处理
|
||||
// 算法: 窗宽窗位映射 + 特性曲线变换
|
||||
// 作者: 李伟 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 位灰度图像)
|
||||
/// 16 位模式下窗宽窗位参数范围扩展到 0-65535,LUT 升级为 ushort[65536]
|
||||
/// </summary>
|
||||
public class FilmEffectProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
where TDepth : struct, IComparable
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<FilmEffectProcessor<TDepth>>();
|
||||
private byte[] _lut8 = new byte[256];
|
||||
private ushort[] _lut16 = new ushort[65536];
|
||||
|
||||
public FilmEffectProcessor()
|
||||
{
|
||||
Name = LocalizationHelper.GetString("FilmEffectProcessor_Name");
|
||||
Description = LocalizationHelper.GetString("FilmEffectProcessor_Description");
|
||||
}
|
||||
|
||||
protected override void InitializeParameters()
|
||||
{
|
||||
// 窗宽窗位默认值适配:8 位用 0-255,16 位用 0-65535
|
||||
// 运行时通过 MaxPixelValue 动态决定,参数范围设为最大(65535)
|
||||
Parameters.Add("WindowCenter", new ProcessorParameter(
|
||||
"WindowCenter",
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_WindowCenter"),
|
||||
typeof(int), 32768, 0, 65535,
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_WindowCenter_Desc")));
|
||||
|
||||
Parameters.Add("WindowWidth", new ProcessorParameter(
|
||||
"WindowWidth",
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_WindowWidth"),
|
||||
typeof(int), 65535, 1, 65535,
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_WindowWidth_Desc")));
|
||||
|
||||
Parameters.Add("Invert", new ProcessorParameter(
|
||||
"Invert",
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_Invert"),
|
||||
typeof(bool), false, null, null,
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_Invert_Desc")));
|
||||
|
||||
Parameters.Add("Curve", new ProcessorParameter(
|
||||
"Curve",
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_Curve"),
|
||||
typeof(string), "Linear", null, null,
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_Curve_Desc"),
|
||||
new string[] { "Linear", "Sigmoid", "Logarithmic", "Exponential" }));
|
||||
|
||||
Parameters.Add("CurveStrength", new ProcessorParameter(
|
||||
"CurveStrength",
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_CurveStrength"),
|
||||
typeof(double), 1.0, 0.1, 5.0,
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_CurveStrength_Desc")));
|
||||
|
||||
Parameters.Add("EdgeEnhance", new ProcessorParameter(
|
||||
"EdgeEnhance",
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_EdgeEnhance"),
|
||||
typeof(double), 0.0, 0.0, 3.0,
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_EdgeEnhance_Desc")));
|
||||
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
public override Image<Gray, TDepth> Process(Image<Gray, TDepth> inputImage)
|
||||
{
|
||||
int windowCenter = GetParameter<int>("WindowCenter");
|
||||
int windowWidth = GetParameter<int>("WindowWidth");
|
||||
bool invert = GetParameter<bool>("Invert");
|
||||
string curve = GetParameter<string>("Curve");
|
||||
double curveStrength = GetParameter<double>("CurveStrength");
|
||||
double edgeEnhance = GetParameter<double>("EdgeEnhance");
|
||||
|
||||
bool is16 = typeof(TDepth) == typeof(ushort);
|
||||
|
||||
if (is16)
|
||||
{
|
||||
BuildLUT16(windowCenter, windowWidth, invert, curve, curveStrength);
|
||||
var img16 = new Image<Gray, ushort>(inputImage.Width, inputImage.Height);
|
||||
var ushortData = inputImage.Data as ushort[,,];
|
||||
int h = inputImage.Height, w = inputImage.Width;
|
||||
Parallel.For(0, h, y =>
|
||||
{
|
||||
for (int x = 0; x < w; x++)
|
||||
img16.Data[y, x, 0] = _lut16[ushortData![y, x, 0]];
|
||||
});
|
||||
|
||||
if (edgeEnhance > 0.01)
|
||||
{
|
||||
using var blurred = new Image<Gray, ushort>(w, h);
|
||||
CvInvoke.GaussianBlur(inputImage, blurred, new System.Drawing.Size(3, 3), 0);
|
||||
Parallel.For(0, h, y =>
|
||||
{
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
int diff = ushortData![y, x, 0] - blurred.Data[y, x, 0];
|
||||
int enhanced = img16.Data[y, x, 0] + (int)(diff * edgeEnhance);
|
||||
img16.Data[y, x, 0] = (ushort)Math.Clamp(enhanced, 0, 65535);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_logger.Debug("Process(16bit): WC={WC}, WW={WW}", windowCenter, windowWidth);
|
||||
return (img16 as Image<Gray, TDepth>)!;
|
||||
}
|
||||
else
|
||||
{
|
||||
BuildLUT8(
|
||||
Math.Clamp(windowCenter, 0, 255),
|
||||
Math.Clamp(windowWidth, 1, 255),
|
||||
invert, curve, curveStrength);
|
||||
|
||||
var byteInput = inputImage as Image<Gray, byte>;
|
||||
var resultImg = byteInput!.Clone();
|
||||
int h = inputImage.Height, w = inputImage.Width;
|
||||
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++)
|
||||
resultImg.Data[y, x, 0] = _lut8[resultImg.Data[y, x, 0]];
|
||||
|
||||
if (edgeEnhance > 0.01)
|
||||
{
|
||||
using var blurred = byteInput.SmoothGaussian(3);
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
float diff = byteInput.Data[y, x, 0] - blurred.Data[y, x, 0];
|
||||
int enhanced = resultImg.Data[y, x, 0] + (int)(diff * edgeEnhance);
|
||||
resultImg.Data[y, x, 0] = (byte)Math.Clamp(enhanced, 0, 255);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("Process(8bit): WC={WC}, WW={WW}", windowCenter, windowWidth);
|
||||
return (resultImg as Image<Gray, TDepth>)!;
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildLUT8(int wc, int ww, bool invert, string curve, double strength)
|
||||
{
|
||||
double halfW = ww / 2.0;
|
||||
double low = wc - halfW, high = wc + halfW;
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
double normalized = ww <= 1 ? (i >= wc ? 1.0 : 0.0)
|
||||
: Math.Clamp((i - low) / (high - low), 0.0, 1.0);
|
||||
double mapped = ApplyCurve(normalized, curve, strength);
|
||||
if (invert) mapped = 1.0 - mapped;
|
||||
_lut8[i] = (byte)Math.Clamp((int)(mapped * 255.0), 0, 255);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildLUT16(int wc, int ww, bool invert, string curve, double strength)
|
||||
{
|
||||
double halfW = ww / 2.0;
|
||||
double low = wc - halfW, high = wc + halfW;
|
||||
for (int i = 0; i < 65536; i++)
|
||||
{
|
||||
double normalized = ww <= 1 ? (i >= wc ? 1.0 : 0.0)
|
||||
: Math.Clamp((i - low) / (high - low), 0.0, 1.0);
|
||||
double mapped = ApplyCurve(normalized, curve, strength);
|
||||
if (invert) mapped = 1.0 - mapped;
|
||||
_lut16[i] = (ushort)Math.Clamp((int)(mapped * 65535.0), 0, 65535);
|
||||
}
|
||||
}
|
||||
|
||||
private static double ApplyCurve(double x, string curve, double strength)
|
||||
=> curve switch
|
||||
{
|
||||
"Sigmoid" => ApplySigmoid(x, strength),
|
||||
"Logarithmic" => ApplyLogarithmic(x, strength),
|
||||
"Exponential" => ApplyExponential(x, strength),
|
||||
_ => x
|
||||
};
|
||||
|
||||
/// <summary>S曲线(Sigmoid):增强中间调对比度</summary>
|
||||
private static double ApplySigmoid(double x, double strength)
|
||||
{
|
||||
double k = strength * 10.0;
|
||||
return 1.0 / (1.0 + Math.Exp(-k * (x - 0.5)));
|
||||
}
|
||||
|
||||
/// <summary>对数曲线:提亮暗部,压缩亮部</summary>
|
||||
private static double ApplyLogarithmic(double x, double strength)
|
||||
{
|
||||
double c = strength;
|
||||
return Math.Log(1.0 + c * x) / Math.Log(1.0 + c);
|
||||
}
|
||||
|
||||
/// <summary>指数曲线:压缩暗部,增强亮部</summary>
|
||||
private static double ApplyExponential(double x, double strength)
|
||||
{
|
||||
double c = strength;
|
||||
return (Math.Exp(c * x) - 1.0) / (Math.Exp(c) - 1.0);
|
||||
}
|
||||
// ============================================================================
|
||||
// Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved.
|
||||
// 文件名: FilmEffectProcessor.cs
|
||||
// 描述: 电子胶片效果算子,模拟传统X射线胶片的显示效果
|
||||
// 功能:
|
||||
// - 窗宽窗位(Window/Level)调整
|
||||
// - 胶片反转(正片/负片)
|
||||
// - 多种胶片特性曲线(线性、S曲线、对数、指数)
|
||||
// - 边缘增强(模拟胶片锐化效果)
|
||||
// - 使用查找表(LUT)加速处理
|
||||
// 算法: 窗宽窗位映射 + 特性曲线变换
|
||||
// 作者: 李伟 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 位灰度图像)
|
||||
/// 16 位模式下窗宽窗位参数范围扩展到 0-65535,LUT 升级为 ushort[65536]
|
||||
/// </summary>
|
||||
public class FilmEffectProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
where TDepth : struct, IComparable
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<FilmEffectProcessor<TDepth>>();
|
||||
private byte[] _lut8 = new byte[256];
|
||||
private ushort[] _lut16 = new ushort[65536];
|
||||
|
||||
public FilmEffectProcessor()
|
||||
{
|
||||
Name = LocalizationHelper.GetString("FilmEffectProcessor_Name");
|
||||
Description = LocalizationHelper.GetString("FilmEffectProcessor_Description");
|
||||
}
|
||||
|
||||
protected override void InitializeParameters()
|
||||
{
|
||||
// 窗宽窗位默认值适配:8 位用 0-255,16 位用 0-65535
|
||||
// 运行时通过 MaxPixelValue 动态决定,参数范围设为最大(65535)
|
||||
Parameters.Add("WindowCenter", new ProcessorParameter(
|
||||
"WindowCenter",
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_WindowCenter"),
|
||||
typeof(int), 32768, 0, 65535,
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_WindowCenter_Desc")));
|
||||
|
||||
Parameters.Add("WindowWidth", new ProcessorParameter(
|
||||
"WindowWidth",
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_WindowWidth"),
|
||||
typeof(int), 65535, 1, 65535,
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_WindowWidth_Desc")));
|
||||
|
||||
Parameters.Add("Invert", new ProcessorParameter(
|
||||
"Invert",
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_Invert"),
|
||||
typeof(bool), false, null, null,
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_Invert_Desc")));
|
||||
|
||||
Parameters.Add("Curve", new ProcessorParameter(
|
||||
"Curve",
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_Curve"),
|
||||
typeof(string), "Linear", null, null,
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_Curve_Desc"),
|
||||
new string[] { "Linear", "Sigmoid", "Logarithmic", "Exponential" }));
|
||||
|
||||
Parameters.Add("CurveStrength", new ProcessorParameter(
|
||||
"CurveStrength",
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_CurveStrength"),
|
||||
typeof(double), 1.0, 0.1, 5.0,
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_CurveStrength_Desc")));
|
||||
|
||||
Parameters.Add("EdgeEnhance", new ProcessorParameter(
|
||||
"EdgeEnhance",
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_EdgeEnhance"),
|
||||
typeof(double), 0.0, 0.0, 3.0,
|
||||
LocalizationHelper.GetString("FilmEffectProcessor_EdgeEnhance_Desc")));
|
||||
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
public override Image<Gray, TDepth> Process(Image<Gray, TDepth> inputImage)
|
||||
{
|
||||
int windowCenter = GetParameter<int>("WindowCenter");
|
||||
int windowWidth = GetParameter<int>("WindowWidth");
|
||||
bool invert = GetParameter<bool>("Invert");
|
||||
string curve = GetParameter<string>("Curve");
|
||||
double curveStrength = GetParameter<double>("CurveStrength");
|
||||
double edgeEnhance = GetParameter<double>("EdgeEnhance");
|
||||
|
||||
bool is16 = typeof(TDepth) == typeof(ushort);
|
||||
|
||||
if (is16)
|
||||
{
|
||||
BuildLUT16(windowCenter, windowWidth, invert, curve, curveStrength);
|
||||
var img16 = new Image<Gray, ushort>(inputImage.Width, inputImage.Height);
|
||||
var ushortData = inputImage.Data as ushort[,,];
|
||||
int h = inputImage.Height, w = inputImage.Width;
|
||||
Parallel.For(0, h, y =>
|
||||
{
|
||||
for (int x = 0; x < w; x++)
|
||||
img16.Data[y, x, 0] = _lut16[ushortData![y, x, 0]];
|
||||
});
|
||||
|
||||
if (edgeEnhance > 0.01)
|
||||
{
|
||||
using var blurred = new Image<Gray, ushort>(w, h);
|
||||
CvInvoke.GaussianBlur(inputImage, blurred, new System.Drawing.Size(3, 3), 0);
|
||||
Parallel.For(0, h, y =>
|
||||
{
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
int diff = ushortData![y, x, 0] - blurred.Data[y, x, 0];
|
||||
int enhanced = img16.Data[y, x, 0] + (int)(diff * edgeEnhance);
|
||||
img16.Data[y, x, 0] = (ushort)Math.Clamp(enhanced, 0, 65535);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_logger.Debug("Process(16bit): WC={WC}, WW={WW}", windowCenter, windowWidth);
|
||||
return (img16 as Image<Gray, TDepth>)!;
|
||||
}
|
||||
else
|
||||
{
|
||||
BuildLUT8(
|
||||
Math.Clamp(windowCenter, 0, 255),
|
||||
Math.Clamp(windowWidth, 1, 255),
|
||||
invert, curve, curveStrength);
|
||||
|
||||
var byteInput = inputImage as Image<Gray, byte>;
|
||||
var resultImg = byteInput!.Clone();
|
||||
int h = inputImage.Height, w = inputImage.Width;
|
||||
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++)
|
||||
resultImg.Data[y, x, 0] = _lut8[resultImg.Data[y, x, 0]];
|
||||
|
||||
if (edgeEnhance > 0.01)
|
||||
{
|
||||
using var blurred = byteInput.SmoothGaussian(3);
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
float diff = byteInput.Data[y, x, 0] - blurred.Data[y, x, 0];
|
||||
int enhanced = resultImg.Data[y, x, 0] + (int)(diff * edgeEnhance);
|
||||
resultImg.Data[y, x, 0] = (byte)Math.Clamp(enhanced, 0, 255);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("Process(8bit): WC={WC}, WW={WW}", windowCenter, windowWidth);
|
||||
return (resultImg as Image<Gray, TDepth>)!;
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildLUT8(int wc, int ww, bool invert, string curve, double strength)
|
||||
{
|
||||
double halfW = ww / 2.0;
|
||||
double low = wc - halfW, high = wc + halfW;
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
double normalized = ww <= 1 ? (i >= wc ? 1.0 : 0.0)
|
||||
: Math.Clamp((i - low) / (high - low), 0.0, 1.0);
|
||||
double mapped = ApplyCurve(normalized, curve, strength);
|
||||
if (invert) mapped = 1.0 - mapped;
|
||||
_lut8[i] = (byte)Math.Clamp((int)(mapped * 255.0), 0, 255);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildLUT16(int wc, int ww, bool invert, string curve, double strength)
|
||||
{
|
||||
double halfW = ww / 2.0;
|
||||
double low = wc - halfW, high = wc + halfW;
|
||||
for (int i = 0; i < 65536; i++)
|
||||
{
|
||||
double normalized = ww <= 1 ? (i >= wc ? 1.0 : 0.0)
|
||||
: Math.Clamp((i - low) / (high - low), 0.0, 1.0);
|
||||
double mapped = ApplyCurve(normalized, curve, strength);
|
||||
if (invert) mapped = 1.0 - mapped;
|
||||
_lut16[i] = (ushort)Math.Clamp((int)(mapped * 65535.0), 0, 65535);
|
||||
}
|
||||
}
|
||||
|
||||
private static double ApplyCurve(double x, string curve, double strength)
|
||||
=> curve switch
|
||||
{
|
||||
"Sigmoid" => ApplySigmoid(x, strength),
|
||||
"Logarithmic" => ApplyLogarithmic(x, strength),
|
||||
"Exponential" => ApplyExponential(x, strength),
|
||||
_ => x
|
||||
};
|
||||
|
||||
/// <summary>S曲线(Sigmoid):增强中间调对比度</summary>
|
||||
private static double ApplySigmoid(double x, double strength)
|
||||
{
|
||||
double k = strength * 10.0;
|
||||
return 1.0 / (1.0 + Math.Exp(-k * (x - 0.5)));
|
||||
}
|
||||
|
||||
/// <summary>对数曲线:提亮暗部,压缩亮部</summary>
|
||||
private static double ApplyLogarithmic(double x, double strength)
|
||||
{
|
||||
double c = strength;
|
||||
return Math.Log(1.0 + c * x) / Math.Log(1.0 + c);
|
||||
}
|
||||
|
||||
/// <summary>指数曲线:压缩暗部,增强亮部</summary>
|
||||
private static double ApplyExponential(double x, double strength)
|
||||
{
|
||||
double c = strength;
|
||||
return (Math.Exp(c * x) - 1.0) / (Math.Exp(c) - 1.0);
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,6 @@ namespace XP.ImageProcessing.Processors;
|
||||
public class GammaProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
where TDepth : struct, IComparable
|
||||
{
|
||||
private byte[] _lookupTable8 = new byte[256];
|
||||
private ushort[] _lookupTable16 = new ushort[65536];
|
||||
private static readonly ILogger _logger = Log.ForContext<GammaProcessor<TDepth>>();
|
||||
|
||||
public GammaProcessor()
|
||||
@@ -53,7 +51,7 @@ public class GammaProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
1.0,
|
||||
0.1,
|
||||
3.0,
|
||||
LocalizationHelper.GetString("GammaProcessor_Gain_Desc")));
|
||||
LocalizationHelper.GetString("GammaProcessor_Gain_Desc")) { IsAdvanced = true });
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
@@ -64,49 +62,53 @@ public class GammaProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
|
||||
if (typeof(TDepth) == typeof(ushort))
|
||||
{
|
||||
BuildLUT16(gamma, gain);
|
||||
var lut16 = BuildLUT16(gamma, gain);
|
||||
var img16 = inputImage as Image<Gray, ushort>;
|
||||
var result = new Image<Gray, ushort>(inputImage.Width, inputImage.Height);
|
||||
Parallel.For(0, inputImage.Height, y =>
|
||||
{
|
||||
for (int x = 0; x < inputImage.Width; x++)
|
||||
result.Data[y, x, 0] = _lookupTable16[img16!.Data[y, x, 0]];
|
||||
result.Data[y, x, 0] = lut16[img16!.Data[y, x, 0]];
|
||||
});
|
||||
_logger.Debug("Process(16bit): Gamma={G}, Gain={Gain}", gamma, gain);
|
||||
return (result as Image<Gray, TDepth>)!;
|
||||
}
|
||||
else
|
||||
{
|
||||
BuildLUT8(gamma, gain);
|
||||
var lut8 = BuildLUT8(gamma, gain);
|
||||
var result = (inputImage as Image<Gray, byte>)!.Clone();
|
||||
int h = inputImage.Height, w = inputImage.Width;
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++)
|
||||
result.Data[y, x, 0] = _lookupTable8[result.Data[y, x, 0]];
|
||||
result.Data[y, x, 0] = lut8[result.Data[y, x, 0]];
|
||||
_logger.Debug("Process(8bit): Gamma={G}, Gain={Gain}", gamma, gain);
|
||||
return (result as Image<Gray, TDepth>)!;
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildLUT8(double gamma, double gain)
|
||||
private static byte[] BuildLUT8(double gamma, double gain)
|
||||
{
|
||||
var lut = new byte[256];
|
||||
double invGamma = 1.0 / gamma;
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
double normalized = i / 255.0;
|
||||
double corrected = Math.Pow(normalized, invGamma) * gain;
|
||||
_lookupTable8[i] = (byte)Math.Clamp((int)(corrected * 255.0), 0, 255);
|
||||
lut[i] = (byte)Math.Clamp((int)(corrected * 255.0), 0, 255);
|
||||
}
|
||||
return lut;
|
||||
}
|
||||
|
||||
private void BuildLUT16(double gamma, double gain)
|
||||
private static ushort[] BuildLUT16(double gamma, double gain)
|
||||
{
|
||||
var lut = new ushort[65536];
|
||||
double invGamma = 1.0 / gamma;
|
||||
for (int i = 0; i < 65536; i++)
|
||||
{
|
||||
double normalized = i / 65535.0;
|
||||
double corrected = Math.Pow(normalized, invGamma) * gain;
|
||||
_lookupTable16[i] = (ushort)Math.Clamp((int)(corrected * 65535.0), 0, 65535);
|
||||
lut[i] = (ushort)Math.Clamp((int)(corrected * 65535.0), 0, 65535);
|
||||
}
|
||||
return lut;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ public class HDREnhancementProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("HDREnhancementProcessor_Method_Desc"),
|
||||
new string[] { "LocalToneMap", "AdaptiveLog", "Drago", "BilateralToneMap" }));
|
||||
new string[] { "LocalToneMap", "AdaptiveLog", "Drago", "BilateralToneMap" }) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("Gamma", new ProcessorParameter(
|
||||
"Gamma",
|
||||
@@ -63,7 +63,7 @@ public class HDREnhancementProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
1.0,
|
||||
0.0,
|
||||
3.0,
|
||||
LocalizationHelper.GetString("HDREnhancementProcessor_Saturation_Desc")));
|
||||
LocalizationHelper.GetString("HDREnhancementProcessor_Saturation_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("DetailBoost", new ProcessorParameter(
|
||||
"DetailBoost",
|
||||
@@ -81,7 +81,7 @@ public class HDREnhancementProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
20.0,
|
||||
1.0,
|
||||
100.0,
|
||||
LocalizationHelper.GetString("HDREnhancementProcessor_SigmaSpace_Desc")));
|
||||
LocalizationHelper.GetString("HDREnhancementProcessor_SigmaSpace_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("SigmaColor", new ProcessorParameter(
|
||||
"SigmaColor",
|
||||
@@ -90,7 +90,7 @@ public class HDREnhancementProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
30.0,
|
||||
1.0,
|
||||
100.0,
|
||||
LocalizationHelper.GetString("HDREnhancementProcessor_SigmaColor_Desc")));
|
||||
LocalizationHelper.GetString("HDREnhancementProcessor_SigmaColor_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("Bias", new ProcessorParameter(
|
||||
"Bias",
|
||||
@@ -99,7 +99,7 @@ public class HDREnhancementProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
0.85,
|
||||
0.0,
|
||||
1.0,
|
||||
LocalizationHelper.GetString("HDREnhancementProcessor_Bias_Desc")));
|
||||
LocalizationHelper.GetString("HDREnhancementProcessor_Bias_Desc")) { IsAdvanced = true });
|
||||
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public class HierarchicalEnhancementProcessor<TDepth> : ImageProcessorBase<TDept
|
||||
1.0,
|
||||
0.0,
|
||||
3.0,
|
||||
LocalizationHelper.GetString("HierarchicalEnhancementProcessor_BaseGain_Desc")));
|
||||
LocalizationHelper.GetString("HierarchicalEnhancementProcessor_BaseGain_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("ClipLimit", new ProcessorParameter(
|
||||
"ClipLimit",
|
||||
@@ -86,7 +86,7 @@ public class HierarchicalEnhancementProcessor<TDepth> : ImageProcessorBase<TDept
|
||||
0.0,
|
||||
0.0,
|
||||
50.0,
|
||||
LocalizationHelper.GetString("HierarchicalEnhancementProcessor_ClipLimit_Desc")));
|
||||
LocalizationHelper.GetString("HierarchicalEnhancementProcessor_ClipLimit_Desc")) { IsAdvanced = true });
|
||||
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
@@ -41,11 +41,11 @@ public class HistogramEqualizationProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
"Method",
|
||||
LocalizationHelper.GetString("HistogramEqualizationProcessor_Method"),
|
||||
typeof(string),
|
||||
"Global",
|
||||
"CLAHE",
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("HistogramEqualizationProcessor_Method_Desc"),
|
||||
new string[] { "Global", "CLAHE" }));
|
||||
new string[] { "Global", "CLAHE" }) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("ClipLimit", new ProcessorParameter(
|
||||
"ClipLimit",
|
||||
@@ -63,7 +63,7 @@ public class HistogramEqualizationProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
8,
|
||||
4,
|
||||
32,
|
||||
LocalizationHelper.GetString("HistogramEqualizationProcessor_TileSize_Desc")));
|
||||
LocalizationHelper.GetString("HistogramEqualizationProcessor_TileSize_Desc")) { IsAdvanced = true });
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ public class RetinexProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("RetinexProcessor_Method_Desc"),
|
||||
new string[] { "SSR", "MSR", "MSRCR" }));
|
||||
new string[] { "SSR", "MSR", "MSRCR" }) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("Sigma1", new ProcessorParameter(
|
||||
"Sigma1",
|
||||
@@ -53,7 +53,7 @@ public class RetinexProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
15.0,
|
||||
1.0,
|
||||
100.0,
|
||||
LocalizationHelper.GetString("RetinexProcessor_Sigma1_Desc")));
|
||||
LocalizationHelper.GetString("RetinexProcessor_Sigma1_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("Sigma2", new ProcessorParameter(
|
||||
"Sigma2",
|
||||
@@ -62,7 +62,7 @@ public class RetinexProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
80.0,
|
||||
1.0,
|
||||
200.0,
|
||||
LocalizationHelper.GetString("RetinexProcessor_Sigma2_Desc")));
|
||||
LocalizationHelper.GetString("RetinexProcessor_Sigma2_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("Sigma3", new ProcessorParameter(
|
||||
"Sigma3",
|
||||
@@ -71,7 +71,7 @@ public class RetinexProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
250.0,
|
||||
1.0,
|
||||
500.0,
|
||||
LocalizationHelper.GetString("RetinexProcessor_Sigma3_Desc")));
|
||||
LocalizationHelper.GetString("RetinexProcessor_Sigma3_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("Gain", new ProcessorParameter(
|
||||
"Gain",
|
||||
@@ -89,7 +89,7 @@ public class RetinexProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
0,
|
||||
-100,
|
||||
100,
|
||||
LocalizationHelper.GetString("RetinexProcessor_Offset_Desc")));
|
||||
LocalizationHelper.GetString("RetinexProcessor_Offset_Desc")) { IsAdvanced = true });
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
|
||||
@@ -39,11 +39,11 @@ public class SharpenProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
"Method",
|
||||
LocalizationHelper.GetString("SharpenProcessor_Method"),
|
||||
typeof(string),
|
||||
"Laplacian",
|
||||
"UnsharpMask",
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("SharpenProcessor_Method_Desc"),
|
||||
new string[] { "Laplacian", "UnsharpMask" }));
|
||||
new string[] { "Laplacian", "UnsharpMask" }) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("Strength", new ProcessorParameter(
|
||||
"Strength",
|
||||
@@ -61,7 +61,7 @@ public class SharpenProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
3,
|
||||
1,
|
||||
15,
|
||||
LocalizationHelper.GetString("SharpenProcessor_KernelSize_Desc")));
|
||||
LocalizationHelper.GetString("SharpenProcessor_KernelSize_Desc")) { IsAdvanced = true });
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,8 @@ public class SuperResolutionProcessor : ImageProcessorBase<byte>
|
||||
int w = inputImage.Width;
|
||||
|
||||
// 获取模型输入信息
|
||||
string inputName = session.InputMetadata.Keys.First();
|
||||
string inputName = session.InputMetadata.Keys.FirstOrDefault()
|
||||
?? throw new InvalidOperationException("ONNX model has no input metadata");
|
||||
var inputMeta = session.InputMetadata[inputName];
|
||||
int[] dims = inputMeta.Dimensions;
|
||||
// dims 格式: [1, H, W, C] (NHWC),C 可能是 1 或 3
|
||||
|
||||
@@ -62,7 +62,7 @@ public class BandPassFilterProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("BandPassFilterProcessor_FilterType_Desc"),
|
||||
new string[] { "Ideal", "Butterworth", "Gaussian" }));
|
||||
new string[] { "Ideal", "Butterworth", "Gaussian" }) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("Order", new ProcessorParameter(
|
||||
"Order",
|
||||
@@ -71,7 +71,7 @@ public class BandPassFilterProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
2,
|
||||
1,
|
||||
10,
|
||||
LocalizationHelper.GetString("BandPassFilterProcessor_Order_Desc")));
|
||||
LocalizationHelper.GetString("BandPassFilterProcessor_Order_Desc")) { IsAdvanced = true });
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ public class BandPassFilterProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
if (highCutoff <= lowCutoff) highCutoff = lowCutoff + 10;
|
||||
|
||||
var floatImage = inputImage.Convert<Gray, float>();
|
||||
var imaginaryImage = new Image<Gray, float>(floatImage.Size);
|
||||
using var imaginaryImage = new Image<Gray, float>(floatImage.Size);
|
||||
imaginaryImage.SetZero();
|
||||
|
||||
using (var planes = new Emgu.CV.Util.VectorOfMat())
|
||||
@@ -139,6 +139,8 @@ public class BandPassFilterProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
result = (result - minVal) * (255.0 / (maxVal - minVal));
|
||||
}
|
||||
|
||||
floatImage.Dispose();
|
||||
mask.Dispose();
|
||||
complexMat.Dispose();
|
||||
dftMat.Dispose();
|
||||
filteredDft.Dispose();
|
||||
|
||||
@@ -58,7 +58,7 @@ public class BilateralFilterProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
75.0,
|
||||
1.0,
|
||||
200.0,
|
||||
LocalizationHelper.GetString("BilateralFilterProcessor_SigmaSpace_Desc")));
|
||||
LocalizationHelper.GetString("BilateralFilterProcessor_SigmaSpace_Desc")) { IsAdvanced = true });
|
||||
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
@@ -69,14 +69,32 @@ public class BilateralFilterProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
double sigmaColor = GetParameter<double>("SigmaColor");
|
||||
double sigmaSpace = GetParameter<double>("SigmaSpace");
|
||||
|
||||
// 16 位时 sigmaColor 按比例放大(原始参数按 8 位语义定义)
|
||||
double effectiveSigmaColor = sigmaColor;
|
||||
if (typeof(TDepth) == typeof(ushort))
|
||||
effectiveSigmaColor = sigmaColor * 256.0;
|
||||
Image<Gray, TDepth> result;
|
||||
|
||||
if (typeof(TDepth) == typeof(ushort))
|
||||
{
|
||||
// OpenCV BilateralFilter only supports 8U and 32F.
|
||||
// For 16-bit images: convert to 32F, apply filter, then convert back.
|
||||
double effectiveSigmaColor = sigmaColor * 256.0;
|
||||
|
||||
using var floatInput = inputImage.Convert<Gray, float>();
|
||||
using var floatResult = floatInput.CopyBlank();
|
||||
CvInvoke.BilateralFilter(floatInput, floatResult, diameter, effectiveSigmaColor, sigmaSpace);
|
||||
result = floatResult.Convert<Gray, TDepth>();
|
||||
|
||||
_logger.Debug("Process (16-bit via 32F): Diameter={D}, SigmaColor={SC}, SigmaSpace={SS}",
|
||||
diameter, effectiveSigmaColor, sigmaSpace);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 8-bit (byte) path: directly supported by OpenCV
|
||||
result = inputImage.CopyBlank();
|
||||
CvInvoke.BilateralFilter(inputImage, result, diameter, sigmaColor, sigmaSpace);
|
||||
|
||||
_logger.Debug("Process (8-bit): Diameter={D}, SigmaColor={SC}, SigmaSpace={SS}",
|
||||
diameter, sigmaColor, sigmaSpace);
|
||||
}
|
||||
|
||||
var result = inputImage.Clone();
|
||||
CvInvoke.BilateralFilter(inputImage, result, diameter, effectiveSigmaColor, sigmaSpace);
|
||||
_logger.Debug("Process: Diameter={D}, SigmaColor={SC}, SigmaSpace={SS}", diameter, effectiveSigmaColor, sigmaSpace);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public class GaussianBlurProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
1.5,
|
||||
0.1,
|
||||
10.0,
|
||||
LocalizationHelper.GetString("GaussianBlurProcessor_Sigma_Desc")));
|
||||
LocalizationHelper.GetString("GaussianBlurProcessor_Sigma_Desc")) { IsAdvanced = true });
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ public class HighPassFilterProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
/// </summary>
|
||||
private Mat CreateHighPassFilter(int rows, int cols, double d0)
|
||||
{
|
||||
var filter = new Image<Gray, float>(cols, rows);
|
||||
using var filter = new Image<Gray, float>(cols, rows);
|
||||
|
||||
int centerX = cols / 2;
|
||||
int centerY = rows / 2;
|
||||
@@ -141,6 +141,6 @@ public class HighPassFilterProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
}
|
||||
}
|
||||
|
||||
return filter.Mat;
|
||||
return filter.Mat.Clone();
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ public class LowPassFilterProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
/// </summary>
|
||||
private Mat CreateLowPassFilter(int rows, int cols, double d0)
|
||||
{
|
||||
var filter = new Image<Gray, float>(cols, rows);
|
||||
using var filter = new Image<Gray, float>(cols, rows);
|
||||
|
||||
int centerX = cols / 2;
|
||||
int centerY = rows / 2;
|
||||
@@ -137,6 +137,6 @@ public class LowPassFilterProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
}
|
||||
}
|
||||
|
||||
return filter.Mat;
|
||||
return filter.Mat.Clone();
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,9 @@ public class MedianFilterProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
"KernelSize",
|
||||
LocalizationHelper.GetString("MedianFilterProcessor_KernelSize"),
|
||||
typeof(int),
|
||||
5,
|
||||
3,
|
||||
1,
|
||||
31,
|
||||
5,
|
||||
LocalizationHelper.GetString("MedianFilterProcessor_KernelSize_Desc")));
|
||||
|
||||
_logger.Debug("InitializeParameters");
|
||||
@@ -51,10 +51,20 @@ public class MedianFilterProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
int kernelSize = GetParameter<int>("KernelSize");
|
||||
if (kernelSize % 2 == 0) kernelSize++;
|
||||
|
||||
var result = inputImage.Clone();
|
||||
CvInvoke.MedianBlur(inputImage, result, kernelSize);
|
||||
// OpenCV MedianBlur: CV_16U not supported. Convert to 32F for 16-bit images.
|
||||
if (typeof(TDepth) == typeof(ushort))
|
||||
{
|
||||
using var floatInput = inputImage.Convert<Gray, float>();
|
||||
using var floatResult = floatInput.CopyBlank();
|
||||
CvInvoke.MedianBlur(floatInput, floatResult, kernelSize);
|
||||
_logger.Debug("Process (16-bit via 32F): KernelSize = {KernelSize}", kernelSize);
|
||||
return floatResult.Convert<Gray, TDepth>();
|
||||
}
|
||||
|
||||
var output = inputImage.CopyBlank();
|
||||
CvInvoke.MedianBlur(inputImage, output, kernelSize);
|
||||
|
||||
_logger.Debug("Process: KernelSize = {KernelSize}", kernelSize);
|
||||
return result;
|
||||
return output;
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public class RemoveOutliersProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
typeof(int),
|
||||
5,
|
||||
1,
|
||||
31,
|
||||
5,
|
||||
LocalizationHelper.GetString("RemoveOutliersProcessor_KernelSize_Desc")));
|
||||
|
||||
Parameters.Add("Threshold", new ProcessorParameter(
|
||||
@@ -88,48 +88,66 @@ public class RemoveOutliersProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
int height = inputImage.Height;
|
||||
|
||||
// 计算邻域中值图
|
||||
var medianImage = inputImage.Clone();
|
||||
CvInvoke.MedianBlur(inputImage, medianImage, kernelSize);
|
||||
// OpenCV MedianBlur: CV_16U not supported. Convert to 32F for 16-bit images.
|
||||
Image<Gray, TDepth> medianImage;
|
||||
if (typeof(TDepth) == typeof(ushort))
|
||||
{
|
||||
using var floatInput = inputImage.Convert<Gray, float>();
|
||||
using var floatMedian = floatInput.CopyBlank();
|
||||
CvInvoke.MedianBlur(floatInput, floatMedian, kernelSize);
|
||||
medianImage = floatMedian.Convert<Gray, TDepth>();
|
||||
}
|
||||
else
|
||||
{
|
||||
medianImage = inputImage.CopyBlank();
|
||||
CvInvoke.MedianBlur(inputImage, medianImage, kernelSize);
|
||||
}
|
||||
|
||||
// 逐像素比较并替换离群点
|
||||
var result = inputImage.Clone();
|
||||
|
||||
for (int y = 0; y < height; y++)
|
||||
try
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
double original = Convert.ToDouble(inputImage.Data[y, x, 0]);
|
||||
double median = Convert.ToDouble(medianImage.Data[y, x, 0]);
|
||||
double diff = original - median;
|
||||
|
||||
bool isOutlier = false;
|
||||
|
||||
switch (outlierType)
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
case "Bright":
|
||||
isOutlier = diff > threshold; // 亮离群点:比邻域中值亮太多
|
||||
break;
|
||||
case "Dark":
|
||||
isOutlier = -diff > threshold; // 暗离群点:比邻域中值暗太多
|
||||
break;
|
||||
case "Both":
|
||||
default:
|
||||
isOutlier = System.Math.Abs(diff) > threshold;
|
||||
break;
|
||||
}
|
||||
double original = Convert.ToDouble(inputImage.Data[y, x, 0]);
|
||||
double median = Convert.ToDouble(medianImage.Data[y, x, 0]);
|
||||
double diff = original - median;
|
||||
|
||||
if (isOutlier)
|
||||
{
|
||||
result.Data[y, x, 0] = medianImage.Data[y, x, 0];
|
||||
bool isOutlier = false;
|
||||
|
||||
switch (outlierType)
|
||||
{
|
||||
case "Bright":
|
||||
isOutlier = diff > threshold; // 亮离群点:比邻域中值亮太多
|
||||
break;
|
||||
case "Dark":
|
||||
isOutlier = -diff > threshold; // 暗离群点:比邻域中值暗太多
|
||||
break;
|
||||
case "Both":
|
||||
default:
|
||||
isOutlier = System.Math.Abs(diff) > threshold;
|
||||
break;
|
||||
}
|
||||
|
||||
if (isOutlier)
|
||||
{
|
||||
result.Data[y, x, 0] = medianImage.Data[y, x, 0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
medianImage.Dispose();
|
||||
finally
|
||||
{
|
||||
medianImage.Dispose();
|
||||
}
|
||||
|
||||
_logger.Debug("Process: KernelSize={K}, Threshold={T}, Type={Type}",
|
||||
kernelSize, threshold, outlierType);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ public class ShockFilterProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
0.25,
|
||||
0.1,
|
||||
1.0,
|
||||
LocalizationHelper.GetString("ShockFilterProcessor_Dt_Desc")));
|
||||
LocalizationHelper.GetString("ShockFilterProcessor_Dt_Desc")) { IsAdvanced = true });
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ public class HorizontalEdgeProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("HorizontalEdgeProcessor_Method_Desc"),
|
||||
new string[] { "Sobel", "Prewitt", "Simple" }));
|
||||
new string[] { "Sobel", "Prewitt", "Simple" }) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("Sensitivity", new ProcessorParameter(
|
||||
"Sensitivity",
|
||||
@@ -52,7 +52,7 @@ public class HorizontalEdgeProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
1.0,
|
||||
0.1,
|
||||
5.0,
|
||||
LocalizationHelper.GetString("HorizontalEdgeProcessor_Sensitivity_Desc")));
|
||||
LocalizationHelper.GetString("HorizontalEdgeProcessor_Sensitivity_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("Threshold", new ProcessorParameter(
|
||||
"Threshold",
|
||||
|
||||
@@ -71,7 +71,7 @@ public class KirschEdgeProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
1.0,
|
||||
0.1,
|
||||
5.0,
|
||||
LocalizationHelper.GetString("KirschEdgeProcessor_Scale_Desc")));
|
||||
LocalizationHelper.GetString("KirschEdgeProcessor_Scale_Desc")) { IsAdvanced = true });
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ public class SobelEdgeProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
null,
|
||||
null,
|
||||
LocalizationHelper.GetString("SobelEdgeProcessor_Direction_Desc"),
|
||||
new string[] { "Both", "Horizontal", "Vertical" }));
|
||||
new string[] { "Both", "Horizontal", "Vertical" }) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("KernelSize", new ProcessorParameter(
|
||||
"KernelSize",
|
||||
@@ -52,7 +52,7 @@ public class SobelEdgeProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
3,
|
||||
1,
|
||||
7,
|
||||
LocalizationHelper.GetString("SobelEdgeProcessor_KernelSize_Desc")));
|
||||
LocalizationHelper.GetString("SobelEdgeProcessor_KernelSize_Desc")) { IsAdvanced = true });
|
||||
|
||||
Parameters.Add("Scale", new ProcessorParameter(
|
||||
"Scale",
|
||||
@@ -61,7 +61,7 @@ public class SobelEdgeProcessor<TDepth> : ImageProcessorBase<TDepth>
|
||||
1.0,
|
||||
0.1,
|
||||
5.0,
|
||||
LocalizationHelper.GetString("SobelEdgeProcessor_Scale_Desc")));
|
||||
LocalizationHelper.GetString("SobelEdgeProcessor_Scale_Desc")) { IsAdvanced = true });
|
||||
_logger.Debug("InitializeParameters");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"nova-hexagon-s-digital-design-system": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.zeroheight.com/mcp/47b75aea1487aad02a5a1ad939378e390931e2a9"
|
||||
}
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"liveServer.settings.port": 5501
|
||||
}
|
||||
@@ -84,6 +84,9 @@ namespace XplorePlane.ViewModels.ImageProcessing
|
||||
public bool HasOutputControls =>
|
||||
OutputFieldOperatorKeys.Contains(OperatorKey) && Parameters.Any(p => p.IsOutputControl);
|
||||
|
||||
/// <summary>是否存在高级参数(用于 UI 控制 Expander 显隐)</summary>
|
||||
public bool HasAdvancedParameters => Parameters.Any(p => p.IsAdvanced && p.IsVisible && !p.IsOutputControl);
|
||||
|
||||
public bool IsExecutionEndNode
|
||||
{
|
||||
get => _isExecutionEndNode;
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using XP.ImageProcessing.Core;
|
||||
using XP.ImageProcessing.Processors;
|
||||
using System.Linq;
|
||||
using XP.ImageProcessing.Core;
|
||||
using XP.ImageProcessing.Processors;
|
||||
|
||||
namespace XplorePlane.ViewModels.ImageProcessing
|
||||
{
|
||||
@@ -23,11 +23,12 @@ namespace XplorePlane.ViewModels.ImageProcessing
|
||||
_value = parameter.Value;
|
||||
MinValue = parameter.MinValue;
|
||||
MaxValue = parameter.MaxValue;
|
||||
Options = parameter.Options;
|
||||
LocalizedOptions = parameter.Options?
|
||||
.Select(option => LocalizeOption(option))
|
||||
.ToArray();
|
||||
Options = parameter.Options;
|
||||
LocalizedOptions = parameter.Options?
|
||||
.Select(option => LocalizeOption(option))
|
||||
.ToArray();
|
||||
IsVisible = parameter.IsVisible;
|
||||
IsAdvanced = parameter.IsAdvanced;
|
||||
ParameterType = parameter.ValueType?.Name?.ToLowerInvariant() switch
|
||||
{
|
||||
"int32" or "int" => "int",
|
||||
@@ -42,11 +43,12 @@ namespace XplorePlane.ViewModels.ImageProcessing
|
||||
public string DisplayName { get; }
|
||||
public object MinValue { get; }
|
||||
public object MaxValue { get; }
|
||||
public string[]? Options { get; }
|
||||
public string[]? LocalizedOptions { get; }
|
||||
public string[]? Options { get; }
|
||||
public string[]? LocalizedOptions { get; }
|
||||
public bool IsVisible { get; }
|
||||
public bool IsAdvanced { get; }
|
||||
public string ParameterType { get; }
|
||||
public bool HasOptions => Options is { Length: > 0 };
|
||||
public bool HasOptions => Options is { Length: > 0 };
|
||||
public bool IsBool => ParameterType == "bool";
|
||||
public bool IsNumeric => ParameterType is "int" or "double";
|
||||
public bool HasRange => IsNumeric && MinValue != null && MaxValue != null;
|
||||
@@ -139,17 +141,17 @@ namespace XplorePlane.ViewModels.ImageProcessing
|
||||
}
|
||||
}
|
||||
|
||||
public string SelectedOption
|
||||
{
|
||||
get => HasOptions
|
||||
? GetLocalizedOption(Convert.ToString(_value, CultureInfo.InvariantCulture) ?? string.Empty)
|
||||
: string.Empty;
|
||||
set
|
||||
{
|
||||
if (HasOptions)
|
||||
Value = GetRawOption(value);
|
||||
}
|
||||
}
|
||||
public string SelectedOption
|
||||
{
|
||||
get => HasOptions
|
||||
? GetLocalizedOption(Convert.ToString(_value, CultureInfo.InvariantCulture) ?? string.Empty)
|
||||
: string.Empty;
|
||||
set
|
||||
{
|
||||
if (HasOptions)
|
||||
Value = GetRawOption(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateValue(object value)
|
||||
{
|
||||
@@ -283,84 +285,84 @@ namespace XplorePlane.ViewModels.ImageProcessing
|
||||
return 3;
|
||||
}
|
||||
|
||||
private static string NormalizeNumericText(string value)
|
||||
private static string NormalizeNumericText(string value)
|
||||
{
|
||||
return value.Trim().TrimEnd('、', ',', ',', '。', '.', ';', ';', ':', ':');
|
||||
}
|
||||
|
||||
private static string LocalizeOption(string option)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(option))
|
||||
return option;
|
||||
|
||||
// 先允许资源覆盖通用名称;没有资源时使用内置通用翻译,保证历史算子
|
||||
// 即使尚未补齐专属资源,也不会把内部英文枚举直接暴露到中文界面。
|
||||
var localized = LocalizationHelper.GetString($"ProcessorOption_{option}");
|
||||
if (!string.Equals(localized, $"ProcessorOption_{option}", StringComparison.Ordinal))
|
||||
return localized;
|
||||
|
||||
if (!CultureInfo.CurrentUICulture.Name.StartsWith("zh", StringComparison.OrdinalIgnoreCase))
|
||||
return SplitWords(option);
|
||||
|
||||
return option switch
|
||||
{
|
||||
"Uniform" => "均匀分层",
|
||||
"Otsu" => "大津法",
|
||||
"Peaks" => "波峰分层",
|
||||
"EqualSpaced" => "等间距",
|
||||
"MidValue" => "层中值",
|
||||
"Spectrum" => "光谱",
|
||||
"Traffic" => "交通灯",
|
||||
"Heat" => "热力图",
|
||||
"Fixed" => "固定阈值",
|
||||
"TopHat" => "顶帽",
|
||||
"LocalContrast" => "局部对比度",
|
||||
"AdaptiveStatistics" => "自适应统计",
|
||||
"Bright" => "亮区域",
|
||||
"Dark" => "暗区域",
|
||||
"Both" => "两者",
|
||||
"Horizontal" => "水平",
|
||||
"Vertical" => "垂直",
|
||||
"None" => "无",
|
||||
"Polygon" => "多边形",
|
||||
"White" => "白色",
|
||||
"Black" => "黑色",
|
||||
"Nearest" => "最近邻",
|
||||
"Bilinear" => "双线性",
|
||||
"Bicubic" => "双三次",
|
||||
"Lanczos" => "Lanczos",
|
||||
"Global" => "全局",
|
||||
"CLAHE" => "CLAHE",
|
||||
"Linear" => "线性",
|
||||
"Logarithmic" => "对数",
|
||||
"Exponential" => "指数",
|
||||
_ => option
|
||||
};
|
||||
}
|
||||
|
||||
private string GetLocalizedOption(string rawOption)
|
||||
{
|
||||
if (Options == null || LocalizedOptions == null)
|
||||
return rawOption;
|
||||
|
||||
var index = Array.IndexOf(Options, rawOption);
|
||||
return index >= 0 && index < LocalizedOptions.Length ? LocalizedOptions[index] : rawOption;
|
||||
}
|
||||
|
||||
private string GetRawOption(string displayOption)
|
||||
{
|
||||
if (Options == null || LocalizedOptions == null)
|
||||
return displayOption;
|
||||
|
||||
var index = Array.IndexOf(LocalizedOptions, displayOption);
|
||||
return index >= 0 && index < Options.Length ? Options[index] : displayOption;
|
||||
}
|
||||
|
||||
private static string SplitWords(string value)
|
||||
{
|
||||
var chars = value.Select((ch, index) => index > 0 && char.IsUpper(ch) ? $" {ch}" : ch.ToString());
|
||||
return string.Concat(chars);
|
||||
}
|
||||
}
|
||||
|
||||
private static string LocalizeOption(string option)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(option))
|
||||
return option;
|
||||
|
||||
// 先允许资源覆盖通用名称;没有资源时使用内置通用翻译,保证历史算子
|
||||
// 即使尚未补齐专属资源,也不会把内部英文枚举直接暴露到中文界面。
|
||||
var localized = LocalizationHelper.GetString($"ProcessorOption_{option}");
|
||||
if (!string.Equals(localized, $"ProcessorOption_{option}", StringComparison.Ordinal))
|
||||
return localized;
|
||||
|
||||
if (!CultureInfo.CurrentUICulture.Name.StartsWith("zh", StringComparison.OrdinalIgnoreCase))
|
||||
return SplitWords(option);
|
||||
|
||||
return option switch
|
||||
{
|
||||
"Uniform" => "均匀分层",
|
||||
"Otsu" => "大津法",
|
||||
"Peaks" => "波峰分层",
|
||||
"EqualSpaced" => "等间距",
|
||||
"MidValue" => "层中值",
|
||||
"Spectrum" => "光谱",
|
||||
"Traffic" => "交通灯",
|
||||
"Heat" => "热力图",
|
||||
"Fixed" => "固定阈值",
|
||||
"TopHat" => "顶帽",
|
||||
"LocalContrast" => "局部对比度",
|
||||
"AdaptiveStatistics" => "自适应统计",
|
||||
"Bright" => "亮区域",
|
||||
"Dark" => "暗区域",
|
||||
"Both" => "两者",
|
||||
"Horizontal" => "水平",
|
||||
"Vertical" => "垂直",
|
||||
"None" => "无",
|
||||
"Polygon" => "多边形",
|
||||
"White" => "白色",
|
||||
"Black" => "黑色",
|
||||
"Nearest" => "最近邻",
|
||||
"Bilinear" => "双线性",
|
||||
"Bicubic" => "双三次",
|
||||
"Lanczos" => "Lanczos",
|
||||
"Global" => "全局",
|
||||
"CLAHE" => "CLAHE",
|
||||
"Linear" => "线性",
|
||||
"Logarithmic" => "对数",
|
||||
"Exponential" => "指数",
|
||||
_ => option
|
||||
};
|
||||
}
|
||||
|
||||
private string GetLocalizedOption(string rawOption)
|
||||
{
|
||||
if (Options == null || LocalizedOptions == null)
|
||||
return rawOption;
|
||||
|
||||
var index = Array.IndexOf(Options, rawOption);
|
||||
return index >= 0 && index < LocalizedOptions.Length ? LocalizedOptions[index] : rawOption;
|
||||
}
|
||||
|
||||
private string GetRawOption(string displayOption)
|
||||
{
|
||||
if (Options == null || LocalizedOptions == null)
|
||||
return displayOption;
|
||||
|
||||
var index = Array.IndexOf(LocalizedOptions, displayOption);
|
||||
return index >= 0 && index < Options.Length ? Options[index] : displayOption;
|
||||
}
|
||||
|
||||
private static string SplitWords(string value)
|
||||
{
|
||||
var chars = value.Select((ch, index) => index > 0 && char.IsUpper(ch) ? $" {ch}" : ch.ToString());
|
||||
return string.Concat(chars);
|
||||
}
|
||||
|
||||
private static bool TryConvertToInt(object value, out int result)
|
||||
{
|
||||
@@ -453,4 +455,4 @@ namespace XplorePlane.ViewModels.ImageProcessing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,6 +526,9 @@
|
||||
<DataTrigger Binding="{Binding IsOutputControl}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsAdvanced}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
@@ -672,6 +675,170 @@
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- 高级参数分组(默认折叠,仅当存在高级参数时显示) -->
|
||||
<Expander Header="高级参数" Style="{StaticResource InspectorExpanderStyle}"
|
||||
IsExpanded="False"
|
||||
Visibility="{Binding SelectedNode.HasAdvancedParameters, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ItemsControl ItemsSource="{Binding SelectedNode.Parameters}">
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style TargetType="ContentPresenter">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding IsAdvanced}" Value="True" />
|
||||
<Condition Binding="{Binding IsVisible}" Value="True" />
|
||||
<Condition Binding="{Binding IsOutputControl}" Value="False" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</MultiDataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,4" TextBlock.FontWeight="Normal">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="92" />
|
||||
<ColumnDefinition Width="*" MinWidth="110" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Margin="0,0,6,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="{StaticResource NovaFontSizeCaption}"
|
||||
Text="{Binding DisplayName}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
ToolTip="{Binding DisplayName}" />
|
||||
|
||||
<Grid Grid.Column="1">
|
||||
<Grid.Style>
|
||||
<Style TargetType="Grid">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSliderInput}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Grid.Style>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="52" />
|
||||
<ColumnDefinition Width="*" MinWidth="52" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Margin="0,0,6,0"
|
||||
Padding="2,2"
|
||||
VerticalAlignment="Center"
|
||||
BorderBrush="{StaticResource NovaOutlineBrush}"
|
||||
BorderThickness="1"
|
||||
FontSize="{StaticResource NovaFontSizeCaption}"
|
||||
Text="{Binding SliderValue, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" />
|
||||
|
||||
<Grid Grid.Column="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Left"
|
||||
FontSize="{StaticResource NovaFontSizeLabelXSmall}"
|
||||
Foreground="{StaticResource NovaOnSurfaceVariantBrush}"
|
||||
Text="{Binding SliderMinimum}" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
FontSize="{StaticResource NovaFontSizeLabelXSmall}"
|
||||
Foreground="{StaticResource NovaOnSurfaceVariantBrush}"
|
||||
Text="{Binding SliderMaximum}" />
|
||||
</Grid>
|
||||
<Slider
|
||||
Grid.Row="1"
|
||||
VerticalAlignment="Center"
|
||||
IsSnapToTickEnabled="{Binding IsIntegerSlider}"
|
||||
LargeChange="{Binding SliderLargeChange}"
|
||||
Maximum="{Binding SliderMaximum}"
|
||||
Minimum="{Binding SliderMinimum}"
|
||||
SmallChange="{Binding SliderSmallChange}"
|
||||
TickFrequency="{Binding SliderTickFrequency}"
|
||||
Value="{Binding SliderValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
MinWidth="90"
|
||||
Padding="2,2"
|
||||
BorderBrush="{StaticResource NovaOutlineBrush}"
|
||||
BorderThickness="1"
|
||||
FontSize="{StaticResource NovaFontSizeCaption}"
|
||||
Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}">
|
||||
<TextBox.Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsTextInput}" Value="False">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsValueValid}" Value="False">
|
||||
<Setter Property="BorderBrush" Value="{StaticResource NovaErrorBrush}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBox.Style>
|
||||
</TextBox>
|
||||
|
||||
<ComboBox
|
||||
Grid.Column="1"
|
||||
MinHeight="24"
|
||||
MinWidth="90"
|
||||
Padding="4,1"
|
||||
HorizontalContentAlignment="Left"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="{StaticResource NovaOutlineBrush}"
|
||||
BorderThickness="1"
|
||||
FontSize="{StaticResource NovaFontSizeCaption}"
|
||||
ItemsSource="{Binding LocalizedOptions}"
|
||||
SelectedItem="{Binding SelectedOption, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<ComboBox.Style>
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasOptions}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ComboBox.Style>
|
||||
</ComboBox>
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="{StaticResource NovaFontSizeCaption}"
|
||||
IsChecked="{Binding BoolValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<CheckBox.Style>
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsBool}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</CheckBox.Style>
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Expander>
|
||||
|
||||
<!-- 输出控制项分组(仅当存在 OutputXxx 参数时显示) -->
|
||||
<Expander Header="输出字段"
|
||||
IsExpanded="True"
|
||||
|
||||
Reference in New Issue
Block a user