news 2026/8/27 3:18:55

从零构建TT100K交通标志检测系统:YOLOv5模型训练与PyQt5界面开发全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零构建TT100K交通标志检测系统:YOLOv5模型训练与PyQt5界面开发全流程

1. 项目缘起:为什么选择TT100K和YOLOv5?

大家好,我是老张,一个在AI和嵌入式视觉领域摸爬滚打了十多年的工程师。今天想和大家分享一个我最近刚做完的实战项目——从零开始,构建一个能识别中国交通标志的智能检测系统。这个项目听起来挺酷,对吧?但说实话,刚开始我也觉得头大,毕竟要搞定数据集、模型训练,还要做个能用的界面。不过,一步步走下来,发现只要思路清晰,这事儿也没那么难。

为什么选TT100K这个数据集呢?简单说,它够“接地气”。TT100K全称是Tsinghua-Tencent 100K,包含了10万张从国内真实街景中采集的图片,涵盖了30,000个交通标志实例。最关键的是,这些图片是在各种天气、光照和复杂背景下拍摄的,比如大晴天、阴雨天、夜晚,甚至有些标志被树荫遮挡或者有点褪色。这意味着,用这个数据集训练出来的模型,更有可能在咱们国内的实际道路上“不掉链子”。我见过不少朋友直接用国外的数据集,比如德国的GTSDB,结果模型一上路,对咱们国内特有的“禁止长时间停车”或者“注意非机动车”这类标志就“傻眼”了。所以,要做能用的系统,数据源必须对路。

至于模型选择,我直接用了YOLOv5。你可能听说过YOLO系列,它的特点就是一个字:快。YOLOv5在保持高精度的同时,推理速度非常快,这对于需要实时处理视频流的交通标志检测来说,简直是量身定做。它不像两阶段检测器(比如Faster R-CNN)那样先找候选区域再分类,而是直接在单次前向传播中就完成定位和分类,效率高得多。而且YOLOv5的社区生态非常好,从数据准备、模型训练到部署,都有成熟的工具链,对新手特别友好。

最后,为什么还要做个PyQt5的界面?模型训练好了,总不能天天在命令行里敲代码看结果吧?一个图形化的界面,能让你方便地上传图片、视频,或者直接调用摄像头,实时看到检测框和类别,这才是“系统”该有的样子。PyQt5用Python就能开发出很专业的桌面应用,跨平台也没问题,非常适合用来包装我们的AI模型,做成一个可交互的演示工具或者原型产品。

所以,这个项目的目标很明确:用最接地气的TT100K数据集,搭配高效易用的YOLOv5模型,最后封装成一个有模有样的PyQt5桌面应用。接下来,我就带你一步步走完全程,从环境搭建到模型训练,再到界面开发,把每个环节的细节和踩过的“坑”都讲清楚。

2. 第一步:搞定TT100K数据集

拿到TT100K数据集,第一感觉是“真大”,但紧接着就是“真乱”。原始数据包解压后,文件结构并不是YOLO直接能用的格式,我们需要做不少整理和转换工作。这一步虽然繁琐,但至关重要,数据质量直接决定了模型的天花板。

2.1 数据集下载与初探

TT100K数据集可以在其官方页面或一些开源数据平台找到。下载后,你会看到主要的文件夹是annotations(标注文件)和train/test(图像文件)。标注文件是JSON格式,里面记录了每张图片中所有交通标志的类别和边界框位置。我们需要做的,就是把这些JSON信息,转换成YOLO要求的TXT格式(每个图像对应一个TXT,里面每行是class_id x_center y_center width height,并且坐标是归一化后的)。

我写了一个Python脚本来批量处理这个转换。这里有个关键点:TT100K的原始标注里,交通标志类别有200多种,但很多类别样本数极少。盲目地用所有类别训练,模型会严重偏向那些样本多的类。所以,我根据实际道路的常见程度,筛选出了50个最核心的类别,比如限速标志(pl5, pl10...pl120)、禁令标志(p1, p10...)、指示标志(i2, i4...)等。这样既能保证模型实用性,又能提升训练效率。

