☰
YOLOv8交通标志检测系统全流程实战:数据集转换、训练与UI封装
2026/10/2 19:01:58 网站建设 项目流程

简介:面向期末大作业与课程设计的YOLOv8交通标志检测识别系统源码,专为需要完成课程项目或毕业设计的学生准备,也适合计算机视觉新人学习目标检测的落地流程。系统可实时检测限速、禁止、指示等常见交通标志,适用于智能交通、辅助驾驶与城市管理等场景。资源共21个文件,涵盖5个Python脚本、3个UI界面文件、训练好的模型权重、配置说明及示例图片等,其中Python脚本负责推理与主流程,UI文件定义交互布局,模型权重用于直接加载检测,压缩包整体82.65MB,目录结构清晰,便于按需查找和扩展。界面基于PySide6开发,交互直观,检测结果可视化;源码注释详细,降低了理解门槛,也能帮助读者快速掌握YOLOv8推理、界面联动与工程组织方式。资源包内还含README部署参考与示例图片,可辅助环境搭建和结果预览。目前已有97人学习,适合用于课程设计、期末答辩演示,以及在此基础上做功能增强与算法改进。

1. 交通标志识别期末项目,为什么选择 YOLOv8 这条主线

期末项目最怕的不是模型效果差,而是交付时拿不出一个「能演示、能讲清楚、能复现」的完整系统。交通标志检测识别恰好是这类项目的标准答案:数据有公开集、任务有明确边界、YOLOv8 又有现成的训练和推理链路,从头到尾三天左右就能跑通。你在知乎、B 站和 GitHub 上搜「YOLOv8 交通标志检测识别系统源码」,看到的绝大部分期末项目都长这样:TT100K 或 CCTSDB 数据集整理成 YOLO 格式 → 训练 yolov8s 或 yolov8n → 封装一个带 UI 的检测界面。这篇就把这条链路完整拆开,从数据集转换脚本写到最后一个演示技巧,你照着做就能交差,也能在答辩时把每个参数讲明白。

2. 把 TT100K 整理成 YOLOv8 能吃的格式:转换脚本与类别处理

2.1 交通标志数据集选型:TT100K 与 CCTSDB 怎么选

做交通标志检测,主流数据集就两个:清华的 TT100K 和长沙理工的 CCTSDB。TT100K 的标注更细,但坑也很明显——原始标注是 JSON 格式,而且类别极多,直接拿去训练会出现一堆只有十几张图的小类别,模型根本学不出来。CCTSDB 类别少,只有 prohibitory、mandatory、warning 三类,训练省事,但答辩时能讲的细节也少。期末项目我一般推荐 TT100K:类别多反而成了你的素材,你可以自己设计「合并哪些类别、保留哪些类别」这个环节,这本身就是答辩时能讲清楚的一个技术决策。

TT100K 原始包里有 images 和 annotations 两个目录,其中 train 和 test 子目录里存放对应的图片,annotations.json 里记录了每张图的 bndbox 和 category 字段。你需要把这些 JSON 解析成 YOLOv8 需要的 txt 格式:每行一个目标,格式为class_id x_center y_center width height,其中坐标是归一化到 [0,1] 的数值,宽高按图片尺寸归一化处理。

2.2 转换脚本:JSON 标注到 YOLO txt 的完整实现

常见做法是写一个独立脚本跑一次,把整个训练集和测试集一次性转完。下面的脚本就是干这件事的:

import json import os from pathlib import Path # 只需要按需保留的类别,key 是 TT100K 类别名,value 是重新编号后的 id CLS_MAP = { "i2": 0, # 禁止驶入 "i4": 1, # 禁止机动车 "i5": 2, # 禁止鸣喇叭 "il100": 3, # 限速 100 "pl50": 4, # 限速 50 "pne": 5, # 禁止停车 "w13": 6, # 注意行人 } ROOT = Path("TT100K") IMG_DIR = ROOT / "images" LABEL_DIR = ROOT / "labels" LABEL_DIR.mkdir(exist_ok=True) def convert_split(split: str): """转换 train 或 test 子集""" ann_file = ROOT / "annotations.json" with open(ann_file, "r", encoding="utf-8") as f: anns = json.load(f) split_imgs = [p for p in (IMG_DIR / split).glob("*")] for img_path in split_imgs: img_name = img_path.stem if img_name not in anns["imgs"]: continue ann = anns["imgs"][img_name] img_w, img_h = ann["width"], ann["height"] objects = ann.get("objects", []) lines = [] for obj in objects: cls = obj["category"] if cls not in CLS_MAP: continue bbox = obj["bbox"] # [x_min, y_min, x_max, y_max] x_min, y_min, x_max, y_max = bbox # 过滤掉明显异常的框 if x_max <= x_min or y_max <= y_min: continue x_center = (x_min + x_max) / 2 / img_w y_center = (y_min + y_max) / 2 / img_h w = (x_max - x_min) / img_w h = (y_max - y_min) / img_h lines.append(f"{CLS_MAP[cls]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if lines: label_path = LABEL_DIR / f"{img_name}.txt" label_path.write_text("\n".join(lines), encoding="utf-8") convert_split("train") convert_split("test")

