1. 车牌识别项目的真实需求拆解很多人第一次接触车牌识别,脑子里想的都是“拍张照,把车牌上的字读出来”,觉得这事儿无非就是OCR。但真到项目落地的时候,你会发现客户提的需求远比“读出来”复杂得多。我做过几个不同场景的车牌识别项目,有停车场出入口的,有高速卡口的,也有小区门禁的,每个场景对识别的侧重点完全不同。停车场关心的是识别速度和通行效率,高速卡口关心的是大角度、高速度下的抓拍准确率,小区门禁则更在意夜间和恶劣天气下的稳定性。Halcon在这类项目里的定位很明确:它是一套底层视觉算法库,不是开箱即用的成品软件。这意味着你需要自己搭建从图像采集、预处理、车牌定位、字符分割到字符识别的完整链路。好处是灵活,坏处是每个环节都得自己调。我见过不少同行一开始想用现成的车牌识别SDK,结果发现要么授权费用高,要么对特定车牌样式支持不好,最后还是回到Halcon自己搭。这个项目的核心需求可以归纳为三点:第一,能稳定定位到车牌区域,不管车牌在图像里是倾斜的、有反光的还是被部分遮挡的;第二,能准确分割出单个字符,尤其是中文车牌里汉字和字母数字混排的情况;第三,能识别出字符内容,并且对易混淆字符(比如0和O、1和I、8和B)有足够的区分能力。这三点听起来简单,但每一步都有坑。适合谁来参考这篇内容?如果你已经会用Halcon做基本的图像处理,比如阈值分割、形态学操作、模板匹配,那这篇内容能帮你把零散的知识点串成一条完整的车牌识别流水线。如果你是完全的新手,建议先补一下Halcon的基础算子,不然直接看车牌识别的流程会有点吃力。2. 图像预处理:把“脏图”变成能用的图2.1 为什么预处理决定了后续所有环节的上限车牌识别的第一步不是找车牌,而是把图像质量提上来。我踩过最大的坑就是拿着一堆低质量图像硬做定位,结果参数怎么调都不对。后来才明白,预处理没做好,后面所有环节都是在跟噪声较劲。预处理的目标很明确:增强车牌区域的对比度,抑制背景干扰,把图像变成“车牌突出、背景干净”的状态。Halcon里常用的预处理算子有几个:rgb1_to_gray做灰度化,emphasize做锐化增强,scale_image做对比度拉伸。但具体用哪个、怎么组合,得看你的图像来源。如果是地感线圈触发抓拍的图像,通常光照比较均匀,灰度化之后直接做边缘提取就行。如果是视频流里截取的帧,可能会有运动模糊,这时候得先做shock_filter或者derivate_gauss来增强边缘。2.2 灰度化不是简单取平均很多人以为灰度化就是把RGB三个通道取平均,但Halcon的rgb1_to_gray用的是加权平均,权重接近人眼对亮度的感知。这个细节在车牌识别里很重要,因为车牌底色(蓝底白字、黄底黑字、绿底白字)在不同通道下的对比度差异很大。我实测过,对于蓝底白字的车牌,在蓝色通道下字符和底色的对比度最高,直接提取蓝色通道做后续处理,比灰度化效果更好。* 读取图像 read_image (Image, 'car_plate.jpg') * 分离通道 decompose3 (Image, R, G, B) * 对于蓝底车牌,蓝色通道对比度更好 * 对于黄底车牌,红色通道更合适这个技巧在项目里很实用:先判断车牌类型,再选择对应的通道做处理。判断方法也简单,统计图像里蓝色像素和黄色像素的比例就行。2.3 光照不均的处理策略实际场景里光照不均是最头疼的问题。地下车库出入口,一边是暗的,一边是强光,同一张图里车牌区域可能一半过曝一半欠曝。Halcon里处理光照不均的经典方法是illuminate算子,它通过估计背景光照分布来做校正。但illuminate的参数不好调,掩膜尺寸选大了校正效果不明显,选小了会把车牌字符也当成光照变化给抹掉。我的经验是:先用大掩膜做一次粗校正,再用小掩膜做局部增强。具体参数得根据图像分辨率来定,一般掩膜尺寸取车牌宽度的1.5到2倍比较合适。如果图像分辨率是1920×1080,车牌宽度大概200像素,那掩膜尺寸取300到400之间。注意:预处理阶段不要过度追求“好看”,目标是让后续的定位和分割更稳定。有时候看起来有点噪点的图像,反而比过度平滑的图像更容易定位。3. 车牌定位:从整张图里把车牌“抠”出来3.1 基于颜色特征的定位方法车牌定位最直接的方法是颜色定位。中国车牌的颜色规范很明确:蓝底白字、黄底黑字、绿底白字(新能源)、白底黑字(警用)。Halcon里做颜色定位的流程是:转换到HSV空间,用threshold提取特定色调范围,再用connection和select_shape筛选出候选区域。* 转换到HSV空间 trans_from_rgb (Image, H, S, V, 'hsv') * 提取蓝色区域(蓝底车牌) threshold (H, BlueRegion, 180, 240) * 连通域分析 connection (BlueRegion, ConnectedRegions) * 筛选矩形度高的区域 select_shape (ConnectedRegions, PlateCandidates, 'rectangularity', 0.8, 1.0)这个方法在光照均匀、背景简单的场景下效果很好,但在复杂背景里容易误检。比如蓝色的车身、蓝色的广告牌都会被选进来。所以颜色定位通常只作为粗定位,后面还得用形状特征和纹理特征来验证。3.2 基于边缘和形态学的定位方法边缘定位的思路是:车牌区域有密集的垂直边缘(字符的笔画),用sobel_amp提取边缘后,做水平方向的形态学闭运算,把字符边缘连成一片,再筛选出矩形区域。* 边缘提取 sobel_amp (GrayImage, EdgeAmplitude, 'sum_abs', 3) * 水平方向闭运算,连接字符边缘 gen_rectangle1 (Rect, 0, 0, 1, 20) closing (EdgeAmplitude, RegionClosing, Rect) * 连通域分析 connection (RegionClosing, ConnectedRegions) * 筛选宽高比在2.5到5之间的区域(车牌典型宽高比) select_shape (ConnectedRegions, PlateRegions, 'ratio', 2.5, 5.0)这个方法的优势是不依赖颜色,对褪色车牌、污损车牌也能工作。缺点是计算量大,而且对参数敏感。闭运算的结构元素尺寸很关键:太小了字符边缘连不起来,太大了会把相邻的车牌和背景连在一起。我的经验是结构元素宽度取字符笔画宽度的3到5倍比较合适。3.3 两种方法的融合策略单独用颜色或者边缘都有局限,实际项目里我通常把两种方法结合起来。先用颜色定位快速筛选出候选区域,再用边缘特征验证。验证的方法也简单:在候选区域里统计垂直边缘的密度,车牌区域的边缘密度明显高于背景。还有一种更稳的策略是多尺度定位。先在降采样后的图像上做粗定位,找到大概位置后再在原图上做精定位。这样既能保证速度,又能保证精度。降采样倍数一般取2到4倍,太小了速度提升不明显,太大了会丢失小目标。定位方法优势局限适用场景颜色定位速度快,实现简单受光照和背景颜色影响大光照均匀、背景简单边缘定位不依赖颜色,鲁棒性好计算量大,参数敏感复杂背景、褪色车牌融合定位兼顾速度和鲁棒性实现复杂度高大多数实际项目3.4 定位后的倾斜校正定位到的车牌往往不是水平的,有倾斜角度。倾斜校正的方法有两种:一种是基于smallest_rectangle2获取车牌的倾斜角度,然后用rotate_image旋转校正;另一种是基于投影法,通过统计水平和垂直方向的投影来估计倾斜角。* 获取最小外接矩形 smallest_rectangle2 (PlateRegion, Row, Column, Phi, Length1, Length2) * 旋转校正 rotate_image (Image, ImageRotated, -Phi, 'constant')smallest_rectangle2返回的Phi是弧度值,旋转的时候要注意方向。我踩过的坑是直接拿Phi去旋转,结果角度方向反了,车牌越转越歪。正确的做法是先测试一下,或者用rotate_image的interpolation参数控制插值方式,一般用bilinear就够了。4. 字符分割:把车牌“切”成单个字4.1 投影法分割的原理和参数调优字符分割最常用的方法是垂直投影法。原理很简单:把车牌区域二值化后,统计每一列的白色像素数量,字符所在列的投影值高,字符之间的间隙投影值低。通过找投影曲线的波谷来确定字符的分割位置。* 二值化 threshold (PlateImage, Region, 100, 255) * 垂直投影 gray_projections (Region, Region, 'simple', HorProjection, VertProjection) * 找波谷 * 这里需要对投影数组做后处理,找到连续的零值区间投影法的难点在于波谷的判定。如果车牌有污损或者字符粘连,投影曲线可能没有明显的波谷。我的经验是设置一个动态阈值:先计算投影值的平均值,然后取平均值的0.2到0.3倍作为波谷判定阈值。这样比固定阈值更适应不同的图像质量。4.2 处理粘连字符和断裂字符粘连字符是分割里最麻烦的情况。两个字符粘在一起,投影曲线上没有波谷,直接分割会切错。处理方法有两种:一种是基于字符宽度的先验知识,如果某个区域的宽度明显大于单个字符的平均宽度,就强制从中间切开;另一种是基于连通域分析,用connection把字符区域分开,再根据面积和宽高比筛选。断裂字符相对好处理,通常是字符笔画太细或者二值化阈值太高导致的。解决方法是用形态学闭运算把断裂的部分连起来,或者降低二值化阈值。但阈值降太低会把背景噪声也带进来,所以得权衡。* 形态学闭运算连接断裂字符 gen_rectangle1 (Rect, 0, 0, 1, 3) closing (CharRegion, CharRegionClosed, Rect) * 连通域分析 connection (CharRegionClosed, CharRegions) * 按面积筛选 select_shape (CharRegions, ValidChars, 'area', 50, 5000)4.3 中文车牌的字符分割特殊性中文车牌的第一个字符是汉字,后面是字母和数字。汉字的笔画比字母数字复杂得多,投影曲线也更不规则。如果统一用一套参数分割,汉字部分很容易切错。我的做法是分两段处理:先分割出第一个汉字,剩下的部分再按字母数字的规则分割。汉字分割的关键是确定汉字的宽度范围。中国车牌的汉字宽度一般在车牌总宽度的1/7到1/6之间,可以根据这个先验知识来约束分割位置。另外,汉字区域的投影曲线通常比字母数字区域更“毛糙”,可以用sigma滤波先平滑一下再找波谷。提示:字符分割的准确率直接影响最终识别率。如果分割阶段切错了,后面的识别再准也没用。所以在这个环节多花时间调参数是值得的。5. 字符识别:从“看到”到“认出”5.1 基于OCR分类器的识别流程Halcon提供了read_ocr和read_ocr_class_mlp等算子来做字符识别。基本流程是:先加载预训练的分类器,然后把分割好的字符区域传给分类器,得到识别结果。* 加载OCR分类器 read_ocr_class_mlp ('Industrial_0-9A-Z_NoRej.omc', OCRHandle) * 识别字符 do_ocr_multi_class_mlp (CharRegions, PlateImage, OCRHandle, Class, Confidence) * 清除句柄 clear_ocr_class_mlp (OCRHandle)Halcon自带的分类器有几种:Industrial_0-9A-Z适合工业场景的字母数字识别,Document_0-9A-Z适合文档扫描,Universal_0-9A-Z通用性更强但精度稍低。对于车牌识别,我一般用Industrial系列的,因为车牌字符的字体比较规范,工业分类器训练时用的样本更接近。5.2 训练自己的OCR分类器Halcon自带的分类器对中国车牌的汉字支持不好,因为汉字不在它的训练集里。所以实际项目里通常需要自己训练分类器。训练流程是:收集大量车牌字符样本,标注好每个样本对应的字符,然后用train_ocr_class_mlp训练。* 创建OCR分类器 create_ocr_class_mlp (8, 10, 'constant', 'default', ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q','R','S','T','U','V','W','X','Y','Z','京','沪','粤','苏','浙','鲁','川','渝','冀','豫','云','辽','黑','湘','皖','闽','赣','鄂','桂','甘','晋','蒙','陕','吉','贵','青','藏','新','宁','琼'], 0.01, 10, 'normalization', 10, 42, OCRHandle) * 训练 train_ocr_class_mlp (OCRHandle, TrainingFile)训练样本的质量比数量更重要。我试过用5000个样本训练,但样本里有很多模糊的、倾斜的,结果识别率还不如用2000个清晰样本训练出来的。样本采集的时候要注意覆盖不同光照条件、不同拍摄角度、不同车牌磨损程度的情况。5.3 易混淆字符的后处理即使分类器训练得很好,0和O、1和I、8和B这些字符还是容易混淆。后处理的思路是利用车牌字符的排列规则来纠正。中国车牌的格式是:汉字+字母+字母数字混合(5位)。第一位一定是汉字,第二位一定是字母,后面五位是字母数字混合但字母不能出现在某些位置。* 后处理规则示例 * 第二位如果是数字0,根据规则应该是字母O * 第五位如果是字母I,根据规则应该是数字1这个规则表可以根据实际车牌规范来定。我一般会维护一个“位置-允许字符集”的映射表,识别结果如果不在允许字符集里,就找最接近的允许字符替换。替换的依据是分类器返回的置信度,选置信度最高的那个。易混淆对区分方法后处理策略0和O位置规则第二位只允许字母,第五位只允许数字1和I位置规则同上8和B位置规则同上2和Z位置规则同上5和S位置规则同上6. 完整流程串联与参数调优实战6.1 从读图到输出的完整代码框架把前面几个环节串起来,一个完整的车牌识别流程大概长这样:* 1. 读取图像 read_image (Image, 'car_plate.jpg') * 2. 预处理 rgb1_to_gray (Image, GrayImage) emphasize (GrayImage, ImageEmphasize, 7, 7, 1.0) * 3. 车牌定位 sobel_amp (ImageEmphasize, EdgeAmplitude, 'sum_abs', 3) gen_rectangle1 (Rect, 0, 0, 1, 20) closing (EdgeAmplitude, RegionClosing, Rect) connection (RegionClosing, ConnectedRegions) select_shape (ConnectedRegions, PlateRegions, 'ratio', 2.5, 5.0) * 4. 倾斜校正 smallest_rectangle2 (PlateRegions, Row, Column, Phi, Length1, Length2) rotate_image (Image, ImageRotated, -Phi, 'bilinear') * 5. 字符分割 threshold (ImageRotated, Region, 100, 255) gray_projections (Region, Region, 'simple', HorProjection, VertProjection) * 根据投影曲线分割字符 * 6. 字符识别 read_ocr_class_mlp ('Industrial_0-9A-Z_NoRej.omc', OCRHandle) do_ocr_multi_class_mlp (CharRegions, ImageRotated, OCRHandle, Class, Confidence) * 7. 后处理 * 根据位置规则纠正易混淆字符这个框架是基础版,实际项目里每个环节都需要根据具体场景调整参数。比如emphasize的掩膜尺寸,图像分辨率高的时候要调大,分辨率低的时候要调小。sobel_amp的滤波核大小也是,一般取3到5之间,太大了边缘会太粗,太小了边缘会断裂。6.2 参数调优的优先级和顺序调参不能瞎调,得有优先级。我的经验是:先调定位参数,定位准了再调分割参数,分割准了最后调识别参数。因为定位错了,后面调得再好也没用。定位参数里最重要的是边缘提取的阈值和形态学结构元素的尺寸。边缘阈值太低会引入大量噪声,太高会丢失弱边缘。我一般先用edges_image算子看一下边缘强度的分布,然后取分布曲线的拐点作为阈值。形态学结构元素的尺寸前面说过,取字符笔画宽度的3到5倍。分割参数里最重要的是二值化阈值和投影波谷的判定阈值。二值化阈值可以用binary_threshold自动计算,也可以用gray_histo看直方图手动选。投影波谷的判定阈值我一般取投影平均值的0.25倍,这个值在大多数场景下都能工作。6.3 实测中的意外情况和处理实测中最常见的意外是车牌定位到了但位置偏了,导致分割的时候把边框也切进去了。处理方法是定位之后用reduce_domain把车牌区域抠出来,再做一次精定位,把边框去掉。精定位的方法可以用boundary提取区域边界,然后向内收缩几个像素。另一个意外是图像里有多辆车牌,定位到了多个候选区域。这时候需要根据区域的位置和大小来筛选。一般离图像中心近的、面积大的候选区域是目标车牌。如果还是分不清,可以结合车辆检测的结果来辅助判断。还有一种情况是车牌被部分遮挡,比如被保险杠挡住了一部分。这种情况下定位可能只能定位到车牌的一部分,分割出来的字符数量不对。处理方法是根据车牌的标准宽高比来推断完整车牌的位置,然后从原图里重新抠取。注意:实际项目里不要追求100%的识别率,能达到95%以上就很好了。剩下的5%可以通过多帧投票或者人工复核来解决。我见过有人为了最后几个百分点的识别率调了一个月参数,投入产出比太低了。7. 性能优化与工程化落地7.1 算法加速的几种手段Halcon的算法在CPU上跑,速度是个瓶颈。一张1080P的图像,完整流程跑下来可能要200到500毫秒。如果要做实时识别,比如视频流每秒25帧,那必须优化。优化的手段有几个:第一,降低处理分辨率。把图像降采样到原来的1/2或1/4,处理速度能提升4到16倍。车牌识别对分辨率的要求没那么高,车牌宽度有100到150像素就足够识别了。第二,缩小处理区域。如果相机位置固定,车牌只会出现在图像的某个区域,那就只处理那个区域,其他部分不处理。第三,用Halcon的并行计算功能。parallelize_operators可以把一些算子分配到多个线程上跑。* 降采样 zoom_image_factor (Image, ImageZoomed, 0.5, 0.5, 'bilinear') * 处理完后再把结果映射回原图坐标7.2 多帧融合提升稳定性视频流场景下,单帧识别可能会出错,但多帧融合能大幅提升稳定性。思路是:对连续多帧的识别结果做投票,出现次数最多的结果作为最终输出。投票的窗口大小一般取5到10帧,太小了起不到融合效果,太大了响应延迟高。还有一种融合策略是利用车牌的物理尺寸约束。同一辆车在连续帧里,车牌的像素宽度变化应该是连续的。如果某一帧识别到的车牌宽度突然跳变,那这一帧的结果大概率是错的,可以丢弃。7.3 工程化部署的注意事项工程化部署的时候,有几个坑我踩过。第一,Halcon的运行时授权问题。开发的时候用的是开发版授权,部署到客户机器上需要运行时授权。这个授权是绑定硬件的,换机器就得重新申请。第二,不同版本的Halcon算子行为可能有差异。我在Halcon 18上调好的参数,换到Halcon 20上跑,结果就不一样了。所以部署的时候要锁定版本。第三,图像采集的接口兼容性。不同品牌的相机SDK不一样,Halcon的open_framegrabber支持的相机品牌有限,不支持的得自己写接口。部署问题表现解决方案授权绑定换机器后无法运行提前申请运行时授权版本差异参数行为不一致锁定Halcon版本相机兼容无法采集图像用相机SDK采集后转Halcon图像7.4 识别结果的置信度过滤Halcon的OCR分类器会返回每个字符的置信度。置信度低的字符大概率是识别错了。我一般设置一个置信度阈值,比如0.7,低于这个值的字符标记为“不确定”,然后触发人工复核或者多帧投票。置信度阈值不能设太高,太高了会导致大量正常字符被标记为不确定,增加复核工作量。也不能设太低,太低了起不到过滤作用。我的经验是先用一批测试图像跑一下,看置信度分布,然后取分布曲线谷底附近的值作为阈值。8. 一些踩坑之后的个人体会车牌识别这个方向,入门容易做精难。我刚开始做的时候,觉得Halcon算子这么多,随便组合一下就能出结果。实际做下来才发现,每个算子背后的参数都有讲究,每个场景都需要单独调优。最深的体会是:不要迷信“通用参数”,网上找到的参数配置直接拿来用,大概率效果不好。因为图像来源、光照条件、车牌样式都不一样,必须根据实际情况调。另一个体会是:预处理和定位环节值得花80%的时间。这两个环节做好了,后面的分割和识别就是水到渠成的事。我见过很多人把大量时间花在训练OCR分类器上,但定位不准,分类器再好也没用。最后分享一个小技巧:调试的时候把中间结果都可视化出来。Halcon的dev_display可以显示每个环节的处理结果,定位到的区域、分割出的字符、识别的置信度,都显示出来。这样一眼就能看出哪个环节出了问题,比盲调参数效率高得多。
1. 车牌识别项目的真实需求拆解很多人第一次接触车牌识别,脑子里想的都是“拍张照,把车牌上的字读出来”,觉得这事儿无非就是OCR。但真到项目落地的时候,你会发现客户提的需求远比“读出来”复杂得多。我做过几个不同场景的车牌识别项目,有停车场出入口的,有高速卡口的,也有小区门禁的,每个场景对识别的侧重点完全不同。停车场关心的是识别速度和通行效率,高速卡口关心的是大角度、高速度下的抓拍准确率,小区门禁则更在意夜间和恶劣天气下的稳定性。Halcon在这类项目里的定位很明确:它是一套底层视觉算法库,不是开箱即用的成品软件。这意味着你需要自己搭建从图像采集、预处理、车牌定位、字符分割到字符识别的完整链路。好处是灵活,坏处是每个环节都得自己调。我见过不少同行一开始想用现成的车牌识别SDK,结果发现要么授权费用高,要么对特定车牌样式支持不好,最后还是回到Halcon自己搭。这个项目的核心需求可以归纳为三点:第一,能稳定定位到车牌区域,不管车牌在图像里是倾斜的、有反光的还是被部分遮挡的;第二,能准确分割出单个字符,尤其是中文车牌里汉字和字母数字混排的情况;第三,能识别出字符内容,并且对易混淆字符(比如0和O、1和I、8和B)有足够的区分能力。这三点听起来简单,但每一步都有坑。适合谁来参考这篇内容?如果你已经会用Halcon做基本的图像处理,比如阈值分割、形态学操作、模板匹配,那这篇内容能帮你把零散的知识点串成一条完整的车牌识别流水线。如果你是完全的新手,建议先补一下Halcon的基础算子,不然直接看车牌识别的流程会有点吃力。2. 图像预处理:把“脏图”变成能用的图2.1 为什么预处理决定了后续所有环节的上限车牌识别的第一步不是找车牌,而是把图像质量提上来。我踩过最大的坑就是拿着一堆低质量图像硬做定位,结果参数怎么调都不对。后来才明白,预处理没做好,后面所有环节都是在跟噪声较劲。预处理的目标很明确:增强车牌区域的对比度,抑制背景干扰,把图像变成“车牌突出、背景干净”的状态。Halcon里常用的预处理算子有几个:rgb1_to_gray做灰度化,emphasize做锐化增强,scale_image做对比度拉伸。但具体用哪个、怎么组合,得看你的图像来源。如果是地感线圈触发抓拍的图像,通常光照比较均匀,灰度化之后直接做边缘提取就行。如果是视频流里截取的帧,可能会有运动模糊,这时候得先做shock_filter或者derivate_gauss来增强边缘。2.2 灰度化不是简单取平均很多人以为灰度化就是把RGB三个通道取平均,但Halcon的rgb1_to_gray用的是加权平均,权重接近人眼对亮度的感知。这个细节在车牌识别里很重要,因为车牌底色(蓝底白字、黄底黑字、绿底白字)在不同通道下的对比度差异很大。我实测过,对于蓝底白字的车牌,在蓝色通道下字符和底色的对比度最高,直接提取蓝色通道做后续处理,比灰度化效果更好。* 读取图像 read_image (Image, 'car_plate.jpg') * 分离通道 decompose3 (Image, R, G, B) * 对于蓝底车牌,蓝色通道对比度更好 * 对于黄底车牌,红色通道更合适这个技巧在项目里很实用:先判断车牌类型,再选择对应的通道做处理。判断方法也简单,统计图像里蓝色像素和黄色像素的比例就行。2.3 光照不均的处理策略实际场景里光照不均是最头疼的问题。地下车库出入口,一边是暗的,一边是强光,同一张图里车牌区域可能一半过曝一半欠曝。Halcon里处理光照不均的经典方法是illuminate算子,它通过估计背景光照分布来做校正。但illuminate的参数不好调,掩膜尺寸选大了校正效果不明显,选小了会把车牌字符也当成光照变化给抹掉。我的经验是:先用大掩膜做一次粗校正,再用小掩膜做局部增强。具体参数得根据图像分辨率来定,一般掩膜尺寸取车牌宽度的1.5到2倍比较合适。如果图像分辨率是1920×1080,车牌宽度大概200像素,那掩膜尺寸取300到400之间。注意:预处理阶段不要过度追求“好看”,目标是让后续的定位和分割更稳定。有时候看起来有点噪点的图像,反而比过度平滑的图像更容易定位。3. 车牌定位:从整张图里把车牌“抠”出来3.1 基于颜色特征的定位方法车牌定位最直接的方法是颜色定位。中国车牌的颜色规范很明确:蓝底白字、黄底黑字、绿底白字(新能源)、白底黑字(警用)。Halcon里做颜色定位的流程是:转换到HSV空间,用threshold提取特定色调范围,再用connection和select_shape筛选出候选区域。* 转换到HSV空间 trans_from_rgb (Image, H, S, V, 'hsv') * 提取蓝色区域(蓝底车牌) threshold (H, BlueRegion, 180, 240) * 连通域分析 connection (BlueRegion, ConnectedRegions) * 筛选矩形度高的区域 select_shape (ConnectedRegions, PlateCandidates, 'rectangularity', 0.8, 1.0)这个方法在光照均匀、背景简单的场景下效果很好,但在复杂背景里容易误检。比如蓝色的车身、蓝色的广告牌都会被选进来。所以颜色定位通常只作为粗定位,后面还得用形状特征和纹理特征来验证。3.2 基于边缘和形态学的定位方法边缘定位的思路是:车牌区域有密集的垂直边缘(字符的笔画),用sobel_amp提取边缘后,做水平方向的形态学闭运算,把字符边缘连成一片,再筛选出矩形区域。* 边缘提取 sobel_amp (GrayImage, EdgeAmplitude, 'sum_abs', 3) * 水平方向闭运算,连接字符边缘 gen_rectangle1 (Rect, 0, 0, 1, 20) closing (EdgeAmplitude, RegionClosing, Rect) * 连通域分析 connection (RegionClosing, ConnectedRegions) * 筛选宽高比在2.5到5之间的区域(车牌典型宽高比) select_shape (ConnectedRegions, PlateRegions, 'ratio', 2.5, 5.0)这个方法的优势是不依赖颜色,对褪色车牌、污损车牌也能工作。缺点是计算量大,而且对参数敏感。闭运算的结构元素尺寸很关键:太小了字符边缘连不起来,太大了会把相邻的车牌和背景连在一起。我的经验是结构元素宽度取字符笔画宽度的3到5倍比较合适。3.3 两种方法的融合策略单独用颜色或者边缘都有局限,实际项目里我通常把两种方法结合起来。先用颜色定位快速筛选出候选区域,再用边缘特征验证。验证的方法也简单:在候选区域里统计垂直边缘的密度,车牌区域的边缘密度明显高于背景。还有一种更稳的策略是多尺度定位。先在降采样后的图像上做粗定位,找到大概位置后再在原图上做精定位。这样既能保证速度,又能保证精度。降采样倍数一般取2到4倍,太小了速度提升不明显,太大了会丢失小目标。定位方法优势局限适用场景颜色定位速度快,实现简单受光照和背景颜色影响大光照均匀、背景简单边缘定位不依赖颜色,鲁棒性好计算量大,参数敏感复杂背景、褪色车牌融合定位兼顾速度和鲁棒性实现复杂度高大多数实际项目3.4 定位后的倾斜校正定位到的车牌往往不是水平的,有倾斜角度。倾斜校正的方法有两种:一种是基于smallest_rectangle2获取车牌的倾斜角度,然后用rotate_image旋转校正;另一种是基于投影法,通过统计水平和垂直方向的投影来估计倾斜角。* 获取最小外接矩形 smallest_rectangle2 (PlateRegion, Row, Column, Phi, Length1, Length2) * 旋转校正 rotate_image (Image, ImageRotated, -Phi, 'constant')smallest_rectangle2返回的Phi是弧度值,旋转的时候要注意方向。我踩过的坑是直接拿Phi去旋转,结果角度方向反了,车牌越转越歪。正确的做法是先测试一下,或者用rotate_image的interpolation参数控制插值方式,一般用bilinear就够了。4. 字符分割:把车牌“切”成单个字4.1 投影法分割的原理和参数调优字符分割最常用的方法是垂直投影法。原理很简单:把车牌区域二值化后,统计每一列的白色像素数量,字符所在列的投影值高,字符之间的间隙投影值低。通过找投影曲线的波谷来确定字符的分割位置。* 二值化 threshold (PlateImage, Region, 100, 255) * 垂直投影 gray_projections (Region, Region, 'simple', HorProjection, VertProjection) * 找波谷 * 这里需要对投影数组做后处理,找到连续的零值区间投影法的难点在于波谷的判定。如果车牌有污损或者字符粘连,投影曲线可能没有明显的波谷。我的经验是设置一个动态阈值:先计算投影值的平均值,然后取平均值的0.2到0.3倍作为波谷判定阈值。这样比固定阈值更适应不同的图像质量。4.2 处理粘连字符和断裂字符粘连字符是分割里最麻烦的情况。两个字符粘在一起,投影曲线上没有波谷,直接分割会切错。处理方法有两种:一种是基于字符宽度的先验知识,如果某个区域的宽度明显大于单个字符的平均宽度,就强制从中间切开;另一种是基于连通域分析,用connection把字符区域分开,再根据面积和宽高比筛选。断裂字符相对好处理,通常是字符笔画太细或者二值化阈值太高导致的。解决方法是用形态学闭运算把断裂的部分连起来,或者降低二值化阈值。但阈值降太低会把背景噪声也带进来,所以得权衡。* 形态学闭运算连接断裂字符 gen_rectangle1 (Rect, 0, 0, 1, 3) closing (CharRegion, CharRegionClosed, Rect) * 连通域分析 connection (CharRegionClosed, CharRegions) * 按面积筛选 select_shape (CharRegions, ValidChars, 'area', 50, 5000)4.3 中文车牌的字符分割特殊性中文车牌的第一个字符是汉字,后面是字母和数字。汉字的笔画比字母数字复杂得多,投影曲线也更不规则。如果统一用一套参数分割,汉字部分很容易切错。我的做法是分两段处理:先分割出第一个汉字,剩下的部分再按字母数字的规则分割。汉字分割的关键是确定汉字的宽度范围。中国车牌的汉字宽度一般在车牌总宽度的1/7到1/6之间,可以根据这个先验知识来约束分割位置。另外,汉字区域的投影曲线通常比字母数字区域更“毛糙”,可以用sigma滤波先平滑一下再找波谷。提示:字符分割的准确率直接影响最终识别率。如果分割阶段切错了,后面的识别再准也没用。所以在这个环节多花时间调参数是值得的。5. 字符识别:从“看到”到“认出”5.1 基于OCR分类器的识别流程Halcon提供了read_ocr和read_ocr_class_mlp等算子来做字符识别。基本流程是:先加载预训练的分类器,然后把分割好的字符区域传给分类器,得到识别结果。* 加载OCR分类器 read_ocr_class_mlp ('Industrial_0-9A-Z_NoRej.omc', OCRHandle) * 识别字符 do_ocr_multi_class_mlp (CharRegions, PlateImage, OCRHandle, Class, Confidence) * 清除句柄 clear_ocr_class_mlp (OCRHandle)Halcon自带的分类器有几种:Industrial_0-9A-Z适合工业场景的字母数字识别,Document_0-9A-Z适合文档扫描,Universal_0-9A-Z通用性更强但精度稍低。对于车牌识别,我一般用Industrial系列的,因为车牌字符的字体比较规范,工业分类器训练时用的样本更接近。5.2 训练自己的OCR分类器Halcon自带的分类器对中国车牌的汉字支持不好,因为汉字不在它的训练集里。所以实际项目里通常需要自己训练分类器。训练流程是:收集大量车牌字符样本,标注好每个样本对应的字符,然后用train_ocr_class_mlp训练。* 创建OCR分类器 create_ocr_class_mlp (8, 10, 'constant', 'default', ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q','R','S','T','U','V','W','X','Y','Z','京','沪','粤','苏','浙','鲁','川','渝','冀','豫','云','辽','黑','湘','皖','闽','赣','鄂','桂','甘','晋','蒙','陕','吉','贵','青','藏','新','宁','琼'], 0.01, 10, 'normalization', 10, 42, OCRHandle) * 训练 train_ocr_class_mlp (OCRHandle, TrainingFile)训练样本的质量比数量更重要。我试过用5000个样本训练,但样本里有很多模糊的、倾斜的,结果识别率还不如用2000个清晰样本训练出来的。样本采集的时候要注意覆盖不同光照条件、不同拍摄角度、不同车牌磨损程度的情况。5.3 易混淆字符的后处理即使分类器训练得很好,0和O、1和I、8和B这些字符还是容易混淆。后处理的思路是利用车牌字符的排列规则来纠正。中国车牌的格式是:汉字+字母+字母数字混合(5位)。第一位一定是汉字,第二位一定是字母,后面五位是字母数字混合但字母不能出现在某些位置。* 后处理规则示例 * 第二位如果是数字0,根据规则应该是字母O * 第五位如果是字母I,根据规则应该是数字1这个规则表可以根据实际车牌规范来定。我一般会维护一个“位置-允许字符集”的映射表,识别结果如果不在允许字符集里,就找最接近的允许字符替换。替换的依据是分类器返回的置信度,选置信度最高的那个。易混淆对区分方法后处理策略0和O位置规则第二位只允许字母,第五位只允许数字1和I位置规则同上8和B位置规则同上2和Z位置规则同上5和S位置规则同上6. 完整流程串联与参数调优实战6.1 从读图到输出的完整代码框架把前面几个环节串起来,一个完整的车牌识别流程大概长这样:* 1. 读取图像 read_image (Image, 'car_plate.jpg') * 2. 预处理 rgb1_to_gray (Image, GrayImage) emphasize (GrayImage, ImageEmphasize, 7, 7, 1.0) * 3. 车牌定位 sobel_amp (ImageEmphasize, EdgeAmplitude, 'sum_abs', 3) gen_rectangle1 (Rect, 0, 0, 1, 20) closing (EdgeAmplitude, RegionClosing, Rect) connection (RegionClosing, ConnectedRegions) select_shape (ConnectedRegions, PlateRegions, 'ratio', 2.5, 5.0) * 4. 倾斜校正 smallest_rectangle2 (PlateRegions, Row, Column, Phi, Length1, Length2) rotate_image (Image, ImageRotated, -Phi, 'bilinear') * 5. 字符分割 threshold (ImageRotated, Region, 100, 255) gray_projections (Region, Region, 'simple', HorProjection, VertProjection) * 根据投影曲线分割字符 * 6. 字符识别 read_ocr_class_mlp ('Industrial_0-9A-Z_NoRej.omc', OCRHandle) do_ocr_multi_class_mlp (CharRegions, ImageRotated, OCRHandle, Class, Confidence) * 7. 后处理 * 根据位置规则纠正易混淆字符这个框架是基础版,实际项目里每个环节都需要根据具体场景调整参数。比如emphasize的掩膜尺寸,图像分辨率高的时候要调大,分辨率低的时候要调小。sobel_amp的滤波核大小也是,一般取3到5之间,太大了边缘会太粗,太小了边缘会断裂。6.2 参数调优的优先级和顺序调参不能瞎调,得有优先级。我的经验是:先调定位参数,定位准了再调分割参数,分割准了最后调识别参数。因为定位错了,后面调得再好也没用。定位参数里最重要的是边缘提取的阈值和形态学结构元素的尺寸。边缘阈值太低会引入大量噪声,太高会丢失弱边缘。我一般先用edges_image算子看一下边缘强度的分布,然后取分布曲线的拐点作为阈值。形态学结构元素的尺寸前面说过,取字符笔画宽度的3到5倍。分割参数里最重要的是二值化阈值和投影波谷的判定阈值。二值化阈值可以用binary_threshold自动计算,也可以用gray_histo看直方图手动选。投影波谷的判定阈值我一般取投影平均值的0.25倍,这个值在大多数场景下都能工作。6.3 实测中的意外情况和处理实测中最常见的意外是车牌定位到了但位置偏了,导致分割的时候把边框也切进去了。处理方法是定位之后用reduce_domain把车牌区域抠出来,再做一次精定位,把边框去掉。精定位的方法可以用boundary提取区域边界,然后向内收缩几个像素。另一个意外是图像里有多辆车牌,定位到了多个候选区域。这时候需要根据区域的位置和大小来筛选。一般离图像中心近的、面积大的候选区域是目标车牌。如果还是分不清,可以结合车辆检测的结果来辅助判断。还有一种情况是车牌被部分遮挡,比如被保险杠挡住了一部分。这种情况下定位可能只能定位到车牌的一部分,分割出来的字符数量不对。处理方法是根据车牌的标准宽高比来推断完整车牌的位置,然后从原图里重新抠取。注意:实际项目里不要追求100%的识别率,能达到95%以上就很好了。剩下的5%可以通过多帧投票或者人工复核来解决。我见过有人为了最后几个百分点的识别率调了一个月参数,投入产出比太低了。7. 性能优化与工程化落地7.1 算法加速的几种手段Halcon的算法在CPU上跑,速度是个瓶颈。一张1080P的图像,完整流程跑下来可能要200到500毫秒。如果要做实时识别,比如视频流每秒25帧,那必须优化。优化的手段有几个:第一,降低处理分辨率。把图像降采样到原来的1/2或1/4,处理速度能提升4到16倍。车牌识别对分辨率的要求没那么高,车牌宽度有100到150像素就足够识别了。第二,缩小处理区域。如果相机位置固定,车牌只会出现在图像的某个区域,那就只处理那个区域,其他部分不处理。第三,用Halcon的并行计算功能。parallelize_operators可以把一些算子分配到多个线程上跑。* 降采样 zoom_image_factor (Image, ImageZoomed, 0.5, 0.5, 'bilinear') * 处理完后再把结果映射回原图坐标7.2 多帧融合提升稳定性视频流场景下,单帧识别可能会出错,但多帧融合能大幅提升稳定性。思路是:对连续多帧的识别结果做投票,出现次数最多的结果作为最终输出。投票的窗口大小一般取5到10帧,太小了起不到融合效果,太大了响应延迟高。还有一种融合策略是利用车牌的物理尺寸约束。同一辆车在连续帧里,车牌的像素宽度变化应该是连续的。如果某一帧识别到的车牌宽度突然跳变,那这一帧的结果大概率是错的,可以丢弃。7.3 工程化部署的注意事项工程化部署的时候,有几个坑我踩过。第一,Halcon的运行时授权问题。开发的时候用的是开发版授权,部署到客户机器上需要运行时授权。这个授权是绑定硬件的,换机器就得重新申请。第二,不同版本的Halcon算子行为可能有差异。我在Halcon 18上调好的参数,换到Halcon 20上跑,结果就不一样了。所以部署的时候要锁定版本。第三,图像采集的接口兼容性。不同品牌的相机SDK不一样,Halcon的open_framegrabber支持的相机品牌有限,不支持的得自己写接口。部署问题表现解决方案授权绑定换机器后无法运行提前申请运行时授权版本差异参数行为不一致锁定Halcon版本相机兼容无法采集图像用相机SDK采集后转Halcon图像7.4 识别结果的置信度过滤Halcon的OCR分类器会返回每个字符的置信度。置信度低的字符大概率是识别错了。我一般设置一个置信度阈值,比如0.7,低于这个值的字符标记为“不确定”,然后触发人工复核或者多帧投票。置信度阈值不能设太高,太高了会导致大量正常字符被标记为不确定,增加复核工作量。也不能设太低,太低了起不到过滤作用。我的经验是先用一批测试图像跑一下,看置信度分布,然后取分布曲线谷底附近的值作为阈值。8. 一些踩坑之后的个人体会车牌识别这个方向,入门容易做精难。我刚开始做的时候,觉得Halcon算子这么多,随便组合一下就能出结果。实际做下来才发现,每个算子背后的参数都有讲究,每个场景都需要单独调优。最深的体会是:不要迷信“通用参数”,网上找到的参数配置直接拿来用,大概率效果不好。因为图像来源、光照条件、车牌样式都不一样,必须根据实际情况调。另一个体会是:预处理和定位环节值得花80%的时间。这两个环节做好了,后面的分割和识别就是水到渠成的事。我见过很多人把大量时间花在训练OCR分类器上,但定位不准,分类器再好也没用。最后分享一个小技巧:调试的时候把中间结果都可视化出来。Halcon的dev_display可以显示每个环节的处理结果,定位到的区域、分割出的字符、识别的置信度,都显示出来。这样一眼就能看出哪个环节出了问题,比盲调参数效率高得多。