1. 先别急着写算子任务复杂度与HALCON选型逻辑1.1 “简易车牌识别”到底简易在哪难点又在哪先说结论车牌识别这个题目看起来是一个入门级机器视觉Demo做起来也是一条固定的流水线——找车牌、切字符、逐个识别。但如果你真在停车场道闸或者园区出入口实地架过相机就会发现简单的是流程不简单的是“如何让这条流程在各种光照、角度、遮挡下都能稳定跑通”。我做个通俗类比识别车牌有点像让一个不熟悉当地路况的司机在晚高峰找一条巷子导航告诉他“前方右转”很简单难的是他在各种雨雾天气、前后车流里还能准确判断右转时机。算法也是一样定位、分割、识别三个环节各自独立时都不难难在环节之间的误差累积——前面定位偏了2个像素后面分割可能就切歪一个字符分割切歪了识别置信度就掉下来。所以做这个项目真正要花心思的不是某个算子的参数而是整条链路的容错设计。简易在哪第一车牌本身是标准化物体普通蓝底白字车牌的宽高比固定在440:140左右字符排列规则、数量基本确定省份简称字母数字字符集是有限的这比做通用文字识别简单得多。第二车牌和车身背景之间通常有明显颜色差异对比度足够给二值化留下了很大的操作空间。难点在哪光照不均、夜间大灯直射、车牌反光、倾斜角度过大、运动模糊、车牌上的灰尘和泥点、铆钉遮挡字符边缘、新能源车牌的8位字符布局……这些情况组合出现时传统图像处理方法每一个环节都可能翻车。所以“简易”二字的准确含义应该是流程标准、算子成熟但想做到稳定可用需要在每个环节做足容错处理。1.2 为什么我会选HALCON而不是自己撸OpenCV做这个项目之前我确实纠结过方案。OpenCV免费开源社区资料多网上能找到一大堆现成车牌识别代码HALCON则是商业机器视觉库要花钱买许可。但综合考量之后我还是选了HALCON核心原因有三第一算子成熟度高开发周期短。HALCON的形态学、连通域分析、OCR分类器都是封装好的工业级算子比如select_shape一个算子就能按面积、宽高比、矩形度多个条件同时筛选区域segment_characters专门做字符分割内部已经处理了字距、粘连等常见问题。用OpenCV的话同样的功能要自己组合一大堆函数还得自己调阈值和边界条件。第二HDevelop交互式调试体验是真正的生产力。HALCON自带的HDevelop开发环境里每一步算子执行后都能立刻看到图像、区域、参数的实时变化你可以一边看结果一边调参。这种“所见即所得”的调试方式在定位不准、二值化不干净这类问题上省的时间非常可观。我后来在C#里封装算法时也是先在HDevelop里把整条链路调通了再导出代码。第三OCR模块开箱即用。HALCON自带的OCR分类器支持训练和识别而且支持中文汉字字符集。车牌上的省份简称各地汉字不属于英文字符集必须要自己训练HALCON的create_ocr_class_mlp、write_ocr_class_mlp这套训练接口做这个事非常顺手。如果你所在团队预算有限、且团队OpenCV功力很深那自研方案完全可行但如果追求项目周期可控、算法稳定性可预期HALCON这条路对中小视觉项目来说性价比很高。1.3 动工之前必须先确认的三个前置条件写第一行代码前我先花了半天确认三件事这三件事直接决定了后面算法难不难写相机安装位置与车牌成像尺寸。车牌在画面里的宽度最好不低于120像素。低于这个数值字符边缘的像素信息太少分割和识别的难度会指数级上升。我之前测试过车牌宽度80像素时识别率大概只有92%但拉到160像素后能稳定在98%以上。所以如果现场条件允许先把相机俯仰角和焦距调好让车牌在视野中间且占幅面宽度的1/3以上再开始写算法。目标场景的光照与帧率。白天室外顺光、逆光、夜间补光三种工况下的图像特征差异很大。逆光时车牌区域整体偏暗字符对比度下降夜间如果补光灯直射车牌可能出现高光溢出。建议先去现场采集50张以上不同时段的真实图像作为后续调参和验证的固定测试集。字符集范围。国内车牌涉及汉字省份简称、大写字母、数字新能源牌还有8位字符。你需要确认自己的识别范围是只覆盖普通蓝牌还是也要兼顾黄牌、白牌、新能源绿牌。字符集越宽训练成本越高千万别等代码写完了才想起来要加字符类型。2. 车牌定位形态学筛选和倾斜校正的完整链路2.1 预处理为什么我先转灰度而不是直接找蓝色很多首版方案喜欢直接用彩色图像分割蓝色区域来定位车牌思路是“车牌是蓝色的我按颜色抠出来就行”。这个思路在光线均匀时确实有效但到了逆光、夜间、阴雨天蓝色分量变化极大拿固定RGB范围去卡要么漏检要么候选区域爆炸。所以我选择先转灰度再做阈值分割把问题简化成“亮暗对比”问题。HALCON里一行算子搞定rgb1_to_gray (Image, GrayImage) scale_image_max (GrayImage, ScaledImage)scale_image_max会把灰度范围拉伸到0到255的满动态范围这一步对偏暗的逆光图像很有帮助它相当于自动做了一个全局对比度增强。注意它只对灰度分布比较均匀的图像效果好如果画面里同时存在极亮天空和极暗地面全局拉伸反而会把中间层次压缩掉这种情况我会改用equ_histo_image做直方图均衡化。调试时可以两个都跑一遍看哪个对当前场景更稳。2.2 候选区域筛选用矩形特征把“长得像车牌”的区域选出来灰度图准备好之后第一步二值化。HALCON里threshold算子是直接按灰度区间切也可以用binary_threshold让它根据灰度直方图自动选阈值。我个人更推荐先用binary_threshold的“smooth_histo”模式自动跑一遍看分割效果再决定要不要手调阈值。手动调阈值的风险是换一批图像、换一个时刻的光照阈值可能就不适用了。二值化之后会得到一堆白色区域其中既有车牌也有保险杠边缘、车灯反光、车身贴纸等杂七杂八的东西。接下来就是用形状特征把它们过滤掉。车牌最显著的特征是宽高比接近3.0到3.5、面积适中、形状接近矩形。我用的是这样一组参数threshold (GrayImage, Regions, 0, 120) connection (Regions, ConnectedRegions) select_shape (ConnectedRegions, PlateCandidates, [area, width, height, rectangularity], and, [3000, 200, 60, 0.7], [60000, 600, 180, 1.0])四个筛选条件分别解释area排除面积过小或过大的噪声碎片width保证区域横向有足够的宽度height排除太高但不是车牌的条状物rectangularity是区域逼近矩形的程度值越接近1越像矩形。这里的面积数值是相对的取决于相机分辨率和你车牌在画面中的实际大小拿到现场图像后先跑一版统计一下候选区域的分布再回调即可。如果多辆车同时出现在画面里这一步可能选出多个候选区域没关系后面可以对每个候选区逐个做分割和识别再根据识别置信度挑分数最高的。2.3 倾斜校正用最小外接矩形解决拍照角度问题车牌在画面里不可能是永远端端正正的。道闸相机安装高度通常在1.5米左右车辆驶过时车牌处于画面下方天然存在透视变形车辆停歪了还会引入水平倾斜。倾斜不校正直接切字符字符区域是歪的分割和识别都会出问题。我的做法是两步走第一步用shape_trans把候选区域转成带角度信息的最小外接矩形第二步用旋转矩阵把图像摆正。shape_trans (PlateCandidates, PlateRect, rectangle2) area_center (PlateRect, Area, Row, Column) tuple_rad (Angle, RadAngle) hom_mat2d_rotate (HomMat2D, -RadAngle, Row, Column) affine_trans_image (Image, RotatedImage, HomMat2D, constant, false)rectangle2模式返回矩形的中心坐标、长边长度和倾斜角Angle注意HALCON返回的角度单位是弧度。旋转时方向取反把它转回水平。affine_trans_image的插值方式用constant质量足够且速度快。透视变形比纯倾斜更麻烦因为它是一侧大一侧小的梯形畸变单靠旋转无法完全校正。轻度的透视可以在shape_trans之后用hom_vector_to_proj_hom_mat2d做一次四点仿射校正如果现场车辆停车位置比较固定、角度可控这个步骤可以先省掉但算法要预留接口等现场测试发现问题再补上。3. 字符分割把干扰物从字符之间赶出去3.1 先清理车牌区域再谈分割定位到车牌区域后不能直接拿原图去切字符必须先做一次区域裁剪和二次二值化。HALCON里的套路是用reduce_domain把图像域限定在车牌矩形内然后再对这个小区域做二值化。reduce_domain (RotatedImage, PlateRect, ImageReduced) threshold (ImageReduced, CharactersRegion, 0, 100)这一轮二值化的阈值和第一轮不同第一轮是为了“找车牌”标准要松一点宁可多选几个候选区这轮是为了“切字符”标准要准目标是让字符区域尽量干净。蓝底白字车牌里字符是亮的背景是暗的所以取灰度值高的区间就能把字符挑出来。如果二值化之后字符有断裂比如字符笔画中间有一截没被选中不要急着改阈值先试试fill_up或closing_circle一类形态学操作把断口补上。一个常见经验是断裂字符的修复优先级高于粘连字符的拆分因为断裂会让后续识别器看到的字符形状不完整置信度大幅下降。3.2 铆钉和“车牌扣”这类干扰怎么处理车牌上除了字符还有固定螺钉、铆钉、车牌边框以及一些车型自带的车牌装饰框。这些东西进入二值化结果后会变成字符周围的杂散区域直接影响分割。我的经验是分层处理先利用字符的几何规律把明显不可能是字符的东西丢掉。车牌的字符区域集中在矩形中间偏下的位置上下两端各有约20到30像素的空白区用于容纳边框和铆钉。所以拿到二值化区域后先按字符高度约束筛掉太矮或太高的区域再对每个连通域计算外接矩形中心把中心点落在车牌上部或下部的区域整体剔除。connection (CharactersRegion, ConnectedChars) select_shape (ConnectedChars, CleanChars, [height, width], and, [30, 15], [120, 80])这里的height范围对应的是单个字符的像素高度width范围要能容纳数字字母但又能排除横向长条状的边框区域。铆钉的直径往往小于字符高度会被height下限过滤掉车牌上下边框是横向长条会被width上限卡掉。3.3 粘连与断裂字符的分割策略单纯用connection分离连通域遇到两个字符之间的笔画粘连最常见的是“1”和后续数字之间、或者“B”两侧笔画过宽就会把两个字符连成一个区域后续识别就会失败。HALCON提供了一个专门的字符分割算子segment_characters它基于字符的形态结构做切分对标准打印字体的效果很不错segment_characters (ImageReduced, ImageInverted, CleanChars, Characters)这个算子的内部逻辑是自动评估字符区域间的间隔按设定的字宽比例切分。实际使用中要注意它更适合规则排列的字符如果车牌本身倾斜校正做得不好、或者字符区域被边框干扰污染segment_characters也会误切成几段。如果现场字符间距比较规则我更推荐一个组合拳先把候选字符区域按从左到右排序sort_region然后算出平均字符宽度再对每个区域宽度明显超出平均宽度的连通域做“按宽度等分”处理。这个思路简单粗暴但也有个致命弱点一旦遇到“1”这种窄字符会让平均宽度被拉低导致原本正常的字符被误切。所以我定了两个原则宁可少切不要乱切先处理断裂再处理粘连。少切一次的处理成本远低于误切导致的整牌识别错误。4. 识别环节字体库训练与运行时参数取舍4.1 先定路线用自带字体还是自己训练分割出单个字符图像后识别环节有两条路线用HALCON自带字体库直接识别还是自己训练字体库。HALCON自带OCR字体库里有工业标准字符集对数字和大写字母的识别效果不错但无法覆盖中文汉字。车牌里最前面的省份简称是汉字比如常见的那几十个省份汉字所以想要识别完整车牌必须自己训练中文字体。训练流程本身不复杂但需要耐心准备样本准备训练图像每张图包含清晰的单个字符或者整张车牌图也行因为HDevelop里可以框选单个字符区域。用write_ocr_trainf把字符区域和对应的字符标签写入训练文件。标签就是该字符本身比如“京”“A”“3”。调用create_ocr_class_mlp创建分类器字符集大小要覆盖训练样本中出现的字符种类。用train_ocr_class_mlp训练最后write_ocr_class_mlp保存成.omc文件。训练集数量上每个字符我建议至少准备100个样本中文省份汉字要尤其多。样本来源可以是在现场采集的车牌图里切出来的真实字符也可以先用HALCON自带字体生成一批模拟字符作为底料再用真实样本做增量训练。只靠自带字体生成的数据训练出来的模型在真实照片上的泛化能力会偏差因为真实图像有光照变化、边缘噪声、油漆磨损这些是字体渲染做不出来的。4.2 运行时识别参数别只盯识别率还要看置信度训练好.omc文件后运行时识别用read_ocr_class_mlp加载再对每个字符区域逐个调用do_ocr_multi_class_mlpread_ocr_class_mlp (OcrHandle, plate_ocr.omc) do_ocr_multi_class_mlp (Characters, ImageReduced, OcrHandle, RecognizedClasses, Confidence)do_ocr_multi_class_mlp返回的RecognizedClasses是识别出的字符数组Confidence是对应的置信度数组。很多人只关心识别结果但我建议把置信度一并拿来做后续过滤。置信度的使用场景有两个第一个场景低置信度字符直接判定为“识别可疑”。当某个字符的置信度低于0.6时它大概率是分割错误或者字符残缺硬给一个识别结果会拉低整牌准确率。宁可返回一个带问号的字符串让上层逻辑去处理也不要给出一个可能错误的结果。第二个场景多候选板式识别时用来决策。如果一次识别出多个车牌候选区域每个区域识别完会得到一组字符和平均置信度排序取平均置信度最高的那个作为最终结果这是最简单也最有效的多候选仲裁策略。字体训练的一个细节HALCON的MLP分类器对输入字符图像分辨率有要求太小比如高度不足20像素的字符训练效果很差。分割后如果字符区域偏小先做一次zoom_image_factor把字符放大到合适尺寸再送进分类器。这个预处理对识别率的提升非常明显。5. 一套可直接改用的Demo流程与三个实测翻车点5.1 主流程脚本走读把前面几节的环节串起来一个完整的HDevelop主流程大概是这样的* 第1步读取图像 read_image (Image, license_plate_sample.png) * 第2步转灰度 对比度拉伸 rgb1_to_gray (Image, GrayImage) scale_image_max (GrayImage, ScaledImage) * 第3步车牌区域定位 threshold (ScaledImage, Regions, 0, 120) connection (Regions, ConnectedRegions) select_shape (ConnectedRegions, PlateCandidates, [area, width, height, rectangularity], and, [3000, 200, 60, 0.7], [60000, 600, 180, 1.0]) * 第4步对每个候选车牌做倾斜校正 count_obj (PlateCandidates, NumCandidates) for i : 1 to NumCandidates by 1 select_obj (PlateCandidates, PlateCandidate, i) shape_trans (PlateCandidate, PlateRect, rectangle2) area_center (PlateRect, Area, Row, Column) tuple_rad (0.0, Angle) * 这里用PlateRect返回的Angle替换 hom_mat2d_rotate (HomMat2D, -Angle, Row, Column) affine_trans_image (Image, RotatedImage, HomMat2D, constant, false) * 第5步裁剪车牌区域并做字符分割 reduce_domain (RotatedImage, PlateRect, ImageReduced) threshold (ImageReduced, CharactersRegion, 0, 100) connection (CharactersRegion, ConnectedChars) select_shape (ConnectedChars, CleanChars, [height, width], and, [30, 15], [120, 80]) segment_characters (ImageReduced, ImageReduced, CleanChars, Characters) * 第6步加载OCR模型并逐个识别 read_ocr_class_mlp (OcrHandle, plate_ocr.omc) do_ocr_multi_class_mlp (Characters, ImageReduced, OcrHandle, PlateText, Confidence) endfor注意上面tuple_rad那行是示意实际应该直接使用shape_trans返回的角度变量。整套流程在HDevelop里跑通之后再导出成C#或C代码封装为DLL给业务系统调用这样调试和集成分开效率最高。5.2 翻车点一固定螺钉把字符边框切断了第一次拿真实停车场图像测试时我遇到一个很隐蔽的问题车牌的固定螺钉正好位于第二和第三字符之间二值化后螺钉区域和字符区域连成一片segment_characters直接把一个螺母当成字符切了出来导致识别结果多了一位。排查的时候我先把分割结果可视化发现那个“假字符”的矩形区域明显比正常字符矮而且位置偏低。解决办法就是前面说的几何筛选——在segment_characters之前先用高度和宽度筛选把非字符区域尽可能清掉并且把检测区域从整个车牌矩形缩到字符集中分布的核心区域避开上下边缘的干扰。5.3 翻车点二车标纹理被误选成车牌候选在有些车型上车头中网的横条纹理在二值化之后形状和车牌非常像——宽度接近、高度接近、矩形度也高。第一版选区只靠形状特征识别时经常把中网区域当成车牌切然后送进OCR一顿乱识别最后返回一个莫名其妙的字符串。后来我加了一道保险对候选区域做颜色验证。车牌候选区域如果是普通蓝牌区域内的主色调应当是蓝色。用decompose3把彩色图拆成RGB三个通道计算候选区域内蓝色通道和红色通道的均值差如果蓝红均值差小于某个阈值就判定“不是蓝牌”直接剔除。这一招能把中网、保险杠、黑色车身贴纸这类干扰一次性滤掉大部分。5.4 翻车点三阴天加逆光全局阈值彻底失效有一次测试时正好赶上阴天加逆光整个车牌区域灰蒙蒙的字符和背景的灰度差被压缩到了不到30个灰度级。全局阈值怎么调都无解——阈值调低了背景噪声全进来调高了字符跟着消失。最后换成了局部阈值方案用mean_image对灰度图做一个平滑作为背景估计然后用dyn_threshold把原始图和背景图做差分这样阈值就是逐像素自适应的能处理光照不均的场景。mean_image (ScaledImage, MeanImage, 21, 21) dyn_threshold (ScaledImage, MeanImage, DynRegions, 8, dark)这里dark表示提取比背景暗的区域正好对应蓝底白字车牌里“字符亮、背景暗”的反转情况如果你的预处理把字符变成了暗色就改成light。均值滤波核大小我用了21这个值要和字符笔画宽度匹配核太小会跟着字符走核太大则失去局部性。6. 识别率怎么才算“能用”评估方法与工程化建议6.1 分环节评估不要只盯“整牌识别率”很多项目组汇报车牌识别效果时只给一个数字“整牌识别率97%”。这个数字当然重要但它掩盖了问题出在哪个环节。我习惯把评估拆成三档车牌定位成功率在所有测试图里车牌区域是否被正确找到并裁剪出来。这个指标低的话后面一切免谈。字符分割正确率在定位成功的图像里字符是否被完整、无损地切成单个字符。分割错误包括多切、漏切、切到干扰物。整牌识别准确率分割正确的图像里OCR结果是否和真实车牌完全一致。分环境统计也很有必要。我每次都会把测试集按白天顺光、白天逆光、夜间补光、雨天、倾斜大角度等工况分开统计目的是找出当前算法最薄弱的场景。比如某一次夜间识别率只有88%一查发现根本不是识别问题而是夜间补光灯角度不对导致车牌高光溢出调整补光灯位置后识别率直接跳到97%。6.2 从“能跑”到“好用”工程化还需要补几刀一个能在HDevelop里跑通的Demo离能部署到现场还有几步路这里说些实在的多帧投票是成本最低的准确率提升方案。道闸场景下车辆从驶入到停稳会有连续数帧图像每帧单独识别最后按字符级投票取频率最高的结果。哪怕单帧识别率只有95%三帧投票后整牌错误率能降到单帧的接近十分之一而且实现成本很低。模型的覆盖范围要提前规划。只做蓝牌的车牌识别相对简单但新能源车牌是8位字符绿底黑字黄牌是单行车牌且字符布局有差异白牌常见于特种车辆。如果你的系统要兼容多种车牌类型建议在第一版设计时就把字符集和分割逻辑做成可配置不要等上线了再推翻重来。置信度要和业务逻辑联动。识别置信度低的字符不要硬给结果前端可以提示“请重新识别”或者触发第二次抓拍。对道闸场景来说一次正确的延迟识别远比一次错误的快速放行更稳妥。就我个人的经验来看HALCON车牌识别项目最容易栽跟头的地方往往不在算法本身而在前期现场实测样本不够和对“简易”二字的误判。拿几十张真实图片、把定位、分割、识别三个环节分别调稳这个项目的成功率就八九不离十了。最后分享一个小技巧训练OCR字体的时候我习惯把收集到的真实字符样本和用write_ocr_trainf存下来的训练缓存分开管理每加一批现场图像就增量训练一次。这样做的好处是模型会随着项目运行逐渐吸收现场的光照、角度分布特征识别率越用越稳。车牌识别这东西没有一劳永逸的模型只有不断喂数据才能持续保持高水位。