这段脚本有几个关键设计。第一,CLS_MAP 决定了你最终模型能识别哪些标志,不是 TT100K 里所有类别都训,只选在道路场景中出现频率高、且样本数足够多的类别,不然会出现类别严重不均衡。第二,归一化坐标必须在转换脚本里完成,YOLOv8 训练时读 txt 里的坐标默认是归一化值,如果这里给了像素值,loss 会直接爆炸,表现为训练初期 mAP 一直是 0。第三,过滤x_max <= x_min这类异常框很有必要,TT100K 早期版本的标注里有少量脏数据。

转换完成后,检查一下 labels 目录下的文件数量和 images 是否对应。一个容易被忽略的坑是:TT100K 的 train 和 test 子目录里有些图片在 annotations.json 里没有对应记录,这些图片不需要生成空 txt,直接跳过即可。空 txt 文件在训练时虽然不会报错,但会触发数据加载器的警告,干扰你判断真正的问题。

2.3 类别再合并与样本均衡:直接决定你 mAP 的上限

只做格式转换还不够,样本分布是个绕不开的问题。TT100K 的类别长尾非常严重:il100 有几千张,某些警告类标志只有三十几张。如果你保留的类别里有这种极端长尾,训练出来的模型会对低频类别直接失灵,表现为推理时该类标志完全检不出。常见的处理办法有三个:一是合并语义相近的类别,比如各方向限速标志合并成一个「限速」类;二是对低频类别做离线增强——复制样本并做透视变换、亮度扰动后重新放回训练集;三是干脆只保留频率前 10 到 15 的类别,答辩时就说「工程上优先保证高频类别的识别准确率」。

我一般建议期末项目做 7 到 10 个类别就够了。类别越少越容易训出好看的结果,演示时也更稳定。做完类别筛选后,重新统计一下训练集里每个类别的图片数,如果最少的类别不足 100 张,就要考虑增强或者合并。这一步省掉的话,后面训练完打开 results.csv 看各类别 AP,你会发现至少有两三个类别 AP 不到 0.3,最后平均 mAP 被拉得很低,答辩时不好看。

3. 训练配置与调参:跑通 100 轮之前先看懂这几个参数

3.1 环境搭建:CPU 版与 GPU 版的取舍

期末项目的训练环境一般分两种情况。如果你手里只有一台普通笔记本,没有 NVIDIA 显卡,那就走 CPU 版路线:安装 CPU 版 PyTorch,然后 pip install ultralytics。CPU 训练 yolov8n 加上 3000 张左右的数据,100 轮的时间大约在 3 到 6 小时,勉强能接受。如果你有 GTX 1660 Ti 或更好的显卡,直接装 CUDA 版 PyTorch,一个多小时就能训完 yolov8s 的 100 轮。

环境搭建的常见做法是:

# 创建独立虚拟环境,避免把系统 Python 搞乱 conda create -n yolo python=3.10 -y conda activate yolo # CPU 版 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install ultralytics # GPU 版(注意 CUDA 版本要和驱动匹配) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install ultralytics

装完后立刻验证一次yolo predict model=yolov8n.pt source=bus.jpg,能出结果就说明环境没问题。这个验证很关键,我见过不少人在这一步翻车:ultralytics 装好了,但 torch 的 CUDA 版本不匹配,import 时直接报错,或者训练时卡在 "CUDA unavailable" 上。跑一次预测能同时确认推理链路和依赖完整性,比直接开训练省时间。

3.2 数据集 YAML 与训练命令:最小可复现配置

训练前要写一个数据集配置文件,告诉 ultralytics 数据在哪、类别有哪些。这个 YAML 文件是整个训练流程的入口,写错类别列表会导致灾难性的后果——模型能训,但推理结果全部对不上号。

# traffic_sign.yaml path: /home/user/project/TT100K train: images/train val: images/test # 类别顺序必须和 CLS_MAP 里的编号完全一致 names: 0: i2 1: i4 2: i5 3: il100 4: pl50 5: pne 6: w13

