
简介这是一份基于Python与卷积神经网络CNN的车牌识别完整项目资源包适合具备一定Python基础和深度学习概念的开发者可用于智能交通、停车场管理等场景的自动车牌识别实践。包内共25个文件涵盖Python源码、图片样本、模型权重数据dat、说明文档md及二次打包的原始项目zip/7z等类型压缩包整体约29.2MB结构上便于对照目录运行与二次开发。已有206人浏览学习印证了其对于入门计算机视觉任务有一定参考价值。通过该包读者可以获取完整的License-Plate-Recognition项目目录结合描述中的知识点从图像预处理、CNN模型搭建到Keras/TensorFlow训练与评估逐步复现一个典型车牌识别流程同时内置的图片与数据文件便于直接测试也可作为课程设计或毕业设计的实现蓝本。整体上这份资源为学习深度学习实战提供了可操作的完整样例。1. 车牌识别的核心链路为什么PythonCNN成为首选方案夜晚的地库出口摄像头抓到的车牌反光严重识别引擎先读出“苏A·3K6T2”再拍一次又变成“浙A·3K6T2”。这种省份汉字错的瞬间正是车牌识别项目里最常见的翻车现场。基于Python-CNN的车牌识别就是把整条链路做成用Python组织图像数据用CNN卷积神经网络自动提取字符特征并分类最后输出标准车牌字符串。标题里的.zip通常是带数据集的源码包解压后一般包括车牌图片、字符标注和训练脚本三件套。这个方向适合两类人想在本地跑通完整流程的算法入门者以及准备把识别能力放进道闸、门禁、停车系统的工程人员。下面从数据组织讲起一路到模型训练、避坑和部署验证。2. 数据准备把车牌图片整理成CNN能吃的样子不管是压缩包里自带的图片还是自己采集的现场照片第一件事永远是让数据目录变成torchvision直接认识的格式。这一步做不好后面模型写得再漂亮loss也降不下去而且很难定位是数据问题还是模型问题。2.1 三种常见标注格式与推荐目录结构做车牌识别数据集常见的标注组织方式有三种。我按实际使用频率整理了一个对比表标注方式存放形式适合模型主要踩坑点整图 车牌框XML/JSON 存四点坐标YOLO、SSD 等检测器需要写解析代码坐标与图片不对齐直接崩字符级标注每个字符一个框 标签字符分类、CRNN裁剪时留白过少字符边缘被切Imagenet式目录按字符类别分文件夹字符分类最省事类别名与真实字符对不上如果你打算走“先检测车牌区域再对字符做分类”这条经典路线推荐用第三种也就是ImageFolder格式。目录结构长这样datasets/char/train/ 京/10001.png 苏/10002.png A/10003.png 0/10004.pngtorchvision的ImageFolder会直接把每个子文件夹名称当成类别名读取时自动生成整数标签省掉手写标签映射表的步骤。需要注意车牌字符类别是“汉字省份 大写字母 数字”混在一起文件名的前缀没意义真正重要的是上一级文件夹名。如果压缩包里给的是XML或JSON格式就得先写一次性转换脚本。常见做法是解析出每个字符的bounding box按x坐标从左到右排序再按“省份汉字、字母、数字”分别归档。这个转换脚本只跑一次但务必抽时间做一次人工抽检因为标注框偏移是车牌数据集里最高发的低级错误。2.2 预处理管线灰度化、resize、归一化与增强车牌字符识别和通用图像分类不太一样输入往往是已经裁切好的车牌区域或单个字符图。字符本身是印刷体颜色信息并不是分类的关键相比之下边缘、纹理、结构才是关键。实际项目里灰度化可以砍掉约三分之二的通道计算量同时减少绿牌和蓝牌底色对特征提取的干扰。import cv2 import numpy as np def preprocess_plate(plate_bgr, size(128, 32)): gray cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2GRAY) gray cv2.resize(gray, size, interpolationcv2.INTER_AREA) gray cv2.equalizeHist(gray) img gray.astype(np.float32) / 255.0 return img这段代码看起来简单但参数设计有讲究。size(128, 32)对应车牌区域的宽高比普通燃油车牌标准尺寸约为440x140长宽比接近3.2:1按这个比例缩放可以避免字符被横向或纵向拉变形。INTER_AREA适合缩小操作能减少摩尔纹如果用INTER_LINEAR缩小后字符边缘容易出现毛刺影响后续卷积核提取边缘特征。equalizeHist做直方图均衡化是为了应付逆光和阴影场景让字符和背景的对比度拉大。最后转成float32并除以255把像素范围压缩到0到1之间这个值域对CNN的收敛稳定性至关重要。如果你决定用RGB输入而不是灰度图归一化就不能直接用/255.0更规范的做法是使用transforms.Normalize配合ImageNet的mean和std。这里有个容易被忽略的点车牌字符图不是自然照片ImageNet统计出来的均值未必最优但作为起点足够实测对收敛速度影响不大。2.3 数据集划分与类别对齐脚本车牌数据集的另一个坑是类别极度不均衡。省份汉字一共31个但每个省的样本量差距可以到10倍以上数字和字母的样本通常比汉字多很多。划分数据集时如果直接用train_test_split整图切很容易出现某个省份汉字在训练集里只有不到50张验证集里却占了大头导致验证指标剧烈震荡。import os import random import shutil from pathlib import Path root Path(datasets/char_all) train_dir Path(datasets/char/train) val_dir Path(datasets/char/val) for cls_dir in root.iterdir(): if not cls_dir.is_dir(): continue imgs list(cls_dir.glob(*.png)) list(cls_dir.glob(*.jpg)) random.shuffle(imgs) val_n max(1, int(len(imgs) * 0.2)) for img in imgs[:val_n]: shutil.copy(str(img), str(val_dir / cls_dir.name / img.name)) for img in imgs[val_n:]: shutil.copy(str(img), str(train_dir / cls_dir.name / img.name))这段脚本有几个容易被忽略的设计点。random.shuffle(imgs)一定要在按类别划分之前做因为摄像头采集的数据常常按时间批次排列直接切前20%会切出一个全是白天或全是阴天的验证集。按类别目录分别取20%保证了每个字符类别在训练集和验证集里的相对比例一致。最后用shutil.copy而不是os.rename因为原始数据往往是反复实验的唯一资产复制一份出来随便折腾出问题还有后悔药。如果每个字符类别的样本总量不足100张按20%划分会进一步饿死训练集。此时可以改成每类固定留5到8张做验证其余全部进训练集反正车牌字符是印刷体类别内差异很小少量验证样本足够评估模型好坏。3. CNN模型构建从卷积层到车牌字符分类器数据准备好之后接下来是模型选择。很多人上来就找大模型但车牌字符识别这个任务有它的特殊性类别固定字符结构相对简单成像质量可控。网络不需要特别深关键是结构对路、训练管线稳定。3.1 为什么弃用OpenCV模板匹配在CNN普及之前车牌字符识别的主流方案是模板匹配。把标准字符图像做归一化后和模板逐像素比对实现简单在字体统一、光照稳定的条件下准确率可以做到95%以上。但实际道闸场景里车牌区域会旋转、模糊、过曝模板匹配对尺度变化和仿射变换非常敏感稍微歪一点匹配分就急剧下降。工程师只能不断扩充模板库把不同角度、不同大小的模板都塞进去最后模板数量膨胀到几千张维护成本极高。CNN的思路不一样。卷积核学到的不是某个固定像素模板而是“边缘-纹理-局部结构”的层级组合。同一个汉字“苏”在标准字体下被拉伸、压缩、轻微模糊卷积特征依然能保持稳定这正是车牌识别这类印刷体任务最需要的泛化能力。深度学习常被当成黑匣子但车牌字符识别恰恰是少数能解释的CNN任务它本质上是一个小规模分类问题类别数只有70个左右不需要上千类的ImageNet式复杂结构。3.2 一个可直接落地的字符分类CNN结构针对车牌字符我一般会用一个比LeNet稍宽、比ResNet浅得多的结构。输入单张字符图输出类别概率向量。import torch.nn as nn class PlateCharCNN(nn.Module): def __init__(self, num_classes70): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.3), nn.Linear(128, 256), nn.ReLU(inplaceTrue), nn.Linear(256, num_classes), ) def forward(self, x): x self.features(x) x self.classifier(x) return x这里的卷积核尺寸统一用3x3padding1保证特征图宽高不变只有池化层做降采样。如果输入是64x64的字符图经过三层MaxPool后特征图变成8x8x128再用AdaptiveAvgPool2d(1)压成1x1x128。这个设计的关键是AdaptiveAvgPool2d让网络对输入尺寸不敏感部署阶段想从64x64换成32x128的长条形字符图也能跑只是精度需要重新验证。num_classes在车牌场景通常是70左右计算公式是31个省份汉字 24个字母I和O不使用 10个数字不同数据集可能略有差异以实际字符类别数为准。Dropout(0.3)放在全连接层前小数据集上能明显抑制过拟合。3.3 训练循环与损失函数骨架分类任务的损失函数直接用交叉熵不需要自己实现softmaxPyTorch的CrossEntropyLoss内部已经做了log_softmax。训练循环的骨架如下import torch from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder from torchvision import transforms transform transforms.Compose([ transforms.Resize((64, 64)), transforms.ToTensor(), transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)), ]) train_set ImageFolder(datasets/char/train, transformtransform) val_set ImageFolder(datasets/char/val, transformtransform) train_loader DataLoader(train_set, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_set, batch_size128, shuffleFalse, num_workers2) model PlateCharCNN(num_classeslen(train_set.classes)) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion torch.nn.CrossEntropyLoss() for epoch in range(30): model.train() running_loss 0.0 for x, y in train_loader: optimizer.zero_grad() out model(x) loss criterion(out, y) loss.backward() optimizer.step() running_loss loss.item() * x.size(0) print(fepoch {epoch 1}, loss {running_loss / len(train_set):.4f})参数里最值得关注的是batch_size64和lr1e-3。batch_size选64在普通消费级显卡上即可跑显存小于4GB就降到32学习率1e-3是Adam在小型分类任务里比较稳妥的起点不建议一开始就用1e-2字符分类这种任务在样本量不大时很容易出现loss震荡甚至NaN。num_workers4在Linux下很顺手Windows下如果启动训练时报DataLoader worker错误直接改成0性能差距可以接受。整个训练循环里epoch数量30只是一个初步参考真实项目里应该以验证集指标为准决定是否提前停止。4. 训练与调参让车牌模型真正收敛的三个关键设定模型结构只是骨架真正让字符分类器在实际场景里跑出高精度的是训练过程中的细节设定。下面这三个参数和策略是我在日常项目中几乎每次都要调整的内容。4.1 学习率与batch_size的配合Adam自带自适应学习率但并不意味着全程不动。在车牌字符这种样本量几千到几万的小任务里Adam在训练后期经常出现loss停在0.3到0.5之间不降的“平台期”。常见做法是配合StepLR每隔若干轮把学习率降一档让参数在更小步长下精调。scheduler torch.optim.lr_scheduler.StepLR( optimizer, step_size8, gamma0.5 ) for epoch in range(30): # 训练代码同上 if epoch % 5 0: current_lr optimizer.param_groups[0][lr] print(flr: {current_lr:.1e}) scheduler.step()step_size8表示每8个epoch调整一次学习率gamma0.5表示学习率乘以0.5。这个衰减节奏对车牌字符识别是比较稳的起点前8个epoch用1e-3快速下降第9到16个epoch用5e-4精调最后阶段用更低学习率逼近最优值。如果训练集只有一两百张图学习率衰减可以提早点比如step_size5。还有一个容易被忽略的点在PyTorch里scheduler.step()要放在每个epoch结束后不能放在每个batch里否则学习率会被过早衰减。4.2 用验证集曲线判断欠拟合还是数据错训练日志里只打印训练集loss是个坏习惯。训练loss持续下降验证loss先降后升这是典型的过拟合信号训练loss和验证loss都纹丝不动那问题大概率不在模型而在数据本身——标签错乱、图片方向不对、类别文件夹里混入了无关图片。验证环节的代码要单独写不能用训练阶段的计算图。model.eval() val_loss 0.0 correct 0 total 0 with torch.no_grad(): for x, y in val_loader: out model(x) val_loss criterion(out, y).item() * x.size(0) pred out.argmax(dim1) correct (pred y).sum().item() total y.size(0) print(fval loss {val_loss / total:.4f}, acc {correct / total:.4f})注意几个细节。model.eval()必须显式调用它会关闭Dropout和BatchNorm的训练行为否则验证结果会有随机波动容易被误判成模型不稳定。torch.no_grad()包裹验证循环避免不必要的梯度计算这一步在验证集较大时能省下不少显存和耗时。out.argmax(dim1)是取每个样本概率最大的类别索引(pred y).sum().item()统计正确数量最后除以total得到准确率。如果训练集loss从第1轮的2.5左右快速降到0.2但验证集loss在0.8左右徘徊多半不是模型能力不够而是光照、角度和训练集差别太大。此时第一反应不是换更大的模型而是回去检查预处理是否一致训练时用的是灰度图验证时如果忘了做同样的直方图均衡指标就会失真。4.3 保存best模型的时机与检查点脚本工程落地的习惯和实验不同实验可以只看最后的acc工程必须随时能回到历史最佳状态。车牌数据集小训练中途经常会出现验证集acc在某个epoch冲到97.8%后面又掉回96.5%的情况。如果等到30轮结束再保存模型拿到的可能不是历史最优。best_acc 0.0 checkpoint_dir checkpoints os.makedirs(checkpoint_dir, exist_okTrue) for epoch in range(30): # 训练与验证代码 if val_acc best_acc: best_acc val_acc torch.save({ epoch: epoch 1, model_state: model.state_dict(), optimizer_state: optimizer.state_dict(), best_acc: best_acc, }, os.path.join(checkpoint_dir, plate_char_best.pt))这段checkpoint策略和只保存model.state_dict()的区别在于把optimizer_state一起存进去意味着即使训练中断也能用torch.load恢复出完全一致的优化器状态继续跑完剩下的epoch而不是从头再来。best_acc也存进去方便回看这个权重到底是什么时候产生的。加载模型做推理时只需要取checkpoint[model_state]而断点续训时需要同时恢复optimizer和scheduler状态。这里有个有经验的工程师都知道的细节保存的model_state在加载时要先model.load_state_dict(checkpoint[model_state])如果后续改了网络结构比如从70类改成65类会报key不匹配。这时不要硬加载先打印state_dict的key核对分类层名称再做strictFalse加载或用迁移学习的方式跳过最后一层。5. 车牌识别避坑指南5个让模型翻车的常见问题训练代码写得再顺真正到了自己的数据集上还是会遇到各种预料之外的状况。下面这些坑我基本都踩过一遍按“现象、原因、解决”整理出来希望能帮你省掉几个晚上的排查时间。5.1 省份汉字识别率上不去先查标注对齐现象模型在字母和数字上准确率能到99%但省份汉字只有75%而且错得极其集中比如“苏”和“浙”互认、“鲁”和“冀”互认。原因汉字结构相似是客观因素更常见的原因是数据集里汉字图片的裁剪框本身就偏了。字符级标注如果裁剪太紧把“苏”字的左边一笔切掉一部分卷积特征就会偏向“浙”的边缘结构另外汉字类别样本量往往只有数字的十分之一模型天然对高频类别有偏好。解决先检查标注。裁切字符时保留外圈5到8像素的margin别把字符边界卡得太死。然后单独统计汉字类别的样本量不足300张的要做针对性增强比如小角度旋转、随机亮度扰动、轻微腐蚀膨胀。最后用混淆矩阵定位具体是哪些汉字对在互相打架这个操作在sklearn.metrics里直接用confusion_matrix和classification_report就能出结果不用自己写循环。如果错的是“苏”和“浙”说明模型学到的特征偏向笔画密度分布而不是结构差异可以在预处理里加强直方图均衡把对比度差异放大。5.2 新能源绿牌与蓝牌字符混淆现象训练时模型在蓝牌字符上acc 98%换到新能源绿牌停车场数据上直接掉到85%而且看不出明显的错误规律感觉随机乱猜。原因绿牌的渐变底色和字符之间的对比度远低于蓝牌残留的绿色背景作为有效信息进入网络后卷积核把底色的统计特征也学进去了。蓝牌训练的模型遇到绿色背景时特征分布完全不一样输出自然不可靠。解决最直接的做法是把输入统一转成灰度图色彩干扰消除大半字符轮廓信息完整保留。如果确实验证集里绿牌仍然偏低就把绿色通道的像素值做一次统计用cv2.inRange把绿色区域置白或置黑后再送识别器。注意这时候就不要再用ImageNet的mean和std做归一化了因为输入已经不是自然图像的RGB分布直接用img / 255.0更合理。5.3 中文路径读写导致OpenCV和Torch直接崩溃现象项目目录在Windows下取了个“车牌识别数据集”的中文名plt.imread读出来的数组是None或者Dataset直接报错说图片找不到但文件明明就在那里。原因OpenCV的imread在Windows下对非ASCII路径支持不好底层走的是C文件接口中文路径转码后找不到文件。这个问题在Linux上不存在所以很多代码在服务器上跑得好好的一拿到Windows办公电脑上就跑不动。解决优先方案是整个项目路径全英文包括用户名目录这是最省心的做法。如果数据集已经在中文目录里可以绕开cv2.imread改用np.fromfile读取二进制再用cv2.imdecode解码import cv2 import numpy as np def imread_chinese(path): data np.fromfile(path, dtypenp.uint8) img cv2.imdecode(data, cv2.IMREAD_COLOR) return img这段代码的原理是绕开文件扩展名解析先以二进制方式把整个文件读进内存再交给cv2.imdecode按图像编码格式解码。实际使用时要检查返回值是否是None因为即使走这个流程文件本身损坏或路径拼错依然会解码失败。5.4 检测环节把广告牌、车贴当车牌现象整图识别时模型对着车身贴的“极速驰骋”四个大字输出了一个“车牌”道闸的屏幕上出现了四个完全不相关的字。原因概览式识别流程里检测阶段的置信度阈值设得太低字符分类器本身对“任意文字序列”没有拒绝能力任何文字区域送进去都会给出一个类别分布拼接起来就是一个假车牌。解决检测端提高置信度阈值到0.5以上同时对候选区域做尺寸过滤。车牌区域的长宽比一般在2.8到3.4之间面积占整个画面的比例有合理范围。识别结果出来后再加一道规则校验不符合车牌结构规则的结果直接丢弃触发重新识别import re def validate_plate(text): pattern r^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{5}[A-HJ-NP-Z0-9挂学警港澳]$ return bool(re.match(pattern, text))这个正则里第一位是省份汉字白名单第二位是城市发牌机关字母后面是5位字符组合最后一位允许“挂、学、警、港澳”等特殊牌照后缀。不要小看这道后处理它能拦下一大半广告牌和车贴造成的误识别。实际项目中规则校验放在所有识别步骤的最后一旦校验失败系统可以选择重新抓帧再识别一次而不是把错误结果直接传给道闸控制器。6. 从字符分类到端到端识别一个值得试的验证与部署技巧字符分类器只是中间产物真正落地成车牌识别系统还需要把“定位车牌区域”和“识别字符”两件事串起来。有一个成本很低的验证技巧先用OpenCV的传统图像处理做车牌定位再用训练好的CNN字符分类器识别整个过程不需要额外训练检测网络适合快速验证模型是否真实可用。import cv2 def locate_plate_candidates(img_gray): blurred cv2.GaussianBlur(img_gray, (5, 5), 0) edges cv2.Canny(blurred, 50, 150) contours, _ cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) ratio w / h area w * h if 2.5 ratio 4.5 and area 3000: candidates.append((x, y, w, h)) return candidates这段代码用了GaussianBlur降噪、Canny边缘提取、轮廓外接矩形过滤三步。ratio的区间2.5到4.5覆盖了蓝牌、绿牌和部分黄牌的长宽比area 3000把太小的噪点轮廓直接淘汰。这套传统定位在光线均匀的园区出入口准确率尚可但遇到强逆光、雨雾天气就会打折扣所以它更适合作为本地验证手段真正要商用还是得训练一个车牌检测网络。实际部署时还有一个非常管用的技巧对识别结果做多帧投票。道闸摄像头的视频流里同一辆车会连续出现多帧把连续5帧的识别结果做字典计数只采纳出现次数最多的那个字符串。这个操作不需要任何额外模型就能把偶发的单帧误识别压下去配合正则校验效果更好。我自己跑车牌识别项目时最常被坑的其实不是模型结构而是数据集里某个汉字文件夹混进了从网上下载的、带有水印的图片模型在验证集上看着很高上线第一天就现出原形。所以我现在每次跑通训练后第一件事是拿现场实拍的车牌图做批测而不是盲信验证集acc。这个习惯救了我不止一次希望帮到你。本文还有配套的精品资源点击获取