
从传统图像处理到深度学习:车牌识别系统演进实践
1. 项目背景简介
这几年智能停车场越来越多,很多商场和写字楼都在把原来那套人工登记、人工收费的系统换成自动识别的。我们当时也正好接到一个需求,要做一个支持车牌自动识别 + 自动计费 + 自动放行的系统,用来提升停车场的出入口通行效率。
其实一开始要求还挺高的:识别准确率要能稳定在 95% 以上,识别+计费流程整体耗时不能超过 1.5 秒,而且最好支持多种车牌样式——蓝的黄的新能源的全都得识别,还得能部署在本地边缘设备上,不能太吃资源。
当时想着,先用一些传统图像处理手段快速搭一套试试看,成本低、见效快。于是我们就先搞了一版基于 OpenCV 的实现。
2. 初版方案:传统图像处理路线
我们最早的一版系统,其实没有用任何深度学习,就是靠图像处理+逻辑判断“硬撸”出来的。虽然精度不高,但原型很快就能跑起来。
2.1 滑窗 + OpenCV 做车牌区域检测
我们用 OpenCV 把图像先做一波预处理,主要流程是灰度化 → 高斯滤波 → Canny 边缘检测,然后就开始滑窗遍历整张图,去找那些长宽比像车牌的矩形区域。
为了过滤掉噪声,我们还加了一些颜色和形状上的判断,比如常见的蓝底白字车牌,在 HSV 色彩空间里很容易就能分出来。靠这些规则,基本能把车牌位置框出来,虽然偶尔也会误框一些广告牌或者后车灯。
2.2 字符分割 + 模板匹配做识别
框出来之后,我们用图像切割把车牌字符一个个分出来,然后做模板匹配。简单说就是和一堆已有的字母、数字图片做对比,看哪个最像,就认成哪个。
说实话,这一步就是纯靠像素比对,样式稍微变一点它就认不出来了。比如字体粗细变了、字歪了、背景有点反光,全都容易识错。
2.3 实际遇到的问题
我们内部测试的时候觉得还凑合,但真上线了就开始掉链子——
• 下雨天车牌有水渍,识别率暴跌;
• 晚上光线暗,边缘提不出来;
• 有些车歪着进场,角度一偏字符就分不准了;
• 模板匹配对字形太敏感,新版新能源车牌基本识别失败。
还有个致命问题是速度太慢了。滑窗处理一整张图耗时很久,尤其分辨率一高,CPU 根本跑不过来。那时候我们就知道,再优化也有限,得换思路了——考虑上深度学习。
3. 升级阶段一:引入 CTPN 做字符区域检测
传统方法在定位车牌区域这一步已经吃力了,但更麻烦的其实是字符切割。一旦车牌有点歪,或者字符之间间距不一致,传统的按列投影法基本就挂了。我们团队那时候几乎一半时间都花在调字符分割上,怎么调都不稳定。
所以后来我们决定直接上深度学习,第一步就是换掉字符定位的部分,用一个更鲁棒的检测模型:CTPN(Connectionist Text Proposal Network)。
3.1 为什么选 CTPN?
那时候还没有像现在这么多轻量级的 OCR 模型。CTPN 虽然不是最新的,但它是专门做自然场景中的文本检测的,特别适合处理那些角度稍歪、背景有干扰的文字区域。而且它的原理比较有意思:不是把整行字符当成一个框,而是把字符行“切”成一段段的小 proposal,然后再把它们串起来,最终框出整段文本。
这种“横向连通”的思想,恰好适合车牌这种一行排开的字符。
3.2 模型使用流程
我们当时复用了别人开源的 CTPN TensorFlow 实现,拿过来以后先在公开数据集上验证能跑起来,再用我们自己采集的车牌图像进行 finetune(微调)。
训练过程倒没太多坑,就是数据标注有点费劲——每张图都要标出字符区域的小框,后来我们写了个半自动工具,先用传统边缘检测预选,再人工微调,大大提高了效率。
训练好之后模型效果立竿见影:
• 哪怕车牌轻微倾斜,也能正确框出整行字符;
• 遇到遮挡或者局部模糊的车牌,也能保证字符区域尽量完整;
• 最关键的是,字符行出来了之后就不用再手动分割字符了!
这对我们后面的字符识别环节简直是质的飞跃。
3.3 性能表现
CTPN 本身不算轻量,尤其是在当年的设备环境下(主要是边缘侧部署),我们做了以下几步来优化:
• 裁剪输入图像,只对车牌区域附近做检测(前面用传统方式做一次粗定位);
• 减小模型输入尺寸,换成更小的 anchor 尺寸;
• 模型量化 + TensorRT 优化一波(这个后来才做,效果不错);
优化之后,CTPN 模块的平均耗时控制在 300~500ms 左右,精度也远比传统方式高不少。
4. 升级阶段二:CRNN + CTC 字符识别方案
字符区域搞定之后,接下来就是让系统“读”出字符了。我们最开始也试过一些轻量级的字符分类网络,比如把每个字符框提出来后单独做分类(Softmax + CNN),但发现效果也不稳定:字符框切得不准就识别错误,而且这种方式对字符的前后顺序毫无理解能力。
所以最终我们上了比较经典的一套组合:CRNN + CTC。
4.1 CRNN 是啥?
简单说,CRNN(Convolutional Recurrent Neural Network)是个三段式结构:
-
CNN 卷积层:提取图像特征;
-
RNN 结构(我们用的是双向 LSTM):处理特征序列,理解前后语境;
-
全连接层 + CTC 输出:生成字符预测结果。
这种结构特别适合处理「一整行字符」的图像,也就是那种你不需要逐个分割字符,而是直接把一张字符区域图喂进去,它就能一口气输出整个字符串的方案。非常适合车牌识别场景。
4.2 为什么要用 CTC?
CTC(Connectionist Temporal Classification)最牛的地方就是不要求字符和位置一一对齐。也就是说,我们训练的时候只需要提供整张图片对应的文字标签,比如:

