已合并 PR 25: 合并最新图像库,打通加载图像和算子流程图拖拽功能

This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
using Emgu.CV;
|
||||
using Emgu.CV.Structure;
|
||||
using Emgu.CV.Util;
|
||||
using System.Globalization;
|
||||
|
||||
namespace XP.ImageProcessing.Core;
|
||||
|
||||
@@ -164,11 +165,45 @@ public abstract class ImageProcessorBase
|
||||
/// </summary>
|
||||
public T GetParameter<T>(string name)
|
||||
{
|
||||
if (Parameters.ContainsKey(name))
|
||||
{
|
||||
return (T)Convert.ChangeType(Parameters[name].Value, typeof(T))!;
|
||||
}
|
||||
if (!Parameters.ContainsKey(name))
|
||||
throw new ArgumentException($"参数 {name} 不存在");
|
||||
|
||||
var parameter = Parameters[name];
|
||||
|
||||
try
|
||||
{
|
||||
if (parameter.Value is T typedValue)
|
||||
return typedValue;
|
||||
|
||||
if (parameter.Value is string textValue)
|
||||
{
|
||||
var normalizedText = NormalizeText(textValue);
|
||||
if (typeof(T) == typeof(string))
|
||||
return (T)(object)textValue;
|
||||
|
||||
if (typeof(T) == typeof(int) && int.TryParse(normalizedText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
|
||||
return (T)(object)intValue;
|
||||
|
||||
if (typeof(T) == typeof(double) && double.TryParse(normalizedText, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
return (T)(object)doubleValue;
|
||||
|
||||
if (typeof(T) == typeof(bool) && bool.TryParse(normalizedText, out var boolValue))
|
||||
return (T)(object)boolValue;
|
||||
}
|
||||
|
||||
return (T)Convert.ChangeType(parameter.Value, typeof(T), CultureInfo.InvariantCulture)!;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"参数 {name} 的值 '{parameter.Value}' 无法转换为 {typeof(T).Name}",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeText(string value)
|
||||
{
|
||||
return value.Trim().TrimEnd('、', ',', ',', '。', '.', ';', ';', ':', ':');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -70,7 +70,7 @@ public class SuperResolutionProcessor : ImageProcessorBase
|
||||
public override Image<Gray, byte> Process(Image<Gray, byte> inputImage)
|
||||
{
|
||||
string model = GetParameter<string>("Model");
|
||||
int scale = int.Parse(GetParameter<string>("Scale"));
|
||||
int scale = GetParameter<int>("Scale");
|
||||
|
||||
// 查找模型文件
|
||||
string modelPath = FindModelFile(model, scale);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using Moq;
|
||||
using Prism.Events;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows.Media.Imaging;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services;
|
||||
@@ -29,7 +33,7 @@ namespace XplorePlane.Tests.Pipeline
|
||||
}
|
||||
|
||||
private PipelineEditorViewModel CreateVm() =>
|
||||
new PipelineEditorViewModel(_mockImageSvc.Object, _mockExecSvc.Object, _mockPersistSvc.Object, _mockLogger.Object);
|
||||
new PipelineEditorViewModel(_mockImageSvc.Object, _mockExecSvc.Object, _mockPersistSvc.Object, new EventAggregator(), _mockLogger.Object);
|
||||
|
||||
// ── 6.1 AddOperatorCommand ────────────────────────────────────
|
||||
|
||||
@@ -44,6 +48,18 @@ namespace XplorePlane.Tests.Pipeline
|
||||
Assert.Equal(0, vm.PipelineNodes[0].Order);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOperator_KnownKey_SetsIconPath()
|
||||
{
|
||||
_mockImageSvc.Setup(s => s.GetAvailableProcessors()).Returns(new[] { "ShockFilter" });
|
||||
|
||||
var vm = CreateVm();
|
||||
vm.AddOperatorCommand.Execute("ShockFilter");
|
||||
|
||||
Assert.Single(vm.PipelineNodes);
|
||||
Assert.Equal("⚡", vm.PipelineNodes[0].IconPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOperator_InvalidKey_NodeNotAdded()
|
||||
{
|
||||
@@ -92,6 +108,36 @@ namespace XplorePlane.Tests.Pipeline
|
||||
Assert.Equal(i, vm.PipelineNodes[i].Order);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadImageFromFile_SetsSourceImage()
|
||||
{
|
||||
var vm = CreateVm();
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".png");
|
||||
|
||||
try
|
||||
{
|
||||
var bitmap = TestHelpers.CreateTestBitmap(8, 8);
|
||||
var encoder = new PngBitmapEncoder();
|
||||
encoder.Frames.Add(BitmapFrame.Create(bitmap));
|
||||
|
||||
using (var stream = File.Create(tempPath))
|
||||
{
|
||||
encoder.Save(stream);
|
||||
}
|
||||
|
||||
vm.LoadImageFromFile(tempPath);
|
||||
|
||||
Assert.NotNull(vm.SourceImage);
|
||||
Assert.NotNull(vm.PreviewImage);
|
||||
Assert.Contains(Path.GetFileName(tempPath), vm.StatusMessage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempPath))
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 6.2 RemoveOperatorCommand ─────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -2,6 +2,7 @@ using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using Moq;
|
||||
using Prism.Events;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -31,7 +32,7 @@ namespace XplorePlane.Tests.Pipeline
|
||||
var mockPersistSvc = new Mock<IPipelinePersistenceService>();
|
||||
var mockLogger = new Mock<ILoggerService>();
|
||||
mockLogger.Setup(l => l.ForModule<PipelineEditorViewModel>()).Returns(mockLogger.Object);
|
||||
return new PipelineEditorViewModel(mockImageSvc.Object, mockExecSvc.Object, mockPersistSvc.Object, mockLogger.Object);
|
||||
return new PipelineEditorViewModel(mockImageSvc.Object, mockExecSvc.Object, mockPersistSvc.Object, new EventAggregator(), mockLogger.Object);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Moq;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace XplorePlane.Tests.Services
|
||||
{
|
||||
public class ImageProcessingServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void DiscoverProcessors_LoadsKnownProcessors()
|
||||
{
|
||||
var logger = new Mock<ILoggerService>();
|
||||
logger.Setup(l => l.ForModule<ImageProcessingService>()).Returns(logger.Object);
|
||||
|
||||
using var service = new ImageProcessingService(logger.Object);
|
||||
|
||||
var processors = service.GetAvailableProcessors();
|
||||
|
||||
Assert.Contains("GaussianBlur", processors);
|
||||
Assert.Contains("ShockFilter", processors);
|
||||
Assert.Contains("BandPassFilter", processors);
|
||||
Assert.Contains("Division", processors);
|
||||
Assert.Contains("Contour", processors);
|
||||
Assert.True(processors.Count >= 20, $"Expected many discovered processors, got {processors.Count}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,7 +268,6 @@ namespace XplorePlane
|
||||
|
||||
// 注册视图和视图模型
|
||||
containerRegistry.RegisterForNavigation<MainWindow>();
|
||||
containerRegistry.RegisterForNavigation<MainWindowB>();
|
||||
containerRegistry.Register<MainViewModel>();
|
||||
containerRegistry.RegisterSingleton<NavigationPropertyPanelViewModel>();
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Prism.Events;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace XplorePlane.Events
|
||||
{
|
||||
public sealed class ManualImageLoadedEvent : PubSubEvent<ManualImageLoadedPayload>
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class ManualImageLoadedPayload
|
||||
{
|
||||
public ManualImageLoadedPayload(BitmapSource image, string filePath)
|
||||
{
|
||||
Image = image;
|
||||
FilePath = filePath;
|
||||
}
|
||||
|
||||
public BitmapSource Image { get; }
|
||||
public string FilePath { get; }
|
||||
public string FileName => System.IO.Path.GetFileName(FilePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Prism.Events;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace XplorePlane.Events
|
||||
{
|
||||
public sealed class PipelinePreviewUpdatedEvent : PubSubEvent<PipelinePreviewUpdatedPayload>
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class PipelinePreviewUpdatedPayload
|
||||
{
|
||||
public PipelinePreviewUpdatedPayload(BitmapSource image, string statusMessage)
|
||||
{
|
||||
Image = image;
|
||||
StatusMessage = statusMessage;
|
||||
}
|
||||
|
||||
public BitmapSource Image { get; }
|
||||
public string StatusMessage { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("XplorePlane.Tests")]
|
||||
@@ -1,12 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media.Imaging;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.ImageProcessing.Core;
|
||||
using XP.ImageProcessing.Processors;
|
||||
|
||||
namespace XplorePlane.Services
|
||||
{
|
||||
@@ -18,40 +21,180 @@ namespace XplorePlane.Services
|
||||
public ImageProcessingService(ILoggerService logger)
|
||||
{
|
||||
_logger = logger?.ForModule<ImageProcessingService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
_processorRegistry = new ConcurrentDictionary<string, ImageProcessorBase>();
|
||||
RegisterBuiltInProcessors();
|
||||
_processorRegistry = new ConcurrentDictionary<string, ImageProcessorBase>(StringComparer.OrdinalIgnoreCase);
|
||||
DiscoverProcessors();
|
||||
}
|
||||
|
||||
private void RegisterBuiltInProcessors()
|
||||
private void DiscoverProcessors()
|
||||
{
|
||||
// 8-bit processors
|
||||
_processorRegistry["GaussianBlur"] = new GaussianBlurProcessor();
|
||||
_processorRegistry["Threshold"] = new ThresholdProcessor();
|
||||
_processorRegistry["Division"] = new DivisionProcessor();
|
||||
_processorRegistry["Contrast"] = new ContrastProcessor();
|
||||
_processorRegistry["Gamma"] = new GammaProcessor();
|
||||
_processorRegistry["Morphology"] = new MorphologyProcessor();
|
||||
_processorRegistry["Contour"] = new ContourProcessor();
|
||||
_processorRegistry["ShockFilter"] = new ShockFilterProcessor();
|
||||
_processorRegistry["BandPassFilter"] = new BandPassFilterProcessor();
|
||||
var assemblies = LoadCandidateAssemblies().ToList();
|
||||
var discoveredCount = 0;
|
||||
|
||||
_logger.Info("Registered {Count} built-in image processors", _processorRegistry.Count);
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
foreach (var processorType in GetProcessorTypes(assembly))
|
||||
{
|
||||
if (!TryCreateProcessor(processorType, out var processor))
|
||||
continue;
|
||||
|
||||
var key = GetProcessorKey(processorType);
|
||||
RegisterProcessorInternal(key, processor, discovered: true);
|
||||
discoveredCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetAvailableProcessors() => new List<string>(_processorRegistry.Keys).AsReadOnly();
|
||||
_logger.Info(
|
||||
"Discovered {ProcessorCount} image processors from {AssemblyCount} assemblies",
|
||||
discoveredCount,
|
||||
assemblies.Count);
|
||||
}
|
||||
|
||||
private IReadOnlyList<Assembly> LoadCandidateAssemblies()
|
||||
{
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => !a.IsDynamic)
|
||||
.ToList();
|
||||
|
||||
var loadedNames = new HashSet<string>(
|
||||
assemblies.Select(a => a.GetName().Name ?? string.Empty),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var path in EnumerateProcessorAssemblyPaths())
|
||||
{
|
||||
var assemblyName = Path.GetFileNameWithoutExtension(path);
|
||||
if (string.IsNullOrWhiteSpace(assemblyName) || loadedNames.Contains(assemblyName))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(path));
|
||||
loadedNames.Add(assembly.GetName().Name ?? assemblyName);
|
||||
assemblies.Add(assembly);
|
||||
_logger.Debug("Loaded processor assembly from {Path}", path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to load processor assembly from {Path}", path);
|
||||
}
|
||||
}
|
||||
|
||||
return assemblies;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateProcessorAssemblyPaths()
|
||||
{
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
var candidateDirectories = new[]
|
||||
{
|
||||
baseDirectory,
|
||||
Path.Combine(baseDirectory, "Processors"),
|
||||
Path.Combine(baseDirectory, "Plugins"),
|
||||
Path.Combine(baseDirectory, "Libs", "ImageProcessing")
|
||||
};
|
||||
|
||||
var paths = new List<string>();
|
||||
|
||||
foreach (var directory in candidateDirectories.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
continue;
|
||||
|
||||
if (string.Equals(directory, baseDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var processorDll = Path.Combine(directory, "XP.ImageProcessing.Processors.dll");
|
||||
if (File.Exists(processorDll))
|
||||
paths.Add(processorDll);
|
||||
}
|
||||
else
|
||||
{
|
||||
paths.AddRange(Directory.GetFiles(directory, "*.dll", SearchOption.TopDirectoryOnly));
|
||||
}
|
||||
}
|
||||
|
||||
return paths.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static IEnumerable<Type> GetProcessorTypes(Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
return assembly
|
||||
.GetTypes()
|
||||
.Where(type => type is { IsClass: true, IsAbstract: false }
|
||||
&& typeof(ImageProcessorBase).IsAssignableFrom(type)
|
||||
&& type.GetConstructor(Type.EmptyTypes) != null);
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
return ex.Types
|
||||
.Where(type => type != null
|
||||
&& type.IsClass
|
||||
&& !type.IsAbstract
|
||||
&& typeof(ImageProcessorBase).IsAssignableFrom(type)
|
||||
&& type.GetConstructor(Type.EmptyTypes) != null)!;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetProcessorKey(Type processorType)
|
||||
{
|
||||
const string suffix = "Processor";
|
||||
return processorType.Name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)
|
||||
? processorType.Name[..^suffix.Length]
|
||||
: processorType.Name;
|
||||
}
|
||||
|
||||
private bool TryCreateProcessor(Type processorType, out ImageProcessorBase processor)
|
||||
{
|
||||
try
|
||||
{
|
||||
processor = (ImageProcessorBase)Activator.CreateInstance(processorType)!;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
processor = null;
|
||||
_logger.Error(ex, "Failed to create processor instance for {ProcessorType}", processorType.FullName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterProcessorInternal(string name, ImageProcessorBase processor, bool discovered)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("Processor name cannot be empty", nameof(name));
|
||||
if (processor == null)
|
||||
throw new ArgumentNullException(nameof(processor));
|
||||
|
||||
_processorRegistry.AddOrUpdate(
|
||||
name,
|
||||
processor,
|
||||
(_, _) => processor);
|
||||
|
||||
if (discovered)
|
||||
_logger.Debug("Discovered processor: {ProcessorName}", name);
|
||||
else
|
||||
_logger.Info("Registered processor: {ProcessorName}", name);
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetAvailableProcessors()
|
||||
{
|
||||
return _processorRegistry.Keys
|
||||
.OrderBy(key => ProcessorUiMetadata.GetCategoryOrder(ProcessorUiMetadata.Get(key).Category))
|
||||
.ThenBy(key => GetProcessorDisplayName(key), StringComparer.CurrentCultureIgnoreCase)
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
}
|
||||
|
||||
public void RegisterProcessor(string name, ImageProcessorBase processor)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Processor name cannot be empty", nameof(name));
|
||||
if (processor == null) throw new ArgumentNullException(nameof(processor));
|
||||
_processorRegistry[name] = processor;
|
||||
_logger.Info("Registered processor: {ProcessorName}", name);
|
||||
RegisterProcessorInternal(name, processor, discovered: false);
|
||||
}
|
||||
|
||||
public IReadOnlyList<ProcessorParameter> GetProcessorParameters(string processorName)
|
||||
{
|
||||
if (_processorRegistry.TryGetValue(processorName, out var processor))
|
||||
return processor.GetParameters().AsReadOnly();
|
||||
|
||||
throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName));
|
||||
}
|
||||
|
||||
@@ -59,13 +202,15 @@ namespace XplorePlane.Services
|
||||
{
|
||||
if (_processorRegistry.TryGetValue(processorName, out var processor))
|
||||
return processor;
|
||||
|
||||
throw new ArgumentException($"Processor not registered or is 16-bit only: {processorName}", nameof(processorName));
|
||||
}
|
||||
|
||||
public string GetProcessorDisplayName(string processorName)
|
||||
{
|
||||
if (_processorRegistry.TryGetValue(processorName, out var p))
|
||||
return string.IsNullOrWhiteSpace(p.Name) ? processorName : p.Name;
|
||||
if (_processorRegistry.TryGetValue(processorName, out var processor))
|
||||
return string.IsNullOrWhiteSpace(processor.Name) ? processorName : processor.Name;
|
||||
|
||||
return processorName;
|
||||
}
|
||||
|
||||
@@ -81,7 +226,6 @@ namespace XplorePlane.Services
|
||||
if (!_processorRegistry.TryGetValue(processorName, out var processor))
|
||||
throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName));
|
||||
|
||||
// Extract pixels on the UI thread (BitmapSource / FormatConvertedBitmap are DependencyObjects)
|
||||
var rawPixels = ImageConverter.ExtractGray8Pixels(source, out int imgWidth, out int imgHeight);
|
||||
|
||||
return await Task.Run(() =>
|
||||
@@ -103,7 +247,7 @@ namespace XplorePlane.Services
|
||||
progress?.Report(0.9);
|
||||
|
||||
var result = ImageConverter.ToBitmapSource(processedEmgu);
|
||||
result.Freeze(); // must freeze before crossing thread boundary
|
||||
result.Freeze();
|
||||
progress?.Report(1.0);
|
||||
|
||||
return result;
|
||||
@@ -131,6 +275,7 @@ namespace XplorePlane.Services
|
||||
if (processor is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
_processorRegistry.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
using System;
|
||||
|
||||
namespace XplorePlane.Services
|
||||
{
|
||||
internal static class ProcessorUiMetadata
|
||||
{
|
||||
private static readonly (string Category, string CategoryIcon, int Order)[] CategoryDefinitions =
|
||||
{
|
||||
("滤波与平滑", "🌀", 0),
|
||||
("图像增强", "✨", 1),
|
||||
("图像变换", "🔁", 2),
|
||||
("数学运算", "➗", 3),
|
||||
("形态学处理", "⬚", 4),
|
||||
("边缘检测", "📐", 5),
|
||||
("检测分析", "🔎", 6),
|
||||
("其他", "⚙", 99),
|
||||
};
|
||||
|
||||
internal static (string Category, string CategoryIcon, string OperatorIcon) Get(string operatorKey)
|
||||
{
|
||||
var category = GetCategory(operatorKey);
|
||||
return (category, GetCategoryIcon(category), GetOperatorIcon(operatorKey, category));
|
||||
}
|
||||
|
||||
internal static string GetCategory(string operatorKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(operatorKey))
|
||||
return "其他";
|
||||
|
||||
if (ContainsAny(operatorKey, "Blur", "Filter", "Shock"))
|
||||
return "滤波与平滑";
|
||||
|
||||
if (ContainsAny(operatorKey, "Contrast", "Gamma", "Retinex", "Histogram", "Sharpen", "Layer",
|
||||
"SubPixel", "SuperResolution", "HDR", "Effect", "PseudoColor", "Color"))
|
||||
return "图像增强";
|
||||
|
||||
if (ContainsAny(operatorKey, "Mirror", "Rotate", "Grayscale", "Threshold"))
|
||||
return "图像变换";
|
||||
|
||||
if (ContainsAny(operatorKey, "Division", "Multiplication", "Difference", "Integral", "Or"))
|
||||
return "数学运算";
|
||||
|
||||
if (ContainsAny(operatorKey, "Morphology"))
|
||||
return "形态学处理";
|
||||
|
||||
if (ContainsAny(operatorKey, "Edge"))
|
||||
return "边缘检测";
|
||||
|
||||
if (ContainsAny(operatorKey, "Measurement", "Detection", "Contour", "FillRate", "Void", "Line", "PointToLine", "Ellipse", "Bga"))
|
||||
return "检测分析";
|
||||
|
||||
return "其他";
|
||||
}
|
||||
|
||||
internal static int GetCategoryOrder(string category)
|
||||
{
|
||||
foreach (var definition in CategoryDefinitions)
|
||||
{
|
||||
if (string.Equals(definition.Category, category, StringComparison.Ordinal))
|
||||
return definition.Order;
|
||||
}
|
||||
|
||||
return 99;
|
||||
}
|
||||
|
||||
internal static string GetCategoryIcon(string category)
|
||||
{
|
||||
foreach (var definition in CategoryDefinitions)
|
||||
{
|
||||
if (string.Equals(definition.Category, category, StringComparison.Ordinal))
|
||||
return definition.CategoryIcon;
|
||||
}
|
||||
|
||||
return "⚙";
|
||||
}
|
||||
|
||||
internal static string GetOperatorIcon(string operatorKey) => GetOperatorIcon(operatorKey, GetCategory(operatorKey));
|
||||
|
||||
private static string GetOperatorIcon(string operatorKey, string category)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(operatorKey))
|
||||
return GetCategoryIcon(category);
|
||||
|
||||
if (ContainsAny(operatorKey, "Shock"))
|
||||
return "⚡";
|
||||
if (ContainsAny(operatorKey, "BandPass"))
|
||||
return "📶";
|
||||
if (ContainsAny(operatorKey, "GaussianBlur", "MeanFilter", "MedianFilter", "BilateralFilter", "LowPassFilter", "HighPassFilter"))
|
||||
return "🌀";
|
||||
if (ContainsAny(operatorKey, "Contrast"))
|
||||
return "🌗";
|
||||
if (ContainsAny(operatorKey, "Gamma"))
|
||||
return "γ";
|
||||
if (ContainsAny(operatorKey, "Retinex"))
|
||||
return "🎛";
|
||||
if (ContainsAny(operatorKey, "Histogram"))
|
||||
return "📊";
|
||||
if (ContainsAny(operatorKey, "Sharpen"))
|
||||
return "✦";
|
||||
if (ContainsAny(operatorKey, "SubPixel", "SuperResolution"))
|
||||
return "🔬";
|
||||
if (ContainsAny(operatorKey, "HDR"))
|
||||
return "💡";
|
||||
if (ContainsAny(operatorKey, "PseudoColor"))
|
||||
return "🎨";
|
||||
if (ContainsAny(operatorKey, "FilmEffect"))
|
||||
return "🎞";
|
||||
if (ContainsAny(operatorKey, "ColorLayer"))
|
||||
return "🧪";
|
||||
if (ContainsAny(operatorKey, "Mirror"))
|
||||
return "↔";
|
||||
if (ContainsAny(operatorKey, "Rotate"))
|
||||
return "⟳";
|
||||
if (ContainsAny(operatorKey, "Grayscale"))
|
||||
return "◻";
|
||||
if (ContainsAny(operatorKey, "Threshold"))
|
||||
return "▣";
|
||||
if (ContainsAny(operatorKey, "Division"))
|
||||
return "➗";
|
||||
if (ContainsAny(operatorKey, "Multiplication"))
|
||||
return "✕";
|
||||
if (ContainsAny(operatorKey, "Difference"))
|
||||
return "Δ";
|
||||
if (ContainsAny(operatorKey, "Integral"))
|
||||
return "∫";
|
||||
if (ContainsAny(operatorKey, "Or"))
|
||||
return "∨";
|
||||
if (ContainsAny(operatorKey, "Morphology"))
|
||||
return "⬚";
|
||||
if (ContainsAny(operatorKey, "Sobel", "Kirsch", "HorizontalEdge"))
|
||||
return "📐";
|
||||
if (ContainsAny(operatorKey, "Contour"))
|
||||
return "✏";
|
||||
if (ContainsAny(operatorKey, "Measurement"))
|
||||
return "📏";
|
||||
if (ContainsAny(operatorKey, "FillRate"))
|
||||
return "🧮";
|
||||
if (ContainsAny(operatorKey, "Void"))
|
||||
return "⚪";
|
||||
if (ContainsAny(operatorKey, "Ellipse"))
|
||||
return "⭕";
|
||||
if (ContainsAny(operatorKey, "PointToLine"))
|
||||
return "📍";
|
||||
if (ContainsAny(operatorKey, "Edge"))
|
||||
return "📐";
|
||||
|
||||
return GetCategoryIcon(category);
|
||||
}
|
||||
|
||||
private static bool ContainsAny(string value, params string[] terms)
|
||||
{
|
||||
foreach (var term in terms)
|
||||
{
|
||||
if (value.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,19 @@ namespace XplorePlane.Services
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var node = enabledNodes[step];
|
||||
var invalidParameters = node.Parameters
|
||||
.Where(p => !p.IsValueValid)
|
||||
.Select(p => p.DisplayName)
|
||||
.ToList();
|
||||
|
||||
if (invalidParameters.Count > 0)
|
||||
{
|
||||
throw new PipelineExecutionException(
|
||||
$"算子 '{node.DisplayName}' 存在无效参数:{string.Join("、", invalidParameters)}",
|
||||
node.Order,
|
||||
node.OperatorKey);
|
||||
}
|
||||
|
||||
var parameters = node.Parameters
|
||||
.Where(p => p.IsValueValid)
|
||||
.ToDictionary(p => p.Name, p => p.Value);
|
||||
|
||||
@@ -37,21 +37,7 @@ namespace XplorePlane.ViewModels
|
||||
private readonly IImageProcessingService _imageProcessingService;
|
||||
private string _searchText = string.Empty;
|
||||
|
||||
// 算子 Key -> (分类名, 分类图标, 算子图标) 映射
|
||||
private static readonly Dictionary<string, (string Category, string CatIcon, string OpIcon)> CategoryMap = new()
|
||||
{
|
||||
["GaussianBlur"] = ("滤波与平滑", "🔵", "🌀"),
|
||||
["GaussianBlur16"] = ("滤波与平滑", "🔵", "🌀"),
|
||||
["BandPassFilter"] = ("滤波与平滑", "🔵", "📶"),
|
||||
["ShockFilter"] = ("滤波与平滑", "🔵", "⚡"),
|
||||
["Contrast"] = ("增强与校正", "🟡", "🔆"),
|
||||
["Gamma"] = ("增强与校正", "🟡", "🌗"),
|
||||
["FlatFieldCorrection16"] = ("增强与校正", "🟡", "📐"),
|
||||
["Threshold"] = ("分割与阈值", "🟢", "📊"),
|
||||
["Division"] = ("分割与阈值", "🟢", "➗"),
|
||||
["Morphology"] = ("形态学与轮廓", "🔴", "🔲"),
|
||||
["Contour"] = ("形态学与轮廓", "🔴", "✏️"),
|
||||
};
|
||||
// UI 元数据(分类 + 图标)由 ProcessorUiMetadata 统一提供,保持工具箱与流水线图标一致
|
||||
|
||||
public OperatorToolboxViewModel(IImageProcessingService imageProcessingService)
|
||||
{
|
||||
@@ -82,10 +68,8 @@ namespace XplorePlane.ViewModels
|
||||
foreach (var key in _imageProcessingService.GetAvailableProcessors())
|
||||
{
|
||||
var displayName = _imageProcessingService.GetProcessorDisplayName(key);
|
||||
var (category, catIcon, opIcon) = CategoryMap.TryGetValue(key, out var info)
|
||||
? info
|
||||
: ("其他", "⚙", "⚙");
|
||||
AvailableOperators.Add(new OperatorDescriptor(key, displayName ?? key, opIcon, category, catIcon));
|
||||
var (category, categoryIcon, operatorIcon) = ProcessorUiMetadata.Get(key);
|
||||
AvailableOperators.Add(new OperatorDescriptor(key, displayName ?? key, operatorIcon, category, categoryIcon));
|
||||
}
|
||||
ApplyFilter();
|
||||
}
|
||||
@@ -122,9 +106,12 @@ namespace XplorePlane.ViewModels
|
||||
private static int GetCategoryOrder(string category) => category switch
|
||||
{
|
||||
"滤波与平滑" => 0,
|
||||
"增强与校正" => 1,
|
||||
"分割与阈值" => 2,
|
||||
"形态学与轮廓" => 3,
|
||||
"图像增强" => 1,
|
||||
"图像变换" => 2,
|
||||
"数学运算" => 3,
|
||||
"形态学处理" => 4,
|
||||
"边缘检测" => 5,
|
||||
"检测分析" => 6,
|
||||
_ => 99
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Win32;
|
||||
using Prism.Events;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
@@ -9,6 +10,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media.Imaging;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Events;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services;
|
||||
|
||||
@@ -22,6 +24,7 @@ namespace XplorePlane.ViewModels
|
||||
private readonly IImageProcessingService _imageProcessingService;
|
||||
private readonly IPipelineExecutionService _executionService;
|
||||
private readonly IPipelinePersistenceService _persistenceService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
private PipelineNodeViewModel _selectedNode;
|
||||
@@ -40,11 +43,13 @@ namespace XplorePlane.ViewModels
|
||||
IImageProcessingService imageProcessingService,
|
||||
IPipelineExecutionService executionService,
|
||||
IPipelinePersistenceService persistenceService,
|
||||
IEventAggregator eventAggregator,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
|
||||
_executionService = executionService ?? throw new ArgumentNullException(nameof(executionService));
|
||||
_persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService));
|
||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_logger = logger?.ForModule<PipelineEditorViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
PipelineNodes = new ObservableCollection<PipelineNodeViewModel>();
|
||||
@@ -60,9 +65,13 @@ namespace XplorePlane.ViewModels
|
||||
SaveAsPipelineCommand = new DelegateCommand(async () => await SaveAsPipelineAsync());
|
||||
DeletePipelineCommand = new DelegateCommand(async () => await DeletePipelineAsync());
|
||||
LoadPipelineCommand = new DelegateCommand(async () => await LoadPipelineAsync());
|
||||
LoadImageCommand = new DelegateCommand(LoadImage);
|
||||
OpenToolboxCommand = new DelegateCommand(OpenToolbox);
|
||||
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
|
||||
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
|
||||
|
||||
_eventAggregator.GetEvent<ManualImageLoadedEvent>()
|
||||
.Subscribe(OnManualImageLoaded);
|
||||
}
|
||||
|
||||
// ── State Properties ──────────────────────────────────────────
|
||||
@@ -88,6 +97,7 @@ namespace XplorePlane.ViewModels
|
||||
if (SetProperty(ref _sourceImage, value))
|
||||
{
|
||||
ExecutePipelineCommand.RaiseCanExecuteChanged();
|
||||
RaisePropertyChanged(nameof(DisplayImage));
|
||||
TriggerDebouncedExecution();
|
||||
}
|
||||
}
|
||||
@@ -96,8 +106,14 @@ namespace XplorePlane.ViewModels
|
||||
public BitmapSource PreviewImage
|
||||
{
|
||||
get => _previewImage;
|
||||
set => SetProperty(ref _previewImage, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _previewImage, value))
|
||||
RaisePropertyChanged(nameof(DisplayImage));
|
||||
}
|
||||
}
|
||||
|
||||
public BitmapSource DisplayImage => PreviewImage ?? SourceImage;
|
||||
|
||||
public string PipelineName
|
||||
{
|
||||
@@ -142,6 +158,7 @@ namespace XplorePlane.ViewModels
|
||||
public DelegateCommand SaveAsPipelineCommand { get; }
|
||||
public DelegateCommand DeletePipelineCommand { get; }
|
||||
public DelegateCommand LoadPipelineCommand { get; }
|
||||
public DelegateCommand LoadImageCommand { get; }
|
||||
|
||||
public DelegateCommand OpenToolboxCommand { get; }
|
||||
|
||||
@@ -183,7 +200,8 @@ namespace XplorePlane.ViewModels
|
||||
}
|
||||
|
||||
var displayName = _imageProcessingService.GetProcessorDisplayName(operatorKey) ?? operatorKey;
|
||||
var node = new PipelineNodeViewModel(operatorKey, displayName)
|
||||
var icon = ProcessorUiMetadata.GetOperatorIcon(operatorKey);
|
||||
var node = new PipelineNodeViewModel(operatorKey, displayName, icon)
|
||||
{
|
||||
Order = PipelineNodes.Count
|
||||
};
|
||||
@@ -296,6 +314,7 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
PreviewImage = result;
|
||||
StatusMessage = "流水线执行完成";
|
||||
PublishPipelinePreviewUpdated(result, StatusMessage);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -315,6 +334,84 @@ namespace XplorePlane.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadImage()
|
||||
{
|
||||
var dialog = new OpenFileDialog
|
||||
{
|
||||
Title = "加载图像",
|
||||
Filter = "图像文件|*.bmp;*.png;*.jpg;*.jpeg;*.tif;*.tiff|所有文件|*.*"
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
LoadImageFromFile(dialog.FileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"加载图像失败:{ex.Message}";
|
||||
_logger.Error(ex, "加载图像失败:{Path}", dialog.FileName);
|
||||
}
|
||||
}
|
||||
|
||||
internal void LoadImageFromFile(string filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
throw new ArgumentException("图像路径不能为空", nameof(filePath));
|
||||
|
||||
var bitmap = new BitmapImage();
|
||||
bitmap.BeginInit();
|
||||
bitmap.UriSource = new Uri(filePath, UriKind.Absolute);
|
||||
bitmap.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bitmap.EndInit();
|
||||
bitmap.Freeze();
|
||||
|
||||
SourceImage = bitmap;
|
||||
PreviewImage = bitmap;
|
||||
StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}";
|
||||
PublishManualImageLoaded(bitmap, filePath);
|
||||
}
|
||||
|
||||
internal void LoadImageFromBitmap(BitmapSource bitmap, string filePath, bool runPipeline = true)
|
||||
{
|
||||
if (bitmap == null)
|
||||
throw new ArgumentNullException(nameof(bitmap));
|
||||
|
||||
SourceImage = bitmap;
|
||||
PreviewImage = bitmap;
|
||||
StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}";
|
||||
PublishManualImageLoaded(bitmap, filePath);
|
||||
|
||||
if (runPipeline)
|
||||
TriggerDebouncedExecution();
|
||||
}
|
||||
|
||||
private void PublishManualImageLoaded(BitmapSource bitmap, string filePath)
|
||||
{
|
||||
_eventAggregator.GetEvent<ManualImageLoadedEvent>()
|
||||
.Publish(new ManualImageLoadedPayload(bitmap, filePath));
|
||||
}
|
||||
|
||||
private void PublishPipelinePreviewUpdated(BitmapSource bitmap, string statusMessage)
|
||||
{
|
||||
if (bitmap == null) return;
|
||||
|
||||
_eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
|
||||
.Publish(new PipelinePreviewUpdatedPayload(bitmap, statusMessage));
|
||||
}
|
||||
|
||||
private void OnManualImageLoaded(ManualImageLoadedPayload payload)
|
||||
{
|
||||
if (payload?.Image == null) return;
|
||||
if (ReferenceEquals(SourceImage, payload.Image)) return;
|
||||
|
||||
SourceImage = payload.Image;
|
||||
PreviewImage = payload.Image;
|
||||
StatusMessage = $"已加载图像:{payload.FileName}";
|
||||
}
|
||||
|
||||
private void CancelExecution()
|
||||
{
|
||||
_executionCts?.Cancel();
|
||||
@@ -442,7 +539,8 @@ namespace XplorePlane.ViewModels
|
||||
{
|
||||
var displayName = _imageProcessingService.GetProcessorDisplayName(nodeModel.OperatorKey)
|
||||
?? nodeModel.OperatorKey;
|
||||
var node = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName)
|
||||
var icon = ProcessorUiMetadata.GetOperatorIcon(nodeModel.OperatorKey);
|
||||
var node = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName, icon)
|
||||
{
|
||||
Order = nodeModel.Order,
|
||||
IsEnabled = nodeModel.IsEnabled
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Prism.Mvvm;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using XP.ImageProcessing.Core;
|
||||
|
||||
namespace XplorePlane.ViewModels
|
||||
@@ -16,7 +18,8 @@ namespace XplorePlane.ViewModels
|
||||
_value = parameter.Value;
|
||||
MinValue = parameter.MinValue;
|
||||
MaxValue = parameter.MaxValue;
|
||||
ParameterType = parameter.ValueType?.Name?.ToLower() switch
|
||||
Options = parameter.Options;
|
||||
ParameterType = parameter.ValueType?.Name?.ToLowerInvariant() switch
|
||||
{
|
||||
"int32" or "int" => "int",
|
||||
"double" => "double",
|
||||
@@ -30,6 +33,7 @@ namespace XplorePlane.ViewModels
|
||||
public string DisplayName { get; }
|
||||
public object MinValue { get; }
|
||||
public object MaxValue { get; }
|
||||
public string[]? Options { get; }
|
||||
public string ParameterType { get; }
|
||||
|
||||
public bool IsValueValid
|
||||
@@ -43,28 +47,160 @@ namespace XplorePlane.ViewModels
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _value, value))
|
||||
ValidateValue(value);
|
||||
var normalizedValue = NormalizeValue(value);
|
||||
if (SetProperty(ref _value, normalizedValue))
|
||||
ValidateValue(normalizedValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateValue(object value)
|
||||
{
|
||||
if (value == null || MinValue == null || MaxValue == null)
|
||||
if (value == null)
|
||||
{
|
||||
IsValueValid = true;
|
||||
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 static string NormalizeNumericText(string value)
|
||||
{
|
||||
return value.Trim().TrimEnd('、', ',', ',', '。', '.', ';', ';', ':', ':');
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
double dVal = Convert.ToDouble(value);
|
||||
double dMin = Convert.ToDouble(MinValue);
|
||||
double dMax = Convert.ToDouble(MaxValue);
|
||||
IsValueValid = dVal >= dMin && dVal <= dMax;
|
||||
result = Convert.ToInt32(value, CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
IsValueValid = true; // é�žæ•°å€¼ç±»åž‹ä¸�å�šèŒƒå›´æ ¡éª?
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using Prism.Ioc;
|
||||
using Prism.Mvvm;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Configuration;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Media.Imaging;
|
||||
using XplorePlane.Events;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Common.PdfViewer.Interfaces;
|
||||
using XP.Hardware.MotionControl.Abstractions;
|
||||
@@ -16,6 +20,7 @@ namespace XplorePlane.ViewModels
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly IContainerProvider _containerProvider;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private string _licenseInfo = "当前时间";
|
||||
|
||||
public string LicenseInfo
|
||||
@@ -36,6 +41,7 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
// 窗口打开命令
|
||||
public DelegateCommand OpenImageProcessingCommand { get; }
|
||||
public DelegateCommand LoadImageCommand { get; }
|
||||
public DelegateCommand OpenPipelineEditorCommand { get; }
|
||||
public DelegateCommand OpenCncEditorCommand { get; }
|
||||
public DelegateCommand OpenMatrixEditorCommand { get; }
|
||||
@@ -64,10 +70,11 @@ namespace XplorePlane.ViewModels
|
||||
private Window _toolboxWindow;
|
||||
private Window _raySourceConfigWindow;
|
||||
|
||||
public MainViewModel(ILoggerService logger, IContainerProvider containerProvider)
|
||||
public MainViewModel(ILoggerService logger, IContainerProvider containerProvider, IEventAggregator eventAggregator)
|
||||
{
|
||||
_logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
_containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider));
|
||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
|
||||
NavigationTree = new ObservableCollection<object>();
|
||||
|
||||
@@ -81,6 +88,7 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
// 窗口打开命令
|
||||
OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理"));
|
||||
LoadImageCommand = new DelegateCommand(ExecuteLoadImage);
|
||||
OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器"));
|
||||
OpenCncEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.CncEditorWindow(), "CNC 编辑器"));
|
||||
OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编排"));
|
||||
@@ -260,6 +268,36 @@ namespace XplorePlane.ViewModels
|
||||
() => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "射线源配置");
|
||||
}
|
||||
|
||||
private void ExecuteLoadImage()
|
||||
{
|
||||
var dialog = new OpenFileDialog
|
||||
{
|
||||
Title = "加载图像",
|
||||
Filter = "图像文件|*.bmp;*.png;*.jpg;*.jpeg;*.tif;*.tiff|所有文件|*.*"
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var bitmap = new BitmapImage();
|
||||
bitmap.BeginInit();
|
||||
bitmap.UriSource = new Uri(dialog.FileName, UriKind.Absolute);
|
||||
bitmap.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bitmap.EndInit();
|
||||
bitmap.Freeze();
|
||||
|
||||
_eventAggregator.GetEvent<ManualImageLoadedEvent>()
|
||||
.Publish(new ManualImageLoadedPayload(bitmap, dialog.FileName));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "加载图像失败:{Path}", dialog.FileName);
|
||||
MessageBox.Show($"加载图像失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteWarmUp()
|
||||
{
|
||||
var messageBoxResult = MessageBox.Show("确认执行射线源暖机操作?", "暖机",
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Windows.Media.Imaging;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.Detector.Abstractions;
|
||||
using XP.Hardware.Detector.Abstractions.Events;
|
||||
using XplorePlane.Events;
|
||||
|
||||
namespace XplorePlane.ViewModels
|
||||
{
|
||||
@@ -39,6 +40,10 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
eventAggregator.GetEvent<ImageCapturedEvent>()
|
||||
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
|
||||
eventAggregator.GetEvent<ManualImageLoadedEvent>()
|
||||
.Subscribe(OnManualImageLoaded, ThreadOption.UIThread);
|
||||
eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
|
||||
.Subscribe(OnPipelinePreviewUpdated, ThreadOption.UIThread);
|
||||
}
|
||||
|
||||
private void OnImageCaptured(ImageCapturedEventArgs args)
|
||||
@@ -75,6 +80,22 @@ namespace XplorePlane.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void OnManualImageLoaded(ManualImageLoadedPayload payload)
|
||||
{
|
||||
if (payload?.Image == null) return;
|
||||
|
||||
ImageSource = payload.Image;
|
||||
ImageInfo = $"手动加载: {payload.FileName}";
|
||||
}
|
||||
|
||||
private void OnPipelinePreviewUpdated(PipelinePreviewUpdatedPayload payload)
|
||||
{
|
||||
if (payload?.Image == null) return;
|
||||
|
||||
ImageSource = payload.Image;
|
||||
ImageInfo = payload.StatusMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 16 位灰度数据线性拉伸为 8 位 BitmapSource(委托给 XP.Common 通用转换器)
|
||||
/// </summary>
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="库版本信息"
|
||||
Width="850"
|
||||
Width="400"
|
||||
Height="600"
|
||||
ResizeMode="CanResizeWithGrip"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
@@ -4,11 +4,9 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
d:DesignHeight="700"
|
||||
d:DesignWidth="350"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
@@ -48,13 +46,14 @@
|
||||
<Grid.RowDefinitions>
|
||||
<!-- Row 0: 工具栏 -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- Row 1: 流水线节点列表 -->
|
||||
<RowDefinition Height="3*" />
|
||||
<!-- Row 2: 分隔线 -->
|
||||
|
||||
<!-- Row 2: 流水线节点列表 -->
|
||||
<RowDefinition Height="2*" MinHeight="180" />
|
||||
<!-- Row 3: 分隔线 -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- Row 3: 参数面板 -->
|
||||
<!-- Row 4: 参数面板 -->
|
||||
<RowDefinition Height="2*" MinHeight="80" />
|
||||
<!-- Row 4: 状态栏 -->
|
||||
<!-- Row 5: 状态栏 -->
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
@@ -90,6 +89,14 @@
|
||||
Content="加载"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="加载流水线" />
|
||||
<!--
|
||||
<Button
|
||||
Width="64"
|
||||
Command="{Binding LoadImageCommand}"
|
||||
Content="加载图像"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="加载输入图像" />
|
||||
-->
|
||||
<Button
|
||||
Command="{Binding ExecutePipelineCommand}"
|
||||
Content="▶"
|
||||
@@ -109,6 +116,9 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- 流水线节点列表(拖拽目标) -->
|
||||
<ListBox
|
||||
x:Name="PipelineListBox"
|
||||
@@ -164,7 +174,7 @@
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="13"
|
||||
Text="⚙" />
|
||||
Text="{Binding IconPath}" />
|
||||
</Border>
|
||||
|
||||
<!-- 算子名称 -->
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Prism.Ioc;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -22,6 +23,18 @@ namespace XplorePlane.Views
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not PipelineEditorViewModel)
|
||||
{
|
||||
try
|
||||
{
|
||||
DataContext = ContainerLocator.Current?.Resolve<PipelineEditorViewModel>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Error(ex, "PipelineEditorViewModel 解析失败");
|
||||
}
|
||||
}
|
||||
|
||||
_logger?.Info("PipelineEditorView DataContext 类型={Type}",
|
||||
DataContext?.GetType().Name);
|
||||
|
||||
@@ -50,7 +63,7 @@ namespace XplorePlane.Views
|
||||
|
||||
if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
|
||||
{
|
||||
_logger?.Warn("Drop 事件触发但数据中无 {Format}", OperatorToolboxView.DragFormat);
|
||||
_logger?.Warn("Drop 事件触发但数据中没有 {Format}", OperatorToolboxView.DragFormat);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,46 @@
|
||||
<Window x:Class="XplorePlane.Views.PipelineEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:roi="clr-namespace:XP.ImageProcessing.RoiControl.Controls;assembly=XP.ImageProcessing.RoiControl"
|
||||
xmlns:views="clr-namespace:XplorePlane.Views"
|
||||
Title="流水线编辑器"
|
||||
Width="700" Height="750"
|
||||
Width="1200"
|
||||
Height="750"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ShowInTaskbar="False">
|
||||
<Grid>
|
||||
<Grid Background="#F3F3F3">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="200" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="240" MinWidth="200" />
|
||||
<ColumnDefinition Width="*" MinWidth="400" />
|
||||
<ColumnDefinition Width="250" MinWidth="250" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<views:OperatorToolboxView Grid.Column="0" />
|
||||
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" Background="#E0E0E0" />
|
||||
<views:PipelineEditorView Grid.Column="2" />
|
||||
|
||||
<Border Grid.Column="0"
|
||||
Margin="8"
|
||||
Background="White"
|
||||
BorderBrush="#D0D0D0"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<views:OperatorToolboxView />
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="1"
|
||||
Margin="8,8,4,8"
|
||||
Background="White"
|
||||
BorderBrush="#D0D0D0"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<roi:PolygonRoiCanvas ImageSource="{Binding DisplayImage}"
|
||||
Background="White" />
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="2"
|
||||
Margin="4,8,8,8"
|
||||
Background="White"
|
||||
BorderBrush="#D0D0D0"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<views:PipelineEditorView />
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,4 +1,7 @@
|
||||
using Prism.Ioc;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using XplorePlane.ViewModels;
|
||||
|
||||
namespace XplorePlane.Views
|
||||
{
|
||||
@@ -7,6 +10,15 @@ namespace XplorePlane.Views
|
||||
public PipelineEditorWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
try
|
||||
{
|
||||
DataContext = ContainerLocator.Current?.Resolve<PipelineEditorViewModel>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,11 @@
|
||||
</StackPanel>
|
||||
</telerik:RadRibbonGroup>
|
||||
<telerik:RadRibbonGroup Header="快捷工具">
|
||||
|
||||
<telerik:RadRibbonButton
|
||||
Command="{Binding LoadImageCommand}"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/open.png"
|
||||
Text="加载图像" />
|
||||
<!-- 快捷工具: 上下两列,带文字 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
@@ -319,7 +323,7 @@
|
||||
telerik:ScreenTip.Title="模块"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/Module.png"
|
||||
Text="模块" />
|
||||
Text="检测模块" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="全部保存"
|
||||
Size="Medium"
|
||||
@@ -331,12 +335,12 @@
|
||||
telerik:ScreenTip.Title="消息"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/message.png"
|
||||
Text="消息" />
|
||||
Text="消息弹窗" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="等待"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/wait.png"
|
||||
Text="等待" />
|
||||
Text="插入等待" />
|
||||
</StackPanel>
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
@@ -384,7 +388,7 @@
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/spiral.png" />
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
<!--
|
||||
<telerik:RadRibbonGroup Header="图像处理">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
@@ -395,6 +399,7 @@
|
||||
SmallImage="/Assets/Icons/workflow.png"
|
||||
Text="流水线编辑器" />
|
||||
</telerik:RadRibbonGroup>
|
||||
-->
|
||||
</telerik:RadRibbonTab>
|
||||
<telerik:RadRibbonTab Header="关于">
|
||||
<telerik:RadRibbonGroup Header="关于">
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:roi="clr-namespace:XP.ImageProcessing.RoiControl.Controls;assembly=XP.ImageProcessing.RoiControl"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
d:DesignHeight="400"
|
||||
d:DesignWidth="600"
|
||||
@@ -22,14 +23,10 @@
|
||||
FontWeight="SemiBold" Foreground="#333333" Text="实时图像" />
|
||||
</Border>
|
||||
|
||||
<!-- 图像显示区域,支持滚动 -->
|
||||
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
|
||||
<Image Source="{Binding ImageSource}"
|
||||
Stretch="None"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
RenderOptions.BitmapScalingMode="NearestNeighbor" />
|
||||
</ScrollViewer>
|
||||
<!-- 图像显示区域,支持滚动、缩放和ROI -->
|
||||
<roi:PolygonRoiCanvas Grid.Row="1"
|
||||
ImageSource="{Binding ImageSource}"
|
||||
Background="White" />
|
||||
|
||||
<!-- 图像信息栏 -->
|
||||
<Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0">
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Page Remove="MainWindow.xaml" />
|
||||
<Page Remove="Views\Main\MainWindowB.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- NuGet 包引用 -->
|
||||
@@ -144,6 +145,7 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<Link>Libs\Hardware\zh-TW\%(Filename)%(Extension)</Link>
|
||||
</None>
|
||||
<Compile Remove="Views\Main\MainWindowB.xaml.cs" />
|
||||
|
||||
<Resource Include="XplorerPlane.ico" />
|
||||
|
||||
@@ -164,7 +166,7 @@
|
||||
<ProjectReference Include="..\XP.Camera\XP.Camera.csproj" />
|
||||
</ItemGroup>
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="xcopy $(SolutionDir)ExternalLibraries\*.* $(TargetDir) /d /y" />
|
||||
<Exec Command="xcopy $(SolutionDir)ExternalLibraries\Models $(TargetDir)Models\ /d /y /i" />
|
||||
<Exec Command="xcopy "$(MSBuildProjectDirectory)\..\ExternalLibraries\*.*" "$(TargetDir)" /d /y" />
|
||||
<Exec Command="xcopy "$(MSBuildProjectDirectory)\..\ExternalLibraries\Models" "$(TargetDir)Models\" /d /y /i" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -1,78 +0,0 @@
|
||||
---------------------------------------------------------------
|
||||
__ __ _ _____ _
|
||||
\ \ / / | | | __ \| |
|
||||
\ V / _ __ | | ___ _ __ ___| |__) | | __ _ _ __ ___
|
||||
> < | '_ \| |/ _ \| '__/ _ \ ___/| |/ _` | '_ \ / _ \
|
||||
/ . \| |_) | | (_) | | | __/ | | | (_| | | | | __/
|
||||
/_/ \_\ .__/|_|\___/|_| \___|_| |_|\__,_|_| |_|\___|
|
||||
| |
|
||||
|_|
|
||||
---------------------------------------------------------------
|
||||
|
||||
|
||||
2026.3.14
|
||||
----------------------
|
||||
1、主页面的布局与拆分 √
|
||||
2、硬件层射线源的集成 √
|
||||
3、图像层集成,包括复刻一个示例界面,优化界面布局及算子中文 √
|
||||
4、浮动图像处理工具箱调研 √
|
||||
5、修复图像工具箱拖拽事件,流水线列表没有生成对应的控件 √
|
||||
|
||||
2026.3.16
|
||||
----------------------
|
||||
1、优化图像处理窗体的页面布局,简洁清晰 √
|
||||
2、新增打开图像工具箱(修复DataContext问题) √
|
||||
3、对主界面B方案进行优化 √
|
||||
|
||||
2026.3.17
|
||||
----------------------
|
||||
1、对界面设计进行优化 ,增加了扫描模式,移除了探测器设置,增加底部工具栏 √
|
||||
|
||||
|
||||
2026.3.18
|
||||
----------------------
|
||||
1、全局数据结构的考虑与设计(多个窗体可以调用公共的数据,如射线源状态,探测器状态,运动位置,图像等) √
|
||||
2、将计划窗体默认隐藏,只有CNC状态下展开 √
|
||||
|
||||
|
||||
2026.3.20
|
||||
----------------------
|
||||
1、软件主界面设计讨论,暂定初稿,给出效果图设计 √
|
||||
2、日志该用XP.Common库和多语言的学习 √
|
||||
|
||||
|
||||
2026.3.26
|
||||
----------------------
|
||||
1、各窗体间数据流的传递,全局数据结构的设计(包括一个基本的说明文档)√
|
||||
2、将telerik 升级到 2024.1.408.310;调整界面和主题;引入 硬件层依赖 √
|
||||
3、图像算子流程文件,保存文件后缀 .imw, image process workflow 缩写 √
|
||||
4、CNC保存文件后缀为.xp, 表示 XplorePlane CNC file 的缩写
|
||||
5、硬件层射线源控件的初步集成(采用库层面的自定义控件方式) √
|
||||
PrismBootstrapper 的执行顺序是:RegisterTypes() → ConfigureModuleCatalog() → InitializeModules() → CreateShell()
|
||||
|
||||
|
||||
|
||||
|
||||
2026.3.27
|
||||
----------------------
|
||||
CNC及矩阵功能的设计与实现,包含以下功能:
|
||||
1、CNC功能设计与实现,包含以下功能:
|
||||
a. CNC状态的定义和管理
|
||||
b. CNC界面设计与实现
|
||||
c. CNC相关数据的传递和处理
|
||||
2、CNC相关的编排工具,如插入节点,插入位置,图像模块,等
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
TO-DO
|
||||
----------------------
|
||||
Reference in New Issue
Block a user