diff --git a/.gitignore b/.gitignore index 5d223a3..d044ede 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ XplorePlane/Libs/Hardware/*.dll XplorePlane/Libs/Hardware/*.pdb XplorePlane/Libs/Native/*.dll XplorePlane/Libs/Native/*.pdb +XplorePlane/XplorePlane/Libs/ImageProcessing/*.dll # 保留 .gitkeep 文件以维持目录结构 !XplorePlane/Libs/**/.gitkeep diff --git a/XplorePlane/App.xaml.cs b/XplorePlane/App.xaml.cs index 9e2296a..5c3be5b 100644 --- a/XplorePlane/App.xaml.cs +++ b/XplorePlane/App.xaml.cs @@ -2,6 +2,7 @@ using System.Windows; using XplorePlane.Views; using XplorePlane.ViewModels; +using XplorePlane.Services; using Prism.Ioc; using Prism.DryIoc; using Prism.Modularity; @@ -9,8 +10,6 @@ using Serilog; using XP.Common.Module; using XP.Hardware.RaySource.Module; using XP.Hardware.RaySource.Services; -using XP.Hardware.RaySource.ViewModels; -using XP.Hardware.RaySource.Views; namespace XplorePlane { @@ -28,11 +27,34 @@ namespace XplorePlane AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; DispatcherUnhandledException += OnDispatcherUnhandledException; - base.OnStartup(e); - - // Initialize Prism with DryIoc - var bootstrapper = new AppBootstrapper(); - bootstrapper.Run(); + try + { + base.OnStartup(e); + + // Initialize Prism with DryIoc + var bootstrapper = new AppBootstrapper(); + bootstrapper.Run(); + } + catch (FileNotFoundException ex) + { + Log.Fatal(ex, "Required DLL not found: {FileName}", ex.FileName); + MessageBox.Show( + $"Required library not found: {ex.FileName}\n\nPlease ensure all required DLLs are present in the Libs/ImageProcessing/ directory.", + "Missing Library", + MessageBoxButton.OK, + MessageBoxImage.Error); + Shutdown(1); + } + catch (TypeLoadException ex) + { + Log.Fatal(ex, "Failed to load type from DLL: {TypeName}", ex.TypeName); + MessageBox.Show( + $"Failed to load required type: {ex.TypeName}\n\nPlease ensure the correct version of DLLs are present in the Libs/ImageProcessing/ directory.", + "Library Load Error", + MessageBoxButton.OK, + MessageBoxImage.Error); + Shutdown(1); + } } private void ConfigureLogging() @@ -137,9 +159,10 @@ namespace XplorePlane containerRegistry.RegisterForNavigation(); containerRegistry.Register(); - // 手动注册 RaySourceOperateView 的 View-ViewModel 映射 - // (库内 RaySourceModule 中此注册被注释掉了,需要在主项目补充) - containerRegistry.RegisterForNavigation(); + // 注册图像处理服务与视图 + containerRegistry.RegisterSingleton(); + containerRegistry.Register(); + containerRegistry.RegisterForNavigation(); Log.Information("依赖注入容器配置完成"); } diff --git a/XplorePlane/Services/IImageProcessingService.cs b/XplorePlane/Services/IImageProcessingService.cs new file mode 100644 index 0000000..c9e5f12 --- /dev/null +++ b/XplorePlane/Services/IImageProcessingService.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; +using ImageProcessing.Core; + +namespace XplorePlane.Services +{ + public interface IImageProcessingService : IDisposable + { + IReadOnlyList GetAvailableProcessors(); + IReadOnlyList GetProcessorParameters(string processorName); + void RegisterProcessor(string name, ImageProcessorBase processor); + + Task ProcessImageAsync( + BitmapSource source, + string processorName, + IDictionary parameters, + IProgress progress = null, + CancellationToken cancellationToken = default); + + Task ProcessRawFrameAsync( + ushort[] pixelData, + int width, + int height, + string processorName, + IDictionary parameters, + CancellationToken cancellationToken = default); + } +} diff --git a/XplorePlane/Services/ImageConverter.cs b/XplorePlane/Services/ImageConverter.cs new file mode 100644 index 0000000..1e22b09 --- /dev/null +++ b/XplorePlane/Services/ImageConverter.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Emgu.CV; +using Emgu.CV.Structure; + +namespace XplorePlane.Services +{ + public static class ImageConverter + { + public static Image ToEmguCV(BitmapSource bitmapSource) + { + if (bitmapSource == null) throw new ArgumentNullException(nameof(bitmapSource)); + + var formatted = new FormatConvertedBitmap(bitmapSource, PixelFormats.Gray8, null, 0); + int width = formatted.PixelWidth; + int height = formatted.PixelHeight; + int stride = width; + byte[] pixels = new byte[height * stride]; + formatted.CopyPixels(pixels, stride, 0); + + var image = new Image(width, height); + image.Bytes = pixels; + return image; + } + + public static BitmapSource ToBitmapSource(Image emguImage) + { + if (emguImage == null) throw new ArgumentNullException(nameof(emguImage)); + + int width = emguImage.Width; + int height = emguImage.Height; + int stride = width; + byte[] pixels = emguImage.Bytes; + + return BitmapSource.Create(width, height, 96, 96, PixelFormats.Gray8, null, pixels, stride); + } + + public static Image ToEmguCV16(BitmapSource bitmapSource) + { + if (bitmapSource == null) throw new ArgumentNullException(nameof(bitmapSource)); + + var formatted = new FormatConvertedBitmap(bitmapSource, PixelFormats.Gray16, null, 0); + int width = formatted.PixelWidth; + int height = formatted.PixelHeight; + int stride = width * 2; // 2 bytes per pixel for 16-bit + byte[] rawBytes = new byte[height * stride]; + formatted.CopyPixels(rawBytes, stride, 0); + + ushort[] pixels = new ushort[width * height]; + Buffer.BlockCopy(rawBytes, 0, pixels, 0, rawBytes.Length); + + var image = new Image(width, height); + // Copy pixel data row by row + for (int y = 0; y < height; y++) + for (int x = 0; x < width; x++) + image.Data[y, x, 0] = pixels[y * width + x]; + + return image; + } + } +} diff --git a/XplorePlane/Services/ImageProcessingException.cs b/XplorePlane/Services/ImageProcessingException.cs new file mode 100644 index 0000000..802b295 --- /dev/null +++ b/XplorePlane/Services/ImageProcessingException.cs @@ -0,0 +1,10 @@ +using System; + +namespace XplorePlane.Services +{ + public class ImageProcessingException : Exception + { + public ImageProcessingException(string message) : base(message) { } + public ImageProcessingException(string message, Exception innerException) : base(message, innerException) { } + } +} diff --git a/XplorePlane/Services/ImageProcessingService.cs b/XplorePlane/Services/ImageProcessingService.cs new file mode 100644 index 0000000..57a5cdb --- /dev/null +++ b/XplorePlane/Services/ImageProcessingService.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; +using Emgu.CV; +using Emgu.CV.Structure; +using ImageProcessing.Core; +using ImageProcessing.Processors; +using Serilog; + +namespace XplorePlane.Services +{ + public class ImageProcessingService : IImageProcessingService + { + private readonly ILogger _logger; + private readonly ConcurrentDictionary _processorRegistry; + private readonly ConcurrentDictionary _processorRegistry16; + + public ImageProcessingService(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _processorRegistry = new ConcurrentDictionary(); + _processorRegistry16 = new ConcurrentDictionary(); + RegisterBuiltInProcessors(); + } + + private void RegisterBuiltInProcessors() + { + // 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(); + + // 16-bit processors (separate registry due to different base class) + _processorRegistry16["GaussianBlur16"] = new GaussianBlurProcessor16(); + _processorRegistry16["FlatFieldCorrection16"] = new FlatFieldCorrectionProcessor16(); + + _logger.Information("Registered {Count8} 8-bit and {Count16} 16-bit built-in image processors", + _processorRegistry.Count, _processorRegistry16.Count); + } + + public IReadOnlyList GetAvailableProcessors() + { + var all = new List(_processorRegistry.Keys); + all.AddRange(_processorRegistry16.Keys); + return all.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.Information("Registered processor: {ProcessorName}", name); + } + + public IReadOnlyList GetProcessorParameters(string processorName) + { + if (_processorRegistry.TryGetValue(processorName, out var processor)) + return processor.GetParameters().AsReadOnly(); + if (_processorRegistry16.TryGetValue(processorName, out var processor16)) + return processor16.GetParameters().AsReadOnly(); + throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName)); + } + + public async Task ProcessImageAsync( + BitmapSource source, + string processorName, + IDictionary parameters, + IProgress progress = null, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!_processorRegistry.TryGetValue(processorName, out var processor)) + throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName)); + + return await Task.Run(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var emguImage = ImageConverter.ToEmguCV(source); + + 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); + progress?.Report(1.0); + + return result; + } + 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); + } + + public async Task ProcessRawFrameAsync( + ushort[] pixelData, + int width, + int height, + string processorName, + IDictionary parameters, + CancellationToken cancellationToken = default) + { + if (pixelData == null) + throw new ArgumentException("pixelData cannot be null", nameof(pixelData)); + if (pixelData.Length != width * height) + throw new ArgumentException( + $"pixelData length {pixelData.Length} does not match width*height {width * height}"); + + if (!_processorRegistry16.TryGetValue(processorName, out var processor)) + throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName)); + + return await Task.Run(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + var image = new Image(width, height); + for (int y = 0; y < height; y++) + for (int x = 0; x < width; x++) + image.Data[y, x, 0] = pixelData[y * width + x]; + + if (parameters != null) + { + foreach (var kvp in parameters) + processor.SetParameter(kvp.Key, kvp.Value); + } + + var processed = processor.Process(image); + + var result = new ushort[width * height]; + for (int y = 0; y < height; y++) + for (int x = 0; x < width; x++) + result[y * width + x] = processed.Data[y, x, 0]; + + return result; + }, cancellationToken); + } + + public void Dispose() + { + foreach (var processor in _processorRegistry.Values) + { + if (processor is IDisposable disposable) + disposable.Dispose(); + } + _processorRegistry.Clear(); + + foreach (var processor in _processorRegistry16.Values) + { + if (processor is IDisposable disposable) + disposable.Dispose(); + } + _processorRegistry16.Clear(); + } + } +} diff --git a/XplorePlane/ViewModels/ImageProcessingViewModel.cs b/XplorePlane/ViewModels/ImageProcessingViewModel.cs new file mode 100644 index 0000000..8856a02 --- /dev/null +++ b/XplorePlane/ViewModels/ImageProcessingViewModel.cs @@ -0,0 +1,157 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Windows.Media.Imaging; +using Prism.Commands; +using Prism.Mvvm; +using Serilog; +using XplorePlane.Services; + +namespace XplorePlane.ViewModels +{ + public class ImageProcessingViewModel : BindableBase + { + private readonly IImageProcessingService _imageProcessingService; + private readonly ILogger _logger; + + private string _selectedProcessor; + private BitmapSource _currentImage; + private BitmapSource _originalImage; + private double _processingProgress; + private bool _isProcessing; + private string _statusMessage; + + public ImageProcessingViewModel(IImageProcessingService imageProcessingService, ILogger logger) + { + _imageProcessingService = imageProcessingService; + _logger = logger; + + AvailableProcessors = new ObservableCollection(); + CurrentParameters = new ObservableCollection(); + + // Populate available processors + foreach (var name in _imageProcessingService.GetAvailableProcessors()) + AvailableProcessors.Add(name); + + // Initialize commands (stubs - implemented in tasks 7.2, 7.4, 7.7) + SelectProcessorCommand = new DelegateCommand(OnSelectProcessor); + ApplyProcessingCommand = new DelegateCommand(OnApplyProcessing); + ResetImageCommand = new DelegateCommand(OnResetImage); + } + + public ObservableCollection AvailableProcessors { get; } + public ObservableCollection CurrentParameters { get; } + + public string SelectedProcessor + { + get => _selectedProcessor; + set => SetProperty(ref _selectedProcessor, value); + } + + public BitmapSource CurrentImage + { + get => _currentImage; + set => SetProperty(ref _currentImage, value); + } + + public BitmapSource OriginalImage + { + get => _originalImage; + set => SetProperty(ref _originalImage, value); + } + + public double ProcessingProgress + { + get => _processingProgress; + set => SetProperty(ref _processingProgress, value); + } + + public bool IsProcessing + { + get => _isProcessing; + set => SetProperty(ref _isProcessing, value); + } + + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + + public DelegateCommand SelectProcessorCommand { get; } + public DelegateCommand ApplyProcessingCommand { get; } + public DelegateCommand ResetImageCommand { get; } + + private void OnSelectProcessor(string processorName) + { + if (string.IsNullOrEmpty(processorName)) return; + + try + { + SelectedProcessor = processorName; + var parameters = _imageProcessingService.GetProcessorParameters(processorName); + CurrentParameters.Clear(); + foreach (var param in parameters) + CurrentParameters.Add(new ProcessorParameterVM(param)); + } + catch (ArgumentException ex) + { + StatusMessage = $"Error loading parameters: {ex.Message}"; + _logger.Warning(ex, "Failed to load parameters for processor: {ProcessorName}", processorName); + } + } + private async void OnApplyProcessing() + { + if (CurrentImage == null || string.IsNullOrEmpty(SelectedProcessor)) return; + + IsProcessing = true; + ProcessingProgress = 0; + + try + { + var parameters = new Dictionary(); + foreach (var param in CurrentParameters) + parameters[param.Name] = param.Value; + + var progress = new Progress(p => ProcessingProgress = p); + + var result = await _imageProcessingService.ProcessImageAsync( + CurrentImage, + SelectedProcessor, + parameters, + progress); + + CurrentImage = result; + StatusMessage = $"Processing complete: {SelectedProcessor}"; + _logger.Information("Image processing completed: {ProcessorName}", SelectedProcessor); + } + catch (ArgumentException ex) + { + StatusMessage = $"Processing error: {ex.Message}"; + _logger.Warning(ex, "Processing failed for processor: {ProcessorName}", SelectedProcessor); + // CurrentImage unchanged + } + catch (OperationCanceledException) + { + StatusMessage = "Processing cancelled"; + _logger.Information("Image processing cancelled"); + // CurrentImage unchanged + } + catch (ImageProcessingException ex) + { + StatusMessage = $"Processing failed: {ex.Message}"; + _logger.Error(ex, "Image processing exception for processor: {ProcessorName}", SelectedProcessor); + // CurrentImage unchanged + } + finally + { + IsProcessing = false; + } + } + private void OnResetImage() + { + CurrentImage = OriginalImage; + StatusMessage = "Image reset to original"; + ProcessingProgress = 0; + } + } +} diff --git a/XplorePlane/ViewModels/ProcessorParameterVM.cs b/XplorePlane/ViewModels/ProcessorParameterVM.cs new file mode 100644 index 0000000..116ffec --- /dev/null +++ b/XplorePlane/ViewModels/ProcessorParameterVM.cs @@ -0,0 +1,38 @@ +using ImageProcessing.Core; +using Prism.Mvvm; + +namespace XplorePlane.ViewModels +{ + public class ProcessorParameterVM : BindableBase + { + private object _value; + + public ProcessorParameterVM(ProcessorParameter parameter) + { + Name = parameter.Name; + DisplayName = parameter.DisplayName; + _value = parameter.DefaultValue; + MinValue = parameter.MinValue; + MaxValue = parameter.MaxValue; + ParameterType = parameter.ValueType?.Name?.ToLower() switch + { + "int32" or "int" => "int", + "double" => "double", + "boolean" or "bool" => "bool", + _ => "enum" + }; + } + + public string Name { get; } + public string DisplayName { get; } + public object MinValue { get; } + public object MaxValue { get; } + public string ParameterType { get; } + + public object Value + { + get => _value; + set => SetProperty(ref _value, value); + } + } +} diff --git a/XplorePlane/Views/ImageProcessingPanelView.xaml b/XplorePlane/Views/ImageProcessingPanelView.xaml new file mode 100644 index 0000000..7f70273 --- /dev/null +++ b/XplorePlane/Views/ImageProcessingPanelView.xaml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +