最新的图像算子集成到图像工具箱

This commit is contained in:
DESKTOP-VO9ISA2\zhengxuan.zhang
2026-04-20 09:54:32 +08:00
parent 5fa6f4025d
commit b16d592087
4 changed files with 183 additions and 34 deletions
@@ -1,12 +1,15 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XP.ImageProcessing.Core; using XP.ImageProcessing.Core;
using XP.ImageProcessing.Processors;
namespace XplorePlane.Services namespace XplorePlane.Services
{ {
@@ -18,40 +21,180 @@ namespace XplorePlane.Services
public ImageProcessingService(ILoggerService logger) public ImageProcessingService(ILoggerService logger)
{ {
_logger = logger?.ForModule<ImageProcessingService>() ?? throw new ArgumentNullException(nameof(logger)); _logger = logger?.ForModule<ImageProcessingService>() ?? throw new ArgumentNullException(nameof(logger));
_processorRegistry = new ConcurrentDictionary<string, ImageProcessorBase>(); _processorRegistry = new ConcurrentDictionary<string, ImageProcessorBase>(StringComparer.OrdinalIgnoreCase);
RegisterBuiltInProcessors(); DiscoverProcessors();
} }
private void RegisterBuiltInProcessors() private void DiscoverProcessors()
{ {
// 8-bit processors var assemblies = LoadCandidateAssemblies().ToList();
_processorRegistry["GaussianBlur"] = new GaussianBlurProcessor(); var discoveredCount = 0;
_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();
_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) public void RegisterProcessor(string name, ImageProcessorBase processor)
{ {
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Processor name cannot be empty", nameof(name)); RegisterProcessorInternal(name, processor, discovered: false);
if (processor == null) throw new ArgumentNullException(nameof(processor));
_processorRegistry[name] = processor;
_logger.Info("Registered processor: {ProcessorName}", name);
} }
public IReadOnlyList<ProcessorParameter> GetProcessorParameters(string processorName) public IReadOnlyList<ProcessorParameter> GetProcessorParameters(string processorName)
{ {
if (_processorRegistry.TryGetValue(processorName, out var processor)) if (_processorRegistry.TryGetValue(processorName, out var processor))
return processor.GetParameters().AsReadOnly(); return processor.GetParameters().AsReadOnly();
throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName)); throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName));
} }
@@ -59,13 +202,15 @@ namespace XplorePlane.Services
{ {
if (_processorRegistry.TryGetValue(processorName, out var processor)) if (_processorRegistry.TryGetValue(processorName, out var processor))
return processor; return processor;
throw new ArgumentException($"Processor not registered or is 16-bit only: {processorName}", nameof(processorName)); throw new ArgumentException($"Processor not registered or is 16-bit only: {processorName}", nameof(processorName));
} }
public string GetProcessorDisplayName(string processorName) public string GetProcessorDisplayName(string processorName)
{ {
if (_processorRegistry.TryGetValue(processorName, out var p)) if (_processorRegistry.TryGetValue(processorName, out var processor))
return string.IsNullOrWhiteSpace(p.Name) ? processorName : p.Name; return string.IsNullOrWhiteSpace(processor.Name) ? processorName : processor.Name;
return processorName; return processorName;
} }
@@ -81,7 +226,6 @@ namespace XplorePlane.Services
if (!_processorRegistry.TryGetValue(processorName, out var processor)) if (!_processorRegistry.TryGetValue(processorName, out var processor))
throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName)); 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); var rawPixels = ImageConverter.ExtractGray8Pixels(source, out int imgWidth, out int imgHeight);
return await Task.Run(() => return await Task.Run(() =>
@@ -103,7 +247,7 @@ namespace XplorePlane.Services
progress?.Report(0.9); progress?.Report(0.9);
var result = ImageConverter.ToBitmapSource(processedEmgu); var result = ImageConverter.ToBitmapSource(processedEmgu);
result.Freeze(); // must freeze before crossing thread boundary result.Freeze();
progress?.Report(1.0); progress?.Report(1.0);
return result; return result;
@@ -131,6 +275,7 @@ namespace XplorePlane.Services
if (processor is IDisposable disposable) if (processor is IDisposable disposable)
disposable.Dispose(); disposable.Dispose();
} }
_processorRegistry.Clear(); _processorRegistry.Clear();
} }
} }
@@ -37,7 +37,7 @@ namespace XplorePlane.ViewModels
private readonly IImageProcessingService _imageProcessingService; private readonly IImageProcessingService _imageProcessingService;
private string _searchText = string.Empty; private string _searchText = string.Empty;
// UI 元数据(分类 + 图标)由 OperatorUiMetadata 统一提供,保持工具箱与流水线图标一致 // UI 元数据(分类 + 图标)由 ProcessorUiMetadata 统一提供,保持工具箱与流水线图标一致
public OperatorToolboxViewModel(IImageProcessingService imageProcessingService) public OperatorToolboxViewModel(IImageProcessingService imageProcessingService)
{ {
@@ -68,7 +68,7 @@ namespace XplorePlane.ViewModels
foreach (var key in _imageProcessingService.GetAvailableProcessors()) foreach (var key in _imageProcessingService.GetAvailableProcessors())
{ {
var displayName = _imageProcessingService.GetProcessorDisplayName(key); 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)); AvailableOperators.Add(new OperatorDescriptor(key, displayName ?? key, operatorIcon, category, categoryIcon));
} }
ApplyFilter(); ApplyFilter();
@@ -106,9 +106,12 @@ namespace XplorePlane.ViewModels
private static int GetCategoryOrder(string category) => category switch private static int GetCategoryOrder(string category) => category switch
{ {
"滤波与平滑" => 0, "滤波与平滑" => 0,
"增强与校正" => 1, "图像增强" => 1,
"分割与阈值" => 2, "图像变换" => 2,
"形态学与轮廓" => 3, "数学运算" => 3,
"形态学处理" => 4,
"边缘检测" => 5,
"检测分析" => 6,
_ => 99 _ => 99
}; };
} }
@@ -183,7 +183,7 @@ namespace XplorePlane.ViewModels
} }
var displayName = _imageProcessingService.GetProcessorDisplayName(operatorKey) ?? operatorKey; var displayName = _imageProcessingService.GetProcessorDisplayName(operatorKey) ?? operatorKey;
var icon = OperatorUiMetadata.GetOperatorIcon(operatorKey); var icon = ProcessorUiMetadata.GetOperatorIcon(operatorKey);
var node = new PipelineNodeViewModel(operatorKey, displayName, icon) var node = new PipelineNodeViewModel(operatorKey, displayName, icon)
{ {
Order = PipelineNodes.Count Order = PipelineNodes.Count
@@ -443,7 +443,7 @@ namespace XplorePlane.ViewModels
{ {
var displayName = _imageProcessingService.GetProcessorDisplayName(nodeModel.OperatorKey) var displayName = _imageProcessingService.GetProcessorDisplayName(nodeModel.OperatorKey)
?? nodeModel.OperatorKey; ?? nodeModel.OperatorKey;
var icon = OperatorUiMetadata.GetOperatorIcon(nodeModel.OperatorKey); var icon = ProcessorUiMetadata.GetOperatorIcon(nodeModel.OperatorKey);
var node = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName, icon) var node = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName, icon)
{ {
Order = nodeModel.Order, Order = nodeModel.Order,
+3 -2
View File
@@ -67,9 +67,10 @@ CNC及矩阵功能的设计与评审,包含以下功能: √
2026.4.20 2026.4.20
---------------------- ----------------------
1、图像算子工具箱的图标 √ 1、图像算子工具箱的图标 √
2、最新的图像算子集成 2、最新的图像算子集成到图像工具箱
3、修复流程图编辑器,并屏蔽 3、修复流程图编辑器
4、主页面加载图像的功能 4、主页面加载图像的功能
5、