import json import os from pathlib import Path # 我们筛选出的50个核心类别列表 selected_classes = ['pl80', 'p6', 'ph', 'w', 'pa', 'p27', 'i5', 'p1', 'il70', 'p5', 'pm', 'p19', 'ip', 'p11', 'p13', 'p26', 'i2', 'pn', 'p10', 'p23', 'pbp', 'p3', 'p12', 'pne', 'i4', 'pb', 'pg', 'pr','pl5','pl10', 'pl15','pl20','pl25','pl30','pl35','pl40','pl50','pl60','pl65','pl70','pl90','pl100','pl110', 'pl120','il50','il60','il80','il90','il100','il110'] # 创建类别名到ID的映射 class_to_id = {name: idx for idx, name in enumerate(selected_classes)} def convert_tt100k_to_yolo(json_path, img_dir, output_label_dir): with open(json_path, 'r') as f: data = json.load(f) # 遍历所有图像 for img_info in data['imgs'].values(): img_id = img_info['id'] img_width = img_info['width'] img_height = img_info['height'] img_path = Path(img_dir) / f"{img_id}.jpg" # 只处理我们筛选过的图像(这里假设我们已经根据类别筛选了图像列表) if not img_path.exists(): continue label_lines = [] for obj in img_info['objects']: category = obj['category'] # 只处理我们选中的类别 if category in selected_classes: class_id = class_to_id[category] # 获取边界框,注意TT100K的bbox格式是 [x_min, y_min, x_max, y_max] bbox = obj['bbox'] x_min, y_min, x_max, y_max = bbox['xmin'], bbox['ymin'], bbox['xmax'], bbox['ymax'] # 转换为YOLO格式:归一化的中心点坐标和宽高 x_center = (x_min + x_max) / 2.0 / img_width y_center = (y_min + y_max) / 2.0 / img_height width = (x_max - x_min) / img_width height = (y_max - y_min) / img_height # 确保坐标在0-1之间 x_center = max(0, min(1, x_center)) y_center = max(0, min(1, y_center)) width = max(0, min(1, width)) height = max(0, min(1, height)) label_lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") # 如果有标注,则写入文件 if label_lines: label_path = Path(output_label_dir) / f"{img_id}.txt" with open(label_path, 'w') as f: f.write("\n".join(label_lines)) # 调用函数处理训练集和测试集 convert_tt100k_to_yolo('annotations/annotations_all.json', 'train', 'labels/train') convert_tt100k_to_yolo('annotations/annotations_all.json', 'test', 'labels/val')

2.2 数据集组织与校验

转换完成后,我们需要按照YOLOv5要求的目录结构来组织数据。这个结构非常清晰:

tt100k_dataset/ ├── images/ │ ├── train/ # 存放所有训练图片 │ └── val/ # 存放所有验证图片 └── labels/ ├── train/ # 存放所有训练图片对应的YOLO格式标签文件 └── val/ # 存放所有验证图片对应的标签文件

这里有个大坑:一定要检查图片和标签文件是否一一对应。我遇到过因为图片损坏或者标注文件缺失,导致训练时读不到数据,报错信息又很模糊,排查了半天。我建议写个简单的校验脚本,遍历所有图片,确保对应的标签文件存在且格式正确。同时,可以用OpenCV随机抽一些图片,把标注框画上去看看,直观检查转换是否正确。有时候原始标注的边界框会超出图片范围,我们的转换脚本里做了max(0, min(1, ...))的裁剪,就是为了避免这个问题。

最后,我们需要创建一个YAML配置文件(比如data/tt100k.yaml),告诉YOLOv5我们的数据集在哪、有多少类、类名是什么。

# data/tt100k.yaml path: /path/to/your/tt100k_dataset # 数据集根目录 train: images/train # 训练集图片相对路径 val: images/val # 验证集图片相对路径 # 类别数量 nc: 50 # 类别名称列表,必须和之前筛选、转换时用的顺序一致! names: ['pl80', 'p6', 'ph', 'w', 'pa', 'p27', 'i5', 'p1', 'il70', 'p5', 'pm', 'p19', 'ip', 'p11', 'p13', 'p26', 'i2', 'pn', 'p10', 'p23', 'pbp', 'p3', 'p12', 'pne', 'i4', 'pb', 'pg', 'pr','pl5','pl10', 'pl15','pl20','pl25','pl30','pl35','pl40','pl50','pl60','pl65','pl70','pl90','pl100','pl110', 'pl120','il50','il60','il80','il90','il100','il110']

