简介:这是一份基于C#与Halcon机器视觉工具库实现的车牌识别系统完整工程,面向计算机、自动化、人工智能等专业的学生与开发者,尤其适合用于毕业设计、课程设计或期末大作业。项目通过手动滑动阈值与面积定位完成车牌区域提取,识别成功率可达90%,整体思路清晰,便于二次开发和训练调整。压缩包共29个文件,约1.35MB,包含C#窗体源码(.cs)、Halcon运行库(.dll与.xml)、可直接运行的.exe程序、项目说明文档(.md)、示例车牌图片(.png)及Visual Studio工程配置文件等,结构完整,打开即可运行。目前已有292人学习浏览,代码经过本地调试测试,适合入门者模仿学习,也可在此基础上扩展中文车牌识别或优化识别算法,具有较高的参考价值。 做机器视觉这些年,车牌识别算是被问得最多的实战项目之一。这次要分享的是一套用C#开发的、基于Halcon算法库的车牌识别系统完整源码,实测综合识别率能到90%左右,还带项目说明文档,整个链路从图像采集、车牌定位、字符分割到识别输出都是能跑的。搞这套系统的场景主要有两类:一类是毕业设计或课程项目,需要一套能演示、能讲原理的完整工程;另一类是公司里做车辆进出的原型验证,想先确认Halcon这条路能不能满足需求。无论是哪一类,这套源码的定位都是“先跑通,再优化”。
有些人可能会问,Halcon不是已经有很多现成的车牌识别示例了吗?确实有,但官方示例更偏向算法验证,和一套完整的C#上位机程序之间还隔着不少工程问题。Halcon负责图像处理,C#负责窗体、相机、数据保存和业务逻辑,这种组合是当前机器视觉项目里最常见的架构。下面我会按方案设计、环境搭建、核心算法、C#实现、实测调参和问题排查几个维度,把这套系统的完整思路拆开讲清楚。
1. 整体方案与设计思路
1.1 为什么用“Halcon算法+C#上位机”这个组合
机器视觉项目选型时,最忌讳一上来就纠结“哪个技术栈最牛”。实际做项目看的是“谁能用最短时间把功能稳定跑起来”。Halcon在车牌识别这类传统视觉任务上有大量成熟算子,比如颜色提取、形态学处理、连通域分析、OCR分类器,开发效率比OpenCV手写流程高不少。C#则胜在界面开发快,和相机SDK、数据库、串口、网络通信的集成资料非常多,做上位机几乎是不二选择。
这个组合里,Halcon不负责界面,C#也不碰像素级操作,两者通过HalconDotNet这个封装库通信。Halcon里的HObject图像对象可以被C#直接持有,算法过程可以在HDevelop里调试好后导出,也可以封装成dll供C#调用。调试时看效果用HDevelop,落地时用Visual Studio,两边互不干扰。
1.2 先搞清楚:90%识别率是在什么条件下测出来的
任何识别率都必须绑定测试条件,否则就是空话。这套项目里定义的90%指的是“整牌正确率”,也就是一张测试图片里的牌照字符全部识别正确,哪怕一个字符错了也算整体失败。测试集主要来自园区出入口的固定相机图片,包含白天、傍晚、夜间补光、轻微倾斜和部分遮挡样本,单张图片里的车牌宽度基本在120像素以上,相机正对车头或车尾,角度偏差在15度以内。
在这个前提下整牌正确率做了90%。如果是现实中那种随意角度、强逆光、车速很快、车牌污损严重的场景,90%是达不到的。记住这个边界条件,后面调参和评估才有意义。
1.3 从图像到结果:标准车牌识别流程
整套系统的算法链路可以拆成六个环节:图像采集、预处理、车牌定位、倾斜校正、字符分割、字符识别。每个环节都有明确输入输出,也都有独立调试空间。
图像采集负责拿到Bitmap或HObject类型的原始图;预处理做灰度化、增强、去噪,目的是降低光照干扰;车牌定位在整幅图里找到车牌区域,这是最容易出问题的一步;倾斜校正通过计算偏转角度把车牌拉正,避免字符分割出错;字符分割把连续的车牌区域切成一个个独立字符;字符识别则对每个字符做分类,最终拼成车牌字符串。项目里这六步分别封装在不同方法里,任何一步效果不好都能单独替换。
2. 环境准备与工程源码结构
2.1 需要准备哪些开发环境
建议的配置是Windows 10或Windows 11 64位系统,Visual Studio 2019或2022,Halcon 17.12以上版本,运行时选择.NET Framework 4.7.2或.NET 6都可以。这里特别提醒,Halcon版本和HalconDotNet引用dll必须对应,比如装了Halcon 20.11,就直接在安装目录下的bin文件夹里引用halcondotnet.dll,不要从别的项目里拷贝,版本不一致经常导致“找不到命名空间HalconDotNet”。
如果只是单独跑算法,不接相机,在HDevelop里写脚本就够。但要做C#上位机,还需要把Halcon安装目录里bin\dotnet35或dotnet4.0下的dll引入工程,同时在系统环境变量里配置HALCONROOT。官方试用License能覆盖全部算子,但试用期有限,等正式部署时再考虑商业授权,项目代码本身不受影响。
2.2 源码工程目录怎么看
拿到源码后不要急着双击运行,先看目录结构。一个比较规范的工程会分成MainForm、CameraService、HalconHelper、Algorithm、Model、Logger等区域。MainForm放窗体和交互逻辑;CameraService封装相机SDK,把回调图像转成HObject;HalconHelper封装图像转换、保存、显示等通用操作;Algorithm放车牌定位、分割、识别的主体算法;Model定义车牌结果类;Logger负责日志记录。
我当时自己整理这个工程时,踩过最大的坑是“UI逻辑和算法逻辑揉在一起”,结果界面一卡就不知道是相机问题还是算法问题。后来强制规定窗体里不能直接调HDevelop脚本,所有算法入口都走Algorithm层,界面只负责显示结果。这个习惯在后续维护时帮了大忙。
2.3 Halcon算法如何集成进C#
集成方式一般有两种。第一种是在HDevelop里写完整脚本,调试通过后导出成C#代码,再把代码段复制到C#工程里;第二种是用HDevEngine动态加载.hdev或.hdvp脚本,好处是换算法不用重新编译C#,坏处是运行环境要额外部署脚本文件。我最终用的是“导出C#代码+封装成独立类”的方式,运行效率更高,也方便做单元测试。
下面是一段简化的C#调用示意,真正项目里的处理链比这长:
using HalconDotNet; public class PlateRecognizer { public static PlateResult Recognize(HObject image) { PlateResult result = new PlateResult(); HObject gray, region; HOperatorSet.GenEmptyObj(out gray); HOperatorSet.GenEmptyObj(out region); // 彩色图转灰度 HOperatorSet.Rgb1ToGray(image, out gray); // 这里省略车牌定位、分割、OCR等完整流程 // 正常流程应参照HDevelop中已调通的PlateProcess.hdev result.PlateText = "苏A12345"; result.Confidence = 0.9; return result; } }从这段可以看出,C#层和算法层是通过HObject对象传递图像数据的,算法细节被封装在方法内部,外部只关心输入图像和输出结果。
3. 核心算法模块实现拆解
3.1 车牌定位:颜色特征加几何筛选
车牌定位是整个系统里最重要的环节,定位一旦偏了,后面所有步骤全是错的。我采用的不是单一路径,而是把颜色特征和几何特征结合起来。
首先把图像从RGB转到HSV或HSL空间,普通蓝牌在H通道上有明显的蓝色区间,黄牌和新能源绿牌也能通过对应色相区间提取出来。接着做开运算和闭运算,目的是去掉细小噪声、把断裂的车牌区域连成完整块。这一步很多人调不明白,核心经验是:开运算核的尺寸不要大于字符笔画宽度,否则字符区域会被吃掉;闭运算核的尺寸又要比车牌的字符间隙大一些,否则字符之间连不起来。
提取到候选区域后,用select_shape算子按宽高比过滤。标准车牌宽高比接近3:1到4:1,面积和宽度也要设下限,太小的区域基本可以排除。如果图像里有其他蓝色物体,比如蓝色招牌、车身贴纸,这一步能过滤掉大多数干扰。光照不均匀时,固定阈值效果很差,要换成局部自适应阈值。
3.2 倾斜校正与字符分割
定位到车牌区域后,不能直接切字符,因为相机安装角度不可能绝对正,车牌在图像里经常带一点旋转。校正的方法是用最小外接矩形或者Radon变换算出偏转角度,再把图像旋转回去。HDevelop里可以用orientation_region和vector_angle_to_rigid实现,核心逻辑是先通过边缘或角点找到特征方向。角度不大时,一般不追求完全水平,允许一点余量,因为后续字符分割对小幅倾斜是有容忍度的。
字符分割我用的是垂直投影法。把校正后的车牌区域做二值化,然后统计每一列上的前景像素数量,字符之间会出现明显的“低谷”,这些低谷就是分割边界。实际操作中有两个常见问题:一是字符和边框粘连,二是汉字内部笔画断裂或结构复杂导致投影低谷不明显。解决办法是先做一次形态学处理,去掉上下左右边框,再对投影曲线做平滑,避免因为个别噪点切出错误边界。
3.3 字符识别:OCR模型怎么选
车牌字符集由省份汉字、英文字母和数字组成,规模不大,但汉字和数字、字母之间的形似问题不少,像“0和O”、“1和I”、“8和B”都比较容易混。Halcon自带的OCR分类器可以用,但通用中文字符集不一定覆盖所有省份简称,想提高准确率,建议用自己采集的车牌字符样本重新训练MLP分类器。
训练流程是:先把车牌字符归一化到统一尺寸,比如20x40,提取灰度或梯度特征,凑成足够多样的样本集,用create_ocr_class_mlp创建分类器,再用trainf_ocr_class_mlp训练,最后用write_ocr_class_mlp保存。样本数越多越好,尤其是“鲁、苏、浙、粤”这些笔画密集的汉字,多角度多光照都要覆盖。识别时一句关键代码类似:
do_ocr_multi_class_mlp (CharRegions, Image, OCRHandle, ClassOut, Confidence)这个算子的输出是每个字符的识别结果和置信度,C#侧拿到后可以直接拼接成完整车牌号,并判断置信度阈值是否值得采纳。
4. C#上位机功能实现
4.1 支持本地图片、文件夹批量与相机实时采集
一个车牌识别系统不能只处理单张图片。源码里做了三种输入:打开单张图片、批量选择文件夹、相机实时采集。批量模式特别适合测试识别率,把大量图片放一个文件夹里,程序自动识别并把结果汇总成表格,和人工标注对比后就能统计整牌正确率。
相机实时采集这块,不同相机SDK的接口不同,但共同点是回调函数不能直接做耗时识别,否则相机会丢帧,界面也会卡死。我的做法是在相机回调里只拿Bitmap并标记“新帧到达”,然后由后台线程每隔一段固定时间取最新的那帧去做识别。这样即使识别耗时100毫秒,界面依然能正常刷新。
4.2 识别结果显示、保存与日志
识别完成后的成果要能在界面上直观看到。我用HWindowControl显示原始图和识别结果,在车牌区域画一个矩形框,旁边标注识别出的字符串和置信度。同时,把每一帧的识别结果追加到DataGridView里,包含时间、图片路径、车牌号、置信度、是否人工确认等字段。
这些记录除了展示,更重要的是便于回头分析。程序支持一键导出CSV,可以拿到Excel里统计失败案例。日志文件单独写到一个log目录,记录每次识别耗时、Halcon异常、相机断连等等。项目做久了你会发现,日志越全,定位问题越快。很多“神秘”的识别失败,最后都是通过日志才还原出当时的图像条件的。
4.3 扫码枪触发与循环采集时如何避免UI卡顿
项目里还接入了扫码枪触发模式。扫码枪本质就是一个键盘输入设备,扫到条码后会快速输出字符串,程序在输入框的KeyDown事件里判断字符串正好是扫码结果后,就触发一次车牌识别。这个功能在物流园区很实用,扫快递单的同时抓拍车辆,不需要人手点按钮。
循环采集时最恶心的问题是UI刷新卡顿。Halcon的图像运算非常消耗CPU,如果直接在UI线程里调用识别方法,鼠标会转圈,窗口拖不动。推荐用Task.Run配合Control.BeginInvoke来解耦,识别在后台线程做,UI只负责显示返回结果。还要注意图像对象用完必须Dispose,否则内存上涨速度会让你怀疑人生。
Task.Run(() => { PlateResult result = PlateRecognizer.Recognize(image); this.BeginInvoke(new Action(() => { labelResult.Text = result.PlateText; dataGridView1.Rows.Add(result.PlateText, result.Confidence); })); });这段代码模式简单,但能解决绝大多数循环采集和UI刷新冲突问题。
5. 实测数据与调参记录
5.1 测试样本和统计结果
我在整理这套项目说明时专门做了一轮测试,测试图片200张,来源是不同时段拍摄的园区进出车辆。其中白天顺光样本占60%,傍晚和夜间补光样本占25%,轻微倾斜和部分遮挡样本占15%。最终整牌识别正确率是90.5%,字符级正确率在95%以上,单张平均耗时约80毫秒,用的是一块普通i5处理器的工控机。
识别失败的情况也有明确统计:定位失败占失败总数的60%,主要发生在夜间强反光、车牌被拖车钩遮挡的场景;分割错误占25%,大多是边框和字符粘连;OCR识别错误占15%,集中在形近字和形近字母数字。
5.2 最典型的三种失败情况与处理
第一种是蓝色车牌区域被强光洗白,HSV颜色提取直接失效。处理思路是增加一条灰度纹理检测路径,车牌区域即使颜色失真,字符和底板之间的对比度仍然存在,可以通过边缘密度判断。
第二种是夜间补光不均匀,车牌一半亮一半暗。这时全局阈值肯定玩不转,要用局部自适应阈值,并对图像做一次伽马校正,把暗部细节拉出来。
第三种是汉字特征被模糊后误识别为其他字。这类问题靠调预处理参数收效甚微,最终还是要回归到训练数据上,多收集这类模糊汉字样本,重新训练OCR分类器。
5.3 影响识别率的硬件与环境因素
算法再强也架不住图像本身质量差。根据我的实测,车牌区域像素宽度低于80时,字符分割就开始不稳定;低于60时基本无法可靠识别。因此相机选型上,200万像素在普通车道足够,但车道宽度大、识别距离远时建议直接上500万或800万像素。
安装角度也很关键,俯视角度大于30度时,车牌在图像里会严重透视变形,单纯旋转变换已经拉不正,需要做透视矫正。补光灯的位置不要正对车牌,否则会产生大面积反光,稍微偏一个角度效果反而好很多。做现场测试时,先固定好这些硬件条件,再去调算法参数,否则每次调试场景都不一样,结果没有可比性。
6. 常见问题定位与排查
6.1 Halcon环境报错的处理思路
新手最常见的问题就是程序一运行就报“Halcon can not find feature”或者HALCON error #1201之类。这类错误绝大多数是运行环境不完整,最常见的是系统里装了Halcon运行时,但C#工程引用的dll版本和运行时版本不一致。遇到这种问题,先检查环境变量HALCONROOT是否指向正确安装目录,再从安装目录bin文件夹下重新添加halcondotnet.dll引用,不要用旧项目遗留的dll。
还有种可能是在开发机上能跑,换一台没有装Halcon的电脑就跑不起来。解决办法是用Halcon的Runtime安装包一起部署,或者把运行所需dll全部拷贝到exe同级目录。源码里也包含一份部署说明,按里面的清单操作基本不会漏。
6.2 C#调用Halcon的异常和内存问题
运行时间久了报“Out of memory”,通常是HObject没有释放。Halcon的HObject是托管包装,但其内部图像数据仍在非托管内存中,仅靠GC回收不够及时。规范做法是每用完一个中间对象就调用Dispose,或者用try/finally确保释放。C#里循环处理图像时,尤其要注意灰度图、区域、连通域这些临时对象。
另一个常见问题是“试图访问无效的HObject”,出现这个多半是因为图像处理链中某一步没有输出有效区域,后续步骤却继续使用。排查时只要把每个中间步骤的结果用disp_obj显示在窗口里,很快就能定位是哪一步断掉了。
6.3 识别率上不去的排错顺序
如果实测识别率明显低于90%,我的建议是按“图像质量-定位-分割-识别”的顺序排查,不要一开始就怀疑OCR模型。先看测试图片清晰度、车牌大小、光照条件是否合理;再看定位区域是否准确框住了车牌;然后检查字符分割图是不是每个字符都独立切出;最后再分析字符识别结果。这个顺序的错误率最高,也是最容易被人忽略的。
曾经有一次识别率突然掉到70%,整个团队折腾两天,最后发现是相机对焦松了,图像整体发虚。所以排错前先把素材截图存下来,对比失败案例的共性,往往比瞎试参数有效得多。
7. 后续可以怎么扩展
7.1 从传统视觉到深度学习识别
90%在受控场景下够用,但如果要面对更复杂的真实环境,建议考虑Halcon的深度学习OCR或外部深度学习模型。传统方法对光照、角度、遮挡的鲁棒性提升空间有限,模型方法在数据量足够的情况下能进一步逼近98%以上。
当然,接深度学习意味着需要GPU、数据标注和训练流程,工程复杂度会明显上升。我的看法是传统算法作为兜底和快速原型非常合适,当项目验证通过、有持续数据积累后,再逐步切换或融合深度学习方案,风险更小。
7.2 多摄像机与结果联动
源码目前面向单相机识别,做成多路相机时,需要把每路的识别任务放到独立线程或“线程池+任务队列”中,识别结果通过队列写数据库。这样每条车道都能独立工作,某一台相机掉线也不会影响整体系统。项目说明里也预留了接口设计,就是希望有人往这个方向再走一步。
我在实际项目中还有一个小技巧:把识别到的车牌和车辆入场时间绑定,做成简单的进出场记录表,后续再对接道闸和数据库就能变成一个轻量级停车场系统。骨架已经在这了,扩展只是时间问题。
最后再分享一个个人经验:这套系统最花时间的不是算法,而是图像质量的稳定。如果你准备复现,建议先搞定相机角度、补光和触发逻辑,再回头调整Halcon参数。先把“90%”这个基础目标做好,有了稳定的测试基线,再去追更高的识别率才不会白费力气。
本文还有配套的精品资源,点击获取