标签:鲁N Y97L0
CTC 会自己在训练中学会怎么对齐字符和图像上的位置,不需要你提前标出每个字符在哪儿。对我们这种实际数据标注难度高的项目太友好了。
4.3 模型训练的一些事
我们训练用的数据主要来自我们自己采集的监控视频截图,用脚本定期抓取画面,然后人工标注车牌文字。清洗过程还是挺繁琐的:
• 把模糊图、反光图去掉;
• 对图片做标准化处理,比如统一宽高比;
• 加了一些图像增强操作,比如旋转、模糊、噪声、色偏,提升模型鲁棒性。
训练过程中也踩过一些坑,比如:
• 序列长度不够长,CTC 输出容易塌缩(输出成重复字符);
• 字符集设计不完善,漏掉了新能源车的“D”“F”前缀;
• 数据分布不均,造成某些字符识别率偏低;
不过整体来说,模型在验证集上的准确率很快就拉到了 95% 左右,最关键的是——在一些传统方法完全识别不出来的场景,它居然能读出来!像那种模糊不清但人眼还能猜的图,它也能成功识别,算是给我们打了一针强心剂。
5. 数据准备与增强实践
说句实话,模型训练靠的不是花里胡哨的网络结构,真的靠的是数据质量。我们这个车牌识别模型能跑得还不错,80%功劳都得归到数据处理这一步上。
5.1 数据从哪来?
我们最初的数据来源是监控视频截图。停车场每辆车进出时摄像头都会抓拍画面,我们写了个脚本,定期从存储系统里抽取图片,带时间戳的那种。
不过这些原始图都不能直接拿来训练,需要经过一轮又一轮清洗。我们干了这些事:
• 图像裁剪:只保留有车牌的部分,背景太大的图直接舍弃;
• 质量筛选:模糊、曝光过度、反光太强的图直接扔掉;
• 人工标注:用一个简单的图像标注工具,人肉输入每张图的真实车牌号;
• 字符集整理:收集所有可能的字符,建立一套车牌字符字典(包括新能源前缀、军牌、省份简称等等);
标注过程真的挺磨人的,不过好在只要首批数据标得够准,后面可以用模型做预标注,再人工纠正,就轻松多了。
5.2 图像增强操作(真的有用)
为了让模型能扛住各种“车牌奇葩状态”,我们给训练图做了一大堆增强:
• 旋转:±15° 以内轻微旋转,模拟歪着进来的车;
• 亮度变化:随机调亮/调暗,模拟白天和夜晚;
• 模糊处理:加一点点高斯模糊,模拟镜头虚焦;
• 添加噪声:模拟雨天、脏污;
• 色调变化:HSV 色彩空间做扰动,模拟白平衡偏差;
这些增强操作真的不是为了凑热闹——我们一开始模型在夜间图表现特别差,后来特地加了一批“夜间风格”的增强图,再训练一次之后明显效果提升了。
而且有些问题增强也解决不了,比如遮挡、反光那些,我们后来干脆拉了一批「问题图」单独训练,变成模型的专项强化练习(笑)。
5.3 数据分布也很重要
一开始我们犯了个低级错误:数据集中上海车牌(沪A、沪B)占了大头,导致模型总是喜欢猜“沪”。后来我们调整了数据分布比例,让不同省份、不同类型的车牌在训练集里更均匀,这才缓过来。
6. 模型部署与系统集成
模型训好了,只能算半成事。真正要上线用,还得把模型变成一个随时能被系统“叫醒”、快速响应的服务,这部分其实我们也花了不少时间。
6.1 用 Flask 封一层接口
我们一开始选的是最直接的方式:Flask + TensorFlow(1.x)原生 Session 来做部署。没用什么 fancy 的部署框架,主要是为了轻量和方便调试,接口结构大概就这样:
• /predict:接收图片(base64 或 multipart),调用模型推理,返回识别结果;
• /ping:健康检查;
• /reload(内部用):重新加载模型,方便后面热更新。
每次请求进来就走一套流程:图像预处理 → CTPN → CRNN+CTC → 后处理输出字符串。
虽然有点原始,但胜在简单可靠,后来部署到线下场景也挺稳定的。
6.2 模型加载和推理效率优化
说实话,TensorFlow 1.x 真不太友好。最开始我们每个请求都建 Session,推理时间能飙到 2~3 秒,完全不能用。后来换成全局加载模型 + 线程锁管理 Session,推理时间立马压到了 500ms 以内。
还有几点优化小心得:
• 模型冻结(freeze_graph):把训练好的 ckpt 转成 pb 文件,推理加载更快;
• Batch 尺寸优化:我们评估时 batch_size 固定为 1,避免不必要的内存开销;
• 图像 resize 尺寸对齐:输入图统一为固定高宽,避免每次动态 reshape;
• 预处理和后处理放主线程处理,只把纯模型推理部分用锁保护,提升并发能力;
这些小改动,虽然不难,但每一条都能救你一口性能。
6.3 系统联动:识别 + 计费 + 放行
模型不是孤岛,它得和停车场管理系统打配合。我们这边做了几个联动:
• 入场识别:摄像头拍照,调用接口识别车牌,记录入场时间;
• 出场识别 + 计费:再次识别后查数据库计算停留时长、费用;
• 放行控制:识别+扣费成功后,下发开闸信号;
• 失败兜底:识别失败或多次识别结果不一致,会转人工审核;
为了防止“卡顿堵车”,我们整个流程控制在 1.5 秒以内,其中模型推理时间不能超过 700ms,剩下的时间给数据查库、业务逻辑和网络请求。
6.4 部署环境
最早几家门店我们用的是一台小型边缘服务器(i5 + 8GB RAM 没有 GPU),压测下来勉强够用。后面上线门店多了,我们换成了轻量级 Docker 服务部署在局域网网关设备上,响应时间更稳,维护也方便。
7. 实战成效与经验总结
系统上线那天其实我们还是挺紧张的,尤其是第一家门店,设备一接好,路口刚好就排着三四辆车等着进出,全靠我们模型不给我们“添堵”。
7.1 系统上线表现
我们系统最终在实际场景中跑得比预期还要稳一些,具体几个关键指标:
• 识别准确率:白天场景稳定在 96%+,夜间略低,但也保持在 93%左右;
• 平均识别时延:单张图片识别时间在 600~800ms 之间(包含全部流程);
• 日均识别量:单店约 500~1000 次识别,高峰能到 2000+;
• 上线门店数:最终部署到 30+ 个停车场,基本没出过重大事故。
更关键的是,原本高峰时段门口排队的情况缓解了不少,整体通行效率提升差不多在 40% 左右,人工干预大幅减少。我们现场运维的同事说,之前门卫经常抓狂写错车牌、算错钱,现在每天轻松很多(笑)。
7.2 遇到的坑(踩过才知道)
虽然结果不错,但中间也踩了不少坑,这里随便列几个,大家可以避避:
• 模型没做字符合法性校验,一开始竟然识别出“1A2B3C4”这种根本不存在的车牌号,后来加了正则规则和置信度筛选才缓解;
• 图片预处理太“干净”,前期训练图像质量偏高,结果一上线就被现实毒打,后来特地加了一批“脏数据”做鲁棒性提升;
• 多线程并发 Session 崩溃,用 Flask 时没加锁导致多线程 Session 冲突,线上一度频繁报错,后来老老实实加了锁管理;
• 字符集不统一,有的标注是“沪A12345”,有的是“沪 A12345”,训练时没注意,结果输出格式不一致,系统识别失败;
这些问题不是模型的问题,是我们“业务落地”经验不够造成的——你得时刻记得这个系统不是跑 demo 的,是要真干活的。
7.3 总结一下:
整个项目做完下来,我自己最大的体会是:
OCR 项目从来不是纯模型问题,而是
数据、业务、部署、系统联动
从传统方法到深度学习,看上去像是“技术升级”,但其实更像是一次「认知升级」:你开始关注更多系统层面的事情,比如服务性能、接口设计、用户体验、甚至运维和日志。
当然,我们这套也不完美,还有很多可以优化的地方,比如后面版本我们也在尝试:
• 用更轻量的模型(比如 MobileNet+CRNN)做前端部署;
• 用 ONNX 或 TensorRT 做跨平台部署;
• 引入日志追踪和自动告警系统,提升系统可维护性;
这些就是后话了,有机会再分享。