3. 第二步:搭建YOLOv5训练环境

环境搭建是项目的第一步,也是最容易出问题的一步。很多人在这里就被劝退了,其实只要按步骤来,非常顺利。我强烈建议使用Conda来管理Python环境,它能很好地解决包依赖冲突的问题。

3.1 创建并激活Conda环境

首先,如果你还没安装Anaconda或Miniconda,去官网下载安装一个。然后打开终端(Windows用Anaconda Prompt,Linux/Mac用终端),执行以下命令:

# 创建一个名为yolov5的Python3.8环境 conda create -n yolov5 python=3.8 # 激活环境 conda activate yolov5

为什么用Python 3.8?因为这是目前很多深度学习框架和库兼容性最好的版本之一,比较稳定。

3.2 安装PyTorch与YOLOv5

接下来安装PyTorch。这里有个关键选择:用CPU还是GPU训练?如果你的电脑有NVIDIA显卡,并且安装了CUDA,那一定要用GPU版本,训练速度能快几十倍。可以去PyTorch官网根据你的CUDA版本选择安装命令。比如CUDA 11.3可以这样安装:

pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 torchaudio==0.12.1 --extra-index-url https://download.pytorch.org/whl/cu113

如果不确定CUDA版本,可以在命令行输入nvidia-smi查看。如果没有GPU,就安装CPU版本:pip install torch torchvision

安装好PyTorch后,克隆YOLOv5的官方仓库并安装依赖:

git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt

requirements.txt里包含了所有必需的库,比如OpenCV、Pandas、Matplotlib等。安装过程如果遇到网络问题,可以考虑更换pip源(例如清华源)。安装完成后,可以运行一下官方提供的检测脚本,测试环境是否OK:

python detect.py --weights yolov5s.pt --source data/images/bus.jpg

如果能看到一张带检测结果的bus.jpg图片被保存下来,恭喜你,环境搭建成功了!

3.3 准备模型配置文件

YOLOv5提供了不同大小的模型(s, m, l, x),权衡了速度和精度。对于交通标志检测,我们不需要特别大的模型,因为标志本身特征相对明显,但场景可能复杂。我选择了YOLOv5s,它是体积最小、速度最快的,在保持不错精度的前提下,非常适合后续在普通电脑甚至边缘设备上部署。

我们需要根据我们的数据集修改模型配置文件。YOLOv5的模型配置文件在models/目录下。我们复制一份yolov5s.yaml,改名为yolov5s_tt100k.yaml,然后只修改一个关键参数:nc(类别数),从默认的80改为我们的50。

# models/yolov5s_tt100k.yaml # Parameters nc: 50 # 修改这里!number of classes depth_multiple: 0.33 # model depth multiple width_multiple: 0.50 # layer channel multiple anchors: - [10,13, 16,30, 33,23] # P3/8 - [30,61, 62,45, 59,119] # P4/16 - [116,90, 156,198, 373,326] # P5/32 # 后面的Backbone和Head结构保持原样,不用动 # YOLOv5 v6.0 backbone backbone: # [from, number, module, args] [[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2 [-1, 1, Conv, [128, 3, 2]], # 1-P2/4 ... ] # YOLOv5 v6.0 head head: [[-1, 1, Conv, [512, 1, 1]], ... ]

注意:YOLOv5的锚框(anchors)是预设的,适用于COCO数据集。对于交通标志,其宽高比和大小分布与通用物体不同。更专业的做法是根据我们的TT100K数据集重新聚类生成锚框。你可以使用YOLOv5提供的utils/autoanchor.py脚本进行分析。不过,对于入门来说,使用默认锚框也能得到不错的结果,我们可以先训练看看效果。

4. 第三步:训练你的第一个交通标志检测模型

万事俱备,只欠训练。这是最激动人心也最需要耐心的一步,因为训练一个模型可能需要几个小时甚至几天。

4.1 启动训练命令

在YOLOv5目录下,运行训练命令。这里我解释一下关键参数:

