
简介一套基于Python与Django框架的深度学习交通标志识别系统源码面向具备一定Python基础、希望快速搭建图像识别Web应用的开发者、学生及毕业设计人员。系统集成YOLOv5模型支持通过上传实景图片或调用笔记本摄像头进行实时交通标志检测可识别并分类标志类型并在窗口即时显示结果同时针对像素模糊、远距离、雾霾、雨天、黑夜等特殊条件做了适配检测后的图像可保存另存后台采用DjangoMySQL包含用户注册登录、后台管理等功能前端页面基于Bootstrap/Layui构建界面美观且代码结构简洁易懂。压缩包共570个文件整体大小约578.68MB涵盖47个Python核心源码、10个HTML页面、31个CSS和62个JS前端文件151个GIF与107个PNG图片素材5个H5模型文件与2个PTH权重文件另含SQL数据库文件、PDF说明文档等可支撑项目直接运行与二次开发。已有291人学习浏览适合作为课程设计、毕业设计或交通标志识别项目参考可直接获取完整前后端代码、模型权重及界面资源。1. 为什么交通标志识别系统要用YOLOv5Django而不是纯OpenCV车载记录仪拍到的限速牌经常带着水雾和暗角传统OpenCV做颜色阈值分割时红色边框的“停车”标志在雨天几乎和背景融为一体识别率掉到惨不忍睹。换成YOLOv5训练的目标检测模型之后同样一张模糊帧模型依然能给出框和类别因为卷积网络学的是语义特征而不是像素颜色规律。再加上Django框架做Web外壳图片上传、笔记本摄像头、后台登录都能在同一套系统里跑通模型只负责推理业务逻辑全部交给Python处理。这套交通标志识别系统源码适合有三到六个月Python经验、想快速跑通一个深度学习Web应用的开发者无论是课程设计还是内部验证工具都很合适。以下会按模型训练、Django接口、摄像头集成、部署验证的顺序把每个环节的关键代码和参数拆开讲。2. YOLOv5模型训练与特殊场景数据增强2.1 交通标志数据集的标注与目录组织交通标志识别本质上是一个小目标检测问题一张1080P图片里标志往往只占几十个像素且同一类别在不同光照下颜色差异很大。因此训练前要把数据集按YOLOv5约定整理好。推荐使用两个目录分别存图片和标注文件而不是把所有东西混在一起。源码包里已经按照这个结构组织你自己扩充数据时也保持一致。dataset/ ├── images/ │ ├── train/ │ │ ├── 000001.jpg │ │ └── 000002.jpg │ └── val/ │ └── 000100.jpg └── labels/ ├── train/ │ ├── 000001.txt │ └── 000002.txt └── val/ └── 000100.txt每个TXT文件名与图片名完全一致每行代表一个目标格式为类别ID、中心点x、中心点y、宽度、高度前四个值为归一化到0-1的小数。比如一张640x480的图里左上角坐标为(100, 120)、宽高为(80, 60)的标志计算方式是x_center(10040)/6400.21875y_center(12030)/4800.3125width80/6400.125height60/4800.125。标注工具可以用LabelImg或anylabeling输出格式选YOLO即可。类别ID需要与data.yaml中的names数组顺序一致否则训练和推理时类别会错位。常见做法是先统计所有标志的大类再给每个大类固定ID不要在中途调整顺序。# 训练集图片路径 train: dataset/images/train # 验证集图片路径 val: dataset/images/val # 类别总数 nc: 10 # 类别名称顺序对应标注文件里的数字ID names: [speed_limit, warning, stop, no_entry, yield, pedestrian, construction, left_turn, right_turn, roundabout]train和val路径可以写绝对路径或相对于yaml文件的路径源码包中默认是相对路径。nc是类别总数names顺序决定标注数字映射到哪个类别。如果数据集中有20类就把names补齐到20项不要只填几个。标注文件里的第一个数字如果超过了nc-1训练会直接报错启动前最好用脚本扫描一遍标签范围。2.2 YOLOv5训练命令与关键超参数YOLOv5的训练入口是train.py先装依赖再跑基础模型。源码包里通常已经带了权重文件或下载脚本如果没有可以用yolov5s.pt作为预训练基座。yolov5s是速度和精度比较均衡的版本交通标志这种目标不太密集的任务不需要上到yolov5x。cd yolov5 pip install -r requirements.txt python train.py --data traffic.yaml --weights yolov5s.pt \ --img 640 --batch-size 16 --epochs 100 \ --device 0 --project runs/train --name traffic_exp--img 640将输入图片缩放到640x640交通标志小目标多不建议降到320否则细纹理丢失如果显存有限可以保持640但把batch-size降到8。--batch-size表示一次迭代送入的图片数量16在8G显存上比较稳。--epochs 100对小数据集来说足够前提是开启早停机制源码包里保留了该逻辑当连续50个epoch没提升就直接结束。训练超参数可以预先写在hyp配置文件里下面是一份针对室外小目标的推荐配置。超参数建议值适用场景img640 / 512标志明显时用512加速小目标多时用640batch-size8 / 16 / 32显存小用8显存大用32lr00.01迁移学习默认值mosaic1.0增强小目标鲁棒性最后10个epoch可关闭hsv_v0.4提升夜间低照度表现translate0.1模拟摄像头偏移训练日志会输出precision、recall、mAP50、mAP50-95四个指标重点关注低分辨率验证集上的mAP50雨雾场景需要单独划分验证集查看。如果recall明显低于precision说明模型漏检较多先把conf阈值降低再看。2.3 针对雾霾、雨天、黑夜的图像增强策略交通标志的特殊天气场景不能只靠常规增强否则模型学到的还是清晰图像特征。常见做法是离线生成一批模拟天气样本加入训练集同时保留原始清晰图作为对照。下面是一个用OpenCV模拟雾天和夜景的函数import cv2 import numpy as np # 模拟雾天将图像与灰色图层按权重混合 def add_fog(image, intensity0.5): h, w image.shape[:2] fog_layer np.full((h, w, 3), 120, dtypenp.uint8) return cv2.addWeighted(image, 1 - intensity, fog_layer, intensity, 0) # 模拟黑夜使用伽马变换降低亮度 def add_night(image, gamma0.5): inv_gamma 1.0 / gamma table np.array([((i / 255.0) ** inv_gamma) * 255 for i in range(256)]).astype(np.uint8) return cv2.LUT(image, table)add_fog用一张全图亮灰色与原始图片按权重混合intensity越高雾越浓add_night通过伽马变换降低整体亮度模拟黑夜。这两种函数可以放到数据集预处理脚本里把每个训练图片额外生成一份雾天和夜景副本打乱后丢进训练集。注意验证集不要做同样增强否则mAP虚高真实场景表现会大打折扣。如果想在YOLOv5里直接开启内置增强修改data/hyps/hyp.scratch-low.yaml# 色调扰动 hsv_h: 0.015 # 饱和度扰动 hsv_s: 0.7 # 亮度扰动调大后暗光样本更多 hsv_v: 0.5 # 旋转角度范围 degrees: 10.0 # 平移范围 translate: 0.1 # 缩放范围 scale: 0.5hsv_h调整色相扰动hsv_s、hsv_v影响饱和度与亮度。夜间图像主要是v通道偏低把hsv_v调大一点能增加暗光样本的多样性。degrees控制在正负10度内避免标志旋转过大scale设置0.5让目标在训练中不断缩放模拟远景小目标。需要说明的是内置增强只对训练输入生效不会污染验证集这是它比离线增强干净的地方。3. Django后端接口设计与图片检测流程3.1 Django项目结构与MySQL数据库配置Django部分采用标准MVT结构。源码包里的traffic_sign项目下管理后台、认证应用和业务应用分开业务应用负责图片上传、摄像头帧接收、调用YOLOv5模型。项目初始化时可以使用python manage.py startapp app生成业务应用目录这与热词里常搜到的“django创建app”流程一致。traffic_sign/ ├── manage.py ├── traffic_sign/ │ ├── settings.py │ └── urls.py ├── app/ │ ├── views.py │ ├── urls.py │ ├── models.py │ ├── utils/ │ │ ├── detector.py │ │ └── transforms.py ├── templates/ │ ├── login.html │ ├── register.html │ └── index.html └── media/ ├── uploads/ └── results/与普通Web项目不同的地方在于多了一个utils目录专门封装YOLOv5模型加载和图像预处理避免在views.py里写太多与业务无关的推理代码。models.py里存放检测记录表transforms.py复用第2章的天气增强函数detector.py负责模型加载和推理参数管理。Django默认使用SQLite但这个系统使用MySQL因为背后的数据库设计里有用户表、检测记录表在实验或生产环境中MySQL更通用。首先在settings.py中修改DATABASESDATABASES { default: { ENGINE: django.db.backends.mysql, NAME: traffic_sign, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, # 必须指定utf8mb4否则中文标签写入会报错 OPTIONS: {charset: utf8mb4}, } }ENGINE指定使用MySQL驱动NAME是数据库名需要提前在MySQL中创建。USER和PASSWORD对应本机数据库账号。OPTIONS里的charset必须写成utf8mb4否则检测结果里带有中文标签时写入数据库会报编码错误。如果本机还没装mysqlclient可以执行pip install mysqlclientWindows用户可以直接从whl文件安装。装完运行python manage.py migrate建立相关表结构。3.2 用户注册登录与Session会话保持Django自带auth模块所以不需要从零写加密逻辑。登录视图用authenticate和login两个函数前者校验用户名密码是否匹配后者把用户ID写入session注册时用User.objects.create_user创建省掉手动hash密码的环节。from django.contrib.auth import authenticate, login from django.contrib.auth.models import User from django.shortcuts import render, redirect def user_login(request): if request.method POST: username request.POST.get(username) password request.POST.get(password) # 校验用户名和密码 user authenticate(request, usernameusername, passwordpassword) if user is not None: # 密码正确写入session login(request, user) return redirect(/index/) return render(request, login.html, {error: 账号或密码错误}) return render(request, login.html) def user_register(request): if request.method POST: username request.POST.get(username) password request.POST.get(password) if User.objects.filter(usernameusername).exists(): return render(request, register.html, {error: 用户名已存在}) # 使用create_user会自动加密密码 User.objects.create_user(usernameusername, passwordpassword) return redirect(/login/) return render(request, register.html)authenticate会调用ModelBackend做密码校验login才真正执行session注册。视图没有直接操作MySQL而是通过Django ORM这样以后换成PostgreSQL也不需要改业务逻辑。模板表单中必须放{% csrf_token %}否则Django会返回403错误。3.3 图片上传、模型推理与结果保存检测视图是整个系统的核心。模型加载最好只做一次放在utils/detector.py中用全局变量保存避免每张图片请求都重新读权重。import torch model None def get_model(): global model if model is None: # 从本地yolov5目录加载训练好的权重 model torch.hub.load(./yolov5, custom, pathbest.pt, sourcelocal) model.conf 0.25 model.iou 0.45 model.max_det 10 return modeltorch.hub.load使用本地yolov5目录加载自定义模型sourcelocal表示不从GitHub下载源码。model.conf控制置信度阈值0.25会保留大多数预测框model.iou是NMS去重阈值0.45比较保守适合多个重叠目标max_det限制单张图片最多输出10个目标防止摄像头画面里出现大量误报。图片上传视图用request.FILES读取文件分块写入磁盘然后调用模型推理。下面这段代码保存原图和结果图两个文件并返回前端需要的JSON。import os import uuid import cv2 from django.http import JsonResponse from .utils.detector import get_model def detect_image(request): if request.method ! POST: return JsonResponse({error: method not allowed}, status405) file request.FILES.get(image) if not file: return JsonResponse({error: no image}, status400) upload_dir media/uploads result_dir media/results os.makedirs(upload_dir, exist_okTrue) os.makedirs(result_dir, exist_okTrue) ext os.path.splitext(file.name)[1] or .jpg # 使用uuid重命名避免同名文件覆盖 filename uuid.uuid4().hex ext raw_path os.path.join(upload_dir, filename) with open(raw_path, wb) as f: for chunk in file.chunks(): f.write(chunk) model get_model() results model(raw_path) rendered results.render()[0] result_path os.path.join(result_dir, filename) cv2.imwrite(result_path, rendered) labels results.pandas().xyxy[0].to_dict(records) return JsonResponse({ result_url: /media/results/ filename, labels: labels })用uuid重命名文件避免两个用户同时上传同名图片时互相覆盖。file.chunks()把上传文件按块读取对几十MB的大图也不会占满内存。results.render()返回的是绘制了边界框的RGB图像这里用cv2.imwrite保存注意cv2的通道顺序是BGR与前端展示没有冲突。如果要把识别历史存进数据库可以在返回前创建一条DetectionResult记录把labels字段用json.dumps序列化后保存。models.py里一份可用的模型定义如下from django.conf import settings from django.db import models class DetectionResult(models.Model): # 外键关联用户允许为空方便调试 user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, nullTrue) original_image models.ImageField(upload_touploads/) result_image models.ImageField(upload_toresults/) labels models.TextField() created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table detection_resultuser字段允许为空是为了支持未登录用户调试生产环境可以去掉nullTrue强制登录才能检测。labels字段存JSON文本查询时用json.loads解析不要为每个目标单独建表那样会拖慢保存速度。3.4 URL路由与API端点设计把页面路由和接口路由放在同一个urls.py里前端可以同时用form表单和AJAX调用。下面是路由表URL方法功能/register/GET/POST用户注册/login/GET/POST用户登录/logout/GET退出登录/index/GET识别主页/detect/image/POST上传图片识别/detect/camera/POST摄像头帧识别from django.urls import path from . import views urlpatterns [ path(register/, views.user_register, nameregister), path(login/, views.user_login, namelogin), path(logout/, views.user_logout, namelogout), path(index/, views.index, nameindex), path(detect/image/, views.detect_image, namedetect_image), path(detect/camera/, views.detect_camera, namedetect_camera), ]接口尽可能按资源命名图片检测用POST而不是GET因为请求里包含图片文件浏览器对GET请求有长度限制。detect/camera接口设计为接收Base64图片字符串与页面中的摄像头帧逻辑一一对应。后续如果要做移动端小程序这两个接口可以直接复用不需要改后端。4. 摄像头实时识别与前端界面集成4.1 用getUserMedia打开笔记本摄像头摄像头识别不是视频流逐帧上传而是前端定时抓取画面压缩成JPEG后POST给后端。这样Django不需要维护WebSocket连接减少复杂性。打开摄像头用navigator.mediaDevices.getUserMedia注意video标签一定要加playsinline属性否则在部分浏览器会出现黑屏。video idvideo width640 height480 autoplay playsinline/video canvas idcanvas width640 height480 styledisplay:none;/canvas div button idcaptureBtn classbtn btn-primary识别当前画面/button span idlabel classbadge bg-success未识别/span /div img idresult classimg-fluid mt-2 alt识别结果canvas与video的宽高一致保证截图不会变形。button按钮触发一次识别实际项目中可以改成定时器自动识别。const video document.getElementById(video); const canvas document.getElementById(canvas); const ctx canvas.getContext(2d); async function startCamera() { // 请求摄像头权限并绑定画面 const stream await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 }, audio: false }); video.srcObject stream; } document.getElementById(captureBtn).addEventListener(click, captureFrame);getUserMedia返回的stream直接赋值给video.srcObject浏览器会自动播放。video约束里的宽高固定为640x480过大的分辨率会让Base64字符串变大上传延迟增加。如果摄像头不支持这个分辨率浏览器会自动选择一个接近的值不影响代码逻辑。4.2 帧抓取与Base64上传帧抓取就是把当前video画面画到canvas上再通过toDataURL转成Base64。上传时用FormData携带这个字符串和CSRF token后端解出来就是一个完整JPEG文件。不推荐把Base64放在JSON里因为JSON转义会引入额外开销。async function captureFrame() { if (video.readyState ! 4) return; ctx.drawImage(video, 0, 0, 640, 480); const dataUrl canvas.toDataURL(image/jpeg, 0.8); const formData new FormData(); formData.append(frame, dataUrl); formData.append(csrfmiddlewaretoken, getCookie(csrftoken)); const response await fetch(/detect/camera/, { method: POST, body: formData }); const result await response.json(); if (result.result_url) { const img new Image(); img.onload () { document.getElementById(result).src result.result_url; if (result.labels result.labels.length 0) { document.getElementById(label).textContent result.labels[0].name; } else { document.getElementById(label).textContent 未识别; } }; img.src result.result_url; } }readyState等于4表示video已经加载到足够数据否则drawImage会捕获黑帧。toDataURL的第二个参数0.8表示JPEG质量摄像头画面通常不需要1.0质量太高只会增加网络传输量。getCookie函数从document.cookie中解析csrftokenDjango对AJAX POST请求要求携带这个token否则返回403错误。返回结果里的result_url是后端保存结果图的地址用Image预加载后再赋值给显示img避免画面闪断。4.3 Django后端摄像头帧接收与保存摄像头接口不需要接收文件对象而是接收Base64字符串。在views.py里定义detect_camera先取出frame字段去掉data:image/jpeg;base64,前缀然后base64解码再用cv2.imdecode还原成图像。这样可以绕开文件上传的multipart解析让前端直接发字符串。import base64 import os import uuid import cv2 import numpy as np from django.http import JsonResponse def detect_camera(request): if request.method ! POST: return JsonResponse({error: method not allowed}, status405) frame_b64 request.POST.get(frame, ) if not frame_b64: return JsonResponse({error: frame is empty}, status400) # 去掉dataURL头只保留Base64字符 if , in frame_b64: frame_b64 frame_b64.split(,)[1] img_bytes base64.b64decode(frame_b64) # 将字节流转换为numpy数组再解码为图像 img_array np.frombuffer(img_bytes, dtypenp.uint8) img cv2.imdecode(img_array, cv2.IMREAD_COLOR) model get_model() results model(img) rendered results.render()[0] result_dir media/camera_results os.makedirs(result_dir, exist_okTrue) filename uuid.uuid4().hex .jpg result_path os.path.join(result_dir, filename) cv2.imwrite(result_path, rendered) labels results.pandas().xyxy[0].to_dict(records) return JsonResponse({ result_url: /media/camera_results/ filename, labels: labels })split(,)[1]是一个从dataURL中取原始数据的通用写法兼容canvas.toDataURL的输出。imdecode从内存缓冲区解码得到的是BGR顺序的图和YOLOv5期望一致。输出文件用uuid命名每帧识别都会生成新图片长时间运行会导致磁盘占用不断增长建议在保存时加入按天分目录的逻辑或者定期清理。4.4 结合Bootstrap/Layui美化识别界面源码包里带了bootstrap.min.css、flat-ui.min.css、layui.css说明界面层直接复用这些成熟样式。在templates/index.html中可以用card组件把小窗口做成独立卡片左侧显示摄像头实时画面右侧显示识别结果和最近一次的标签。div classrow div classcol-md-6 div classcard div classcard-header实时画面/div div classcard-body text-center video idvideo classimg-fluid autoplay playsinline/video /div /div /div div classcol-md-6 div classcard div classcard-header识别结果/div div classcard-body text-center img idresult classimg-fluid alt识别结果/ p classmt-2 span idlabel classbadge bg-primary等待识别/span button classbtn btn-light idcaptureBtn识别画面/button /p /div /div /div /div这里用btn-light而不是btn-primary让“识别画面”按钮在摄像头画面旁边不那么抢眼。如果要做成定时识别可以在startCamera成功后调用setInterval(captureFrame, 500)每500毫秒识别一帧。频率太高时CPU可能跟不上配置低就改成1000毫秒。现象原因处理方法摄像头黑屏video标签没有playsinline加上playsinline属性403 CSRF校验失败AJAX请求缺少csrfmiddlewaretoken从cookie读取token并写入表单识别结果延迟高每帧都保存完整图片降低截取分辨率或把间隔调大多个框重叠严重NMS阈值过低把model.iou调到0.5这几个问题都是实际运行中容易遇到的。前端报错时先看浏览器控制台Network标签页能看到接口返回的状态码403是CSRF问题500则是Python代码异常通常Django控制台会打印traceback。5. 模型鲁棒性调优与部署验证训练结束时不能只看训练集mAP需要在低像素、夜景、雨雾样本上单独验证。常见做法是准备一个weather_val文件夹里面全是原始相机直出的未处理照片再用下面的命令批量跑一次。python detect.py --weights runs/train/traffic_exp/weights/best.pt \ --source weather_val --img 640 --conf 0.25 \ --save-txt --save-conf --project runs/val_weather--save-txt保存每个目标的类别和坐标--save-conf额外保存置信度。如果天气样本的召回率低于清晰样本说明增强策略还不够可以回到第2章调高hsv_v和离线雾天增强强度。部署推理阶段还有两个实用技巧。第一个是开启测试时增强在detect.py或val.py加--augment参数YOLOv5会对输入做翻转、缩放、色彩调整等多尺度推理通常能提升两到三个点mAP但CPU上的耗时会翻三倍。第二个是适当降低模型阈值夜间漏检多时把model.conf从0.25降到0.2如果画面中误报增多优先调高model.iou到0.5。# 部署时的模型参数 model.conf 0.22 model.iou 0.5 model.max_det 20 # 导出ONNX加速CPU推理 model.export(imgsz640, formatonnx, dynamicTrue)conf降到0.22后会有更多候选框进入NMS配合iou0.5可以抑制重叠框。model.export导出ONNX格式之后用onnxruntime替换torch推理在CPU上能快一截dynamic参数让模型接受不同尺寸输入。注意导出ONNX后需要修改utils/detector.py的加载逻辑换成onnxruntime.InferenceSession来加载。最后用Django自带的测试客户端验证接口是否完整python manage.py shell -c from django.test import Client; cClient(); print(c.post(/login/, {username:admin,password:admin123}).status_code)这个命令模拟浏览器向登录接口发POST请求返回302说明登录成功并跳转到了主页。接着把摄像头几帧图片批量POST到/detect/camera/观察返回JSON里labels是否包含预期类别这一步通过就可以放到内网环境给业务人员试用了。本文还有配套的精品资源点击获取