简介:这是一套基于深度学习的交通标志识别系统完整毕业设计源码包,采用Django框架搭建前后端,搭配MySQL数据库,面向计算机相关专业毕业设计或课程设计人群。系统支持实景图片检测、摄像头实时识别,可对普通场景及低像素、远距离、雾霾、雨天、黑夜等特殊天气条件下的交通标志进行分类,检测结果支持图像保存;模型基于YOLOv5训练,Python界面简约美观。资源共570个文件,含Python与Django核心代码、训练好的YOLOv5模型权重(h5/pth)、前端页面(HTML/CSS/JS)、图层素材(png/gif/jpg)及说明文档与SQL文件,压缩包整体约578.97MB,便于直接部署与二次开发。目前已有66人学习下载,适合需要从零搭建交通标志识别毕业设计项目的读者参考。
1. 基于深度学习的交通标志识别系统:先认清这条技术主线
这套系统的技术主线其实只有一条:前端上传一张交通标志图片,后端用深度学习模型完成分类,把分类结果和历史记录写进 MySQL,再由 Django 渲染回页面。识别精度由模型决定,工程体验由 Django 决定,数据能否沉淀由 MySQL 决定。许多源码包把这三层混在一起交付,拿到手后不知道从哪一行开始跑,也分不清训练脚本与 Web 代码的边界,这是接触这类 python 毕业设计项目时最先要解决的问题。适合阅读这类项目源码的人大致有三类:准备做毕业设计的学生、想练完整 AI + Web 链路的开发者、要在公司内部快速搭一个图像识别 demo 的工程师。下面按这套系统最标准的工程结构,把模型训练、Django 集成、MySQL 数据表、前后端联调和部署排错完整走一遍。
2. 深度学习模型先落地:CNN 结构与训练参数怎么定
2.1 不从头设计网络,选 ResNet18 做迁移学习
交通标志识别是典型的图像分类任务,输入是一张包含标志的图片,输出是这个标志属于哪一类。在深度学习方案里,卷积神经网络是绝对主流,但具体选哪一版网络结构,直接影响训练时间和最终精度。这里不建议自己搭一个多层卷积栈,毕设场景的数据量通常只有几千到几万张,手工搭的网络很容易欠拟合或者收敛不稳定。常见做法是加载预训练模型做迁移学习。
对比三个热度最高的候选结构,按参数量、适用数据和真实场景表现来选型。
| 网络结构 | 参数量级别 | 适用数据规模 | 在这个任务里的表现 |
|---|---|---|---|
| LeNet-5 | 约 6 万 | 极小样本,如 MNIST | 结构过浅,43 类交通标志容易欠拟合 |
| ResNet18 | 约 1170 万 | 万级图像分类 | 残差结构收敛稳,预训练权重易得,首选 |
| MobileNetV3 | 约 250 万 | 移动端轻量化场景 | 精度略低,但推理快,适合边缘部署 |
我一般会选 ResNet18 而不是 ResNet50。原因是 18 层在几千张训练图上已经能跑出 96% 以上的验证精度,训练速度快,过拟合风险低;50 层在小数据集上反而更容易陷入过拟合,而且显存占用高,很多学生机房里的 GPU 扛不住。如果换到 TT100K 这种含 152 类的大规模中国交通标志数据集,再考虑升级到 ResNet50 不迟。
2.1.1 数据集选择与类别映射
训练之前先明确数据从哪来。公开数据集有两个主流选择:一个是德国交通标志数据集 GTSRB,共 43 类,样本量大,和国外论文对比方便;另一个是清华的 TT100K 数据集,面向中国交通场景,类别更细更多。如果毕业设计面向国内场景,用 TT100K 更合适,但需要花时间做类别筛选,因为原始标注里有很多低频类别,直接把全量拿来训练效果不会好。
训练集和验证集按 8:2 分层切分,不要简单随机打乱。分层切分的目的是保证每一类在验证集都有样本,否则某个类别样本全落在训练集,验证准确率的波动会非常大。切分后把图片按类别编号/图片的目录结构组织,方便用 PyTorch 的ImageFolder直接加载。
2.2 数据增强参数:先定 Resize,再定扰动强度
交通标志图像的难点不在背景复杂,而在拍摄角度、光照和遮挡。同一个限速标志,逆光拍出来和阴天拍出来差异很大。数据增强能把这些差异模拟进训练集。
import torch from torchvision import datasets, transforms # 训练集增强:标注图片通常有轻微倾斜和光照变化 transform_train = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomRotation(15), transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) # 验证集只做缩放和归一化,不做随机扰动,保证评估稳定 transform_val = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_data = datasets.ImageFolder("data/train", transform=transform_train) val_data = datasets.ImageFolder("data/val", transform=transform_val)参数不是拍脑袋定的。RandomRotation(15)指随机旋转正负 15 度,交通标志本身有边框,旋转超过 20 度会让模型学到错误的几何特征。RandomAffine里translate=(0.1, 0.1)表示在水平和垂直方向最多平移 10% 的像素宽度,模拟抓拍时标志不在画面中心的情况。ColorJitter保持亮度变化在正负 20% 以内,这个范围不会改变标志的底色语义,比如红色禁令标志在亮度降低后仍然是红色,不会和蓝色指示标志混淆。归一化均值方差直接用 ImageNet 预训练权重对应的值,加载 PyTorch 官方ResNet18_Weights时换算关系是现成的。
2.3 用 PyTorch 跑通训练脚本与评估指标
训练脚本是整套系统里最独立的部分,跑完之后要产出两样东西:模型权重文件和一个类别映射表。类别映射表非常重要,模型输出的是序号,比如 13,这个 13 对应“限速 40”还是“禁止通行”,靠映射表才知道。
from torchvision import models import torch.nn as nn import torch.optim as optim model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(512, 43) # 替换最后一层全连接,输出类别数 criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-4) for epoch in range(30): model.train() for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() torch.save(model.state_dict(), f"checkpoints/traffic_sign_{epoch}.pth")几个关键点:第一,model.fc = nn.Linear(512, 43)是迁移学习的固定写法,预训练权重只保留前面卷积层的通用特征提取能力,最后一层随机初始化,让它从头学习 43 类交通标志的差异。第二,学习率用1e-4而不是默认的1e-3,因为预训练权重已经在一个大模型上收敛过,过大的学习率会把学好的底层特征破坏掉。第三,weight_decay=1e-4是 L2 正则化,小数据集上能有效抑制过拟合,但不要超过1e-3,否则模型学不动。
训练 30 个 epoch 后,不要只看准确率,要打印每个类别的召回率。交通标志数据集的类别天然不均衡,“限速 30”类在路面出现频率远低于“禁止驶入”,如果某个类别的召回率低于 90%,说明模型对这个标志存在系统性误判,通常做法是检查该类样本数量,再做针对性增强。评估脚本最后把label_map = {0: "限速20", 1: "限速30", ...}导出成 JSON,Web 段直接用它做推理结果转译。
3. Django 工程骨架与 MySQL 连接配置
3.1 用 django-admin 初始化项目与 App 划分
模型训练完,进入 Web 工程部分。这套系统的后端采用 Django 的 MVT 架构,项目的目录结构按照“配置目录 + 业务 App”拆分。我习惯把 Django 项目的配置目录命名为config,业务模块按功能拆成两个 App:recognition负责模型加载和图片识别,history负责识别记录的增删改查。
mkdir traffic_sign_system && cd traffic_sign_system python -m venv venv source venv/bin/activate # Windows 系统用 venv\Scripts\activate pip install django pymysql pillow django-admin startproject config . python manage.py startapp recognition python manage.py startapp history虚拟环境这一步不能省。很多毕设源码包直接依赖系统 Python 环境,系统里 Django 版本和项目要求的版本不一致,一启动就会报django.core.exceptions.ImproperlyConfigured之类的错误。pip install django pymysql pillow这三个依赖是起步需要:pymysql 是 Django 连接 MySQL 的驱动,pillow 是处理上传图片的底层库。如果后面要部署到生产环境,再补一个 gunicorn,本地调试不需要。
App 划分的逻辑是:recognitionApp 放模型推理、图片预处理、上传接口;historyApp 放历史记录的表结构和管理视图。这样训练相关代码和业务记录代码不混在一起,答辩时讲模块设计也更清晰。注意 App 名不要叫model或utils,太泛化的命名在 Django 里容易和第三方模块产生命名冲突。
3.2 settings.py 三处关键配置:数据库、媒体文件与静态文件
Django 项目跑起来之前,settings.py里有三处必须动。数据库配置是第一个,否则默认会去连 SQLite,标题里既然要求 MySQL,需要在这一步直接把引擎切过去。
# config/settings.py import pymysql pymysql.install_as_MySQLdb() DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'traffic_sign_system', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } } MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' STATIC_URL = '/static/' STATICFILES_DIRS = [BASE_DIR / 'static']pymysql.install_as_MySQLdb()这行是 Python 3 环境下的标准操作,纯 Python 的 pymysql 把自己伪装成 MySQLdb,Django 才能正常执行数据库操作。OPTIONS里的charset: utf8mb4必须显式声明,否则存入中文时可能出现字符集告警。MEDIA_ROOT指向项目根目录下的media文件夹,用户上传的交通标志原图会写到这个位置;STATICFILES_DIRS指定前端 CSS、JS 文件的位置。
3.3 MySQL 建库与 Django 迁移
数据库需要在 MySQL 服务端先建好,Django 不会自动建库,只会自动建表。
CREATE DATABASE traffic_sign_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;建库后执行项目迁移。migrate会依据models.py中定义的模型在 MySQL 里生成对应的物理表,同时生成 Django 框架自身的会话表、权限表。
python manage.py makemigrations python manage.py migrate python manage.py createsuperuser执行完migrate后,在 MySQL Workbench 里执行SHOW TABLES;,能看到django_migrations、auth_user等系统表和后续自定义的业务表。这一步是验证 Django 和 MySQL 连接是否打通的最快方式。如果执行迁移时报django.db.utils.OperationalError: (2003, "Can't connect to MySQL server on '127.0.0.1'"),先查 MySQL 服务是否启动,再用mysql -uroot -p命令行确认账号密码无误。
4. 数据库模型:用 Django ORM 管理识别历史记录
4.1 TrafficSignHistory 表结构与字段设计
识别历史记录是这套系统里唯一需要落库的业务数据。没做这块之前很多初学者会疑惑:每识别一张图片就往 MySQL 写一条记录,会不会太重?实际上识别过程本身不需要 MySQL 参与,模型是直接加载在内存里的,MySQL 只在识别完成后负责把记录存下来,供前端展示历史页面和统计用途。表结构按“一条记录对应一次识别请求”来设计。
# history/models.py from django.db import models class TrafficSignHistory(models.Model): image = models.ImageField(upload_to="sign_images/%Y%m/") predict_code = models.IntegerField(verbose_name="类别编号") predict_name = models.CharField(max_length=64, verbose_name="标志名称") confidence = models.FloatField(verbose_name="置信度") created_at = models.DateTimeField(auto_now_add=True, verbose_name="识别时间") class Meta: db_table = "traffic_sign_history" ordering = ["-created_at"]字段设计围绕一个原则:前端页面展示什么,表里就存什么。image字段存的是上传图片的相对路径,Django 会自动在MEDIA_ROOT下创建sign_images/年月子目录,避免所有图片堆在一个目录里。predict_code是模型推理产出的类别编号,类型用IntegerField而不是CharField,因为后续做类别统计时整数能直接参与聚合运算。predict_name冗余存储中文名称,例如“限速40”,这样前端查询历史列表时不需要再查label_map。confidence存模型输出的 softmax 概率值,保留四位小数即可。Meta里用ordering = ["-created_at"]让新旧记录默认按时间倒序排列。
4.2 常用查询操作:筛选、排序、删除与分页
Django ORM 的查询语法在毕设源码里出现频率很高,这里给出几种最常用的操作,覆盖需求分析中的“历史记录预览、按类别筛选、定期清理”三个功能点。
# 最近 20 条识别记录,只需展示页面用 recent_records = TrafficSignHistory.objects.all()[:20] # 统计某个类别出现了多少次,用于图表展示 limit_count = TrafficSignHistory.objects.filter(predict_code=12).count() # 删除 30 天前的记录,清理历史数据 from datetime import timedelta, datetime cutoff_time = datetime.now() - timedelta(days=30) TrafficSignHistory.objects.filter(created_at__lt=cutoff_time).delete() # 分页查询,当前页为 page_no,每页 10 条 from django.core.paginator import Paginator paged_records = Paginator(TrafficSignHistory.objects.all(), 10).get_page(page_no)ORM 和原生 SQL 的对应关系可以简单对照,写论文的数据流图时也用得上:
| Django ORM 写法 | 对应的 SQL 行为 |
|---|---|
.filter(predict_code=12) | WHERE predict_code = 12 |
.order_by('-created_at') | ORDER BY created_at DESC |
.exclude(predict_name='限速40') | WHERE NOT predict_name = '限速40' |
.count() | COUNT(*) 聚合查询 |
使用Paginator时,page_no从请求参数里读取,注意捕获PageNotAnInteger和EmptyPage异常,否则用户手动拼接一个不存在的页码时会把 500 错误返回给浏览器。
4.3 为什么热识别走内存,冷查记录走 MySQL
这套系统的性能要点在于区分热路径和冷路径。用户上传图片后,识别过程完全在内存中完成:图片解码为 Tensor,模型前向传播,softmax 取最大值,这个过程只需要几十毫秒,MySQL 不参与。识别完成后,把结果写入 MySQL,这是异步的落库操作,即使 MySQL 写入慢一点,用户感知到的响应时间也不会有明显增加。
很多项目把历史记录查询做得过重,每次返回详情页都去查整张表,数据量大了之后页面变卡。正确处理是列表页只查当前页 20 条记录,图片地址直接用 Django 模板的{{ record.image.url }}渲染缩略图,不要对ImageField做额外的 IO 操作。如果后续记录量超过十万条,再考虑给created_at加数据库索引,但毕设阶段这个表的数据规模通常几千条,不加索引也能顺畅工作。
5. 前后端联调:图片上传、模型预测与结果展示
5.1 视图函数与异步 AJAX 接口
前后端联调是整个系统最需要耐心的环节,问题通常出在数据传输格式和接口约定上。先定义接口行为:前端向/predict/发送 POST 请求,参数名是image,后端接收图片、保存副本、执行模型推理、落库,最后返回一个 JSON 对象。
# recognition/views.py import os import torch from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.conf import settings from history.models import TrafficSignHistory @csrf_exempt def predict_view(request): if request.method != "POST": return JsonResponse({"code": 1, "msg": "only POST allowed"}) image_file = request.FILES.get("image") if not image_file: return JsonResponse({"code": 1, "msg": "no image uploaded"}) # 第一步:保存上传图片 image_path = os.path.join(settings.MEDIA_ROOT, image_file.name) with open(image_path, "wb") as f: for chunk in image_file.chunks(): f.write(chunk) # 第二步:模型推理(token 预训练模型在 Django 启动时加载) tensor = preprocess_image(image_path) with torch.no_grad(): probs = torch.softmax(model(tensor), dim=1) conf, idx = torch.max(probs, dim=1) # 第三步:写入 MySQL,返回 JSON record = TrafficSignHistory.objects.create( image=image_file.name, predict_code=int(idx.item()), predict_name=label_map[int(idx.item())], confidence=round(float(conf.item()), 4) ) return JsonResponse({ "code": 0, "data": { "id": record.id, "name": record.predict_name, "confidence": record.confidence, "image_url": record.image.url, } })@csrf_exempt加在视图上是为了调试方便,传统表单 POST 需要携带 CSRF Token,而前端用 AJAX 上传文件时要先 GET 一个 token 再拼到请求头,过程繁琐。但需要注意,线上部署时应去掉这个装饰器,改用前端请求头携带X-CSRFToken,否则会存在跨站请求伪造风险。image_file.chunks()是 Django 文件对象的分块读取方法,大图片上传时按块写入磁盘,不会一次性把整个文件加载进内存。模型实例应该在 Django 启动时加载一次,不要放到视图函数内部torch.load,否则每次请求都会重新初始化模型,首包延迟会高达数秒。
5.2 前端页面与识别结果渲染
前端页面用原生 HTML + JavaScript 加一个fetch请求即可,不需要引入 Vue 或 React 这类框架,毕业设计场景里页面逻辑简单,原生写法反而更容易向评委讲清楚。
async function uploadAndPredict() { const fileInput = document.getElementById("imageInput"); if (fileInput.files.length === 0) { alert("请先选择一张交通标志图片"); return; } const formData = new FormData(); formData.append("image", fileInput.files[0]); const resp = await fetch("/predict/", { method: "POST", body: formData }); const result = await resp.json(); if (result.code === 0) { document.getElementById("resultName").textContent = result.data.name; document.getElementById("resultConf").textContent = result.data.confidence; document.getElementById("resultImage").src = result.data.image_url; } else { alert("识别失败:" + result.msg); } }这里有几个规范要点:FormData的字段名必须和 Django 视图里request.FILES.get("image")的键名一致,改成img或file都会导致后端取不到文件对象。响应 JSON 里统一包一层code字段,0表示成功,非零表示业务异常,这样前端判断逻辑只有一行分支。result.image_url直接用 Django 生成的完整媒体链接,不要在 JS 里手动拼接字符串,手动拼容易因MEDIA_URL变化而出错。
5.3 本地跑通的完整验证路径
前后端代码写完,最怕一次性写完所有功能再启动测试,一旦出错很难定位是哪一层的问题。建议按三步递进验证:先验证模型推理,再验证接口,最后验证页面。
第一步,在 Django shell 里手动构造一次推理:
python manage.py shellfrom recognition.inference import predict_single_image print(predict_single_image("media/test/speed_40.jpg"))第二步,用命令行工具模拟一次 HTTP 请求:
curl -X POST -F "image=@media/test/speed_40.jpg" http://127.0.0.1:8000/predict/第三步,浏览器打开http://127.0.0.1:8000/,走一遍完整上传流程。三步全部通过后,再去看历史记录页和 MySQL 里的数据,基本就没有联调死角了。
6. 部署避坑与论文亮点:让项目从“能跑”到“能答辩”
6.1 三个高频报错:从现象到修复
这类系统里出现频率最高的三个问题,整理成排查表,每一个都对应具体的源码位置和修复方法。
| 报错现场 | 根因 | 修复动作 |
|---|---|---|
启动时ModuleNotFoundError: No module named 'pymysql' | 环境里没装 MySQL 驱动 | pip install pymysql,并在settings.py调用install_as_MySQLdb() |
上传图片后 500,日志显示No such file or directory | 识别前没有先保存文件或路径拼接错误 | 用os.path.join(settings.MEDIA_ROOT, ...)拼接完整路径,不要直接拿原始文件名写文件 |
| 识别结果全是同一个类别 | 训练时类别映射和推理时不一致 | 打印模型输出索引和映射表,确认label_map是否从训练脚本里导出 |
第三个问题排查起来最隐蔽。常见原因是训练脚本里的ImageFolder自动按目录名排序生成类别编号,而推理脚本手工写的映射顺序和它不同。解决方案是在训练结束后,把train_data.class_to_idx原样导出成 JSON 文件,推理时直接json.load读取,绝不在 Web 工程里手工编写映射字典。
6.2 说明文档与论文撰写顺序
“说明文档+LW”是这套系统交付时要有的配套材料,但真正动笔时建议按执行顺序反过来整理。先把环境搭建文档写好,把requirements.txt导出来,再写系统总体设计和数据库设计,最后补实验结果和总结。这样写的逻辑是:环境文档里的每一步都是自己真实跑过一遍的,不会出现照着文档做却启动不了的情况。答辩演示时,如果现场网络不允许安装依赖,提前准备一个离线 wheel 包目录或者把虚拟环境直接拷贝到演示机器上,能节省大量时间。
6.3 用 Grad-CAM 给模型加一个可视化亮点
最后提一个让项目在答辩时明显出彩的技巧:Grad-CAM 可视化。原理不复杂,将模型最后一层卷积输出的特征图与梯度加权求和,得到的热力图能直观展示模型关注的是图片中的哪块区域。在模型推理时注册一个 forward hook,把特征图和梯度同时取出来,用平均梯度做权重叠加,再把热力图叠加到原图,输出成一张可视化图片。这里不需要新增数据库字段,把生成的热力图存到media/gradcam/目录即可,前端在识别结果旁展示一次,说明模型的决策依据是标志区域而不是背景干扰。这个功能代码量不大,但能体现对模型可解释性的理解。
本文还有配套的精品资源,点击获取