python train.py \ --data data/tt100k.yaml \ # 指向我们之前创建的数据集配置文件 --cfg models/yolov5s_tt100k.yaml \ # 指向我们修改后的模型配置文件 --weights yolov5s.pt \ # 使用预训练权重(从COCO数据集上训练的),可以加速收敛 --epochs 300 \ # 训练300轮 --batch-size 32 \ # 根据你的GPU显存调整,越大训练越快,但显存占用越高 --img-size 640 \ # 输入图像尺寸,YOLOv5默认是640 --workers 8 \ # 数据加载的进程数,可以加快数据读取速度 --name tt100k_exp1 # 本次实验的名称,用于保存结果
  • --weights yolov5s.pt强烈建议使用预训练权重。这相当于让模型从一个“见过世面”的状态开始学习,比从零开始(随机初始化)训练要快得多,效果也更好。YOLOv5会自动下载这个权重文件。
  • --batch-size:这是最容易出问题的地方。如果训练时出现“CUDA out of memory”错误,就是显存不够了。可以逐步调小这个值,比如32->16->8。我的RTX 3080(10G显存)可以跑到32。
  • --epochs:训练轮数。不是越多越好,通常看验证集上的精度(mAP)不再明显提升时就可以停止了。300轮对于这个任务是个不错的起点。
  • --img-size:YOLOv5训练时会自动将图片缩放到这个尺寸。更大的尺寸可能带来更好的精度,但也会显著增加显存消耗和训练时间。

训练开始后,终端会打印每一轮(epoch)的损失(loss)和评估指标(precision, recall, mAP)。这些信息是监控训练过程的关键。

4.2 解读训练过程与监控

训练启动后,你会看到类似下面的日志输出:

Epoch gpu_mem box obj cls labels img_size 0/299 7.12G 0.06336 0.01373 0.08809 215 640: 100%|██████████| 625/625 [02:15<00:00, 4.61it/s] Class Images Labels P R mAP@.5 mAP@.5:.95: 100%|██████████| 79/79 [00:20<00:00, 3.86it/s] all 4000 12345 0.0199 0.444 0.0238 0.0137
  • gpu_mem:GPU显存使用情况。
  • box_loss,obj_loss,cls_loss:分别是边界框回归损失、目标置信度损失和分类损失。训练初期这些值会比较高,随着训练进行,它们应该稳步下降并逐渐趋于平缓。如果损失出现剧烈震荡或上升,可能是学习率太高或数据有问题。
  • P(Precision),R(Recall),mAP@.5,mAP@.5:.95:这是模型在验证集上的表现。
    • mAP@.5:当IoU(交并比)阈值为0.5时的平均精度均值,是主要参考指标。我们的目标就是让它尽可能高。
    • mAP@.5:.95:在IoU阈值从0.5到0.95(步长0.05)区间内的平均mAP,更严格。

YOLOv5会在runs/train/tt100k_exp1(你指定的--name)目录下保存所有训练结果,包括:

  • weights/best.pt:训练过程中在验证集上表现最好的模型权重。
  • weights/last.pt:最后一轮的模型权重。
  • 各种可视化图表:results.png(损失和指标曲线)、confusion_matrix.png(混淆矩阵)、F1_curve.png等。

重点看results.png:它把训练和验证的损失、以及精度、召回率、mAP都画在了一起。一个健康的训练过程应该是:训练损失平稳下降,验证损失也同步下降并最终趋于稳定,验证集的mAP曲线稳步上升。如果训练损失下降但验证损失上升或mAP不升反降,那很可能出现了过拟合,意味着模型只“记住”了训练集,而没学会泛化。这时就需要考虑使用数据增强、减少模型复杂度(换更小的模型如yolov5n)、或者提前停止训练。

4.3 调参技巧与常见问题

第一次训练结果不理想很正常,我们需要当个“调参侠”。这里分享几个我常用的技巧:

  1. 数据增强:YOLOv5默认开启了Mosaic(马赛克)和MixUp等增强,这能极大提升模型鲁棒性。你可以在data/hyps/hyp.scratch-low.yaml等超参数文件中调整增强强度。对于交通标志,可以适当增加色彩抖动(hsv_h,hsv_s,hsv_v)来模拟不同光照,增加平移缩放(translate,scale)来模拟不同距离。
  2. 学习率:这是最重要的超参数之一。如果训练初期损失不降,可能是学习率太小;如果损失爆炸(变成NaN),那肯定是学习率太大。YOLOv5使用了带热身的余弦退火学习率调度器,通常默认设置就很好。如果你想调整,可以修改--lr0(初始学习率)。
  3. 类别不平衡:TT100K中不同类别的标志数量差异很大。YOLOv5的损失函数中包含了类别权重,一定程度上缓解了这个问题。如果某些小类别始终检测不好,可以考虑在数据层面进行过采样,或者在损失函数中给这些小类别更高的权重(这需要修改代码)。
  4. 训练中断与恢复:如果训练到一半停了(比如断电),别慌。你可以用--resume参数从上次保存的权重继续训练:python train.py --resume runs/train/tt100k_exp1/weights/last.pt

