1. 为什么把车牌识别当作 PyTorch 实战项目
1.1 车牌识别看起来简单,实际上卡在哪儿
先说一个反直觉的现象:车牌识别在工程里看起来特别成熟,门口停车场、高速收费口都在用,似乎是个“老掉牙”的需求。但当我自己把任务拆开,用 PyTorch 从零搭一套识别流程时,才发现事情没那么简单。车牌识别不是单一任务,它至少包含两件事:第一步是“车牌在哪”,第二步是“车牌上的字符是什么”。很多初学者会直接把整张图片丢给一个模型,指望它输出一行车牌号,这种做法在约束环境里能跑通,但放到真实场景马上翻车。
P10 周这个项目,我建议的定位是“字符识别”,也就是默认你已经把车牌区域从大图里截出来了,专心做第二步:把类似“粤B12345”这样的字符序列识别出来。这样项目边界清晰,能在有限时间里把 PyTorch 的卷积网络、序列建模、损失函数、解码策略全走一遍。如果你非要把目标检测也包进来,那你实际要处理的问题是“目标检测 + 分类 + 序列识别”的组合,工作量会翻好几倍。
这个项目最适合谁?两种人。一种是已经跑通 PyTorch 官方教程、会写简单 CNN 分类器的初学者,需要找一个“比手写数字识别复杂一点、但又不至于完全无从下手”的进阶项目;另一种是做传统图像处理出身的人,比如之前用 OpenCV 做模板匹配字符分割,遇到倾斜、光照不均、字体差异就想看看深度学习方案到底有多大优势。
1.2 车牌识别和通用 OCR 的差异
车牌识别(LPR)经常被人拿来和通用 OCR 对比,但两者其实有很多不同。OCR 处理的是印刷体文字,尺度相对均匀、字体变化大,长文本居多;车牌识别的字符数量一般很短,像中国车牌是 7 到 8 个字符,但字符类别里同时包含数字和汉字,还有一些省份简称。
车牌字符有几个非常实际的问题:
- 字符类别不均衡。汉字“粤”“京”“苏”等出现频率相对固定,数字和字母的样本量远大于生僻省份汉字,训练时很容易让模型在汉字上犯迷糊。
- 相似字符容易混淆。比如 0 和 O、1 和 I、8 和 B,单看一个字符几乎没法区分,必须依赖上下文或车牌规则来纠错。
- 字符间距不是完全等宽的。新能源车牌比普通蓝牌多一位,而且有渐变分隔符,直接用固定切分法做字符分割非常痛苦。
正因如此,主流的深度学习车牌识别方案分成了两条路线:一是先做字符级检测或分割,再对每个字符单独分类;二是用 CRNN 这种“卷积 + 序列建模”的结构,把整张车牌图像直接映射成一个字符串序列。后者对工程实现更友好,也是我在这次 P10 周项目里采用的主要思路。
1.3 这次选型的边界:用“识别”来回避“检测”
我在设计这个项目时,刻意做了一个范围裁剪:检测部分不在 PyTorch 训练范围内,而是用 OpenCV 的轮廓查找、颜色过滤等传统方式来做车牌定位,或者干脆用现成的检测模型把结果切出来。这样做的原因很简单——第 10 周的项目应该把核心精力放在“识别”这个环节,否则周报里大半时间都被检测调参吃掉了。
但“回避检测”不等于不处理图像。实际项目中,输入的车牌图像往往不是完美的水平正视图,可能是倾斜的、暗光的、模糊的。我在预处理阶段加了透视矫正、双线性插值缩放、灰度化或保留 RGB 通道等操作。后面我会专门讲预处理对训练收敛的影响,这一步如果能做好,比你在网络结构上花三天时间调参更有效。
2. 环境准备是一个劝退重灾区
2.1 PyTorch 版本和 CUDA 的搭配
PyTorch 环境配置是所有项目里最磨人的步骤,没有之一。打开各种教程会发现有 CPU 版、GPU 版、conda 安装、pip 安装,很容易让人懵。我个人的建议是:能用 conda 就不用 pip,原因不是 conda 比 pip 更快,而是 conda 在管理 CUDA 相关依赖时不容易把系统搞乱。
先说版本搭配。如果电脑有 NVIDIA 显卡,你先别急着装 PyTorch,先打开终端输入nvidia-smi,看右上角的 CUDA Version。这个版本表示你的显卡驱动最多能支持到多少,不是说你必须装一样版本的 CUDA 工具包。然后去 PyTorch 官网选择对应的安装命令。举个例子,如果你看到显卡驱动支持 CUDA 12.1,那就选pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121,这套组合一般比较稳。如果看到的是 CUDA 11.8,就选cu118对应的命令。
常见的坑是:Windows 用户直接装了最新版 PyTorch,结果报一堆 DLL 加载失败,最后发现是显卡驱动太老。我的建议是先把驱动升级到官网最新的稳定版,再选一个相对成熟的 PyTorch 版本,比如 2.1 或 2.2,不要追最新。新版本特性再好,对入门项目没有实质影响,反而可能因为生态兼容问题耽误时间。
2.2 除了 PyTorch 还要装什么
车牌识别项目不只依赖 PyTorch,我把常用依赖清单列在这里,直接复制就能用:
torch、torchvision:核心训练库。opencv-python:图像读取、预处理、透视变换。numpy:数组操作,绕不开。matplotlib:画 loss 曲线和查看样本。tensorboard或wandb:训练过程可视化,建议装一个。lmdb或pillow:如果数据量大,用 LMDB 加速读取;数据少的话 pillow 就够。
还要提醒一个细节:opencv-python 和 opencv-contrib-python 不能同时装,否则会互相覆盖。国内网络下载慢的话,可以把 pip 源换成清华源,命令是pip install -i https://pypi.tuna.tsinghua.edu.cn/simple 包名,能快非常多。
2.3 CPU 模式下能不能跑这个项目
如果你没显卡,或者显卡是多年前的入门级,也不要直接放弃。车牌识别用的图像通常被缩放到高 32 或 48 像素,宽度 128 到 160 像素,数据量并不大。一个简单的 CRNN 模型在 CPU 上训练,如果数据集只有几千张,跑 30 到 50 个 epoch 也就是几十分钟到两三个小时的事。我用老款笔记本的 CPU 跑过一次,完全能接受。
但有几个操作在 CPU 上特别慢:一是数据增强里的随机仿射变换;二是 Batch Size 太大时矩阵计算和损失回传都会拖慢;三是验证阶段频繁做 CTC 解码。所以 CPU 训练时,数据增强可以简单一点,只做亮度扰动、轻微平移和裁剪,别上太重的仿射变换。Batch Size 调到 16 或 32,不要盲目模仿别人 GPU 环境下的 128。
3. 数据集:样本从哪来,标注怎么统一
3.1 开源数据集推荐与取舍
车牌识别的公开数据集不算多,最常用的几个:
- CCPD(中国城市停车场数据集):国内车牌,样本量大,包含多种复杂场景,标注信息藏在文件名里,需要解析。
- CRPD(中国道路车牌数据集):覆盖道路场景,倾斜和模糊样本较多,适合做泛化测试。
- 自己合成:用字体、背景、噪声、透视变换拼接出来的数据,虽然真实感差一些,但可以精确控制字符组合和分布,对起步阶段很有用。
CCPD 虽然好用,但它的标注格式比较特殊:文件名里编码了边界框、四个角点、车牌号等信息。比如类似025-95_113-154&383_386&473-386&473_177&454_154&383_363&402-0_0_22_27_27_33_16-37-15.jpg这种文件名,前一段表示车牌号和背景的简单特征,后面包含角点坐标和字符数。你需要写一个解析函数把这些字段抽出来。这个工作不复杂,但容易漏,特别是角点顺序和字符标签的对应关系,一旦搞错,训练出来的模型基本不能用。
3.2 标注格式统一化的小工具
无论你从哪个数据集拿数据,最后最好统一成我这种格式,省得每次训练都要改数据加载器。我习惯的做法是:生成一个train.txt,每一行左边是图片路径,右边是车牌字符串,中间用空格隔开:
data/images/0001.jpg 粤B12345 data/images/0002.jpg 京A88888然后在Dataset类里读取这个文件,按行解析。这样做的好处是换数据集时只需要写一次数据清洗脚本,把原始标注转成这种格式,训练代码完全不用改。
字符到索引的映射表也要提前定义好。中国车牌可能出现的字符包括 31 个省份简称汉字、24 个字母(I 和 O 一般不用,因为容易和数字 1、0 混淆)、10 个数字。最终字符表大概是 70 个左右,记得加一个blank符用于 CTC 损失。字符表一旦确定就不能中途乱改,否则之前训练好的模型权重会全部作废。
3.3 数据增广:车牌场景真正有用的操作
车牌识别任务的数据增广和 ImageNet 分类不太一样。你不能随便做翻转,车牌文字左右翻转以后没有任何意义;也不能做太夸张的旋转,超过 30 度基本就看不出原来的字了。我实验下来,最有效的增广操作有这么几个:
- 亮度扰动和对比度扰动:模拟白天晚上不同光照,对鲁棒性提升非常明显。
- 仿射变换里的轻度旋转、缩放、平移:模拟摄像头安装角度差异,角度控制在正负 10 度左右。
- 高斯噪声和运动模糊:模拟雨雪天气、车辆行驶中的拖影。
- 随机裁剪后再缩放:模拟车牌在画面中不完整或被遮挡的情况。
- 透视畸变:把矩形车牌压成不规则的四边形,模拟侧方拍摄。
这里有个小技巧:先把图像缩放到固定大小,再做增广,最后再 Resize 到网络输入尺寸。如果顺序反过来,先裁剪大图再缩放,容易引入不可控的尺度变化。我用的是albumentations库,因为它的处理速度比 torchvision 自带的 transform 快不少,而且各种空间变换的参数更直观。
增广强度也需要控制。我大概是这样设置的:
| 增广操作 | 概率 | 参数范围 |
|---|---|---|
| 亮度/对比度 | 0.5 | 亮度系数 0.8~1.2 |
| 旋转 | 0.3 | 角度 ±8 度 |
| 平移 | 0.3 | 水平 ±10%,垂直 ±5% |
| 高斯模糊 | 0.2 | 核大小 3 到 5 |
| 随机裁剪 | 0.2 | 裁剪后缩放回原尺寸 |
| 透视畸变 | 0.3 | 畸变幅度 0.02~0.05 |
这套配置在我的实验里效果不错。你可以先按这个跑,再根据自己数据的实际情况调整。
4. 模型实现:CRNN + CTC 的拆解
4.1 为什么用 CRNN 而不是 YOLO 直接读字符
看车牌识别,很多人第一反应是“用 YOLO 做检测,检测完字符再分类”。这个方案确实可行,但有一个工程问题:你需要先检测出每个字符的位置,再做字符分类。字符级检测的训练数据标注成本很高,而且相邻字符挨得近,小目标检测很容易漏检。
CRNN 的思路完全不同。它把整张车牌图看作一个序列,卷积层负责提取视觉特征,循环层负责建模字符之间的顺序关系,最后用 CTC 损失来对齐输入和输出,不需要精确知道每个字符的边界。这个思路和语音识别里的“序列到序列”非常像,对不定长文本特别友好。车牌本身是定长或近似定长的,但字符数有 7 个也有 8 个,用 CTC 可以天然规避“先分割再识别”易出错的毛病。
4.2 骨干网络用 ResNet18 还是自定义小网络
模型结构我建议不要一开始就上重型网络。车牌图像分辨率不高,字符细节也不复杂,一个轻量级 CNN 骨干就足够了。我试过 ResNet18 和一个自己定义的四层卷积网络,最后的准确率差距在 1 个百分点以内,但推理速度和显存占用差距明显。如果你后续打算部署到嵌入式设备,轻量骨干几乎是必须的。
我的推荐结构是这样的:
- 输入图像灰度化后缩放到 W x H = 128 x 32,不过也可以保留 RGB 三通道,看你对颜色的利用需求。如果用灰度图,参数量更小,训练更快。
- 先用几个卷积层提取特征,下采样到高度为 1 的特征图,比如把 32 的高度逐步降到 1,宽度保留在 32 或 16 左右。
- 把特征图按宽度方向展开成序列,送入两层双向 LSTM 或 GRU。
- 最后接一个全连接层,输出每个时刻在所有字符类别上的概率分布。
4.3 PyTorch 实现主干和 CTC 解码
核心代码骨架我直接贴一段,方便你照着自己的数据改造:
import torch import torch.nn as nn class LPRNet(nn.Module): def __init__(self, num_classes): super().__init__() # 输入: (batch, 3, 32, 128) 或 (batch, 1, 32, 128) self.conv1 = nn.Sequential( nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2) # 高度 16, 宽度 64 ) self.conv2 = nn.Sequential( nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2) # 高度 8, 宽度 32 ) self.conv3 = nn.Sequential( nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True) ) self.conv4 = nn.Sequential( nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=(2, 1), stride=(2, 1)) # 高度 4, 宽度 32 ) self.conv5 = nn.Sequential( nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(512), nn.ReLU(inplace=True) ) # 高度从 4 压到 1 self.conv6 = nn.Sequential( nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(512), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=(4, 1), stride=(4, 1)) # 高度 1, 宽度 32 ) self.gru = nn.GRU(512, 256, batch_first=True, bidirectional=True, num_layers=2) self.fc = nn.Linear(512, num_classes) # 双向 GRU 输出维度是 512 def forward(self, x): x = self.conv1(x) x = self.conv2(x) x = self.conv3(x) x = self.conv4(x) x = self.conv5(x) x = self.conv6(x) # 去掉高度维度,得到 (batch, 宽度, 512) x = x.squeeze(2) # 维度变为 (B, W, C) x = x.permute(0, 2, 1) # 调整顺序 x, _ = self.gru(x) x = self.fc(x) # 输出形状: (batch, width, num_classes) return x这个结构参考了 LPRNet 的思路,但我去掉了 NIN 那样复杂的 1x1 卷积,保持简单直接。forward返回的形状是(batch, time_step, num_classes),和 PyTorch 内置的CTCLoss期望的格式略有不同,训练时需要转置成(time_step, batch, num_classes):
logits = model(images) # (batch, width, num_classes) log_probs = logits.permute(1, 0, 2) # (width, batch, num_classes) loss = ctc_loss(log_probs, targets, input_lengths, target_lengths)这里的width就是时间步数,也就是 32。你可以把它理解为“从左往右扫描了 32 个位置,每个位置预测一个字符的概率分布”。
5. 训练调参过程中容易被忽视的细节
5.1 损失函数、优化器到底怎么选
CTC Loss 是必须的,PyTorch 里直接调用torch.nn.CTCLoss即可。它的核心作用是把“每一步输出一个概率分布”的序列,和“一个任意长度的目标字符串”对齐。语音识别用 CTC,车牌识别又用 CTC,原因都是文本长度和特征序列长度不等,且不知道每个字符对应的具体位置。CTC 把对齐问题通过动态规划隐式解决了,训练时你只需要提供输入序列长度和目标序列长度。
优化器我推荐AdamW,初始学习率设成1e-3左右。如果你发现训练初期 loss 下降慢,可以先调低学习率到3e-4试试,不要一上来就换优化器。SGD 配合动量在带 BatchNorm 的网络里也能跑,但对超参数敏感,入门期容易被学习率折腾得失去信心。AdamW 的权重衰减项设成1e-4,正则化效果比 L2 惩罚更稳。
5.2 Learning Rate、Batch Size、图像高度的连环影响
这是一个特别容易被忽略的组合问题。LSTM/GRU 这类循环网络对输入序列长度很敏感,而输入序列长度由特征图的宽度决定。图像宽度越大,时间步越多,模型表达能力越强,但训练时间越长,也越容易过拟合。图像高度同样有影响:高度太低,字符细节丢失,相似字符容易混淆;高度太高,卷积下采样后仍然保留大量无关背景,反而干扰序列建模。我实验下来,32 x 128是一个性价比很高的输入尺寸。
Batch Size 方面,CTC Loss 在 batch 维度上做平均,batch 太小时,梯度噪声大,训练不稳定;batch 太大时,显卡显存吃紧,LSTM 的序列长度又让反向传播变得很重。我的经验是 GPU 显存 6G 以上用 64,4G 左右用 32,CPU 训练用 16。如果你发现 loss 曲线像锯齿一样上下跳,先把 batch 调大一倍试试。
5.3 Loss 不下降时的排查顺序
我在训练过程里踩过不少坑,总结出一个固定排查顺序,遇到 loss 不降先别慌:
- 先看数据有没有问题。打印几个 batch 的图片和对应标签,确认图像灰度化、归一化是否正常,标签和图像是否对得上。标签错一位,模型永远学不会。
- 再看 loss 是否从“纯随机”的水平起步。如果一开始 loss 就很低,说明字符表可能配错了,或者目标都为空。
- 检查
input_lengths和target_lengths的维度。CTC Loss 对长度要求极其严格,搞错一个 batch 里的长度矩阵,loss 计算就会错位。 - 用一个小数据集(几百张)过拟合测试。如果一个 batch 重复训练 100 步后 loss 能降得很低,说明模型和数据加载没问题,问题在训练集太大或增广过强;如果小数据集都过拟合不了,那就是模型或优化器配置的问题。
- 看梯度是否异常。在训练循环里打印梯度的范数
torch.nn.utils.clip_grad_norm_(model.parameters(), 10),梯度爆炸基本就是学习率太大或 LSTM 层太深。
5.4 模型保存和验证阶段的正确姿势
模型保存不能只存参数字典,还要把字符表一起存下来。我习惯把整个训练配置打包成一个字典:
torch.save({ 'state_dict': model.state_dict(), 'char_dict': char_dict, 'input_height': 32, 'input_width': 128, 'epoch': epoch, 'val_acc': best_acc, }, 'checkpoint.pth')这样推理脚本读取后,能完整恢复模型,不需要额外手动指定字符表。
验证阶段不能只看 loss。CTC Loss 下降不代表每个字符都识别对了,必须写一个解码函数,把预测结果变成字符串后再算准确率。准确率可以按“整牌正确率”算,也可以按“字符正确率”算。对车牌识别来说,整牌正确率最严格,因为错一个字符在实际业务里就是“识别失败”。如果你的模型整牌正确率一直上不去,可以退一步看字符正确率,从而判断问题到底出在汉字上还是数字字母上。
6. 推理后处理:从字符序列变成一块正确车牌
6.1 CTC 解码头要注意的重复字符问题
训练完成后,推理时不是直接取概率最大的类别,而是要做一个“去重”操作。CTC 的解码规则是:先找到每个时间步上概率最大的字符,把连续重复的字符合并,再去掉 blank 符号。
举个例子,模型输出了一段序列:
粤 粤 B B 1 2 2 2 3 3 4 5 - 5 - 空 空 空解码时先把连续重复的合并,比如“粤 粤”合并成“粤”,“2 2 2”合并成“2”,“3 3”合并成“3”,但要注意:如果同一个字符中间隔着 blank,比如1 空 1,那它们不能被合并,要保留成两个“1”。
def ctc_decode(pred): pred = pred.argmax(dim=-1) # (batch, width) result = [] for batch_idx in range(pred.size(0)): char_indices = [] prev = None for t in range(pred.size(1)): idx = pred[batch_idx, t].item() if idx != blank_index and idx != prev: char_indices.append(idx) prev = idx plate = ''.join([int2char[idx] for idx in char_indices]) result.append(plate) return result这里最容易出的 bug 就是去重逻辑写错。普通的“相邻去重”在车牌识别里其实不够,因为合法的车牌号里确实会出现相同字符相邻的情况,比如“京A00001”里有连续的 0,TSP 里还要求“00”保留为两个 0。CTC 的合并规则只合并相邻且之间没有 blank 的相同字符,这一点实现时一定要仔细。
6.2 车牌规则约束帮你纠错
解码完毕后,还不能直接当成最终结果。车牌有很强的规则约束,比如中国大陆普通蓝牌是“省份简称 + 字母 + 5 位数字/字母”,新能源车牌是“省份简称 + 字母 + 6 位数字/字母”。这些规则能拿来纠错。
我常用的后处理逻辑是这样的:
- 第一位必须是汉字,如果不是,检查概率最高的几个候选里有没有汉字,替换上去。
- 第二位必须是字母,如果不是,看看是不是 0 和 O、1 和 I 的混淆,按车牌规则强制纠正。
- 后面几位只允许数字和大写字母,但要注意 I 和 O 在大多数车牌里不出现,把它们校正为 1 和 0。
- 总长度校验:普通蓝牌 7 位,新能源车牌 8 位。如果解码结果长度不对,可以结合模型输出的置信度重新选择候选序列。
置信度信息可以用log_softmax后取前 k 个候选来做 beam search。对车牌这种短序列,beam width 设 4 或 5 就够了,一个简单的 beam search 不会比 CTC greedy 慢多少,但整牌正确率通常能有 1 到 2 个百分点的提升。
6.3 实际场景里提升识别率的几个野路子
这里分享几个不正规但实测有效的办法,供参考。
第一个是投票法。同一个车牌往往能从视频流里抽到多帧,比如连续 5 帧都识别到同一块区域,最后把 5 次结果做个众数投票或按置信度加权合并。单帧识别率 95% 只能算及格,但 5 帧投票后基本能到 99%。这个思路特别适合停车场道闸这种场景,摄像头反正对着一个方向,多帧信息不用白不用。
第二个是模板校正。如果车牌底色已知,比如蓝底白字、黄底黑字、绿底黑字(新能源),可以在预处理时做颜色过滤,排除背景干扰。甚至可以直接根据底色在字符识别后校验省份汉字和字母组合的合法性。
第三个是边缘锐化。这个看起来有点“传统”,但在光照不足的场景里,轻微的cv2.filter2D锐化能让卷积网络提取到更清晰的边缘特征。注意锐化系数不要太大,否则图像会发白,反而丢失字符细节。
7. OV7670 和端侧部署的现实问题
7.1 OV7670 到底能不能和 PyTorch 联动
搜索热度里出现了 OV7670 摄像头模块,我猜很多人是单片机出身,想拿摄像头采集图像再喂给 PyTorch 做车牌识别。这里必须先泼一盆冷水:OV7670 是一个 30 万像素、8 位并口输出的老式摄像头模块,通常接 STM32、Arduino 这类 MCU。它采集到的原始数据是 Bayer 格式的 RAW 图,需要做色彩插值、格式转换、去噪、白平衡才能变成正常使用的 RGB 图。
而且 OV7670 模块通常不带 ISP(图像信号处理器),直接输出的图像质量比较差。车牌识别需要清晰、不过曝、不偏色的图像,如果拿 OV7670 的原始输出去做端侧识别,效果大概率不会好。更现实的做法是:
- 用 OV7670 做简单的图像采集和预览,数据通过串口或 WiFi 传到电脑。
- 在电脑上用 PyTorch 训练车牌识别模型。
- 将训练好的模型转换成 ONNX 或 TensorRT,部署在带 GPU 的开发板或边缘设备上。
所以 OV7670 本身不能称为“PyTorch 车牌识别”的主力设备。它更适合做入门级的图像采集实验,但要真正跑到识别那一步,最好换用带 ISP 的 USB 摄像头,一块树莓派摄像头或手机摄像头都比它省心。
7.2 端侧部署的常规思路
如果你真的想部署到嵌入式设备,我的建议是先跑通“采集 → 传输 → 推理”的完整闭环,再考虑模型压缩。
第一步可以考虑把 PyTorch 模型导出为 ONNX:
model.eval() dummy_input = torch.randn(1, 3, 32, 128, device='cuda') torch.onnx.export( model, dummy_input, 'lprnet.onnx', input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch'}, 'output': {0: 'batch'}}, opset_version=11 )然后可以换个平台推理,比如用 ONNX Runtime 在 ARM Linux 上跑。注意 LSTM 转 ONNX 时,有些算子在不同框架中兼容性不一致,如果导出报错,可以把双向 GRU 换成卷积或 Transformer 的自注意力模块,精度损失不大,但兼容性好很多。
部署时还有一个容易被忽视的问题:模型输入尺寸和摄像头输出尺寸不一致时,要固定一个统一预处理流程。在 PyTorch 训练时如果用了随机增广,推理时就不能再用随机操作,必须锁定到完全确定的预处理顺序:缩放、归一化、转张量、转通道顺序(从 HWC 转 CHW)。
8. 项目收尾:这一步落地后还能怎么延展
P10 周做到这儿,基础版车牌识别模型已经能跑通了。如果你的目标是做一个更完整的系统,接下来有几条路可以根据兴趣选一条继续往下挖。
第一条路是把“检测”补上。我已经在开头说了这次刻意绕开了检测,但应用端“检测 + 识别”才是完整方案。推荐你尝试用 SSD 或 YOLO 系列检测车牌区域,然后把检测结果裁剪出来,送入已有的识别模型。这是工程上最稳妥的衔接方式,不需要重新设计网络,只要把两个训练好的模型串起来就行。
第二条路是做成一个视频流实时识别程序。用 OpenCV 的VideoCapture读取摄像头或视频文件,每隔几帧抽一帧,做透视变换后送入模型,最后把识别结果叠加显示在画面上。这个过程中你会接触到推理延迟优化、画面稳定、多目标车牌追踪等更贴近真实项目的问题。
第三条路是往模型轻量化方向走。试一下把 ResNet18 换成 MobileNet 系列,或者用参数共享和剪枝减少模型体积。把模型从几十 MB 压到几 MB,推理速度能提升很多。有兴趣的还可以用 TensorRT 做 FP16 量化,对 GPU 推理加速特别明显。
我在完成这一周项目的实际感受是:车牌识别最大的门槛不在网络结构,而在数据处理和工程细节。一个能跑的分类网络到处都是,难的是把“识别出的字符串”变成“一块真实可用的正确车牌”,这中间涉及字符表的定义、CTC 解码的边界处理、车牌规则的约束纠错,以及和上下游模块的配合。如果你按这条链路完整走一遍,PyTorch 的掌握程度会比单纯刷十个分类项目扎实得多。
最后再分享一个小技巧:训练结束后,专门留出一批“难例”图——比如模糊的、倾斜的、曝光异常的、字符粘连严重的,单独统计模型在这些样本上的准确率。这个做法比只看整体准确率更能暴露出系统在实际环境里的短板,也是往工程化方向走时最有价值的一步。