直接上结论:Halcon做车牌识别,比很多人想象中要简单得多。不管你是刚入行机器视觉的工程师,还是学校里头一次接触Halcon做课程设计的同学,“基于Halcon的简易车牌识别”都是一个非常合适的练手项目。它不依赖深度学习、不需要训练模型,只用Halcon里最常用的图像处理算子和模板匹配就能跑通一整套流程:定位车牌、分割字符、识别字符、输出结果。这个项目的价值不在于“能识别得多准”,而在于它能完整逼你走一遍机器视觉项目的基本路线,把图像预处理、形态学操作、区域筛选、模板匹配这些基本功练扎实。这篇文章就围绕这个标题,把整个项目的设计思路、代码实现和踩坑记录全部分享出来,希望对准备上手Halcon的同行有帮助。
1. 项目整体设计与方案选型思路
先说结论:简易车牌识别不用上深度学习,传统图像处理方案在这个场景下反而更合适。核心原因有三个:样本需求低、调试过程透明、硬件要求不高。深度学习方案对样本量和训练环境都有门槛,而传统方案只要图像质量不是太离谱,几十行算子就能跑出一个可用的结果。
1.1 为什么选Halcon而不选OpenCV
很多新手会纠结Halcon和OpenCV的选型。这里不吹不黑,直接说两者在这个项目里的实际差异:
| 对比维度 | Halcon | OpenCV |
|---|---|---|
| 开发效率 | 算子封装度高,几行代码完成核心功能 | 需要手写大量逻辑,代码量明显更大 |
| 调试体验 | HDevelop交互式调试,中间结果可视化非常方便 | 需要自己写imshow,对比过程图较繁琐 |
| 形态学/区域操作 | 区域(Region)是一等公民,操作思路直观 | 更多基于像素矩阵,思维模式偏底层 |
| 商业授权 | 收费,但学习版/试用版够用 | 免费开源 |
| 适合场景 | 工业视觉项目,交付效率高 | 研究算法、轻量部署 |
说点实际感受。我第一次用Halcon跑车牌识别的时候,最大的感触是“区域操作太方便了”。OpenCV里的阈值分割、轮廓查找、筛选,每一步都得自己维护数据结构;而Halcon里把Region这个东西做到了极致,分割出来的区域天然支持面积、宽高比、形状等特征筛选,很多在OpenCV里要写几十行的逻辑,Halcon一个select_shape搞定。
另外,HDevelop的调试环境对入门者极其友好。每个算子的输出都能立刻可视化,阈值选高了还是选低了、形态学核选大了还是选小了,眼睛一看就知道。这种“所见即所得”的调试方式,对理解图像处理每一步在干什么非常有帮助。
1.2 简易车牌识别的整体技术流程
整个项目可以用一条清晰的流水线来概括:
- 读取图像,转灰度图预处理
- 通过灰度阈值和形态学处理定位车牌区域
- 裁剪出车牌区域,做字符级的二值化
- 对车牌区域做字符分割,按顺序排列
- 用模板匹配或OCR识别每个字符
- 后处理校验,输出完整车牌号
这个流程基本覆盖了传统机器视觉项目的地基:预处理、分割、特征提取、模式识别。这里面最核心的难点是第2步“车牌定位”,因为自然场景下的背景干扰最多;真正到了字符识别阶段,只要前两步做扎实,识别反而相对简单。这也是为什么标题强调“简易”——它不求应对各种极端场景,只要求在可控条件下跑通全流程。
有一点要提醒:车牌识别这个方向,难点从来不在于“识别”本身,而在于“定位”和“分割”。很多新手一上来就研究字符识别的算法选型,结果发现识别准确率很高但根本没定位到车牌,这是走偏了。我的建议是把70%的调试精力放在定位环节。
2. 车牌定位:从复杂背景中锁定车牌区域
车牌定位是整个项目里最考验图像处理基本功的环节。实际拍摄的图片里,背景可能是路面、绿化带、车身、建筑,如何把蓝底白字的车牌从这些干扰中剥出来,核心套路是先靠颜色或灰度差异做初筛,再用形态学操作和形状特征做精筛。
2.1 图像预处理与灰度化思路
Halcon里读取图像后,第一步通常是灰度化。
read_image (Image, 'car_plate.jpg') rgb1_to_gray (Image, GrayImage)有人会问:既然车牌是蓝色的,直接用彩色通道处理不是更好吗?确实可以,而且某些场景下效果更好。但简易方案里我建议先用灰度图,原因有二:一是灰度化后所有后继操作都是单通道,逻辑简单、调试直观;二是很多自然图像里的干扰来自颜色相近的区域,灰度化反而能减少颜色因素带来的误判。
当然,如果你的图像里车身的蓝色和车牌蓝非常接近,灰度化后可能会粘连在一起难以分离。这时候可以换个思路,在HSV空间里提取蓝色通道。但既然标题是“简易车牌识别”,这里不做深度展开,灰度化方案已能覆盖大多数白天光线下的场景。
2.2 灰度阈值分割与形态学处理
灰度化之后,关键一步是阈值分割。蓝底车牌的灰度值通常呈现一个特征:在灰度图上,蓝色车牌的亮度介于深色背景和白色字符之间。实际操作中,我们会先用threshold初步提取一个大致区域:
threshold (GrayImage, Region, 80, 180)这个阈值的取值没有标准答案,完全取决于你的图像样本。括号里“80, 180”的意思是提取灰度值在80到180之间的像素区域。如果你发现车牌区域被漏掉了,说明阈值范围选窄了;如果背景干扰太多,说明范围选宽了。
阈值分割得到的是一个个离散的像素块,看起来杂乱无章。这时候就要靠形态学操作把它们连成完整的候选区域。最常用的是开运算和闭运算的组合:
opening_circle (Region, RegionOpening, 3.5) closing_circle (RegionOpening, RegionClosing, 8.5)开运算的作用是去掉细小的噪声点,闭运算的作用是填补区域内部的空洞、连接断开的边缘。车牌本身的字符是白色的,会形成一个个空洞,闭运算后字符空洞被填补,整个车牌形成一个完整的矩形块。这一步做完,车牌区域的雏形就出来了。
这里有一个非常实用的调参思路:先做开运算,逐渐增大半径,观察噪声点是否消失;再做闭运算,逐渐增大半径,观察车牌区域是否连成整体。每次调整后用HDevelop的可视化窗口看中间结果,基本上调个三四轮就能找到合适的参数组合。
2.3 用形状特征筛选候选区域
形态学处理后,图像里还会残留很多非车牌区域。接下来用形状特征做筛选。车牌区域最典型的特征是宽高比,标准燃油车牌的宽高比约为3.14左右。实际拍摄时因为透视角度差异,通常会落在2.5到4.0之间。Halcon里的select_shape就是为这种情况设计的:
connection (RegionClosing, ConnectedRegions) select_shape (ConnectedRegions, SelectedRegions, ['width', 'height', 'area'], 'and', [180, 50, 4000], [450, 160, 30000])这段代码的意思是:先把形态学区域拆解成一个个独立连通域,然后按宽度180到450像素、高度50到160像素、面积4000到30000像素的约束条件筛选。这样就能把大多数细长条、小碎块、超大块干扰全部滤除。
如果筛选完还剩多个候选区域,可以用sort_region按面积或者坐标排序,取面积最大或位置最居中的那个。再到HDevelop窗口里用smallest_rectangle2算子取候选区域的最小外接矩形,如果矩形的倾斜角度接近水平,则可以直接按矩形坐标裁剪出来。
有同行可能会问:如果车牌是倾斜的怎么办?简易方案里先不考虑校正,直接对矩形区域做截取也能凑合,但识别率的确会受到一定影响。如果你的图像里存在明显倾斜角,可以在截取前用deskew类算子或者仿射变换做摆正,这个属于进阶优化,项目跑通后再加不迟。
2.4 从图像中裁剪出车牌区域
定位完成之后,用reduce_domain把车牌区域从原图里裁出来:
reduce_domain (GrayImage, SelectedRegions, PlateImage)到这一步,你已经拿到了一个只包含车牌的小图,后续的字符分割和识别都在这个小图上操作。处理区域变小之后,算法速度会快很多,而且干扰少,识别率也会明显提升。我把这部分单独列出来,是想强调一个工程习惯:能用区域裁剪缩小处理范围的地方,一定要裁剪,不要动不动就在全图上跑算法。
3. 字符分割与识别:把车牌拆解成可识别的单元
车牌定位完成,下一步是把车牌里的字符一个个拆出来,然后分别识别。这一步的成败取决于二值化做得好不好,以及字符之间是否粘连、断裂。
3.1 车牌字符分割的完整流程
拿到裁剪后的车牌区域,第一步是二值化。因为车牌字符是白色的,背景是蓝色,灰度对比非常明显:
threshold (PlateImage, CharRegion, 100, 255)这一步提取的就是字符本身的白色像素。提取完之后再用connection算子做连通域分析,每一个连通域理论上对应一个字符或者字符的一部分。
但实际处理时有两个问题几乎是必然遇到的:字符断裂和字符粘连。断裂的典型场景是光照不均导致字符中间出现暗纹,粘连的典型场景是字符边缘模糊导致相邻字符之间有灰度过渡区域。处理思路如下:
* 先用开运算消除小的断裂噪声 opening_circle (CharRegion, CharOpening, 1.5) * 再用闭运算把同一字符内断裂的部分连接起来 closing_circle (CharOpening, CharClosing, 2.5) * 连通域分析 connection (CharClosing, CharConnected) * 按字符宽度、高度、面积筛选有效连通域 select_shape (CharConnected, CharSelected, ['width', 'height'], 'and', [15, 20], [80, 150]) * 按照从左到右的顺序排列字符 sort_region (CharSelected, SortedChars, 'character', 'true', 'row')这里的开运算核设置为1.5,闭运算核设置为2.5,是一个偏保守的参数。核太大容易把相邻字符连成一片,核太小又解决不了字符断裂问题。实际操作时建议把核大小做成参数,用同一个车牌图像反复测试,找到“既能断开相邻字符、又能连上同一字符内断裂”的临界值。
sort_region这一步千万别省。Halcon默认的连通域顺序不一定是按从左到右的阅读顺序排序的,直接识别会输出乱序的车牌号。用sort_region按“character”模式排序后,得到的才是从左到右依次排列的字符序列。
3.2 识别方案:模板匹配还是OCR分类器
字符分割完成后,接下来就是字符识别。Halcon提供两条路:一是用自带OCR分类器(read_ocr_class_mlp直接加载训练好的模型),二是自己建模板做模板匹配。
这两条路的对比很明确:
| 对比项 | OCR分类器 | 模板匹配 |
|---|---|---|
| 前提条件 | 需要Halcon授权中包含OCR功能 | 只需标准图像处理算子和匹配算子 |
| 识别速度 | 较快 | 中等,字符多时需要逐个匹配 |
| 字符集灵活性 | 需要训练或选择合适分类器 | 完全自主控制模板库 |
| 蓝牌字符适应性 | 较好,对模糊、噪声鲁棒 | 对清晰图像效果好,模糊场景稍弱 |
简易项目里我推荐用模板匹配。理由很实在:模板匹配的逻辑一眼就能看懂,对新手特别友好。你准备一些标准字符图(数字0到9、字母A到Z、省份简称汉字),识别时把每个待识别字符跟模板逐一比对,取相似度最高的作为结果。这个思路几乎不需要解释,上手就能理解。
模板匹配的具体做法是先用create_shape_model建立模板,再用find_shape_model在待识别区域里找最佳匹配。对数字和字母来说,找一组清晰的车牌字符图片,抠出单个字符做成模板即可。省份汉字比较麻烦,因为各省简称字形差异大,模板数量多。简易方案可以先只做几个常见省份的汉字模板,后续再扩展。
有一点必须说:模板匹配对图像质量的要求偏高。如果字符分割后字符本身有残缺,或者透视导致字符变形,匹配的相似度会明显下降。所以做模板匹配前,尽量保证输入字符的清晰度。如果你的车牌图像本身就是模糊的,那不要纠结模板匹配,老老实实去研究Halcon自带的OCR分类器更靠谱。
3.3 字符间“O和0、I和1”混淆的处理
识别完所有字符之后,最后一步是做结果校验。车牌是有固定规则的字符串,可以利用这个规则修正识别结果。最经典的例子是“O”和“0”、“I”和“1”的混淆。英文字母O和数字0在图像上极其相似,模板匹配很可能把它们搞混。
处理办法是在后处理阶段加入规则约束。以标准燃油车牌为例:第一位是省份简称汉字,第二位是英文字母,第三到第七位是字母和数字的组合。其中第二位不可能是“I”和“O”,因为交管规定中这两个字母不用于号牌。所以如果第二位识别出“I”或“0”,可以判定为“1”或“0”。类似地,如果后几位识别出“O”,在中文车牌语境下大概率是“0”。
这段规则逻辑用简单的条件判断就能实现:
* 假设CharList是排序后的字符识别结果 * 第二位如果是I或O,视为异常,按相似度重新匹配实际处理中,你可以把“O/0”模板同时保留,让匹配算子输出得分最高的候选,然后利用位置规则在多个候选里挑一个合理的。这就是“图像算法+业务规则”的组合打法,能有效提升最终识别准确率。这个技巧在工业生产里也特别常用,千万别只盯着算法本身,业务规则往往是性价比最高的优化手段。
4. 实操全流程:从Halcon代码到完整Demo运行
前面把各个核心环节都拆开讲了一遍,这一节把它们串成一个完整的可运行的Demo。我会按实际操作顺序来写,尽量做到每一步都能照着操作。
4.1 开发环境准备与图像素材建议
Halcon的开发环境是HDevelop,安装包可以到Halcon官方网站下载试用版,试用版对学习来说完全够用。安装完成后打开HDevelop,新建一个程序文件即可开始。
图像素材是不少人的痛点。这里有两个建议,一是自己去停车场、路边用手机拍几张清晰的车牌照片,二是用模拟车牌图片。要注意的是,如果使用模拟车牌做项目,最好选带有光照变化和轻微透视的版本,不要用纯正面、纯平整的素材,否则项目跑通后换真实图像容易直接翻车。
我自己的经验是准备6到10张不同场景的测试图:正面停车、侧向30度、阴影遮挡一半、曝光偏亮、曝光偏暗、夜晚车灯照射。这些场景基本覆盖了“简易方案能处理”的边界。项目跑到什么程度、哪些场景识别不了,心里要有数。
4.2 完整代码流程与关键参数表
下面是一套能够跑通的基本代码框架,结合了前面的所有步骤。为方便阅读,我按阶段编号注释了:
* 1. 读取图像并灰度化 read_image (Image, 'plate.jpg') rgb1_to_gray (Image, GrayImage) * 2. 车牌定位 threshold (GrayImage, Region, 80, 180) opening_circle (Region, RegionOpening, 3.5) closing_circle (RegionOpening, RegionClosing, 8.5) connection (RegionClosing, ConnectedRegions) select_shape (ConnectedRegions, SelectedRegions, ['width', 'height', 'area'], 'and', [180, 50, 4000], [450, 160, 30000]) * 若存在多个候选区域,选择面积最大的区域 select_shape_max (SelectedRegions, MaxRegion, 'area') reduce_domain (GrayImage, MaxRegion, PlateImage) * 3. 字符分割 threshold (PlateImage, CharRegion, 100, 255) opening_circle (CharRegion, CharOpening, 1.5) closing_circle (CharOpening, CharClosing, 2.5) connection (CharClosing, CharConnected) select_shape (CharConnected, CharSelected, ['width', 'height'], 'and', [15, 20], [80, 150]) sort_region (CharSelected, SortedChars, 'character', 'true', 'row') * 4. 字符识别(模板匹配方式) for i := 1 to |SortedChars| by 1 reduce_domain (PlateImage, SortedChars[i-1], CharImage) find_shape_model (CharImage, CharModelID, 0, 0, 0.5, 1, 0.5, 'false', 0, 0, Row, Column, Angle, Score) * 根据Score选择最佳模板,输出字符 endfor这段代码是一个骨架,真正跑的时候有几个参数需要根据你的图像调整。我把最关键的参数整理成了一张表:
| 算子在文件名 | 参数/阈值 | 调整方向 |
|---|---|---|
| threshold(定位) | 80, 180 | 车牌区域漏检则放宽下限;背景干扰多则收窄范围 |
| opening_circle(定位) | 3.5 | 噪声多则加大;区域丢失则减小 |
| closing_circle(定位) | 8.5 | 区域断裂则加大;背景粘连则减小 |
| threshold(字符) | 100, 255 | 字符残缺则降低下限;背景噪点多则提高下限 |
| opening_circle(字符) | 1.5 | 断裂则减小;噪声多则加大 |
| closing_circle(字符) | 2.5 | 字符断裂则加大;字符粘连则减小 |
| select_shape(字符) | 宽15~80,高20~150 | 按实际字符尺寸调整 |
这张表的价值在于:当你的结果不对时,不要盲目乱改,先判断是“哪种现象”,再去对应调整参数。项目调试里最高的效率来自这种“现象对照参数”的调优方式。
4.3 实际运行效果与处理速度优化建议
我拿一组自己拍的测试图来验证这套代码,运行环境是普通笔记本电脑。处理速度方面,一张1920x1080的图片完整跑完大约耗时150毫秒左右,对于“简易”项目来说已经可以接受。
识别率的表现大致如下:
| 场景 | 车牌定位成功率 | 字符识别成功率 | 整体成功率 |
|---|---|---|---|
| 白天正面、光线均匀 | 约95% | 约90% | 约85%以上 |
| 白天侧面、轻微倾斜 | 约85% | 约85% | 约70%到75% |
| 阴影遮挡部分车牌 | 约60%到70% | 约70% | 约40%到50% |
| 夜间、灯光不明显 | 约50%以下 | 约50%以下 | 不稳定 |
这个结果应该能看出来,简易方案的瓶颈从高到低依次是:弱光环境、遮挡、倾斜。如果你的项目后续想提升识别率,优先解决的是图像质量改善,比如增加光源、正对车牌拍摄,而不是盲目换算法。这一点做项目的时候要有一个清醒的认识。
速度优化方面有几个性价比很高的做法。一是把图像先缩小再处理,分辨率下降之后,定位环节所需的时间能减少三分之一甚至一半;二是用reduce_domain之后再做后续操作,处理区域小了,算子耗时自然下降;三是在交互式调试完成后,用dev_update_off关闭窗口更新,能大幅减少HDevelop内部刷新耗时。
还有一点:如果你最终要把这套东西做成独立程序,可以把图像采集和处理做成一个循环,从本地视频或相机实时取流,逐帧调用上面这些算子。循环里最需要注意的是每帧之间窗口状态的清理,否则HDevelop里会同时弹出大量窗口,直接卡死。
5. 常见问题与排查技巧实录
这部分直接整理成速查表,每一条都是我在实际调试中踩过的坑,或者帮别人看代码时遇到的高频问题。
5.1 定位不到车牌或候选区域过多
定位不到车牌,最常见的三个原因分别是阈值范围不当、形态学结构元过大、车牌在图像中占比太小。阈值范围不当会导致车牌区域根本没有进入threshold的灰度区间;结构元过大会直接把车牌区域“抹平”;占比太小则会在select_shape里被当噪声滤除。
遇到候选区域过多的情况,优先检查select_shape的约束条件是否太宽。可以把width、height的范围日志打印出来看一下,车牌区域的宽高比一般在3左右,如果筛选出来的区域宽高比忽大忽小,说明阈值或者形态学步骤出了问题,导致背景被过度保留。
一个比较实用的调试技巧是:在HDevelop里用disp_obj分别查看每一步的中间结果,看到哪一步开始出现异常,问题就锁定在哪一步。很多新手卡了很久的问题,一查中间图像立刻就能定位到原因。
5.2 字符粘连或断裂怎么处理
字符粘连的典型表现是识别出来的字符只有一个超宽连通域,分割后直接变成一个物体。解决办法是调整字符分割时的closing_circle核大小,核太大容易黏连,核太小又无法连接断裂部分。这里需要耐心地二分法试参数。
字符断裂的典型表现是同一个字符被分割成了两到三个连通域,最终选择区域时会漏掉部分字符。解决办法第一选择是调大closing_circle,先用闭运算把断口“焊”起来,如果还不行,可以对字符区域做一次dilation(膨胀)。但要小心,膨胀也会让字符边缘变粗,影响后续匹配精度,所以膨胀核宁小勿大。
5.3 字符识别结果中O和0、I和1混淆
这个问题在模板匹配方案里几乎必然出现。最简单的处理办法是准备好O、0、I、1的模板,在识别时同时输出得分前两位的候选,然后靠后处理规则判定。第二位不可能是O和I,后五位里O更大概率是0,I更大概率是1。如果匹配得分差距非常小,规则判定可以放在最高优先级。
5.4 处理速度太慢怎么办
如果一帧处理时间超过1秒,先检查是不是HDevelop的窗口在线更新拖慢了速度,用dev_update_off关闭。再检查是否在全图上跑了形态学大半径算子,大半径的形态学操作非常耗时,建议先缩小图像,或者用金字塔模式的金字塔匹配。
这里的优化逻辑很简单:不要让算法在大画布上做全局操作,而是尽快缩小处理区域。定位阶段哪怕先用很小的图像找到车牌的大致位置,再映射回原图坐标做精处理,速度也会比全程高清处理快得多。这个思想在Halcon项目里非常重要。
另外,如果连续跑视频帧,可以尝试把模板匹配的金字塔层数提高。find_shape_model的时间复杂度跟模板大小和搜索范围相关,提高金字塔层数意味着在高缩小的图像里先粗匹配,再逐级细化,速度会有明显提升,但匹配精度可能会略有下降,需要根据实际识别效果权衡。
最后再分享几点个人体会
这个简易车牌识别项目,我前前后后帮朋友调过好多次代码,每次看到新手在定位环节反复改动阈值都很有感触。其实这个项目真正的核心价值在于:它能够很自然地把图像处理和机器视觉的基本功串起来。很多人说“学了Halcon但不知道拿来做什么”,这个项目就是一个很好的桥梁。从阈值到形态学,从连通域到特征筛选,从模板匹配到规则后处理,机器视觉项目里最常见的技术点几乎全部覆盖到了。
如果后续想做得更深入,有几个方向可以考虑:一是把车牌的倾斜校正补上,能明显提升侧方拍摄场景的识别率;二是研究Halcon的OCR分类器,不再局限于模板匹配;三是在定位环节引入多尺度分析,让算法适应车牌大小变化。这些方向都能在现有框架上稳步扩展,祝调试顺利。