已合并 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:
LI Wei.lw
2026-08-17 11:00:09 +08:00
30 changed files with 798 additions and 581 deletions
@@ -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)
{
@@ -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");
}
@@ -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-65535LUT 升级为 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-65535LUT 升级为 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
View File
@@ -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"