2026/10/30 15:41:41

C#上位机四路相机Halcon OCR实时采集与多线程避坑实战

C#上位机四路相机Halcon OCR实时采集与多线程避坑实战 简介这份资源面向具备一定C#与机器视觉基础的开发者聚焦多相机实时OCR采集场景提供一套可直接运行的上位机工程代码。项目基于C#联合Halcon实现四路相机同步采集与字符识别涵盖图像尺寸获取、窗口显示区域设置、ROI矩形区域生成与OCR识别对象释放等关键流程适合工业检测、流水线读码、多工位字符识别等应用方向的二次开发与学习参考。压缩包共100个文件约2.54MB以34个cs源码文件为核心配合14个dll动态库、6个resx与resources资源文件、3个csproj工程文件及sln解决方案另有pdb调试符号、cache编译缓存、config配置与exe可执行文件等结构完整便于直接编译调试。目前已有1866人学习下载。读者可从中获取多相机采集与Halcon OCR联调的完整工程框架、ROI定位与图像显示的实现思路以及可直接运行的示例代码快速搭建自己的视觉上位机项目。1. 四路相机 OCR 实时采集这套 C# 上位机代码到底能省多少事产线上四台工业相机同时对着四个工位做字符识别这种需求在 3C 装配、食品包装、药品追溯里非常常见。真到自己动手写问题就来了相机怎么分配、Halcon 的 OCR 句柄怎么在多线程里不打架、四个画面怎么在 UI 上不卡顿地刷新、识别结果怎么和工位号对上。这套Camare.rar给的就是一个能直接跑起来的 C# 上位机工程把四路相机的采集、Halcon OCR 识别、结果回传整合在了一个 WinForm 框架里。它适合两类人一类是刚接手上位机项目、需要一份能跑通的参考骨架的工程师另一类是已经在用 Halcon 做单相机识别、想扩到多相机但不确定线程和句柄怎么管的熟手。下面我按拆包后的实际结构把能抄的部分和会翻车的地方都过一遍。2. 工程结构与多相机采集链路从相机枚举到四路回调2.1 先看清工程里有什么别急着 F5拿到压缩包解压后常见做法是先看目录再决定动不动代码。这类 C# 联合 Halcon 的上位机工程结构一般跑不出这几块相机采集层、Halcon 算法层、UI 层、配置层。你打开解决方案时重点确认三件事——引用的 HalconDotNet 版本、相机 SDK 的引用路径、以及主窗体里相机初始化的位置。因为 Halcon 的halcondotnet.dll是分版本的工程用的哪个版本你本机就得装对应版本的 Halcon否则一编译就是「找不到类型或命名空间」。模块典型文件职责相机采集CameraManager.cs / CameraDevice.cs枚举设备、开流、回调取图算法识别HalconOcr.cs创建 OCR 句柄、加载分类器、执行识别界面MainForm.cs四画面显示、结果列表、启停控制配置App.config / xml相机序列号、ROI、分类器路径这里有个选型上的现实问题为什么用 Halcon 的 OCR 而不是 Tesseract。热词里tesseract ocr 安装包、umi-ocr本地ocr识别搜的人不少但工业字符识别场景下Halcon 的 OCR 分类器对低对比度、有噪点、字体固定的喷码识别率明显更稳而且它和采集在同一套句柄体系里不用把图像存盘再喂给外部进程。代价是 license 和体积这个后面避坑章细说。2.2 四路相机怎么枚举和绑定别靠索引硬编码多相机最容易翻车的一步就是设备绑定。很多人图省事用GetCameraList()[0]到[3]按索引开相机结果换一台机器、插拔一次 USB顺序全乱A 工位的相机识别到了 B 工位的图。正确做法是按序列号或用户自定义名绑定。下面是我一般会写的枚举与绑定逻辑思路是先把所有设备信息读出来再按配置里的序列号匹配。// 枚举所有相机并按序列号建立映射避免索引漂移 public Dictionarystring, ICamera BindCameras(Liststring serials) { var map new Dictionarystring, ICamera(); var devices CameraSDK.EnumerateDevices(); // 返回设备信息列表 foreach (var sn in serials) // serials 来自配置文件 { var dev devices.FirstOrDefault(d d.SerialNumber sn); if (dev null) { // 记录日志而不是直接抛异常缺一台不该拖垮整个程序 Log.Warn($未找到序列号为 {sn} 的相机); continue; } var cam CameraSDK.Open(dev); map[sn] cam; } return map; }逻辑说明serials从配置读顺序即工位顺序和物理接口无关。参数上EnumerateDevices返回的SerialNumber是稳定标识比索引可靠得多。如果某台相机没找到这里选择记日志跳过而不是抛异常是为了让程序在缺相机时还能起来方便现场排查而不是一开机就崩。2.3 回调里区分四个相机别在回调线程里碰 UI热词里c# directshow uvc 回调里区分多个摄像头问的正是这个痛点。相机 SDK 的回调通常跑在独立线程四路回调可能同时进来。区分相机靠的是回调参数里的设备句柄或序列号不是靠全局变量。更关键的是回调里绝对不能直接更新控件否则就是经典的跨线程异常或者界面假死。// 回调中只做取图和投递识别和 UI 更新都甩出去 private void OnFrameArrived(object sender, FrameEventArgs e) { string sn e.DeviceSerial; // 用序列号区分是哪路相机 Bitmap bmp e.ToBitmap(); // 转成托管位图 // 投递到识别队列队列内部按 sn 分发避免回调阻塞 _ocrQueue.Enqueue(new FrameTask(sn, bmp)); }逻辑说明e.DeviceSerial是区分相机的唯一依据FrameTask把序列号和图像打包。识别放到独立消费线程UI 更新用BeginInvoke回到主线程。参数上队列要有界防止相机帧率高于识别速度时内存暴涨这个在避坑章会展开。3. Halcon OCR 句柄管理与识别流程多线程下怎么不打架3.1 OCR 句柄不是线程安全的一个相机一个句柄Halcon 的 OCR 句柄HOCRCnn或传统HOCRBox在多线程里共享是会出问题的轻则识别结果串台重则直接崩。血泪经验是每个相机对应一个独立的 OCR 句柄或者用锁把识别串行化。四路相机如果识别耗时不高串行加锁最省心如果每路都要几十毫秒那就得每路一个句柄并行跑。下面演示每路独立句柄的初始化。// 每个工位独立初始化一个 OCR 句柄互不干扰 private HOCRCnn CreateOcrHandle(string classifierPath) { HOCRCnn ocr new HOCRCnn(); // 加载预训练分类器路径来自配置别写死 ocr.ReadClassifier(classifierPath, default); // 设置识别参数是否返回置信度、是否区分大小写 ocr.SetParam(mode, auto); return ocr; }逻辑说明ReadClassifier加载的是 Halcon 训练好的 OCR 分类器文件路径必须可配置因为不同项目字体不同。SetParam的mode控制识别模式auto适合字符规整的场景。参数怎么调要看你的字符类型数字和字母混排时建议单独训练分类器通用分类器对易混字符0/O、1/l识别率会掉。3.2 从图像到结果的完整识别链路识别不是把图丢进去就完事中间要做预处理。工业现场光照不均、有反光直接识别率很低。常见做法是先转灰度、做局部阈值或浓淡补正再定位字符区域最后才送 OCR。热词里halcon浓淡补正的算法就是干这个的。// 单帧识别预处理 - 字符分割 - OCR public string Recognize(HObject image, HOCRCnn ocr) { HObject gray, region, chars; HOperatorSet.Rgb1ToGray(image, out gray); // 转灰度 // 动态阈值分割适应光照变化 HOperatorSet.DynThreshold(gray, gray, out region, 30, light); // 连通域拆分出单个字符 HOperatorSet.Connection(region, out chars); // 执行识别返回字符串 HTuple result; ocr.Recognize(chars, out result); return result?.S ?? string.Empty; }逻辑说明Rgb1ToGray降维减少计算量DynThreshold的30是灰度差阈值光照越不均可以适当调大Connection把字符区域拆成独立连通域OCR 才能逐字识别。如果字符粘连Connection会拆不开这时要加形态学腐蚀或者用SegmentCharacters。识别结果result.S是字符串空结果要判空否则 UI 显示会报错。3.3 识别结果和工位号怎么对上四路相机的结果必须带工位标识否则后面追溯、报警、存库全乱。做法是在FrameTask里就带上序列号识别完成后把(工位, 结果, 时间戳)一起投递到结果处理层。别用「第几个识别完就是第几个工位」这种假设多线程下完成顺序和相机顺序没关系这是新手最常踩的坑之一。4. 避坑与常见问题排查四相机 OCR 现场最容易翻的几处4.1 相机亮度异常某一路明显偏暗或偏亮现象四路画面里有一路亮度和其他三路差很多识别率骤降。原因通常是这路相机的曝光或增益被自动模式带偏了或者光源单独接了不同电源。解决把四路相机的曝光、增益都设成手动固定值别用自动如果光源独立检查供电和触发同步。热词里多相机同步采集某一个相机亮度异常说的就是这个本质是自动曝光在各自为政。4.2 Halcon license 报错程序启动就退出现象编译通过一运行就提示 license 无效或找不到。原因Halcon 的 license 和版本、加密狗绑定工程引用的halcondotnet.dll版本和你本机装的 Halcon 版本不一致。解决确认工程引用的 DLL 版本装对应版本的 Halcon 运行时license 文件放到系统指定目录。别想着用高版本 DLL 配低版本 license必翻车。4.3 界面卡死四路画面刷新像幻灯片现象相机一开UI 就不响应点按钮没反应。原因回调线程里直接更新了 PictureBox或者识别在主线程跑。解决回调只取图投递识别放独立线程UI 更新统一用BeginInvoke。另外四路画面别每帧都刷按显示帧率抽帧比如每 3 帧刷一次肉眼完全够用。4.4 内存持续上涨跑几小时就崩现象任务管理器里内存一路涨最后 OutOfMemory。原因图像对象没释放Halcon 的HObject和Bitmap都是非托管资源用完不Dispose就泄漏。解决识别完的HObject及时DisposeBitmap用using包起来队列要有上限满了丢旧帧而不是无限堆。4.5 识别结果偶尔串台A 工位显示 B 工位的字符现象结果和工位对不上偶发。原因多个相机共享了一个 OCR 句柄或者结果队列没带工位标识。解决每路独立句柄或者识别加锁结果结构体强制带序列号从采集到显示全程不丢标识。5. 进阶把四路识别做成可配置、可验证的稳定模块5.1 用配置驱动相机和 ROI别把参数写死在代码里工程能跑通只是第一步现场换产品、换字体、调 ROI 是常态。我一般会把相机序列号、每路的 ROI 区域、分类器路径、识别阈值全放到 XML 或 JSON 配置里程序启动读配置改参数不用重新编译。热词里c# json 匹配配置就是这个思路。下面是一个配置结构的示例。{ cameras: [ { serial: CAM001, station: A, roi: [100, 100, 400, 200], classifier: ocr_a.goc }, { serial: CAM002, station: B, roi: [120, 90, 380, 210], classifier: ocr_b.goc } ], ocr: { minConfidence: 0.7, timeoutMs: 200 } }逻辑说明roi是[x, y, width, height]每路可不同classifier允许不同工位用不同分类器因为字体可能不一样minConfidence低于阈值的结果标记为可疑不直接放行。这样现场调参只改文件不动代码。5.2 怎么验证这套代码在你机器上真的能用别一上来就接四个真相机。我习惯分三步验证第一步用工程自带的图片或本地图片文件模拟输入确认 OCR 链路通第二步接一台真相机确认采集和识别闭环第三步再扩到四台观察 CPU、内存和识别耗时。验证时重点看三个指标单帧识别耗时决定能不能实时、四路并发时 CPU 占用决定要不要降帧、连续跑两小时内存曲线决定有没有泄漏。这三个数达标了这套代码才算在你现场立住。5.3 一个具体技巧识别耗时波动大时先看队列积压现场最常见的是识别偶尔卡一下结果延迟。别急着优化算法先看识别队列的长度。如果队列持续增长说明识别速度跟不上采集速度这时候要么降采集帧率要么抽帧识别要么把预处理简化。我一般会在界面上放一个队列长度指示超过阈值就报警。从那以后我每次上多相机 OCR 项目都强制先把队列监控加上再谈识别率不然调到最后都不知道瓶颈在哪。希望帮到你。本文还有配套的精品资源点击获取