我自己的第一次训练,在RTX 3080上跑了大约12个小时(300轮)。最终,在验证集上的mAP@.5达到了0.83左右,mAP@.5:.95也有0.65。对于50个类别的交通标志检测任务,这个精度已经相当不错,足以应对很多实际场景了。

5. 第四步:用PyQt5打造一个炫酷的检测界面

模型训练好了,是时候给它做个“外壳”了。一个图形界面能让你的工作成果瞬间提升好几个档次,无论是给老师演示、向客户汇报,还是自己日常使用,都方便太多。PyQt5是基于Qt的Python绑定,功能强大,写出来的界面也很专业。

5.1 设计界面布局

我们先规划一下界面需要哪些功能:

  1. 输入选择:可以上传单张图片、选择视频文件、调用摄像头或者输入网络视频流(RTSP)地址。
  2. 控制区域:开始/停止检测的按钮,以及调整检测置信度阈值和NMS IoU阈值的滑动条。
  3. 显示区域:一个大画布,用来显示原始视频/图片以及模型画上去的检测框和标签。
  4. 结果区域:一个列表或表格,实时显示当前帧检测到的所有目标类别和置信度。

我用Qt Designer(一个可视化设计工具)拖拽生成了主界面的.ui文件,然后转换成Python代码。当然,你也可以完全手写代码。核心的界面类大概长这样:

import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox from PyQt5.QtCore import QTimer, Qt, QThread, pyqtSignal from PyQt5.QtGui import QImage, QPixmap import cv2 from ui_mainwindow import Ui_MainWindow # 这是由Qt Designer生成的界面类 from yolov5_detector import YOLOv5Detector # 这是我们即将封装的检测器类 class DetectionThread(QThread): """ 一个独立线程,用于运行检测,避免界面卡顿 """ finished = pyqtSignal() # 检测完成信号 result_ready = pyqtSignal(np.ndarray, list) # 发送检测结果(图像和检测信息) def __init__(self, detector, image): super().__init__() self.detector = detector self.image = image def run(self): # 在这里调用YOLOv5模型进行检测 result_img, detections = self.detector.detect(self.image) self.result_ready.emit(result_img, detections) self.finished.emit() class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self): super().__init__() self.setupUi(self) # 初始化UI self.detector = None # YOLOv5检测器实例 self.cap = None # 视频捕获对象 self.timer = QTimer() # 定时器,用于视频流 self.current_mode = None # 当前模式:'image', 'video', 'camera' # 连接信号与槽 self.pushButton_load_model.clicked.connect(self.load_model) self.pushButton_open_image.clicked.connect(self.open_image) self.pushButton_open_video.clicked.connect(self.open_video) self.pushButton_open_camera.clicked.connect(self.open_camera) self.pushButton_start.clicked.connect(self.start_detection) self.pushButton_stop.clicked.connect(self.stop_detection) self.horizontalSlider_conf.valueChanged.connect(self.conf_threshold_changed) self.timer.timeout.connect(self.update_frame) # 定时器超时,读取下一帧 # 初始化界面状态 self.pushButton_stop.setEnabled(False) def load_model(self): """ 加载训练好的YOLOv5模型权重 """ model_path, _ = QFileDialog.getOpenFileName(self, "选择模型权重", "", "PyTorch Model (*.pt)") if model_path: try: # 这里初始化我们的YOLOv5检测器 self.detector = YOLOv5Detector(model_path, conf_thres=0.5, iou_thres=0.45) QMessageBox.information(self, "成功", "模型加载成功!") self.pushButton_open_image.setEnabled(True) self.pushButton_open_video.setEnabled(True) self.pushButton_open_camera.setEnabled(True) except Exception as e: QMessageBox.critical(self, "错误", f"模型加载失败:{str(e)}")

