TURBO-569:更新工程结构;将导航相机标定和校准功能迁移到XP.Camera类

This commit is contained in:
李伟
2026-04-20 16:09:17 +08:00
parent e166eca3d7
commit 9218384e3f
24 changed files with 2429 additions and 124 deletions
+97 -124
View File
@@ -1,6 +1,6 @@
# XP.Camera 使用说明
基于 .NET 8 WPF 的工业相机控制类库,采用工厂模式 + 统一接口设计,支持多品牌相机扩展。当前已实现 Basler pylon SDK 驱动。
基于 .NET 8 WPF 的工业相机控制与标定类库,采用工厂模式 + 统一接口设计,支持多品牌相机扩展。当前已实现 Basler pylon SDK 驱动。
## 环境要求
@@ -12,16 +12,36 @@
```
XP.Camera/
├── ICameraController.cs # 控制器接口 + 工厂接口
├── CameraFactory.cs # 统一工厂(根据品牌创建控制器)
├── BaslerCameraController.cs # Basler 实现
├── CameraModels.cs # CameraInfo、ImageGrabbedEventArgs、GrabErrorEventArgs
── CameraExceptions.cs # CameraException、ConnectionLostException、DeviceNotFoundException
├── PixelConverter.cs # 像素数据 → WPF BitmapSource 转换工具
├── Core/ # 相机核心抽象
├── ICameraController.cs # 控制器接口 + 工厂接口
│ ├── CameraFactory.cs # 统一工厂(根据品牌创建控制器)
├── CameraModels.cs # CameraInfo、ImageGrabbedEventArgs、GrabErrorEventArgs
│ └── CameraExceptions.cs # CameraException、ConnectionLostException、DeviceNotFoundException
├── Basler/ # Basler 品牌实现
│ └── BaslerCameraController.cs # Basler pylon SDK 实现
├── Converters/ # 数据转换工具
│ └── PixelConverter.cs # 像素数据 → WPF BitmapSource 转换
├── Calibration/ # 相机标定模块
│ ├── CalibrationProcessor.cs # 九点标定(DLT 单应性矩阵,像素→世界坐标)
│ ├── ChessboardCalibrator.cs # 棋盘格标定(Zhang's 方法,内参 + 畸变校正)
│ ├── IDialogService.cs # ICalibrationDialogService 接口
│ ├── DefaultCalibrationDialogService.cs # 默认实现(标准 WPF MessageBox
│ ├── CalibrationLocalizedStrings.cs # XAML 本地化绑定辅助
│ ├── Controls/ # 标定 UI 控件(UserControl
│ │ ├── CalibrationControl.xaml/.cs # 九点标定界面
│ │ ├── ChessboardCalibrationControl.xaml/.cs # 棋盘格标定界面
│ │ └── ImageCanvasControl.xaml/.cs # 图像画布(缩放/平移)
│ ├── ViewModels/ # 标定视图模型
│ │ ├── CalibrationViewModel.cs
│ │ └── ChessboardCalibrationViewModel.cs
│ └── Resources/ # 本地化资源
│ ├── CalibrationResources.resx # 中文(默认)
│ ├── CalibrationResources.en-US.resx # 英文
│ └── CalibrationResources.Designer.cs
└── XP.Camera.csproj
```
所有类型统一`XP.Camera` 命名空间下。
所有相机核心类型在 `XP.Camera` 命名空间下,标定模块在 `XP.Camera.Calibration` 命名空间下。
## 项目引用
@@ -48,27 +68,12 @@ Console.WriteLine($"已连接: {info.ModelName} (SN: {info.SerialNumber})");
在 Prism / DI 容器中注册:
```csharp
// App.xaml.cs
var config = AppConfig.Load();
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
containerRegistry.RegisterSingleton<ICameraController>(() =>
new CameraFactory().CreateController(config.CameraType));
```
ViewModel 中注入使用:
```csharp
public class MyViewModel
{
private readonly ICameraController _camera;
public MyViewModel(ICameraController camera)
{
_camera = camera;
}
}
```
相机品牌通过配置文件 `config.json` 指定:
```json
@@ -82,60 +87,85 @@ public class MyViewModel
```csharp
_camera.ImageGrabbed += (s, e) =>
{
// PixelConverter 返回已 Freeze 的 BitmapSource,可跨线程传递
var bitmap = PixelConverter.ToBitmapSource(
e.PixelData, e.Width, e.Height, e.PixelFormat);
Application.Current.Dispatcher.Invoke(() =>
{
CameraImageSource = bitmap;
});
Application.Current.Dispatcher.Invoke(() => CameraImageSource = bitmap);
};
```
XAML 绑定:
```xml
<Image Source="{Binding CameraImageSource}" Stretch="Uniform" />
```
### 4. 软件触发采集流程
```csharp
camera.Open();
camera.SetExposureTime(10000); // 10ms
camera.StartGrabbing();
// 每次需要采集时调用(结果通过 ImageGrabbed 事件返回)
camera.ExecuteSoftwareTrigger();
camera.StopGrabbing();
camera.Close();
```
### 5. 实时连续采集(链式触发)
### 5. 使用标定模块
收到上一帧后立即触发下一帧,自动适配任何帧率:
标定模块完全自包含,可独立使用,无需外部依赖。
#### 棋盘格标定(相机内参 + 畸变校正)
```csharp
private volatile bool _liveViewRunning;
using XP.Camera.Calibration;
using XP.Camera.Calibration.ViewModels;
using XP.Camera.Calibration.Controls;
_camera.ImageGrabbed += (s, e) =>
// 使用默认对话框服务(标准 WPF MessageBox
var dialogService = new DefaultCalibrationDialogService();
var viewModel = new ChessboardCalibrationViewModel(dialogService);
var window = new Window
{
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
Application.Current.Dispatcher.Invoke(() => CameraImageSource = bitmap);
if (_liveViewRunning)
_camera.ExecuteSoftwareTrigger(); // 链式触发下一帧
Title = "棋盘格标定",
Width = 1600, Height = 900,
Content = new ChessboardCalibrationControl { DataContext = viewModel }
};
window.ShowDialog();
```
// 启动实时
_camera.StartGrabbing();
_liveViewRunning = true;
_camera.ExecuteSoftwareTrigger(); // 触发第一帧
#### 九点标定(像素→世界坐标)
// 停止实时
_liveViewRunning = false;
```csharp
var dialogService = new DefaultCalibrationDialogService();
var viewModel = new CalibrationViewModel(dialogService);
var window = new Window
{
Title = "九点标定",
Width = 1400, Height = 850,
Content = new CalibrationControl { DataContext = viewModel }
};
window.ShowDialog();
```
#### 纯算法调用(不使用 UI
```csharp
// 棋盘格标定
var calibrator = new ChessboardCalibrator();
calibrator.CalibrateFromImages(imagePaths, boardWidth: 11, boardHeight: 8, squareSize: 15f, out string error);
calibrator.SaveCalibration("camera_calibration.json");
// 九点标定
var processor = new CalibrationProcessor();
processor.Calibrate(points);
var worldPoint = processor.PixelToWorld(new PointF(100, 200));
```
#### 自定义对话框服务
如需自定义弹框样式,实现 `ICalibrationDialogService` 接口即可:
```csharp
public class MyDialogService : ICalibrationDialogService
{
// 实现所有接口方法,使用自定义 UI 组件...
}
```
## 核心接口
@@ -149,94 +179,37 @@ _liveViewRunning = false;
| `StartGrabbing()` | 以软件触发模式启动采集 |
| `ExecuteSoftwareTrigger()` | 触发一帧采集 |
| `StopGrabbing()` | 停止采集 |
| `Get/SetExposureTime` | 曝光时间(微秒) |
| `Get/SetGain` | 增益值 |
| `Get/SetWidth/Height` | 图像尺寸 |
| `Get/SetPixelFormat` | 像素格式(Mono8 / BGR8 / BGRA8 |
### 参数读写
### 事件
| 方法 | 说明 |
| 事件 | 说明 |
|------|------|
| `Get/SetExposureTime(double)` | 曝光时间(微秒) |
| `Get/SetGain(double)` | 增益值 |
| `Get/SetWidth(int)` | 图像宽度(自动校正到有效值) |
| `Get/SetHeight(int)` | 图像高度(自动校正到有效值) |
| `Get/SetPixelFormat(string)` | 像素格式(Mono8 / BGR8 / BGRA8 |
| `ImageGrabbed` | 成功采集一帧图像 |
| `GrabError` | 图像采集失败 |
| `ConnectionLost` | 相机连接意外断开 |
### ICameraFactory
| 方法 | 说明 |
|------|------|
| `CreateController(string cameraType)` | 根据品牌名创建控制器 |
当前支持的 `cameraType` 值:`"Basler"`
## 事件
| 事件 | 说明 | 触发线程 |
|------|------|----------|
| `ImageGrabbed` | 成功采集一帧图像 | StreamGrabber 回调线程 |
| `GrabError` | 图像采集失败 | StreamGrabber 回调线程 |
| `ConnectionLost` | 相机连接意外断开 | pylon SDK 事件线程 |
> 所有事件均在非 UI 线程触发。更新 WPF 界面时需通过 `Dispatcher.Invoke` 调度。
> `PixelConverter.ToBitmapSource()` 返回的 BitmapSource 已调用 `Freeze()`,可直接跨线程传递。
> 所有事件均在非 UI 线程触发,更新 WPF 界面时需通过 `Dispatcher.Invoke` 调度。
## 异常处理
```csharp
try
{
camera.Open();
}
catch (DeviceNotFoundException)
{
// 无可用相机设备
}
catch (CameraException ex)
{
// 其他相机错误,ex.InnerException 包含原始 SDK 异常
}
```
| 异常类型 | 场景 |
|---------|------|
| `DeviceNotFoundException` | 无可用相机 |
| `ConnectionLostException` | 相机物理断开 |
| `CameraException` | SDK 操作失败(基类) |
| `InvalidOperationException` | 未连接时访问参数,未采集时触发 |
| `TimeoutException` | 软件触发等待超时 |
## 扩展其他品牌相机
1. 实现 `ICameraController` 接口
```csharp
public class HikvisionCameraController : ICameraController
{
// 实现所有接口方法...
}
```
2.`CameraFactory.cs` 中注册:
```csharp
public ICameraController CreateController(string cameraType)
{
return cameraType switch
{
"Basler" => new BaslerCameraController(),
"Hikvision" => new HikvisionCameraController(),
_ => throw new NotSupportedException($"不支持的相机品牌: {cameraType}")
};
}
```
3. 配置文件切换品牌即可,业务代码无需修改。
1. `Basler/` 同级创建新文件夹,实现 `ICameraController` 接口
2.`CameraFactory.cs` 中注册新品牌
3. 配置文件切换品牌即可,业务代码无需修改
## 线程安全
- 所有公共方法Open / Close / StartGrabbing / StopGrabbing / ExecuteSoftwareTrigger / 参数读写)均线程安全
- 所有公共方法均线程安全
- 事件回调不持有内部锁,不会导致死锁
- `Open()` / `Close()` 幂等,重复调用安全
## 日志
使用 Serilog 静态 API`Log.ForContext<T>()`),与宿主应用共享同一个日志管道。宿主应用只需在启动时配置 `Log.Logger` 即可。