基于YOLOv8改进的红绿灯倒计时识别:小目标检测与部署实践
简介面向智能交通与计算机视觉开发者这套源码以改进YOLOv8模型为核心实现红绿灯倒计时数字的实时检测与识别。针对真实路口的复杂光照与遮挡条件算法在原始模型基础上完成70余项改进涉及网络结构、特征提取与训练策略等并配套完整数据集标注与训练流程帮助读者掌握从数据准备、模型训练到部署验证的全链路方法。包内共26个文件以19张图像样本、4个Python脚本及txt/docx/md说明文档为主脚本覆盖训练、验证、预测与Web可视化交互等核心环节说明文档则提供环境配置与复现指引压缩包仅2.97MB轻量易部署。配合前端界面用户能直接查看信号灯状态与倒计时数字并可扩展交通流量分析等功能适用于智慧城市课题研究或项目原型开发。目前已有88人学习使用按说明操作即可快速跑通整套识别流程。1. 红绿灯倒计时识别为什么通用YOLOv8直接上线会翻车红绿灯倒计时识别在城市智能交通里是一个典型的小目标场景画面里的数字普遍不到30像素白天有逆光反射夜晚有灯盘光晕、车灯扫过和雨滴模糊。直接用COCO预训练的YOLOv8跑路口视频白天勉强能用一进夜间就漏检、误检成片两位数变成一位数红灯倒计时和绿灯倒计时互相串。标题里这套方案真正解决的不是“跑通YOLOv8”而是把改进模型、数据集标注训练和Web前端可视化串成一条能复现、能部署的链路。适合正在做智能交通视觉、车路协同或基于yolov8的毕业设计的工程师和学生——你需要的不是单个权重文件而是一套从采集到上线的完整闭环。2. 拆解70余项创新点改进倒计时数字场景的四类结构改动拿到这类带“70余项创新点改进”字样的项目我习惯先做减法。70多项听起来多拆开就是四个方向检测层、注意力、特征融合、轻量化。后面再加训练策略和数据增强来凑数真正顶用的就那几个。下面按部署目标为GPU加边缘盒子双轨的场景讲避免为了凑创新点把模型改到没法落地的程度。2.1 小目标检测层P2浅层特征为什么不能省倒计时数字在1080p画面里的高度通常不到30像素超过一半的字符集中在10到20像素之间。YOLOv8默认的三个检测头分别对应stride 8、16、32stride 32那条分支对10像素以下的目标基本没有响应stride 8是主力但特征图分辨率仍然不够字符的笔画细节到了深层已经被卷积稀释得差不多。常见做法是加一个P2检测头把backbone浅层stride 4的特征引出来再和深层特征做融合。在ultralytics的模型配置文件里一种改动示意是这样的# yolov8_custom.yaml 片段示意需对齐你自己yolov8.yaml的行号 backbone: - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2 - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4 - [-1, 1, C2f, [128, True]] # 2-P2 输出 # ... 继续到深层 head: - [-1, 1, nn.Upsample, [None, 2, nearest]] # 上采样后和P2层融合 - [[-1, 2], 1, Concat, [1]] - [-1, 1, C2f, [128]] # 检测头从1个变成4个对应stride 4/8/16/32 - [-1, 1, Detect, [nc, [128, 256, 512, 1024]]]这段yaml示范的是“加一路P2”的思想先用上采样把深层特征放大到P2的尺寸再通过Concat把浅层的笔画纹理拼进来。关键参数是最后一行Detect里的nc必须和数据集的类别数一致channels列表要和前面的C2f输出对齐否则一加载就报shape mismatch。加P2层之后FLOPs大约涨30%显存占用同步上升。对于数字字符普遍大于30像素的数据集P2带来的提升可能不到1个点却明显拖慢推理。我的习惯是先看标注框尺寸分布如果小框占比超过30%P2就值得上如果全是近景大字符改输入分辨率比加检测层更划算。另一个折中是把训练图从640升级到960或1280但这会让batch必须调小训练时间拉长不算白赚。2.2 注意力机制ECA/CBAM/SE三种选哪个夜间红绿灯最大的干扰不是目标小而是灯盘光晕把数字包住。光晕本质上是高亮背景会让特征图的主通道被“过曝区域”占满数字的纹理被压得很低。注意力机制在这里的作用就是重新分配通道权重让网络更关注字符纹理而不是整片发光区。SE只做通道注意力参数极少效果稳定CBAM在通道基础上加了空间注意力对遮挡有帮助ECA用一维卷积替代SE里的全连接计算量更小是三者里最轻的。红绿灯数字场景我一般用ECA插在C2f输出后面因为空间注意力在字符粘连严重时会误增强整片光晕反而帮倒忙。一个可用的ECA模块长这样# model/common.py 里新增ECA模块插在C2f之后即可 class ECA(nn.Module): def __init__(self, c, k_size3): super().__init__() self.avg_pool nn.AdaptiveAvgPool2d(1) self.conv nn.Conv1d(1, 1, kernel_sizek_size, padding(k_size - 1) // 2, biasFalse) self.sigmoid nn.Sigmoid() def forward(self, x): b, c, _, _ x.size() y self.avg_pool(x) # 全局平均池化得到通道描述 y y.view(b, 1, c) y self.conv(y) # 1D卷积做跨通道交互 y y.view(b, c, 1, 1) return x * self.sigmoid(y) # 对原特征图做通道重标定参数说明k_size控制跨通道交互范围小模型用3容量大的结构可以试5类别的c是输入通道数不需要手工指定。ECA不改变输出shape所以插入后不需要改后续任何层。在夜间训练时可以观察tensorboard里ECA输出的激活分布如果光晕区域的通道权重被明显压低说明注意力发挥了作用如果激活分布和没加时差不多问题不在注意力而在特征融合。2.3 检测头与特征融合BiFPN、可变形卷积与YOLOv8 head改进的边界yolov8 head改进是检索热度很高的方向但很多人忽略了一个前提YOLOv8默认的decoupled head已经把分类和回归分支拆开了这在倒计时数字场景里已经够用真正值得动的是neck部分的特征融合。BiFPN是常见选择它给每条融合路径分配一个可学习权重夜间某层特征被光晕污染时网络自己会把这条路径的权重压低。一个工程上够用的加权融合实现思路是这样# BiFPN加权融合的核心逻辑示意非完整实现 # p2_up是从深层上采样到P2的特征p3是当前层原始特征 w1 torch.relu(self.w1_fuse) w2 torch.relu(self.w2_fuse) feat (w1 * p2_up w2 * p3) / (w1 w2 1e-4)这里的w1_fuse和w2_fuse是初始化为1的可学习参数分母加1e-4避免除零。训练完后去tensorboard看这两个权重的最终值哪个被压到接近0就说明对应那条通路在帮倒忙可以直接剪掉来提速。BiFPN的完整实现还有反复上下采样对红绿灯这类单目标小场景收益不大做一层加权融合就够了。可变形卷积DCN对形变字符友好但显存和推理时间都涨得太猛边缘盒子上基本不用考虑。我的判断标准是如果数字本身不形变坏的只是光照DCN的收益会被它的代价完全吃掉。真正要试的是给检测头加一层轻量上下文聚合比如在分类分支前加一个3x3卷积扩大感受野让数字附近的灯盘状态也能被感知。2.4 轻量化方向为RK3588和GPU双轨留好开关城市路口组网部署不可能每路摄像头后面挂一张2080Ti。rk3588部署yolov8是现在被问得很多的落地路径它走NPU需要转RKNNint8量化后通常掉1到3个点。改进模型时如果一开始就堆满注意力、BiFPN转RKNN时大概率遇到算子不支持一步一回退。常见的处理方法是先把backbone里的普通卷积替换成GhostConv用通道剪枝砍掉冗余再导出onnx转rknn。GhostConv的思路是让一部分通道走线性变换另一部分走标准卷积量化和算子映射都友好很多。下表是我常用的部署目标对比部署目标推理框架典型耗时主要踩坑服务器GPUTensorRT FP163到8ms动态shape要固定batch1RK3588 NPURKNN int820到40ms算子兼容、量化校准集偏差参数说明RKNN int8的量化校准集最好用夜间和白天各一半的样本只用白天图片校准会导致夜间低亮度通道被压缩掉点更严重。改进结构时尽量只用Conv、ReLU、Concat这些基础算子少用softmax密度大的模块否则rknn toolkit转换阶段会报不支持或反复回退CPU。这就是为什么我在改模型前先问清楚部署平台而不是先把70项改进全叠上去。3. 从路口视频到YOLO格式数据集标注规范与转换脚本数据集是这套系统里投入产出比最高的一环。很多人把精力花在堆改进模块上最后发现夜间漏检的根因是夜间样本太少。这一章把采集、标注、转换、增强讲透每一步都按可复现的标准来。3.1 采集和抽帧覆盖白天、夜晚、逆光与球机俯视角红绿灯倒计时数据的来源通常是架设在路口的高位摄像头录像。常见做法是找一个允许临时架设设备的路口录制一个下午加一个晚上再把视频按固定间隔抽帧保存。抽帧脚本不复杂但有几个细节容易翻车。import cv2 cap cv2.VideoCapture(intersection_01.mp4) interval 5 # 每5帧取1帧 idx 0 out_idx 0 while cap.isOpened(): ret, frame cap.read() if not ret: break if idx % interval 0: # 裁掉天空和路面只保留灯盘区域缩小图片体积 roi frame[100:900, 200:1600] cv2.imwrite(fraw/im_{out_idx:06d}.jpg, roi, [cv2.IMWRITE_JPEG_QUALITY, 95]) out_idx 1 idx 1 if idx 6000: # 控制单视频总量 break cap.release()逻辑说明interval决定样本量和相邻帧冗余度。5帧抽1帧能保证同一个数字在画面不同位置出现多次又不至于全是近重复帧。裁剪ROI这步很重要1080p原图里灯盘只占一角裁掉无关区域后标注时能省大量时间训练时也少很多背景干扰。抽完帧之后一定要人工快速刷一遍把运动模糊、过曝和车辆遮挡灯盘的帧删掉这步是整个标注流程里最便宜的清洗。3.2 标注策略数字区域框还是单字符框这是第一个要拍板的设计决策直接影响后续识别逻辑。方案A是把整个倒计时区域框成一个目标类别叫countdown数字识别交给后面的OCR方案B是把每个字符单独框出来类别直接是digit_0到digit_9模型输出即识别结果。标题里要“检测与识别”一次做完所以最终系统通常走方案B。方案标注成本小目标难度识别准确率适合场景A区域框OCR低低依赖OCR鲁棒性快速验证B单字符框高高模型端到端可控最终交付两个方案的取舍很清楚方案A标注量少但两位数字粘连时OCR会读错而且又多一个子系统要维护方案B的模型直接输出数字类别后处理只需要按x坐标排序拼成int链路短夜间的泛化能力掌握在模型手里。方案B的标注规范有三条硬规矩字符只框发光的数字不要把灯盘的暗色底框包进来两位数字中间有间隙时务必分开框遇到数字闪烁或半暗状态宁可不标也不要框半个字符。3.3 Labelme转YOLO格式坐标归一化与过滤小框用labelme标注用于yolov8是主流流程输出是jsonYOLO训练要的是txt。转换脚本本身不难难在细节坐标要除以宽高变成相对值类别ID必须和data.yaml里的names顺序一致太小或太偏的框要过滤。import json, glob, os class_map { digit_0: 0, digit_1: 1, digit_2: 2, digit_3: 3, digit_4: 4, digit_5: 5, digit_6: 6, digit_7: 7, digit_8: 8, digit_9: 9 } def convert(json_path, size_filter8): with open(json_path, encodingutf-8) as f: data json.load(f) img_w, img_h data[imageWidth], data[imageHeight] lines [] for shape in data[shapes]: label shape[label] if label not in class_map: continue pts shape[points] x1 min(p[0] for p in pts) y1 min(p[1] for p in pts) x2 max(p[0] for p in pts) y2 max(p[1] for p in pts) w, h x2 - x1, y2 - y1 if w size_filter or h size_filter: continue cx (x1 x2) / 2 cy (y1 y2) / 2 lines.append( f{class_map[label]} {cx/img_w:.6f} {cy/img_h:.6f} f{w/img_w:.6f} {h/img_h:.6f} ) out os.path.splitext(json_path)[0] .txt with open(out, w, encodingutf-8) as f: f.write(\n.join(lines)) for j in glob.glob(labelme_json/*.json): convert(j)参数说明size_filter单位是像素过滤阈值按你的最小数字尺寸定一般取8到12。小于这个尺寸的框学习不到纹理只会给loss引入噪声。所有坐标都归一化到0到1之间训练时才不会被letterbox拉伸搞错。脚本跑完要抽查几个txt行数是否和json里的有效框数一致有没有出现大于1.0的坐标值空图是否对应空txt。这一步漏检后面训练出来全是NaN loss。3.4 训练集划分与增强mosaic、mixup和模拟夜间光晕数据划分建议用脚本随机切7:2:1但注意一个前提同一个路口视频连续抽出来的帧不要同时落在train和val里否则验证集等于看了训练集的内容指标虚高。按“视频片段”为单位划分比按帧划分更诚实。yolov8训练自己的数据集时增强参数在训练命令里直接暴露yolo detect train \ datatraffic_light.yaml \ modelyolov8_custom.yaml \ imgsz640 \ mosaic1.0 \ mixup0.2 \ fliplr0.5 \ hsv_h0.015 \ hsv_s0.4 \ hsv_v0.3参数说明mosaic前10个epoch可以开着后面建议关掉或降到0.5让模型在接近真实分布的数据上收敛mixup不要超过0.3红绿灯数字的边界纹理在mixup下容易被糊掉fliplr可以大胆开左右翻转不改变数字语义hsv_v增强用来模拟各种LED亮度和屏幕老化色偏夜间数据不够时它的价值最大。提示数据增强是最便宜的泛化手段但它替代不了真实夜间样本。光晕模拟只能让模型不崩做不到和真实车灯扫过完全一致。夜间样本至少要占到30%否则一切结构改进都是缘木求鱼。4. 用Ultralytics在Ubuntu20.04上跑通完整训练流程这一章是从零到权重文件的完整路径。环境、配置、训练、验证四步走每一步都会遇到让你想摔键盘的问题这里把最常见的先踩掉。4.1 环境搭建CPU版本先跑通GPU版本不踩CUDA坑ubuntu20.04搭建yolov8环境cpu版本是很多人的第一步因为可以先验证数据和代码链路。命令就那么几条坑全在版本匹配。# CPU版本先验证代码和数据集 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install ultralytics # GPU版本先查驱动支持的CUDA版本再动手 nvidia-smi | grep CUDA Version conda create -n yolo python3.10 -y conda activate yolo pip install torch2.1.0 torchvision0.16.0 --index-url https://download.pytorch.org/whl/cu121 pip install ultralytics逻辑说明CPU版本的关键是下载Pytorch的cpu wheel否则pip会在nvidia源里绕圈子。装完在Python里跑import torch; print(torch.cuda.is_available())输出True才继续False说明装成了CPU版。GPU版本最常翻车的不是CUDA版本本身而是torch和torchvision版本没对齐安装时把它们固定成同一索引版本能省掉大半问题。参数说明cu121对应CUDA 12.1如果驱动版本不够就降到cu118。CPU训练时把batch调到8workers降到2否则机器响应都困难。训练中途显存溢出时先看batch和imgsz别急着换小模型——多数情况下是cacheTrue把数据集全塞进显存了关掉cache就行。4.2 数据配置文件与模型参数yaml里最容易写错的两个字段训练前要准备好数据配置yaml和模型配置yaml。数据yaml最常见的两个错误一是path写成绝对路径但不带train和val的相对子路径二是names写成dict但代码期望list。# traffic_light.yaml path: /home/workspace/traffic_dataset train: images/train val: images/val names: 0: digit_0 1: digit_1 2: digit_2 3: digit_3 4: digit_4 5: digit_5 6: digit_6 7: digit_7 8: digit_8 9: digit_9说明path是根目录train和val写相对路径这是ultralytics的标准读法别在train里写绝对路径再让path多余重复。names保持list风格严格按下标顺序对应类别如果模型配置里的nc和这个列表长度不一致训练时loss会异常抖动或直接报错。模型配置如果只做数字识别nc就是10如果还带红黄绿状态nl要另算别混在一个nc里。我一般先用yolov8n.yaml跑通数据链路确认loss能降下来再换改进结构。数据链路没跑通就上大模型遇到问题根本分不清是数据错了还是模型错了。4.3 训练命令与关键参数含义从imgsz到epochs一次看懂yolov8训练自己的数据集最常用的命令就是下面这一行。很多人照着别人的命令抄结果batch太大OOM、lr太高loss爆炸根本原因是不理解每个参数的含义。yolo detect train \ datatraffic_light.yaml \ modelyolov8_custom.yaml \ pretrainedyolov8n.pt \ imgsz640 \ batch16 \ epochs200 \ optimizerSGD \ lr00.01 \ lrf0.01 \ warmup_epochs3 \ patience30 \ cacheTrue \ device0参数说明imgsz640是速度与精度的平衡点字符小于10像素先试960显存不够就降batch。epochs不用一上来就300先用100看loss曲线是否还在降早停patience30会帮你省时间。optimizer首选SGD倒计时数字这种小目标对lr敏感lr0从0.01起步loss爆炸就降到0.005。lrf是最终学习率与初始学习率的比值0.01表示最后收敛到初始值的百分之一适合长时间训练。cacheTrue适合小数据集能把全部图片加载到显存或内存显著提速几万张的大数据集不开cache反而省事。注意同一个命令在不同GPU上的loss曲线不会完全一样。关键不是复现别人的loss值而是复现“下降趋势”。趋势对训练就在往前走。4.4 训练完成后的验证损失函数曲线图、PR曲线和混淆矩阵怎么看训练完会在runs/detect/trainN/目录下生成一堆产物。results.png就是yolov8画损失函数曲线图最直接的来源train/val的box_loss和cls_loss都在里面。先看val曲线有没有在最后阶段向上翘翘了就是过拟合epochs减到拐点附近重新训。然后看混淆矩阵里background被误判成digit的比例这个数值往往占夜间误检的大头。yolo detect val \ datatraffic_light.yaml \ modelruns/detect/train/weights/best.pt \ imgsz640 \ batch16跑完自动生成混淆矩阵和F1曲线。如果background列亮点多说明负样本不足或conf阈值太低需要回到数据集补背景样本。我还会把置信度低于0.25的预测单独拉出来看图通常一半是夜间小数字漏检那是结构改进没到位不是阈值问题另一半是标注框画偏了模型学歪了。验证时不要只盯mAP红绿灯倒计时场景更关心“连续帧数字串是否正确”。mAP高但两位数只认出其中一位对系统毫无意义。所以验证阶段要加一个序列检查把连续10帧的识别结果拼成时间序列看数字是否递减、是否有跳变这比单帧mAP更能反映真实路口体验。5. 智能交通视觉系统的避坑清单训练与部署阶段的五个常见翻车点这个方向我实际做过的次数不少踩过的坑往往不在模型结构上而在数据语义和部署交界处。下面五条是按出现频率排序的真实经历每条按现象、原因、解决展开。5.1 两位数字粘连模型把“15”识别成“1”或“5”现象10和9这种两位数字模型输出只有一个框类别在digit_1和digit_0之间漂移最终秒数变成1或9。原因标注时两个字符框距离太近训练时NMS把两个框合并或者特征图分辨率不足以把两个字符的纹理分开。解决标注时强制分离两个框之间至少留2像素间隙后处理按x坐标排序后如果两个框中心距小于0.6倍平均框宽就判定为粘连回炉重标。这个问题的根子在数据不在模型。5.2 夜间车灯反光把数字“洗白”现象白天mAP有0.93晚上布设后漏检率明显上升数字字符在画面上看起来还在但模型就是检测不到。原因训练集里夜间样本比例不到20%光度分布失衡车灯反光和LED高亮区处于同一灰度区间特征被高亮背景淹没。解决训练集重做白天和夜晚各占一半开启hsv_v增强并在自定义增强里叠加高斯噪声模拟夜噪给网络加ECA注意力压制大光斑。这类问题不改结构光靠调阈值是堵不住的。5.3 一个路口很准换个路口就掉点现象A路口效果很好迁移到B路口后数字识别率从0.9掉到0.7。原因采集时只录了一个方向、一个球机高度B路口的灯盘安装角度、LED色温、镜头透视完全不同。解决至少录三个不同朝向的路口按“路口”为单位划分train和val不要按帧随机分训练时加大hsv扰动让模型学的是数字结构而不是某个路口的LED色。域随机化是这里性价比最高的手段。5.4 测试集指标很高接上视频流后FPS骤降现象离线评测一帧10ms连上摄像头后只剩5fps监控画面肉眼可见地卡顿。原因视频流丢帧、推理和采集串行、前端可视化抢了推理线程。解决推理线程只做resize加infer显示线程另开用队列存帧队满丢旧帧保证推理永远处理最新帧。这个坑和模型无关但是上线观感最直接的翻车点十个人里有八个先怀疑模型慢实际是线程模型写错了。5.5 手机屏幕被识别成倒计时数字现象路侧画面拍到行人手机手机屏幕上的数字被模型当成digit_x输出告警乱报。原因训练负样本不足模型学到了“发光数字”特征而不是“红绿灯数字”的特征。解决从采集视频里把非信号灯的发光体抽出来做负样本用随机位置stamp到训练图像里部署时加ROI约束只允许信号灯区域产生检测框ROI由前端界面绘制并保存。负样本策略对误检的抑制效果比调conf阈值有用得多。6. 从PyTorch权重到Web前端可视化推理接口与实时交互界面权重文件只是中间产物智能交通系统最终要以Web界面呈现给值班人员。这一章用一个最小闭环把模型和前端串起来。6.1 FastAPI封装推理接口视频帧进、JSON结果出模型训练完要落地成服务。FastAPI是目前最省事的方案把推理循环包进独立进程别和Web服务抢GIL。常见接口设计如下from fastapi import FastAPI, UploadFile import numpy as np, cv2 from ultralytics import YOLO app FastAPI() model YOLO(best.pt) app.post(/detect) async def detect(file: UploadFile): data np.frombuffer(await file.read(), np.uint8) img cv2.imdecode(data, cv2.IMREAD_COLOR) results model.predict(img, imgsz640, conf0.35, verboseFalse) boxes, labels [], [] for r in results: for box in r.boxes: labels.append(int(box.cls[0])) boxes.append([float(v) for v in box.xyxy[0]]) return {digits: sort_digits(boxes, labels), boxes: boxes}逻辑说明sort_digits按x坐标把单字符框从左到右排序拼成字符串再转int得到完整倒计时秒数。conf0.35是调试出来的经验值夜间可以降到0.25——这个参数要开放给前端配置。6.2 WebSocket推送检测结果从“问一次答一次”变成“主动推帧”前端要实时HTTP轮询不够用。WebSocket是这类大屏可视化最常用的通道服务端每隔200ms推一次当前识别结果前端按帧渲染。const ws new WebSocket(ws://192.168.1.100:8000/ws); ws.onmessage (evt) { const data JSON.parse(evt.data); document.getElementById(countdown).innerText data.countdown; drawBoxes(data.boxes); // canvas画检测框 };参数说明推送间隔200ms意味着1秒最多5次刷新人眼足够带宽也很低。如果前端要做视频流叠加效果最好把原始帧经RTSP转HLS播放器显示检测框走WebSocket叠加两者分离才不会互相卡。6.3 前端交互界面的核心模块监控画面、数字面板与异常告警界面我习惯分成三块左侧实时监控画面连视频流画面上用canvas叠加检测框和当前秒数右侧面板按红、绿、黄状态列出当前相位和倒计时底部是异常告警条当模型连续N帧识别出的数字不递减时触发“信号灯状态异常”提示。ROI绘制功能放在设置面板里值班人员可以手动框出信号灯范围后端据此过滤误检。这个界面的价值不只是展示它也是排查模型的工具当告警频率异常升高时值班人员能直接看到是哪路相机的哪个ROI在误报从而快速定位是新路口没标好还是模型需要补数据。我现在的习惯是上线任何可视化系统前先拿一段录制的路口视频跑“回放测试”因为浏览器播放和WebSocket推送各自有延迟显示层的抖动用户会直接归因到模型头上。先把红线画成ROI约束、把conf参数留在前端配置项里再谈模型优化。这套思路希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →