最新的图像算子集成到图像工具箱
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -67,9 +67,10 @@ CNC及矩阵功能的设计与评审,包含以下功能: √
|
|||||||
2026.4.20
|
2026.4.20
|
||||||
----------------------
|
----------------------
|
||||||
1、图像算子工具箱的图标 √
|
1、图像算子工具箱的图标 √
|
||||||
2、最新的图像算子集成
|
2、最新的图像算子集成到图像工具箱
|
||||||
3、修复流程图编辑器,并屏蔽
|
3、修复流程图编辑器
|
||||||
4、主页面加载图像的功能
|
4、主页面加载图像的功能
|
||||||
|
5、
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user