5.2 封装YOLOv5检测器

为了让PyQt5界面能方便地调用模型,我们把检测逻辑封装成一个单独的类YOLOv5Detector。这个类负责加载模型、预处理图像、运行推理和后处理(画框)。

import torch import numpy as np import cv2 from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords from utils.augmentations import letterbox class YOLOv5Detector: def __init__(self, weights_path, conf_thres=0.5, iou_thres=0.45, device='cuda:0'): self.device = torch.device(device if torch.cuda.is_available() else 'cpu') self.model = attempt_load(weights_path, device=self.device) # 加载模型 self.model.eval() # 设置为评估模式 self.conf_thres = conf_thres self.iou_thres = iou_thres self.names = self.model.module.names if hasattr(self.model, 'module') else self.model.names # 获取类别名 def detect(self, image_bgr): """ 对输入的BGR图像进行检测 Args: image_bgr: numpy数组,BGR格式的图片 Returns: result_image: 画了检测框的BGR图片 detections: 检测结果列表,每个元素为 [x1, y1, x2, y2, conf, class_id] """ # 1. 图像预处理 (Letterbox) img = letterbox(image_bgr, new_shape=640, stride=self.model.stride, auto=True)[0] # Convert HWC to CHW, BGR to RGB img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB img = np.ascontiguousarray(img) img = torch.from_numpy(img).to(self.device) img = img.float() / 255.0 # 归一化 0-255 to 0.0-1.0 if img.ndimension() == 3: img = img.unsqueeze(0) # 增加batch维度 # 2. 推理 with torch.no_grad(): pred = self.model(img, augment=False)[0] # 3. NMS后处理 pred = non_max_suppression(pred, self.conf_thres, self.iou_thres, classes=None, agnostic=False) detections = [] # 4. 处理每一张图片的预测结果 (我们一次只处理一张) for i, det in enumerate(pred): if len(det): # 将框的坐标从预处理后的img尺寸映射回原始image_bgr尺寸 det[:, :4] = scale_coords(img.shape[2:], det[:, :4], image_bgr.shape).round() for *xyxy, conf, cls in det: # 每个检测结果: [x1, y1, x2, y2, confidence, class_id] detections.append([int(xyxy[0]), int(xyxy[1]), int(xyxy[2]), int(xyxy[3]), float(conf), int(cls)]) # 5. 在原始图像上画框 result_image = image_bgr.copy() for det in detections: x1, y1, x2, y2, conf, cls_id = det label = f'{self.names[cls_id]} {conf:.2f}' # 画矩形框 cv2.rectangle(result_image, (x1, y1), (x2, y2), (0, 255, 0), 2) # 计算文本背景大小 (text_width, text_height), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2) # 画文本背景 cv2.rectangle(result_image, (x1, y1 - text_height - baseline), (x1 + text_width, y1), (0, 255, 0), -1) # 写文本 cv2.putText(result_image, label, (x1, y1 - baseline), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 2) return result_image, detections

这个类做了几件关键事:letterbox函数保持图像长宽比进行缩放和填充,确保输入尺寸固定;non_max_suppression过滤掉重叠的冗余框;scale_coords把检测框坐标映射回原始图像尺寸;最后用OpenCV把框和标签画上去。

5.3 实现多线程与实时检测

在GUI中直接进行模型推理(尤其是视频流)会阻塞主线程,导致界面“卡死”。所以我们必须用QThread把检测任务放到后台线程去执行。上面代码中的DetectionThread就是干这个的。对于视频或摄像头,我们使用QTimer定时触发帧的抓取和检测。

def open_camera(self): """ 打开摄像头 """ if self.detector is None: QMessageBox.warning(self, "警告", "请先加载模型!") return self.current_mode = 'camera' self.cap = cv2.VideoCapture(0) # 0代表默认摄像头 if not self.cap.isOpened(): QMessageBox.critical(self, "错误", "无法打开摄像头!") return self.timer.start(30) # 约33FPS,启动定时器 self.pushButton_start.setEnabled(False) self.pushButton_stop.setEnabled(True) def update_frame(self): """ 定时器槽函数,从视频源读取一帧并进行检测 """ if self.cap is not None and self.cap.isOpened(): ret, frame = self.cap.read() if ret: # 将BGR的OpenCV图像转换为RGB,用于Qt显示 rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 在这里可以调用检测器,但为了流畅性,可以每N帧检测一次,或者放到线程里 # 简单起见,这里直接在主线程检测(可能会卡) if self.detector and self.pushButton_start.isChecked(): # 假设开始按钮是toggle的 result_img, detections = self.detector.detect(frame) rgb_image = cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB) self.update_detection_list(detections) # 更新右侧结果列表 # 将图像显示到QLabel上 h, w, ch = rgb_image.shape bytes_per_line = ch * w qt_img = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) self.label_display.setPixmap(QPixmap.fromImage(qt_img).scaled( self.label_display.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) else: self.timer.stop() self.cap.release()