注意一个关键点:names的键是 0 到 6,值是类别名,这个顺序一旦固定,后面训练和推理都用它。训练命令:

yolo detect train \ data=traffic_sign.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ patience=15 \ cache=True \ project=runs/traffic_sign \ name=exp1

这里有几个参数值得逐条说。model=yolov8s.pt表示加载 COCO 预训练权重做迁移学习,在交通标志这种小数据集上,从 COCO 权重起步比从零训练收敛快得多,这也是期末项目能在短时间训出可用模型的根本原因。imgsz=640是训练输入尺寸,交通标志在整图中偏小,用 640 在速度和召回之间比较平衡,升到 800 对小目标有改善但训练时间变长。patience=15表示验证集指标连续 15 轮不提升就早停,防止你忘记盯训练导致过拟合。cache=True会把数据集缓存进内存,训练速度提升明显,但内存占用会变大,8G 内存的机器建议关掉。

训练跑起来之后,看训练日志里的 box_loss、cls_loss、dfl_loss 三个值。正常情况是前 10 轮 loss 快速下降,之后缓慢下降。如果 loss 曲线出现明显反弹,说明学习率偏大或者 batch 太小,YOLOv8 默认的优化器是 SGD,你可以改成 AdamW 再试一轮,这是期末项目里最常用的备选方案。

3.3 训练后必看的结果文件:results.csv 和混淆矩阵

训练完成后,去runs/traffic_sign/exp1/目录下看几个文件。results.csv里记录了每一轮的精确率、召回率、mAP50 和 mAP50-95,这是答辩时最直接的量化依据。confusion_matrix.png能帮你快速发现哪些类别互相混淆——比如限速 50 和限速 100 容易互相误判,因为外形相似且尺寸小。val_batch0_pred.jpg是验证集的可视化预测,用来向别人展示模型的直观效果。

一个常见的疑惑是 mAP50 和 mAP50-95 有什么区别。mAP50 只要求预测框和真实框的 IoU 大于 0.5 就算命中,mAP50-95 则把阈值从 0.5 到 0.95 按步长 0.05 累加求平均。交通标志检测这个场景,交叠率要求其实不高——只要框能把标志大致框住即可——所以你答辩时主推 mAP50 就好,一般做到 0.85 以上就算不错的期末项目水平。mAP50-95 偏低也不要慌,它包含了对边界框精度的更严格评价,小目标物体天然吃亏。

4. 从权重到「系统」:检测封装与界面源码怎么组织

4.1 源码结构:期末项目该有的模块划分

训练出 best.pt 之后,真正的「系统源码」才开始。一个合格的期末项目代码包,通常长这样:

traffic_sign_system/ ├── train.py # 训练入口,封装上面的训练命令 ├── detect.py # 核心检测逻辑,封装 YOLO 推理 ├── ui_main.py # PyQt5 界面入口 ├── utils/ │ ├── dataset_split.py # 数据集划分与格式转换 │ └── plot_loss.py # 画损失曲线 ├── weights/ │ └── best.pt # 训练好的权重 ├── requirements.txt └── README.md

这个结构不复杂,但每一层都有明确职责。detect.py 是对 ultralytics 推理接口的二次封装,ui_main.py 只负责界面交互,不写任何检测逻辑。这样答辩的时候,被问到「检测效果不好怎么排查」,你能理直气壮地说「先单测 detect.py,再查界面调用」,而不是在一坨混着 GUI 代码的脚本里找 bug。

4.2 检测封装:图片、视频、摄像头三种入口统一接口

detect.py 的核心功能是把 YOLO 预测结果转成界面能用的数据格式。下面这段是常见实现:

from ultralytics import YOLO class TrafficSignDetector: def __init__(self, weights_path: str, conf: float = 0.4, iou: float = 0.5): self.model = YOLO(weights_path) self.conf = conf self.iou = iou self.names = self.model.names def predict(self, source): """source 可以是图片路径、视频路径或摄像头编号""" results = self.model.predict( source=source, conf=self.conf, iou=self.iou, imgsz=640, verbose=False, ) return self._format_results(results) def _format_results(self, results): dets = [] for r in results: boxes = r.boxes for box in boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = float(box.conf[0]) cls_id = int(box.cls[0]) dets.append({ "bbox": [x1, y1, x2, y2], "conf": conf, "cls_name": self.names[cls_id], }) return dets

这段代码有两个值得注意的设计。第一,_format_results把 ultralytics 的 Results 对象转成了纯 Python 字典列表,好处是 UI 层不依赖 ultralytics 的数据结构,以后换模型框架不用改界面代码。第二,conf和iou作为构造参数暴露出来,这样界面上的置信度滑条可以直接改这个值,不用动推理逻辑。

