3677bb85a4
- ProcessorParameter新增IsAdvanced属性 - ProcessorParameterVM导出IsAdvanced供XAML绑定 - PipelineNodeViewModel新增HasAdvancedParameters - PipelineEditorView增加'高级参数'Expander(默认折叠) - 修复Expander内FontWeight继承导致的字体样式不一致
459 lines
16 KiB
C#
459 lines
16 KiB
C#
// ── 算子参数 ViewModel ──
|
||
// 将 ProcessorParameter 模型包装为可绑定的 ViewModel,
|
||
// 支持参数值编辑/校验/重置/格式化和类型转换。
|
||
using Prism.Mvvm;
|
||
using System;
|
||
using System.Globalization;
|
||
using System.Linq;
|
||
using XP.ImageProcessing.Core;
|
||
using XP.ImageProcessing.Processors;
|
||
|
||
namespace XplorePlane.ViewModels.ImageProcessing
|
||
{
|
||
/// <summary>算子参数 ViewModel,可绑定参数名/值/范围/类型</summary>
|
||
public class ProcessorParameterVM : BindableBase
|
||
{
|
||
private object _value;
|
||
private bool _isValueValid = true;
|
||
|
||
public ProcessorParameterVM(ProcessorParameter parameter)
|
||
{
|
||
Name = parameter.Name;
|
||
DisplayName = parameter.DisplayName;
|
||
_value = parameter.Value;
|
||
MinValue = parameter.MinValue;
|
||
MaxValue = parameter.MaxValue;
|
||
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",
|
||
"double" => "double",
|
||
"boolean" or "bool" => "bool",
|
||
_ => "enum"
|
||
};
|
||
ValidateValue(_value);
|
||
}
|
||
|
||
public string Name { get; }
|
||
public string DisplayName { get; }
|
||
public object MinValue { get; }
|
||
public object MaxValue { 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 IsBool => ParameterType == "bool";
|
||
public bool IsNumeric => ParameterType is "int" or "double";
|
||
public bool HasRange => IsNumeric && MinValue != null && MaxValue != null;
|
||
public bool IsSliderInput => HasRange;
|
||
public bool IsTextInput => !IsBool && !HasOptions && !IsSliderInput;
|
||
|
||
/// <summary>是否为输出控制参数(Name 以 "Output" 开头的 bool 参数)</summary>
|
||
public bool IsOutputControl => IsBool && Name.StartsWith("Output", StringComparison.Ordinal);
|
||
|
||
public double SliderMinimum => TryConvertToDouble(MinValue, out var minValue) ? minValue : 0d;
|
||
public double SliderMaximum => TryConvertToDouble(MaxValue, out var maxValue) ? maxValue : 100d;
|
||
public double SliderTickFrequency => ResolveTickFrequency();
|
||
|
||
/// <summary>键盘/滚轮细调步距(等于 TickFrequency)</summary>
|
||
public double SliderSmallChange => ResolveTickFrequency();
|
||
|
||
/// <summary>PageUp/PageDown 或点击轨道的粗调步距(约为范围的 10%,对齐到 SmallChange 的整数倍)</summary>
|
||
public double SliderLargeChange => ResolveLargeChange();
|
||
|
||
public bool IsIntegerSlider => ParameterType == "int";
|
||
|
||
public bool IsValueValid
|
||
{
|
||
get => _isValueValid;
|
||
private set => SetProperty(ref _isValueValid, value);
|
||
}
|
||
|
||
public object Value
|
||
{
|
||
get => _value;
|
||
set
|
||
{
|
||
var normalizedValue = NormalizeValue(value);
|
||
if (!Equals(_value, normalizedValue))
|
||
{
|
||
_value = normalizedValue;
|
||
ValidateValue(normalizedValue);
|
||
RaisePropertyChanged(nameof(Value));
|
||
RaisePropertyChanged(nameof(BoolValue));
|
||
RaisePropertyChanged(nameof(SelectedOption));
|
||
RaisePropertyChanged(nameof(SliderValue));
|
||
RaisePropertyChanged(nameof(DisplayValueText));
|
||
}
|
||
}
|
||
}
|
||
|
||
public double SliderValue
|
||
{
|
||
get => TryConvertToDouble(_value, out var sliderValue) ? sliderValue : SliderMinimum;
|
||
set
|
||
{
|
||
if (!IsSliderInput)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Value = ParameterType == "int"
|
||
? (object)Convert.ToInt32(Math.Round(value, MidpointRounding.AwayFromZero), CultureInfo.InvariantCulture)
|
||
: Math.Round(value, ResolveDecimalPlaces(), MidpointRounding.AwayFromZero);
|
||
}
|
||
}
|
||
|
||
public string DisplayValueText
|
||
{
|
||
get
|
||
{
|
||
if (ParameterType == "int" && TryConvertToInt(_value, out var intValue))
|
||
{
|
||
return intValue.ToString(CultureInfo.InvariantCulture);
|
||
}
|
||
|
||
if (ParameterType == "double" && TryConvertToDouble(_value, out var doubleValue))
|
||
{
|
||
return doubleValue.ToString($"F{ResolveDecimalPlaces()}", CultureInfo.InvariantCulture);
|
||
}
|
||
|
||
return Convert.ToString(_value, CultureInfo.InvariantCulture) ?? string.Empty;
|
||
}
|
||
}
|
||
|
||
public bool BoolValue
|
||
{
|
||
get => ParameterType == "bool" && TryConvertToBool(_value, out var boolValue) && boolValue;
|
||
set
|
||
{
|
||
if (ParameterType == "bool")
|
||
{
|
||
Value = 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)
|
||
{
|
||
if (value == null)
|
||
{
|
||
IsValueValid = false;
|
||
return;
|
||
}
|
||
|
||
if (ParameterType == "int")
|
||
{
|
||
IsValueValid = TryConvertToInt(value, out var intValue) && IsWithinRange(intValue);
|
||
return;
|
||
}
|
||
|
||
if (ParameterType == "double")
|
||
{
|
||
IsValueValid = TryConvertToDouble(value, out var doubleValue) && IsWithinRange(doubleValue);
|
||
return;
|
||
}
|
||
|
||
if (ParameterType == "bool")
|
||
{
|
||
IsValueValid = TryConvertToBool(value, out _);
|
||
return;
|
||
}
|
||
|
||
if (Options is { Length: > 0 })
|
||
{
|
||
var stringValue = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
|
||
IsValueValid = Options.Contains(stringValue, StringComparer.OrdinalIgnoreCase);
|
||
return;
|
||
}
|
||
|
||
IsValueValid = true;
|
||
}
|
||
|
||
private object NormalizeValue(object value)
|
||
{
|
||
if (value == null)
|
||
return value;
|
||
|
||
if (ParameterType == "int" && TryConvertToInt(value, out var intValue))
|
||
return intValue;
|
||
|
||
if (ParameterType == "double" && TryConvertToDouble(value, out var doubleValue))
|
||
return doubleValue;
|
||
|
||
if (ParameterType == "bool" && TryConvertToBool(value, out var boolValue))
|
||
return boolValue;
|
||
|
||
return value;
|
||
}
|
||
|
||
private bool IsWithinRange(double value)
|
||
{
|
||
if (MinValue != null && TryConvertToDouble(MinValue, out var minValue) && value < minValue)
|
||
return false;
|
||
|
||
if (MaxValue != null && TryConvertToDouble(MaxValue, out var maxValue) && value > maxValue)
|
||
return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
private double ResolveTickFrequency()
|
||
{
|
||
if (!HasRange)
|
||
{
|
||
return 1d;
|
||
}
|
||
|
||
double range = SliderMaximum - SliderMinimum;
|
||
|
||
if (ParameterType == "int")
|
||
{
|
||
// 整数步距:范围越大步距越大,避免刻度过密
|
||
if (range <= 20) return 1d;
|
||
if (range <= 100) return 5d;
|
||
if (range <= 500) return 10d;
|
||
if (range <= 2000) return 50d;
|
||
return Math.Ceiling(range / 40d / 10d) * 10d; // 约 40 步,对齐到 10 的倍数
|
||
}
|
||
|
||
// 浮点步距:按范围量级决定精度
|
||
if (range <= 1d) return 0.01d;
|
||
if (range <= 10d) return 0.1d;
|
||
if (range <= 100d) return 1d;
|
||
return Math.Ceiling(range / 100d); // 约 100 步
|
||
}
|
||
|
||
private double ResolveLargeChange()
|
||
{
|
||
if (!HasRange)
|
||
{
|
||
return 10d;
|
||
}
|
||
|
||
double range = SliderMaximum - SliderMinimum;
|
||
double small = ResolveTickFrequency();
|
||
|
||
// 粗调约为范围的 10%,并对齐到 SmallChange 的整数倍(至少 1 步)
|
||
double rough = range * 0.1d;
|
||
double steps = Math.Max(1d, Math.Round(rough / small));
|
||
return steps * small;
|
||
}
|
||
|
||
private int ResolveDecimalPlaces()
|
||
{
|
||
if (ParameterType == "int")
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
double tick = SliderTickFrequency;
|
||
if (tick >= 1d)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
if (tick >= 0.1d)
|
||
{
|
||
return 1;
|
||
}
|
||
|
||
if (tick >= 0.01d)
|
||
{
|
||
return 2;
|
||
}
|
||
|
||
return 3;
|
||
}
|
||
|
||
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 bool TryConvertToInt(object value, out int result)
|
||
{
|
||
switch (value)
|
||
{
|
||
case int intValue:
|
||
result = intValue;
|
||
return true;
|
||
|
||
case string stringValue:
|
||
stringValue = NormalizeNumericText(stringValue);
|
||
return int.TryParse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out result)
|
||
|| int.TryParse(stringValue, NumberStyles.Integer, CultureInfo.CurrentCulture, out result);
|
||
|
||
default:
|
||
try
|
||
{
|
||
result = Convert.ToInt32(value, CultureInfo.InvariantCulture);
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
result = default;
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
private static bool TryConvertToDouble(object value, out double result)
|
||
{
|
||
switch (value)
|
||
{
|
||
case double doubleValue:
|
||
result = doubleValue;
|
||
return true;
|
||
|
||
case float floatValue:
|
||
result = floatValue;
|
||
return true;
|
||
|
||
case int intValue:
|
||
result = intValue;
|
||
return true;
|
||
|
||
case long longValue:
|
||
result = longValue;
|
||
return true;
|
||
|
||
case string stringValue:
|
||
stringValue = NormalizeNumericText(stringValue);
|
||
return double.TryParse(stringValue, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result)
|
||
|| double.TryParse(stringValue, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.CurrentCulture, out result);
|
||
|
||
default:
|
||
try
|
||
{
|
||
result = Convert.ToDouble(value, CultureInfo.InvariantCulture);
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
result = default;
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
private static bool TryConvertToBool(object value, out bool result)
|
||
{
|
||
switch (value)
|
||
{
|
||
case bool boolValue:
|
||
result = boolValue;
|
||
return true;
|
||
|
||
case string stringValue:
|
||
return bool.TryParse(stringValue.Trim(), out result);
|
||
|
||
default:
|
||
try
|
||
{
|
||
result = Convert.ToBoolean(value, CultureInfo.InvariantCulture);
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
result = default;
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|