尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

.NET 6 + WPF + OpenCVSharp + ReactiveUI 构建机器视觉上位机实践

.NET 6 + WPF + OpenCVSharp + ReactiveUI 构建机器视觉上位机实践 做机器视觉相关的上位机开发如果你恰好又是个 .NET 程序员那这几年应该没少在几个技术栈之间来回折腾。我以前用 WinForms 写过不少工具后来换 WPF再后来发现图像处理不引 OpenCV 根本玩不转等界面逻辑复杂到一定程度MVVM 框架不上一套又不行。这篇文章就是把我目前最顺手的这套组合——.NET 6 WPF OpenCVSharp ReactiveUI——原原本本拆开讲一遍为什么这么搭、工程怎么建、代码怎么写、坑在哪里。适合正在选型的人参考也适合已经上手但被各种细节卡住的朋友对照排查。1. 为什么把这四个东西拼在一起1.1 .NET 6 到底解决了什么很多老项目现在还停在 .NET Framework 4.7.2 或者 .NET Core 3.1如果只是写点内部小工具说实话区别不大。但一旦涉及工业视觉、视频流处理、多线程调度这些场景.NET 6 的优势就非常明显了。首先是性能。.NET 6 是首个 LTS 版本JIT 引入了 Tiered Compilation 的进一步优化实际跑图像处理循环、大数组运算的时候和 .NET Core 3.1 比是有体感提升的。其次是 API 的整合比如System.Drawing在跨平台项目里被各种吐槽而 .NET 6 里虽然也没把 System.Drawing 完全干掉但结合 OpenCVSharp 根本不需要直接接触 Bitmap图像数据全程用 Mat 传递内存可控得多。还有一个常被忽略的点.NET 6 的项目文件简化得干干净净SDK 风格默认开启NuGet 引用几行搞定。你不再需要面对 packages.config 或者一堆 AssemblyInfo 的配置文件这对做视觉工具这类快速迭代的软件非常重要。1.2 WPF 仍然是 Windows 桌面 UI 的最优解之一虽然 MAUI 宣传得很热闹但真要做一个严肃的 Windows 上位机应用WPF 还是最稳的选择。它的数据绑定、模板、样式系统在 Windows 生态里沉淀了十几年成熟度和生态资源不是新框架能比的。WPF 在视觉呈现上有两个核心优势一个是渲染管线基于 DirectX动画和图像刷新足够流畅另一个是布局系统非常自由适合做工业软件里那种复杂的仪器面板、实时数据显示、多区域仪表盘布局。再加上第三方控件库比如 HandyControl、MaterialDesignInXAML界面做出来体面很多。工业软件丑了十几年现在用 WPF 不用花太多功夫就能提升一个档次。1.3 OpenCVSharp图像处理库里的实用派OpenCV 是图像处理和计算机视觉的事实标准而 OpenCVSharp 是 .NET 平台封装做得最顺手的一个版本。它在 GitHub 上有中文文档社区活跃API 设计和 C 原版非常接近熟悉 OpenCV 的人几乎零成本上手。举个例子我要做模板匹配C 里写matchTemplateC# 里就是Cv2.MatchTemplate参数几乎一致。这在参考算法原型、读 OpenCV 官方文档的时候特别省心。更重要的是OpenCVSharp 直接封装了 Mat 类型和 WPF 的BitmapSource之间只需要做一次像素格式转换实时显示视频流完全够用。引用一句话总结我的感受OpenCVSharp 是那种“你不觉得它多厉害但没了它你写图像处理会痛苦十倍”的库。1.4 ReactiveUI让 MVVM 从“能用”变成“好用”很多 WPF 项目从 Code-Behind 迁到 MVVM 之后问题迎刃而解但代码写多了又会遇到新的麻烦属性通知代码重复、异步操作难管理、事件和命令的订阅散落得到处都是。这时候 ReactiveUI 的价值就出来了。ReactiveUI 是以 Reactive Extensions 为基础的 MVVM 框架它把“属性变化”“命令执行”“事件触发”都统一成了可观察的数据流。以前在 ViewModel 里写一堆 INotifyPropertyChanged 样板代码现在一个RaiseAndSetIfChanged搞定不说还能用WhenAnyValue把多个属性的联动关系声明式地写出来代码可读性和维护性高很多。更实际的好处是它的异步支持。比如相机采图、图像处理这些耗时操作ReactiveUI 的ReactiveCommand自带异步执行、并发控制、执行状态跟踪不用再手工写async/await加 try-catch 加按钮禁用恢复这一套一个命令对象全包了。2. 项目结构设计与工程搭建2.1 整体分层架构这套组合的定位是桌面端的视觉应用/上位机常见的场景是相机采集一帧图像经过图像处理后在界面上显示结果同时把数据记录下来或者对接 PLC。基于这个定位我把项目分成四层界面层ViewWPF 的 XAML 窗口、用户控件、样式资源视图模型层ViewModel所有界面状态、交互逻辑、命令、数据绑定服务层Service相机接口、图像处理算法、日志、配置读写、通信模块模型层Model数据结构、枚举、常量。这个分层的原则是ViewModel 不直接引用 OpenCVSharp 和相机 SDK 的类型。图像处理的结果统一抽象成自定义的数据模型比如ProcessResult这样更换相机品牌或者替换算法库时需要改动的范围被限制在服务层不至于把整个界面逻辑推翻重来。2.2 创建项目和 NuGet 包选择用 .NET 6 创建一个 WPF 项目很简单Visual Studio 2022 里直接选“WPF 应用程序”模板目标框架选 .NET 6.0。如果你喜欢命令行也可以用dotnet new wpf -n MyVisionApp。接下来是 NuGet 包。我当前项目用的版本组合是包名版本用途OpenCvSharp44.x核心图像处理库OpenCvSharp4.runtime.win4.xWindows 本地运行库包含了 OpenCV 原生 DLLReactiveUI18.xMVVM 框架基础ReactiveUI.WPF18.xWPF 平台注册绑定 WPF 调度器HandyControl3.x引入一套现代化的 WPF 控件样式省去很多控件模板编写时间注意OpenCvSharp4 和 OpenCvSharp4.runtime.win 必须一起引而且版本要一致。只引主包不引 runtime,程序跑起来会在加载OpenCvSharp.dll时抛BadImageFormatException或者找不到 native 依赖。ReactiveUI.WPF 一定要装否则它默认使用的是 Reactive Extensions 的默认调度器在 WPF 里更新界面时会出现跨线程访问的问题。2.3 工程目录规划具体的目录结构我建议这样设计MyVisionApp/ ├── App.xaml ├── Views/ │ ├── MainWindow.xaml │ ├── CameraView.xaml │ └── ResultView.xaml ├── ViewModels/ │ ├── MainViewModel.cs │ ├── CameraViewModel.cs │ └── ResultViewModel.cs ├── Services/ │ ├── ICameraService.cs │ ├── CameraService.cs │ ├── IImageProcessService.cs │ └── ImageProcessService.cs ├── Models/ │ ├── ProcessResult.cs │ ├── CameraConfig.cs │ └── ApplicationConfig.cs ├── Helpers/ │ └── MatToBitmapSourceConverter.cs └── Resources/ ├── Styles/ └── Images/ViewModel 和 View 放在不同的文件夹用命名空间区分。这样分层清晰后人接手也容易定位代码。3. 相机采集与图像处理管线实战3.1 相机帧数据的抓取流程相机采图是视觉应用的第一步。工业相机 SDK 各有不同但大体的流程是初始化相机列表选择指定相机设置采集参数比如曝光时间、增益、触发模式开始采集循环从缓冲区取帧拿到原始图像数据后封装成 OpenCVSharp 的 Mat处理完之后把 Mat 转成 BitmapSource 显示到界面上。以海康相机为例SDK 回调函数里会提供一个IntPtr指针指向图像数据缓冲区我把它转成 Mat 的写法如下public Mat FrameToMat(IntPtr data, int width, int height, int stride) { var mat new Mat(height, width, MatType.CV_8UC3, data, stride); return mat.Clone(); }注意这里用了Clone()。因为相机的缓冲区是 SDK 内部管理的回调函数返回之后缓冲区可能被覆盖如果不克隆后面图像处理时数据随时会变。这是个非常容易踩的坑尤其是当你把 Mat 扔进异步任务里去处理的时候。3.2 图像处理的流程编排图像处理管线的编排要抽象成一个服务接口这样算法替换、参数调整都不会破坏上层代码。我习惯这样设计public interface IImageProcessService { ProcessResult Process(Mat input); }具体实现里就是一连串的 OpenCV 操作。以一个简单的外观检测流程为例灰度化、高斯滤波、二值化、找轮廓、筛选轮廓、绘制结果。用 OpenCVSharp 的写法是public ProcessResult Process(Mat input) { using var gray new Mat(); using var blurred new Mat(); using var binary new Mat(); using var output input.Clone(); Cv2.CvtColor(input, gray, ColorConversionCodes.BGR2GRAY); Cv2.GaussianBlur(gray, blurred, new Size(5, 5), 0); Cv2.Threshold(blurred, binary, 0, 255, ThresholdTypes.Otsu); Cv2.FindContours(binary, out var contours, out var hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple); var result new ProcessResult(); foreach (var contour in contours) { var area Cv2.ContourArea(contour); if (area 500) { var rect Cv2.BoundingRect(contour); Cv2.Rectangle(output, rect, new Scalar(0, 0, 255), 2); result.Items.Add(new DefectInfo { X rect.X, Y rect.Y, Width rect.Width, Height rect.Height }); } } result.OutputImage output; return result; }这里有个细节值得说Mat 是实现了 IDisposable 的中间变量我全部用using包住避免内存泄漏。但输出图像不能释放因为要送给界面显示。到底谁负责释放项目里必须有明确约定我写的是服务方法只负责返回结果不负责释放返回的 Mat上层拿到 Mat处理完显示逻辑后自己释放。3.3 帧率、延迟和性能调优做视觉最容易遇到的问题就是“图像卡顿”和“CPU 爆高”。我在实际调优过程中总结了几条基本原则不要在 UI 线程做任何图像处理。采集回调、算法处理、文件保存这些全部扔到后台线程。WPF 里可以用Task.Run也可以让 ReactiveUI 的调度器来接管。控制帧率或者处理频率。有些场景相机出图 60 帧但检测算法需要 200ms这时候如果每一帧都去排队处理队列会越来越长延迟越拉越高。我通常在回调里判断一下上一帧处理是否仍在进行如果是直接丢弃当前帧。这样保证处理的是“最新帧”而不是“最旧帧”。图像缩放后显示。相机源图可能是 500 万像素甚至更高但界面上显示区域可能只有 1000 像素宽。把 500 万像素的 BitmapSource 直接放在界面上GPU 压力大内存也大。我习惯在显示前用Cv2.Resize缩放到一个合理的分辨率比如高度不超过 800。模板匹配等算法仍用原图数据只有最终显示用缩放图。利用 Mat 的连续内存特性。做像素级操作时尽量避免GetPixel/SetPixel这种逐像素方法调用太慢了。应该用Mat.GetArray/SetArray或者Mat.GetGenericIndexer一次性拿一整块数据操作。我测试过一个场景500 万像素灰度图用SetPixel逐像素遍历耗时接近 70ms而用Mat.GetArraybyte读取进 byte[] 再循环处理直接降到 5ms 以内。差距是数量级的。4. ReactiveUI 在 WPF 里的 MVVM 实践4.1 从 INotifyPropertyChanged 到 ReactiveUI用传统 MVVM 写属性通知每个属性要写一段成模板的代码private string _status; public string Status { get _status; set { _status value; OnPropertyChanged(); } }这种代码一个 ViewModel 里写十个八个还受得了写几十个就要疯了。ReactiveUI 让我直接这么写private string _status; public string Status { get _status; set this.RaiseAndSetIfChanged(ref _status, value); }如果是字段直接做属性简写方式也行。更重要的是ReactiveUI 把属性之间联动关系表达得非常干货。比如“只有图像处理完成才能点击保存按钮”传统写法得在多个 setter 里主动触发SaveCommand.CanExecute的更新而 ReactiveUI 里这样写_saveCommand ReactiveCommand.Create( SaveAction, this.WhenAnyValue(x x.IsProcessFinished, finished finished));IsProcessFinished变了命令自动重新计算能否执行UI 上的按钮状态跟着变。这套机制的本质是把命令和属性的关系声明式地建立起来逻辑复杂之后作用非常明显。4.2 命令绑定和交互处理ReactiveUI 中命令类的绑定View 里通常写Button Content开始检测 Command{Binding StartDetectCommand} /注意ReactiveUI 的命令类型是ReactiveCommandUnit, Unit或者ReactiveCommandTParam, TResult。如果是带参数的需要在 XAML 里设置CommandParameter。还有一类常见交互是弹窗、打开对话框、消息提示。传统做法是在 View 的 Code-Behind 里写事件处理但 MVVM 模式下最好用交互服务。ReactiveUI 本身提供了InteractionTInput, TOutput机制我一般用它来处理“确认删除”“选择文件”这类操作。ViewModel 里定义一个public Interactionstring, bool ConfirmInteraction { get; } new();触发时var confirmed await ConfirmInteraction.Handle(确定要删除该条记录吗?);View 里在注册交互时处理this.WhenActivated(d { d(ViewModel.ConfirmInteraction.RegisterHandler(async interaction { var result MessageBox.Show(interaction.Input, 确认, MessageBoxButton.YesNo); interaction.SetOutput(result MessageBoxResult.Yes); })); });这套代码把界面交互和业务逻辑解耦得很彻底。ViewModel 完全不知道 MessageBox 是什么以后想换自定义弹窗只需要改 View 层的注册逻辑。这也是我在项目里推荐的做法。4.3 后台任务与调度器ReactiveUI 和异步结合得好是它相比其他 MVVM 框架的巨大优势。图像处理、相机操作、文件读写这些都是耗时任务如果在命令执行过程中直接await Task.Run(...)命令对象会自动处理执行状态按钮自动禁用。写完这个逻辑完全不需要手写 IsBusy。一个典型的处理命令StartProcessCommand ReactiveCommand.CreateFromTask(async () { await Task.Run(() { var result _imageProcessService.Process(_cameraService.LatestFrame); ProcessResults.Add(result); IsProcessFinished true; }); });ReactiveCommand 的IsExecuting可以直接绑定到界面 Loader 或者进度条的可视性上ProgressBar IsIndeterminateTrue Visibility{Binding StartProcessCommand.IsExecuting, Converter{StaticResource BooleanToVisibilityConverter}} /这个写法可以省掉一半的布尔类型状态变量。需要特别提醒ReactiveUI 默认在 UI 线程调度,因此命令体内部更新集合、属性通常没问题。但如果你用Task.Run包裹了代码那么在另一个线程上更新集合仍然要回到 UI 线程。所以上面的例子是在 Task.Run 里算算完回到命令主线程再更新集合这样是安全的。反过来如果把集合更新也放进 Task.Run 里就会爆跨线程异常。5. WPF 界面设计中的高频细节5.1 图像实时显示Mat 转 BitmapSource 的正确姿势OpenCVSharp 的图像是 BGR 顺序而 WPF 的 BitmapSource 通常用 Bgra32 格式。如果直接转换不处理通道顺序显示的图像红蓝互换。我写了一个转换辅助类public static BitmapSource ToBitmapSource(this Mat mat) { if (mat.Empty()) return null; Mat converted; if (mat.Channels() 1) { converted mat.Clone(); } else { converted new Mat(); Cv2.CvtColor(mat, converted, ColorConversionCodes.BGR2BGRA); } var bitmap BitmapSource.Create( converted.Width, converted.Height, 96, 96, PixelFormats.Bgra32, null, converted.Data, converted.Step * converted.Height, converted.Step); return bitmap.Freeze(); }Freeze()这一步相当重要冻结后的 BitmapSource 可以跨线程访问不冻结的话图像在采集后台线程转成 BitmapSource 再传给 UI 线程时会有内存访问异常的风险。转完以后原始 Mat 就可以安全释放了。性能方面如果相机分辨率高且帧率要求高尽量复用 Mat。每帧都new Mat()又释放GC 压力很大。常见做法是开一个固定大小的 Mat循环里只做CopyTo或者直接在采集回调里填数据。5.2 用滚动条控制 PerspectiveCamera3D 场景交互WPF 的 3D 能力经常被低估实际上用 WPF 做轻量化 3D 展示是很方便的。比如用户想看工件表面的三维形貌或者想在视觉检测结果里渲染一个简单的 3D 包围盒用 WPF 的Viewport3D加PerspectiveCamera就够了。那“滚动条控制 PerspectiveCamera”是怎么回事我的实现思路是滚动条数值映射到相机的Position和LookDirection。比如一个纵向滚动条控制相机沿 Z 轴缩放横向滚动条控制绕 Y 轴旋转。this.WhenAnyValue( x x.ZoomValue, x x.RotateValue) .Subscribe((zoom, rotate) { var angle rotate / 100.0 * 360.0; var distance 500 - zoom * 10; Camera.Position new Point3D( distance * Math.Sin(angle), distance * Math.Cos(angle), distance * 0.6); Camera.LookDirection new Vector3D( -Camera.Position.X, -Camera.Position.Y, -Camera.Position.Z); Camera.UpDirection new Vector3D(0, 1, 0); });这里把滚动条的 Value 范围设置成 0-100映射成角度和距离操作起来手感比较线性。一个小经验Set 3D 相机后记得调用Viewport3D.InvalidateVisual()。因为场景中的几何模型没变化时WPF 不会自动重新渲染相机视角变化。不要问我为什么知道这个踩过坑的人都懂。5.3 DataGrid 列显示完整内容的思路热词里有“wpf datagrid.columns 鼠标放在上面 显示 完整内容”这几乎是做系统必然遇到的需求。DataGrid 列宽有限单元格内容超长被截断用户想知道完整内容最自然的交互是鼠标悬停显示 Tooltip。两种做法第一种列级 ToolTip。这个最简单直接在 DataGridTextColumn 绑定时设置DataGridTextColumn Header备注 Binding{Binding Remark} DataGridTextColumn.ElementStyle Style TargetTypeTextBlock Setter PropertyTextTrimming ValueCharacterEllipsis/ Setter PropertyToolTip Value{Binding Remark}/ /Style /DataGridTextColumn.ElementStyle /DataGridTextColumn注意这个绑定必须有TextTrimming这样用户看到截断的省略号自然知道悬停可以看完整内容。第二种单元格模板。如果不仅想显示文本还想显示状态、图片或者其他控件DataGridTemplateColumn Header备注 DataGridTemplateColumn.CellTemplate DataTemplate TextBlock Text{Binding Remark} TextTrimmingCharacterEllipsis ToolTip{Binding Remark}/ /DataTemplate /DataGridTemplateColumn.CellTemplate /DataGridTemplateColumn实际操作中还要注意一点当用户鼠标移出单元格时ToolTip 不会立刻消失而是有默认延迟。如果希望 ToolTip 响应更即时可以设置ToolTipService.ShowDuration和ToolTipService.InitialShowDelay。5.4 数据验证给用户明确的错误提示WPF 自带的数据验证机制配合控件库使用比如 HandyControl 里的 NumericUpDown最常见的问题是验证失败之后错误提示不明显或者根本不显示。如果你使用的是INotifyDataErrorInfo或者IDataErrorInfo实现数据验证需要确保控件触发验证的条件。以 NumericUpDown 为例默认情况下你输入非法字符时值的绑定不会更新验证不会触发界面没有任何反应。这时候要配合controls:NumericUpDown Value{Binding ExposureTime, ModeTwoWay, UpdateSourceTriggerPropertyChanged, ValidatesOnNotifyDataErrorsTrue, ValidatesOnDataErrorsTrue, NotifyOnValidationErrorTrue}/然后在 Validation.Error 事件里弹个提示或者用Validation.ErrorTemplate把验证失败的控件边框标红。HandyControl 还提供了InfoElement.Placeholder和ShowErrorTip之类的附加属性可以让你在控件下方显示一个错误提示气泡。我目前最顺手的做法是自定义一个Validation.ErrorTemplateControlTemplate x:KeyValidationErrorTemplate StackPanel AdornedElementPlaceholder/ TextBlock Text{Binding ErrorContent} ForegroundRed FontSize12 Margin0,2,0,0/ /StackPanel /ControlTemplate这样验证错误就直接显示在控件下方用户不用去看状态栏或者弹窗体验直观得多。6. 常见问题与排查技巧实录6.1 图像显示黑屏/空白这个问题十次里有八次是像素格式或者 Stride 不对。WPF 的BitmapSource.Create需要传入正确的stride每行字节数计算公式是stride width * bytesPerPixel padding如果不做填充对齐stride就简单地等于width * channels。比如 24 位 BGR 图像stride 就是width * 3。但很多图像数据源会对齐到 4 字节这时候直接传width * 3就会导致图像扭曲或黑屏。解决思路用 Mat 的Step属性每行实际字节数不要自己手算。这也是我上面代码里直接用converted.Step的原因。还有一个常见坑Mat.Empty()判断不可靠有些 Mat 非空但是 Data 指针无效转 BitmapSource 时会崩建议在转换前再检查mat.Data ! IntPtr.Zero。6.2 相机文件/资源释放失败这个问题比较隐蔽。相机 SDK 在采集时会锁定相关资源如果你没有正确停止采集就尝试释放相机经常会出现“文件被占用”或“释放超时”的异常严重的话还会导致整个进程退出。我的做法是界面上统一放一个“停止采集”按钮点击后先停止采集、再释放相机最后置空引用在窗口关闭事件里做兜底清理protected override void OnClosed(EventArgs e) { _cameraService.Stop(); _cameraService.Dispose(); base.OnClosed(e); }相机回调里的 Mat 要Clone()之后再处理原因是有些相机 SDK 的回调在图像处理没结束时就归还了缓冲区导致处理一半数据变成错误内容。6.3 ReactiveUI 绑定不起作用/界面不刷新ReactiveUI 的属性绑定依赖RaiseAndSetIfChanged而集合刷新依赖ReactiveList或者ReactiveCollection也就是ObservableCollection的替代品。如果你用的是普通ListT你往集合里 Add 数据界面当然不可能刷新。可能你用了普通属性但忘了调用RaiseAndSetIfChanged那自然也没反应。排查思路按优先级属性是不是RaiseAndSetIfChanged赋值的ViewModel 是不是继承了ReactiveObjectView 的 DataContext 是否真的指向了这个 ViewModel如果你用了 InteractionView 里是否在WhenActivated注册了我经常犯的一个错是在 ViewModel 构造函数里订阅了某事件但忘记在WhenActivated里 Register导致页面的某些联动逻辑只在页面刚加载时执行一次之后再也不生效。6.4 内存和 CPU 占用一直往上飙这个问题几乎每个做视觉上位机的都会遇到。最常见的原因是Mat 没有释放。每帧都 clone 一个新 Mat但处理完不 Dispose。解决方法是处理完立马Dispose()或者用using。BitmapSource 每帧新建而且没有 Freeze。冻结的 BitmapSource 更利于 WPF 的缓存和复用不冻结时每次布局更新都得重新提交 GPU 显存。事件订阅没有取消。比如采集服务每帧触发FrameReady事件如果 ViewModel 每次重建都重新订阅旧对象没有被回收事件链越来越多内存自然只涨不跌。解决方法是尽量用 ReactiveUI 的WhenActivated/DisposeWith来管理订阅生命周期或者在 ViewModel 的Dispose里取消订阅。我曾经排查过一个 CPU 占用 95% 的“疑难杂症”最后发现是图像处理循环里写了一个for循环里面每次迭代都调用了一个创建临时 Mat 的方法临时 Mat 没释放GC 线程被疯狂触发CPU 就被吃满了。这种问题用 dotMemory 或者简单任务管理器看内存曲线就能定位。7. 踩过几次坑之后我留下的习惯代码写多了自然有些固定习惯不一定适用于所有人但至少能让这套技术栈用得省心一些。第一个习惯是所有和硬件相关的代码都做超时控制。相机初始化、采集、图像处理任何一个环节都可能因为硬件异常卡住。我在调用这些操作时一律用CancellationTokenSource加超时时间超时后自动取消并弹出错误提示。从实战效果看这个机制救过我多次——尤其是现场设备偶发异常如果不做超时处理软件会一直转圈用户只能强杀进程。第二个习惯是把图像处理服务设计成可替换的接口。当时项目里同时试验了 OpenCVSharp 和另一个图像处理库因为服务层是接口抽象的切换时只改了依赖注入注册界面完全没动。后来这套架构帮我们在同一个框架下跑过模板匹配、颜色检测、二维码识别等多个算法模块每个模块就是注册一个新的服务实现。第三个习惯是定期做内存审查。视觉应用跑几个小时之后内存翻倍这是最要命的线上故障。我通常会在开发阶段连续运行程序几小时观察内存曲线。如果内存锯齿状上升而不回落基本可以断定是资源泄漏不用等用户反馈主动修掉。如果你正准备入坑这套技术栈我的建议是先从一个小工具开始不要一上来就做大系统。先做一个能打开相机、实时显示图像、做一个二值化处理的 200 行小工具把 Mat 转换、ReactiveCommand、异步处理跑通然后再往工程化方向演进。一旦这些核心环节自己写过一遍后面加什么功能都只是工作量问题谈不上太大的风险。
返回列表