简介:这份资源面向具备一定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 Dictionary<string, ICamera> BindCameras(List<string> serials) { var map = new Dictionary<string, 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及时Dispose,Bitmap用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 项目,都强制先把队列监控加上再谈识别率,不然调到最后都不知道瓶颈在哪。希望帮到你。
本文还有配套的精品资源,点击获取