Python深度学习车牌识别系统毕设实战:从CNN模型到Flask部署
简介这份资源是面向计算机相关专业学生与深度学习入门者的毕业设计论文文档主题为基于Python的深度学习车牌识别系统设计与实现可帮助读者理解如何将卷积神经网络、递归神经网络等技术应用于车牌图像的特征提取、分类与识别并兼顾智能交通、智能停车场等实际场景需求。压缩包内共1个docx文件约1.14MB内容为完整论文正文涵盖绪论、研究背景与意义、系统设计与实现等章节结构完整便于直接参考写作框架与实验思路。目前已有881人学习下载适合需要完成同类课题、准备毕业设计或希望快速了解车牌识别系统整体方案与开发流程的读者可从中获取选题论证、技术选型与系统设计等参考内容。1. 从一份毕设文档说起Python 深度学习车牌识别系统到底在做什么很多同学拿到“基于 Python 的深度学习车牌识别系统”这个题目时第一反应是去搜现成的开源项目结果发现要么是纯 OpenCV 模板匹配的老方案要么是依赖一堆私有 SDK 的工业级代码根本跑不起来。这份毕设文档的价值在于它把系统边界划得很清楚前端上传车牌图片后端用深度学习模型做检测与识别结果落库到 MySQL再通过统计分析、车牌地图、车牌管理几个页面把数据用起来。整套东西不追求工业级精度但链路完整适合用来理解“一个车牌识别系统从图像输入到业务展示”的全过程。它解决的核心问题是把传统人工录入车牌的方式替换成自动识别同时把识别结果结构化存储支撑后续的查询、统计和可视化。适合谁正在做计算机相关毕设的本科生、想入门深度学习落地场景的开发者、以及需要快速搭一个车牌识别 Demo 验证业务可行性的人。文档里提到的技术栈是 Python 深度学习 MySQL功能模块包括登录、后台、车牌识别、统计分析、车牌地图、车牌管理这些就是后续所有实现的骨架。2. 车牌识别链路拆解从图像预处理到 CNN 字符分类车牌识别不是“丢一张图给模型就出结果”这么简单。工业上通常拆成四步车牌检测、车牌矫正、字符分割、字符识别。这份毕设文档虽然没把每一步写得很细但从它提到的“卷积神经网络”和“递归神经网络”来看设计者的意图是用 CNN 做特征提取用 RNN 或 CTC 做序列识别。下面按可复现的思路把链路讲清楚。2.1 图像预处理与车牌定位的工程做法实际拿到一张车辆照片背景复杂、光照不均、车牌倾斜直接送进模型效果很差。常见做法是先做灰度化、高斯模糊、边缘检测再用形态学操作把车牌区域连成块最后用轮廓筛选出候选区域。OpenCV 是这一步的主力工具Python 环境下安装opencv-python即可。import cv2 import numpy as np def locate_plate(image_path): # 读取图像并转灰度 img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊去噪核大小根据图像分辨率调整 blur cv2.GaussianBlur(gray, (5, 5), 0) # Sobel 算子提取垂直边缘车牌字符垂直特征明显 sobel cv2.Sobel(blur, cv2.CV_8U, 1, 0, ksize3) # 二值化阈值需要根据实际光照微调 _, binary cv2.threshold(sobel, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 闭运算连接字符区域 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 查找轮廓并按面积筛选 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) ratio w / float(h) # 中国车牌宽高比大致在 2.5 到 5 之间 if 2.5 ratio 5 and w 80: candidates.append((x, y, w, h)) return candidates这段代码的逻辑是灰度化降低计算量高斯模糊抑制噪声Sobel 提取垂直边缘是因为车牌字符的垂直笔画丰富OTSU 自动阈值避免手动调参闭运算把断开的字符连成一个矩形块最后用宽高比和最小宽度过滤掉明显不是车牌的轮廓。参数方面高斯核(5,5)适合 720P 左右的图片如果图片更大可以调到(7,7)闭运算的核(17,5)是经验值横向大是为了连接同一行的字符。失败时优先看二值化结果如果车牌区域没连成块把闭运算核的宽度加大。2.2 CNN 字符识别模型的输入输出设计定位到车牌区域后需要把车牌裁剪出来做透视矫正再送进 CNN 做字符识别。这里有两种主流方案一种是检测加识别两阶段另一种是端到端。毕设文档里提到的 CNN 更接近单字符分类的思路也就是把车牌切成单个字符每个字符做一个 65 分类省份简称 字母 数字。常见做法是先把字符统一缩放到 20x20 或 32x32再送进一个轻量 CNN。import torch import torch.nn as nn class PlateCharCNN(nn.Module): def __init__(self, num_classes65): super().__init__() # 输入假设为 1x32x32 的灰度字符图 self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.AdaptiveAvgPool2d(1) ) self.classifier nn.Linear(128, num_classes) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x)模型结构很简单三层卷积加池化最后全局平均池化接全连接。输入是单通道 32x32输出是 65 维向量对应字符类别。训练时用交叉熵损失优化器选 Adam学习率 1e-3。参数说明num_classes根据实际字符集调整如果只做蓝牌可以设 65如果包含新能源绿牌要扩展到 68 左右。AdaptiveAvgPool2d(1)的好处是不用关心输入尺寸变化但字符图最好还是统一 resize。失败时看训练集准确率如果一直上不去检查字符分割是否把相邻字符粘在一起了。2.3 数据表设计与识别结果的落库逻辑识别出来的车牌号不能只打印在控制台要存进 MySQL 供后续查询和统计。毕设文档里给了user和news两张表但车牌识别系统至少还需要一张plate_record表来存识别记录。常见字段包括车牌号、车牌颜色、识别时间、图片路径、置信度、所属用户。CREATE TABLE plate_record ( id INT AUTO_INCREMENT PRIMARY KEY, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号, plate_color VARCHAR(10) DEFAULT blue COMMENT 车牌颜色, confidence FLOAT DEFAULT 0 COMMENT 识别置信度, image_path VARCHAR(255) COMMENT 原图存储路径, user_id INT COMMENT 关联用户, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_plate_no (plate_no), INDEX idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时给plate_no和create_time加索引是因为统计分析页面要按车牌号聚合、按时间范围筛选。utf8mb4字符集是为了兼容特殊字符。confidence字段存浮点数方便后续按置信度过滤低质量识别结果。如果识别结果要关联到具体用户user_id外键关联user表的主键。注意 MySQL 5.7 和 8.0 在默认字符集上有差异建库时显式指定utf8mb4可以避免中文乱码。3. 系统功能模块实现登录、识别、统计与地图的代码落地毕设文档把功能模块列得很全但没给具体实现。这一章按“能跑起来”的标准把登录鉴权、车牌识别接口、统计分析查询、车牌地图数据聚合这几个核心模块的代码补上。技术选型上Web 框架用 Flask 或 Django 都行这里以 Flask 为例因为它轻量、适合毕设场景。3.1 登录鉴权与 MD5 密码存储的改进写法文档里提到密码用 MD5 加密这在毕设里很常见但 MD5 已经不安全了。如果只是应付检查可以保留但建议至少加盐。下面是一个 Flask 登录接口的写法用hashlib做 MD5 加盐同时用 session 维持登录态。from flask import Flask, request, session, jsonify import hashlib import pymysql app Flask(__name__) app.secret_key your_secret_key_here def hash_password(password, saltplate_sys): # 加盐 MD5比裸 MD5 稍好但生产环境建议 bcrypt return hashlib.md5((password salt).encode(utf-8)).hexdigest() app.route(/login, methods[POST]) def login(): username request.form.get(username) password request.form.get(password) conn pymysql.connect(hostlocalhost, userroot, passwordroot, dbplate_db) cursor conn.cursor(pymysql.cursors.DictCursor) cursor.execute(SELECT * FROM user WHERE yonghuming%s, (username,)) user cursor.fetchone() if user and user[mima] hash_password(password): session[user_id] user[id] return jsonify({code: 0, msg: 登录成功}) return jsonify({code: 1, msg: 用户名或密码错误})逻辑说明先按用户名查库再用同样的加盐规则计算密码哈希做比对。参数方面salt可以每个用户随机生成存库这里为了简化用固定盐。session依赖secret_key部署时一定要换成随机字符串。失败时先确认数据库连接参数再检查user表里的密码字段是不是按同样规则存的。注意 SQL 语句用参数化查询避免拼接字符串导致注入。3.2 车牌识别接口的请求参数与返回结构识别接口是整个系统的核心前端上传图片后端调用模型推理返回车牌号和置信度。下面是一个 Flask 接口的骨架把图片保存、模型加载、结果落库串起来。import os from werkzeug.utils import secure_filename UPLOAD_FOLDER ./uploads ALLOWED_EXTENSIONS {png, jpg, jpeg} def allowed_file(filename): return . in filename and filename.rsplit(., 1)[1].lower() in ALLOWED_EXTENSIONS app.route(/api/recognize, methods[POST]) def recognize(): if file not in request.files: return jsonify({code: 1, msg: 未上传文件}) file request.files[file] if not allowed_file(file.filename): return jsonify({code: 1, msg: 格式不支持}) filename secure_filename(file.filename) save_path os.path.join(UPLOAD_FOLDER, filename) file.save(save_path) # 调用识别函数返回车牌号和置信度 plate_no, confidence predict_plate(save_path) # 落库 conn pymysql.connect(hostlocalhost, userroot, passwordroot, dbplate_db) cursor conn.cursor() cursor.execute( INSERT INTO plate_record (plate_no, confidence, image_path, user_id) VALUES (%s, %s, %s, %s), (plate_no, confidence, save_path, session.get(user_id)) ) conn.commit() return jsonify({code: 0, plate_no: plate_no, confidence: confidence})参数说明UPLOAD_FOLDER要提前创建并确保有写权限ALLOWED_EXTENSIONS限制上传格式防止恶意文件secure_filename过滤文件名中的特殊字符。predict_plate是封装好的识别函数内部走前面讲的定位加 CNN 分类流程。返回结构里code0表示成功plate_no和confidence给前端展示。失败时先看上传目录权限再看模型文件路径是否正确。3.3 统计分析页面的 SQL 聚合查询统计分析页面要展示不同维度的数据比如按车牌颜色统计、按日期统计识别量、按省份统计车牌归属。这些都可以用 SQL 聚合查询直接出结果不用在 Python 里做复杂计算。统计维度SQL 关键子句适用场景按车牌颜色GROUP BY plate_color饼图展示蓝牌绿牌占比按日期GROUP BY DATE(create_time)折线图展示每日识别量按省份GROUP BY LEFT(plate_no, 1)地图展示各省车牌数量按置信度区间GROUP BY FLOOR(confidence*10)/10柱状图展示识别质量分布-- 按日期统计最近 7 天的识别量 SELECT DATE(create_time) AS day, COUNT(*) AS cnt FROM plate_record WHERE create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time) ORDER BY day; -- 按省份简称统计车牌数量用于地图着色 SELECT LEFT(plate_no, 1) AS province, COUNT(*) AS cnt FROM plate_record GROUP BY LEFT(plate_no, 1) ORDER BY cnt DESC;第一条查询用DATE_SUB取最近 7 天GROUP BY DATE(create_time)按天聚合适合前端 ECharts 折线图。第二条用LEFT(plate_no, 1)取车牌第一个字符作为省份简称聚合后给地图组件用。注意如果车牌号字段有空值或格式不对LEFT会返回空字符串查询前最好加WHERE plate_no IS NOT NULL AND LENGTH(plate_no) 7。3.4 车牌地图的数据聚合与前端对接车牌地图页面通常用 ECharts 的中国地图组件后端只需要返回省份简称和对应数量。省份简称和 ECharts 地图的name字段要对应上比如“京”对应北京“粤”对应广东。常见做法是在后端维护一个映射字典把车牌首字转成地图省份名。PROVINCE_MAP { 京: 北京, 津: 天津, 沪: 上海, 渝: 重庆, 冀: 河北, 豫: 河南, 云: 云南, 辽: 辽宁, 黑: 黑龙江, 湘: 湖南, 皖: 安徽, 鲁: 山东, 新: 新疆, 苏: 江苏, 浙: 浙江, 赣: 江西, 鄂: 湖北, 桂: 广西, 甘: 甘肃, 晋: 山西, 蒙: 内蒙古, 陕: 陕西, 吉: 吉林, 闽: 福建, 贵: 贵州, 粤: 广东, 青: 青海, 藏: 西藏, 川: 四川, 宁: 宁夏, 琼: 海南 } def get_map_data(): conn pymysql.connect(hostlocalhost, userroot, passwordroot, dbplate_db) cursor conn.cursor() cursor.execute(SELECT LEFT(plate_no,1) AS p, COUNT(*) AS c FROM plate_record GROUP BY p) rows cursor.fetchall() result [] for p, c in rows: if p in PROVINCE_MAP: result.append({name: PROVINCE_MAP[p], value: c}) return result这段代码把数据库里的省份简称转成 ECharts 能识别的省份名返回[{name: 北京, value: 12}, ...]结构。参数方面PROVINCE_MAP要覆盖所有车牌首字缺一个就会导致地图上该省份没数据。前端拿到数据后直接setOption即可。失败时先检查数据库里有没有plate_no为空或首字不在映射表里的记录。4. 模型训练与调优从数据集准备到置信度阈值调参前面讲的是推理链路但模型不训练就是随机权重识别率惨不忍睹。这一章讲数据集怎么准备、训练时看什么指标、置信度阈值怎么定以及毕设场景下怎么用最少的数据量把效果做到能演示。4.1 车牌数据集的采集与标注格式公开的车牌数据集有 CCPD、CRPD 等但毕设场景下更常见的是自己用手机拍几十张车辆照片再用标注工具画框。标注格式推荐用 YOLO 的 txt 格式每行类别 x_center y_center width height坐标归一化到 0 到 1。如果只做字符识别可以先把车牌区域裁出来再按字符切分每个字符存成一个图片文件文件夹名就是类别标签。# 目录结构示例 dataset/ train/ 京/ 001.jpg 002.jpg 沪/ 001.jpg val/ 京/ 003.jpg这种按类别分文件夹的方式可以直接用 PyTorch 的ImageFolder加载省去写自定义 Dataset 的麻烦。注意训练集和验证集要按 8:2 划分且同一辆车的图片不能同时出现在两边否则验证准确率会虚高。4.2 训练过程中的 loss 曲线与过拟合判断训练时重点看两个指标训练 loss 和验证准确率。如果训练 loss 持续下降但验证准确率停滞甚至下降说明过拟合了。常见做法是加 Dropout、数据增强随机旋转、亮度调整、早停。下面是一个训练循环的骨架。import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.Grayscale(), transforms.Resize((32, 32)), transforms.RandomRotation(5), # 随机旋转 5 度以内 transforms.ColorJitter(brightness0.2), # 亮度扰动 transforms.ToTensor() ]) train_set datasets.ImageFolder(dataset/train, transformtransform) val_set datasets.ImageFolder(dataset/val, transformtransform) train_loader DataLoader(train_set, batch_size64, shuffleTrue) val_loader DataLoader(val_set, batch_size64) model PlateCharCNN(num_classeslen(train_set.classes)) optimizer optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() for epoch in range(30): model.train() for imgs, labels in train_loader: optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() # 验证阶段 model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: outputs model(imgs) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(fEpoch {epoch}, Val Acc: {correct/total:.4f})参数说明batch_size64适合显存 4G 以上的环境显存小就降到 32 或 16lr1e-3是 Adam 的常用初始学习率如果 loss 震荡可以降到 1e-4RandomRotation(5)模拟车牌轻微倾斜ColorJitter模拟光照变化。训练轮数 30 是参考值实际看验证准确率什么时候不再提升就停。失败时先检查ImageFolder加载的类别数是否和模型输出维度一致。4.3 置信度阈值对识别率与误报的权衡模型输出的是每个类别的概率取最大概率对应的类别作为结果。但概率值本身可以作为置信度用来过滤低质量识别。阈值设高了漏识别多设低了误识别多。毕设演示场景下建议阈值设在 0.7 到 0.85 之间先保证展示效果。def predict_with_threshold(model, img_tensor, threshold0.8): model.eval() with torch.no_grad(): outputs model(img_tensor.unsqueeze(0)) probs torch.softmax(outputs, dim1) max_prob, pred torch.max(probs, 1) if max_prob.item() threshold: return None, max_prob.item() # 置信度不足返回空 return pred.item(), max_prob.item()逻辑说明softmax把 logits 转成概率分布max取最大概率和对应类别。如果最大概率低于阈值返回None表示拒识。参数方面threshold可以根据业务调整高速卡口场景可以设 0.9 以上停车场场景 0.7 就够。失败时看被拒识的图片如果是模糊或倾斜导致的先优化预处理而不是降阈值。5. 部署与排错Flask MySQL 环境下的常见问题定位代码写完了跑不起来才是最头疼的。这一章列几个毕设部署阶段高频出现的问题以及对应的排查命令和修复思路。环境以 Windows Python 3.8 MySQL 5.7 为例Linux 下命令略有差异但思路一致。5.1 Python 依赖版本冲突的排查方法深度学习项目最容易死在依赖版本上。PyTorch、OpenCV、NumPy 之间的版本兼容性很敏感。常见做法是先用pip list看已安装版本再对照官方兼容表。如果报ImportError或DLL load failed大概率是版本不匹配。# 查看当前环境所有包版本 pip list # 导出依赖清单方便重建环境 pip freeze requirements.txt # 创建干净虚拟环境重新安装 python -m venv venv venv\Scripts\activate pip install torch1.10.0 torchvision0.11.0 opencv-python4.5.5.64 numpy1.21.6参数说明PyTorch 1.10 搭配 torchvision 0.11 是经过验证的组合OpenCV 4.5.5 和 NumPy 1.21 兼容性较好。如果显卡支持 CUDA装 GPU 版 PyTorch 要去官网查对应 CUDA 版本的安装命令。失败时先确认 Python 版本3.9 以上某些老版本包可能没有预编译 wheel。5.2 MySQL 连接失败与中文乱码的修复Flask 连 MySQL 报Access denied或Unknown database先检查三件事MySQL 服务是否启动、用户名密码是否正确、数据库是否已创建。中文乱码则是字符集问题建库建表时都要指定utf8mb4。-- 创建数据库时指定字符集 CREATE DATABASE plate_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 检查现有表的字符集 SHOW CREATE TABLE plate_record; -- 修改表字符集 ALTER TABLE plate_record CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;连接字符串里也要加charsetutf8mb4否则 Python 和 MySQL 之间的传输编码可能不一致。如果还是乱码检查 Flask 返回 JSON 时有没有设置ensure_asciiFalse以及前端页面的meta charsetutf-8是否写对。5.3 模型推理速度慢的优化方向如果识别一张图要好几秒演示时体验很差。优化方向有三个一是减小模型输入尺寸从 32x32 降到 24x24二是用 ONNX Runtime 或 TensorRT 加速推理三是把模型加载放在全局不要每次请求都重新加载。# 全局加载模型避免重复初始化 import torch model PlateCharCNN(num_classes65) model.load_state_dict(torch.load(plate_cnn.pth, map_locationcpu)) model.eval() # 推理时用 torch.no_grad() 关闭梯度计算 with torch.no_grad(): output model(img_tensor)参数说明map_locationcpu保证在没有 GPU 的机器上也能加载model.eval()切换 BN 和 Dropout 到推理模式。如果用了 GPU把模型和输入都.cuda()。ONNX 导出的命令是torch.onnx.export(model, dummy_input, plate.onnx)然后用onnxruntime.InferenceSession加载速度通常能提升 2 到 3 倍。5.4 识别结果落库失败的日志排查接口返回成功但数据库里没数据或者报IntegrityError先看 Flask 的日志输出。常见原因是字段长度不够、非空字段没传值、外键关联的记录不存在。在cursor.execute后面加conn.commit()是必须的忘了提交事务数据不会真正写入。import logging logging.basicConfig(levellogging.INFO) try: cursor.execute(INSERT INTO plate_record (plate_no, confidence) VALUES (%s, %s), (plate_no, confidence)) conn.commit() logging.info(fInsert success: {plate_no}) except Exception as e: conn.rollback() logging.error(fInsert failed: {e})逻辑说明try块包住执行和提交except里先回滚再记录错误。logging比print更适合排查因为可以输出到文件并带时间戳。参数方面plate_no字段长度如果设了VARCHAR(20)识别结果超过 20 个字符就会截断或报错建表时留够余量。6. 从毕设到可用用置信度分布反推模型短板模型上线后别只看识别率这一个数字。把plate_record表里的confidence字段拉出来做分布统计能看出很多问题。比如置信度集中在 0.5 到 0.7 之间说明模型对大部分样本都不太确定可能是训练数据太少或类别不均衡如果某些省份简称的置信度普遍偏低说明该类样本在训练集中占比不足。-- 按置信度区间统计识别量 SELECT CASE WHEN confidence 0.9 THEN 0.9-1.0 WHEN confidence 0.8 THEN 0.8-0.9 WHEN confidence 0.7 THEN 0.7-0.8 WHEN confidence 0.6 THEN 0.6-0.7 ELSE 0.0-0.6 END AS conf_range, COUNT(*) AS cnt FROM plate_record GROUP BY conf_range ORDER BY conf_range DESC;这条查询把置信度分桶统计如果 0.0 到 0.6 区间占比超过 30%说明有大量低质量识别需要回头检查预处理环节或者补充训练数据。另一个技巧是关联image_path字段把低置信度的图片挑出来人工看通常能发现共性问题比如夜间光照、车牌污损、角度过大。把这些图片加入训练集重新训练比盲目调参有效得多。# 导出低置信度记录供人工复核 cursor.execute(SELECT plate_no, confidence, image_path FROM plate_record WHERE confidence 0.7 ORDER BY confidence ASC LIMIT 50) low_conf_samples cursor.fetchall() for sample in low_conf_samples: print(f车牌: {sample[0]}, 置信度: {sample[1]:.2f}, 图片: {sample[2]})参数说明LIMIT 50控制每次复核的数量避免一次性拉太多。ORDER BY confidence ASC让最差的排前面。把这些样本的图片路径整理出来批量重新标注后加入训练集迭代两三轮通常能看到置信度分布明显右移。这个思路比单纯调阈值更治本也是从毕设 Demo 走向可用系统的关键一步。本文还有配套的精品资源点击获取