ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

交通信号灯数据集COCO转YOLO实战:从标注验货到训练避坑

交通信号灯数据集COCO转YOLO实战:从标注验货到训练避坑 简介一份面向目标检测与计算机视觉实践的交通信号灯数据集覆盖红、绿、黄三类信号灯实例采用COCO格式标注适合用于模型训练、验证与算法评测。资源包含2000个文件其中1995张jpg图像提供真实道路及复杂背景下的信号灯样本3个json文件对应COCO标准标注信息可直接接入主流检测框架另有2个txt文件用于类别说明或数据划分压缩包总大小223.94MB目录结构清晰便于按需解压使用。目前已有594人学习下载。该数据集可支撑交通场景感知、智能驾驶辅助等方向的研究与教学省去自行采集和标注的繁琐流程帮助开发者快速搭建实验数据聚焦模型设计与调优。对于需要规范标注数据的初学者和研究人员是一份较为实用的起步资源。1. 交通信号灯数据集三色识别为什么没有想象中简单「交通信号灯数据集」听起来是感知任务里最没门槛的一类——红灯停、绿灯行、黄灯等一等三分类而已。可真把模型放到夜间路口、逆光、小雨天和强反射路面跑一遍你会发现这三个颜色的识别准确率往往连白天的一半都不到。问题大多不在模型结构而在数据本身灯体小、亮度高、颜色受曝光和白平衡影响大COCO格式的标注一旦在类别映射、边框范围这些细节上含糊训练出来的检测器就只能在测试集上自欺欺人。这套zip提供的就是带红绿黄三类标签、按COCO JSON组织的信号灯数据解压后通常能看到 images 目录和 annotations 目录适合自动驾驶感知、智慧交通路侧巡检、端侧信号灯识别这类场景。我按自己处理信号灯项目的习惯把这份数据从验货到训练、再到评估的完整路径拆开讲。新手可以照着每一步操作熟手可以重点看避坑章节里的参数边界。2. 拆开COCO标注信号灯数据的JSON结构、字段含义与验货手段2.1 从instances_train.json读懂三类标记categories、images、annotations拿到COCO格式的数据集第一步不是急着训练而是先把JSON结构摸清楚。一个标准的COCO标注文件包含五个顶层字段info数据集说明、licenses许可、images图片列表、annotations标注列表、categories类别列表。其中信号灯项目真正关心的是后三个。打开文件前先跑一段统计脚本确认图片数、框数和类别名避免后面在错误的映射关系上浪费半天。import json import collections with open(annotations/instances_train.json, r, encodingutf-8) as f: coco json.load(f) images coco[images] annotations coco[annotations] categories coco[categories] print(图片数:, len(images)) print(标注框数:, len(annotations)) # 建立 category_id 到类别名的索引这就是后面转格式时用的“标记名字典” cat_id_to_name {cat[id]: cat[name] for cat in categories} print(类别定义:, cat_id_to_name) # 统计每个类别的样本数量信号灯数据很容易出现黄灯样本远少于红灯、绿灯的情况 cls_counter collections.Counter(ann[category_id] for ann in annotations) for cat_id, cnt in cls_counter.items(): print(f类别 {cat_id} ({cat_id_to_name[cat_id]}): {cnt} 个标注)这段脚本重点看三件事。第一categories里每个类别的id通常从 1 开始连续编号但有些标注工具生成的id不连续甚至从 0 开始这直接决定后面转换脚本要不要做偏移。第二annotations里的bbox字段是[x, y, width, height]格式不是[x1, y1, x2, y2]很多第一次接触COCO的人在这里看走眼。第三类别名要统一成英文小写red、green、yellow如果数据集里混入了 amber、red_light 这类别名后面训练时类别数会莫名变多。2.2 信号灯数据集是怎么标注出来的工具的选用、灯体框法与版本管理COCO格式本身只是一种组织方式真正决定信号灯数据集好不好用的是标注时的框选策略。我的习惯是优先按「单个灯体」框选而不是框整个黑底灯组。框住一盏亮着的红灯bbox 是紧凑的小目标框住整个红绿灯组bbox 变成中大型目标检测器学到的其实是「灯组区域」而不是「灯的颜色」。这两种策略在mAP数字上差别不大但接下游比如根据灯体位置估算距离、判断倒计时时不紧凑的bbox会带来明显误差。常见的标注工具是Labelme或X-AnyLabeling。Labelme的默认导出格式是单个JSON文件之后需要再从JSON合成COCOX-AnyLabeling可以直接导出COCO格式。处理信号灯这种小物体建议画矩形框而不是多边形因为绿灯和黄灯的发光区域边界模糊多边形标注的主观性会比矩形更大。多人协作时原始标注文件常用SVN这类版本管理工具维护这里有个血泪教训SVN按行合并文本多人同时改同一个标注JSON必然产生冲突而且冲突后留下的往往是一份语法损坏的JSON解析到一半就报错。我一般让每个人负责不同的图片ID段各自提交最后合并时统一重排 annotation id 和 image_id而不是直接让SVN自动合并。标注策略上还有一条需要提前决定信号灯的黄灯闪烁和红绿灯切换瞬间同一盏灯可能拍到半亮半暗的过渡态。这种帧要么删掉要么标记为单独的 blinking 类别不要硬塞进 yellow 类里否则模型会把闪烁状态和稳定状态学到两套特征。2.3 拿到数据集先验货可视化检查与目标尺寸分布统计JSON结构没问题、标注数量合理并不代表数据可以直接训练。我每次必做两件事抽样可视化、统计目标尺寸分布。抽样可视化可以在图上画出bbox并保存到本地目录人工排查有没有错标、漏标、框得离谱的样本。OpenCV的putText不支持中文所以输出标签直接用类别ID或英文名。import json import os from PIL import Image, ImageDraw, ImageFont with open(annotations/instances_train.json, r, encodingutf-8) as f: coco json.load(f) images_by_id {img[id]: img for img in coco[images]} cat_id_to_name {cat[id]: cat[name] for cat in coco[categories]} os.makedirs(check, exist_okTrue) # 只画前200个标注防止一次生成太多图片 count 0 for ann in coco[annotations]: if count 200: break img_info images_by_id[ann[image_id]] img_path os.path.join(images, img_info[file_name]) if not os.path.exists(img_path): continue img Image.open(img_path).convert(RGB) draw ImageDraw.Draw(img) x, y, w, h [int(v) for v in ann[bbox]] draw.rectangle([x, y, x w, y h], outlinered, width3) label cat_id_to_name.get(ann[category_id], str(ann[category_id])) draw.text((x, max(0, y - 15)), label, fillred) out_path os.path.join(check, fimg{img_info[id]}_ann{ann[id]}.jpg) img.save(out_path) count 1 print(已生成可视化样本到 check/ 目录)花几分钟看完这些图能发现很多JSON统计暴露不了的问题有没有把车尾灯当红灯框进去、有没有把倒计时数字当成整盏灯、黄灯和红灯在视觉上是否真的可分。另外要统计一下目标尺寸——信号灯在路口画面里通常是几十个像素的小目标知道这个分布后才能决定训练时的输入分辨率。统计方法很简单把所有bbox的宽高分别取出来算分位数如果中位数宽高小于32像素那训练时imgsz640大概率是不够的后面第3章我会给出参数建议。3. 让标注跑起来COCO转YOLO、数据集划分与信号灯训练参数3.1 先划分数据集而不是先转格式按视频片段分组的划分脚本很多人拿到数据先写转换脚本把COCO转成YOLO txt再考虑划分训练验证集。我习惯反过来先划分图片再做格式转换。原因是信号灯数据通常来自连续视频抽帧相邻帧之间高度相似。如果用随机划分同一段视频的前一帧进了训练集、后一帧进了验证集验证集mAP会虚高等模型部署到新路口立刻打回原形。import json import random random.seed(42) with open(annotations/instances_train.json, r, encodingutf-8) as f: coco json.load(f) images coco[images] image_ids [img[id] for img in images] # 按 80/20 做随机划分仅适用于非视频连续帧的数据 train_ids set(random.sample(image_ids, int(len(image_ids) * 0.8))) val_ids set(image_ids) - train_ids print(训练集图片数:, len(train_ids)) print(验证集图片数:, len(val_ids)) # 在文件系统里按 train/val 子目录复制或移动图片 import shutil import os os.makedirs(images/train, exist_okTrue) os.makedirs(images/val, exist_okTrue) for img in images: src os.path.join(images, img[file_name]) if not os.path.exists(src): continue dst_dir images/train if img[id] in train_ids else images/val shutil.copy(src, os.path.join(dst_dir, img[file_name]))如果数据来自多个路段或者多段视频简单随机划分依然不靠谱。正确做法是按「视频片段ID」或「拍摄场景ID」分组划分保证同一个场景的帧只在训练集或只在验证集。可以用 scikit-learn 的GroupShuffleSplitgroups数组里每个元素对应一张图片所属的视频片段编号test_size0.2表示抽出20%的片段做验证。参数上要注意random_state必须固定否则每次运行划分结果不同调参时对比评估就没意义了。如果 zip 里已经自带了 train/val 的 JSON 文件说明作者已经做了这一步你就没必要再划分直接进入转换阶段就行。3.2 COCO转YOLO txt的转换脚本类别映射、归一化与越界处理YOLO系列训练框架读不了COCO的JSON它需要每张图片对应一个同名txt文件每行五个数字类别ID、归一化中心点x、归一化中心点y、归一化框宽、归一化框高。转换逻辑不复杂但有两个坑。第一是类别ID偏移COCO的category_id从1开始YOLO要求的类别ID从0开始两者差1。第二是bbox越界标注工具偶尔会把框画出图片边界转换时不做clip会让模型学到错误的目标位置。import json import os def coco_to_yolo(coco_path, images_dir, out_dir, cat_offset1, valid_cat_idsNone): with open(coco_path, r, encodingutf-8) as f: coco json.load(f) images_by_id {img[id]: img for img in coco[images]} os.makedirs(out_dir, exist_okTrue) for ann in coco[annotations]: cat_id ann[category_id] # 如果指定了有效类别集合跳过不在集合内的类别 if valid_cat_ids and cat_id not in valid_cat_ids: continue img images_by_id[ann[image_id]] img_w, img_h img[width], img[height] x, y, w, h ann[bbox] # 处理越界框中心点被截断时重新计算有效宽高 x2 min(x w, img_w) y2 min(y h, img_h) x max(x, 0) y max(y, 0) w x2 - x h y2 - y if w 0 or h 0: continue # 类别ID按偏移量转换并做合法性校验 cls_id cat_id - cat_offset if cls_id 0: continue # 转为YOLO归一化的中心点坐标 cx (x w / 2) / img_w cy (y h / 2) / img_h nw w / img_w nh h / img_h txt_name os.path.splitext(img[file_name])[0] .txt txt_path os.path.join(out_dir, txt_name) with open(txt_path, a, encodingutf-8) as f: f.write(f{cls_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n) print(转换完成标注文件输出到:, out_dir) coco_to_yolo( coco_pathannotations/instances_train.json, images_dirimages/train, out_dirlabels/train, cat_offset1, valid_cat_ids{1, 2, 3} # 假设1red, 2green, 3yellow )脚本里cat_offset是给类别ID偏移用的如果数据集的category_id恰好是0、1、2这里就传0。valid_cat_ids用于过滤掉噪声类别比如某些信号灯数据集里混入了「行人过街按钮」这类附属目标。转换完成后检查几个关键数字labels/train里txt文件的数量应该约等于图片数量随便打开一个txt文件确认五列数值都在0到1之间再数一下每种类别出现的总行数和COCO JSON里的分布对得上才对。3.3 最小训练配置img尺寸、色彩增强、翻转与mosaic的取舍转换完成后进入训练环节。信号灯检测器和常规目标检测有个显著区别颜色本身就是最强的类别特征所以一切可能破坏颜色语义的数据增强都要谨慎。我在Ultralytics YOLO配置里一般这样做。# signal_light.yaml path: ./ train: images/train val: images/val nc: 3 names: 0: red 1: green 2: yellowyolo detect train \ modelyolov8s.pt \ datasignal_light.yaml \ imgsz1280 \ epochs120 \ hsv_h0 \ hsv_s0.2 \ hsv_v0.2 \ fliplr0 \ flipud0.3 \ mosaic0.5 \ batch-1几个关键参数说明。hsv_h0是我做信号灯检测必改的参数。HSV色调扰动会把红色变成橙色、把黄色变成绿色这种增强在普通数据集上能提升泛化能力在信号灯数据上等于往类别标签里撒盐。HSV饱和度和亮度扰动可以留一点用来模拟早晚光线变化但幅度控制在0.2以内。fliplr0同样是信号灯特有问题。圆饼灯水平翻转不影响语义但数据里如果有箭头灯左转箭头、右转箭头水平翻转后箭头方向颠倒类别标签就错了。如果确定数据里全是圆饼灯开fliplr0.5可以增加样本多样性我一般宁愿不开因为信号灯在路口的物理位置左右分布本就固定翻转带来的增益有限。mosaic0.5是一个值得反复试验的参数。Mosaic把四张图拼成一张小目标在拼接过程中很容易被裁掉一半变成半个红灯。我通常从0.5起步如果验证集小目标AP不涨就降到0.3或直接关闭。imgsz1280是所有参数里对检测效果影响最大的一个。信号灯在1080p画面里往往只有20到50像素imgsz640意味着目标缩到十几像素特征几乎消失。显存够就上1280不够就退到960再不够就640配合SAHI切片推理但后者会增加部署复杂度。训练参数和硬件对应关系可以参考这个表模型imgsz典型显存适用场景YOLOv8n6406GB以内端侧实时推理YOLOv8s12808-12GB大多数路侧场景YOLOv8m128016GB以上夜间小目标较多的场景4. 信号灯数据集避坑5个让模型翻车的常见问题与排查4.1 黄灯被模型当成了红灯现象白天测试效果不错傍晚和夜间黄灯识别准确率明显下降混淆矩阵里 red 和 yellow 两类互串严重。原因有两层。第一层是标注问题标注者主观上对黄灯和红灯的色相边界把握不一致或者标注工具里有人写了amber、yellow_light这种别名导致标记名字典不统一类别数量从3个变成4个。第二层是数据分布问题黄灯在路口的点亮时间最短稳定黄灯样本可能只有红灯样本的三分之一模型天然倾向把不确定的样本分到多数类。解决先回到第2章的统计脚本检查categories定义和类别分布。把标记名字典统一成red、green、yellow三个ID。如果类别确实不平衡优先给黄灯类加权重或者用数据增强提高黄灯帧的出现频率不要简单粗暴地复制黄灯样本到训练集容易过拟合。4.2 夜间过曝让红灯变成白色灯片现象夜间测试时红灯漏检率高有时候检测器把红灯当成白灯或黄灯输出低置信度结果。原因信号灯LED亮度很高相机ISP自动曝光为了平衡整体画面会把亮着的红灯过曝成一片白。这种样本里红色通道接近饱和RGB特征已经丢失模型在白天学到的「红色红灯」规则失效。解决先把过曝样本抽样画出来看看确认是不是这个问题。如果过曝比例高第一个手段是做数据清洗把红通道饱和面积超过一定比例的样本单独挑出来人工确认还能不能辨认第二个手段是训练时对图像做亮度增强模拟不同曝光条件下的灯体颜色变化第三个手段是专门采集夜间和黄昏数据让训练集覆盖从欠曝到过曝的完整区间。这三种手段按成本从低到高排列先做清洗再看效果决定要不要增加数据。4.3 验证集mAP虚高随机划分把同一段视频拆散了现象训练集和验证集mAP都很高达到0.85以上但换一个没见过的路口视频测试mAP掉到0.4。原因数据来自视频抽帧时相邻两帧的画面几乎一样。随机划分数据集时第10帧进了训练集、第12帧进了验证集模型等于「提前见过」验证集的内容。解决用第3章的GroupShuffleSplit按视频片段划分保证验证集里出现的路口和场景在训练阶段完全没出现过。如果zip数据里没有提供视频片段ID可以用文件名里的时间戳或地点字段猜测分组。验证结果以新场景测试为准不要只看验证集曲线。4.4 COCO evaluator的area字段陷阱现象用COCO API评估时发现 AP_small 异常低、AP_medium 异常高但人工看图检测效果还行。原因COCO评估里的小目标、中目标、大目标划分依赖annotations里的area字段。很多转换脚本把area直接赋值成bbox的宽乘高像素面积如果原图分辨率是1920×1080而评估代码里用默认的32×32、96×96像素阈值来分桶大量信号灯会被归错桶导致小目标统计失真。解决确认area和bbox使用的单位一致。如果只有bboxarea w * h评估时也按这个单位算。另外要检查area有没有被写成归一化值0到1之间的小数这个错误会让所有目标都被分到small类里。排查方法很简单随机抽20个标注手工计算一下bbox面积和JSON里的area字段对比。4.5 命令行参数与配置文件打架现象照着网上的命令训练时终端报您使用的是不受支持的命令行标记或者明明传了hsv_h0训练日志里增强参数还是默认值。原因新版本的YOLO训练脚本对命令行参数做了白名单校验任何未定义的参数都会被当成非法标记拒绝。另一部分参数如果同时出现在配置文件和命令行里命令行覆盖规则和配置文件覆盖规则在不同版本里不一致。解决把训练超参数统一写进data.yaml或独立的args.yaml命令行只保留模型路径和数据路径这种最简单的内容。改配置后先打印一遍完整配置确认生效再启动训练。这个习惯能省下大量反复启动训练的等待时间。5. 验证不止mAP分距离评估与信号灯状态平滑mAP是及格线不是终点。信号灯检测接下游任务时至少要回答两个问题近处能不能稳定识别、远处能不能及时识别。所以我建议把目标按像素高度分桶评估比如h 20px、20 h 60px、h 60px三档分别看各档的查全率。原因是信号灯越小检测难度越高三档指标能直接告诉你模型的实际可用距离范围而这个信息mAP是给不出来的。分析脚本不需要额外依赖直接从验证集结果里过滤出不同高度的目标框分别算precision和recall。第二种验证思路是检测器输出之后加状态平滑。信号灯本身有严格的时序状态链——红灯、绿灯、黄灯按固定顺序切换单帧检测结果却经常出现跳变红灯结束时某一帧被误判成黄灯下一帧又变回红灯。这种噪声对下游决策系统很不友好。一个成本最低的平滑方法是用滑窗投票取当前帧前N帧的检测结果做众数统计只有某个类别占比超过阈值才输出。from collections import deque, Counter # 维护最近5帧的检测类别状态 state_buf deque(maxlen5) def smooth_state(current_state, threshold0.6): if current_state is not None: state_buf.append(current_state) if len(state_buf) 3: return current_state counter Counter(state_buf) main_state, count counter.most_common(1)[0] if count / len(state_buf) threshold: return main_state return None参数上maxlen5表示滑窗5帧threshold0.6意味着5帧里要有3帧以上赞成同一状态才输出。这套逻辑对单帧误检有抑制作用对持续一两帧的漏检也能补回来。如果你想做得更细可以做带时长的状态机信号灯的每一种状态至少持续若干秒实践中我给每帧打上时间戳和周期序号用这类「日期和周期标记」约束状态切换不允许逆序发生模型输出和先验状态冲突时直接按先验状态返回。我处理信号灯项目的习惯是先把第2章的验货脚本跑一遍确认数据没有大类错标再按第3章的流程划分、转换、训练最后用第5章的分距离评估验证部署边界。这套流程看着朴素但能避开大多数靠玄学调参的弯路。希望帮到你。本文还有配套的精品资源点击获取
返回列表