C# WinForms结合Halcon与DirectShow实现摄像头二维码识别
简介:frmWindowTest.rar 是一份面向C# WinForm开发者的Halcon机器视觉集成示例工程,演示通过DirectShow调用笔记本内置摄像头捕获实时视频流,并利用Halcon库完成二维码识别与信息解析。压缩包总计36个文件,约11.05MB,包含源代码文件(.cs)以及编译生成的.dll、.exe、.pdb等程序集,另有配置文件(.config/.settings)、界面资源(.resx/.resources)和测试二维码图片(.jpg)与说明文本(.txt),目录结构为标准Visual Studio解决方案布局,便于直接打开调试与二次开发。资源已吸引461人学习,特别适合刚接触WinForm、DirectShow摄像头采集或Halcon机器视觉的读者。通过该工程,可快速掌握在C#中托管ActiveX控件、构建滤波图管理器、配置Halcon二维码读取参数等关键流程,内置的测试二维码与图像也能帮助验证代码正确性,为后续工业视觉项目提供完整的参考起点。
1. 拆解 frmWindowTest:C# + Halcon 调摄像头读二维码,一条能跑通的最小实现
frmWindowTest 这个压缩包我拿到手,拆开以后的第一感觉是:它不是一个教学用的空壳窗体,而是把整条视觉识别链路闭环了。整个工程用 C# WinForms 做界面,摄像头采集交给 DirectShow,二维码解码交给 Halcon,压缩包里还附带了一张 22.jpg 测试二维码。也就是说,你不需要先找打印机打码,就能把“摄像头出图 → 图像进算法 → 画面里解码”这条链路完整跑一遍。它适合两类人:一类是刚开始把 Halcon 接进 C# 桌面应用、被官方文档绕晕的开发;另一类是准备做机器视觉(也有人写作“机械视觉”)原型选型,需要一份现成参照代码的工程师。下面的内容按我实际拆包顺序来:先看工程结构,再分别拆 DirectShow 和 Halcon 两条技术链,最后集中过一遍最容易翻车的坑。
2. 工程骨架与两条技术链:从 .sln 到 22.jpg,DirectShow 和 Halcon 各解哪一段
解压以后,目录结构大概是这样的:
frmWindowTest/ ├─ frmWindowTest.sln ├─ frmWindowTest/ │ ├─ frmWindowTest.csproj │ ├─ Form1.cs │ ├─ Form1.Designer.cs │ ├─ Form1.resx │ ├─ Program.cs │ ├─ App.config │ ├─ 22.jpg │ ├─ Properties/ │ ├─ bin/ │ └─ obj/ └─ .vs/.vs是 Visual Studio 的本地缓存目录,和源码逻辑无关,可以放心忽略。整个解决方案只有一个项目frmWindowTest,没有拆多个类库,这符合 Demo 的定位:所有功能都集中在主窗体代码里,跑起来、改起来都直接。
2.1 压缩包内各个文件的职责
| 文件/目录 | 职责 | 在这个工程里的角色 |
|---|---|---|
| frmWindowTest.sln | 解决方案文件 | Visual Studio 双击打开的入口 |
| frmWindowTest.csproj | 项目文件 | 记录程序集引用、平台目标、.NET 版本 |
| Form1.cs | 主窗体的逻辑代码 | 摄像头初始化和 Halcon 识别的主战场 |
| Form1.Designer.cs | 设计器生成的控件布局 | 面板、按钮、文本框都在这里定义 |
| Form1.resx | 窗体的资源文件 | 图标、字符串等嵌入资源 |
| Program.cs | 程序入口 | Main 函数启动 Application.Run(new Form1()) |
| App.config | 应用配置文件 | 运行时框架绑定等设置 |
| Properties/ | 程序集信息文件 | AssemblyInfo.cs 控制版本号和特性 |
| bin/ | 编译输出目录 | exe 和引用的 dll 全落在这里 |
| obj/ | 中间编译产物 | 编译过程临时文件,不需要手动碰 |
| 22.jpg | 二维码测试图 | 自带的验证图片,用来确认 Halcon 链路 |
刚接触这份代码时,重点看 Form1.cs 和 Form1.Designer.cs。Designer 文件会告诉你窗体上实际有哪些控件:通常有一个用于显示视频流的 Panel,一个触发识别的按钮,一个显示解码结果的 ListBox 或 TextBox。而 Form1.cs 里会分成两块,一块是 DirectShow 的初始化和预览,另一块是 Halcon 模型的建立和解码调用。bin 目录下的 Debug 文件夹里能看到 HalconDotNet.dll 和 halcon.dll,这说明工程用的是 Halcon 的 .NET 托管接口,不是底层 C++ 接口。App.config 和 Properties 目录基本不用动,除非换目标机器时要调整 .NET 运行时版本。
2.2 为什么是 DirectShow + Halcon:老牌采集链路配上工业视觉解码
摄像头采集这块,工程选的是 DirectShow 而不是 Media Foundation 或 OpenCV 的 VideoCapture。DirectShow 是 Windows 上非常老牌的流媒体框架,核心概念是滤波器图(Filter Graph):把摄像头设备、视频渲染器、采样器等组件像链条一样连起来,数据从源滤波器流出,经过中间滤波器处理,最后到达渲染器或你的回调。在这个工程里,链路要做三件事:找到笔记本内置摄像头、把实时视频渲染到窗体的 Panel 控件里、在需要识别时从流中抓取一帧图像。DirectShow 做这类需求非常成熟,网上资料多,兼容性也稳定;换成 USB 工业相机或采集卡时,只是换源滤波器的问题,整体结构不变。
识别部分没有选 OpenCV + ZXing,而是用 Halcon。Halcon 在工业视觉中的地位不用多说,它的二维码解码算子对倾斜、畸变、光照不均的鲁棒性比开源方案好不少;而且不少工厂环境里已经买了 Halcon 授权,选型时自然会优先用它。这个项目的场景因此非常清晰:DirectShow 负责“看到画面”,Halcon 负责“看懂画面”,两者在 Form1.cs 里通过摄像头抓帧这个动作衔接。理解这个分工后,后面调参就有了方向——画质花屏、不出图,去查 DirectShow;解码率低、识别慢,去调 Halcon 侧参数。如果一开始就混在一起排查,很容易陷入盲目改代码的循环。
2.3 C# 集成 Halcon 的落地方式:直接引用 HalconDotNet 的 DLL
这个工程集成 Halcon 的方式是最直接的一种:项目引用 HalconDotNet.dll,代码里直接调用托管接口。它分两层:上层 HalconDotNet.dll 是 .NET 包装,封装了 HImage、HDataCode2D、HOperatorSet 这些类;底层是真正干活的 halcon.dll,C++ 实现的算法核心。部署时这两个文件都必须放在 exe 同目录,缺任何一个,运行到调用算子的那一步就会抛异常。
除了直接引用,官方还有两种集成路径:一种是 HDevEngine,在 C# 里加载 .hdev 脚本执行,好处是 HDevelop 里调好的算法不用翻译就能跑,缺点是每次调用要解析脚本,效率略低;另一种是把 HDevelop 脚本导出为 C# 文件再编进工程,逻辑固化,适合算法基本不变的场景。直接引用的方式前期写起来稍繁琐,但每步做了什么都很清楚,做自定义逻辑最方便,所以是大多数 WinForm 项目的默认选择。这里有个非常容易踩的坑:HalconDotNet 的 x86/x64 版本必须和工程的平台目标一致,否则经常在启动阶段就报“无法加载 DLL”。在 64 位系统里用 32 位相机 SDK 折腾,纯属浪费时间——先把平台目标统一,再谈别的。
3. DirectShow 摄像头画面接入 WinForm:滤波器图构建与预览显示
3.1 构建 Capture Graph:从 DirectShowLib 引用到出图的关键步骤
DirectShow 在 C# 里直接用 COM 接口声明非常啰嗦,最常见的做法是引用开源封装库 DirectShowLib。用它的 DsDevice、FilterGraph、CaptureGraphBuilder2 这几个类,可以省掉大量重复的接口定义。先看构建滤波器图的核心代码:
using DirectShowLib; IGraphBuilder graphBuilder = (IGraphBuilder)new FilterGraph(); ICaptureGraphBuilder2 capBuilder = (ICaptureGraphBuilder2)new CaptureGraphBuilder2(); capBuilder.SetFiltergraph(graphBuilder); // 枚举系统里的摄像头设备,取索引 0 的设备 DsDevice[] devices = DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice); if (devices.Length == 0) { throw new InvalidOperationException("未检测到可用摄像头设备"); } Type deviceType = Type.GetTypeFromCLSID(devices[0].Clsid); IBaseFilter sourceFilter = (IBaseFilter)Activator.CreateInstance(deviceType); graphBuilder.AddFilter(sourceFilter, "camera");这段代码做的事情很直白:先建一个 Filter Graph 实例,再用 CaptureGraphBuilder2 来管理它;接着枚举系统里 VideoInputDevice 类目的所有设备,取第一台摄像头;最后把它的源滤波器加到图中。Activator.CreateInstance的参数是 COM 类标识符(CLSID),每个摄像头驱动都有唯一的 Clsid,不能写死;换一台电脑、加一个 USB 摄像头,设备列表的顺序都可能变——这也是后面要给工程加设备下拉栏的原因。
滤波器加进去以后,下一步把预览通路跑起来:
IBaseFilter renderer = (IBaseFilter)new VideoRendererDefault(); graphBuilder.AddFilter(renderer, "renderer"); // 把摄像头源滤波器的输出引脚连到渲染器,建立预览通路 int hr = capBuilder.RenderStream(PinCategory.Preview, MediaType.Video, sourceFilter, null, renderer); if (hr != 0) { throw new COMException("RenderStream 失败,HRESULT=" + hr.ToString("X8")); } IMediaControl mediaControl = (IMediaControl)graphBuilder; mediaControl.Run();RenderStream是这里最关键的调用:它自动找源滤波器的视频输出引脚,在中间插入必要的滤波器,连到渲染器。参数PinCategory.Preview表示预览通路,MediaType.Video限定只匹配视频流;如果把它改成PinCategory.Capture,抓帧的采样器就要走这条通路拿数据。返回的 HRESULT 为 0 表示成功,非 0 基本就是驱动不兼容或设备被占用。很多黑屏问题就是因为忽略了返回值——排查时先确认它是不是 0,再去看画面,这是个很高效的过滤手段。
3.2 把视频窗口嵌进 Panel:IVideoWindow 的参数与坑
预览通路建立后,默认会弹出一个独立视频窗口。要让它显示在窗体的 Panel 里,需要通过 IVideoWindow 把视频窗口的所有者改成 Panel 的句柄,同时关掉边框、调整尺寸:
IVideoWindow videoWindow = (IVideoWindow)graphBuilder; videoWindow.put_Owner(panelCamera.Handle); videoWindow.put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS); videoWindow.put_Left(0); videoWindow.put_Top(0); videoWindow.put_Width(panelCamera.ClientSize.Width); videoWindow.put_Height(panelCamera.ClientSize.Height);两个样式常量分别是 WS_CHILD=0x40000000、WS_CLIPSIBLINGS=0x04000000,作用是让视频窗口变成 Panel 的子窗口并裁剪兄弟窗口区域。需要注意,put_Owner 之后视频窗口就成了 WinForms 体系外的子窗口,Panel 每次 Resize 时都必须重设 Width/Height,否则视频区域不跟随 Panel,底部会留下一片黑边。另外 put_WindowStyle 不只要设 CHILD,还要同时清掉原来可能存在的 WS_CAPTION 和 WS_THICKFRAME,否则视频左上角会偏移几个像素。这个“偏移”问题我用过一次,定位了半小时,最后发现是样式没设全,气得不行——这些小参数在 DirectShow 集成里就是这么折腾人。
3.3 逐帧抓图给 Halcon:SampleGrabber 回调和缓冲区共享
扫二维码是对一帧静止图像做解码,不能直接把视频窗口截图喂给 Halcon——窗口截图在画面刷新不完整时会抓到半帧,这是最典型的坑。正确做法是加一个 SampleGrabber 滤波器,让它在视频流经过时回调我们的代码。抓帧的用法如下:
ISampleGrabber grabber = (ISampleGrabber)new SampleGrabber(); graphBuilder.AddFilter((IBaseFilter)grabber, "grabber"); AMMediaType mediaType = new AMMediaType { majorType = MediaType.Video, subType = FormatType.RGB24 }; grabber.SetMediaType(mediaType); // 参数 0 表示回调缓冲区模式,不阻塞视频流 grabber.SetCallback(new MySampleGrabberCallback(), 0); // Capture 通路连到 grabber,再连到渲染器 capBuilder.RenderStream(PinCategory.Capture, MediaType.Video, sourceFilter, (IBaseFilter)grabber, null);把PinCategory从 Preview 换成 Capture 是关键一步:Capture 通路上的数据往往是原始视频帧,并且我们可以用 SetMediaType 强制指定 RGB24 输出格式,这样回调拿到的就是无压缩的 RGB 数据,不用自己解析 YUV 格式。这里有常见坑:不少摄像头默认输出 MJPG 或 YUY2,想让 SampleGrabber 强制转 RGB24 必须在 RenderStream 之前先 SetMediaType,连接完成后媒体类型可能已经协商成驱动默认值,再改就晚了。
回调类写法如下,重点注意这里只做拷贝和解码触发,不直接调 Halcon 解码:
class MySampleGrabberCallback : ISampleGrabberCB { public int SampleCB(double sampleTime, IMediaSample pSample) { return 0; // 本工程使用 BufferCB,这里直接忽略 } public int BufferCB(double sampleTime, IntPtr pBuffer, int bufferLen) { // 把该帧的 RGB24 数据拷出来,避免 DirectShow 复用缓冲区 byte[] frame = new byte[bufferLen]; System.Runtime.InteropServices.Marshal.Copy(pBuffer, frame, 0, bufferLen); // 这时的 frame 就是干净的 RGB24 图像数据 OnFrameCaptured?.Invoke(frame); return 0; } }要点是使用 BufferCB 而不是 SampleCB,BufferCB 能直接拿到原始内存指针。在回调里做Marshal.Copy拷贝,而不是直接引用指针,是因为 DirectShow 的缓冲区是复用的——如果不同步拷贝,下一帧到达时整份数据就被覆盖。OnFrameCaptured事件把帧交给 UI 线程或独立解码线程,回调本身只做拷贝,避免阻塞采样线程导致画面丢帧。
注意:如果摄像头分辨率很高,BufferCB 里频繁
new byte[bufferLen]会产生大量短生命周期对象,建议用对象池或预分配缓冲区复用,否则长时间运行会出现 GC 频繁、画面卡顿的问题。
4. Halcon 二维码解码:模型参数、单帧流程和结果展示
4.1 创建二维码模型:为什么用 create_data_code_2d_model,参数怎么给
Halcon 解码二维码,入口是一个数据码二维模型(Data Code 2D Model)。创建代码在 C# 里这样写:
HDataCode2D dataCode = new HDataCode2D(); dataCode.CreateDataCode2dModel("QRCode", new HTuple(), new HTuple());CreateDataCode2dModel 的三个参数分别是:码制类型、通用参数名列表、通用参数值列表。工程里 QRCode 是最常见的场景;要兼容 DataMatrix,把第一参数换成“DataMatrix”再建一个模型就行,两个模型可以并存,分别去解码。后两个参数传空 HTuple 表示使用默认参数,简单场景下直接跑得通。
需要精细调参时,通过 SetDataCode2dParam 逐项覆盖。结合摄像头扫码场景,我常调整的是这几个参数:
| 参数名 | 含义 | 常见改动 |
|---|---|---|
| default_param | 整体识别策略 | "standard" 通用;"robust" 对透视畸变更抗噪 |
| contrast_tolerance | 对比度容差 | 曝光不稳定时从 1 调到 2 或 4 |
| min_module_size | 最小模块尺寸(像素) | 码离得远时设 8,离得近时设 4 |
| max_module_size | 最大模块尺寸(像素) | 按视野中二维码的实际占比估算 |
min_module_size 对识别率影响最大:设太大,小码找不到;设太小,图像里的噪点会被当成码去训练,产生大量误报。经验做法是按图像宽度的 1%~2% 给初始值,再用固定分辨率相机把二维码放在视野最远端,量出它实际占用的像素宽度来回填。
4.2 单帧解码流程:从静态图到摄像头帧
先用工程自带的 22.jpg 把单个环节跑通:
HImage image = new HImage("file", "22.jpg"); HXLDCont symbolContours; HTuple decodedStrings; // 模型在初始化时创建,这里重复使用 dataCode.FindDataCode2d(image, new HTuple(), new HTuple(), out symbolContours, out decodedStrings);FindDataCode2d的输出参数有两个:symbolContours 是找到的符号轮廓(XLD),可以用来框出二维码的位置;decodedStrings 是解码后的字符串列表,一个元素对应一个码。输入参数的第二、三个同样是通用参数名和值,传空代表使用创建时的属性;如果只想对当前这一帧临时提高宽容度,可以在这里传"contrast_tolerance", 4,它会覆盖模型里的默认值。注意这是临时覆盖,不影响模型本身。如果现场识别率一直不稳,正确的做法是回头改模型参数,而不是在调用处反复调临时参数去碰运气。
摄像头回调来的 RGB24 字节数组,构建 HImage 的方式如下:
// frame 是 BufferCB 里拷贝出的 RGB24 数据 HImage cameraImage = new HImage("rgb", width, height, frame);这里的关键是像素格式匹配:DirectShow 端 SetMediaType 设 RGB24,HImage 的 "rgb" 刚好对齐;如果某些摄像头驱动不配合,依然输出 YUY2,HImage 就会出现色偏或花屏。遇到这种情况,不要试图在 Halcon 侧硬解,应该回到滤波器图里,在 SampleGrabber 前插入一个色彩空间转换滤波器,或者在 SetMediaType 时先协商好。把像素格式问题丢给 Halcon 侧处理,是典型的找错层。
4.3 解码结果展示与位置信息提取
解码后要把结果亮出来,同时显示一下码的位置信息,这对调试很有用。取位置信息需要调 GetDataCode2dResults:
HTuple row, col, angle; HOperatorSet.GetDataCode2dResults(modelHandle, "row", null, out row); HOperatorSet.GetDataCode2dResults(modelHandle, "col", null, out col); HOperatorSet.GetDataCode2dResults(modelHandle, "angle", null, out angle);这里注意一个容易混淆的点:GetDataCode2dResults的第一个参数是模型句柄,不是 FindDataCode2d 返回的结果句柄;结果句柄是在 FindDataCode2d 内部由运行时管理的,用来衔接多次算子调用。我通常要么全程用 HDataCode2D 类方法,要么全程用 HOperatorSet 加句柄,避免混合使用导致句柄释放顺序混乱。句柄漏了,短时间看不出问题,长时间运行后内存会缓慢增长,这对于机器视觉现场程序来说是很头疼的稳定性问题。
如果画面里同时出现多个二维码,decodedStrings 就是一个字符串元组,长度代表码的数量。Demo 一般显示第一个,但我建议把全部遍历出来丢进 ListBox,这样你拿 22.jpg 和手机屏幕反复换场景时,能直观看到一帧图能解出几个码,对后面调尺寸参数很有帮助。多码逻辑也能顺带验证你的遍历代码是否健壮,避免现场出现多码时程序只认第一个的尴尬。
5. 避坑与排查:摄像头不出图、识别率低、退出卡死的高频问题
这份工程代码量不大,但我从拆包、运行、改动的过程看,最容易出事的恰恰是几个看起来不起眼的环节。下面挑五个高频情况,按现象、原因、解决三步记录。
5.1 摄像头预览黑屏或灰屏
现象:窗体正常启动,Panel 区域是黑色或灰色,没有异常弹窗;或者启动后能出画面,但切换一下窗口再回来就黑了。原因:最常见的是mediaControl.Run()没被调用,预览通路建好了却不跑,渲染器永远拿不到第一帧;其次是切换窗口或 Panel 重建句柄后,IVideoWindow 的 owner 没有更新,旧句柄失效后画面就黑屏。解决:先确认 Run 被调用且返回 HRESULT 为 0;然后在 Panel 的 Resize 和 ParentChanged 事件里重设 put_Owner 以及宽高,不要只设一次就指望它一直生效。切窗口黑屏的土办法是每次窗体激活时重新 put_Owner 一次,方法不优雅但是有效。
5.2 找不到摄像头或 COM 插件报错
现象:DsDevice.GetDevicesOfCat 返回空列表,或 AddFilter 时抛 COMException;同一段代码在同事电脑上正常。原因:笔记本内置摄像头被其他程序占用时,设备枚举可能为空,AddFilter 也会失败;另外很多机器有多个摄像头设备(前摄、后摄、IR 摄像头),固定用索引 0 换台机器就可能选到错误的设备。解决:把枚举到的设备做成下拉菜单,让使用者手动选择;AddFilter 捕获异常后提示“摄像头被占用”,加一个重试按钮;工业现场 USB 摄像头的插拔也应纳入运行时的错误恢复,不能只依赖启动时枚举一次。
5.3 实时画面解码率忽高忽低
现象:22.jpg 静态识别基本百分百成功,切到摄像头后识别率掉到一半以下,换张二维码更明显。原因:分辨率过高,例如 1920×1080 下二维码占的像素太少,解码时容易被当背景噪声;另一个大坑是自动曝光,车间灯光一波动画面就糊;部分摄像头还有自动对焦,来回拉风箱的时候画面模糊,解码自然失败。解决:把摄像头降到 640×480 或 1280×720,让码在画面里占更大的像素面积;在滤波器链路上尝试关闭自动曝光、固定焦距(不是所有摄像头驱动都支持);如果必须大图,先在模型里把 min_module_size 和 max_module_size 设准,再把 ROI 区域裁出来识别,不要整幅图直接丢给解码。
5.4 帧回调里直接做 Halcon 解码,把预览卡成一帧一帧
现象:画面像幻灯片一样一格一格跳,CPU 占用率很高;有时关闭窗体后任务管理器里还有进程残留。原因:BufferCB 运行在 DirectShow 的流线程里,你在回调里把整帧转成 HImage 再做 FindDataCode2d,一次解码 30~60 毫秒,而视频流一帧只有 33 毫秒左右,直接堵死了整条流。解决:回调里只做拷贝并入队,解码交给 UI 线程的 Timer 或独立线程去消费队列。这个结构改动是让 Demo 走向可用的关键,哪怕现在只是一帧一识别,也按这个模式搭,不然后面联调只会越调越乱。
5.5 关闭窗体时进程卡死不退出
现象:点 X 关闭窗口后,进程还挂在任务管理器里,窗口不见了但进程不退。原因:DirectShow 没停,mediaControl 还处于 Running 状态;或者解码线程阻塞在队列等待上,而线程没有被通知退出。解决:在 OnFormClosing 里按顺序处理——先把视频窗口与 Panel 剥离,再mediaControl.Stop(),然后向解码线程设置退出标志并 Join 等待,最后释放 graphBuilder。顺序上我倾向先剥窗口再 Stop,否则视频窗口的通知链里会对已销毁句柄发起访问,偶发崩溃。把退出流程写好,摄像头工程才谈得上稳定,机器视觉现场程序往往要长时间挂机,退出问题一出就是冒烟事故。
6. 验证与进阶:让 Demo 从“能跑”变成“能稳定用”
6.1 先做静态验证,再上摄像头
复现完这个工程后,我的第一条经验是:不要一上来就开摄像头联调,否则问题混在一起根本没法定位。先按下面这张表分层验证:
| 验证步骤 | 操作 | 预期结果 |
|---|---|---|
| 1. Halcon 链路 | 启动后点“读取图片”,选择 22.jpg | 文本框输出二维码内容,无异常 |
| 2. DirectShow 链路 | 点击“启动摄像头” | Panel 显示实时画面,缩放窗体不黑屏 |
| 3. 联合链路 | 用手机屏幕展示一个二维码,对准摄像头 | 解码结果稳定刷新,与真实内容一致 |
跑完三步,哪段有问题直接缩小到对应章节排查,不用把 DirectShow 和 Halcon 混在一起猜。能省下大量排查时间。
6.2 进阶:一个队列加一个线程,把回调压力隔开
如果这个 Demo 要拿去做实际应用,建议把帧回调里的逻辑改成“入队 + 独立解码线程”。这是便宜实用的小改造,大部分功能不用动:
private ConcurrentQueue<byte[]> frameQueue = new ConcurrentQueue<byte[]>(); public int BufferCB(double sampleTime, IntPtr pBuffer, int bufferLen) { byte[] copy = new byte[bufferLen]; Marshal.Copy(pBuffer, copy, 0, bufferLen); frameQueue.Enqueue(copy); // 只入队,不解码 return 0; }识别线程循环从队列里取一帧,处理完再取下一帧:
while (isRunning && frameQueue.TryDequeue(out byte[] frame)) { HImage image = new HImage("rgb", width, height, frame); HXLDCont contours; HTuple decoded; dataCode.FindDataCode2d(image, paramName, paramValue, out contours, out decoded); // 这里再把结果通过 BeginInvoke 回写到 UI }同时给队列设一个长度上限,超限时直接丢弃旧帧,保证解码线程处理的是最新画面,避免内存持续膨胀。做完这个改造,你会发现 UI 不卡了,识别率也稳了,因为采集和图像处理彻底分开了。从那以后,我每次拿到摄像头加图像库的 Demo 包,都会先检查回调结构是不是“一个队列加一个线程”,如果回调里直接埋了识别逻辑,我宁愿先重构再看效果,这个习惯帮我避开了很多现场联调的坑。希望帮到你。
本文还有配套的精品资源,点击获取