合并图像处理库,删除图像lib库

This commit is contained in:
李伟
2026-04-13 13:40:37 +08:00
parent 2a762396d5
commit c7ce4ea6a1
105 changed files with 16341 additions and 133 deletions
+25
View File
@@ -12,8 +12,10 @@ using XplorePlane.Services.Cnc;
using XplorePlane.Services.Matrix;
using XplorePlane.Services.Measurement;
using XplorePlane.Services.Recipe;
using XplorePlane.Services.Camera;
using XplorePlane.ViewModels.Cnc;
using XplorePlane.Views.Cnc;
using XP.Camera;
using Prism.Ioc;
using Prism.DryIoc;
using Prism.Modularity;
@@ -146,6 +148,22 @@ namespace XplorePlane
Log.Error(ex, "射线源资源释放失败");
}
// 释放相机服务资源
try
{
var bootstrapper = AppBootstrapper.Instance;
if (bootstrapper != null)
{
var cameraService = bootstrapper.Container.Resolve<ICameraService>();
cameraService?.Dispose();
Log.Information("相机服务资源已释放");
}
}
catch (Exception ex)
{
Log.Error(ex, "相机服务资源释放失败");
}
Log.CloseAndFlush();
base.OnExit(e);
}
@@ -211,6 +229,7 @@ namespace XplorePlane
containerRegistry.RegisterForNavigation<MainWindow>();
containerRegistry.RegisterForNavigation<MainWindowB>();
containerRegistry.Register<MainViewModel>();
containerRegistry.RegisterSingleton<NavigationPropertyPanelViewModel>();
// 注册图像处理服务与视图
containerRegistry.RegisterSingleton<IImageProcessingService, ImageProcessingService>();
@@ -266,6 +285,12 @@ namespace XplorePlane
containerRegistry.RegisterForNavigation<CncPageView>();
containerRegistry.RegisterForNavigation<MatrixPageView>();
// ── 相机服务(单例)──
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
containerRegistry.RegisterSingleton<ICameraController>(() =>
new CameraFactory().CreateController("Basler"));
containerRegistry.RegisterSingleton<ICameraService, CameraService>();
Log.Information("依赖注入容器配置完成");
}
@@ -0,0 +1,194 @@
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Media.Imaging;
using XP.Camera;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
namespace XplorePlane.Services.Camera
{
/// <summary>
/// 相机管理服务实现,封装 ICameraController 并将状态同步到 IAppStateService。
/// </summary>
public class CameraService : ICameraService
{
private readonly ICameraController _controller;
private readonly IAppStateService _appState;
private readonly ILoggerService _logger;
private volatile bool _liveViewRunning;
private bool _disposed;
private readonly Stopwatch _fpsStopwatch = new();
private int _frameCount;
public bool IsConnected => _controller.IsConnected;
public bool IsGrabbing => _controller.IsGrabbing;
public bool IsLiveView => _liveViewRunning;
public event EventHandler<BitmapSource> FrameArrived;
public event EventHandler ConnectionLost;
public CameraService(
ICameraController controller,
IAppStateService appState,
ILoggerService logger)
{
_controller = controller ?? throw new ArgumentNullException(nameof(controller));
_appState = appState ?? throw new ArgumentNullException(nameof(appState));
_logger = logger?.ForModule<CameraService>() ?? throw new ArgumentNullException(nameof(logger));
_controller.ImageGrabbed += OnImageGrabbed;
_controller.GrabError += OnGrabError;
_controller.ConnectionLost += OnConnectionLost;
}
public CameraInfo Connect()
{
var info = _controller.Open();
_logger.Info("相机已连接: {ModelName} (SN: {SerialNumber})", info.ModelName, info.SerialNumber);
var resolution = $"{_controller.GetWidth()}x{_controller.GetHeight()}";
_appState.UpdateCameraState(new CameraState(
IsConnected: true,
IsStreaming: false,
CurrentFrame: null,
Width: _controller.GetWidth(),
Height: _controller.GetHeight(),
FrameRate: 0));
return info;
}
public void Disconnect()
{
_liveViewRunning = false;
_controller.Close();
_appState.UpdateCameraState(CameraState.Default);
_logger.Info("相机已断开");
}
public void StartGrabbing()
{
_controller.StartGrabbing();
_fpsStopwatch.Restart();
_frameCount = 0;
UpdateStreamingState(true);
}
public void TriggerOnce()
{
_controller.ExecuteSoftwareTrigger();
}
public void StopGrabbing()
{
_liveViewRunning = false;
_controller.StopGrabbing();
_fpsStopwatch.Stop();
UpdateStreamingState(false);
}
public void StartLiveView()
{
if (!_controller.IsGrabbing)
StartGrabbing();
_liveViewRunning = true;
_controller.ExecuteSoftwareTrigger();
_logger.Info("实时预览已启动");
}
public void StopLiveView()
{
_liveViewRunning = false;
_logger.Info("实时预览已停止");
}
// ── 参数读写(直接委托给 controller)──
public double GetExposureTime() => _controller.GetExposureTime();
public void SetExposureTime(double microseconds) => _controller.SetExposureTime(microseconds);
public double GetGain() => _controller.GetGain();
public void SetGain(double value) => _controller.SetGain(value);
public int GetWidth() => _controller.GetWidth();
public void SetWidth(int value) => _controller.SetWidth(value);
public int GetHeight() => _controller.GetHeight();
public void SetHeight(int value) => _controller.SetHeight(value);
public string GetPixelFormat() => _controller.GetPixelFormat();
public void SetPixelFormat(string format) => _controller.SetPixelFormat(format);
// ── 事件处理 ──
private void OnImageGrabbed(object sender, ImageGrabbedEventArgs e)
{
try
{
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
// 计算帧率
_frameCount++;
double fps = 0;
if (_fpsStopwatch.ElapsedMilliseconds > 0)
fps = _frameCount / (_fpsStopwatch.ElapsedMilliseconds / 1000.0);
// 更新全局状态
_appState.UpdateCameraState(new CameraState(
IsConnected: true,
IsStreaming: true,
CurrentFrame: bitmap,
Width: e.Width,
Height: e.Height,
FrameRate: fps));
// 通知 UI
var app = Application.Current;
app?.Dispatcher.BeginInvoke(() => FrameArrived?.Invoke(this, bitmap));
// 链式触发下一帧
if (_liveViewRunning)
_controller.ExecuteSoftwareTrigger();
}
catch (Exception ex)
{
_logger.Error(ex, "处理相机图像帧时出错");
}
}
private void OnGrabError(object sender, GrabErrorEventArgs e)
{
_logger.Warn($"相机采集错误: ErrorCode={e.ErrorCode}, {e.ErrorDescription}");
}
private void OnConnectionLost(object sender, EventArgs e)
{
_liveViewRunning = false;
_appState.UpdateCameraState(CameraState.Default);
_logger.Warn("相机连接意外断开");
var app = Application.Current;
app?.Dispatcher.BeginInvoke(() => ConnectionLost?.Invoke(this, EventArgs.Empty));
}
private void UpdateStreamingState(bool isStreaming)
{
var current = _appState.CameraState;
_appState.UpdateCameraState(current with { IsStreaming = isStreaming });
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_liveViewRunning = false;
_controller.ImageGrabbed -= OnImageGrabbed;
_controller.GrabError -= OnGrabError;
_controller.ConnectionLost -= OnConnectionLost;
_controller.Dispose();
_logger.Info("CameraService 已释放");
}
}
}
@@ -0,0 +1,55 @@
using System;
using System.Windows.Media.Imaging;
using XP.Camera;
namespace XplorePlane.Services.Camera
{
/// <summary>
/// 相机管理服务接口,封装 ICameraController 并桥接到 AppStateService。
/// </summary>
public interface ICameraService : IDisposable
{
bool IsConnected { get; }
bool IsGrabbing { get; }
bool IsLiveView { get; }
/// <summary>连接相机,返回设备信息。</summary>
CameraInfo Connect();
/// <summary>断开相机连接。</summary>
void Disconnect();
/// <summary>启动单帧采集(软件触发模式)。</summary>
void StartGrabbing();
/// <summary>触发一次采集。</summary>
void TriggerOnce();
/// <summary>停止采集。</summary>
void StopGrabbing();
/// <summary>启动实时预览(链式触发)。</summary>
void StartLiveView();
/// <summary>停止实时预览。</summary>
void StopLiveView();
// ── 参数读写 ──
double GetExposureTime();
void SetExposureTime(double microseconds);
double GetGain();
void SetGain(double value);
int GetWidth();
void SetWidth(int value);
int GetHeight();
void SetHeight(int value);
string GetPixelFormat();
void SetPixelFormat(string format);
/// <summary>最新一帧图像(已 Freeze,可跨线程)。</summary>
event EventHandler<BitmapSource> FrameArrived;
/// <summary>相机连接断开事件。</summary>
event EventHandler ConnectionLost;
}
}
@@ -22,12 +22,5 @@ namespace XplorePlane.Services
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);
}
}
@@ -58,27 +58,5 @@ namespace XplorePlane.Services
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;
}
}
}
@@ -16,13 +16,11 @@ namespace XplorePlane.Services
{
private readonly ILoggerService _logger;
private readonly ConcurrentDictionary<string, ImageProcessorBase> _processorRegistry;
private readonly ConcurrentDictionary<string, ImageProcessorBase16> _processorRegistry16;
public ImageProcessingService(ILoggerService logger)
{
_logger = logger?.ForModule<ImageProcessingService>() ?? throw new ArgumentNullException(nameof(logger));
_processorRegistry = new ConcurrentDictionary<string, ImageProcessorBase>();
_processorRegistry16 = new ConcurrentDictionary<string, ImageProcessorBase16>();
RegisterBuiltInProcessors();
}
@@ -39,20 +37,10 @@ namespace XplorePlane.Services
_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.Info("Registered {Count8} 8-bit and {Count16} 16-bit built-in image processors",
_processorRegistry.Count, _processorRegistry16.Count);
_logger.Info("Registered {Count} built-in image processors", _processorRegistry.Count);
}
public IReadOnlyList<string> GetAvailableProcessors()
{
var all = new List<string>(_processorRegistry.Keys);
all.AddRange(_processorRegistry16.Keys);
return all.AsReadOnly();
}
public IReadOnlyList<string> GetAvailableProcessors() => new List<string>(_processorRegistry.Keys).AsReadOnly();
public void RegisterProcessor(string name, ImageProcessorBase processor)
{
@@ -66,8 +54,6 @@ namespace XplorePlane.Services
{
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));
}
@@ -82,8 +68,6 @@ namespace XplorePlane.Services
{
if (_processorRegistry.TryGetValue(processorName, out var p))
return string.IsNullOrWhiteSpace(p.Name) ? processorName : p.Name;
if (_processorRegistry16.TryGetValue(processorName, out var p16))
return string.IsNullOrWhiteSpace(p16.Name) ? processorName : p16.Name;
return processorName;
}
@@ -142,49 +126,6 @@ namespace XplorePlane.Services
}, 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)
@@ -194,12 +135,6 @@ namespace XplorePlane.Services
}
_processorRegistry.Clear();
foreach (var processor in _processorRegistry16.Values)
{
if (processor is IDisposable disposable)
disposable.Dispose();
}
_processorRegistry16.Clear();
}
}
}
}
@@ -0,0 +1,384 @@
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Media.Imaging;
using XP.Camera;
using Serilog;
namespace XplorePlane.ViewModels
{
/// <summary>
/// 相机预览 ViewModel,移植自 ImageProcessing.MainWindowViewModel 的相机控制逻辑。
/// </summary>
public class NavigationPropertyPanelViewModel : BindableBase, IDisposable
{
private static readonly ILogger _logger = Log.ForContext<NavigationPropertyPanelViewModel>();
private readonly ICameraController _camera;
private volatile bool _liveViewRunning;
private bool _disposed;
#region Properties
private BitmapSource? _cameraImageSource;
public BitmapSource? CameraImageSource
{
get => _cameraImageSource;
set => SetProperty(ref _cameraImageSource, value);
}
private bool _isCameraConnected;
public bool IsCameraConnected
{
get => _isCameraConnected;
set
{
if (SetProperty(ref _isCameraConnected, value))
{
ConnectCameraCommand.RaiseCanExecuteChanged();
DisconnectCameraCommand.RaiseCanExecuteChanged();
StartGrabCommand.RaiseCanExecuteChanged();
StopGrabCommand.RaiseCanExecuteChanged();
ApplyExposureCommand.RaiseCanExecuteChanged();
ApplyGainCommand.RaiseCanExecuteChanged();
ApplyWidthCommand.RaiseCanExecuteChanged();
ApplyHeightCommand.RaiseCanExecuteChanged();
ApplyPixelFormatCommand.RaiseCanExecuteChanged();
RefreshCameraParamsCommand.RaiseCanExecuteChanged();
OpenCameraSettingsCommand.RaiseCanExecuteChanged();
}
}
}
private bool _isCameraGrabbing;
public bool IsCameraGrabbing
{
get => _isCameraGrabbing;
set
{
if (SetProperty(ref _isCameraGrabbing, value))
{
StartGrabCommand.RaiseCanExecuteChanged();
StopGrabCommand.RaiseCanExecuteChanged();
}
}
}
private string _cameraStatusText = "未连接";
public string CameraStatusText
{
get => _cameraStatusText;
set => SetProperty(ref _cameraStatusText, value);
}
private bool _isLiveViewEnabled;
public bool IsLiveViewEnabled
{
get => _isLiveViewEnabled;
set
{
if (SetProperty(ref _isLiveViewEnabled, value))
{
if (value)
StartLiveView();
else
StopLiveView();
}
}
}
private string _cameraPixelCoord = "";
public string CameraPixelCoord
{
get => _cameraPixelCoord;
set => SetProperty(ref _cameraPixelCoord, value);
}
private double _exposureTime;
public double ExposureTime
{
get => _exposureTime;
set => SetProperty(ref _exposureTime, value);
}
private double _gainValue;
public double GainValue
{
get => _gainValue;
set => SetProperty(ref _gainValue, value);
}
private int _imageWidth;
public int ImageWidth
{
get => _imageWidth;
set => SetProperty(ref _imageWidth, value);
}
private int _imageHeight;
public int ImageHeight
{
get => _imageHeight;
set => SetProperty(ref _imageHeight, value);
}
private string _selectedPixelFormat = "Mono8";
public string SelectedPixelFormat
{
get => _selectedPixelFormat;
set => SetProperty(ref _selectedPixelFormat, value);
}
public ObservableCollection<string> PixelFormatOptions { get; } = new() { "Mono8", "BGR8", "BGRA8" };
#endregion
#region Commands
public DelegateCommand ConnectCameraCommand { get; }
public DelegateCommand DisconnectCameraCommand { get; }
public DelegateCommand StartGrabCommand { get; }
public DelegateCommand StopGrabCommand { get; }
public DelegateCommand ApplyExposureCommand { get; }
public DelegateCommand ApplyGainCommand { get; }
public DelegateCommand ApplyWidthCommand { get; }
public DelegateCommand ApplyHeightCommand { get; }
public DelegateCommand ApplyPixelFormatCommand { get; }
public DelegateCommand RefreshCameraParamsCommand { get; }
public DelegateCommand OpenCameraSettingsCommand { get; }
#endregion
public NavigationPropertyPanelViewModel(ICameraController camera)
{
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
ConnectCameraCommand = new DelegateCommand(ConnectCamera, () => !IsCameraConnected);
DisconnectCameraCommand = new DelegateCommand(DisconnectCamera, () => IsCameraConnected);
StartGrabCommand = new DelegateCommand(StartGrab, () => IsCameraConnected && !IsCameraGrabbing);
StopGrabCommand = new DelegateCommand(StopGrab, () => IsCameraGrabbing);
ApplyExposureCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetExposureTime(ExposureTime)), () => IsCameraConnected);
ApplyGainCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetGain(GainValue)), () => IsCameraConnected);
ApplyWidthCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetWidth(ImageWidth)), () => IsCameraConnected);
ApplyHeightCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetHeight(ImageHeight)), () => IsCameraConnected);
ApplyPixelFormatCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetPixelFormat(SelectedPixelFormat)), () => IsCameraConnected);
RefreshCameraParamsCommand = new DelegateCommand(RefreshCameraParams, () => IsCameraConnected);
OpenCameraSettingsCommand = new DelegateCommand(OpenCameraSettings, () => IsCameraConnected);
}
#region Camera Methods
private void ConnectCamera()
{
try
{
_camera.ImageGrabbed += OnCameraImageGrabbed;
_camera.GrabError += OnCameraGrabError;
_camera.ConnectionLost += OnCameraConnectionLost;
var info = _camera.Open();
IsCameraConnected = true;
CameraStatusText = $"已连接: {info.ModelName} (SN: {info.SerialNumber})";
_logger.Information("Camera connected: {ModelName}", info.ModelName);
RefreshCameraParams();
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to connect camera");
CameraStatusText = $"连接失败: {ex.Message}";
IsCameraConnected = false;
}
}
private void DisconnectCamera()
{
try
{
IsLiveViewEnabled = false;
_camera.Close();
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to disconnect camera");
}
finally
{
_camera.ImageGrabbed -= OnCameraImageGrabbed;
_camera.GrabError -= OnCameraGrabError;
_camera.ConnectionLost -= OnCameraConnectionLost;
IsCameraConnected = false;
IsCameraGrabbing = false;
CameraStatusText = "未连接";
CameraImageSource = null;
_logger.Information("Camera disconnected");
}
}
private void StartGrab()
{
try
{
_camera.StartGrabbing();
IsCameraGrabbing = true;
CameraStatusText = "采集中...";
// 如果已勾选实时,自动启动 Live View
if (IsLiveViewEnabled)
{
StartLiveView();
}
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to start grabbing");
CameraStatusText = $"采集失败: {ex.Message}";
}
}
private void StopGrab()
{
try
{
IsLiveViewEnabled = false;
_camera.StopGrabbing();
IsCameraGrabbing = false;
CameraStatusText = "已停止采集";
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to stop grabbing");
}
}
private void StartLiveView()
{
if (!IsCameraGrabbing) return;
_liveViewRunning = true;
CameraStatusText = "实时采集中...";
try { _camera.ExecuteSoftwareTrigger(); }
catch (Exception ex) { _logger.Error(ex, "Live view trigger failed"); }
}
private void StopLiveView()
{
_liveViewRunning = false;
if (IsCameraGrabbing)
CameraStatusText = "采集中...";
}
private void RefreshCameraParams()
{
try
{
ExposureTime = _camera.GetExposureTime();
GainValue = _camera.GetGain();
ImageWidth = _camera.GetWidth();
ImageHeight = _camera.GetHeight();
SelectedPixelFormat = _camera.GetPixelFormat();
_logger.Information("Camera parameters refreshed");
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to read camera parameters");
CameraStatusText = $"读取参数失败: {ex.Message}";
}
}
private void ApplyCameraParam(Action action)
{
try
{
action();
_logger.Information("Camera parameter applied");
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to apply camera parameter");
CameraStatusText = $"设置参数失败: {ex.Message}";
}
}
private void OpenCameraSettings()
{
RefreshCameraParams();
var window = new Views.CameraSettingsWindow(this);
window.Owner = Application.Current.MainWindow;
window.Show();
}
#endregion
#region Camera Event Handlers
private void OnCameraImageGrabbed(object? sender, ImageGrabbedEventArgs e)
{
try
{
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
var app = Application.Current;
if (app == null) return;
app.Dispatcher.Invoke(() =>
{
CameraImageSource = bitmap;
});
if (_liveViewRunning)
{
_camera.ExecuteSoftwareTrigger();
}
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to process camera image");
}
}
private void OnCameraGrabError(object? sender, GrabErrorEventArgs e)
{
_logger.Error("Camera grab error: [{ErrorCode}] {ErrorDescription}", e.ErrorCode, e.ErrorDescription);
var app = Application.Current;
if (app == null) return;
app.Dispatcher.Invoke(() =>
{
CameraStatusText = $"采集错误: {e.ErrorDescription}";
});
}
private void OnCameraConnectionLost(object? sender, EventArgs e)
{
_logger.Warning("Camera connection lost");
var app = Application.Current;
if (app == null) return;
app.Dispatcher.Invoke(() =>
{
IsCameraConnected = false;
IsCameraGrabbing = false;
CameraStatusText = "连接已断开";
CameraImageSource = null;
});
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_liveViewRunning = false;
try { _camera.Dispose(); }
catch (Exception ex) { _logger.Error(ex, "Error disposing camera"); }
_disposed = true;
}
#endregion
}
}
@@ -0,0 +1,57 @@
<Window x:Class="XplorePlane.Views.CameraSettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="相机参数设置"
Width="320" Height="420"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize"
ShowInTaskbar="False">
<StackPanel Margin="15">
<TextBlock Text="曝光时间 (µs)" FontSize="11" Foreground="#666" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,10">
<Button DockPanel.Dock="Right" Content="设置" Width="45" Height="26" FontSize="11"
Margin="6,0,0,0" Command="{Binding ApplyExposureCommand}" />
<TextBox Text="{Binding ExposureTime, UpdateSourceTrigger=PropertyChanged}"
Height="26" FontSize="12" VerticalContentAlignment="Center" Padding="4,0" />
</DockPanel>
<TextBlock Text="增益" FontSize="11" Foreground="#666" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,10">
<Button DockPanel.Dock="Right" Content="设置" Width="45" Height="26" FontSize="11"
Margin="6,0,0,0" Command="{Binding ApplyGainCommand}" />
<TextBox Text="{Binding GainValue, UpdateSourceTrigger=PropertyChanged}"
Height="26" FontSize="12" VerticalContentAlignment="Center" Padding="4,0" />
</DockPanel>
<TextBlock Text="图像宽度 (px)" FontSize="11" Foreground="#666" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,10">
<Button DockPanel.Dock="Right" Content="设置" Width="45" Height="26" FontSize="11"
Margin="6,0,0,0" Command="{Binding ApplyWidthCommand}" />
<TextBox Text="{Binding ImageWidth, UpdateSourceTrigger=PropertyChanged}"
Height="26" FontSize="12" VerticalContentAlignment="Center" Padding="4,0" />
</DockPanel>
<TextBlock Text="图像高度 (px)" FontSize="11" Foreground="#666" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,10">
<Button DockPanel.Dock="Right" Content="设置" Width="45" Height="26" FontSize="11"
Margin="6,0,0,0" Command="{Binding ApplyHeightCommand}" />
<TextBox Text="{Binding ImageHeight, UpdateSourceTrigger=PropertyChanged}"
Height="26" FontSize="12" VerticalContentAlignment="Center" Padding="4,0" />
</DockPanel>
<TextBlock Text="像素格式" FontSize="11" Foreground="#666" Margin="0,0,0,2" />
<DockPanel Margin="0,0,0,10">
<Button DockPanel.Dock="Right" Content="设置" Width="45" Height="26" FontSize="11"
Margin="6,0,0,0" Command="{Binding ApplyPixelFormatCommand}" />
<ComboBox SelectedItem="{Binding SelectedPixelFormat}"
ItemsSource="{Binding PixelFormatOptions}"
Height="26" FontSize="12" VerticalContentAlignment="Center" />
</DockPanel>
<Rectangle Height="1" Fill="#E0E0E0" Margin="0,2,0,10" />
<Button Content="读取当前参数" Height="30" FontSize="12"
Command="{Binding RefreshCameraParamsCommand}" />
</StackPanel>
</Window>
@@ -0,0 +1,13 @@
using System.Windows;
namespace XplorePlane.Views
{
public partial class CameraSettingsWindow : Window
{
public CameraSettingsWindow(object viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
}
+11
View File
@@ -252,6 +252,17 @@
SmallImage="/Assets/Icons/xyz.png"
Text="运动控制" />
</StackPanel>
<!-- 第三列: 相机设置 -->
<StackPanel>
<telerik:RadRibbonButton
telerik:ScreenTip.Description="打开相机参数设置对话框"
telerik:ScreenTip.Title="相机设置"
Click="CameraSettings_Click"
Size="Medium"
SmallImage="/Assets/Icons/detector2.png"
Text="相机设置" />
</StackPanel>
</telerik:RadRibbonGroup>
<telerik:RadRibbonGroup telerik:ScreenTip.Title="图像算子" Header="图像算子">
+17
View File
@@ -1,4 +1,5 @@
using System.Windows;
using Prism.Ioc;
using Telerik.Windows.Controls;
using XplorePlane.ViewModels;
@@ -36,5 +37,21 @@ namespace XplorePlane.Views
_toolboxWindow.Activate();
}
}
private void CameraSettings_Click(object sender, RoutedEventArgs e)
{
var bootstrapper = AppBootstrapper.Instance;
if (bootstrapper == null) return;
var vm = bootstrapper.Container.Resolve<NavigationPropertyPanelViewModel>();
if (!vm.IsCameraConnected)
{
MessageBox.Show("请先连接相机", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var window = new CameraSettingsWindow(vm) { Owner = this };
window.Show();
}
}
}
@@ -4,14 +4,18 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="300"
d:DesignHeight="600"
d:DesignWidth="400"
mc:Ignorable="d">
<Grid Background="#FFFFFF">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- 标题栏 -->
<Border
Grid.Row="0"
Background="#F0F0F0"
@@ -23,13 +27,80 @@
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="#333333"
Text="导航" />
Text="相机预览" />
</Border>
<ScrollViewer
<!-- 相机图像显示区域 -->
<Border
Grid.Row="1"
Background="Black"
VerticalScrollBarVisibility="Auto">
<StackPanel Margin="4" />
</ScrollViewer>
Background="#000000"
BorderBrush="#CCCCCC"
BorderThickness="1">
<Image
x:Name="imgCamera"
Source="{Binding CameraImageSource}"
Stretch="Uniform"
StretchDirection="Both"
RenderOptions.BitmapScalingMode="HighQuality"
MouseMove="ImgCamera_MouseMove"
MouseLeave="ImgCamera_MouseLeave" />
</Border>
<!-- 状态信息 -->
<Border
Grid.Row="2"
Background="#000000"
Padding="8,4">
<TextBlock FontSize="12">
<Run Foreground="#FFFFFF" Text="{Binding CameraStatusText, Mode=OneWay}" />
<Run Text=" " />
<Run Foreground="#0078D4" Text="{Binding CameraPixelCoord, Mode=OneWay}" />
</TextBlock>
</Border>
<!-- 控制按钮栏 -->
<Border
Grid.Row="3"
Background="#F0F0F0"
BorderBrush="#DDDDDD"
BorderThickness="0,1,0,0"
Padding="4">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button
Command="{Binding ConnectCameraCommand}"
Padding="8,4"
Margin="2"
Background="#4CAF50"
Foreground="#000000"
Content="连接" />
<Button
Command="{Binding DisconnectCameraCommand}"
Padding="8,4"
Margin="2"
Background="#F44336"
Foreground="#000000"
Content="断开" />
<Button
Command="{Binding StartGrabCommand}"
Padding="8,4"
Margin="2"
Background="#2196F3"
Foreground="#000000"
Content="开始采集" />
<Button
Command="{Binding StopGrabCommand}"
Padding="8,4"
Margin="2"
Background="#FF9800"
Foreground="#000000"
Content="停止采集" />
<CheckBox
IsChecked="{Binding IsLiveViewEnabled}"
VerticalAlignment="Center"
Margin="6,0,0,0"
Foreground="#333333"
Content="实时" />
</StackPanel>
</Border>
</Grid>
</UserControl>
@@ -1,12 +1,54 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using XplorePlane.ViewModels;
using Prism.Ioc;
namespace XplorePlane.Views
{
public partial class NavigationPropertyPanelView : UserControl
{
private NavigationPropertyPanelViewModel? _viewModel;
public NavigationPropertyPanelView()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (DataContext is NavigationPropertyPanelViewModel) return;
var bootstrapper = AppBootstrapper.Instance;
if (bootstrapper != null)
{
_viewModel = bootstrapper.Container.Resolve<NavigationPropertyPanelViewModel>();
DataContext = _viewModel;
}
}
private void ImgCamera_MouseMove(object sender, MouseEventArgs e)
{
if (_viewModel?.CameraImageSource == null) return;
var image = (Image)sender;
var pos = e.GetPosition(image);
int px = (int)(pos.X / image.ActualWidth * _viewModel.CameraImageSource.PixelWidth);
int py = (int)(pos.Y / image.ActualHeight * _viewModel.CameraImageSource.PixelHeight);
if (px >= 0 && px < _viewModel.CameraImageSource.PixelWidth &&
py >= 0 && py < _viewModel.CameraImageSource.PixelHeight)
{
_viewModel.CameraPixelCoord = $"X: {px}, Y: {py}";
}
}
private void ImgCamera_MouseLeave(object sender, MouseEventArgs e)
{
if (_viewModel != null)
_viewModel.CameraPixelCoord = "";
}
}
}
+12 -28
View File
@@ -53,28 +53,12 @@
<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>
<Reference Include="ImageProcessing.Controls">
<HintPath>Libs\ImageProcessing\ImageProcessing.Controls.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="ImageROIControl">
<HintPath>Libs\ImageProcessing\ImageROIControl.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<!-- Telerik UI for WPF 2024 Q1 - NetCore (v2024.1.408.310) -->
<PropertyGroup>
<TelerikDir>D:\Program Files (x86)\Progress\Telerik UI for WPF 2024 Q1\Binaries\NetCore</TelerikDir>
<TelerikDir>C:\Program Files (x86)\Progress\Telerik UI for WPF 2024 Q1\Binaries\NetCore</TelerikDir>
<BaseOutputPath>..\bin\</BaseOutputPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="Telerik.Windows.Controls">
@@ -170,16 +154,6 @@
<Link>Libs\Hardware\zh-TW\%(Filename)%(Extension)</Link>
</None>
<!-- 图像处理外部运行时 DLL -->
<None Include="Libs\ImageProcessing\ExternalLibraries\*.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<!-- 图像处理中文卫星程序集 -->
<None Include="Libs\ImageProcessing\zh-CN\*.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>Libs\ImageProcessing\zh-CN\%(Filename)%(Extension)</Link>
</None>
<Resource Include="GapInspect.ico" />
<!-- 配置文件 -->
@@ -187,4 +161,14 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ImageProcessing.Controls\ImageProcessing.Controls.csproj" />
<ProjectReference Include="..\ImageProcessing.Core\ImageProcessing.Core.csproj" />
<ProjectReference Include="..\ImageProcessing.Processors\ImageProcessing.Processors.csproj" />
<ProjectReference Include="..\ImageROIControl\ImageROIControl.csproj" />
<ProjectReference Include="..\XP.Camera\XP.Camera.csproj" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="xcopy $(SolutionDir)ExternalLibraries $(TargetDir) /d /y /s" />
</Target>
</Project>