最近整理自己毕设阶段写的车牌识别项目,把代码和文档一起开源了。这套系统的核心能力很简单:输入一张包含车辆的图片,先由目标检测网络把车牌区域框出来,再用字符识别网络把车牌号读出来,最后输出文本和坐标,可以直接跑在停车场出入口模拟场景,也能处理手机随手拍的照片。从立项到跑通,前后花了三个月,踩了不少坑,也总结了一些通用经验。如果你正在准备类似的毕设,或者想了解一个识别类CV项目从模型训练到服务化部署的完整路径,这篇复盘应该能帮你少走一些弯路。
1. 先把问题拆清楚:为什么是“检测+识别”两步走
1.1 端到端和两步方案,我为什么选了后者
车牌识别在方向上大致分成两条路线,一条是端到端,另一条是两步走。端到端方案输入整车照片,输出车牌号文本,看起来流程最短,实际做起来对数据量和训练技巧要求很高。它要把“找车牌”和“读字符”两个任务揉在一个网络里,一旦效果不好,问题出在检测还是识别,很难定位。
两步方案则是先检测车牌区域,再把车牌区域单独送进识别模型。我最终选了后者,理由是毕业设计需要“每一步都能讲清楚”。检测和识别可以分开训练、分开评测、分开调试,哪个环节效果差就调哪个,实验记录也好写。答辩的时候,你可以明确说检测模块用的什么结构、识别模块用的什么结构、各自达到多少准确率,这个逻辑评审老师接受度很高。
如果你的目标是快速做出一个能玩的Demo,端到端确实省事。但如果要写论文、要做系统演示,两步法的可控性会让整个项目推进顺畅很多。
1.2 完整流程:从一张图片到一串车牌号
我实现的完整数据流是这样的:
- 读入图片,做基本缩放;
- 目标检测模型输出车牌框的四个角点坐标;
- 根据四个角点计算透视变换矩阵,把倾斜的车牌区域“拉正”;
- 拉正后的区域做尺寸归一化,送入字符识别模型;
- 字符模型输出字符序列,经过去重和映射,得到最终车牌号。
很多教程不会重点讲透视校正这一步,但我实际做下来发现它非常关键。停车场摄像头经常是斜着装的,检测框出来是一个不规则四边形,如果只是按矩形把原始区域切下来,字符是歪的,识别准确率会掉得很难看。用四个角点做一次透视变换,把车牌区域“摆正”,识别模型拿到的是规整输入,后面会省心很多。
1.3 技术栈选择:PyTorch、YOLO 和 LPRNet 是怎么凑到一起的
这套系统最终的技术栈是:PyTorch 作为深度学习框架,YOLO 系列做车牌检测,LPRNet 做字符识别,FastAPI 提供HTTP接口,前端用简单的 HTML/JavaScript 做交互演示。
选择这组搭配不是因为它们最强,而是因为它们“资料最多、坑最浅”。PyTorch 在学术项目里属于默认选项,调试和可视化都方便。YOLO 有成熟的训练脚本和导出接口,适合快速验证。LPRNet 结构轻,参数量小,识别车牌这种固定字符序列的任务效果足够,而且CPU也能跑得动,答辩现场不依赖GPU本身就是一个加分项。
我把三种方案对比了一下,列成表格更清楚:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 传统图像处理(边缘检测+模板匹配) | 无需GPU,速度快 | 光照、角度一复杂就崩 | 固定角度、单一场景 |
| 端到端识别网络 | 流程短,无中间模块 | 数据要求大,训练难度高 | 数据充足的研究项目 |
| 检测+识别两步方案 | 模块解耦,可控性好 | 流程稍长,需要维护两个模型 | 毕设、工程落地项目 |
2. 车牌检测模块:数据、训练和几个容易翻车的地方
2.1 数据集怎么准备:公开数据、标注转换和数据清洗
数据是整个项目里最花时间的环节。公开数据集方面,我用了CCPD和CRPD这两类常见数据集,前者主要来自停车场场景,标注重合度高;后者包含更多路边场景,背景更杂,两者互补效果比较好。
CCPD的标注格式比较特殊,它的文件名本身就编码了车牌信息和角点坐标。实际使用之前最好写一个脚本,统一转成我自己项目里的标注格式,比如YOLO常用的txt标签。这一步不要偷懒,格式统一了,后面写训练和评测脚本才省心。
转换脚本的核心逻辑大致是:从图片文件名解析出车牌字符、角点坐标和车牌类型,再按目标检测的格式写出归一化后的中心点坐标和宽高。转换完后一定要抽查可视化结果,把标注框画在图片上人工看一遍。我抽查时发现有些样本标注框明显偏移,这种数据混进训练集会拖低模型精度,宁可删掉也不要留着。
2.2 数据增强:不要只做“翻转+亮度”的幼儿园配置
车牌检测的难点不在标准正面照,而在各种奇怪角度和光照。数据增强我加了这几类,都是实测有用的:
- 随机透视变换:模拟摄像头斜拍,这是提升倾斜车牌检测能力的最有效一招;
- 亮度/对比度扰动:模拟早晚光线变化;
- 高斯模糊和运动模糊:模拟车辆行驶时的动态模糊;
- 随机色温偏移:模拟黄昏和路灯下偏黄的画面;
- 上下文扩充:车牌周围保留适当背景,避免模型只学会找“纯车牌”。
这里有一个容易被忽略的细节:YOLO自带的mosaic增强在训练后期我建议关掉。车牌是长宽比固定的细长目标,mosaic把四张图拼在一起后,车牌容易被切碎或在拼接边界处变形,后期反而影响收敛。我的做法是前150轮开mosaic,后50轮关掉,只用基础增强。
2.3 训练超参数和后处理细节
训练参数方面,我用的输入分辨率是640×640,batch size根据显存定,一般单卡16或32。训练200轮左右,优化器选择AdamW,学习率用余弦退火,从1e-3衰减到1e-5。
评估的时候不要只看一个平均mAP,按场景分桶统计会更真实。我习惯分成白天、夜晚、强逆光、远距离小目标四组,分别计算AP。实测发现,如果训练集中夜间图片比例少于10%,夜间AP会明显掉到不可用的程度,这不是调参能解决的,只能去补夜间数据。
检测模型输出后还有一个后处理细节值得提一下。NMS的IoU阈值我设置的是0.45,置信度阈值是0.25。对于车牌这种画面里一般只有一个目标的情况,可以进一步加一个“取最大面积框”的逻辑,避免误检的小框干扰。
3. 车牌字符识别模块:LPRNet 是怎么把字符念出来的
3.1 先聊聊老一代的“字符分割”为什么被淘汰
早期车牌识别大多是“先分割再识别”的思路,先通过投影法把车牌区域里的一个个字符切出来,再对每个字符做分类。这套方法在车牌端正、光照均匀、字符间距标准的情况下效果不错,但只要摄像头角度一歪,字符区域互相粘连,投影分割就会失效。
我在项目初期也试过基于轮廓和连通域的分割方案,结果在倾斜车牌的测试集上识别率惨不忍睹。后来果断切到序列识别方案,也就是LPRNet这种“整行输入、整行输出”的架构,彻底避开了字符分割这个不稳定的中间环节。
3.2 LPRNet 的原理:CNN+序列建模+CTC,一句话版
LPRNet大致分三段。第一段是卷积网络,对输入图像提取特征,得到带时间步的特征序列;第二段是序列建模模块,把每一列特征看成一个“时间步”,建模相邻字符之间的依赖;第三段是CTC损失,解决“图像列和字符怎么对齐”的问题。
CTC可以这样理解:你不需要告诉模型“第几列是第几个字”,模型会预测一个比较长的字符序列,CTC负责把重复字符合并、去掉空白符,再和真实车牌文本对齐。这个过程就像做听写时不需要给字幕加时间轴,只要整句内容对上了,中间怎么对齐由网络自己学。
车牌字符识别正好适合CTC,因为车牌长度不固定。普通蓝牌是7位,新能源绿牌是8位,黄牌又可能是8位或更多,如果设计一个固定输出的分类模型反而别扭,用CTC正好解决不定长序列的输出问题。
3.3 字符集设计、相似字符处理和样本均衡
车牌字符集包括汉字、字母和数字。常规蓝牌第一位是省份汉字,第二位是发牌机关字母,后面是数字和字母;新能源绿牌前两位一般是“省份汉字+D/F”;字母I和O在正式车牌里通常不出现,但数字1和0会出现,所以训练字典里不能简单拿掉相似字符,否则模型遇到1和0时容易被带偏。
训练字符识别模型最大的坑是样本不均衡。数字0到9的样本数量通常远多于省份汉字,热门汉字可能上千张,冷门汉字可能就几十张。如果不处理,模型对冷门汉字的识别率会非常差。我的做法是统计校验集上每个类别的准确率,找出准确率偏低的类别,再去数据集中补充对应样本。补数据比调参数有用得多,效果提升非常明显。
3.4 裁剪输入的小细节,直接影响最终效果
字符识别模型的输入建议统一到168×48或240×80这样的固定尺寸。裁剪车牌区域时四周要留一点边框,不要卡得太紧。有一次我图省事,用检测框的精确坐标直接裁图,结果把车牌上下边缘裁掉了,字符的下半部分缺失,识别准确率掉了好几个点。
后来我在裁剪坐标外围扩展约10%再送进识别模型,问题就消失了。这个细节在代码里只是一两行的事,但对识别效果的影响不小。另外,识别模型的输入可以用灰度图也可以用三通道图,实测在LPRNet这种结构下,灰度图训练速度更快,准确率相差不大。
4. 从模型到能演示的小系统:服务化部署那些事
4.1 完整的推理流水线怎么设计
模型训练好以后,真正要交付的是一个“能用”的系统,而不是两个孤零零的模型权重。我的做法是写一个检测器类和一个识别器类,内部统一封装预处理、推理、后处理,主流程就是一个函数调用:
image = read_image(file_path) boxes = detector.detect(image) # 返回角点+置信度 if not boxes: return {"plate": "", "confidence": 0} plate_roi = perspective_crop(image, boxes[0]) # 透视校正 text = recognizer.recognize(plate_roi) # 字符识别 return {"plate": text, "score": confidence}如果画面里没有检测到车牌,直接返回“未检测到车牌”,不要强行识别。这个逻辑看起来简单,但实际上很多半成品项目会在这里犯错,没检测到就硬往识别模块里塞,最后输出一堆乱码。
4.2 用 FastAPI 做接口,用网页做演示
服务端我用的FastAPI,只暴露一个识别接口,接收图片,返回车牌号、坐标框、置信度。接口定义大致长这样:
from fastapi import FastAPI, UploadFile app = FastAPI() @app.post("/recognition") async def recognition(file: UploadFile): image = read_image(await file.read()) result = pipeline(image) return result前端用一个简单的网页,可以选择上传本地图片,也可以调摄像头拍照。识别结果直接画在图片上,框出车牌位置并显示车牌号。这里有一个实操细节:如果摄像头实时推流,每一帧都同步调用模型会非常卡,我建议把推理频率控制在每秒3到5次,中间的帧直接丢弃,这样演示流畅度够,代码也不复杂。
4.3 CPU 和 GPU 的部署取舍:答辩现场没显卡怎么办
如果你答辩现场不一定有GPU,可以把模型导出成ONNX格式,用ONNX Runtime做CPU推理。我实测下来,普通笔记本CPU上,检测加识别单帧大概在120到180毫秒,虽然算不上实时,但做演示完全够用。如果机器有独立显卡,可以进一步考虑TensorRT加速,但TensorRT需要花时间处理动态输入尺寸和字符模型的算子兼容,建议放到系统已经稳定运行之后再考虑。
另外,FastAPI服务默认会用同步方式调用推理函数,多个请求同时进来时会有排队问题。毕设场景其实不需要高并发,但提前单测一下还是很有必要的。简单方式是用线程池隔离推理逻辑,或者干脆直接同步调用,把最大并发数限制为1,避免显存冲突。
4.4 开源仓库结构怎么组织才不劝退人
既然决定开源,仓库结构最好一开始就想清楚。我的组织方式是:数据工具脚本、模型定义、训练脚本、推理模块、服务端、前端页面、权重下载说明、README分开存放。权重文件如果太大,不直接传进仓库,而是写一个下载脚本,训练和推理脚本会自动检查权重文件是否存在。
README里写清楚Python版本、操作系统依赖、模型权重放置位置,并给出一个最简运行命令。数据集版权需要在说明里单独标注,不要把数据集文件直接传进仓库。别人clone下来能跑通,这个项目才真的有参考价值,否则空有代码没有说明,和没开源没什么区别。
5. 常见问题排查实录与答辩经验
5.1 识别结果离谱:99%先查预处理
遇到“车牌明明是某地牌照,识别出来却是数字加字母乱码”这类问题,第一反应不要是换模型,先看预处理输出。我在排查同学的问题时发现,他裁剪出来的车牌区域左侧汉字被裁掉了一半,模型当然只能瞎猜。正确做法是把透视校正后的图片打印出来看一眼,确认输入是否正常,再判断问题出在检测还是识别。
我把常见问题整理成了表格:
| 现象 | 常见原因 | 排查顺序 |
|---|---|---|
| 整体准确率低 | 数据集场景单一、样本不足 | 先看预处理输出,再补数据 |
| 汉字识别错 | 汉字样本不均衡 | 统计类别准确率,补冷门样本 |
| 蓝牌准、绿牌差 | 训练集中绿牌太少 | 单独补绿牌数据 |
| 检测框跳变 | 置信度阈值太高或夜图太暗 | 调低阈值,做帧间平滑 |
| 字符残缺 | 裁剪时没有外扩 | 裁剪框向外扩10% |
5.2 检测框跳变和漏检怎么处理
监控视频里同一辆车,上一帧框住了,下一帧丢了,再下一帧又框回来,这种问题通常出在置信度阈值设置上。优先把阈值从0.5调到0.25试试;如果还不行,大概率是某一帧画面过暗,可以加一个简单的帧间平滑,用上一帧的框位置做预测,在邻域内搜索。毕设场景不需要复杂的跟踪算法,简单方案已经够用。
5.3 绿牌、黄牌、倾斜车牌这些特殊场景
普通蓝牌识别率做到接近可用并不难,但绿牌和黄牌会偷偷拉低平均准确率。绿牌比蓝牌更长更窄,字符间距也不同,而且首字符后面是字母D或F,字典里要覆盖这些前缀结构。黄牌经常出现在货车和大巴上,摄像头视角更高,车牌在画面里更倾斜,检测阶段就容易被漏掉。这类问题的解法不是调参,而是补数据,单独准备一部分绿牌和黄牌的图片,训练效果立刻改善。
5.4 毕业设计答辩和开源的一个提醒
答辩演示时,不要只准备“顺利识别”的正例。我整理了一个examples目录,里面有正常识别、夜间识别、倾斜车牌、故意失败的场景,对失败场景还要能说清楚失败原因和改进方向。答辩老师不会因为你有一个失败案例就扣分,最怕的是你说不出失败为什么失败。
开源上再说两句。代码里不要留绝对路径,所有路径配置用相对路径或环境变量。注释可以多一些,但不要写成流水账。如果真的想帮助后来人,把你踩过的坑写进README的“常见问题”一节,这个比贴十段代码都有价值。我也把自己项目开源出去了,后续有人提issue或者改进,这本身就是对这项技术做实操验证的一种方式。
我个人做完整套项目最大的体会是,车牌识别早几年像是CV入门大作业,现在加了检测网络之后,工程难度和训练复杂度都上了一个台阶。真正把模型部署成服务端、把摄像头画面接进来之后,对网络模型、前后端协作、异步推理这些概念的理解会有质的提升。如果你也在做类似的毕设,不用纠结是不是一定要用最新的模型,把流程走通、把边界想清楚,远比堆叠高深的技巧有用。