From 1c6c2ac67550fed2c958af738655bd7f0b78f114 Mon Sep 17 00:00:00 2001 From: "zhengxuan.zhang" Date: Mon, 20 Apr 2026 11:07:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=81=E7=A8=8B=E5=9B=BE?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=E7=95=8C=E9=9D=A2=E5=8F=8A=E5=88=9D?= =?UTF-8?q?=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="加载流水线" /> +