关于视频流和摄像头推理,YOLOv8 的 predict 接口支持传入摄像头编号,比如source=0表示打开第一个摄像头。但要注意,逐帧推理在 CPU 上通常只有 5 到 10 帧每秒,GPU 上能到 30 帧以上。如果发现界面卡顿,优先把 imgsz 降到 480,或者换 yolov8n 权重,而不是在代码里加线程——线程只能让界面不卡死,推理速度本身不会有提升。

4.3 UI 界面:PyQt5 检测系统的骨架

期末项目的 UI 不需要花哨,但要功能完整:图片选择、开始检测、结果显示、实时画面。用 PyQt5 实现时,核心是 QThread 跑推理,主线程只管刷新画面,不然界面会直接冻住。下面给一个简化版:

import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QPushButton, QFileDialog, QVBoxLayout, QWidget) from PyQt5.QtGui import QPixmap, QImage import cv2 from detect import TrafficSignDetector class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("YOLOv8 交通标志检测系统") self.detector = TrafficSignDetector("weights/best.pt", conf=0.4) self.image_label = QLabel("选择图片开始检测") btn_open = QPushButton("打开图片") btn_open.clicked.connect(self.open_image) layout = QVBoxLayout() layout.addWidget(self.image_label) layout.addWidget(btn_open) container = QWidget() container.setLayout(layout) self.setCentralWidget(container) def open_image(self): path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "Image files (*.jpg *.png *.bmp)" ) if not path: return dets = self.detector.predict(path) img = cv2.imread(path) for det in dets: x1, y1, x2, y2 = map(int, det["bbox"]) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) label = f"{det['cls_name']} {det['conf']:.2f}" cv2.putText(img, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) rgb_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w, ch = rgb_img.shape qimg = QImage(rgb_img.data, w, h, ch * w, QImage.Format_RGB888) self.image_label.setPixmap(QPixmap.fromImage(qimg).scaledToWidth(800))

这个骨架跑通后,你可以往里面加摄像头读取、视频进度条、置信度滑条。但注意一点:滑条调节的置信度之所以要实时生效,是因为系统中图片检测、视频检测和摄像头检测共用同一个 detector 实例。如果你把置信度写死在界面逻辑里,三个入口就会各自维护一套参数,后期改起来非常痛苦。这种「参数入口统一」的设计,答辩时也是加分项。

5. 训练到演示的 5 个典型翻车现场:避坑实测

5.1 环境冲突:ultralytics 装上了,import 却报错

现象是训练命令yolo detect train一执行就报ModuleNotFoundError或AttributeError,而且报错信息指向的模块各不相同,看起来毫无规律。原因多半是环境中存在多个 Python 环境,pip 装到了某个环境,但命令行用的是另一个环境。

解决方法是先确认当前解释器路径:which python和python -m pip show ultralytics,如果 show 出来的路径和你运行命令的解释器不一致,说明环境混了。最稳妥的后悔药是直接重建一个干净的 conda 环境,按上面的步骤重装。这不算什么高深技术,但期末周这种时间紧张的时候,重装环境反而比排查依赖树快得多。

5.2 训练时数据集警告 class X has no labels

现象是训练日志里出现WARNING: 0 labels found in 313 images for class X,而且对应的类别 AP 永远是 0。原因有两种:一是这个类别的样本在转换脚本里被过滤掉了,比如 CLS_MAP 里写了这个类别,但标注框坐标异常被x_max <= x_min的过滤条件删光了;二是数据集划分出了问题,该类图片全部被划到验证集而训练集里一张没有。

解决方法是先统计labels目录下每个类别的样本数量:用脚本遍历所有 txt,对每个类别计数,确认训练集和验证集都有该类别。如果确实某个类别样本数为 0,回到 CLS_MAP 检查类别名拼写,TT100K 的类别名都是小写字母加数字,拼写不匹配是常事。

5.3 训练中途显存溢出 CUDA out of memory

现象是训练跑到第 6 到第 8 轮时(刚好是 Mosaic 增强开始生效的阶段)直接报 OOM 退出。原因不是显存真的不够,而是cache=True把数据集缓存进显存——不是,cache 是缓存在内存里,真正吃显存的是 batch size 和 imgsz。你用的 16 batch + 640 imgsz,在 6G 显存的卡上已经是临界值了。

解决方法是先把 batch 降到 8,如果还崩就关掉 cache 并把 imgsz 降到 512。显存溢出没有玄学,就是 batch 和 imgsz 的乘积超了卡的物理极限。还有一种隐蔽情况是别的进程占了显存,训练前用nvidia-smi看一下,如果有残留的 Python 进程,先 kill 掉再训练。

