Files
XplorePlane/XplorePlane/Services/ImageProcessing/ImageProcessingService.cs
T
zhengxuan.zhang 1546aec567 优化高级模块CNC执行的可视化
CNC执行 → PipelineExecutionService(返回 LastStepOutputData)
                  → CncExecutionService(调用 PushDetectionOverlay)
                  → MainViewportService(触发 DetectionOverlayUpdated 事件)
                  → ViewportPanelView(订阅事件,调用 DetectionOverlayRenderer)
                  → PolygonRoiCanvas.SetDetectionOverlayCanvas(插入叠加层 Canvas)
2026-05-19 14:10:16 +08:00

303 lines
12 KiB
C#

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;
namespace XplorePlane.Services
{
public class ImageProcessingService : IImageProcessingService
{
private readonly ILoggerService _logger;
private readonly ConcurrentDictionary<string, ImageProcessorBase> _processorRegistry;
public ImageProcessingService(ILoggerService logger)
{
_logger = logger?.ForModule<ImageProcessingService>() ?? throw new ArgumentNullException(nameof(logger));
_processorRegistry = new ConcurrentDictionary<string, ImageProcessorBase>(StringComparer.OrdinalIgnoreCase);
DiscoverProcessors();
}
private void DiscoverProcessors()
{
var assemblies = LoadCandidateAssemblies().ToList();
var discoveredCount = 0;
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);
}
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)
{
RegisterProcessorInternal(name, processor, discovered: false);
}
public IReadOnlyList<ProcessorParameter> GetProcessorParameters(string processorName)
{
if (_processorRegistry.TryGetValue(processorName, out var processor))
return processor.GetParameters().AsReadOnly();
throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName));
}
public ImageProcessorBase GetProcessor(string processorName)
{
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 processor))
return string.IsNullOrWhiteSpace(processor.Name) ? processorName : processor.Name;
return processorName;
}
public async Task<BitmapSource> ProcessImageAsync(
BitmapSource source,
string processorName,
IDictionary<string, object> parameters,
IProgress<double> progress = null,
CancellationToken cancellationToken = default)
{
var (image, _) = await ProcessImageWithOutputAsync(source, processorName, parameters, progress, cancellationToken)
.ConfigureAwait(false);
return image;
}
public async Task<(BitmapSource image, IReadOnlyDictionary<string, object> outputData)> ProcessImageWithOutputAsync(
BitmapSource source,
string processorName,
IDictionary<string, object> parameters,
IProgress<double> progress = null,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (!_processorRegistry.TryGetValue(processorName, out var processor))
throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName));
var rawPixels = ImageConverter.ExtractGray8Pixels(source, out int imgWidth, out int imgHeight);
return await Task.Run(() =>
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var emguImage = ImageConverter.ToEmguCVFromPixels(rawPixels, imgWidth, imgHeight);
if (parameters != null)
{
foreach (var kvp in parameters)
processor.SetParameter(kvp.Key, kvp.Value);
}
progress?.Report(0.1);
var processedEmgu = processor.Process(emguImage);
progress?.Report(0.9);
var result = ImageConverter.ToBitmapSource(processedEmgu);
result.Freeze();
progress?.Report(1.0);
var snapshot = new Dictionary<string, object>(processor.OutputData.Count);
foreach (var kv in processor.OutputData)
{
// 不将大型 Emgu 图像对象序列化到快照中
if (kv.Key == "RenderedResultImage" || kv.Key == "RoiMask") continue;
snapshot[kv.Key] = kv.Value;
}
return (result, snapshot);
}
catch (OperationCanceledException)
{
throw;
}
catch (ArgumentException)
{
throw;
}
catch (Exception ex)
{
_logger.Error(ex, "Image processing failed for processor: {ProcessorName}", processorName);
throw new ImageProcessingException($"Image processing failed: {ex.Message}", ex);
}
}, cancellationToken).ConfigureAwait(false);
}
public void Dispose()
{
foreach (var processor in _processorRegistry.Values)
{
if (processor is IDisposable disposable)
disposable.Dispose();
}
_processorRegistry.Clear();
}
}
}