5.4 打包与发布

整个项目开发完成后,你可能会想分享给朋友或者在没有Python环境的电脑上运行。这时候就需要打包成可执行文件(.exe)。我推荐使用PyInstaller,它非常方便。

首先安装PyInstaller:pip install pyinstaller。然后,在项目根目录下创建一个简单的启动脚本run_app.py,里面就几行代码:

import sys from main_window import MainWindow from PyQt5.QtWidgets import QApplication if __name__ == '__main__': app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())

接着,在命令行中运行打包命令:

pyinstaller --onefile --windowed --add-data "best.pt;." --hidden-import=torch --hidden-import=torchvision run_app.py
  • --onefile:打包成单个exe文件。
  • --windowed:运行时不显示控制台窗口(适合GUI应用)。
  • --add-data "best.pt;.":将你的模型权重文件best.pt一起打包进去。分号前是源文件,分号后是打包后在exe中的相对路径(.代表根目录)。
  • --hidden-import:显式告诉PyInstaller包含一些它可能找不到的依赖(如PyTorch)。

打包过程可能会比较慢,并且生成的exe文件会比较大(因为包含了Python解释器和所有库)。完成后,在dist文件夹里就能找到你的可执行文件了。把这个exe和模型文件(如果没打包进去)一起发给别人,他们双击就能运行你的交通标志检测系统了!

6. 踩坑心得与性能优化建议

做到这里,一个完整的系统就算跑通了。但根据我的经验,从“跑通”到“好用”,还有一段路要走。下面分享几个我踩过的坑和优化建议,希望能帮你少走弯路。

坑1:数据集类别不平衡导致某些标志永远检不出来。TT100K里,“限速5公里”和“限速120公里”的图片数量可能差几十倍。模型自然会偏向学习样本多的类别。我的解决办法是:第一,在准备数据时,可以适当对稀有类别的图片进行复制或数据增强(如旋转、轻微变色)。第二,在YOLOv5的损失函数中,分类损失部分本身就有类别权重,但你可以尝试修改utils/loss.py中的BCEWithLogitsLoss,为不同类别设置不同的权重,给样本少的类别更高的权重。

坑2:模型在真实场景(如手机拍摄的视频)上表现不佳。这很可能是因为领域差异。TT100K数据集虽然来自街景,但和手机摄像头拍摄的角度、分辨率、畸变还是有区别。解决办法是进行模型微调(Fine-tuning)。用手机在校园或小区里拍几百张包含交通标志的图片,手动用LabelImg之类的工具标注一下(这个过程叫“数据标注”,挺枯燥但很必要)。然后用你训练好的best.pt作为预训练权重,用这批新数据再训练几十轮。学习率要设得比第一次训练小一个数量级(例如--lr0 0.001),这样模型就能快速适应新场景,效果提升会非常明显。

坑3:PyQt5界面在检测视频时卡顿。即使用了多线程,如果每帧都进行检测,对于大模型(如YOLOv5l)或高分辨率视频,界面依然会卡。优化策略:

  • 降低检测频率:不是每一帧都检测,比如每3帧检测一次,中间帧直接显示结果或使用跟踪算法(如ByteTrack)来维持框的位置。
  • 降低推理分辨率:训练时用640x640,部署时如果对精度要求不是极致,可以尝试用更小的尺寸(如480x480)进行推理,速度会快很多。
  • 使用TensorRT或ONNX Runtime加速:PyTorch模型转成ONNX格式,再用TensorRT(NVIDIA GPU)或ONNX Runtime(CPU/GPU)进行推理,速度通常能有数倍提升。YOLOv5官方提供了export.py脚本,可以很方便地导出ONNX模型。

