简介:面向地铁场景目标检测任务,这套数据集以警戒线(warning_line)为唯一检测类别,提供清晰现场图片和1635个矩形标注框,覆盖真实环境下的地铁警戒线形态,适合作为YOLO、Faster R-CNN等模型训练与算法验证的基准数据。包内划分JPEGImages、Annotations、labels三个文件夹,分别存放图片、VOC格式xml标注和YOLO格式txt标注;压缩包共2000个文件,以xml和txt标注文件为主,整体约116.34MB,解压后即可接入常用检测框架。所有图片为清晰原图,未做增强,标签采用矩形框并保证标注合理准确,便于使用者直接开展训练、评测或数据清洗。已有158人浏览学习,适合目标检测初学者练习标注格式转换,也适合工程人员快速搭建地铁场景警戒线检测实验。
1. 地铁警戒线检测数据集到底解决什么问题:一张图里的线,比一箱图里的物体更难标
地铁站台的黄黑警戒线,看起来是画面里一条简单线条,真丢给目标检测模型却是个麻烦目标:细、长、横贯画面、贴在人群脚边,背景稍微乱一点就容易漏。要做这类视觉检测,第一步就是数据,而“数据集地铁警戒线检测数据集yolo+voc格式1125张.zip”这类资源解决的正是这一步——1125张真实站台图片,每张图同时带YOLO的txt标签和VOC的XML标签,解压出来就能直接进训练管线。适合谁?做站台越线告警、列车进站安全辅助、轨道交通安全项目的人,或者刚拿到一批细长小目标数据想练手的研究者。下面按我拆这类包的习惯,把格式、训练、转换和踩坑一条条讲清楚。
2. 读懂YOLO与VOC两套标注格式:解压检查、坐标换算与差异对比
拿到这个zip,第一件事不是急着写训练脚本,而是把压缩包当成陌生代码库一样盘一遍:看看目录怎么组织、标签里都有哪些类别、坐标是怎么存的。因为后面所有训练和转换脚本都是建立在这套结构上的,结构看错了,后面全是白忙。
2.1 先解压,再把目录结构整个盘一遍
我一般会在干净目录里解压,文件名带中文也不影响。解压后先用 find 统计图片和标签数量,1125张图,正常应该看到 1125 张图片文件和对应数量的标注文件。数量对不上,说明包里有缺图或缺标签,得先处理再开工。
mkdir -p metro_line && cd metro_line unzip ../数据集地铁警戒线检测数据集yolo+voc格式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 既然标了 yolo+voc 双格式,大概率两套都在,解压后先确认好再往下走,别拿到哪种就当哪种用。
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_path="check.jpg"): img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path, "r", encoding="utf-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(f"saved 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与ImageSets
VOC格式用XML存标注,一个图片对应一个XML。核心结构是<object>标签下挂<name>类别名和<bndbox>四个绝对像素坐标。和YOLO不一样,这里不用归一化,宽高直接是原图的像素值。另外还有个<difficult>字段,值为1表示这个目标特别难认,VOC官方建议训练时跳过。
<annotation> <filename>0001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>warning_line</name> <difficult>0</difficult> <bndbox> <xmin>712</xmin> <ymin>512</ymin> <xmax>1180</xmax> <ymax>546</ymax> </bndbox> </object> </annotation>用Python解析XML画框也很直接:
import xml.etree.ElementTree as ET import cv2 def draw_voc_xml(img_path, xml_path, out_path="voc_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,遇到difficult=1直接跳过。如果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_ok=True) os.makedirs(f"{out_root}/labels/{split}", exist_ok=True) 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(f"train={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 \ data=metro_line.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=1280 \ batch=8 \ lr0=0.005 \ patience=20 \ mosaic=0.5 \ close_mosaic=10 \ project=runs \ name=metro_line_v1这条命令里没有一个是随手写的,对警戒线这类细长目标,参数的影响特别大。
| 参数 | 推荐值 | 为什么这么设 |
|---|---|---|
| imgsz | 1280 | 警戒线宽度在1080p图里往往只有4~8像素,640输入下采样后几乎不可见;显存不够先降到960,再不够才用640跑通流程 |
| epochs | 150 | 1125张是小数据集,100轮以内容易欠拟合,150轮配合patience早停够用 |
| batch | 8 | 显存允许就往上加,BN统计量更稳定;8G卡跑不动1280就减batch,别减imgsz |
| lr0 | 0.005 | 小数据集用默认0.01偏激进,loss容易飞;0.005是保守但稳的起点 |
| mosaic | 0.5 | YOLOv8默认mosaic=1.0,但线条目标被四图拼接切碎后标签丢失严重,降到0.5保留增强又减少副作用 |
| close_mosaic | 10 | 最后10轮关掉拼接,让模型在接近真实分布的数据上做最后一轮拟合 |
第一轮训练不要追求指标,重点看两件事:loss有没有稳定下降、验证集mAP有没有在合理范围。1125张图,如果loss下降很慢,先查标签画框对不对;如果loss掉得快但mAP上不去,十有八九是目标太小,把imgsz往上抬。训练日志里每个epoch都会打印 mAP50 和 mAP50-95,以 mAP50-95 为准,警戒线这种细长目标在COCO指标里天然吃亏,别被单类别的mAP50骗了。
提示:如果显存实在紧,YOLOv8n配imgsz=640也能跑通,但要想清楚——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字段在清洗时很实用。如果原始标注里有一些严重遮挡、只有几个像素的模糊目标,标了difficult=1,转YOLO时可以直接跳过。如果数据没有这个字段,那只能自己看图挑,工作量完全不同。
4.2 把VOC转回YOLO:XML到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", encoding="utf-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;第二个坑是difficult=1必须跳过;第三个坑是坐标越界,有些标注框的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 线条目标太小:imgsz=640训完,现场全漏检
现象:验证集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_ok=True) 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(f"corrupted: {fname}")5.4 BN崩溃:loss突然变成nan,训练直接报废
现象:训练前几十轮正常,某轮开始loss变为nan,后面永远nan,只能中断。有时输出里还会带“BatchNorm”相关的告警。
原因:小数据集配上偏大的学习率,尤其lr0给到0.01以上时,BN层的统计量在少量样本上剧烈抖动,累积到一定程度直接发散。mosaic增强在细长目标上切出大量异常标签,也会加剧这个问题。
解决:lr0从0.005起步,别用默认0.01;batch尽量提到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命令的老毛病改了不少。现在每次新数据集到手,第一件事永远是拆包、对标签、检查损坏图,这几步多花半小时,后面能省下好几天调参时间。尤其是细长小目标这个方向,数据和标注的质量直接决定模型上限。希望帮到你。
本文还有配套的精品资源,点击获取