Python车牌识别系统实战:从YOLO检测到CRNN识别的避坑指南 简介这是一套面向计算机相关专业学生的车牌识别系统大作业源码基于Python与深度学习实现适合正在准备课程设计、期末大作业或需要项目实战练习的学习者参考。项目已获98分评价可作为完整方案直接借鉴或二次开发。压缩包共18个文件约4.73MB包含5个py脚本、5个png与3个jpg图像素材、2个dat模型数据文件以及1个pptx演示文稿、1个md说明文档和1个gitignore配置覆盖代码、图片、模型与展示材料。其中py文件承担图像处理、字符识别与主流程调度dat文件为训练好的SVM模型pptx可用于答辩汇报。目前已有483人学习下载。读者可从中获取完整的车牌定位、字符分割与识别实现思路参考图像预处理与模型调用方式并借助演示文稿与说明文档快速理解项目结构适合作为大作业模板或深度学习入门实战案例。1. 从一份课程设计源码说起Python 车牌识别到底难在哪很多人拿到「计算机大作业设计源码基于Python车牌识别系统深度学习项目源码.zip」这类压缩包时第一反应是解压、装依赖、跑main.py然后发现摄像头打不开、模型权重缺失、识别结果全是乱码。我带过几届毕业设计也帮同事改过不少这类大作业血泪经验是车牌识别这个题目看着简单真正卡人的从来不是「深度学习」四个字而是数据从哪来、检测框怎么对齐、字符怎么切分、中文省份简称怎么映射这几件脏活。这套方案的核心链路是图像采集 → 车牌定位检测→ 字符分割 → 字符识别 → 结果输出。深度学习主要用在检测和识别两段常见做法是 YOLO 系列做检测、CRNN 或 CNN 做字符分类。它适合计算机、自动化、电子信息类的大作业和课程设计也适合想入门 CV 落地的新手——因为车牌识别有明确输入输出、有公开数据集、有可量化的准确率指标比「图像分类识别猫狗」更接近真实工程。下面我按能复现的顺序把这条链路拆开讲清楚。2. 环境与数据把源码跑起来前必须搞定的两件事2.1 Python 环境与依赖的版本对齐这类源码包最常见的翻车点就是版本。作者写代码时用的可能是 Python 3.8 PyTorch 1.10你本地是 Python 3.11 PyTorch 2.xtorch.load直接报权重不兼容。我一般会先建独立虚拟环境再按源码里的requirements.txt装没有这个文件就手动锁版本。# 创建并激活虚拟环境避免污染全局 Python python -m venv plate_env # Windows 用 plate_env\Scripts\activateLinux/macOS 用下面这行 source plate_env/bin/activate # 安装核心依赖版本按源码实际需求调整 pip install torch1.10.0 torchvision0.11.0 -f https://download.pytorch.org/whl/cpu pip install opencv-python4.5.5.64 pip install numpy1.21.6 pillow9.0.1逻辑说明venv隔离环境是为了防止你系统里已有的 numpy 版本和源码冲突torch的 CPU 版本足够跑推理大作业阶段没必要上 CUDA除非你要自己训练。参数上opencv-python建议锁在 4.5.x因为 4.8 之后部分cv2.findContours的返回值结构变了老代码会报「too many values to unpack」。装完先跑一句python -c import cv2, torch; print(cv2.__version__, torch.__version__)验证能打印出版本号再往下走。2.2 车牌数据集的来源与标注格式源码包里通常自带一个data/或dataset/目录但很多是空的或者只有几张示例图。真正要跑通你得有带标注的车牌数据。常见做法是用公开的中文车牌数据集CCPD 系列是绕不开的或者自己用手机拍几十张再标注。数据用途常见来源标注格式注意点检测训练CCPD、自采图YOLO txt类别 x y w h 归一化坐标必须归一化到 0-1识别训练裁剪后的车牌图文件名即标签如京A12345.jpg省份简称要统一编码测试验证从训练集划 10%-20%同训练格式别用训练图当测试图如果你只是复现推理、不打算重训那源码里的权重文件通常是.pt或.pth就是命根子。注意权重文件往往因为体积大被作者排除在压缩包外或者放在网盘里。拿到权重后先确认它的输入尺寸YOLOv5 常见是 640×640CRNN 常见是 32×100 或 32×320尺寸对不上识别结果会整体错位。3. 车牌检测用 YOLO 把车牌框出来的最小实现3.1 检测模型选型与推理代码检测这一环YOLOv5 是这类大作业里出现频率最高的原因是工程封装好、文档多、单文件推理简单。下面是一段可以直接抄的推理代码假设你已经有了训练好的plate_det.pt。import cv2 import torch # 加载本地权重map_location 保证在无 GPU 机器上也能跑 model torch.hub.load(ultralytics/yolov5, custom, pathweights/plate_det.pt, force_reloadFalse) model.conf 0.4 # 置信度阈值低于此值的框丢弃 model.iou 0.45 # NMS 交并比阈值控制重叠框合并 img cv2.imread(test_car.jpg) results model(img) # 推理返回检测结果对象 boxes results.xyxy[0] # 格式 [x1, y1, x2, y2, conf, cls] for box in boxes: x1, y1, x2, y2, conf, cls box.tolist() # 按框裁剪车牌区域留 2 像素余量防止切掉边缘字符 plate img[int(y1)-2:int(y2)2, int(x1)-2:int(x2)2] cv2.imwrite(plate_crop.jpg, plate)逻辑说明torch.hub.load会去拉 YOLOv5 的仓库代码第一次运行需要联网如果环境不能联网就把 yolov5 源码整个拷进项目再改成本地加载。conf和iou是两个必调参数——conf调低会多出误检框调高会漏掉模糊车牌iou调低会让相邻字符框被合并。裁剪时留余量是因为检测框有时贴字符太紧后续分割会把边缘字符切掉一半。3.2 检测框后处理倾斜校正与边界处理真实拍摄的车牌很少是水平的尤其是手机随手拍倾斜角度可能到 15 度以上。不校正直接送进识别字符分割会崩。常见做法是用cv2.minAreaRect拿到最小外接矩形再做仿射变换。import numpy as np def crop_plate(img, box): x1, y1, x2, y2 map(int, box[:4]) plate img[y1:y2, x1:x2] gray cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY) # 二值化后找轮廓用最小外接矩形估计倾斜角 _, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) coords np.column_stack(np.where(thresh 0)) angle cv2.minAreaRect(coords)[-1] if angle -45: angle 90 angle # 按角度旋转校正 h, w plate.shape[:2] M cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) return cv2.warpAffine(plate, M, (w, h), borderValue(255, 255, 255))逻辑说明cv2.threshold用 OTSU 自动找阈值比手动设 127 稳。minAreaRect返回的角度范围是 -90 到 0小于 -45 时要加 90 修正否则会转反。borderValue设白色是因为车牌底色多为蓝或绿旋转后补白边对后续分割干扰最小。这一步的参数没有固定值角度阈值我一般设 3 度以内不校正超过才转避免小角度反复插值导致图像模糊。4. 字符识别CRNN 与 CNN 两条路怎么选4.1 CRNN 序列识别适合不定长车牌新能源车牌是 8 位普通车牌 7 位如果用固定输出的 CNN 分类就得为两种长度各训一个模型。CRNN 把识别当成序列问题输入整张车牌图输出字符序列天然支持变长。下面是 CRNN 推理的核心片段。import torch.nn as nn class CRNN(nn.Module): def __init__(self, num_classes): super().__init__() # CNN 提特征输入 32x100 灰度图 self.cnn nn.Sequential( nn.Conv2d(1, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), ) # RNN 处理序列双向 LSTM 捕捉上下文 self.rnn nn.LSTM(128, 256, bidirectionalTrue, batch_firstTrue) self.fc nn.Linear(512, num_classes) def forward(self, x): feat self.cnn(x) # [B, 128, H/4, W/4] feat feat.mean(dim2) # 沿高度压缩得到序列 feat feat.permute(0, 2, 1) # [B, W/4, 128] out, _ self.rnn(feat) return self.fc(out) # [B, T, num_classes]逻辑说明mean(dim2)是把特征图高度方向取平均把二维特征压成一维序列这是 CRNN 的经典做法。bidirectionalTrue让每个时间步同时看到前后文对「京A」这种前后关联强的字符提升明显。num_classes要包含省份简称、字母、数字和 CTC 的 blank 占位符常见是 65 类左右。解码时用 CTC greedy 或 beam searchgreedy 够用beam search 在模糊图上更稳但慢。4.2 CNN 分类识别简单但要做字符分割如果源码用的是 CNN 分类那流程里一定多一步字符分割。分割质量直接决定识别上限。常见做法是投影法对二值化后的车牌图做垂直投影找波谷切分。def split_chars(binary_plate): # 垂直投影统计每列白色像素数 proj np.sum(binary_plate, axis0) / 255 chars, start [], None for i, v in enumerate(proj): if v 0 and start is None: start i elif v 0 and start is not None: # 宽度小于 5 像素的段视为噪点丢弃 if i - start 5: chars.append(binary_plate[:, start:i]) start None return chars逻辑说明proj是每列白点数量字符所在列白点多字符间隙列接近 0。i - start 5这个阈值是过滤噪点的太小会把「1」这种窄字符也丢掉太大则会把「川」这种多竖线字符切碎。分割后每个字符图要 resize 到统一尺寸常见 20×20再送 CNN。这条路的好处是模型小、训练快坏处是分割一崩全崩车牌有污损或铆钉遮挡时基本没救。5. 避坑与排查那些让识别率从 95% 掉到 60% 的细节5.1 现象识别结果全是「京A」或固定几个字符原因CTC 解码时 blank 占比过高模型没学到有效特征或者标签编码时把 blank 放在了索引 0 但训练时没对齐。解决检查标签映射表确认 blank 索引和训练时一致用几张训练集图片先验证模型在训练数据上的输出如果训练集都错说明是训练流程问题不是推理问题。5.2 现象检测框位置对但裁剪出来的图是黑的原因OpenCV 读图是 BGR而模型训练时用的是 RGB通道顺序反了导致颜色异常或者img[y1:y2, x1:x2]里 y 和 x 写反。解决统一在推理前做cv2.cvtColor(img, cv2.COLOR_BGR2RGB)并打印裁剪图的 shape 确认宽高没颠倒。5.3 现象中文省份简称识别成乱码或问号原因字体文件缺失或编码表没加载。很多源码用 PIL 在图上画识别结果如果系统没有中文字体ImageFont.truetype会回退到默认字体中文全变方框。解决显式指定字体路径Linux 下常见是/usr/share/fonts/truetype/wqy/wqy-zenhei.ttcWindows 下用C:/Windows/Fonts/simhei.ttf。5.4 现象同一张图多次运行结果不一致原因模型没有设eval()模式Dropout 和 BatchNorm 在推理时仍在随机或更新统计量。解决推理前加model.eval()并用with torch.no_grad():包住前向传播既稳定结果又省显存。5.5 现象新能源绿牌识别率明显低于蓝牌原因训练集里蓝牌占绝大多数绿牌样本少模型对绿牌底色和 8 位字符长度都不适应。解决在数据增强里对绿牌做过采样或者单独微调一个绿牌分支推理时对绿牌适当降低检测置信度阈值因为绿牌底色和车身对比度有时更低。6. 把准确率再往上推一个可验证的调优习惯大作业做到能跑通只是及格线真正拉开差距的是你有没有一套可量化的验证方法。我自己的习惯是先固定一个 50 张图的测试集每改一个参数就跑一遍记录准确率而不是凭感觉说「好像变好了」。具体做法是写一个批量测试脚本把测试集图片路径和真实车牌号写成两列跑完自动比对并输出混淆矩阵。import os from collections import defaultdict def batch_eval(model, test_dir, label_file): correct, total 0, 0 conf_mat defaultdict(int) with open(label_file, r, encodingutf-8) as f: lines [l.strip().split(,) for l in f if l.strip()] for img_name, true_label in lines: img_path os.path.join(test_dir, img_name) pred_label predict_single(model, img_path) # 你的单图推理函数 total 1 if pred_label true_label: correct 1 else: # 记录错在哪一类方便定位是检测问题还是识别问题 conf_mat[(true_label[:1], pred_label[:1])] 1 print(f准确率: {correct/total:.2%}) return conf_mat逻辑说明conf_mat按省份简称统计错误分布如果错误集中在某几个省份说明是字符识别问题如果错误分散且预测结果长度都不对说明是检测或分割问题。这个脚本的价值在于把「玄学调参」变成「有数据支撑的决策」。参数上测试集至少 50 张少于这个数波动太大标签文件用 UTF-8 编码避免中文乱码。调优顺序我一般按这个优先级先保证检测框 IoU 在 0.8 以上再调识别模型输入尺寸最后才动学习率和数据增强。因为检测错了识别再准也没用。另外别忽视图像预处理——很多翻车案例是输入图分辨率太高直接 resize 到模型输入尺寸后车牌区域只剩几十像素字符糊成一团。常见做法是先按检测框裁剪再单独对车牌区域做超分或至少保持原始分辨率送识别。最后说个我踩过的坑有次为了刷高准确率把测试集图片也放进训练集跑了一遍结果报告里 99% 的准确率答辩时老师随手拍了一张新图直接识别成乱码。从那以后我养成了一个习惯——测试集单独放一个文件夹训练脚本里硬编码排除这个路径宁可准确率数字难看也要保证它是真的。希望帮到你。本文还有配套的精品资源点击获取