坑4:打包后的exe文件巨大,且运行时提示缺少DLL。PyInstaller打包PyTorch时,会把整个torch库都包进去,导致文件很大(可能超过1GB)。可以尝试使用pip install torch --index-url https://download.pytorch.org/whl/cu118安装CUDA 11.8版本的PyTorch,这个版本对打包更友好一些。对于缺少DLL的问题,通常是CUDA相关库没打包进去。一个比较“笨”但有效的方法是:找到你CUDA安装目录下的bin文件夹(比如C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8\bin),把里面所有.dll文件复制到和你的exe同一个目录下。

最后,这个项目不仅仅是一个教程,它更是一个完整的起点。你可以基于它做很多扩展:比如把模型部署到树莓派或Jetson Nano上做成一个车载设备;或者增加一个语音提示模块,检测到“限速”或“停止”标志时发出警报;再或者把检测结果通过网络发送到服务器,构建一个简单的车路协同演示系统。深度学习的乐趣,就在于把想法变成现实的过程。希望这篇长文能帮你顺利走完从数据到模型再到应用的全流程,祝你玩得开心!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/14 17:00:50

多模态工业异常检测算法性能深度解析与选型指南

1. 从“单眼看”到“双眼看”&#xff1a;为什么工业质检需要多模态&#xff1f; 干了这么多年AI落地项目&#xff0c;我最深的体会就是&#xff1a;工厂里的质检问题&#xff0c;从来都不是教科书里那种干净、理想化的场景。你面对的不是实验室里精心标注的MNIST手写数字&…

作者头像 李华
网站建设 2026/7/14 17:00:48

LightOnOCR-2-1B在C语言项目中的应用:轻量级文本识别方案

LightOnOCR-2-1B在C语言项目中的应用&#xff1a;轻量级文本识别方案 1. 引言 在日常的C语言项目中&#xff0c;我们经常需要处理各种文档和图像中的文本信息。传统的OCR解决方案要么过于笨重&#xff0c;要么需要复杂的多阶段处理流程&#xff0c;这让很多C语言开发者望而却…

作者头像 李华
网站建设 2026/7/14 17:00:51

FLUX.2-Klein-9B快速入门:三步搞定人物换装,电商出图效率翻倍

FLUX.2-Klein-9B快速入门&#xff1a;三步搞定人物换装&#xff0c;电商出图效率翻倍 1. 电商出图的痛点&#xff0c;一个AI模型就能解决 如果你是做服装、饰品或者任何需要真人展示的电商卖家&#xff0c;下面这个场景你一定不陌生&#xff1a;新款到货&#xff0c;急着上架…

作者头像 李华
网站建设 2026/7/14 17:01:08

保姆级教程:手把手教你快速部署Qwen3-0.6B-FP8文本生成模型

保姆级教程&#xff1a;手把手教你快速部署Qwen3-0.6B-FP8文本生成模型 想体验最新一代的Qwen3大模型&#xff0c;但又担心部署复杂、配置繁琐&#xff1f;今天&#xff0c;我们就来彻底解决这个问题。我将带你一步步&#xff0c;在几分钟内完成Qwen3-0.6B-FP8文本生成模型的部…

作者头像 李华
网站建设 2026/7/14 17:01:05

[实战避坑] NVIDIA Orin 边缘计算平台部署 PyTorch 生态全记录

1. 环境检查&#xff1a;摸清“家底”&#xff0c;避免开局踩坑 拿到一台别人用过的NVIDIA Orin设备&#xff0c;我的第一反应不是兴奋&#xff0c;而是心里有点打鼓。这感觉就像租房子&#xff0c;前任房客留下了什么“宝藏”或“坑”&#xff0c;你完全不清楚。直接上手安装P…

作者头像 李华
网站建设 2026/7/14 17:01:04

WPF TextBox 输入限制实战:从基础到高级控制

1. 从零开始&#xff1a;为什么我们需要控制TextBox的输入&#xff1f; 刚开始接触WPF开发的时候&#xff0c;我总觉得TextBox就是个简单的输入框&#xff0c;用户想输什么就输什么呗。直到我接手了一个财务系统项目&#xff0c;里面有个金额输入框&#xff0c;用户不仅能输入数…

作者头像 李华