地铁警戒线检测数据集:YOLO与VOC格式详解及YOLOv8训练实战
发布时间:2026/9/28 16:59:13来源:尧图网络
简介面向地铁场景目标检测任务这套数据集以警戒线warning_line为唯一检测类别提供清晰现场图片和1635个矩形标注框覆盖真实环境下的地铁警戒线形态适合作为YOLO、Faster R-CNN等模型训练与算法验证的基准数据。包内划分JPEGImages、Annotations、labels三个文件夹分别存放图片、VOC格式xml标注和YOLO格式txt标注压缩包共2000个文件以xml和txt标注文件为主整体约116.34MB解压后即可接入常用检测框架。所有图片为清晰原图未做增强标签采用矩形框并保证标注合理准确便于使用者直接开展训练、评测或数据清洗。已有158人浏览学习适合目标检测初学者练习标注格式转换也适合工程人员快速搭建地铁场景警戒线检测实验。1. 地铁警戒线检测数据集到底解决什么问题一张图里的线比一箱图里的物体更难标地铁站台的黄黑警戒线看起来是画面里一条简单线条真丢给目标检测模型却是个麻烦目标细、长、横贯画面、贴在人群脚边背景稍微乱一点就容易漏。要做这类视觉检测第一步就是数据而“数据集地铁警戒线检测数据集yolovoc格式1125张.zip”这类资源解决的正是这一步——1125张真实站台图片每张图同时带YOLO的txt标签和VOC的XML标签解压出来就能直接进训练管线。适合谁做站台越线告警、列车进站安全辅助、轨道交通安全项目的人或者刚拿到一批细长小目标数据想练手的研究者。下面按我拆这类包的习惯把格式、训练、转换和踩坑一条条讲清楚。2. 读懂YOLO与VOC两套标注格式解压检查、坐标换算与差异对比拿到这个zip第一件事不是急着写训练脚本而是把压缩包当成陌生代码库一样盘一遍看看目录怎么组织、标签里都有哪些类别、坐标是怎么存的。因为后面所有训练和转换脚本都是建立在这套结构上的结构看错了后面全是白忙。2.1 先解压再把目录结构整个盘一遍我一般会在干净目录里解压文件名带中文也不影响。解压后先用 find 统计图片和标签数量1125张图正常应该看到 1125 张图片文件和对应数量的标注文件。数量对不上说明包里有缺图或缺标签得先处理再开工。mkdir -p metro_line cd metro_line unzip ../数据集地铁警戒线检测数据集yolovoc格式1125张.zip # 看整体结构 find . -maxdepth 2 -type d | sort # 分别统计图片和标注数量 find . -name *.jpg -o -name *.png -o -name *.bmp | wc -l find . -name *.txt -o -name *.xml | wc -l这套命令里unzip 的-d参数可以指定解压目录不加也行但统一目录方便后面写相对路径。数量统计很关键如果图片 1125 张、txt 标签 1125 个、xml 标签 1125 个那说明标注齐全如果某个格式数量对不上后面转换的时候一定会在某个角落翻车。常见的打包结构有两种。一种是 YOLO 风格images/train、images/val、labels/train、labels/val图片和标签按集分开另一种是 VOC 风格JPEGImages放图、Annotations放xml、ImageSets/Main放划分清单。这个 zip 既然标了 yolovoc 双格式大概率两套都在解压后先确认好再往下走别拿到哪种就当哪种用。2.2 YOLO格式txt里那五个数字是怎么把一条线框住的YOLO 的标签是一个文本文件对应一张同名图片文件里每行五个数字类别id center_x center_y width height。这里的前四个坐标全部归一化到 0~1中心点坐标和宽高都除以原图宽高。和VOC的绝对像素坐标完全是两套逻辑混用必挂。比如一行0 0.8123 0.5401 0.0934 0.0212意思是类别0的目标中心在图片横向81%的位置、纵向54%的位置宽度占图宽9.3%高度占图高2.1%。想验证标注画得对不对最快的办法是自己写个脚本把框画回图上。import cv2 def draw_yolo_txt(img_path, txt_path, out_pathcheck.jpg): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) cx, cy, bw, bh [float(x) for x in parts[1:]] x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, str(cls_id), (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(out_path, img) print(fsaved to {out_path}, size{w}x{h}) draw_yolo_txt(images/train/0001.jpg, labels/train/0001.txt)这段脚本把归一化坐标换算回像素坐标核心就一个公式x1(cx-bw/2)*w宽高同理。画框时额外把类别id写上能顺便检查类别分布。如果画出来的框偏到警戒线外、或者框太小几乎看不见先别怪标注检查是不是把width height和center_x center_y的顺序看反了。这个错误很常见因为有些老版本YOLO是class xmin ymin xmax ymax的绝对坐标写法。2.3 VOC格式XML的bndbox、difficult与ImageSetsVOC格式用XML存标注一个图片对应一个XML。核心结构是object标签下挂name类别名和bndbox四个绝对像素坐标。和YOLO不一样这里不用归一化宽高直接是原图的像素值。另外还有个difficult字段值为1表示这个目标特别难认VOC官方建议训练时跳过。annotation filename0001.jpg/filename size width1920/width height1080/height depth3/depth /size object namewarning_line/name difficult0/difficult bndbox xmin712/xmin ymin512/ymin xmax1180/xmax ymax546/ymax /bndbox /object /annotation用Python解析XML画框也很直接import xml.etree.ElementTree as ET import cv2 def draw_voc_xml(img_path, xml_path, out_pathvoc_check.jpg): img cv2.imread(img_path) tree ET.parse(xml_path) root tree.getroot() for obj in root.findall(object): name obj.find(name).text dif obj.find(difficult) if dif is not None and dif.text 1: continue bnd obj.find(bndbox) x1 int(bnd.find(xmin).text) y1 int(bnd.find(ymin).text) x2 int(bnd.find(xmax).text) y2 int(bnd.find(ymax).text) cv2.rectangle(img, (x1, y1), (x2, y2), (255, 0, 0), 2) cv2.putText(img, name, (x1, max(30, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imwrite(out_path, img) draw_voc_xml(JPEGImages/0001.jpg, Annotations/0001.xml)这里用ET.parse读XML遍历所有object遇到difficult1直接跳过。如果XML里还有segmented之类的字段不用管YOLO训练用不到。VOC的划分清单在ImageSets/Main下的train.txt、val.txt、test.txt文件里内容只是文件名列表没有路径。如果你手里的包VOC部分没有这个目录就用YOLO的 images/labels 结构来认定划分两边以实际为准。3. 用YOLOv8在这个数据集上跑通第一次训练划分、配置与参数设定格式盘清楚了下一步就是把1125张图喂给YOLOv8。这个数据量不大属于典型的小数据集精调场景。我一般用YOLOv8n起步先跑通流程再根据结果决定要不要换大模型。整个链路就三步划分数据集、写配置文件、跑训练。3.1 把1125张按7:2:1切成train/val/test一个不会串数据的脚本划分数据集最容易犯的错是图片和标签没同步移动导致训练集里有图没标签、验证集里有标签没图。我的做法是先把所有文件路径读进内存再统一分配import os import random import shutil random.seed(42) src_images images_all src_labels labels_all out_root metro_split train_ratio, val_ratio 0.7, 0.2 # test 占 0.1 for split in [train, val, test]: os.makedirs(f{out_root}/images/{split}, exist_okTrue) os.makedirs(f{out_root}/labels/{split}, exist_okTrue) img_files [f for f in os.listdir(src_images) if f.lower().endswith((.jpg, .png, .bmp))] random.shuffle(img_files) n_train int(len(img_files) * train_ratio) n_val int(len(img_files) * val_ratio) n_test len(img_files) - n_train - n_val assignments ( [(train, f) for f in img_files[:n_train]] [(val, f) for f in img_files[n_train:n_train n_val]] [(test, f) for f in img_files[n_train n_val:]] ) for split, fname in assignments: img_src os.path.join(src_images, fname) label_stem os.path.splitext(fname)[0] label_src os.path.join(src_labels, label_stem .txt) if not os.path.exists(label_src): print(f[skip] missing label: {fname}) continue shutil.copy(img_src, os.path.join(out_root, images, split, fname)) shutil.copy(label_src, os.path.join(out_root, labels, split, label_stem .txt)) print(ftrain{n_train}, val{n_val}, test{n_test})这个脚本有几个关键点random.seed(42)固定随机种子保证每次划分结果一致复现实验时不会被随机性干扰先算好三个集合的边界再统一分配避免边循环边切导致数量不均复制图片前检查同名txt是否存在缺标签的图直接跳并打印宁可少一张也不要让训练管线中途报错。3.2 写data.yaml类别名顺序决定标签id的生死YOLOv8用yaml文件描述数据路径和类别路径用绝对路径最省心类别顺序必须和标签txt里的id一一对应。假设这个数据集只有“警戒线”一个类别写成下面这样path: /data/metro_line/metro_split train: images/train val: images/val test: images/test names: 0: warning_line注意names的顺序不是随便排的。如果标签txt里写的是0而yaml的names里 0 对应其他类别名训练照样跑验证集指标也正常但推理出来框上的名字是错的部署到业务里就等于白干。稳妥做法是先用第2章的脚本随机挑几张画框确认id再写yaml。如果包里有类别清单文件先读一遍。还有一点yaml里写中文类别名也行但部分可视化工具对中文支持不好建议用英文或拼音。3.3 训练参数怎么定细长目标别用默认一套走天下启动训练用命令行就行我常用的命令如下yolo detect train \ datametro_line.yaml \ modelyolov8n.pt \ epochs150 \ imgsz1280 \ batch8 \ lr00.005 \ patience20 \ mosaic0.5 \ close_mosaic10 \ projectruns \ namemetro_line_v1这条命令里没有一个是随手写的对警戒线这类细长目标参数的影响特别大。参数推荐值为什么这么设imgsz1280警戒线宽度在1080p图里往往只有4~8像素640输入下采样后几乎不可见显存不够先降到960再不够才用640跑通流程epochs1501125张是小数据集100轮以内容易欠拟合150轮配合patience早停够用batch8显存允许就往上加BN统计量更稳定8G卡跑不动1280就减batch别减imgszlr00.005小数据集用默认0.01偏激进loss容易飞0.005是保守但稳的起点mosaic0.5YOLOv8默认mosaic1.0但线条目标被四图拼接切碎后标签丢失严重降到0.5保留增强又减少副作用close_mosaic10最后10轮关掉拼接让模型在接近真实分布的数据上做最后一轮拟合第一轮训练不要追求指标重点看两件事loss有没有稳定下降、验证集mAP有没有在合理范围。1125张图如果loss下降很慢先查标签画框对不对如果loss掉得快但mAP上不去十有八九是目标太小把imgsz往上抬。训练日志里每个epoch都会打印 mAP50 和 mAP50-95以 mAP50-95 为准警戒线这种细长目标在COCO指标里天然吃亏别被单类别的mAP50骗了。提示如果显存实在紧YOLOv8n配imgsz640也能跑通但要想清楚——640下警戒线可能只剩2像素宽基本等于让模型盲猜。4. VOC格式不是白给的回转YOLO脚本、类别映射与跨数据集迁移很多人在YOLO项目里拿到VOC格式数据第一反应是转成YOLO txt这没错。但如果只盯着“转格式”这一步就浪费了VOC格式的价值。VOC结构里显式的类别名、绝对坐标、difficult标记在跨数据集合并、数据清洗和迁移学习时反而更好用。4.1 什么场景下应该保留VOC格式不急着转两种情况我建议先用VOC。一是要做跨数据集合并比如想把另一个站台的VOC数据并进来训练二是想把数据喂给老牌检测框架如MMDetection、Faster R-CNN配套的VOC数据集类。这两类场景里VOC的可读性和兼容性都比YOLO txt好。另外VOC的difficult字段在清洗时很实用。如果原始标注里有一些严重遮挡、只有几个像素的模糊目标标了difficult1转YOLO时可以直接跳过。如果数据没有这个字段那只能自己看图挑工作量完全不同。4.2 把VOC转回YOLOXML到txt的转换脚本与四个边界坑标题说这个数据集同时是yolo和voc格式但很多同类型数据集常常只给VOC需要自己转。即使这里两套都在也建议自己跑一遍转换因为能借机检查VOC和YOLO两套的坐标是否一致。转换脚本是固定套路import xml.etree.ElementTree as ET import os class_map {warning_line: 0} # 按names顺序对齐 def voc2yolo(xml_path, out_txt_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) with open(out_txt_path, w, encodingutf-8) as out: for obj in root.findall(object): name obj.find(name).text if name not in class_map: continue dif obj.find(difficult) if dif is not None and dif.text 1: continue bnd obj.find(bndbox) xmin int(bnd.find(xmin).text) ymin int(bnd.find(ymin).text) xmax int(bnd.find(xmax).text) ymax int(bnd.find(ymax).text) if xmax xmin or ymax ymin: continue xmin max(0, xmin) ymin max(0, ymin) xmax min(img_w, xmax) ymax min(img_h, ymax) cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h out.write(f{class_map[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n)这段脚本踩的坑都在细节里。第一个坑是类别没在class_map里时直接continue否则会写一个错误的id第二个坑是difficult1必须跳过第三个坑是坐标越界有些标注框的xmax比图片宽度还大不截断训练时会报边界错误第四个坑是如果一张图里所有目标都被过滤掉了必须保留一个空txt文件。YOLO训练要求每张图都有同名txt缺文件比空文件更严重。转换完建议做个抽样对比找10张图分别用YOLO txt画框和VOC XML画框叠在一起看差异。正常情况下两个框应该完全重合如果差了几个像素多半是归一化时用了size里的宽高和实际图片宽高不一致检查图片是否被resize过。4.3 用VOC做跨数据集融合的一个稳妥流程如果想把这个数据集和其他站台拍摄的数据合并我一般走一条中间态流程——不直接拿VOC转YOLO而是所有数据集先转成同一种中间格式用脚本把所有VOC XML读出来转成一个扁平的结构化文件比如CSV字段为image_path class_name xmin ymin xmax ymax在CSV层面做维度统一确认所有图片的宽高规格、类别名写法比如有的标warning_line有的标yellow_line这一步合并掉清洗逻辑统一处理筛掉面积过小、宽高比异常的框最后再从CSV统一生成YOLO格式txt避免不同数据集各转各的最后杂乱无章。这个流程看着多一步但在做多站点数据时能少折腾好几轮。绕开直接XML到txt核心原因是原始VOC里标注规范不可控有人用difficult、有人不用有人类别名大小写混用直接对XML写转换逻辑每个包都有不同的幺蛾子。统一到CSV后后面所有操作只跟CSV打交道心理负担小很多。5. 避坑记录1125张的警戒线数据集最容易在5个环节翻车小数据集训练是个精雕细活的活1125张图说多不多说少不少但足够把训练链路里的常见坑全部踩一遍。这些坑不是模型结构的问题全是数据和训练配置层面的每一条我都见过不止一次翻车现场。5.1 类别编号对不上loss降了mAP却一直是0现象训练日志里loss稳步下降最后一个epoch mAP50还是0甚至验证集每个batch都报“no detections”。看标签也很正常图上也画得出框。原因data.yaml里的names顺序和图里真实类别id错位。比如数据集的txt写的是0 warning_line但yaml里把0写成了其他类别。YOLO训练时不校验类别名称和标签id的对应关系模型一直按错误的id学习验证时按正确id去匹配一个都匹配不上。解决训练前写个两行脚本统计训练集标签里实际出现的id集合和yaml里的names逐一对照。我每次换数据集都会跑一遍这个检查几秒钟的事情省掉一整天的排查时间。5.2 线条目标太小imgsz640训完现场全漏检现象验证集mAP50有0.85看起来不错但放到现场1080p的画面里远处的警戒线完全检测不到近处的也断断续续。原因警戒线是超大宽高比目标在640分辨率输入下线宽只剩几个像素下采样到特征层时信息基本丢失。这是典型的细长小目标问题和模型大小没关系换yolov8x也一样漏。解决训练imgsz直接上1280显存不够就把batch降到4再不行用960。推理时如果还是怕漏采用切片推理SAHI类方案把大图切成512或640的小块分别检测再拼回结果。这个组合对细长小目标非常有效。5.3 zip解压后的图片是坏的训练到一半就EOF崩溃现象训练到第40个epoch时突然报错类似Expected image file extension ...或者cv2.error ... Broken image训练中断前面几十个epoch白跑。原因zip包在传输或复制过程中损坏某张jpg实际只有十几KB或者0KB也可能标注齐全但图片本身从源头就是坏的。这类问题随机出现前期验证集恰好没抽中它。解决写个脚本把每张图用OpenCV读一遍读不出来的统一移动到corrupted/目录并把对应的txt、xml一起移走。宁可少几十张图也不要让一颗老鼠屎毁掉整个训练。import cv2, os, shutil img_dir images_all label_dirs [labels_all] bad_dir corrupted os.makedirs(bad_dir, exist_okTrue) for fname in os.listdir(img_dir): path os.path.join(img_dir, fname) img cv2.imread(path) if img is None: shutil.move(path, os.path.join(bad_dir, fname)) stem os.path.splitext(fname)[0] for ld in label_dirs: cand os.path.join(ld, stem .txt) if os.path.exists(cand): shutil.move(cand, os.path.join(bad_dir, stem .txt)) print(fcorrupted: {fname})5.4 BN崩溃loss突然变成nan训练直接报废现象训练前几十轮正常某轮开始loss变为nan后面永远nan只能中断。有时输出里还会带“BatchNorm”相关的告警。原因小数据集配上偏大的学习率尤其lr0给到0.01以上时BN层的统计量在少量样本上剧烈抖动累积到一定程度直接发散。mosaic增强在细长目标上切出大量异常标签也会加剧这个问题。解决lr0从0.005起步别用默认0.01batch尽量提到8以上让BN统计量更稳。如果已经nan了先降低lr重启别再抱着原来的参数硬跑。小数据集上稳定比激进重要。5.5 白天夜间样本失衡验证集指标虚高夜间部署全灭现象验证集mAP50有0.87但把模型拿到夜间或黄昏的站台照片上一条线都检测不出来。翻数据后发现大部分训练图都是白天拍的。原因随机划分数据集时val里也大部分是白天图模型只要学会白天分布就能拿到高分。夜间亮度、灯光反射、地面反光的特征模型完全没见过mAP虚高只是假象。解决划分时按“白天/黄昏/夜间”分层抽。如果原始数据没有时段标注自己快速看一眼图片亮度值做个粗略分类。训练时适当加强亮度、对比度增强YOLOv8里对应hsv_h、hsv_s、hsv_v三个参数把默认的0.015、0.7、0.4稍微调大一点能补一些夜间泛化能力。提示这类数据集部署到真实站台前一定要单独准备一组不同时段、不同站台的测试图指标才会可信。6. 让1125张再多出几十次有效训练场景化拆验证集与难例回流模型训练到这里基本流程已经跑通。但如果想把1125张的价值榨干最后一步很关键——重新审视验证集的拆法以及把验证集变成挖掘难例的工具。这两件事做对了小数据集也能练出能用的模型。6.1 验证集按场景分组而不是随机打散随机划分在小数据集上是懒办法也容易埋雷。同一站台、同一时段拍的图片往往高度相似随机切分时这些相似图一半进了训练集、一半进了验证集验证指标会虚高到失真。现场换一个站台立刻现原形。更稳的做法是按站台号、拍摄时段或者摄像头机位分组划分前先把图片按这些维度归堆然后让同一堆的图整组进入train或val不要从堆里随机抽。这样验证集模拟的是“换个场景”的效果指标更有参考价值。花半小时整理分组后面省下的是现场调试的两三天。6.2 从val里挖难例把漏检样本回流到训练集验证集除了看指标还是最现成的难例来源。我常用一个小循环训练完一版模型后拿它对val集推理把置信度在0.3到0.6之间的检测结果全部导出来人工看一眼。这批结果里通常有三类情况模型框得很准但置信度低说明目标太细长导致置信度被压框的位置不对说明模型把背景当成了线完全没有框说明漏检。前两类是模型的问题通过调参数解决。第三类里如果图片上肉眼清晰可见警戒线却没框出来强烈怀疑是标注漏标了。把这类图找出来补上标签单独放进训练集再训一轮。这个“推理-挖难例-回标-再训练”的循环1125张的数据集跑两轮往往比换更大的模型提升更明显。我一般会保留每一轮的难例清单训完新模型后重新推理一遍确认上一轮的难例是否真的被治住了。这些年我拿到数据集就急着敲train命令的老毛病改了不少。现在每次新数据集到手第一件事永远是拆包、对标签、检查损坏图这几步多花半小时后面能省下好几天调参时间。尤其是细长小目标这个方向数据和标注的质量直接决定模型上限。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网