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、