尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

交通信号灯目标检测实战:COCO转YOLO与YOLOv8训练全流程解析

交通信号灯目标检测实战:COCO转YOLO与YOLOv8训练全流程解析 简介面向计算机视觉项目实践的交通信号灯检测数据集适合目标检测算法研究者、自动驾驶或智能交通方向的开发者及相关专业学生使用。数据集覆盖道路场景下不同角度、光照与距离的红、绿、黄三类信号灯图像共包含2000个文件其中1995张jpg图片为原始样本3个json为遵循coco格式的目标标注文件另有2个txt文本文件压缩包整体约223.94MB。目前已有594人学习下载。得益于标注格式规范用户可直接借助MMDetection、YOLO等框架读取数据集快速完成训练集与验证集划分也可用于对比不同检测算法的性能表现。对于缺少标注样本、需要开展信号灯识别实验的开发者而言这份数据能显著降低数据采集与标注成本支撑课程设计、算法调优或竞赛验证等场景需求。1. 交通信号灯数据集红绿黄三色识别与 COCO 标注的落地价值做辅助驾驶或交通场景目标检测的同行大概率都经历过自己拿手机蹲路口拍红绿灯的狼狈拍回来的图角度歪、过曝、远处信号灯小到只有十几个像素标完一整天脖子都是硬的。这份交通信号灯数据集图片来自 Roboflow 公开采集统一归一化成 jpg全部带有 COCO 格式的 json 标注类别覆盖红、绿、黄三色信号灯。对正在做车辆辅助驾驶、V2X 路侧感知或者交通流量分析的人来说它省掉的是最枯燥的采集和清洗环节直接进入模型训练阶段。数据集本身的图片尺寸已经过预处理标注框贴合灯体拿来即用不挑框架YOLO 系、MMDetection、Detectron2 都能直接吃。适合的人群很明确入门目标检测但不想从零标数据的新手以及需要一套干净三色标注做 baseline 验证的算法工程师。2. COCO 格式解析先搞懂 json 里五个字段再开始训练2.1 COCO 标注的目录结构与字段含义数据集的标注文件是标准 COCO 格式打开 annotations 目录下的 json会看到 info、licenses、images、annotations、categories 五个顶层字段。这里我只关注后三个因为信息都在它们身上。import json with open(annotations/instances_train.json, r) as f: coco json.load(f) # 只看 categories确认类别 id 与名称的对应关系 for cat in coco[categories]: print(cat[id], cat[name])运行这段脚本你会看到 id 为 1、2、3 的类别分别是 red、green、yellow 或类似命名具体类别名以压缩包内 json 为准。这个 id 顺序很重要后面转 YOLO 格式时类别 id 就是从 0 开始重新映射的映射错了模型直接学歪。images 字段里每条记录包含 id、file_name、width、height。annotations 字段里每条记录包含 id、image_id、category_id、bbox、area、segmentation、iscrowd。segmentation 对交通信号灯这种刚性目标通常是个多边形或多点列表但训练时一般只用 bboxsegmentation 只是顺带标注的不必纠结。2.2 数据分布统计训练前必做的三件事拿到数据集第一件事不是急着训练而是先做数据体检。我用一段脚本统计三个核心指标类别数量分布、bbox 尺寸分布、图片尺寸分布。from collections import Counter area_bins {tiny: 0, small: 0, medium: 0, large: 0} category_counter Counter() img_sizes set() for ann in coco[annotations]: cat_id ann[category_id] category_counter[cat_id] 1 area ann[area] if area 32 * 32: area_bins[tiny] 1 elif area 96 * 96: area_bins[small] 1 elif area 256 * 256: area_bins[medium] 1 else: area_bins[large] 1 for img_info in coco[images]: img_sizes.add((img_info[width], img_info[height])) print(类别分布:, category_counter) print(目标尺寸分布:, area_bins) print(图片尺寸:, img_sizes)交通信号灯属于典型的小目标bbox 面积大概率集中在 tiny 和 small 区间这是这个数据集的性格。图片尺寸如果都是统一值说明 Roboflow 导出时做过分辨率归一省去你手动 resize 的麻烦如果尺寸不统一训练时就需要 dataloader 做 letterbox。这一步统计的意义在于它会直接影响 anchor 设置和推理分辨率。如果 bbox 平均面积只有几十乘几十像素训练分辨率还硬上 1280召回率会惨不忍睹因为小目标特征在下采样过程中被抹掉了。3. 标注格式转换COCO 转 YOLO 的脚本与坐标归一化3.1 COCO bbox 与 YOLO bbox 的坐标差异COCO 的 bbox 格式是 [x, y, width, height]x、y 是左上角坐标单位是像素。YOLO 的格式是 [class_id, x_center, y_center, width, height]其中中心点和宽高都做了归一化除以图片宽高。这个转换不复杂但坐标系的差异在刚接触时很容易让人翻车——COCO 的 x、y 是左上角YOLO 的中心点坐标换算时要记得加上半宽半高。3.2 转换脚本完整实现import json import os def coco_to_yolo(coco_json_path, output_dir): os.makedirs(output_dir, exist_okTrue) with open(coco_json_path, r) as f: coco json.load(f) # 构建 image_id 到文件名的映射 img_dict {} for img in coco[images]: img_dict[img[id]] img # 建立 category id 到 0,1,2 的映射顺序按 json 中的顺序 cat_id_map {} for idx, cat in enumerate(coco[categories]): cat_id_map[cat[id]] idx # 按 image_id 聚合 annotations anns_by_img {} for ann in coco[annotations]: image_id ann[image_id] if image_id not in anns_by_img: anns_by_img[image_id] [] anns_by_img[image_id].append(ann) for image_id, anns in anns_by_img.items(): img_info img_dict[image_id] img_w img_info[width] img_h img_info[height] txt_name os.path.splitext(img_info[file_name])[0] .txt txt_path os.path.join(output_dir, txt_name) with open(txt_path, w) as f: for ann in anns: cls_id cat_id_map[ann[category_id]] x, y, w, h ann[bbox] # COCO 左上角坐标转 YOLO 中心点归一化坐标 x_center (x w / 2) / img_w y_center (y h / 2) / img_h w_norm w / img_w h_norm h / img_h # 防止归一化后的值越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w_norm min(max(w_norm, 0.0), 1.0) h_norm min(max(h_norm, 0.0), 1.0) f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n) print(f转换完成共处理 {len(anns_by_img)} 张图片) # 使用示例 coco_to_yolo(annotations/instances_train.json, labels/train) coco_to_yolo(annotations/instances_val.json, labels/val)转换脚本的核心逻辑是把标注信息按图片重新聚合然后逐条做坐标换算。这里最关键的是 cat_id_map 的构建方式——COCO 的 category id 不一定是连续的 1、2、3可能有跳号直接用 enumerate 重新映射是安全的。归一化时加 clamp 是为了防止某些标注边界超出图像范围这在手工标注的数据集里时有发生不处理的话训练时 YOLO 会报错。3.3 转换后的完整性校验转换完别急着训练先做一次反向校验。我最常用的方法是随机抽 10 张图把 YOLO 的 txt 转回像素坐标画框肉眼对比原图和标注是否对齐。import cv2 import random def visualize_yolo(img_path, txt_path, class_names): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path, r) as f: lines f.readlines() for line in lines: cls_id, xc, yc, bw, bh map(float, line.split()) # YOLO 转回像素坐标 x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) color (0, 0, 255) if int(cls_id) 0 else (0, 255, 0) if int(cls_id) 1 else (0, 255, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[int(cls_id)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) cv2.imshow(check, img) cv2.waitKey(0) cv2.destroyAllWindows() # 随机抽一张 img_files os.listdir(images/train) random_img random.choice(img_files) txt_file os.path.splitext(random_img)[0] .txt visualize_yolo(os.path.join(images/train, random_img), os.path.join(labels/train, txt_file), [red, green, yellow])如果画出来的框有些偏了半个车身多半是转换脚本里坐标计算写错了。如果整体都正常只是个别小目标框略微偏移那是原始标注本身的误差这个数据集整体质量还行不需要返工。校验这一步建议养成习惯格式转换类任务最怕的就是批量转完才发现系统性错误回头再排查浪费半天。4. 模型训练配置基于 YOLOv8 的三色检测实战4.1 数据配置文件与超参设置数据准备好了接下来用 YOLOv8 训练。训练前需要写一个 data.yaml指定 train、val 路径和类别信息。# traffic_light.yaml train: /path/to/traffic-light-dataset/images/train val: /path/to/traffic-light-dataset/images/val nc: 3 names: 0: red 1: green 2: yellow这里要注意 names 列表的顺序必须和转换脚本里 cat_id_map 的映射顺序完全一致。系统不会替你校验这个只会按索引对齐顺序错了训练出来的模型就是张冠李戴。4.2 训练命令与小目标优化策略信号灯目标小默认的 640 输入分辨率下远处灯体可能只有 10x20 像素。我是这么处理的训练分辨率用 640但把 anchor 的尺度下限调小推理时用 1280 分辨率做超分推理小目标召回率会明显上来。yolo detect train \ modelyolov8n.pt \ datatraffic_light.yaml \ imgsz640 \ epochs100 \ batch16 \ patience20 \ optimizerAdamW \ lr00.001 \ lrf0.01 \ warmup_epochs3 \ projecttraffic_light_exp参数说明model 用 yolov8n.pt 做预训练权重不要自己从随机权重开始训信号灯虽然是小目标数据集但底层特征提取器在 COCO 上预训练过迁移过来收敛快得多。imgsz640 是训练分辨率兼顾速度和精度epochs 100 轮在这个量级的数据集上足够了配 patience20 做早停。batch 大小取决于显存16 在 24G 卡上比较稳妥显存小就降到 8。warmup_epochs3 在前 3 轮用较低学习率让网络先适应数据分布防止前期震荡。训练过程中重点关注 loss 曲线的 convergence 行为。正常情况 bbox_loss 和 cls_loss 都在稳步下降如果 cls_loss 抖动剧烈大概率是类别不均衡——黄灯样本天然少于红灯绿灯红绿灯在实际路口中红灯绿灯占比高黄灯只在切换瞬间亮起样本少是这类数据集的通病。处理办法是给黄灯类别加 loss 权重但 YOLOv8 默认不支持 per-class loss weight我一般通过过采样黄灯样本来缓解或者训练时用 mosaic 增强把黄灯单独拼进来。4.3 训练结果评估别只看 mAP训练完成后验证集结果会输出 Precision、Recall、mAP50、mAP50-95 四个指标。对交通信号灯场景我更关注 Recall 而不是 mAP——漏检一个红灯在自动驾驶场景里是安全事故误检一个红灯笼当红灯顶多算是虚惊一场漏检是真问题。信号灯目标小mAP50-95 不会很高正常范围在 0.6 到 0.8 之间已经算可用不要被 COCO 榜单上 0.9 的数值迷惑那是大型目标数据集的表现。小目标数据集 mAP 天然被拉低。5. 常见问题排查五条血泪踩坑记录5.1 标注框错位 —— 换格式后前 100 张必须肉眼检查现象转换完 COCO 转 YOLO 后训练loss 一开始不降画框发现框整体偏移。原因最常见的是 COCO 的 bbox 某些工具导出的坐标原点是图片中心而不是左上角或者某些框的宽高被写反了。数据集来自 Roboflow 导出正常不会出现这个问题但手工转换脚本写错坐标偏移量的情况很常见。解决不要相信脚本输出训练前随机抽 100 张图做可视化。我现在的习惯是转换完先画 50 张看一遍再训练。看起来浪费时间实际省的是训练完发现学歪了再排查的几小时。5.2 黄灯类别召回率极低现象训练 100 轮后红绿灯 AP 都到 0.85 以上黄灯只有 0.4 左右。原因数据集本身黄灯样本少信号灯在路口的亮灯时间比例决定了黄灯出现的频率天然低。另外黄灯和红灯在某些光照条件下视觉特征接近橙色偏红互相混淆。解决按 1:1:1 对黄灯做增强复制把黄灯样本过采样到和红灯一样多。数据增强时多做 HSV 空间扰动让模型对颜色差异更敏感。如果还不行考虑用颜色空间分离预处理输入模型。5.3 小目标漏检严重验证集上远处灯体全丢现象mAP 指标一切正常但实际跑视频流时距离 50 米以外的信号灯完全检测不到。原因训练和推理分辨率不一致。训练时用的 640 输入推理时也用了 640远处的灯体可能只有 8x16 像素特征在下采样到 P5 层时被压缩得只剩几个像素。解决训练时保持 640推理时上采样到 1280 或者 960。YOLOv8 对输入分辨率有一定泛化能力实测 640 训练的模型在 960 推理时小目标召回率能提升 10% 到 15%。如果项目对实时性要求高舍不得上采样推理那就只能换 P2 层结构的小目标检测头。5.4 类别顺序错乱red 被识别成 green现象训练完测试红灯全被识别成绿灯但模型本身 AP 很高。原因data.yaml 里的 names 顺序和转换脚本里的 cat_id_map 顺序不一致。比如转换脚本里 id0 映射 red但 data.yaml 里 0 写成了 green模型实际学的是第一个索引类别只是名字标错了。解决训练前做一个最小的 sanity check——从数据集里取一张红灯图预测看输出类别。不要看 mAPmAP 是对的因为 label 和预测是按同一个错误顺序对齐的模型本身没学错是你的名字表写错了导致可视化的时候张冠李戴。5.5 训练早期 loss 直接 NaN现象epoch 1 结束 loss 变成 nan后面全部无效。原因学习率设置过大或者 batch size 过小导致梯度爆炸。信号灯图片整体偏暗深色图像上的梯度本身就比普通自然图像大配合大 lr 容易炸。解决把 lr0 从默认的 0.01 降到 0.001batch 调到 16 以上。如果还炸检查数据里有没有全黑的坏图——数据清洗阶段可以写个脚本把所有灰度标准差小于 5 的图筛出来删掉。6. 模型部署验证从指标到真实场景的最后一公里6.1 用视频流做真实场景验证训练完的模型在验证集上指标再好看也不代表在真实场景能稳定工作。我每次做完信号灯检测模型都会用一段实际路口拍摄的视频做端到端验证。from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourceintersection.mp4, imgsz1280, conf0.25, saveTrue, device0, classes[0, 1, 2] )推理时分辨率提到 1280 是为了让小目标信号灯有更多像素可检测。conf 阈值 0.25 是一个经验值信号灯误检的代价比漏检小所以阈值可以适当放低。classes 参数限定输出类别防止模型把远处红色刹车灯误检成红灯。6.2 从检测到业务逻辑三色状态判定检测模型输出的是框和类别但实际业务要的是当前路口信号灯状态。我的做法是取单帧内所有检测框按类别投票决定当前状态——如果不做时序滤波单帧误检会导致状态跳变。def get_traffic_light_state(detections, conf_threshold0.4): detections: list of (bbox, cls_id, conf) 返回当前帧信号灯状态: red, green, yellow, unknown votes {red: 0, green: 0, yellow: 0} for bbox, cls_id, conf in detections: if conf conf_threshold: continue state {0: red, 1: green, 2: yellow}[cls_id] votes[state] conf # 用置信度加权 total sum(votes.values()) if total 0: return unknown best_state max(votes, keyvotes.get) best_ratio votes[best_state] / total # 需要超过 60% 的置信度占比才判定为当前状态 if best_ratio 0.6: return unknown return best_state这段代码用置信度加权投票替代简单计数防止多个低置信度误检框聚集后形成错误多数。判定阈值 0.6 按实际场景可以调整如果发现状态跳变频繁就调高如果发现真实状态切换检测慢就调低。信号灯检测的坑在于小目标加颜色敏感。红绿灯在图像里占的面积小但颜色特征极其明确——这份数据集把三色分开标注比笼统的 traffic_light 单类别标注更能让模型学到颜色语义。从那以后我每次做信号灯相关项目第一件事就是先拿这套 COCO 标注跑一遍 baseline再谈改进。希望帮到你。本文还有配套的精品资源点击获取
返回列表