5.4 摄像头推理卡成 PPT,检测结果却正常

现象是图片检测速度很快,一接摄像头就一帧一帧地跳。原因是逐帧推理的耗时是固定的,摄像头每传一帧,模型就要完整推理一次。CPU 上跑 yolov8s 做一次 640 分辨率推理大约需要 300 到 600 毫秒,帧率自然只有 2 到 3 帧。

解决办法不是改代码,而是降推理负载:换 yolov8n 权重、imgsz 降到 480、conf 阈值提高到 0.5。如果还卡,就把视频流做跳帧处理,每 3 帧检测一次,中间两帧直接显示原画面。跳帧这个方案在答辩演示时效果很好,画面连续性好,观众感知不到你做了跳帧。

5.5 模型在训练集上效果惊艳,演示时却频繁漏检

现象是验证集 mAP50 有 0.88,但拿手机拍的真实道路照片测,好几个标志直接漏掉。原因是数据分布差异:TT100K 的图片大多来自行车记录仪,和实验室里拍的测试照片在角度、光线、模糊程度上差异很大。漏检大部分发生在小目标上——标志在画面里只占几十个像素,模型学到的特征不够。

解决方法是去验证集里挑那些标志位置居中、尺寸足够大的图片做演示素材,再把 conf 阈值从默认的 0.25 降到 0.15。对于期末项目来说,这个操作不是为了刷指标,而是为了让演示过程稳定不翻车。不要把这个技巧当成规避问题的手段,答辩时被问到「你这个系统在真实场景下表现如何」,你要诚实回答小目标漏检是当前方案的已知短板,并说出 upsampling 或切图检测的改进思路。

6. 让演示更稳的三个细节:置信度阈值、素材挑选与损失曲线图

答辩现场的不可控因素很多,投影仪分辨率、电脑性能、现场光线都可能让你的检测结果翻车。三个细节能让你的演示明显更稳。第一,在 UI 里把置信度滑条的默认值设为 0.3,而不是 ultralytics 默认的 0.25。这个值在 TT100K 测试集上经过验证:阈值降到 0.25 时,误检框会明显变多,演示时屏幕上同时出现几个错误的框,你还要花时间解释,非常掉价。0.3 是一个平衡点,漏检可控、误检少、画面干净。

第二,演示素材要提前筛选。从验证集里找 8 到 10 张标志清晰、尺寸适中、包含两到三个类别目标且无遮挡的图片,按顺序放在一个专门的 demo 文件夹里。不要现场随机选图片,更不要临时去网上找。你训练好的模型对 TT100K 的测试集有天然适应性,但网上随便找的图片大概率来自其他数据集或真实拍摄,分布差异会导致不可预期的结果。素材固定之后,提前用 UI 工具完整过一遍,确认每张图都能检出至少一个目标。

第三,训练曲线图要用代码画,不要截图训练日志。用 pandas 读 results.csv,画出 loss 曲线和 mAP50 曲线,存成 PNG 放在项目根目录下。答辩时讲模型的收敛过程,一张图比一百句文字都直观。

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/traffic_sign/exp1/results.csv") # 去掉首列,它只是轮次索引 df = df.drop(columns=df.columns[0]) fig, axes = plt.subplots(1, 2, figsize=(12, 4)) # 损失曲线 loss_cols = [c for c in df.columns if "loss" in c] for col in loss_cols: axes[0].plot(df[col], label=col) axes[0].set_title("Loss Curves") axes[0].legend() # mAP 曲线 axes[1].plot(df["metrics/mAP50(B)"], label="mAP50") axes[1].plot(df["metrics/mAP50-95(B)"], label="mAP50-95") axes[1].set_title("mAP Curves") axes[1].legend() plt.tight_layout() plt.savefig("training_curves.png", dpi=150)

这个脚本核心就两个点:filter 出 loss 列和 mAP 列,然后双轴画图。用它画出的图能直观展示模型收敛,答辩评委一眼就能看出你做了训练分析,而不是只跑了默认参数。我会在答辩前把这张图也放进 PPT 里,讲到训练过程时切过去,比现场打开终端翻日志要专业得多。

这套流程走下来,数据集转换、训练调参、系统封装、演示素材,每一环都落在可控范围内。期末项目做到这个程度,不仅是为了交差,也给你以后做检测类任务留了一套可以复用的模板。把 best.pt 和数据集留着,后面想尝试轻量化部署到 RK3588 这类边缘设备时,你只需要重新导出 ONNX 再转 RKNN,前面的训练和验证环节不用重做。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询