📚 目录
- 项目架构概述
- Prism框架详解
- 模块化设计模式
- 核心技术实现
- [1. Prism Region导航(页面切换)](#1. Prism Region导航(页面切换))
- [2. ReactiveUI数据绑定](#2. ReactiveUI数据绑定)
- [3. MahApps.Metro UI框架](#3. MahApps.Metro UI框架)
- [4. C++/CLI OpenCV互操作](#4. C++/CLI OpenCV互操作)
- [5. 模块间通信](#5. 模块间通信)
- 实战开发指南
- OpenCV集成完整示例
- 性能优化与最佳实践
项目架构概述
技术栈总览
┌─────────────────────────────────────────────────────────────┐
│ OpenCV Visual Localization System │
├─────────────────────────────────────────────────────────────┤
│ UI层: MahApps.Metro + Material Design │
│ MVVM: Prism 8.1 + ReactiveUI 19.5 │
│ DI容器: Unity Container │
│ 日志: NLog 5.3 │
│ 图像处理: OpenCV C++ + C++/CLI 互操作 │
└─────────────────────────────────────────────────────────────┘
Prism框架详解
什么是Prism?
Prism是一个用于构建松耦合、可维护、可测试的WPF/Xamarin应用程序的框架,提供了以下核心功能:
Prism核心功能:
├── 模块化开发 (Modularity)
├── 依赖注入 (DI/IOC)
├── 区域导航 (Region Navigation)
├── 事件聚合器 (Event Aggregator)
└── 对话服务 (Dialog Service)
Prism应用生命周期
csharp
public partial class App : PrismApplication // 继承PrismApplication
{
// 步骤1: 创建主窗口(Shell)
protected override Window CreateShell()
{
return new ShellView(); // 返回主窗口实例
}
// 步骤2: 注册服务到DI容器
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// containerRegistry.Register<接口, 实现类>();
// containerRegistry.RegisterSingleton<单例接口, 实现类>();
}
// 步骤3: 配置模块目录
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
// 添加模块
moduleCatalog.AddModule<CoreModule>();
}
// 步骤4: 应用启动完成
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Logs.LogInfo("启动应用程序");
}
}
Prism应用启动流程
应用程序启动
↓
PrismApplication初始化
↓
创建DI容器 (Unity Container)
↓
调用 RegisterTypes() 注册服务
↓
调用 ConfigureModuleCatalog() 配置模块
↓
加载所有模块
↓
每个模块调用 RegisterTypes() 注册自己的服务
↓
每个模块调用 OnInitialized() 初始化
↓
调用 CreateShell() 创建主窗口
↓
主窗口显示,应用程序运行
模块化设计模式
什么是模块化?
模块化将大型应用程序拆分成多个独立的功能单元:
传统单体应用 vs 模块化应用:
┌─────────────────────┐
│ 单体应用(难以维护) │
│ │
│ 所有代码在一起 │
│ - 难以维护 │
│ - 难以测试 │
│ - 团队协作困难 │
└─────────────────────┘
┌──────────────────────────────────────┐
│ 模块化应用(易于维护) │
├──────────┬──────────┬──────────┤
│ 模块A │ 模块B │ 模块C │
│ (日志) │ (配置) │ (UI) │
│ │ │ │
│ 独立开发 │ 独立开发 │ 独立开发 │
│ 独立测试 │ 独立测试 │ 独立测试 │
│ 独立部署 │ 独立部署 │ 独立部署 │
└──────────┴──────────┴──────────┘
如何创建Prism模块
步骤1: 创建类库项目
bash
# 创建新的类库项目
dotnet new classlib -n Company.Application.UI
cd Company.Application.UI
# 安装Prism包
dotnet add package Prism.Wpf
dotnet add package Prism.Unity.Wpf
dotnet add package ReactiveUI.WPF
步骤2: 创建模块类
csharp
using Prism.Modularity;
using Prism.Ioc;
using Company.Application.UI.ViewModels;
using Company.Application.UI.Views;
using Company.Application.UI.Services;
namespace Company.Application.UI
{
// 1. 实现IModule接口
public class ApplicationModule : IModule
{
// 2. 模块初始化时调用
public void OnInitialized(IContainerProvider containerProvider)
{
// 这里可以执行初始化逻辑
// 例如:订阅事件、初始化服务等
}
// 3. 注册模块内的服务和类型
public void RegisterTypes(IContainerRegistry containerRegistry)
{
// 注册视图和视图模型
containerRegistry.Register<DashboardView>();
containerRegistry.Register<DashboardViewModel>();
// 注册服务
containerRegistry.Register<ICameraService, CameraService>();
// 注册单例服务
containerRegistry.RegisterSingleton<IImageProcessingService, ImageProcessingService>();
}
}
}
步骤3: 在Shell中注册模块
csharp
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
// 方式1: 直接添加模块引用
moduleCatalog.AddModule<CoreModule>();
moduleCatalog.AddModule<ApplicationModule>();
// 方式2: 从程序集加载模块(适用于插件化架构)
// moduleCatalog.AddModule<ApplicationModule>(InitializationMode.OnDemand);
}
模块加载模式
| 模式 | 说明 | 使用场景 |
|---|---|---|
WhenAvailable |
应用启动时立即加载 | 核心功能模块 |
OnDemand |
按需加载 | 不常用功能、插件 |
Automatic |
自动发现并加载 | 插件化架构 |
核心技术实现
1. Prism Region导航(页面切换)
什么是Region?
Region是XAML中定义的逻辑容器,用于动态显示内容:
┌─────────────────────────────────────────────────────┐
│ ShellView (主窗口) │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌─────────────────────────────────┐ │
│ │导航区域 │ │ 内容区域 (ContentRegion) │ │
│ │ │ │ │ │
│ │ - 监控 │ │ ┌─────────────────────────┐ │ │
│ │ - 标定 │ │ │ │ │ │
│ │ - 识别 │ │ │ 当前显示的View │ │ │
│ │ │ │ │ (DashboardView) │ │ │
│ └──────────┘ │ │ │ │ │
│ │ └─────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
实现Region导航
步骤1: 在ShellView中定义Region
xml
<Window x:Class="Company.Shell.Views.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
Title="OpenCV视觉定位系统" Height="900" Width="1400"
WindowStartupLocation="CenterScreen"
WindowState="Maximized">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 顶部标题栏 -->
<Border Grid.Row="0" Background="#2D2D30" Padding="10">
<StackPanel Orientation="Horizontal">
<TextBlock Text="📷 OpenCV视觉定位系统"
FontSize="20"
FontWeight="Bold"
Foreground="White"/>
</StackPanel>
</Border>
<!-- 主内容区域 -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 左侧导航区域 -->
<Border Grid.Column="0" Background="#3C3C3C">
<StackPanel Margin="10">
<!-- 导航按钮 - 使用Region导航 -->
<Button Content="📊 监控面板"
Command="{Binding NavigateCommand}"
CommandParameter="Dashboard"
Margin="0,5"
Height="40"/>
<Button Content="🎯 相机标定"
Command="{Binding NavigateCommand}"
CommandParameter="CameraCalibration"
Margin="0,5"
Height="40"/>
<Button Content="🔍 图像识别"
Command="{Binding NavigateCommand}"
CommandParameter="PatternRecognition"
Margin="0,5"
Height="40"/>
<Button Content="⚙️ 系统设置"
Command="{Binding NavigateCommand}"
CommandParameter="Settings"
Margin="0,5"
Height="40"/>
</StackPanel>
</Border>
<!-- 右侧内容区域 - 关键!定义Region -->
<ContentControl prism:RegionManager.RegionName="ContentRegion"
Grid.Column="1"/>
</Grid>
</Grid>
</Window>
关键点:
xmlns:prism="http://prismlibrary.com/"- 引入Prism命名空间prism:RegionManager.RegionName="ContentRegion"- 定义Region名称
步骤2: 配置View和ViewModel的自动关联
csharp
public void RegisterTypes(IContainerRegistry containerRegistry)
{
// 注册视图和视图模型
containerRegistry.Register<DashboardView>();
containerRegistry.Register<DashboardViewModel>();
// 配置自动关联(重要!)
containerRegistry.RegisterForNavigation<DashboardView, DashboardViewModel>();
containerRegistry.RegisterForNavigation<CameraCalibrationView, CameraCalibrationViewModel>();
containerRegistry.RegisterForNavigation<PatternRecognitionView, PatternRecognitionViewModel>();
}
自动关联原理:
当导航到 "Dashboard" 时:
↓
Prism查找名为 "DashboardView" 的视图
↓
从DI容器获取 DashboardViewModel 实例
↓
将 ViewModel 设置为 View 的 DataContext
↓
将 View 显示在目标 Region 中
步骤3: 在ViewModel中实现导航逻辑
csharp
using Prism.Commands;
using Prism.Mvvm;
using Prism.Regions;
namespace Company.Shell.ViewModels
{
public class ShellViewModel : BindableBase
{
private readonly IRegionManager _regionManager;
private readonly IEventAggregator _eventAggregator;
public ShellViewModel(IRegionManager regionManager, IEventAggregator eventAggregator)
{
_regionManager = regionManager; // 注入Region管理器
_eventAggregator = eventAggregator; // 注入事件聚合器
NavigateCommand = new DelegateCommand<string>(Navigate);
}
public DelegateCommand<string> NavigateCommand { get; }
private void Navigate(string navigationPath)
{
if (string.IsNullOrWhiteSpace(navigationPath))
return;
// 方式1: 使用Uri导航
_regionManager.RequestNavigate("ContentRegion", navigationPath);
// 方式2: 带参数导航
// var parameters = new NavigationParameters();
// parameters.Add("Id", 123);
// _regionManager.RequestNavigate("ContentRegion", navigationPath, parameters);
// 方式3: 带回调的导航
// _regionManager.RequestNavigate("ContentRegion", navigationPath, NavigationCallback);
}
private void NavigationCallback(NavigationResult result)
{
if (result.Result == true)
{
// 导航成功
Logs.LogInfo($"导航到 {result.Context.Uri} 成功");
}
else
{
// 导航失败
Logs.LogError($"导航到 {result.Context.Uri} 失败: {result.Error?.Message}");
}
}
}
}
Region导航的完整流程
用户点击 "监控面板" 按钮
↓
触发 NavigateCommand,传递参数 "Dashboard"
↓
ShellViewModel.Navigate("Dashboard") 执行
↓
_regionManager.RequestNavigate("ContentRegion", "Dashboard")
↓
RegionManager查找名为 "ContentRegion" 的Region
↓
在模块目录中查找名为 "Dashboard" 的导航
↓
找到 DashboardView → DashboardViewModel 的注册
↓
从DI容器获取或创建 DashboardViewModel 实例
↓
将 ViewModel 设置为 View 的 DataContext
↓
如果View已实现INavigationAware,调用其导航方法
↓
将View显示在ContentRegion中
↓
导航完成!
###INavigationAware接口
当View或ViewModel需要参与导航流程时,实现此接口:
csharp
public class DashboardViewModel : BindableBase, INavigationAware
{
// 导航到该视图时调用
public void OnNavigatedTo(NavigationContext navigationContext)
{
// 从导航参数中获取数据
var id = navigationContext.Parameters.GetValue<int>("Id");
// 初始化视图数据
LoadDashboardData();
Logs.LogInfo("进入监控面板");
}
// 导航离开该视图时调用(可以取消导航)
public bool IsNavigationTarget(NavigationContext navigationContext)
{
// 返回true:重用当前实例
// 返回false:创建新实例
return true;
}
// 从该视图导航离开时调用
public void OnNavigatedFrom(NavigationContext navigationContext)
{
// 清理资源
// 保存状态
Logs.LogInfo("离开监控面板");
}
}
多Region布局
xml
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 顶部栏 -->
<ContentControl prism:RegionManager.RegionName="HeaderRegion" Grid.Row="0"/>
<!-- 主内容区 -->
<ContentControl prism:RegionManager.RegionName="MainRegion" Grid.Row="1"/>
<!-- 状态栏 -->
<ContentControl prism:RegionManager.RegionName="FooterRegion" Grid.Row="2"/>
</Grid>
csharp
// 同时导航到多个Region
_regionManager.RequestNavigate("HeaderRegion", "HeaderView");
_regionManager.RequestNavigate("MainRegion", "DashboardView");
_regionManager.RequestNavigate("FooterRegion", "StatusView");
2. ReactiveUI数据绑定
什么是ReactiveUI?
ReactiveUI是一个基于**响应式编程(Rx.NET)**的MVVM框架,它允许你以声明式的方式处理异步事件和属性变化。
传统绑定 vs ReactiveUI
❌ 传统方式(INotifyPropertyChanged)
csharp
public class DashboardViewModel : INotifyPropertyChanged
{
private string _status;
public string Status
{
get => _status;
set
{
if (_status != value)
{
_status = value;
OnPropertyChanged(nameof(Status));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
问题:
- 代码冗长
- 容易出错
- 难以处理复杂逻辑
✅ ReactiveUI方式
csharp
public class DashboardViewModel : ReactiveObject
{
[ObservableAsPropertyProperty]
public string Status { get; }
public DashboardViewModel()
{
// 定义响应式属性
var statusSignal = Observable
.Interval(TimeSpan.FromSeconds(1))
.Select(_ => DateTime.Now.ToString("HH:mm:ss"));
statusSignal
.ToPropertyEx(this, x => x.Status)
.DisposeWith(Disposables);
}
}
ReactiveUI核心概念
1. ObservableAsPropertyProperty - 派生属性
csharp
public class CameraViewModel : ReactiveObject
{
// 源属性
[Reactive]
private bool _isCameraConnected;
// 派生属性 - 自动计算
[ObservableAsPropertyProperty]
public string ConnectionStatus { get; }
public CameraViewModel()
{
// 当 IsCameraConnected 改变时,自动更新 ConnectionStatus
this.WhenAnyValue(x => x.IsCameraConnected)
.Select isConnected => isConnected ? "已连接" : "未连接"
.ToPropertyEx(this, x => x.ConnectionStatus);
}
}
2. WhenAnyValue - 属性变化监听
csharp
public class PatternRecognitionViewModel : ReactiveObject
{
[Reactive]
private double _threshold;
[Reactive]
private int _minMatchCount;
public PatternRecognitionViewModel()
{
// 监听多个属性变化
this.WhenAnyValue(
x => x.Threshold,
x => x.MinMatchCount
)
.Throttle(TimeSpan.FromMilliseconds(300)) // 防抖
.Subscribe(tuple =>
{
var (threshold, minCount) = tuple;
UpdateRecognitionParameters(threshold, minCount);
})
.DisposeWith(Disposables);
}
private void UpdateRecognitionParameters(double threshold, int minCount)
{
Logs.LogInfo($"更新参数: 阈值={threshold}, 最小匹配数={minCount}");
}
}
3. Command - 响应式命令
csharp
public class ImageProcessingViewModel : ReactiveObject
{
public ReactiveCommand<Unit, Unit> ProcessImageCommand { get; }
[Reactive]
private bool _isProcessing;
[ObservableAsPropertyProperty]
public bool CanProcess { get; }
public ImageProcessingViewModel()
{
// 定义命令
ProcessImageCommand = ReactiveCommand.CreateFromTask(
ProcessImageAsync,
this.WhenAnyValue(x => x.CanProcess) // CanExecute条件
);
// 命令执行时更新状态
ProcessImageCommand
.IsExecuting
.ToPropertyEx(this, x => x.IsProcessing);
// 命令执行完成后的处理
ProcessImageCommand
.Subscribe(_ =>
{
Logs.LogInfo("图像处理完成");
});
}
private async Task ProcessImageAsync()
{
// 处理图像
await Task.Delay(1000);
}
}
ReactiveUI + OpenCV实战示例
csharp
public class RealTimeDetectionViewModel : ReactiveObject
{
private readonly ICameraService _cameraService;
private readonly IImageProcessingService _imageProcessingService;
[Reactive]
private WriteableBitmap _currentFrame;
[Reactive]
private int _detectedObjectsCount;
[Reactive]
private double _processingTime;
[ObservableAsPropertyProperty]
public string StatusText { get; }
public ReactiveCommand<Unit, Unit> StartDetectionCommand { get; }
public ReactiveCommand<Unit, Unit> StopDetectionCommand { get; }
private readonly CancellationTokenSource _cts = new();
public RealTimeDetectionViewModel(
ICameraService cameraService,
IImageProcessingService imageProcessingService)
{
_cameraService = cameraService;
_imageProcessingService = imageProcessingService;
// 创建命令
var canStart = this.WhenAnyValue(x => x.CurrentFrame)
.Select(frame => frame != null);
StartDetectionCommand = ReactiveCommand.CreateFromTask(
StartDetectionAsync,
canStart
);
StopDetectionCommand = ReactiveCommand.Create(StopDetection);
// 状态文本自动更新
this.WhenAnyValue(
x => x.DetectedObjectsCount,
x => x.ProcessingTime
)
.Select(tuple =>
{
var (count, time) = tuple;
return $"检测到 {count} 个对象 | 处理时间: {time:F2}ms";
})
.ToPropertyEx(this, x => x.StatusText);
}
private async Task StartDetectionAsync()
{
await Task.Run(async () =>
{
while (!_cts.Token.IsCancellationRequested)
{
var stopwatch = Stopwatch.StartNew();
try
{
// 1. 从相机获取图像
var frame = await _cameraService.CaptureFrameAsync();
// 2. 调用OpenCV处理
var result = await _imageProcessingService.DetectObjectsAsync(frame);
// 3. 更新UI
await Dispatcher.InvokeAsync(() =>
{
CurrentFrame = result.ProcessedImage.ToWriteableBitmap();
DetectedObjectsCount = result.ObjectCount;
ProcessingTime = stopwatch.ElapsedMilliseconds;
});
}
catch (Exception ex)
{
Logs.LogError(ex, "图像检测失败");
}
}
}, _cts.Token);
}
private void StopDetection()
{
_cts.Cancel();
}
}
3. MahApps.Metro UI框架
什么是MahApps.Metro?
MahApps.Metro是一个用于WPF的UI工具包,提供了现代化的Metro/Fluent Design风格控件。
配置MahApps.Metro
步骤1: 安装NuGet包
bash
dotnet add package MahApps.Metro
dotnet add package MahApps.Metro.IconPacks
步骤2: 在App.xaml中引入资源
xml
<Application x:Class="Company.Shell.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- MahApps.Metro 资源 -->
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Dark.Blue.xaml" />
<!-- 图标包 -->
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro.IconPacks;component/Themes/MaterialDesign.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
步骤3: 使用MahApps.Metro控件
xml
<!-- 1. MetroWindow - 现代化窗口 -->
<mah:MetroWindow x:Class="Company.Shell.Views.ShellView"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
Title="OpenCV视觉定位系统"
BorderThickness="0"
GlowBrush="{DynamicResource MahApps.Brushes.Accent}"
WindowStartupLocation="CenterScreen">
<Grid>
<!-- 2. HamburgerMenu - 汉堡菜单 -->
<mah:HamburgerMenu>
<mah:HamburgerMenu.ItemsSource>
<mah:HamburgerMenuItem Icon="{mah:MaterialIcon Kind=Home}"
Label="监控面板"
Tag="Dashboard"/>
<mah:HamburgerMenuItem Icon="{mah:MaterialIcon Kind=Camera}"
Label="相机标定"
Tag="CameraCalibration"/>
<mah:HamburgerMenuItem Icon="{mah:MaterialIcon Kind=Magnify}"
Label="图像识别"
Tag="PatternRecognition"/>
</mah:HamburgerMenu.ItemsSource>
</mah:HamburgerMenu>
<!-- 3. ToggleSwitch - 开关 -->
<mah:ToggleSwitch Header="自动曝光"
IsOn="{Binding AutoExposure, Mode=TwoWay}"
OnLabel="开"
OffLabel="关"/>
<!-- 4. NumericUpDown - 数字输入 -->
<mah:NumericUpdown Value="{Binding Threshold}"
Minimum="0"
Maximum="100"
Interval="1"
StringFormat="{}{0}%"/>
<!-- 5. ProgressRing - 加载动画 -->
<mah:ProgressRing IsActive="{Binding IsProcessing}"
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
<!-- 6. FlipView - 图片浏览 -->
<mah:FlipView ItemsSource="{Binding DetectedObjects}">
<mah:FlipView.ItemTemplate>
<DataTemplate>
<Image Source="{Binding Image}"
Width="400"
Height="300"/>
</DataTemplate>
</mah:FlipView.ItemTemplate>
</mah:FlipView>
</Grid>
</mah:MetroWindow>
MahApps.Metro样式自定义
xml
<!-- 自定义主题色 -->
<Color x:Key="PrimaryColor">#FF2196F3</Color>
<Color x:Key="SecondaryColor">#FF1976D2</Color>
<SolidColorBrush x:Key="MahApps.Brushes.Accent"
Color="{StaticResource PrimaryColor}"/>
<SolidColorBrush x:Key="MahApps.Brushes.Accent2"
Color="{StaticResource SecondaryColor}"/>
<!-- 自定义按钮样式 -->
<Style x:Key="ModernButton"
TargetType="Button"
BasedOn="{StaticResource MahApps.Styles.Button}">
<Setter Property="Background"
Value="{DynamicResource MahApps.Brushes.Accent}"/>
<Setter Property="Foreground"
Value="White"/>
<Setter Property="BorderThickness"
Value="0"/>
<Setter Property="Padding"
Value="15,8"/>
<Setter Property="FontSize"
Value="14"/>
<Setter Property="Cursor"
Value="Hand"/>
<Style.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Setter Property="Background"
Value="{DynamicResource MahApps.Brushes.Accent2}"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- 使用自定义样式 -->
<Button Style="{StaticResource ModernButton}"
Content="处理图像"
Command="{Binding ProcessCommand}"/>
4. C++/CLI OpenCV互操作
为什么需要C++/CLI?
场景:C#调用C++ OpenCV库
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ C# WPF UI │─────►│ C++/CLI包装 │─────►│ C++ OpenCV │
│ (Managed) │ │ (Bridge) │ │ (Native) │
└─────────────┘ └─────────────┘ └─────────────┘
C++/CLI基础语法
托管类型 (Managed Types)
cpp
// 引用类型 (类似C#的class)
public ref class ImageProcessor {
public:
// 托管属性
property int Width {
int get() { return _width; }
void set(int value) { _width = value; }
}
private:
int _width;
};
// 值类型 (类似C#的struct)
public value struct Point3D {
public:
double X;
double Y;
double Z;
};
// 接口
public interface class IImageFilter {
void Process(cv::Mat^ image);
};
实现OpenCV包装器
步骤1: 创建C++/CLI项目
bash
# 创建C++/CLI类库
# 在Visual Studio中: 新建项目 -> CLR类库 -> OpenCVWrapper
步骤2: 配置项目属性
xml
<!-- OpenCVWrapper.vcxproj -->
<PropertyGroup>
<ConfigurationType>DynamicLibrary</ConfigurationType>
<CLRSupport>true</CLRSupport> <!-- 启用CLR支持 -->
<CommonLanguageRuntimeSupport>/clr</CommonLanguageRuntimeSupport>
</PropertyGroup>
步骤3: 创建包装器类
cpp
#pragma once
#include <opencv2/opencv.hpp>
#include <msclr/marshal.h>
using namespace System;
using namespace System::Drawing;
using namespace System::Runtime::InteropServices;
namespace OpenCVWrapper {
// 托管图像类
public ref class ManagedImage {
public:
property int Width;
property int Height;
property IntPtr DataPtr; // 指向图像数据的指针
ManagedImage(int width, int height, void* data) {
Width = width;
Height = height;
DataPtr = IntPtr(data);
}
// 转换为Bitmap(用于WPF显示)
Bitmap^ ToBitmap() {
// 将OpenCV Mat转换为GDI+ Bitmap
Bitmap^ bitmap = gcnew Bitmap(Width, Height,
PixelFormat::Format24bppRgb);
// 锁定位图数据
Imaging::BitmapData^ bmpData = bitmap->LockBits(
Rectangle(0, 0, Width, Height),
Imaging::ImageLockMode::WriteOnly,
PixelFormat::Format24bppRgb);
// 复制数据
memcpy(bmpData->Scan0.ToPointer(),
DataPtr.ToPointer(),
Width * Height * 3);
bitmap->UnlockBits(bmpData);
return bitmap;
}
};
// 主包装器类
public ref class ImageProcessor {
private:
cv::Ptr<cv::FaceDetectorYN> faceDetector;
cv::Size frameSize;
public:
// 构造函数
ImageProcessor(int width, int height) {
frameSize = cv::Size(width, height);
// 初始化OpenCV检测器
String^ modelPath = "models/face_detection_yunet_2023mar.onnx";
faceDetector = cv::FaceDetectorYN::create(
cv::String(MarshalString(modelPath)),
"",
frameSize,
0.9, // scoreThreshold
0.3, // nmsThreshold
5000 // topK
);
}
// 将String转换为std::string
std::string MarshalString(String^ s) {
const char* chars = (const char*)(Marshal::StringToHGlobalAnsi(s)).ToPointer();
std::string str(chars);
Marshal::FreeHGlobal(IntPtr((void*)chars));
return str;
}
// 图像处理方法
ManagedImage^ DetectFaces(array<Byte>^ imageBytes, int width, int height) {
// 1. 将字节数组转换为cv::Mat
pin_ptr<Byte> ptr = &imageBytes[0];
unsigned char* data = ptr;
cv::Mat mat(height, width, CV_8UC3, data);
// 2. 检测人脸
cv::Mat faces;
faceDetector->detect(mat, faces);
// 3. 绘制检测结果
for (int i = 0; i < faces.rows; i++) {
cv::Rect face(
(int)faces.at<float>(i, 0),
(int)faces.at<float>(i, 1),
(int)faces.at<float>(i, 2),
(int)faces.at<float>(i, 3)
);
cv::rectangle(mat, face, cv::Scalar(0, 255, 0), 2);
}
// 4. 返回托管图像对象
return gcnew ManagedImage(width, height, mat.data);
}
// 边缘检测
ManagedImage^ DetectEdges(array<Byte>^ imageBytes, int width, int height,
double threshold1, double threshold2) {
pin_ptr<Byte> ptr = &imageBytes[0];
unsigned char* data = ptr;
cv::Mat mat(height, width, CV_8UC3, data);
cv::Mat gray, edges;
// 转换为灰度图
cv::cvtColor(mat, gray, cv::COLOR_BGR2GRAY);
// Canny边缘检测
cv::Canny(gray, edges, threshold1, threshold2);
// 转换回彩色
cv::Mat result;
cv::cvtColor(edges, result, cv::COLOR_GRAY2BGR);
return gcnew ManagedImage(width, height, result.data);
}
// 图像匹配
double MatchTemplate(array<Byte>^ sourceBytes, array<Byte>^ templateBytes,
int sourceWidth, int sourceHeight,
int templateWidth, int templateHeight,
[Out] int% x, [Out] int% y) {
pin_ptr<Byte> sourcePtr = &sourceBytes[0];
pin_ptr<Byte> templatePtr = &templateBytes[0];
cv::Mat source(sourceHeight, sourceWidth, CV_8UC3, sourcePtr);
cv::Mat templ(templateHeight, templateWidth, CV_8UC3, templatePtr);
cv::Mat result;
// 模板匹配
cv::matchTemplate(source, templ, result, cv::TM_CCOEFF_NORMED);
// 找到最佳匹配位置
double minVal, maxVal;
cv::Point minLoc, maxLoc;
cv::minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc);
x = maxLoc.x;
y = maxLoc.y;
return maxVal; // 返回相似度(0-1)
}
// 保存图像
void SaveImage(array<Byte>^ imageBytes, int width, int height,
String^ filePath) {
pin_ptr<Byte> ptr = &imageBytes[0];
unsigned char* data = ptr;
cv::Mat mat(height, width, CV_8UC3, data);
cv::imwrite(MarshalString(filePath), mat);
}
};
}
步骤4: 在C#中使用包装器
csharp
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Threading.Tasks;
namespace Company.Application.UI.Services
{
public interface IImageProcessingService
{
Task<Bitmap> DetectFacesAsync(byte[] imageBytes, int width, int height);
Task<Bitmap> DetectEdgesAsync(byte[] imageBytes, int width, int height,
double threshold1, double threshold2);
Task<(double similarity, int x, int y)> MatchTemplateAsync(
byte[] sourceBytes, byte[] templateBytes,
int sourceWidth, int sourceHeight,
int templateWidth, int templateHeight);
Task SaveImageAsync(byte[] imageBytes, int width, int height, string filePath);
}
}
csharp
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using OpenCVWrapper;
namespace Company.Application.UI.Services
{
public class ImageProcessingService : IImageProcessingService
{
private ImageProcessor _processor;
public ImageProcessingService()
{
// 初始化C++处理器
_processor = new ImageProcessor(1920, 1080);
}
public Task<Bitmap> DetectFacesAsync(byte[] imageBytes, int width, int height)
{
return Task.Run(() =>
{
// 调用C++包装器
var managedImage = _processor.DetectFaces(imageBytes, width, height);
// 转换为Bitmap
return managedImage.ToBitmap();
});
}
public Task<Bitmap> DetectEdgesAsync(byte[] imageBytes, int width, int height,
double threshold1, double threshold2)
{
return Task.Run(() =>
{
var managedImage = _processor.DetectEdges(
imageBytes, width, height, threshold1, threshold2);
return managedImage.ToBitmap();
});
}
public Task<(double similarity, int x, int y)> MatchTemplateAsync(
byte[] sourceBytes, byte[] templateBytes,
int sourceWidth, int sourceHeight,
int templateWidth, int templateHeight)
{
return Task.Run(() =>
{
int x = 0, y = 0;
double similarity = _processor.MatchTemplate(
sourceBytes, templateBytes,
sourceWidth, sourceHeight,
templateWidth, templateHeight,
out x, out y);
return (similarity, x, y);
});
}
public Task SaveImageAsync(byte[] imageBytes, int width, int height, string filePath)
{
return Task.Run(() =>
{
_processor.SaveImage(imageBytes, width, height, filePath);
});
}
}
}
步骤5: 在ViewModel中使用服务
csharp
using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Controls;
using ReactiveUI;
using Company.Application.UI.Services;
namespace Company.Application.UI.ViewModels
{
public class FaceDetectionViewModel : ReactiveObject
{
private readonly IImageProcessingService _imageService;
[Reactive]
private BitmapImage _currentImage;
[Reactive]
private int _detectedFacesCount;
public ReactiveCommand<string, Unit> LoadImageCommand { get; }
public ReactiveCommand<Unit, Unit> DetectFacesCommand { get; }
public FaceDetectionViewModel(IImageProcessingService imageService)
{
_imageService = imageService;
// 创建命令
LoadImageCommand = ReactiveCommand.CreateFromTask<string>(LoadImageAsync);
DetectFacesCommand = ReactiveCommand.CreateFromTask(DetectFacesAsync);
}
private async Task LoadImageAsync(string filePath)
{
try
{
// 读取图像
var bitmap = new Bitmap(filePath);
// 转换为BitmapImage用于WPF显示
CurrentImage = ConvertBitmapToBitmapImage(bitmap);
Logs.LogInfo($"图像加载成功: {filePath}");
}
catch (Exception ex)
{
Logs.LogError(ex, "图像加载失败");
MessageBox.Show($"图像加载失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async Task DetectFacesAsync()
{
if (CurrentImage == null)
{
MessageBox.Show("请先加载图像", "提示",
MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
try
{
// 将BitmapImage转换为字节数组
var imageBytes = BitmapImageToBytes(CurrentImage);
// 调用C++ OpenCV服务
var resultBitmap = await _imageService.DetectFacesAsync(
imageBytes,
CurrentImage.PixelWidth,
CurrentImage.PixelHeight);
// 显示结果
CurrentImage = ConvertBitmapToBitmapImage(resultBitmap);
// 更新检测数量(这里简化处理,实际应从OpenCV返回)
DetectedFacesCount = 1;
Logs.LogInfo($"人脸检测完成,检测到 {DetectedFacesCount} 个人脸");
}
catch (Exception ex)
{
Logs.LogError(ex, "人脸检测失败");
MessageBox.Show($"检测失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private byte[] BitmapImageToBytes(BitmapImage bitmapImage)
{
using (var stream = new MemoryStream())
{
BitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapImage));
encoder.Save(stream);
return stream.ToArray();
}
}
private BitmapImage ConvertBitmapToBitmapImage(Bitmap bitmap)
{
using (var memory = new MemoryStream())
{
bitmap.Save(memory, ImageFormat.Png);
memory.Position = 0;
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memory;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
return bitmapImage;
}
}
}
}
C++/CLI内存管理最佳实践
cpp
// ✅ 正确:使用析构函数和终结器
public ref class SafeWrapper {
private:
NativeResource* nativeResource;
public:
SafeWrapper() {
nativeResource = new NativeResource();
}
// 析构函数(确定性清理,由Dispose调用)
~SafeWrapper() {
this->!SafeWrapper(); // 调用终结器
}
// 终结器(非确定性清理,由GC调用)
!SafeWrapper() {
if (nativeResource != nullptr) {
delete nativeResource;
nativeResource = nullptr;
}
}
};
// C#中使用
using (var wrapper = new SafeWrapper()) {
// 使用wrapper
} // 自动调用Dispose
// ❌ 错误:忘记清理内存
public ref class UnsafeWrapper {
NativeResource* nativeResource;
// 内存泄漏!没有清理代码
};
5. 模块间通信
Prism事件聚合器(Event Aggregator)
事件聚合器实现了发布-订阅模式,用于模块间松耦合通信。
模块A发布事件 ──► Event Aggregator ──► 模块B订阅事件
模块C订阅事件 ◄──────────────────────────┘
定义事件
csharp
// 1. 定义事件类(继承PubSubEvent<TPayload>)
public class ImageProcessedEvent : PubSubEvent<ImageProcessedEventArgs>
{
}
// 2. 定义事件参数
public class ImageProcessedEventArgs
{
public BitmapImage ProcessedImage { get; set; }
public int DetectedObjectsCount { get; set; }
public DateTime Timestamp { get; set; }
}
发布事件
csharp
public class ImageProcessingViewModel : ReactiveObject
{
private readonly IEventAggregator _eventAggregator;
public ImageProcessingViewModel(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
}
private async Task ProcessImageAsync()
{
// 处理图像
var result = await _imageService.ProcessAsync(imageBytes);
// 发布事件
_eventAggregator
.GetEvent<ImageProcessedEvent>()
.Publish(new ImageProcessedEventArgs
{
ProcessedImage = result.Image,
DetectedObjectsCount = result.ObjectCount,
Timestamp = DateTime.Now
});
}
}
订阅事件
csharp
public class DashboardViewModel : ReactiveObject, IDisposable
{
private readonly IEventAggregator _eventAggregator;
private SubscriptionToken _subscriptionToken;
[Reactive]
private BitmapImage _lastProcessedImaged;
public DashboardViewModel(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
// 订阅事件
_subscriptionToken = _eventAggregator
.GetEvent<ImageProcessedEvent>()
.Subscribe(OnImageProcessed);
}
private void OnImageProcessed(ImageProcessedEventArgs args)
{
// 在UI线程更新
RxApp.MainThreadScheduler.Schedule(() =>
{
LastProcessedImage = args.ProcessedImage;
DetectedObjectsCount = args.DetectedObjectsCount;
Logs.LogInfo($"收到图像处理事件: {args.Timestamp}");
});
}
public void Dispose()
{
// 取消订阅
_eventAggregator
.GetEvent<ImageProcessedEvent>()
.Unsubscribe(_subscriptionToken);
}
}
过滤器和线程选项
csharp
// 带过滤器的订阅
_subscriptionToken = _eventAggregator
.GetEvent<ImageProcessedEvent>()
.Subscribe(
OnImageProcessed,
ThreadOption.PublisherThread, // 在发布者线程执行
false, // 保持订阅者引用
args => args.DetectedObjectsCount > 0 // 过滤器:只处理有检测对象的事件
);
// 在UI线程订阅
_subscriptionToken = _eventAggregator
.GetEvent<ImageProcessedEvent>()
.Subscribe(
OnImageProcessed,
ThreadOption.UIThread // 确保在UI线程执行
);
实战开发指南
创建完整的视觉检测模块
模块结构
Company.VisionDetection/
├── VisionDetectionModule.cs
├── ViewModels/
│ ├── CameraCalibrationViewModel.cs
│ ├── PatternRecognitionViewModel.cs
│ └── QualityInspectionViewModel.cs
├── Views/
│ ├── CameraCalibrationView.xaml
│ ├── PatternRecognitionView.xaml
│ └── QualityInspectionView.xaml
├── Services/
│ ├── ICalibrationService.cs
│ ├── CalibrationService.cs
│ ├── IRecognitionService.cs
│ └── RecognitionService.cs
└── Converters/
└── CalibrationStatusConverter.cs
1. 相机标定视图
xml
<UserControl x:Class="Company.VisionDetection.Views.CameraCalibrationView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标题 -->
<TextBlock Grid.Row="0"
Text="🎯 相机标定"
FontSize="24"
FontWeight="Bold"
Margin="0,0,0,20"/>
<!-- 主内容 -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 左侧:相机图像 -->
<Border Grid.Column="0"
BorderBrush="Gray"
BorderThickness="1"
CornerRadius="5">
<Grid>
<Image Source="{Binding CurrentImage}"
Stretch="Uniform"/>
<!-- 检测到的角点 -->
<ItemsControl ItemsSource="{Binding DetectedCorners}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Width="10"
Height="10"
Fill="Red"
Stroke="White"
StrokeThickness="2">
<Ellipse.RenderTransform>
<TranslateTransform X="{Binding X}"
Y="{Binding Y}"/>
</Ellipse.RenderTransform>
</Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 加载动画 -->
<mah:ProgressRing Grid.Column="0"
IsActive="{Binding IsCapturing}"
Width="80"
Height="80"/>
</Grid>
</Border>
<!-- 右侧:控制面板 -->
<StackPanel Grid.Column="1" Margin="20,0,0,0">
<!-- 标定参数 -->
<GroupBox Header="标定参数" Margin="0,0,0,15">
<StackPanel>
<TextBlock Text="棋盘格行数:" Margin="0,0,0,5"/>
<mah:NumericUpdown Value="{Binding RowsCount}"
Minimum="3"
Maximum="20"
Margin="0,0,0,10"/>
<TextBlock Text="棋盘格列数:" Margin="0,0,0,5"/>
<mah:NumericUpdown Value="{Binding ColsCount}"
Minimum="3"
Maximum="20"
Margin="0,0,0,10"/>
<TextBlock Text="方格尺寸(mm):" Margin="0,0,0,5"/>
<mah:NumericUpdown Value="{Binding SquareSize}"
Minimum="10"
Maximum="100"
Interval="5"/>
</StackPanel>
</GroupBox>
<!-- 操作按钮 -->
<GroupBox Header="操作" Margin="0,0,0,15">
<StackPanel>
<Button Content="📷 捕获图像"
Command="{Binding CaptureImageCommand}"
Height="35"
Margin="0,0,0,10"/>
<Button Content="🔍 检测角点"
Command="{Binding DetectCornersCommand}"
Height="35"
Margin="0,0,0,10"/>
<Button Content="✅ 开始标定"
Command="{Binding StartCalibrationCommand}"
Height="35"
Margin="0,0,0,10"/>
<Button Content="💾 保存结果"
Command="{Binding SaveCalibrationCommand}"
Height="35"/>
</StackPanel>
</GroupBox>
<!-- 标定状态 -->
<GroupBox Header="标定状态">
<StackPanel>
<TextBlock Text="已捕获图像:"
Margin="0,0,0,5"/>
<TextBlock Text="{Binding CapturedImagesCount}"
FontSize="18"
FontWeight="Bold"
Margin="0,0,0,15"/>
<TextBlock Text="标定状态:"
Margin="0,0,0,5"/>
<TextBlock Text="{Binding CalibrationStatus}"
Foreground="{Binding CalibrationStatus,
Converter={StaticResource StatusConverter}}"
FontSize="14"
FontWeight="Bold"/>
</StackPanel>
</GroupBox>
</StackPanel>
</Grid>
<!-- 底部:标定结果 -->
<Expander Grid.Row="2"
Header="标定结果"
IsExpanded="{Binding IsCalibrationComplete}"
Margin="0,20,0,0">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="相机内参矩阵:" FontWeight="Bold"/>
<TextBox Text="{Binding CameraMatrix}"
IsReadOnly="True"
Height="80"
Margin="0,5"/>
<TextBlock Text="畸变系数:"
FontWeight="Bold"
Margin="0,10,0,5"/>
<TextBox Text="{Binding DistortionCoefficients}"
IsReadOnly="True"
Height="40"
Margin="0,5"/>
</StackPanel>
<StackPanel Grid.Column="1" Margin="20,0,0,0">
<TextBlock Text="重投影误差:" FontWeight="Bold"/>
<TextBlock Text="{Binding ReprojectionError, StringFormat='{}{0:F4} 像素'}"
FontSize="18"
FontWeight="Bold"
Foreground="Green"/>
</StackPanel>
</Grid>
</Expander>
</Grid>
</UserControl>
csharp
using System;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using ReactiveUI;
using Company.VisionDetection.Services;
namespace Company.VisionDetection.ViewModels
{
public class CameraCalibrationViewModel : ReactiveObject
{
private readonly ICalibrationService _calibrationService;
[Reactive]
private BitmapImage _currentImage;
[Reactive]
private bool _isCapturing;
[Reactive]
private int _rowsCount = 9;
[Reactive]
private int _colsCount = 6;
[Reactive]
private double _squareSize = 25.0;
[Reactive]
private int _capturedImagesCount;
[Reactive]
private string _calibrationStatus = "未开始";
[Reactive]
private bool _isCalibrationComplete;
[ObservableAsPropertyProperty]
public string CameraMatrix { get; }
[ObservableAsPropertyProperty]
public string DistortionCoefficients { get; }
[ObservableAsPropertyProperty]
public double ReprojectionError { get; }
public ObservableCollection<CornerPoint> DetectedCorners { get; }
= new ObservableCollection<CornerPoint>();
public ReactiveCommand<Unit, Unit> CaptureImageCommand { get; }
public ReactiveCommand<Unit, Unit> DetectCornersCommand { get; }
public ReactiveCommand<Unit, Unit> StartCalibrationCommand { get; }
public ReactiveCommand<Unit, Unit> SaveCalibrationCommand { get; }
public CameraCalibrationViewModel(ICalibrationService calibrationService)
{
_calibrationService = calibrationService;
// 创建命令
var canCapture = this.WhenAnyValue(x => x.IsCapturing).Select(isCapturing => !isCapturing);
var canDetect = this.WhenAnyValue(
x => x.CurrentImage,
x => x.IsCapturing
).Select(tuple => tuple.Item1 != null && !tuple.Item2);
CaptureImageCommand = ReactiveCommand.CreateFromTask(
CaptureImageAsync, canCapture);
DetectCornersCommand = ReactiveCommand.CreateFromTask(
DetectCornersAsync, canDetect);
StartCalibrationCommand = ReactiveCommand.CreateFromTask(
StartCalibrationAsync,
this.WhenAnyValue(x => x.CapturedImagesCount).Select(count => count >= 3));
SaveCalibrationCommand = ReactiveCommand.CreateFromTask(
SaveCalibrationAsync,
this.WhenAnyValue(x => x.IsCalibrationComplete));
}
private async Task CaptureImageAsync()
{
try
{
IsCapturing = true;
// 从相机捕获图像(这里模拟)
await Task.Delay(500);
// 实际项目中应从相机服务获取
var capturedImage = await _calibrationService.CaptureImageAsync();
CurrentImage = ConvertBitmapToBitmapImage(capturedImage);
Logs.LogInfo("图像捕获成功");
}
catch (Exception ex)
{
Logs.LogError(ex, "图像捕获失败");
MessageBox.Show($"捕获失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
IsCapturing = false;
}
}
private async Task DetectCornersAsync()
{
try
{
if (CurrentImage == null) return;
var imageBytes = BitmapImageToBytes(CurrentImage);
// 调用C++ OpenCV服务检测角点
var corners = await _calibrationService.DetectChessboardCornersAsync(
imageBytes,
CurrentImage.PixelWidth,
CurrentImage.PixelHeight,
RowsCount,
ColsCount);
// 更新UI
DetectedCorners.Clear();
foreach (var corner in corners)
{
DetectedCorners.Add(new CornerPoint { X = corner.X, Y = corner.Y });
}
CalibrationStatus = $"检测到 {corners.Count} 个角点";
Logs.LogInfo($"角点检测完成,检测到 {corners.Count} 个角点");
}
catch (Exception ex)
{
Logs.LogError(ex, "角点检测失败");
MessageBox.Show($"检测失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async Task StartCalibrationAsync()
{
try
{
CalibrationStatus = "标定中...";
// 执行相机标定
var result = await _calibrationService.CalibrateCameraAsync(
RowsCount, ColsCount, SquareSize);
// 更新结果
CameraMatrix = result.CameraMatrix;
DistortionCoefficients = result.DistortionCoefficients;
ReprojectionError = result.ReprojectionError;
IsCalibrationComplete = true;
CalibrationStatus = "标定完成";
Logs.LogInfo($"相机标定完成,重投影误差: {result.ReprojectionError:F4} 像素");
}
catch (Exception ex)
{
Logs.LogError(ex, "相机标定失败");
CalibrationStatus = "标定失败";
MessageBox.Show($"标定失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async Task SaveCalibrationAsync()
{
try
{
var saveDialog = new SaveFileDialog
{
Filter = "XML文件|*.xml|所有文件|*.*",
FileName = "camera_calibration.xml"
};
if (saveDialog.ShowDialog() == true)
{
await _calibrationService.SaveCalibrationResultAsync(saveDialog.FileName);
MessageBox.Show("标定结果已保存", "成功",
MessageBoxButton.OK, MessageBoxImage.Information);
}
}
catch (Exception ex)
{
Logs.LogError(ex, "保存标定结果失败");
MessageBox.Show($"保存失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private byte[] BitmapImageToBytes(BitmapImage bitmapImage)
{
using (var stream = new MemoryStream())
{
BitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapImage));
encoder.Save(stream);
return stream.ToArray();
}
}
private BitmapImage ConvertBitmapToBitmapImage(Bitmap bitmap)
{
using (var memory = new MemoryStream())
{
bitmap.Save(memory, ImageFormat.Png);
memory.Position = 0;
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memory;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
return bitmapImage;
}
}
}
public class CornerPoint
{
public double X { get; set; }
public double Y { get; set; }
}
}
OpenCV集成完整示例
C++图像处理核心库
CMakeLists.txt
cmake
cmake_minimum_required(VERSION 3.20)
project(OpenCVNativeCore)
set(CMAKE_CXX_STANDARD 17)
# 查找OpenCV
find_package(OpenCV REQUIRED)
# 源文件
set(SOURCES
src/ImageProcessor.cpp
src/CalibrationService.cpp
src/PatternRecognition.cpp
)
set(HEADERS
include/ImageProcessor.h
include/CalibrationService.h
include/PatternRecognition.h
)
# 创建静态库
add_library(OpenCVNativeCore STATIC ${SOURCES} ${HEADERS})
# 包含目录
target_include_directories(OpenCVNativeCore PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${OpenCV_INCLUDE_DIRS}
)
# 链接库
target_link_libraries(OpenCVNativeCore PUBLIC
${OpenCV_LIBS}
)
# Windows DLL配置
if(WIN32)
set_target_properties(OpenCVNativeCore PROPERTIES
WINDOWS_EXPORT_ALL_SYMBOLS ON
)
endif()
ImageProcessor.h
cpp
#pragma once
#include <opencv2/opencv.hpp>
#include <vector>
#include <memory>
namespace OpenCVNative {
struct ProcessedResult {
cv::Mat processedImage;
std::vector<cv::Point2f> detectedPoints;
double processingTime;
std::string message;
};
struct DetectionResult {
cv::Rect boundingBox;
double confidence;
std::string label;
};
class ImageProcessor {
public:
ImageProcessor();
~ImageProcessor();
// 图像预处理
cv::Mat Preprocess(const cv::Mat& input);
cv::Mat EnhanceContrast(const cv::Mat& input, double alpha = 1.5, double beta = 0.0);
cv::Mat Denoise(const cv::Mat& input, int h = 10);
// 边缘检测
cv::Mat DetectEdges(const cv::Mat& input, double threshold1 = 50.0,
double threshold2 = 150.0);
std::vector<cv::Point2f> DetectCorners(const cv::Mat& input,
int maxCorners = 100,
double qualityLevel = 0.01,
double minDistance = 10.0);
// 轮廓检测
std::vector<std::vector<cv::Point>> DetectContours(const cv::Mat& input,
double minArea = 100.0);
cv::Mat DrawContours(const cv::Mat& input,
const std::vector<std::vector<cv::Point>>& contours,
const cv::Scalar& color = cv::Scalar(0, 255, 0),
int thickness = 2);
// 模板匹配
cv::Point MatchTemplate(const cv::Mat& input, const cv::Mat& templ,
double& maxVal, int method = cv::TM_CCOEFF_NORMED);
// 颜色检测
cv::Mat DetectColor(const cv::Mat& input, const cv::Scalar& lower,
const cv::Scalar& upper);
// 形态学操作
cv::Mat MorphologyOpen(const cv::Mat& input, int kernelSize = 5);
cv::Mat MorphologyClose(const cv::Mat& input, int kernelSize = 5);
cv::Mat MorphologyGradient(const cv::Mat& input, int kernelSize = 5);
private:
cv::Ptr<cv::CLAHE> clahe;
void InitializeCLAHE();
};
} // namespace OpenCVNative
ImageProcessor.cpp
cpp
#include "ImageProcessor.h"
#include <chrono>
namespace OpenCVNative {
ImageProcessor::ImageProcessor() {
InitializeCLAHE();
}
ImageProcessor::~ImageProcessor() {
// 析构函数
}
void ImageProcessor::InitializeCLAHE() {
clahe = cv::createCLAHE(2.0, cv::Size(8, 8));
}
cv::Mat ImageProcessor::Preprocess(const cv::Mat& input) {
cv::Mat output;
// 1. 降噪
cv::Mat denoised;
cv::fastNlMeansDenoisingColored(input, denoised, 10.0, 10.0, 7, 21);
// 2. 锐化
cv::Mat sharpened;
cv::Mat kernel = (cv::Mat_<float>(3, 3) <<
0, -1, 0,
-1, 5, -1,
0, -1, 0
);
cv::filter2D(denoised, sharpened, -1, kernel);
// 3. 自适应直方图均衡化
cv::Mat lab, channels[3];
cv::cvtColor(sharpened, lab, cv::COLOR_BGR2Lab);
cv::split(lab, channels);
clahe->apply(channels[0], channels[0]);
cv::merge(channels, 3, lab);
cv::cvtColor(lab, output, cv::COLOR_Lab2BGR);
return output;
}
cv::Mat ImageProcessor::EnhanceContrast(const cv::Mat& input, double alpha, double beta) {
cv::Mat output;
input.convertTo(output, -1, alpha, beta);
return output;
}
cv::Mat ImageProcessor::Denoise(const cv::Mat& input, int h) {
cv::Mat output;
cv::fastNlMeansDenoisingColored(input, output, h, h, 7, 21);
return output;
}
cv::Mat ImageProcessor::DetectEdges(const cv::Mat& input, double threshold1,
double threshold2) {
cv::Mat gray, edges;
// 转换为灰度图
if (input.channels() == 3) {
cv::cvtColor(input, gray, cv::COLOR_BGR2GRAY);
} else {
gray = input.clone();
}
// 高斯模糊降噪
cv::GaussianBlur(gray, gray, cv::Size(5, 5), 1.4);
// Canny边缘检测
cv::Canny(gray, edges, threshold1, threshold2);
// 转换回彩色用于显示
cv::Mat output;
cv::cvtColor(edges, output, cv::COLOR_GRAY2BGR);
return output;
}
std::vector<cv::Point2f> ImageProcessor::DetectCorners(const cv::Mat& input,
int maxCorners,
double qualityLevel,
double minDistance) {
cv::Mat gray;
// 转换为灰度图
if (input.channels() == 3) {
cv::cvtColor(input, gray, cv::COLOR_BGR2GRAY);
} else {
gray = input.clone();
}
std::vector<cv::Point2f> corners;
std::vector<cv::KeyPoint> keypoints;
// 使用FAST检测器
auto detector = cv::FastFeatureDetector::create(10);
detector->detect(gray, keypoints);
// 转换为Point2f
for (const auto& kp : keypoints) {
corners.push_back(kp.pt);
}
return corners;
}
std::vector<std::vector<cv::Point>> ImageProcessor::DetectContours(
const cv::Mat& input, double minArea) {
cv::Mat gray, binary;
// 转换为灰度图
if (input.channels() == 3) {
cv::cvtColor(input, gray, cv::COLOR_BGR2GRAY);
} else {
gray = input.clone();
}
// 二值化
cv::threshold(gray, binary, 127, 255, cv::THRESH_BINARY);
// 查找轮廓
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL,
cv::CHAIN_APPROX_SIMPLE);
// 过滤小轮廓
std::vector<std::vector<cv::Point>> filteredContours;
for (const auto& contour : contours) {
double area = cv::contourArea(contour);
if (area >= minArea) {
filteredContours.push_back(contour);
}
}
return filteredContours;
}
cv::Mat ImageProcessor::DrawContours(const cv::Mat& input,
const std::vector<std::vector<cv::Point>>& contours,
const cv::Scalar& color, int thickness) {
cv::Mat output = input.clone();
cv::drawContours(output, contours, -1, color, thickness);
return output;
}
cv::Point ImageProcessor::MatchTemplate(const cv::Mat& input, const cv::Mat& templ,
double& maxVal, int method) {
cv::Mat result;
// 模板匹配
cv::matchTemplate(input, templ, result, method);
// 归一化
cv::normalize(result, result, 0, 1, cv::NORM_MINMAX, -1, cv::Mat());
// 找到最佳匹配位置
double minVal;
cv::Point minLoc, maxLoc;
cv::minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc);
if (method == cv::TM_SQDIFF || method == cv::TM_SQDIFF_NORMED) {
maxVal = 1.0 - minVal;
return minLoc;
} else {
return maxLoc;
}
}
cv::Mat ImageProcessor::DetectColor(const cv::Mat& input,
const cv::Scalar& lower,
const cv::Scalar& upper) {
cv::Mat hsv, mask, result;
// 转换为HSV颜色空间
cv::cvtColor(input, hsv, cv::COLOR_BGR2HSV);
// 颜色范围检测
cv::inRange(hsv, lower, upper, mask);
// 形态学操作,去除噪声
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5, 5));
cv::morphologyEx(mask, mask, cv::MORPH_CLOSE, kernel);
// 提取结果
cv::bitwise_and(input, input, result, mask);
return result;
}
cv::Mat ImageProcessor::MorphologyOpen(const cv::Mat& input, int kernelSize) {
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE,
cv::Size(kernelSize, kernelSize));
cv::Mat output;
cv::morphologyEx(input, output, cv::MORPH_OPEN, kernel);
return output;
}
cv::Mat ImageProcessor::MorphologyClose(const cv::Mat& input, int kernelSize) {
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE,
cv::Size(kernelSize, kernelSize));
cv::Mat output;
cv::morphologyEx(input, output, cv::MORPH_CLOSE, kernel);
return output;
}
cv::Mat ImageProcessor::MorphologyGradient(const cv::Mat& input, int kernelSize) {
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE,
cv::Size(kernelSize, kernelSize));
cv::Mat output;
cv::morphologyEx(input, output, cv::MORPH_GRADIENT, kernel);
return output;
}
} // namespace OpenCVNative
C#服务层
csharp
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using OpenCVWrapper;
namespace Company.VisionDetection.Services
{
public class ImageProcessingService : IImageProcessingService
{
private ImageProcessor _processor;
public ImageProcessingService()
{
_processor = new ImageProcessor(1920, 1080);
}
public async Task<byte[]> PreprocessImageAsync(byte[] imageBytes, int width, int height)
{
return await Task.Run(() =>
{
var result = _processor.Preprocess(imageBytes, width, height);
return MatToBytes(result, width, height);
});
}
public async Task<List<Point>> DetectCornersAsync(byte[] imageBytes, int width, int height,
int maxCorners = 100,
double qualityLevel = 0.01,
double minDistance = 10.0)
{
return await Task.Run(() =>
{
var corners = _processor.DetectCorners(imageBytes, width, height,
maxCorners, qualityLevel, minDistance);
return corners;
});
}
public async Task<byte[]> DetectEdgesAsync(byte[] imageBytes, int width, int height,
double threshold1 = 50.0,
double threshold2 = 150.0)
{
return await Task.Run(() =>
{
var result = _processor.DetectEdges(imageBytes, width, height,
threshold1, threshold2);
return MatToBytes(result, width, height);
});
}
private byte[] MatToBytes(IntPtr matData, int width, int height)
{
// 从OpenCV Mat数据转换为字节数组
byte[] bytes = new byte[width * height * 3];
Marshal.Copy(matData, bytes, 0, bytes.Length);
return bytes;
}
}
}
性能优化与最佳实践
1. 异步处理
csharp
// ✅ 正确:异步处理图像
public async Task ProcessImageAsync()
{
await Task.Run(() =>
{
// CPU密集型操作在后台线程
var result = _imageService.Process(image);
});
// UI更新在主线程
CurrentImage = result;
}
// ❌ 错误:阻塞UI线程
public void ProcessImage()
{
var result = _imageService.Process(image); // 阻塞UI
CurrentImage = result;
}
2. 内存管理
csharp
// ✅ 正确:及时释放资源
using (var bitmap = new Bitmap(path))
{
// 处理图像
} // 自动释放
// ✅ 正确:手动释放
var bitmap = new Bitmap(path);
try
{
// 处理图像
}
finally
{
bitmap?.Dispose();
}
// ❌ 错误:忘记释放
var bitmap = new Bitmap(path);
// 处理图像
// 内存泄漏!
3. 图像缓存
csharp
public class ImageCacheService
{
private readonly Dictionary<string, BitmapImage> _cache = new();
private readonly object _lock = new();
public BitmapImage GetOrLoad(string path)
{
lock (_lock)
{
if (_cache.TryGetValue(path, out var cached))
{
return cached;
}
var bitmap = LoadImage(path);
_cache[path] = bitmap;
return bitmap;
}
}
public void Clear()
{
lock (_lock)
{
foreach (var item in _cache.Values)
{
// 释放资源
}
_cache.Clear();
}
}
}
4. 批量处理
csharp
// ✅ 正确:批量处理
public async Task ProcessBatchAsync(List<string> imagePaths)
{
var tasks = imagePaths.Select(path =>
ProcessImageAsync(path)
);
await Task.WhenAll(tasks);
}
// ❌ 错误:串行处理
public async Task ProcessBatch(List<string> imagePaths)
{
foreach (var path in imagePaths)
{
await ProcessImageAsync(path); // 串行执行
}
}
总结
核心要点回顾
-
Prism框架
- 模块化架构:
IModule接口 - 依赖注入:
RegisterTypes方法 - Region导航:
RegionManager.RequestNavigate - 事件聚合器:
IEventAggregator
- 模块化架构:
-
ReactiveUI
- 响应式属性:
[Reactive]和[ObservableAsPropertyProperty] - 属性监听:
WhenAnyValue - 响应式命令:
ReactiveCommand
- 响应式属性:
-
C++/CLI互操作
- 托管代码:
public ref class - 原生代码:
class或struct - 数据封送:
Marshal类 - 内存管理:析构函数
~和终结器!
- 托管代码:
-
OpenCV集成
- 图像预处理:降噪、增强、锐化
- 特征检测:边缘、角点、轮廓
- 模板匹配:
matchTemplate - 颜色检测:HSV空间