
简介一份面向深度学习车牌识别实战的完整工程资源聚焦LPRNet与MTCNN的协同工作流程适合计算机视觉初学者、算法工程师及智能交通项目开发者。资源包共870个文件、约543.9MB其中包含大量ckpt模型权重文件覆盖LPRNet不同迭代次数、Python源码、PyTorch模型文件pth以及图片与标注文件并附带UI设计、文档和演示视频便于从训练到部署全流程参考学习。已有406人浏览学习。内容在检测端使用MTCNN进行车牌定位识别端采用LPRNet完成字符序列输出并给出了预处理、裁剪、识别到后处理的完整实现思路。通过这一资源读者可以快速搭建车牌识别实验环境获取现有训练权重和推理代码研究不同训练轮次下的模型表现同时也能参考工程目录结构与数据处理脚本为自定义数据集训练和模型优化提供直接基础。1. 为什么在停车场项目里选MTCNNLPRNet这条路先说说我遇到的实际场景。当时接了一个园区停车场的车牌识别需求要对着出入口相机抓拍画面做实时识别要求既要能跑在普通服务器上又要保持较高的准确率。最早团队里有人提议直接用YOLO v2或者YOLO v4做端到端检测再配一个分类网络去读字符。说实话这个方案本身没什么问题但在真实项目里会遇到几个很现实的难点。第一个痛点是标注成本。YOLO系目标检测要画的是“车牌整体位置框”这还好办跟着矩形框拉就行。但如果你走的是先检测再分割字符的路线就必须额外标注字符级的位置框一个车牌七到八个字符一辆车一个框变成七八个框标注工作量直接翻倍。第二个痛点是字符切分在真实场景里非常脆弱车牌上有螺丝钉、有污渍、有倾斜、有反光字符粘连和断裂是常态一旦分割错一位识别结果基本就废了。所以我最终选的是MTCNN做车牌区域检测、LPRNet做车牌字符序列识别。这两个模型组合起来最大的好处是不需要做字符级分割。MTCNN负责把车牌位置找出来LPRNet用CTC对齐的方式直接输出一串字符整条pipeline干净利落。当时LPRNet在精度上虽然不一定比得上某些重型模型但胜在轻量、推理快、部署简单对于停车场这种追求“够用且稳定”的场景非常合适。适合参考这条路线的人有两类一类是正在做车牌识别相关课题的学生想了解一套能跑通的完整方案另一类是工程落地人员想找一个不依赖商业SDK、自己可控的识别模块。我下面写的所有内容都会围绕这套方案的原理、代码、工程坑和调优经验展开。2. MTCNN做车牌检测改改模型就上不用从零训练2.1 MTCNN原本是用来干嘛的MTCNN全称是Multi-task Cascaded Convolutional Networks最早是2016年提出的人脸检测算法。它由三个级联网络组成P-Net、R-Net、O-Net。P-Net在图像金字塔上快速生成大量候选人脸框R-Net做进一步筛选和精修O-Net输出最终的人脸框以及 landmarks 关键点坐标。关键点在于MTCNN的三个网络内部都有多条任务分支除了框回归分支之外还带了分类分支。而结构上的这种多任务设计天然决定了它可以被迁移到其他检测任务上。车牌检测和人脸检测在视觉形态上有不少相似之处——都是一个矩形目标、包含内部纹理、相对独立地出现在画面中。所以我们不需要把模型结构推倒重来只需要把训练数据换成车牌图片把输出类别从“人脸/非人脸”改成“车牌/非车牌”就能得到一个相当能用的车牌检测器。2.2 数据准备与标注格式转换MTCNN的训练数据需要特定格式的标注核心字段包括目标框左上角和右下角坐标、类别标签、以及关键点坐标人脸场景下是双眼鼻子嘴角车牌场景下可以留空或者标注车牌四角。我当时从停车场相机里抓了大概两万多张图然后通过人工半自动方式标了框。半自动的方式很简单先用一个已经训练好的YOLO模型对图片做预标注然后再人工修正那些偏移比较大的框。这样两万张图两个人花三天时间能标完。如果项目预算少、时间紧也可以用开源车牌数据集做预训练然后用自己场景的数据做微调这样需要的标注量可以降到几千张。数据标注完成后需要转成MTCNN训练用的TFRecord或者自定义DataLoader格式。我这里用的是PyTorch版本所以直接写了一个继承Dataset的类。核心处理包括随机裁剪、颜色抖动、仿射变换、水平翻转。注意车牌上的字符顺序不能搞反水平翻转这种增强方式要谨慎——翻转之后“京A12345”会变成“54321A京”虽然检测框是对的但后续识别模型会有问题。所以我训练检测器的时候翻转增强的概率设得很低只在竖直方向做轻微平移扰动。2.3 关键训练参数与结构微调用MTCNN训练车牌检测和原始论文里的训练策略保持一致就好。损失函数用的是多任务加权和L_total alpha * L_cls beta * L_box gamma * L_landmark其中分类损失用交叉熵框回归损失用欧氏距离。车牌场景没有关键点标注我就把gamma设成0alpha设为1beta设为0.5让网络把重心放在分类和框回归上。这里有个细节原始MTCNN的框回归用的是坐标差值的归一化即 dx (gt_x1 - pred_x1) / size训练时标签也要做同样的归一化处理否则loss会震荡得很厉害。我在微调过程中把O-Net最后一层的卷积核数量稍微减少了一点从原始256减到128因为车牌检测相比人脸检测类别内变化更小不需要那么强的表示能力减下来之后模型体积缩小推理速度提升精度没有明显下降。2.4 检测结果的后处理细节MTCNN输出的是一个矩形框加置信度。到了实际使用阶段检测框往往不是特别紧贴车牌边缘所以要做一步外扩。对外扩的比例我一般取10%到15%具体看相机安装角度。如果车牌在画面里比较小外扩比例要更大一些给后续识别模型留出足够的上下文避免字符被切断。另外要处理一个现实问题MTCNN会输出大量重叠框。原始代码用NMS做抑制但NMS阈值对结果影响很大。停车场场景里一辆车通常只有一个车牌但反光、阴影可能导致多个高响应区域。我实测NMS的IoU阈值设在0.4比较合适太低会把同一个车牌的多个候选框全部保留太高又会把贴近车牌的误检框也算进来。还要加一个置信度阈值过滤这个值设在0.85以上因为停车场的相机角度相对固定检测难度低于通用场景宁可漏检一点也不能让误检冲垮后面识别模块。3. LPRNet车牌识别无分割序列识别的核心逻辑3.1 为什么车牌识别要抛弃字符分割传统车牌识别算法一般分三步定位车牌、分割字符、逐个字符分类。字符分割看着简单实际上在真实环境里是最大的坑。车牌中间有小圆点分隔符不同省份的汉字宽度不一样新能源车牌比普通车牌多一位字符还有双层黄牌这种特殊类型。分割算法稍微调不好整个识别结果就崩了。LPRNet走的是另一条路。它检测完车牌之后把整张车牌图片输入网络通过CNN提取视觉特征再经过RNN建模序列关系最后用CTC loss对齐预测序列和真实车牌文本。整个过程完全没有字符分割网络自己学习字符之间的边界在哪里。这种做法对字符粘连、遮挡、模糊都有更好的鲁棒性。3.2 LPRNet的网络结构拆解LPRNet的骨干网络是一个轻量CNN由多个卷积块堆叠而成。每个卷积块包含卷积、BatchNorm、ReLU部分块还加了MaxPooling。输入图片的典型尺寸是 94x24宽高比接近4:1正好匹配普通车牌的形状。CNN部分提取的特征图会送进一个双向LSTM用于捕捉字符序列的上下文依赖。LSTM输出的特征经过全连接层映射到字符类别空间最终得到形状为 [T, num_classes] 的序列预测。其中T是时间步数等同于特征图在宽度方向的序列长度num_classes是字符集大小加一个CTC空白符号。我自己复现的时候CNN部分主要参考了原始LPRNet论文的结构但把LSTM的隐藏层大小从原来的128调成了64。因为中文车牌字符集不大常见省份汉字加上字母数字也就是七十多个类别序列长度本身也不长隐藏层太大容易过拟合而且推理速度会受影响。下面是核心模型结构的简化代码方便理解整体流程import torch import torch.nn as nn class SmallBasicBlock(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.conv nn.Conv2d(in_channels, out_channels, kernel_size3, stride1, padding1) self.bn nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) def forward(self, x): return self.relu(self.bn(self.conv(x))) class LPRNet(nn.Module): def __init__(self, num_classes): super().__init__() self.backbone nn.Sequential( SmallBasicBlock(3, 64), nn.MaxPool2d(kernel_size3, stride1, padding1), SmallBasicBlock(64, 128), nn.MaxPool2d(kernel_size3, stride2, padding1), SmallBasicBlock(128, 256), SmallBasicBlock(256, 256), nn.MaxPool2d(kernel_size3, stride2, padding1), SmallBasicBlock(256, 512), SmallBasicBlock(512, 512), ) self.adaptor nn.Conv2d(512, 128, kernel_size1) self.rnn nn.LSTM(128, 128, num_layers2, bidirectionalTrue, batch_firstTrue) self.linear nn.Linear(256, num_classes) def forward(self, x): x self.backbone(x) # [N, C, H, W] x self.adaptor(x) # 降维 x x.mean(dim2) # 高度方向压缩得到序列特征 x x.permute(0, 2, 1) # [N, W, C] x, _ self.rnn(x) x self.linear(x) # [N, W, num_classes] return x代码里有个值得注意的地方高度方向不是用全连接层压扁而是直接做全局平均池化。这么做的好处是让网络对车牌字符在竖直方向上的位置变化不那么敏感车牌偏上偏下一点都能识别。3.3 字符集设计、训练细节与损失函数字符集我定义为省份汉字京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云陕甘青宁新藏、大写字母排除I和O防止和数字1、0混淆、数字0到9、新能源车牌的特殊字符“挂”“学”“警”等。常见场景下全部加起来约70个类别再加一个CTC空白符。训练数据方面我收集了差不多二十万张车牌图片包含正常角度、倾斜、模糊、夜间、雨天等不同情况。这里面大概十五万张来自停车场相机五万张来自网络公开数据集。数据增强非常关键尤其是光照扰动和透视变换。车牌识别最容易翻车的就是强反光和夜间低照度所以我把随机亮度调整范围设到了正负40%随机对比度调整也在正负30%以上。透视变换的幅度不能太大水平方向偏移控制在±15度以内超过这个角度人眼识别都困难。训练用的损失函数是PyTorch内置的CTCLoss要传三个关键参数预测序列logits、目标序列标签、以及每条样本的序列长度和目标长度。CTC的beam search解码参数在我这里用的是beam_width10再结合一个简单的词典过滤把不可能出现的连续重复字符合并效果比纯贪心解码好不少。OCR模型训练还有个容易被忽视的点学习率策略。CTCLoss的收敛曲线通常会在训练初期下降很快然后陷入长尾。我在训练LPRNet时先用1e-3的学习率跑前5个epoch做warmup再切换到余弦退火。总共训练了差不多20个epoch就能达到一个比较稳定的精度继续增加epoch收益不大反而可能在结构化噪声上过拟合。3.4 训练数据与真实场景的差异前面提到数据增强但还是要强调一个我踩过的坑训练数据里的“干净车牌”太多了。很多公开数据集里的车牌都是正对着相机拍的正视图车牌表面干净、光照均匀。而真实停车场里车牌经常是脏的、歪的、装在前保险杠弧面上的还有大量加了边框和底托的车牌。如果只用公开数据集训练模型在本地测试集上可能看起来有99%的准确率一上现场就掉到七八成。我的经验是从现场相机抓取足够多的真实图片加入训练集至少要占30%以上。哪怕现场图片标注质量差一些只要框的位置基本准确对模型提升的效果也远好于再增加几万张标准图。另一个做法是合成数据用程序把真实的省份汉字、字母数字随机组合叠加到真实背景图上再做随机的变形、加噪、调色。这个方法在小样本场景下特别有用我后来在项目里就用合成数据把识别准确率又往上拉了两三个百分点。4. 从单张图片到停车场相机流工程落地的几个大坑4.1 相机取流与车辆触发模型调好之后真正往停车场项目里部署才发现难点根本不在模型本身而在工程链路。停车场出入口一般是海康、大华这类IPC相机常见两种取流方式一种是直接用RTSP拉视频流另一种是通过相机SDK或者ONVIF协议获取抓拍图片。RTSP拉流简单但容易卡顿和丢帧适合做调试不适合做正式计费。SDK取图比较稳定但每个厂商的SDK风格差异大接口文档也写得参差不齐。还有一种更主流的做法是用硬件触发。出入口相机自带地感线圈或者雷达触发相机拍下车辆照片后通过HTTP或者MQTT把图片推送到服务端。现场项目里很多是大华和海康对接如果走的相机私有协议需要去查厂商SDK里抓拍回调字段的说明。MQTT是另一种常见的对接方式很多停车平台会把相机抓拍图片作为消息内容推送到MQTT broker服务端订阅主题拿到图片之后再跑检测识别。这种方式解耦性最好前端相机和后端AI服务各管各的出了问题也方便排查。我们当时就是订阅了以设备编号为标识的主题图片以base64编码放在JSON消息体里服务端收到后解码再进识别流程。4.2 低照度与逆光场景的图像预处理停车场出入口最容易翻车的两类图像夜间低照度、白天强逆光。夜间画面整体很暗车牌区域可能曝光不足字符模糊逆光场景则相反车牌区域过曝字符消失在一片白色里。针对这两种情况图像预处理比换更大的模型更有效。我实测下来一套组合拳很管用先做灰度化再做自适应直方图均衡化CLAHE最后做轻度锐化。CLAHE的clipLimit参数我一般设为2.0网格大小设为8x8这个组合在大多数停车场场景下都能把车牌字符的对比度拉起来。还有一个技巧是相机本身的ISP参数调整比如开启宽动态模式WDR让车牌局部曝光更均匀。宽动态对逆光场景的提升比任何算法后处理都明显。顺带说一下补光灯。很多停车场装了白光补光灯或红外补光灯来辅助抓拍。红外光下车牌上的反光涂层会让字符变成白色、背景变成黑色这种“反色”情况模型是不认识的。如果现场装了红外补光灯一定要把红外模式下的反色图片也加入训练集或者做反色增强。4.3 车牌颜色、双层车牌与新能源车牌模型训练的时候只考虑了蓝底白字、黄底黑字、绿底黑字、白底黑字等常见类型但实际跑起来还会遇到很多边缘情况。新能源车牌是绿底黑字字符比普通车牌多一位LPRNet这种序列识别模型天然能处理变长输入比固定长度分类的方案要友好很多。双层黄牌车牌的宽高比和普通车牌完全不同直接resize到94x24会严重变形。我针对双层车牌单独训练了一个模型或者更简单的方式检测出车牌框之后先根据框的宽高比判断是单层还是双层再决定resize到哪个尺寸。另外大货车车牌经常挂得很低相机角度大拍出来是严重的透视图。MTCNN检测出来的框是矩形如果不做矫正直接把透视变形的车牌送进LPRNet识别率会明显下降。我后来在检测和识别之间加了一个透视矫正模块用车牌角点估计一个单应矩阵把车牌矫正成正面视角。这个步骤对倾斜较大的车牌提升非常明显准确率能从85%左右拉到95%以上。4.4 并发与响应时间优化停车场项目有一个硬性指标从车辆进入识别区域到道闸抬杆整个耗时必须在几百毫秒到一秒以内。识别服务如果还要排大队现场体验会非常差。我当时的做法是把检测和识别拆成两个服务中间用消息队列衔接。检测服务负责从相机图片里找车牌框找到之后把裁剪好的车牌图片发到识别服务。两个服务都可以独立水平扩展识别服务瓶颈明显时就多加几个实例。推理层面MTCNN和LPRNet都是轻量模型在GPU服务器上单张图片的推理时间加起来不超过20毫秒即使只用CPU也能跑到100毫秒以内。真正的耗时大头反而在图像解码和网络传输上。如果你用的是Python尽量用OpenCV的imdecode而不是PIL去解码解码速度差距在2倍以上。服务端收到MQTT消息时也不要直接就在回调线程里跑推理先把图片数据丢进队列用独立的工作线程池去消费避免回调阻塞导致消息积压。5. 实测效果与调优经验让模型在恶劣环境下稳住5.1 一个真实场景的精度数据最后说一下我在某个园区停车场项目里的实测数据。这个项目是双车道出入口一个方向进一个方向出相机安装高度约1.5米抓拍距离3到5米。用了大概一个月的抓拍图做测试集总共约一万张覆盖白天、夜晚、雨天、逆光、跟车等场景。测试结果比较理想车牌检测率正确检出车牌且不存在框错位在99%左右车牌识别准确率在检测正确的前提下字符全部识别正确约96.5%整条流水线端到端准确率约95.5%。这里的准确率定义为“一个字符都不错”如果允许一位字符错误而触发二次识别或者人工审核那可用率可以到98%以上。其实单纯看96%的准确率好像没有比商用SDK高但我更在意的是在可控成本下实现了自主可控。商业SDK是按年授权的停车场这种场景一年license费用不低而且断网情况下没法用。自研方案模型文件几十MB离线也能跑后续针对特殊场景调优也不受制于人。5.2 提升准确率的三个有效手段第一是二次识别。现场车辆不是静止的抓拍瞬时角度差一点识别结果就可能出问题。我实现的方案是车辆触发抓拍后连拍三张分别识别然后用投票或置信度加权的方式决定最终结果。这个方法直接把端到端准确率提升了将近2个百分点而且实现成本极低。第二是字符级别的置信度分析。LPRNet的CTC解码结果可以拿到每个字符位置的置信度如果某个字符的置信度低于阈值比如0.7就标记为低置信度。在停车场后台逻辑里遇到低置信度字符时优先走人工审核或者再触发一次识别而不是直接把错误结果输出到计费系统。第三是针对特定省份做地域化微调。如果你负责的项目集中在某个省可以额外收集该省的车牌数据做微调尤其是省名汉字的识别。车牌省份汉字本身是一个固定集合但手写风格的汉字印刷体在不同字体下还是有差异的微调之后省份识别准确率能明显提升。比如“渝”和“湘”在某些字体和光照下容易互相混淆加数据微调后这类问题大幅减少。5.3 模型部署的几个坑部署阶段我遇到过不少问题挑两个有代表性的说说。第一个是ONNX导出时的动态形状问题。PyTorch导ONNX默认用固定尺寸输入但LPRNet输入的宽度在检测框外扩之后可能有小的波动写死尺寸会导致运行时resize不准。解决方式是把输入尺寸所有维度都设为动态轴导出命令里指定dynamic_axes。当时这个问题排查了好几个小时表面上是推理报错实际上是因为动态维度没开。第二个是TensorRT加速下的精度抖动。在FP16精度下LPRNet的CTC输出分布会发生细微变化导致个别字符识别错误。尤其是汉字类别之间的差异本来就小量化误差可能会把置信度排名打乱。如果追求稳定建议TensorRT用FP32如果需要FP16性能可以在导出前做QAT量化感知训练直接PTQ的话效果不太可控。5.4 一个容易被忽略的现场问题相机视角一致性模型在一个停车场调得再好换一个场地可能效果就打折。原因往往不是模型泛化能力不行而是相机的安装高度、俯仰角、抓拍距离变了车牌在画面里的大小、角度、宽高比都和训练数据有差异。我刚接手第二个停车场项目时就吃了这个亏。第一个项目相机装在1.5米高度画面里车牌大约占80到100像素宽第二个项目相机装在2.5米高度车牌在画面里只有40像素宽识别率直接掉到90%。后来把训练数据里加入小目标样本同时调高测试集里小尺寸车牌的权重才重新拉回95%以上。如果你在多个场地部署最好在每个场地上线后先跑一周数据收集用新场地数据做增量微调。6. 后续还能往哪扩展从识别到停车平台LPRNet加MTCNN这套组合本身已经解决了“车牌是什么”的问题但在停车场项目里真正要做的其实是“车牌对应哪个车、该怎么计费”。我后来在这个项目基础上做了三个扩展方向。第一个方向是车辆特征融合。只认车牌有一个天然弱点套牌车。后来我增加了车辆颜色、车型、车标这些辅助信息用另外一个轻量分类模型做多任务输出和车牌信息一并上传到平台。这样即使车牌的某一位字符识别不确定也能用车辆特征做交叉验证降低误判概率。第二个方向是云边协同。直接把AI推理放在摄像头端做不现实因为大部分相机没有GPU但可以在边缘小主机上部署模型只把识别结果上传云端。这样网络抖动不影响本地落杆云端故障也不影响现场的兜底运行。我这边实际用的方案是NVIDIA Jetson系列边缘盒子跑LPRNet加MTCNN绰绰有余。第三个方向是异常事件处理。比如无牌车、车牌遮挡、跟车闯闸这些情况模型是没法解决的需要在平台上做业务逻辑的兜底。我当时加了一个事件回调模块识别失败时抓拍现场图和全景图推送给管理人员辅助人工远程放行。AI识别不是万能的但是配合好的业务流程能覆盖掉绝大多数长尾场景。再分享一个关于数据流的小技巧不管你是用MQTT对接相机还是用SDK拉图都建议保留一个本地图片落盘目录。出问题的时候翻日志不如看图快保留一份原始抓拍图和一份识别中间结果图能让你在一个月后排查问题的时候少掉很多头发。我最后说句实在话这套方案放到今天不算最新最前沿但它胜在结构简单、每个环节都可解释、可替换。如果你正在基于LPRNet和MTCNN做类似的车牌识别项目完全可以把我的经历当成一份参考。从模型选型到部署运维每一步都有比我想象中更多的细节这些细节才是真正确保项目稳定运行的关键。本文还有配套的精品资源点击获取