首页 / 资讯中心 / 文章详情

YOLO斑马线检测实战:标签格式转换、数据划分与训练避坑指南

YOLO斑马线检测实战:标签格式转换、数据划分与训练避坑指南 ★ FEATURED ARTICLE
简介面向目标检测学习者和YOLO系列实战用户YOLO斑马线目标检测数据集基于真实场景拍摄共包含1000张高质量图片全部经LabelImg逐张人工标注标注框质量可靠整体场景丰富适合斑马线检测、交通场景识别及自动驾驶辅助视觉任务。标注数据同时提供VOC、COCO与YOLO三种主流格式标签按目录分类存放VOC与YOLO格式可分别用于经典检测框架和YOLOv5、YOLOv8等模型训练免去格式转换与标注整理的重复工作。资源包共2000个文件以1000个xml和990个txt标注文件为主另有yaml配置、Python划分脚本及HTML教程文档整体200.4MB目录结构清晰方便按需取用。随包附带Linux与Windows两套YOLO环境搭建及训练案例教程结合具体案例说明如何修改参数并训练自定义数据集同时提供一键划分脚本可灵活生成训练集、验证集与测试集满足不同实验设置。目前已有320人学习下载适合需要规范数据集、想快速跑通YOLO训练流程的入门与进阶学习者。1. 拿到YOLO斑马线训练包先别急着把命令敲下去三种格式标签和划分脚本才是这套数据的关键斑马线检测这个任务听起来小做起来全是细节。拿YOLO做目标检测的人都有体会真正卡住进度的不是模型结构而是数据标注格式不统一、坐标定义对不上、train/val/test切不干净随便一个都能让一次训练白跑好几个小时。这套方案的核心不是网络有多新而是把1000张斑马线图片连同VOC、COCO、YOLO三种格式的标签、可复现的划分脚本一起给到你拿到手直接喂给Ultralytics的YOLO就能开工。适合被格式转换折磨过的老手也适合想快速跑通第一个垂类检测项目的新手我自己第一次跑斑马线就是被坐标转换坑掉了两天。下面按落地顺序讲先读懂三种格式再做标签体检和划分然后训练、避坑、验收。2. 读懂VOC、COCO、YOLO三种标签格式坐标定义、类别索引与转换脚本2.1 VOC的XML左上右下像素坐标标注时最直观VOC格式源自PASCAL VOC竞赛是LabelImg这类工具默认导出的格式之一。一个斑马线标注在XML里长这样annotation folderimages/folder filename000001.jpg/filename size width1920/width height1080/height /size object namezebra_crossing/name bndbox xmin300/xmin ymin450/ymin xmax1200/xmax ymax850/ymax /bndbox /object /annotationXML里存的是像素绝对值。坐标系和OpenCV读图一致原点在图片左上角x向右增长y向下增长。bndbox给出的是框的左上角xmin, ymin和右下角xmax, ymax肉眼看一眼就能判断框在哪、框多大这是VOC最友好的地方。有一个细节需要注意size里的宽高必须和实际图片一致。很多标注工具会先把图片缩放到屏幕再标注导出的XML里size写的是缩放后的尺寸直接用真实图片训练坐标会整体偏移。所以拿到XML先抽查几张用脚本比对size和实际图片的宽高。VOC的类别名是字符串由name标签给出。单类数据集里只有zebra_crossing问题不大多类的时候字符串和数字索引的映射就要靠你自己维护一张字典这个我在2.4的转换脚本里会讲到。2.2 COCO的JSON左上角加宽高的bbox与从1开始的category_idCOCO格式是把整个数据集塞进一个JSON文件里核心是三个数组images、annotations、categories。一个斑马线样本对应的片段是{ images: [{id: 1, file_name: 000001.jpg, width: 1920, height: 1080}], annotations: [{id: 1, image_id: 1, category_id: 1, bbox: [300, 450, 900, 400], area: 360000, iscrowd: 0}], categories: [{id: 1, name: zebra_crossing}] }COCO的bbox是[x, y, width, height]意思是左上角x、左上角y、框宽、框高它和VOC的“给右下角坐标”是两种完全不同的描述方式。新手最容易犯的错是把VOC的xmax直接当成COCO的width写进去结果框全部被拉歪。VOC转COCO时width要用xmax - xmin算出来。另一个细节是category_id。COCO官方数据集里类别id习惯从1开始0留给背景。但这不是语法规定只是惯例。所以你在转YOLO之前必须确认自己的COCO JSON里categories的id和name是一张干净的映射表一旦出现id0的类别转YOLO后class_id就变成了-1训练直接出错。area字段对检测训练不是必须的但COCO格式要求它存在一般用width乘height算出来。iscrowd表示该标注是否是群体目标斑马线这类地面标线不会设成1。2.3 YOLO的TXT归一化中心点坐标训练时唯一真正读入的格式YOLO格式最简洁一张图对应一个TXT文件文件名和图片同名。每个目标一行五个数字0 0.390625 0.601852 0.468750 0.370370第一个数字是class_id从0开始后面四个是x_center、y_center、width、height全部除以图片宽高做了归一化取值都在0到1之间。0.390625来自1920宽度下的750像素中心点除以19200.601852来自650除以1080以此类推。Ultralytics训练时读的是TXT不是XML也不是JSON。它通过图片路径推断标签路径images/train/000001.jpg对应的标签是labels/train/000001.txt。所以配置数据集时不需要在命令里告诉YOLO标签在哪只要确保images和labels两个目录同级、内部子目录同名即可。TXT还有一个被忽略的特性如果一张图里没有目标对应TXT应该是一个空文件而不是不创建文件。做标签体检的时候要把“空TXT”和“缺失TXT”区分对待这个在下一章展开。三种格式的核心差异我总结过一张小表做转换的时候对照着看不容易错格式存储方式坐标定义类别表示训练是否直接使用VOC一张图一个XMLxmin, ymin, xmax, ymax像素字符串name否COCO整个数据集一个JSONx, y, width, height像素整数category_id常从1开始否YOLO一张图一个TXTx_center, y_center, width, height归一化整数class_id从0开始是2.4 把VOC转成YOLO格式转换脚本与四个边界坑拿到带VOC标注的数据集第一步永远是转成YOLO的TXT。我一般用下面这个脚本按目录遍历import os import xml.etree.ElementTree as ET class_map {zebra_crossing: 0} # 多类时在字典里继续加 def voc_to_yolo(xml_path, img_width, img_height): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: continue # 未登记的类别直接跳过防止脏数据进训练 cls_id class_map[name] box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 先算宽高再归一化顺序不能反 w (xmax - xmin) / img_width h (ymax - ymin) / img_height x_center ((xmin xmax) / 2) / img_width y_center ((ymin ymax) / 2) / img_height lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return lines逻辑说明先用name查字典得到class_id再从bndbox读四个角点宽度用xmax减xmin得到像素宽度再除以图片宽归一化中心点是两个角点平均值再归一化。这样算出的w和h一定是正数不会出现翻转框。参数说明cls_id从0开始是YOLO的规定。img_width和img_height必须来自实际图片不要直接用XML里的size这是我踩过的最隐蔽的坑——标注工具导出XML时有时会把size写成预览图尺寸和原图不一致坐标全部错位。这个脚本在真实数据上会撞到四个边界情况要逐一处理第一xmin大于等于xmax或ymin大于等于ymax标注人把框拉反了或拉成一条线这种标注要么丢弃要么人工修正不能让宽高变成负数或0进训练集。第二框超出图片边界比如xmax算出来是1930而图片宽只有1920。这类标注多数是标注时鼠标滑到画布外把坐标clamp回[0, 1]区间能保住样本但要在日志里打出来方便回头抽查。第三size和实际图片尺寸不一致。这个只能加断言如果转换出的中心点或宽高不在0到1之间直接停止并打印xml_path人工核对不能默默写进TXT。第四一张图有多个目标且类别混在一起时TXT里必须保持多行。有些脚本只取第一个object会把大量正样本丢掉mAP起步就吃亏。提示批量转换后随机抽5张图在原图上按TXT坐标画框这一眼比任何日志都能更快暴露坐标问题。3. 拿到数据先体检再划分标签完整性校验脚本与1000张图的场景去重3.1 体检脚本缺标签、空标注和越界坐标一票否决不管数据集是别人打包发的还是自己标注的第一件事不是训练是体检。体检规则很简单每张图片都有同名TXTTXT里每行恰好5个数字class_id不超出你规划的类别数。用一段脚本就能查完import os img_dir images label_dir labels nc 1 # 类别数和后面data.yaml保持一致 missing [] empty [] bad_line [] for name in sorted(os.listdir(img_dir)): base os.path.splitext(name)[0] txt_path os.path.join(label_dir, base .txt) if not os.path.exists(txt_path): missing.append(base) continue with open(txt_path) as f: lines [line.strip() for line in f if line.strip()] if not lines: empty.append(base) continue for line in lines: parts line.split() if len(parts) ! 5: bad_line.append(base) break cls_id int(parts[0]) if cls_id 0 or cls_id nc: bad_line.append(base) break print(f缺失标签: {len(missing)}, 空标签: {len(empty)}, 格式错误: {len(bad_line)})逻辑说明按扩展名去掉后缀拿到base名字去labels目录找同名TXT文件不存在记入missing存在但没有非空行记入empty。每个非空行用split切分长度不是5说明这一行格式坏了比如坐标里混进了逗号或者用了中文空格。参数说明nc一定要和后面data.yaml里的nc保持一致。sorted保证每次输出顺序稳定方便人工回看。跑完这步missing和empty要区分对待missing意味着这张图根本没有目标标注我会直接把它从训练集剔除empty是“确实没有目标”的图保留下来做负样本帮模型学会不误检。坐标越界的检查在转换脚本里已经做过这里只做格式门禁。两件套跑完数据集才允许进入划分阶段。3.2 固定随机种子的train/val/test划分脚本与参数调整划分脚本是这个训练包的另一半价值。最朴素的8:1:1切分用random.shuffle加切片就能实现但有几点必须写对import os import random import shutil random.seed(42) # 固定种子让划分结果可复现 img_dir images label_dir labels val_ratio 0.1 test_ratio 0.1 names sorted([f for f in os.listdir(img_dir) if f.lower().endswith(.jpg)]) random.shuffle(names) n len(names) n_val int(n * val_ratio) n_test int(n * test_ratio) val names[:n_val] test names[n_val:n_val n_test] train names[n_val n_test:] for split_name in [train, val, test]: os.makedirs(fimages/{split_name}, exist_okTrue) os.makedirs(flabels/{split_name}, exist_okTrue) for split_name, split_list in [(train, train), (val, val), (test, test)]: for name in split_list: base os.path.splitext(name)[0] shutil.copy(os.path.join(img_dir, name), fimages/{split_name}/{name}) shutil.copy(os.path.join(label_dir, base .txt), flabels/{split_name}/{base}.txt) print(ftrain{len(train)} val{len(val)} test{len(test)})逻辑说明先固定随机种子再对所有图片名shuffle按比例切片最后复制到三个子目录。复制而不是移动有两个原因一是保留原始完整数据集给后续“后悔药”二是调整比例后重新跑一遍就行不用重新收集数据。参数说明val_ratio和test_ratio在1000张图时各取0.1就是100张。检测任务里验证集100张够看趋势但要得到稳定的mAP估计建议test留到150张左右把比例调成0.075和0.15。斑马线是单类目标正样本密度低val太小会让mAP曲线抖动得像心电图。图片后缀不统一时endswith(.jpg)会漏掉png。稳妥的做法是用os.path.splitext(name)[1].lower() in {.jpg, .jpeg, .png}取后缀标签复制时直接用base拼.txt不要试图把.jpg替换成.txt因为原始后缀可能是.JPGsplitext拿到的base才是对的。3.3 场景去重视频抽帧数据不能全随机切分这是斑马线数据集最容易翻车的隐藏问题。如果1000张图里有大量是从同一段路口监控视频抽出来的帧相邻帧之间只差一两个像素的位移全随机划分会让train和val里都出现同一路口的几乎相同画面。模型等于把答案背下来了val的mAP虚高几个点换一个新路口立刻原形毕露。正确做法是按场景分组后再划分。做法是给每张图维护一个scene_id抽帧的图按视频文件名分组独立拍照的图一张算一组先对组打乱再往train/val/test里装整组。1000张图如果只有三五个场景再怎么划验证都是虚的此时要做的不只是划得更花哨而是去补充不同路口、不同时段的图让场景数先上去。判断自己的数据是不是抽帧的方法很简单把images目录按文件名排序看是否有连续编号的规律再看同编号下的画面是否近似。如果有要么重新抽帧拉大间隔要么按组划分。这一步决定验证集有没有参考价值比后面的训练参数重要得多。4. 用Ultralytics YOLO跑通斑马线训练data.yaml配置、训练命令与日志判读4.1 搭建目录结构与data.yamlimages和labels同级路径用绝对路径训练前把数据摆成Ultralytics默认的样子比在代码里写各种自定义DatasetLoader省心得多dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamldata.yaml是唯一需要手写的配置文件内容很少但每个字段都讲究path: /home/user/dataset # 数据集根目录绝对路径最稳 train: images/train # 相对path的路径指向images子目录 val: images/val test: images/test # 有就填val命令能顺便算test指标 nc: 1 names: 0: zebra_crossing关键点是train/val/test指向的是images目录而不是labels目录。Ultralytics会自动去同级找labels目录靠的是“图片和标签目录结构对称”这个约定。如果你手动把标签放到别处反而要在配置里写额外的label路径没必要。names的索引从0开始和TXT里的class_id严格对应。如果你在划分后改过类别记得回头把labels里所有TXT的class_id再刷一遍否则class_id2的斑马线会被训练当成另一类。4.2 训练最小命令与必调超参imgsz、batch、epochs、patience、device环境配置用pip install ultralytics一条命令搞定新版入口统一yolov8和yolo11的用法一致。训练命令我一般这样起yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ patience20 \ device0modelyolov8n.pt会加载在COCO上预训练过的权重。斑马线不在COCO的80类里但backbone学到的纹理、边缘、几何特征完全可以迁移1000张图的数据集从随机初始化开始训基本等于自虐。想更快用yolov8n.pt想更准换yolov8s.pt显存不够就保持nano。imgsz640是默认值对斑马线这类长条地面标线通常够用如果测试图是1080p且漏检集中在远处小目标可以提到960代价是训练时间涨将近一倍、显存涨一半以上。epochs设100加上patience20实际很多次在80轮左右就早停不会死等。batch16在12G显存的卡上能跑。注意Ultralytics把batch理解成总batch size不是单卡batch多卡时它自动均分。device0用第一张GPUCPU也能训但1000张图也要熬一个晚上不建议。这里有个参数新手喜欢调但我建议先别动optimizer。默认的auto会自动切换优化器SGD和AdamW的差异在现在版本里被抹平了很多先跑出一个baseline再去动优化器才有的放矢。4.3 训练日志判读mAP50为主box/cls/dfl损失曲线做旁证训练结束看runs/detect/train下的results.csv每一行是一轮的结果。我习惯先看mAP50-95单类斑马线任务里它比类间区分度更有参考价值。mAP50到0.9以上说明标注质量和数据量都够只有0.5左右就要回头怀疑标签坐标是不是转歪了。损失的判断有个反直觉的点train loss降、val loss也降才是真的学到train loss降、val loss不降甚至升说明模型在背训练集开始过拟合此时加大数据增强比加轮数有用train loss和val loss一起降但mAP不动大概率是正负样本比例问题回数据集里看是不是背景图太多。results.csv里同时有box_loss、cls_loss、dfl_loss三条损失曲线。box_loss和dfl_loss训练中期震荡下降是正常的如果出现断崖式暴涨先怀疑学习率而不是模型结构。cls_loss在单类任务里会很快压到很低不用太盯着。validation的mAP曲线震荡是常态尤其val只有100张图、斑马线在一部分图里还是小目标的时候连续几轮不涨交给patience早停别自己盯着曲线手动干预。5. 斑马线目标检测训练最容易翻车的五个坑5.1 坑一VOC转YOLO把右下角坐标当宽高直接归一化现象训练能跑loss也在降但val的mAP50一直卡在0.1以下可视化出来的框全部缩在图片右下角位置和真实斑马线完全对不上。原因VOC给的是左上角和右下角转YOLO时有人直接把(xmax, ymax)当成框的宽高去归一化没有先算xmax - xmin、ymax - ymin。因为右下角坐标一定大于真实宽高归一化后w和h都偏大中心点也右移下移框整体被拉错位。解决写转换脚本时先算像素宽高再归一化加一条断言如果xmax - xmin小于等于0就抛出异常。批量转换后随机抽5张图在原图上按TXT坐标画框用肉眼确认位置这一步是必须的。5.2 坑二COCO的category_id与YOLO的class_id错位现象多类数据集里模型把斑马线检成了另一类或者某一类始终不触发训练日志里某个类别的loss异常高。原因COCO的category_id习惯从1开始YOLO的class_id从0开始映射时直接拿category_id当class_id用全部类别错位一位还可能出现class_id-1的非法数据。解决无论转换脚本是谁写的转完TXT后跑一遍class_id盘点统计labels目录里出现过哪些数字和data.yaml里的names一一比照。单类数据集这个问题不明显一旦多类0和1的区别就是斑马线和一个不能混淆的干扰物之间的区别。# 统计labels目录出现的全部class_id grep -o ^[0-9] labels/*.txt | sort | uniq -c输出里出现负数或大于nc的数字说明映射表写错了回转换脚本里改class_map。出现多个数字而nc1说明类别没合并干净训练会把同一个东西当两拨来学。5.3 坑三划分脚本漏切labels目录训练时报找不到标签现象训练命令跑起来终端闪过一行warning大意是no labels found在对应路径程序不崩继续跑最后val的mAP是0train loss降了但什么也没学会。原因Ultralytics按图片路径推断标签路径images/train里有图但labels/train是空的它不会因为“没有标签”而拒绝训练只是真的在拿空标签训练整个流程等于白跑一次。解决划分完成后数一遍images/train下的jpg数量和labels/train下的txt数量必须严格相等再进训练。数量对不上时检查copy路径是不是写错或者标签目录里有没有多出空文件夹。我现在每次划分完都会打印四个数字train图、train标签、val图、val标签对齐了才放心。5.4 坑四验证集损失反弹就急着调学习率结果更糟现象训练到第80轮val loss开始向上翘有人立刻把learning_rate调小重新训练结果mAP50反而停在更低的水平前面几十轮白练。原因val set只有100张图样本少mAP和loss的波动本来就大尤其斑马线小目标多的时候val loss单轮反弹很可能是抽到了几张难的图而不是真的过拟合。此时真正该看的是mAP50是否还在涨mAP还在涨就不要动参数。解决mAP优先原则loss曲线只做参考。训练策略交给Ultralytics的patience和自动学习率调度它们默认就在val mAP上早停。手动干预只发生在mAP连续十几轮不涨且loss明显发散的时候而且一次只改一个参数别动完学习率又去动batch。5.5 坑五夜间远视角漏检光调模型不如先调数据增强现象白天测试效果不错一到夜间、雨天或者远处透视角度斑马线大片漏检模型像是直接“失明”。原因斑马线靠黑白条纹的对比度被模型识别夜间亮度低远视角条纹在640分辨率下只有十几个像素宽特征少得可怜。模型本身对小目标不友好这不是换个neck结构就能立刻解决的。解决数据优先。在训练时把Ultralytics内置的hsv_v增强拉到0.2到0.3模拟夜间低亮度加一些对比度变化模拟雨后反光。再配合imgsz提到960推理时把conf阈值降到0.2以下用连续帧确认来过滤误检。这一套组合下来夜间漏检率通常能改善一半以上比你折腾一小时改网络结构划算得多。如果接的是1080p视频流640分辨率的推理耗时在主流显卡上都不是瓶颈部署时真正的坑往往在阈值和帧间滤波。6. 训练完别急着部署用测试集扫置信度阈值做一次验收训练产出best.pt但这不等于能上线。我每次都会先拿测试集跑一遍预测看保存的可视化图这一步能暴露很多日志看不出的问题。yolo predict modelruns/detect/train/weights/best.pt \ sourcedataset/images/test \ conf0.25 \ saveTrue \ save_txtTrueconf0.25是起始阈值漏检多就往下降误检多就往上升。save_txtTrue会把每个检测框的类别和置信度写出来方便统计。翻图时重点看四个角落、透视变形处和遮挡一半的斑马线这些位置最容易出现边界漏检。可视化没问题之后再跑一次带splittest的验证拿到最终的mAP50-95和训练时的val曲线对比。如果test的指标明显低于val说明划分时场景泄漏了回第3章去检查是不是没按场景去重。再扫一遍conf从0.05到0.5的阈值画precision和recall曲线部署阈值取在拐点往上收5个百分点误检可控的同时尽量不漏。我去年做智慧交通项目就在这个环节吃过亏。第一次训练完直接拿0.5默认阈值上线白天没事夜间误检一堆路口栏杆的影子全被当成斑马线。后来补了夜间样本、重训、扫阈值白天夜间才都过验收。那之后我养成一个习惯不管数据集是谁给的先体检再划分后扫阈值再部署顺序一步不乱。这套流程跑熟了斑马线这个方向从数据到模型基本就不剩什么玄学了。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站