#0015 初步集成图像库

This commit is contained in:
zhengxuan.zhang
2026-03-14 19:51:30 +08:00
parent 80cd34fe76
commit 47e163f774
19 changed files with 632 additions and 11 deletions
+1
View File
@@ -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
+33 -10
View File
@@ -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<MainWindow>();
containerRegistry.Register<MainViewModel>();
// 手动注册 RaySourceOperateView 的 View-ViewModel 映射
// (库内 RaySourceModule 中此注册被注释掉了,需要在主项目补充)
containerRegistry.RegisterForNavigation<RaySourceOperateView, RaySourceOperateViewModel>();
// 注册图像处理服务与视图
containerRegistry.RegisterSingleton<IImageProcessingService, ImageProcessingService>();
containerRegistry.Register<ImageProcessingViewModel>();
containerRegistry.RegisterForNavigation<ImageProcessingPanelView>();
Log.Information("依赖注入容器配置完成");
}
@@ -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<string> GetAvailableProcessors();
IReadOnlyList<ProcessorParameter> GetProcessorParameters(string processorName);
void RegisterProcessor(string name, ImageProcessorBase processor);
Task<BitmapSource> ProcessImageAsync(
BitmapSource source,
string processorName,
IDictionary<string, object> parameters,
IProgress<double> progress = null,
CancellationToken cancellationToken = default);
Task<ushort[]> ProcessRawFrameAsync(
ushort[] pixelData,
int width,
int height,
string processorName,
IDictionary<string, object> parameters,
CancellationToken cancellationToken = default);
}
}
+64
View File
@@ -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<Gray, byte> 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<Gray, byte>(width, height);
image.Bytes = pixels;
return image;
}
public static BitmapSource ToBitmapSource(Image<Gray, byte> 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<Gray, ushort> 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<Gray, ushort>(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;
}
}
}
@@ -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) { }
}
}
@@ -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<string, ImageProcessorBase> _processorRegistry;
private readonly ConcurrentDictionary<string, ImageProcessorBase16> _processorRegistry16;
public ImageProcessingService(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_processorRegistry = new ConcurrentDictionary<string, ImageProcessorBase>();
_processorRegistry16 = new ConcurrentDictionary<string, ImageProcessorBase16>();
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<string> GetAvailableProcessors()
{
var all = new List<string>(_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<ProcessorParameter> 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<BitmapSource> ProcessImageAsync(
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));
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<ushort[]> ProcessRawFrameAsync(
ushort[] pixelData,
int width,
int height,
string processorName,
IDictionary<string, object> 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<Gray, ushort>(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();
}
}
}
@@ -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<string>();
CurrentParameters = new ObservableCollection<ProcessorParameterVM>();
// 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<string>(OnSelectProcessor);
ApplyProcessingCommand = new DelegateCommand(OnApplyProcessing);
ResetImageCommand = new DelegateCommand(OnResetImage);
}
public ObservableCollection<string> AvailableProcessors { get; }
public ObservableCollection<ProcessorParameterVM> 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<string> 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<string, object>();
foreach (var param in CurrentParameters)
parameters[param.Name] = param.Value;
var progress = new Progress<double>(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;
}
}
}
@@ -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);
}
}
}
@@ -0,0 +1,75 @@
<UserControl x:Class="XplorePlane.Views.ImageProcessingPanelView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Left panel: processor selection + parameters -->
<Grid Grid.Column="0" Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="200"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Processors" FontWeight="Bold" Margin="0,0,0,5"/>
<ListBox Grid.Row="1"
ItemsSource="{Binding AvailableProcessors}"
SelectedItem="{Binding SelectedProcessor}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<EventSetter Event="Selected"
Handler="OnProcessorSelected"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
<TextBlock Grid.Row="2" Text="Parameters" FontWeight="Bold" Margin="0,10,0,5"/>
<ScrollViewer Grid.Row="3" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding CurrentParameters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,2">
<TextBlock Text="{Binding DisplayName}"/>
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<StackPanel Grid.Row="4" Orientation="Horizontal" Margin="0,10,0,5">
<Button Content="Apply" Command="{Binding ApplyProcessingCommand}" Margin="0,0,5,0" Width="80"/>
<Button Content="Reset" Command="{Binding ResetImageCommand}" Width="80"/>
</StackPanel>
<ProgressBar Grid.Row="5" Value="{Binding ProcessingProgress}" Minimum="0" Maximum="1" Height="10"/>
</Grid>
<!-- Right panel: image preview + status -->
<Grid Grid.Column="1" Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" BorderBrush="Gray" BorderThickness="1">
<Image Source="{Binding CurrentImage}" Stretch="Uniform"/>
</Border>
<TextBlock Grid.Row="1" Text="{Binding StatusMessage}" Margin="0,5,0,0" TextWrapping="Wrap"/>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,20 @@
using System.Windows.Controls;
using System.Windows;
using XplorePlane.ViewModels;
namespace XplorePlane.Views
{
public partial class ImageProcessingPanelView : UserControl
{
public ImageProcessingPanelView()
{
InitializeComponent();
}
private void OnProcessorSelected(object sender, RoutedEventArgs e)
{
if (DataContext is ImageProcessingViewModel vm && sender is ListBoxItem item)
vm.SelectProcessorCommand.Execute(item.Content as string);
}
}
}
+3 -1
View File
@@ -18,7 +18,9 @@
FontWeight="SemiBold" Foreground="#333333" Text="射线源" />
</Border>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<rayViews:RaySourceOperateView />
<!-- TODO: 暂时屏蔽,待 DI 容器解析问题修复后恢复 -->
<!-- <rayViews:RaySourceOperateView /> -->
<TextBlock Text="射线源控件(待集成)" Margin="8" Foreground="#999999" />
</ScrollViewer>
</Grid>
</UserControl>
+15
View File
@@ -47,6 +47,16 @@
<HintPath>Libs\Native\BR.AN.PviServices.dll</HintPath>
<Private>True</Private>
</Reference>
<!-- 图像处理库 DLL 引用 -->
<Reference Include="ImageProcessing.Core">
<HintPath>Libs\ImageProcessing\ImageProcessing.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="ImageProcessing.Processors">
<HintPath>Libs\ImageProcessing\ImageProcessing.Processors.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Reference Include="Telerik.Windows.Controls">
@@ -149,6 +159,11 @@
<Link>zh-TW\%(Filename)%(Extension)</Link>
</None>
<!-- 图像处理外部运行时 DLL -->
<None Include="Libs\ImageProcessing\ExternalLibraries\*.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<!-- 配置文件 -->
<None Update="App.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Binary file not shown.
Binary file not shown.