简介针对城市智能交通场景中红绿灯倒计时数字的实时检测需求这份源码包提供了一套基于改进YOLOv8的完整解决方案面向计算机视觉开发者、交通系统研究人员及算法落地工程师。资源共包含26个文件压缩包大小约2.97MB主要类型有4个Python脚本对应训练、验证、预测及Web交互界面、19张用于数据集展示与检测效果验证的PNG图片以及txt、md、docx格式的说明文档可清晰呈现从数据标注、模型训练到前端可视化的整体流程。在技术实现上项目对YOLOv8模型进行了70余项创新优化涵盖网络结构调整、特征提取增强与算法策略改进等方面并配有完整的数据集标注与训练流程说明。通过Web前端可视化界面用户可以直接观察红绿灯状态及倒计时数字识别结果。目前已有88人学习该资源适合希望快速复现智能交通视觉系统、或深入研究YOLO改进方法的读者借助文档与脚本可快速搭建实验环境减少重复开发成本。1. 红绿灯倒计时数字检测为什么不能只靠YOLOv8原版在路口场景里红绿灯倒计时数字通常是LED点阵或数码管显示目标小、亮度高、还带闪烁和反光拿原版YOLOv8直接去检测会出现漏检和误检交替出现的情况。很多毕业设计或者路侧感知项目卡在这一步误以为换个大模型就能解决实际上一路排查下来发现瓶颈在数据集标注粒度、网络头部对小目标的感知能力以及前后帧数字识别的时序一致性。这个标题里给到的方案核心思路不是简单跑通YOLOv8而是围绕红绿灯倒计时数字这类小目标做针对性改进再配上标注、训练、Web端可视化的完整链路。适合谁看准备用YOLOv8做目标检测落地项目、尤其对交通场景感兴趣的学生或测试工程师。2. 搭出能跑的YOLOv8底座环境配置与数据集标注2.1 本地环境怎么选CPU也能跑但推荐CUDA先把环境踩实。YOLOv8的官方实现基于ultralytics库一行pip install ultralytics就能装上但模型训练依赖PyTorch所以第一步是确认PyTorch能不能识到你的GPU。我一般会先跑一段快速诊断代码import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else No GPU)这里torch.cuda.is_available()返回True说明CUDA链路没问题。很多人的翻车点是直接pip install torch装的是CPU版本GPU明明存在却用不了。解决方法是按照PyTorch官网给的CUDA版本命令安装比如pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118。如果你只有CPUYOLOv8训练也能跑但速度只有GPU的几十分之一。我之前用一台i5的笔记本跑一个2000张的红绿灯数字数据集一个Epoch要10分钟100个Epoch就得十几个小时。这种场景下建议先去搞一个免费GPU环境跑通验证或者把输入尺寸从640降到320先做冒烟测试确认代码和标注没问题时再上完整训练。另外只用CPU推理的话用ONNX导出再跑也不慢后面Web端展示时我会选择导出成ONNX格式。2.2 用labelme标注红绿灯倒计时数字类别定义与json转txtYOLOv8训练数据用的是txt格式的标签每行是class_id x_center y_center width height但标注时用labelme画多边形最方便因为它能处理细长条的数字区域。常见做法是先把红绿灯倒计时数字定义成单类别countdown_digit因为无论是红灯倒计时还是绿灯倒计时模型只需要识别出数字区域具体是哪个颜色由交通灯检测模块去关联。标注完一张图labelme会保存一个同名json文件里面是点和形状信息。YOLOv8不认识这种格式需要写一个转换脚本。我常用的转换逻辑如下import json import os from PIL import Image def labelme_to_yolo(json_path, save_path, class_list): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] yolo_lines [] for shape in data[shapes]: label shape[label] if label not in class_list: continue cls_id class_list.index(label) points shape[points] # [[x1,y1],[x2,y2],...] xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) x_center (x_min x_max) / 2 / img_w y_center (y_min y_max) / 2 / img_h width (x_max - x_min) / img_w height (y_max - y_min) / img_h yolo_lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(save_path, w) as f: f.write(\n.join(yolo_lines)) class_list [countdown_digit] # 对images目录下每张图对应的json调用函数即可这段代码的关键在于坐标归一化。YOLO要求中心点坐标和宽高都是相对于图片尺寸的比例而不是像素绝对值。x_center (x_min x_max) / 2 / img_w这一步就是把像素中心点换算到0~1之间。你需要注意的是labelme的shape类型如果画的是多边形points可能有很多点但只要取最小外接矩形就够了不需要拟合旋转框。我踩过的坑是有些人标注的习惯是只框数字本身比如单个数字3、5但我建议直接框整个倒计时区域。因为红绿灯倒计时经常是两位数把区域整体框住能让模型学到“两位数字”的整体特征识别阶段再配合OCR模型去做数值解析比单独框一个数字再拼接要稳定得多。2.3 数据集划分与训练前检查标注完成之后把图片和txt标签按ImageNet的目录习惯放好dataset/ images/ train/ val/ labels/ train/ val/然后写一个划分脚本按比例随机分配文件。这里有一个很容易漏掉的细节划分时必须保证图片和标签同名不然训练时报错说找不到标签。我一般会在划分脚本里做一次文件名交集校验import os import random from shutil import copy2 img_dir dataset/images/all label_dir dataset/labels/all train_img_dir dataset/images/train train_label_dir dataset/labels/train random.seed(42) img_files [f for f in os.listdir(img_dir) if f.endswith(.jpg)] random.shuffle(img_files) train_files img_files[:int(len(img_files) * 0.8)] for f in train_files: base os.path.splitext(f)[0] label_file os.path.join(label_dir, base .txt) if not os.path.exists(label_file): print(f缺失标签: {label_file}) continue copy2(os.path.join(img_dir, f), train_img_dir) copy2(label_file, train_label_dir)这个脚本里random.seed(42)保证每次运行划分结果一致避免之后改参数复训时因为数据划分布不同导致结果对不上。另外建议在训练前用脚本统计一下标注框的尺寸分布如果大部分框的宽度或高度小于32像素就说明这是典型的小目标问题。YOLOv8原版在P3层8倍下采样做检测对这类小目标天然不敏感后面就得改网络结构。3. 改进YOLOv870余项创新点里真正有用的三处标题里写了70余项创新点说实话源码包里真正能稳定提点的通常就那么几个。我不建议把所有改进都堆上去会增加调参难度和推理延迟。按我的实测对红绿灯倒计时数字这类场景以下三处改动最划算。3.1 头部改进增加浅层小目标检测头YOLOv8的检测头默认从三个尺度输出特征图分别是8倍、16倍、32倍下采样。8倍下采样特征图已经丢失了很多细节而红绿灯倒计时数字在路侧摄像头画面里可能只有10x30像素占特征图不足一个格子。常见的做法是在原本三个检测头之上再加一个4倍下采样的P2检测头。具体在ultralytics代码里是通过修改yaml中的head结构实现的# yolov8n-smart.yaml 节选 head: - [-1, 4, [64, 128]] # P2/4 下采样 ...但直接改yaml不生效因为ultralytics的backbone输出节点和head层数是对应死的真正动手时你要在ultralytics/nn/modules/head.py里增加一个检测层并把Detect的ch参数加上P2特征图的通道数。很多源码包采用的是重写一个自定义模型文件的方式核心逻辑是让backbone输出更浅层的特征图给检测头。调参说明增加P2头后计算量大概上涨20%如果你的设备是Jetson或者嵌入式板卡需要考虑推理延迟。我的做法是保留P2头做训练导出ONNX时做一个分支选择部署端用传统图像处理定位数字区域再用一个小分类网络识别数字这样能把延迟压回30ms以内。3.2 注意力机制给数字区域更多权重红绿灯倒计时数字和背景颜色相近比如红灯倒计时是红色数字衬在黑色底板上图像上对比度和纹理都有限。给backbone最后两层加注意力机制能明显提升特征响应。源码包里最常见的实现是加CBAM或者SE注意力。我复现过CBAM效果比SE稳定class CBAM(nn.Module): def __init__(self, c1, reduction16): super().__init__() self.channel_att nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(c1, c1 // reduction, 1), nn.ReLU(inplaceTrue), nn.Conv2d(c1 // reduction, c1, 1), nn.Sigmoid() ) self.spatial_att nn.Sequential( nn.Conv2d(2, 1, kernel_size7, padding3), nn.Sigmoid() ) def forward(self, x): x_out x * self.channel_att(x) avg_out torch.mean(x_out, dim1, keepdimTrue) max_out, _ torch.max(x_out, dim1, keepdimTrue) spatial torch.cat([avg_out, max_out], dim1) x_out x_out * self.spatial_att(spatial) return x_out这段代码是CBAM的压缩版。通道注意力用全局平均池化生成每个通道的权重空间注意力则把通道信息压缩成2个特征图再卷积生成空间权重。接入位置我建议放在backbone第4层和第5层输出后而不是所有层都加。原因是倒计时数字是高层语义和低层纹理的结合中高层特征既包含纹理又保留了一定的空间分辨率放太前面会把计算浪费在背景上放太后面又丢失位置信息。我在实验中发现一个很有意思的现象加了CBAM之后mAP提了大概2个百分点但是推理时间增加了近10ms。如果你的硬件很吃紧可以考虑只在训练时开CBAM部署时用一份移除了CBAM的简化权重做蒸馏推理不过这就太折腾了一般项目不这么搞。3.3 训练策略与损失函数改进网络结构改了损失函数也可以调。YOLOv8默认用了分类损失BCE、回归损失CIoU和DFL。对数字检测来说误检的代价比漏检高因为交通系统宁愿没检测到也不愿意报一个错误的数字。我通常会把分类损失改成focal loss把难样本的梯度放大# ultralytics/models/loss.py 中修改分类损失部分 import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, gamma2.0, alpha0.25): super().__init__() self.gamma gamma self.alpha alpha def forward(self, pred, target): bce F.binary_cross_entropy_with_logits(pred, target, reductionnone) p torch.sigmoid(pred) p_t p * target (1 - p) * (1 - target) focal_weight (1 - p_t) ** self.gamma focal_weight focal_weight * (self.alpha * target (1 - self.alpha) * (1 - target)) loss focal_weight * bce return loss.mean()这里的alpha控制正负样本权重gamma控制难易样本权重。红绿灯倒计时数字的样本量在各帧中相对均匀但负样本背景中被误认为是数字的区域数量远大于正样本所以把alpha设成0.25做正样本加权比较合适。改了损失函数后要同步调整训练超参数比如lr和weight_decay否则容易不收敛。另外我也常看到有人把训练时的mosaic增强关闭原因是在交通路口这种场景mosaic会把多个图的数字区域截断拼在一起导致标注框丢失反而降低精度。我建议mosaic设为0.5而不是全关保留一部分让它增加多样性。4. 训练自己的数据集参数含义与loss曲线判读4.1 yolo训练命令与参数解释一切准备就绪后用ultralytics的命令行训练最省事。这是我的常用命令yolo detect train \ modelmodels/yolov8n-smart.yaml \ datadataset/smart_traffic.yaml \ epochs150 \ batch16 \ imgsz640 \ lr00.01 \ lrf0.01 \ optimizerSGD \ device0 \ # 指定GPU序号CPU则用devicecpu workers4 \ seed42 \ cacheTrue逐个参数说model是指向你自己定义的yaml文件这个yaml里包含了模型结构和改进后的head定义data是数据集的yaml内容大概是train: 路径,val: 路径,nc: 1,names: [countdown_digit]batch在GPU显存不够时会报CUDA out of memory我就把16改成8或者在配置里加ampFalse关闭混合精度训练再试imgsz根据原图分辨率来路侧摄像头原始分辨率1080p直接缩放到640会导致数字更小所以我一般用imgsz960代价是训练时间翻倍optimizerSGD比Adam在目标检测里更稳但收敛速度慢想要快速出效果可以换成AdamW。4.2 画损失函数曲线图看模型有没有收敛训练过程中ultralytics会在runs/detect/train/目录下生成results.png和results.csv。我一般不看results.png因为目前版本的ultralytics在训练完成后才绘制训练中看不到实时曲线。想看实时loss曲线需要单独写个回调或者用tensorboard。推荐一个很轻量的做法训练命令里加上projectmydetect nameexp_smart这样脚本轮询csvimport pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/exp_smart/results.csv) plt.figure(figsize(10, 6)) plt.plot(df[epoch], df[train/box_loss], labelbox_loss) plt.plot(df[epoch], df[train/cls_loss], labelcls_loss) plt.plot(df[epoch], df[val/box_loss], labelval_box_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.title(Loss Curves) plt.savefig(loss_curve.png)这里的train/box_loss是回归框损失train/cls_loss是分类损失。一个常见规律是cls_loss下降快因为只有一个类别好学box_loss下降慢因为小目标的中心点偏差比例比较大即使只差几个像素相对于不足20像素宽的目标来说损失就已经很大了。看曲线时重点看val损失有没有在30个epoch后开始反弹反弹就是过拟合信号要调大weight_decay或者加早停。4.3 断点续训与模型验证长训练任务最怕跑到一半断电或报错。ultralytics支持断点续训命令是yolo detect train resumeTrue modelruns/detect/exp_smart/weights/last.pt。last.pt存的是最近一个epoch结束的状态包含优化器参数和epoch数续训时不需要重新指定数据集配置。我一般是每跑20个epoch就把best.pt复制一份到带时间戳的目录防止后面改了代码把旧的best覆盖掉。验证阶段跑yolo detect val \ modelruns/detect/exp_smart/weights/best.pt \ datadataset/smart_traffic.yaml \ conf0.25 \ iou0.5 \ splittestconf是置信度阈值交通场景建议设0.4以上宁可漏检也不能误检——误检会把7识别成1影响后面的倒计时逻辑。iou是NMS的IoU阈值0.5是常规值对密集数字区域可以降到0.45防止重复框。验证输出的txt里会有每类的precision、recall和mAP50。对红绿灯数字我关注的不是mAP而是recall在低置信度下是否足够高如果recall低那很可能是标注框太小需要回看3.1节是否加了P2头如果precision低那就是背景误检多要回看focal loss的alpha设置。5. 踩坑记录从标注到推理的5个常见问题现象1训练时报错“unable to find dataset label file”原因data yaml里的路径写的是相对路径启动命令时工作目录不在项目根目录。解决把data yaml里的路径全部改成绝对路径或者用yolo detect train前先cd到项目根目录。我当时卡了半小时最后发现是Windows下路径分隔符\被当成转义字符了统一改成/就正常了。现象2Loss曲线震荡严重val_loss不下降原因这是我在用改进后的模型时遇到的。新加的检测头初始化权重和原来不匹配前20个epoch回归梯度特别大。解决不要一下子改所有结构先加P2头保持其他部分用原版训练50个epoch确认曲线平滑了再加CBAM。另外把warmup_epochs从3调到5让学习率慢慢爬升避免开局就把权重冲飞。现象3推理时漏检特别多尤其是距离远的数字原因视频里红绿灯数字往往在画面边缘且透视变形严重训练集里这样的样本太少。解决做数据增强时把translate、scale调大fliplr0.0——因为数字7翻转后会变成别的形态其实数字翻转不影响类别但红绿灯的位置固定翻转后可能和另一边信号灯冲突。更有效的做法是收集跨路口的素材不同安装角度和镜头高度的截图然后按镜头高度分bucket训练。现象4CPU推理速度太慢达不到实时原因模型参数和输入分辨率太高。解决导出成TensorRT。即使没有GPU也可以在普通PC上先用onnxruntime-gpu跑但真正要实时还是要走engine格式。具体做法是先转onnx再用trtexec转engine。如果没有NVIDIA GPU就退而求其次用yolov8n权重并把推理尺寸设成416配合torch.no_grad()和torch.cuda.synchronize()省掉同步开销。现象5Web端显示检测框和原画错位原因前端展示的视频流分辨率如果和模型推理分辨率不一致没有做坐标映射。解决后端推理时记录原始图像的宽高把模型输出的归一化坐标乘回原始宽高再传给前端。如果是用Flask的VideoFeed模式前端img标签显示的是JPEG流框是画在图片上的就不会错位但如果是用canvas叠加就要确保视频分辨率固定不能自适应屏幕。6. 接上Web前端可视化把检测结果变成实时交互界面训练好的模型要变成能演示的东西最直接的做法是搭一个轻量级的Web服务。我用Flask加一个简单的HTML页面就能搞定不引入React/Vue这些重框架。后端启动一个线程读摄像头或视频文件把帧送入模型推理把检测框和识别到的数字用OpenCV画在帧上然后压缩成JPEG传给前端。from flask import Flask, Response import cv2, torch, numpy as np from ultralytics import YOLO app Flask(__name__) model YOLO(best.pt) def generate(): cap cv2.VideoCapture(traffic_sample.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break results model(frame, conf0.4, verboseFalse) annotated results[0].plot() ret, jpeg cv2.imencode(.jpg, annotated) yield (b--frame\r\n bContent-Type: image/jpeg\r\n\r\n jpeg.tobytes() b\r\n) app.route(/video_feed) def video_feed(): return Response(generate(), mimetypemultipart/x-mixed-replace; boundaryframe) if __name__ __main__: app.run(host0.0.0.0, port5000)这段代码里results[0].plot()直接返回带标注的帧。为了让页面更智能可以再增加一个阈值判断当连续3帧检测到倒计时数字变化时再通过WebSocket把新的倒计时值推给前端页面同时更新文字提示而不是每一帧都刷新大数字这样能减少闪烁。我自己的教训是别把模型加载和推理放在Flask的请求线程里跑。因为Flask默认单进程多个客户端同时打开视频流会导致推理队列堵塞表现为页面卡顿。解决方法是用multiprocessing开一个独立推理进程把检测结果放到共享队列Flask只负责读取队列中的最新帧。另外视频流是阻塞式的如果客户端断开连接generate()不会立刻退出要加cap.release()和超时判断否则后台会堆积僵尸线程。最后说到这个Web界面其实演示效果好坏往往在细节前端加一个“当前倒计时数字”的大字号显示配一个红绿状态图标比单纯显示坐标框要直观得多。这一套做下来整个项目从模型训练到工程展示就算闭环了。希望帮到你。本文还有配套的精品资源点击获取