WPF MVVM 模式(调Prism库)项目创建笔记 —— 包含C++/CLI 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应用生命周期

App.xaml.cs

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: 创建模块类

ApplicationModule.cs

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中注册模块

App.xaml.cs

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

ShellView.xaml

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的自动关联

ApplicationModule.cs

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中实现导航逻辑

ShellViewModel.cs

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中引入资源

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: 创建包装器类

OpenCVWrapper.h

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#中使用包装器

IImageProcessingService.cs

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);
    }
}

ImageProcessingService.cs

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中使用服务

FaceDetectionViewModel.cs

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. 相机标定视图

CameraCalibrationView.xaml

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>

CameraCalibrationViewModel.cs

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);  // 串行执行
    }
}

总结

核心要点回顾

  1. Prism框架

    • 模块化架构:IModule接口
    • 依赖注入:RegisterTypes方法
    • Region导航:RegionManager.RequestNavigate
    • 事件聚合器:IEventAggregator
  2. ReactiveUI

    • 响应式属性:[Reactive][ObservableAsPropertyProperty]
    • 属性监听:WhenAnyValue
    • 响应式命令:ReactiveCommand
  3. C++/CLI互操作

    • 托管代码:public ref class
    • 原生代码:classstruct
    • 数据封送:Marshal
    • 内存管理:析构函数~和终结器!
  4. OpenCV集成

    • 图像预处理:降噪、增强、锐化
    • 特征检测:边缘、角点、轮廓
    • 模板匹配:matchTemplate
    • 颜色检测:HSV空间
复制代码
相关推荐
清辞8532 小时前
【Day4】C++竞赛每日练习
数据结构·c++·算法
‎ദ്ദിᵔ.˛.ᵔ₎2 小时前
C++ 继承
开发语言·c++
朱一头zcy2 小时前
Java基础复习08:IO流(File类与IO流概述、字节输入输出流、字符输入输出流、缓冲流、字符转换流、对象序列化、打印流、Commons-io包介绍)
java·笔记
史迪仔01122 小时前
[QML] Popup 与 Dialog
开发语言·前端·javascript·c++·qt
John.Lewis2 小时前
C++加餐课-stack_queue:计算器-逆波兰表达式
开发语言·c++
七夜zippoe2 小时前
DolphinDB数据模型:表、分区与分布式表
分布式·wpf·数据模型··dolphindb
SilentSamsara2 小时前
Linux 管道与重定向:命令行精髓的结构性解析
linux·运维·服务器·c++·云原生
zjeweler11 小时前
“网安+护网”终极300多问题面试笔记-3共3-综合题型(最多)
笔记·网络安全·面试·职场和发展·护网行动
lclin_202011 小时前
VS2010兼容|C++系统全能监控工具(彩色界面+日志带单位+完整版)
c++·windows·系统监控·vs2010·编程实战