From 5fa6f4025d334fdc8314c783d8807ef94fe7f040 Mon Sep 17 00:00:00 2001 From: "DESKTOP-VO9ISA2\\zhengxuan.zhang" Date: Mon, 20 Apr 2026 09:37:28 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9B=BE=E5=83=8F?= =?UTF-8?q?=E7=AE=97=E5=AD=90=E5=B7=A5=E5=85=B7=E7=AE=B1=E7=9A=84=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=E4=B8=8E=E6=B5=81=E7=A8=8B=E5=9B=BE=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E4=BF=9D=E6=8C=81=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Pipeline/PipelineEditorViewModelTests.cs | 14 +++++++++- .../OperatorToolboxViewModel.cs | 24 +++-------------- .../PipelineEditorViewModel.cs | 8 +++--- .../ImageProcessing/PipelineEditorView.xaml | 4 +-- XplorePlane/XplorePlane.csproj | 6 ++--- XplorePlane/readme.txt | 26 +++++++++++++++---- 6 files changed, 48 insertions(+), 34 deletions(-) diff --git a/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs b/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs index 19eb3b6..cb13fd8 100644 --- a/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs +++ b/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs @@ -44,6 +44,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() { @@ -276,4 +288,4 @@ namespace XplorePlane.Tests.Pipeline Assert.True(vmMax.IsValueValid); } } -} \ No newline at end of file +} diff --git a/XplorePlane/ViewModels/ImageProcessing/OperatorToolboxViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/OperatorToolboxViewModel.cs index b64ee03..a39b047 100644 --- a/XplorePlane/ViewModels/ImageProcessing/OperatorToolboxViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/OperatorToolboxViewModel.cs @@ -37,21 +37,7 @@ namespace XplorePlane.ViewModels private readonly IImageProcessingService _imageProcessingService; private string _searchText = string.Empty; - // 算子 Key -> (分类名, 分类图标, 算子图标) 映射 - private static readonly Dictionary CategoryMap = new() - { - ["GaussianBlur"] = ("滤波与平滑", "🔵", "🌀"), - ["GaussianBlur16"] = ("滤波与平滑", "🔵", "🌀"), - ["BandPassFilter"] = ("滤波与平滑", "🔵", "📶"), - ["ShockFilter"] = ("滤波与平滑", "🔵", "⚡"), - ["Contrast"] = ("增强与校正", "🟡", "🔆"), - ["Gamma"] = ("增强与校正", "🟡", "🌗"), - ["FlatFieldCorrection16"] = ("增强与校正", "🟡", "📐"), - ["Threshold"] = ("分割与阈值", "🟢", "📊"), - ["Division"] = ("分割与阈值", "🟢", "➗"), - ["Morphology"] = ("形态学与轮廓", "🔴", "🔲"), - ["Contour"] = ("形态学与轮廓", "🔴", "✏️"), - }; + // UI 元数据(分类 + 图标)由 OperatorUiMetadata 统一提供,保持工具箱与流水线图标一致 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) = OperatorUiMetadata.Get(key); + AvailableOperators.Add(new OperatorDescriptor(key, displayName ?? key, operatorIcon, category, categoryIcon)); } ApplyFilter(); } @@ -128,4 +112,4 @@ namespace XplorePlane.ViewModels _ => 99 }; } -} \ No newline at end of file +} diff --git a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs index c84bba7..7d1162a 100644 --- a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs @@ -183,7 +183,8 @@ namespace XplorePlane.ViewModels } var displayName = _imageProcessingService.GetProcessorDisplayName(operatorKey) ?? operatorKey; - var node = new PipelineNodeViewModel(operatorKey, displayName) + var icon = OperatorUiMetadata.GetOperatorIcon(operatorKey); + var node = new PipelineNodeViewModel(operatorKey, displayName, icon) { Order = PipelineNodes.Count }; @@ -442,7 +443,8 @@ namespace XplorePlane.ViewModels { var displayName = _imageProcessingService.GetProcessorDisplayName(nodeModel.OperatorKey) ?? nodeModel.OperatorKey; - var node = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName) + var icon = OperatorUiMetadata.GetOperatorIcon(nodeModel.OperatorKey); + var node = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName, icon) { Order = nodeModel.Order, IsEnabled = nodeModel.IsEnabled @@ -515,4 +517,4 @@ namespace XplorePlane.ViewModels return dir; } } -} \ No newline at end of file +} diff --git a/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml b/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml index 4c80928..65378df 100644 --- a/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml +++ b/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml @@ -164,7 +164,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="13" - Text="⚙" /> + Text="{Binding IconPath}" /> @@ -314,4 +314,4 @@ - \ No newline at end of file + diff --git a/XplorePlane/XplorePlane.csproj b/XplorePlane/XplorePlane.csproj index 1b10f08..62361c6 100644 --- a/XplorePlane/XplorePlane.csproj +++ b/XplorePlane/XplorePlane.csproj @@ -164,7 +164,7 @@ - - + + - \ No newline at end of file + diff --git a/XplorePlane/readme.txt b/XplorePlane/readme.txt index 4655797..0763ea3 100644 --- a/XplorePlane/readme.txt +++ b/XplorePlane/readme.txt @@ -46,16 +46,14 @@ 1、各窗体间数据流的传递,全局数据结构的设计(包括一个基本的说明文档)√ 2、将telerik 升级到 2024.1.408.310;调整界面和主题;引入 硬件层依赖 √ 3、图像算子流程文件,保存文件后缀 .imw, image process workflow 缩写 √ -4、CNC保存文件后缀为.xp, 表示 XplorePlane CNC file 的缩写 +4、CNC保存文件后缀为.xp, 表示 XplorePlane CNC file 的缩写 √ 5、硬件层射线源控件的初步集成(采用库层面的自定义控件方式) √ PrismBootstrapper 的执行顺序是:RegisterTypes() → ConfigureModuleCatalog() → InitializeModules() → CreateShell() - - -2026.3.27 +2026.4.16 ---------------------- -CNC及矩阵功能的设计与实现,包含以下功能: +CNC及矩阵功能的设计与评审,包含以下功能: √ 1、CNC功能设计与实现,包含以下功能: a. CNC状态的定义和管理 b. CNC界面设计与实现 @@ -66,6 +64,24 @@ CNC及矩阵功能的设计与实现,包含以下功能: +2026.4.20 +---------------------- +1、图像算子工具箱的图标 √ +2、最新的图像算子集成 +3、修复流程图编辑器,并屏蔽 +4、主页面加载图像的功能 + + + + + + + + + + + + From b16d5920874025e8dd697d9070262c56e989a02a Mon Sep 17 00:00:00 2001 From: "DESKTOP-VO9ISA2\\zhengxuan.zhang" Date: Mon, 20 Apr 2026 09:54:32 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E6=9C=80=E6=96=B0=E7=9A=84=E5=9B=BE?= =?UTF-8?q?=E5=83=8F=E7=AE=97=E5=AD=90=E9=9B=86=E6=88=90=E5=88=B0=E5=9B=BE?= =?UTF-8?q?=E5=83=8F=E5=B7=A5=E5=85=B7=E7=AE=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ImageProcessing/ImageProcessingService.cs | 195 +++++++++++++++--- .../OperatorToolboxViewModel.cs | 13 +- .../PipelineEditorViewModel.cs | 4 +- XplorePlane/readme.txt | 5 +- 4 files changed, 183 insertions(+), 34 deletions(-) diff --git a/XplorePlane/Services/ImageProcessing/ImageProcessingService.cs b/XplorePlane/Services/ImageProcessing/ImageProcessingService.cs index a539c42..7025567 100644 --- a/XplorePlane/Services/ImageProcessing/ImageProcessingService.cs +++ b/XplorePlane/Services/ImageProcessing/ImageProcessingService.cs @@ -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() ?? throw new ArgumentNullException(nameof(logger)); - _processorRegistry = new ConcurrentDictionary(); - RegisterBuiltInProcessors(); + _processorRegistry = new ConcurrentDictionary(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++; + } + } + + _logger.Info( + "Discovered {ProcessorCount} image processors from {AssemblyCount} assemblies", + discoveredCount, + assemblies.Count); } - public IReadOnlyList GetAvailableProcessors() => new List(_processorRegistry.Keys).AsReadOnly(); + private IReadOnlyList LoadCandidateAssemblies() + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic) + .ToList(); + + var loadedNames = new HashSet( + 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 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(); + + 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 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 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 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,7 +275,8 @@ namespace XplorePlane.Services if (processor is IDisposable disposable) disposable.Dispose(); } + _processorRegistry.Clear(); } } -} \ No newline at end of file +} diff --git a/XplorePlane/ViewModels/ImageProcessing/OperatorToolboxViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/OperatorToolboxViewModel.cs index a39b047..2eb128c 100644 --- a/XplorePlane/ViewModels/ImageProcessing/OperatorToolboxViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/OperatorToolboxViewModel.cs @@ -37,7 +37,7 @@ namespace XplorePlane.ViewModels private readonly IImageProcessingService _imageProcessingService; private string _searchText = string.Empty; - // UI 元数据(分类 + 图标)由 OperatorUiMetadata 统一提供,保持工具箱与流水线图标一致 + // UI 元数据(分类 + 图标)由 ProcessorUiMetadata 统一提供,保持工具箱与流水线图标一致 public OperatorToolboxViewModel(IImageProcessingService imageProcessingService) { @@ -68,7 +68,7 @@ namespace XplorePlane.ViewModels foreach (var key in _imageProcessingService.GetAvailableProcessors()) { var displayName = _imageProcessingService.GetProcessorDisplayName(key); - var (category, categoryIcon, operatorIcon) = OperatorUiMetadata.Get(key); + var (category, categoryIcon, operatorIcon) = ProcessorUiMetadata.Get(key); AvailableOperators.Add(new OperatorDescriptor(key, displayName ?? key, operatorIcon, category, categoryIcon)); } ApplyFilter(); @@ -106,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 }; } diff --git a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs index 7d1162a..064f686 100644 --- a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs @@ -183,7 +183,7 @@ namespace XplorePlane.ViewModels } var displayName = _imageProcessingService.GetProcessorDisplayName(operatorKey) ?? operatorKey; - var icon = OperatorUiMetadata.GetOperatorIcon(operatorKey); + var icon = ProcessorUiMetadata.GetOperatorIcon(operatorKey); var node = new PipelineNodeViewModel(operatorKey, displayName, icon) { Order = PipelineNodes.Count @@ -443,7 +443,7 @@ namespace XplorePlane.ViewModels { var displayName = _imageProcessingService.GetProcessorDisplayName(nodeModel.OperatorKey) ?? nodeModel.OperatorKey; - var icon = OperatorUiMetadata.GetOperatorIcon(nodeModel.OperatorKey); + var icon = ProcessorUiMetadata.GetOperatorIcon(nodeModel.OperatorKey); var node = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName, icon) { Order = nodeModel.Order, diff --git a/XplorePlane/readme.txt b/XplorePlane/readme.txt index 0763ea3..b866ee3 100644 --- a/XplorePlane/readme.txt +++ b/XplorePlane/readme.txt @@ -67,9 +67,10 @@ CNC及矩阵功能的设计与评审,包含以下功能: √ 2026.4.20 ---------------------- 1、图像算子工具箱的图标 √ -2、最新的图像算子集成 -3、修复流程图编辑器,并屏蔽 +2、最新的图像算子集成到图像工具箱 +3、修复流程图编辑器 4、主页面加载图像的功能 +5、 From 1c6c2ac67550fed2c958af738655bd7f0b78f114 Mon Sep 17 00:00:00 2001 From: "zhengxuan.zhang" Date: Mon, 20 Apr 2026 11:07:00 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E5=9B=BE=E7=BC=96=E8=BE=91=E5=99=A8=E7=95=8C=E9=9D=A2=E5=8F=8A?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Pipeline/PipelineEditorViewModelTests.cs | 33 ++++ .../Services/ImageProcessingServiceTests.cs | 28 +++ XplorePlane/App.xaml.cs | 1 - XplorePlane/Properties/AssemblyInfo.cs | 3 + .../ImageProcessing/ProcessorUiMetadata.cs | 161 ++++++++++++++++++ .../PipelineEditorViewModel.cs | 50 +++++- .../{Main => Help}/LibraryVersionsView.xaml | 0 .../LibraryVersionsView.xaml.cs | 0 .../{Main => Help}/LibraryVersionsWindow.xaml | 2 +- .../LibraryVersionsWindow.xaml.cs | 0 .../ImageProcessing/PipelineEditorView.xaml | 22 ++- .../PipelineEditorView.xaml.cs | 17 +- .../ImageProcessing/PipelineEditorWindow.xaml | 45 ++++- .../PipelineEditorWindow.xaml.cs | 14 +- XplorePlane/Views/Main/ViewportPanelView.xaml | 13 +- .../{ => Setting}/CameraSettingsWindow.xaml | 0 .../CameraSettingsWindow.xaml.cs | 0 XplorePlane/XplorePlane.csproj | 2 + XplorePlane/readme.txt | 4 +- 19 files changed, 363 insertions(+), 32 deletions(-) create mode 100644 XplorePlane.Tests/Services/ImageProcessingServiceTests.cs create mode 100644 XplorePlane/Properties/AssemblyInfo.cs create mode 100644 XplorePlane/Services/ImageProcessing/ProcessorUiMetadata.cs rename XplorePlane/Views/{Main => Help}/LibraryVersionsView.xaml (100%) rename XplorePlane/Views/{Main => Help}/LibraryVersionsView.xaml.cs (100%) rename XplorePlane/Views/{Main => Help}/LibraryVersionsWindow.xaml (99%) rename XplorePlane/Views/{Main => Help}/LibraryVersionsWindow.xaml.cs (100%) rename XplorePlane/Views/{ => Setting}/CameraSettingsWindow.xaml (100%) rename XplorePlane/Views/{ => Setting}/CameraSettingsWindow.xaml.cs (100%) diff --git a/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs b/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs index cb13fd8..33b250f 100644 --- a/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs +++ b/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs @@ -1,4 +1,7 @@ using Moq; +using System; +using System.IO; +using System.Windows.Media.Imaging; using XP.Common.Logging.Interfaces; using XplorePlane.Models; using XplorePlane.Services; @@ -104,6 +107,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] diff --git a/XplorePlane.Tests/Services/ImageProcessingServiceTests.cs b/XplorePlane.Tests/Services/ImageProcessingServiceTests.cs new file mode 100644 index 0000000..6afc85d --- /dev/null +++ b/XplorePlane.Tests/Services/ImageProcessingServiceTests.cs @@ -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(); + logger.Setup(l => l.ForModule()).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}."); + } + } +} diff --git a/XplorePlane/App.xaml.cs b/XplorePlane/App.xaml.cs index d3e02c0..9005fb3 100644 --- a/XplorePlane/App.xaml.cs +++ b/XplorePlane/App.xaml.cs @@ -268,7 +268,6 @@ namespace XplorePlane // 注册视图和视图模型 containerRegistry.RegisterForNavigation(); - containerRegistry.RegisterForNavigation(); containerRegistry.Register(); containerRegistry.RegisterSingleton(); diff --git a/XplorePlane/Properties/AssemblyInfo.cs b/XplorePlane/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..f2bf64e --- /dev/null +++ b/XplorePlane/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("XplorePlane.Tests")] diff --git a/XplorePlane/Services/ImageProcessing/ProcessorUiMetadata.cs b/XplorePlane/Services/ImageProcessing/ProcessorUiMetadata.cs new file mode 100644 index 0000000..69b40c3 --- /dev/null +++ b/XplorePlane/Services/ImageProcessing/ProcessorUiMetadata.cs @@ -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; + } + } +} diff --git a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs index 064f686..b20b57c 100644 --- a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs @@ -60,6 +60,7 @@ 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(MoveNodeUp); MoveNodeDownCommand = new DelegateCommand(MoveNodeDown); @@ -88,6 +89,7 @@ namespace XplorePlane.ViewModels if (SetProperty(ref _sourceImage, value)) { ExecutePipelineCommand.RaiseCanExecuteChanged(); + RaisePropertyChanged(nameof(DisplayImage)); TriggerDebouncedExecution(); } } @@ -96,9 +98,15 @@ 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 { get => _pipelineName; @@ -142,6 +150,7 @@ namespace XplorePlane.ViewModels public DelegateCommand SaveAsPipelineCommand { get; } public DelegateCommand DeletePipelineCommand { get; } public DelegateCommand LoadPipelineCommand { get; } + public DelegateCommand LoadImageCommand { get; } public DelegateCommand OpenToolboxCommand { get; } @@ -316,6 +325,45 @@ 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)}"; + } + private void CancelExecution() { _executionCts?.Cancel(); diff --git a/XplorePlane/Views/Main/LibraryVersionsView.xaml b/XplorePlane/Views/Help/LibraryVersionsView.xaml similarity index 100% rename from XplorePlane/Views/Main/LibraryVersionsView.xaml rename to XplorePlane/Views/Help/LibraryVersionsView.xaml diff --git a/XplorePlane/Views/Main/LibraryVersionsView.xaml.cs b/XplorePlane/Views/Help/LibraryVersionsView.xaml.cs similarity index 100% rename from XplorePlane/Views/Main/LibraryVersionsView.xaml.cs rename to XplorePlane/Views/Help/LibraryVersionsView.xaml.cs diff --git a/XplorePlane/Views/Main/LibraryVersionsWindow.xaml b/XplorePlane/Views/Help/LibraryVersionsWindow.xaml similarity index 99% rename from XplorePlane/Views/Main/LibraryVersionsWindow.xaml rename to XplorePlane/Views/Help/LibraryVersionsWindow.xaml index 55347bd..d6abfc1 100644 --- a/XplorePlane/Views/Main/LibraryVersionsWindow.xaml +++ b/XplorePlane/Views/Help/LibraryVersionsWindow.xaml @@ -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"> diff --git a/XplorePlane/Views/Main/LibraryVersionsWindow.xaml.cs b/XplorePlane/Views/Help/LibraryVersionsWindow.xaml.cs similarity index 100% rename from XplorePlane/Views/Main/LibraryVersionsWindow.xaml.cs rename to XplorePlane/Views/Help/LibraryVersionsWindow.xaml.cs diff --git a/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml b/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml index 65378df..837f1e0 100644 --- a/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml +++ b/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml @@ -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"> @@ -48,13 +46,14 @@ - - - + + + + - + - + @@ -90,6 +89,12 @@ Content="加载" Style="{StaticResource ToolbarBtn}" ToolTip="加载流水线" /> +