news 2026/10/16 10:54:03

Halcon车牌识别实战:传统图像处理搞定定位与字符分割

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Halcon车牌识别实战:传统图像处理搞定定位与字符分割

直接上结论:Halcon做车牌识别,比很多人想象中要简单得多。不管你是刚入行机器视觉的工程师,还是学校里头一次接触Halcon做课程设计的同学,“基于Halcon的简易车牌识别”都是一个非常合适的练手项目。它不依赖深度学习、不需要训练模型,只用Halcon里最常用的图像处理算子和模板匹配就能跑通一整套流程:定位车牌、分割字符、识别字符、输出结果。这个项目的价值不在于“能识别得多准”,而在于它能完整逼你走一遍机器视觉项目的基本路线,把图像预处理、形态学操作、区域筛选、模板匹配这些基本功练扎实。这篇文章就围绕这个标题,把整个项目的设计思路、代码实现和踩坑记录全部分享出来,希望对准备上手Halcon的同行有帮助。

1. 项目整体设计与方案选型思路

先说结论:简易车牌识别不用上深度学习,传统图像处理方案在这个场景下反而更合适。核心原因有三个:样本需求低、调试过程透明、硬件要求不高。深度学习方案对样本量和训练环境都有门槛,而传统方案只要图像质量不是太离谱,几十行算子就能跑出一个可用的结果。

1.1 为什么选Halcon而不选OpenCV

很多新手会纠结Halcon和OpenCV的选型。这里不吹不黑,直接说两者在这个项目里的实际差异:

对比维度HalconOpenCV
开发效率算子封装度高,几行代码完成核心功能需要手写大量逻辑,代码量明显更大
调试体验HDevelop交互式调试,中间结果可视化非常方便需要自己写imshow,对比过程图较繁琐
形态学/区域操作区域(Region)是一等公民,操作思路直观更多基于像素矩阵,思维模式偏底层
商业授权收费,但学习版/试用版够用免费开源
适合场景工业视觉项目,交付效率高研究算法、轻量部署

说点实际感受。我第一次用Halcon跑车牌识别的时候,最大的感触是“区域操作太方便了”。OpenCV里的阈值分割、轮廓查找、筛选,每一步都得自己维护数据结构;而Halcon里把Region这个东西做到了极致,分割出来的区域天然支持面积、宽高比、形状等特征筛选,很多在OpenCV里要写几十行的逻辑,Halcon一个select_shape搞定。

另外,HDevelop的调试环境对入门者极其友好。每个算子的输出都能立刻可视化,阈值选高了还是选低了、形态学核选大了还是选小了,眼睛一看就知道。这种“所见即所得”的调试方式,对理解图像处理每一步在干什么非常有帮助。

1.2 简易车牌识别的整体技术流程

整个项目可以用一条清晰的流水线来概括:

  1. 读取图像,转灰度图预处理
  2. 通过灰度阈值和形态学处理定位车牌区域
  3. 裁剪出车牌区域,做字符级的二值化
  4. 对车牌区域做字符分割,按顺序排列
  5. 用模板匹配或OCR识别每个字符
  6. 后处理校验,输出完整车牌号

这个流程基本覆盖了传统机器视觉项目的地基:预处理、分割、特征提取、模式识别。这里面最核心的难点是第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分类器,不再局限于模板匹配;三是在定位环节引入多尺度分析,让算法适应车牌大小变化。这些方向都能在现有框架上稳步扩展,祝调试顺利。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 10:54:03

Promise.race、all、allSettled区别与异步并发选型指南

深入剖析Promise.race、Promise.all和Promise.allSettled的区别如果你写过一点异步代码,肯定遇到过这种场景:多个请求同时发出去,要么全等、要么等最快那个、要么不管成功失败都要拿结果。Promise.all、Promise.race、Promise.allSettled这三…

作者头像 李华
网站建设 2026/10/16 10:53:30

JCache缓存驱逐与过期:触发机制、底层差异与生产实践

最近有小伙伴在准备面试时问到我:JCache(JSR-107)里缓存驱逐(Eviction)和缓存过期(Expiration)到底有什么区别?我反问他怎么理解,他说“驱逐不就是把过期的删掉吗&#x…

作者头像 李华
网站建设 2026/10/16 10:53:03

RAGFlow本地RAG框架:开箱即用的文档问答系统

简介:RagFlow-main 是一个面向大模型研发与工程化实践的开源框架代码库,适用于AI算法工程师、深度学习开发者及高校科研人员,聚焦于流图驱动的大规模模型训练与部署优化。资源包含564个文件,以97个Python核心模块、95个TypeScript…

作者头像 李华
网站建设 2026/10/16 10:53:01

MCP实战:为Claude Code接入Google实时搜索

Claude Code 用久了,最让人难受的不是它写代码的能力,而是它查不到新东西。模型的知识有截止时间,你问它某个库的最新版本、某个报错的最新解法、某个框架刚发布的新接口,它只会基于记忆和推理来回答,经常给出一个看起…

作者头像 李华
网站建设 2026/10/16 10:52:52

净化门厂家怎么选?一线采购的选型逻辑与考察要点

干了这么多年净化工程采购,经常有人上来就问一句“净化门厂家哪家好”,说实话这个问题我特别怕。净化门这东西,看着就是一扇门,实际拆开讲,门板材质、填充物、密封结构、五金件、自动化配置,每一项都能差出…

作者头像 李华
网站建设 2026/10/16 10:51:52

iOS卡片堆叠无限滑动实现原理与工程实践

1. 项目概述:为什么“卡片堆叠无限滑动”成了iOS界面设计的高频刚需 最近在好几个实际项目里,都碰到了同一个需求:用户要像翻扑克牌一样,一张张滑动浏览内容,每张卡片带轻微位移和透明度渐变,滑到最前时自动…

作者头像 李华