☰
地铁警戒线检测数据集:1125张双格式标注,YOLOv8训练全流程指南
2026/10/10 12:55:16 网站建设 项目流程

简介:这份数据集聚焦地铁站台警戒线检测任务,同时提供VOC与YOLO两种标准格式,适合刚开始接触目标检测、需要现成标注数据来训练“warning_line”单类别模型的计算机视觉初学者,也可用于轨道交通智能巡检相关的算法验证与方案演示。压缩包约116.34MB,内含1125张未经增强的清晰JPEG图片,并配套XML标注与TXT标签文件,标注与文本文件合计约2000个,目录区分图片、XML与TXT三个文件夹,可直接适配VOC和YOLO两种读取方式。所有警戒线目标均以矩形框精确标注,标签统一为warning_line,全数据集共计1635个标注框,标注风格一致,省去了自行整理和格式转换的麻烦,下载后即可按常规流程划分训练集与验证集。目前已有159人学习/浏览该数据集,对于需要快速搭建警戒线检测训练数据的项目而言,这份资源能显著降低数据准备门槛,也方便在此基础上部署数据增强或迁移学习实验。

1. 地铁警戒线检测数据集:1125张双格式标注,拿来就能训YOLO

地铁站台的黄色警戒线是安全防线的核心,但监控视频里它经常被遮挡、反光、磨损,用视觉模型自动检测时总差那么一点意思。这份数据集正好补上这块缺口:1125张清晰jpg,全部配好VOC格式xml和YOLO格式txt,标签统一是warning_line,一共1635个矩形框。它不像COCO那样类别杂、背景乱,而是单一目标、场景聚焦,特别适合做安全帽、越线检测这类项目的前置模型。新手可以拿它练手YOLO入门流程,老手可以直接把它的标注格式迁移到自己的地铁监控数据集上,省掉一半打标时间。整个压缩包解压后就是JPEGImages、Annotations、labels三个文件夹,一一对应,组织得很干净。

2. 数据集结构与标注格式:VOC和YOLO的坐标换算与目录对应

2.1 三层目录结构:JPEGImages、Annotations、labels如何一一对应

先看解压后的物理结构。压缩包内是三个文件夹,JPEGImages里放着1125张jpg,文件名形如sl_images157.jpg;Annotations里是1125个同名xml文件;labels里是1125个同名txt文件。文件名完全一致,只是扩展名不同,这就是VOC数据集最标准的组织方式,也是YOLO训练前最常见的原始形态。

我拿到任何数据集,做的第一件事永远是核对数量。用一段bash命令快速验证:

for d in JPEGImages Annotations labels; do echo "$d: $(ls $d | wc -l)" done

这段命令遍历三个目录,分别统计文件数。如果三个数字都是1125,说明图片、XML、TXT一一对应;如果某个目录少几个文件,后面训练时就会报“No labels found”之类的错误。我一般解压之后先跑这个,不然后面排错会浪费一晚上。

另外要确认文件名没有特殊字符。这个数据集的文件名是sl_images加数字,很安全。有的数据集带着中文、空格或者括号,在Windows上跑YOLO时路径解析特别容易翻车,这种我通常会提前重命名。

2.2 XML与TXT的坐标换算:归一化公式与手动验证脚本

VOC格式的xml里,每个目标用绝对像素坐标表示。这是Annotations目录下典型的xml片段:

<annotation> <filename>sl_images157.jpg</filename> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>warning_line</name> <bndbox> <xmin>128</xmin> <ymin>245</ymin> <xmax>512</xmax> <ymax>280</ymax> </bndbox> </object> </annotation>

而YOLO格式的txt,每行是“类别编号 中心点x 中心点y 宽度 高度”,所有值都归一化到0~1之间。这个数据集只有warning_line一个类别,所以txt里每行的第一个数字永远是0。从xml换算到YOLO的标准公式是:

  • x_center = (xmin + xmax) / 2 / width
  • y_center = (ymin + ymax) / 2 / height
  • bbox_width = (xmax - xmin) / width
  • bbox_height = (ymax - ymin) / height

可以看到,中心点坐标就是左右边界的中点除以图片宽度,框的宽高就是像素差除以图片宽高。归一化的目的是让模型训练时不依赖输入图片的实际分辨率,这也是为什么YOLO在训练时用imgsz参数统一缩放到640x640,而标注坐标仍然有效。

为了确认xml和txt记录的是同一个框,我会写一个对比脚本,手动计算坐标再和txt逐行比对:

import xml.etree.ElementTree as ET xml_path = "Annotations/sl_images157.xml" txt_path = "labels/sl_images157.txt" tree = ET.parse(xml_path) root = tree.getroot() w = int(root.find("size/width").text) h = int(root.find("size/height").text) objects = root.findall("object") print(f"XML中目标数: {len(objects)}") with open(txt_path, "r") as f: lines = [line.strip().split() for line in f.readlines()] print(f"TXT中目标数: {len(lines)}") for obj, line in zip(objects, lines): xmin = int(obj.find("bndbox/xmin").text) ymin = int(obj.find("bndbox/ymin").text) xmax = int(obj.find("bndbox/xmax").text) ymax = int(obj.find("bndbox/ymax").text) x_center = (xmin + xmax) / 2 / w y_center = (ymin + ymax) / 2 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h print(f"TXT类别 {line[0]}, 手动计算: {x_center:.4f} {y_center:.4f} {bw:.4f} {bh:.4f}") print(f"TXT实际值 : {line[1]} {line[2]} {line[3]} {line[4]}")

这段脚本先解析xml拿到图片宽高和每个目标的bbox,再按公式换算成YOLO坐标,最后和txt里存的值比较。误差在0.001以内说明标注一致;如果误差超过0.01,说明生成txt时可能用了错误的宽高,或者xml本身就被改过。注意脚本里zip会按顺序配对,如果xml和txt的目标数不一致,循环会漏掉多余的目标,所以前面先打印数量,这个细节很重要。

这个数据集的总框数是1635,平均每张图约1.45个框。地铁警戒线通常是一条贯穿画面横向的连续线段,但人工标注会把被柱子遮挡的地方拆成多个矩形框,所以一张图出现2~3个框是正常的,不必担心。理解这个特点,后续训练时就不会对框数分布感到困惑。

3. 准备YOLOv8训练:目录重组、data.yaml与训练参数说明

3.1 从VOC目录到YOLO目录:划分训练集和验证集

解压后的目录是VOC风格,但YOLOv8要求的数据布局是images和labels两个大目录,下面再分train和val。具体结构是:

datasets/ warning_line/ images/ train/ val/ labels/ train/ val/

很多新手直接把JPEGImages路径填给YOLOv8,结果训练脚本报错说找不到labels,就是因为目录结构不对。YOLO训练时是根据图片路径推断标签路径的,它会把images替换成labels,再在train或val子目录里找同名txt。所以images/train和labels/train下的文件名必须严格一致。

我一般用Python脚本完成划分,按8:2比例随机切分,同时复制图片和标签:

import os import random from shutil import copyfile img_dir = "JPEGImages" label_dir = "labels" train_img_dir = "datasets/warning_line/images/train" train_label_dir = "datasets/warning_line/labels/train" val_img_dir = "datasets/warning_line/images/val" val_label_dir = "datasets/warning_line/labels/val" for d in [train_img_dir, train_label_dir, val_img_dir, val_label_dir]: os.makedirs(d, exist_ok=True) names = [f[:-4] for f in os.listdir(img_dir) if f.endswith(".jpg")] random.seed(42) random.shuffle(names) split_idx = int(len(names) * 0.8) train_names = names[:split_idx] val_names = names[split_idx:] for name in train_names: copyfile(os.path.join(img_dir, name + ".jpg"), os.path.join(train_img_dir, name + ".jpg")) copyfile(os.path.join(label_dir, name + ".txt"), os.path.join(train_label_dir, name + ".txt")) for name in val_names: copyfile(os.path.join(img_dir, name + ".jpg"), os.path.join(val_img_dir, name + ".jpg")) copyfile(os.path.join(label_dir, name + ".txt"), os.path.join(val_label_dir, name + ".txt")) print(f"训练集: {len(train_names)}, 验证集: {len(val_names)}")

这段脚本先读取所有jpg文件名,去掉扩展名得到文件主名;然后固定随机种子42,保证每次划分结果一致,方便复现;按8:2算出切分下标,把前80%命名为train_names,后20%命名为val_names;最后复制对应的jpg和txt到目标目录。固定随机种子很重要,不然两次划分出来的数据不一样,模型对比就不公平了。

验证集比例不建议超过20%。地铁警戒线场景相对单一,1125张图用225张做验证已经足够,再多会挤占训练样本。如果以后你往里面补充数据,也尽量保持这个比例。

3.2 编写data.yaml与训练命令参数说明

数据准备好后,写一个data.yaml告诉YOLOv8数据在哪、有几个类别:

path: D:/datasets/warning_line train: images/train val: images/val nc: 1 names: ['warning_line']

注意path要填绝对路径,或者你当前工作目录下能直接访问的相对路径。train和val是相对path的路径,不要写成绝对路径。nc是类别数量,这里只有1类,所以是1;names列表第一项对应类别编号0,顺序不能换,否则训练时的类别名就错位了。

然后启动训练:

yolo detect train data=data.yaml model=yolov8n.pt epochs=100 batch=16 imgsz=640

这里几个关键参数我拆开说:

  • model=yolov8n.pt:n是nano,最轻量的版本。地铁警戒线只有1类且特征明显,不需要用yolov8x这种大模型,nano或small就够,训练速度快,部署也方便。如果你的显卡显存足够大,换yolov8s.pt精度会略高一点。
  • epochs=100:这个数据量100轮足够收敛。如果只看loss还在下降,可以加到150,但要注意过拟合,毕竟只有900张训练图。
  • batch=16:6GB显存跑yolov8n刚刚好,如果爆显存就降到8。batch太小会让训练震荡,太大容易显存不足。
  • imgsz=640:YOLOv8默认就是640。如果你的原始图片更大,可以试试1280,但警戒线本身是长条目标,对小目标检测提升不明显,640已经够用。

训练过程中会输出每轮的loss、mAP50、mAP50-95等指标。mAP50表示IoU阈值0.5下的平均精度,对这类视觉检测项目是核心指标;mAP50-95则是从0.5到0.95取多个阈值平均,更严格,也更适合衡量模型在真实场景中的泛化能力。建议盯住val/box_loss和mAP50这两栏,如果mAP50稳定在0.9以上,这个模型基本能用了。这套流程就是你用YOLOv8训练自己的数据集时最常用的一套标准操作。

如果不想用命令行,也可以直接用Python API:

from ultralytics import YOLO model = YOLO("yolov8n.pt") model.train(data="data.yaml", epochs=100, batch=16, imgsz=640)

这种方式适合需要动态切换参数的场景,比如在Jupyter里做参数扫描。命令行和API的底层逻辑完全一样,看个人习惯。我一般会在命令行跑,因为方便重定向日志,训练完直接就能看到结果。

4. 标注质量检查:框数统计、坐标校验与可视化验证

4.1 统计总框数与类别分布

拿到数据集后,先不要急着训练,花十分钟做一次标注体检。摘要里写了总框数1635,标签种类1个,这些数字要在代码里核实。用Python统计所有txt文件:

import glob txt_files = glob.glob("labels/*.txt") total_boxes = 0 class_count = {} empty_files = [] for f in txt_files: with open(f, "r") as fh: lines = [l.strip() for l in fh if l.strip()] if not lines: empty_files.append(f) total_boxes += len(lines) for line in lines: cls = int(line.split()[0]) class_count[cls] = class_count.get(cls, 0) + 1 print(f"总框数: {total_boxes}") print(f"类别分布: {class_count}") print(f"空标签文件数: {len(empty_files)}") if empty_files: print("空文件示例:", empty_files[:5])

这段脚本遍历labels目录下的所有txt,统计每个文件的非空行数,累加得到总框数;同时按类别编号统计框数。如果输出的总框数和1635一致,类别分布只有{0: 1635},说明标签文件没有漏标、没有多余类别。若空标签文件数大于0,就要注意了,训练时YOLO会自动跳过空标签的图片,但如果你用这些图做验证集,mAP会偏低。

这个脚本还有一个隐藏用处:检查标签连续性。有些数据集类别编号是0、1、3,中间跳了2,YOLOv8训练时会提示“class 2 is not in names”,但程序不会崩,只是你不知道它内部怎么处理。这里只要确认所有txt第一列都是0,就完全不会有这个问题。

4.2 坐标范围校验:防止异常框干扰训练

归一化坐标必须全部在0到1之间,超出这个范围就说明标注框越出了图片边界,或者转换脚本有bug。写一个快速校验:

import glob bad_files = [] for f in glob.glob("labels/*.txt"): with open(f, "r") as fh: for line in fh: parts = line.strip().split() if len(parts) != 5: bad_files.append((f, "字段数不为5", line.strip())) continue _, xc, yc, bw, bh = map(float, parts) if not (0 <= xc <= 1 and 0 <= yc <= 1 and 0 <= bw <= 1 and 0 <= bh <= 1): bad_files.append((f, "坐标越界", line.strip())) print(f"异常文件数: {len(bad_files)}") for item in bad_files[:10]: print(item)

这个脚本检查两个点:一是每行是否有5个字段,多了少了都说明标签格式损坏;二是中心点坐标和宽高是否都在合法范围内。对于地铁警戒线这类长条形目标,宽高比可能会比较极端,比如宽0.8、高0.05,这是正常的,不要因为某个值接近0就误判为异常。

坐标越界最常见的后果是训练时loss变成NaN,模型权重直接废掉。如果发现越界,不要手动去改txt,正确做法是回到xml重新转换,改xml里的xmin、xmax等绝对坐标,再重新生成txt,这样保证两边一致。数据集的边界框由人工标注,一般不会越界,但这个检查能帮你发现一些隐藏的角落问题。

4.3 可视化验证:把标注框画回原图

数字层面的检查通过了,还要看视觉效果。文本标注只是一串坐标,实际框是不是贴合警戒线,必须画出来看。用OpenCV把txt里的归一化坐标换算回像素坐标,画在原图上:

import cv2 img = cv2.imread("JPEGImages/sl_images157.jpg") h, w = img.shape[:2] with open("labels/sl_images157.txt", "r") as f: lines = f.readlines() for line in lines: cls, xc, yc, bw, bh = map(float, line.split()) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, "warning_line", (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 1) cv2.imwrite("check_sl_images157.jpg", img) print("保存到 check_sl_images157.jpg")

这段脚本把YOLO的归一化中心坐标换算成左上角和右下角的像素坐标:x1=xc-bw/2,x2=xc+bw/2,再乘以图片宽w;y方向同理,最后乘以高h。换算时注意先乘后取整,避免精度丢失。画框用的是OpenCV的rectangle,putText顺便把类别名标在框上方。

建议随机抽20张图批量执行,然后拼成一张大图网格,一眼就能看出有没有框错位、漏标、多标的情况。地铁警戒线场景有个典型问题:警戒线在站台边缘会反光,有些标注员会把反光的亮条也框进去,导致训练时模型学到错误特征。可视化检查能帮你发现这类噪声,如果标注确实有问题,可以单独剔除这些图片,或者联系标注方重新修正。这个数据集标注质量相对干净,但每次训练前我仍然会把可视化检查跑一遍,养成习惯可以有效避免后期返工。

5. 避坑指南:训练地铁警戒线模型常见的5个问题

训练目标检测模型,最怕的不是模型结构选错,而是标注数据里的暗坑。这个数据集只有1类,看起来简单,但正因为简单,很多问题反而被忽略。我把踩过的坑按“现象→原因→解决”写在这里,每一条都是真实翻过车的记录。

5.1 训练后mAP50始终为0

现象:训练了100轮,日志里box_loss在下降,但mAP50一直显示0,验证集上预测框全部是空的。

原因:标签文件里的类别编号不是从0开始的。有人拿别的数据集拼接时,把warning_line的txt首列写成了1,而data.yaml里nc=1、names=[‘warning_line’],YOLOv8会把编号0当成唯一类别,编号1的样本全部算作背景,一个正样本都匹配不上。

解决:先跑第4章的统计脚本,确认空标签以外的txt首列全是0。如果混入了非0编号,写一个小脚本批量替换,或者从xml重新生成txt,不要手动改,容易改错。

5.2 训练时提示No labels found

现象:启动训练后,终端输出类似“train: No labels found in D:/datasets/warning_line/labels/train”,然后直接退出。

原因:数据集目录里只有images,没有labels,或者labels目录层级不对。很多人把VOC原生的Annotations当成标签目录填进去了,但YOLO需要的是txt,不是xml。

解决:确认labels目录下是txt文件,并且文件名与images目录下的jpg一一对应。还有可能你的路径大小写不对,在Windows上必须要分清大小写。

5.3 验证时预测框偏移半个身位

现象:训练起来后loss正常,但在验证集上画出的预测框总偏在警戒线上方或下方,看起来错位。

原因:训练时用了数据增强,但只增强了图片没增强标签。这个数据集自身的图片没有增强,但如果你在代码里加了自定义增强,比如随机裁剪、透视变换,而标签坐标没有同步变换,就会出现这种偏移。

解决:使用albumentations的同步变换,或者直接用YOLO内置的增强参数,比如hsv_h、hsv_s、degrees、translate,这些参数会自动同步应用于标签框,不用自己操心。

5.4 训练时loss变成NaN

现象:前几轮loss正常,到了某一轮突然变成NaN,精度也随之崩溃。

原因:坐标越界导致的。某个txt里出现了大于1的归一化坐标,或者宽度、高度为0,模型在回归时计算出的IoU为负,最终导致梯度爆炸。这个数据集本身比较干净,但如果你自己补充标注数据,很容易出现这种问题。

解决:用第4章的坐标范围校验脚本扫描所有标签,把越界行剔除或重新生成。另外,如果图片中有完全遮挡的目标,标注员可能会把框缩到一个点,这类数据直接删掉就好,不要留在训练集中。

5.5 训练时类别数不对

现象:训练模型时日志显示“nc”不是1,而是2或更多,导致warning_line的类别索引错乱。

原因:data.yaml中names列表写错了,或者yaml文件语法有问题,YOLO解析出了多余的类别。还有可能是txt里出现了其他类别编号,比如0和1混杂。

解决:先用4.1的统计脚本确认类别分布只有0,再检查data.yaml的缩进。YAML对缩进敏感,names列表必须用“-”开头,而且nc必须写1。如果names写成了“[‘warning_line’, ‘something’]”,nc还是1,训练就会报类别不匹配的错误,这种属于典型的低级坑,却特别容易犯。

5.6 一条快速自检清单

把上面这些坑汇总成一份自查顺序:先跑ls统计三个目录文件数量,确保都是1125;再跑第4章的统计脚本,确认总框数1635、类别只有0;然后跑坐标范围校验,确认没有越界;最后随机抽20张图可视化,确认框贴合。这套流程走下来大概十分钟,但能避免后面几十个小时的无效训练。我现在的习惯是,无论数据集是下载的还是别人给我的,都强制先走一遍,再往data.yaml里填路径,一次都没再翻过车。

6. 导出ONNX模型:实测推理与后续优化

训练完成后,best.pt保存在runs/detect/train/weights/下。把它导出成ONNX,就可以脱离PyTorch环境,在CPU甚至边缘设备上跑推理。

6.1 导出ONNX

yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640 opset=12

导出时会自动做模型简化,输出best.onnx文件。opset=12是兼容性比较好的版本,老一点的推理框架也能识别。

6.2 用ONNXRuntime做一次推理

import cv2 import numpy as np import onnxruntime as ort sess = ort.InferenceSession("best.onnx") img = cv2.imread("sl_images157.jpg") img_resized = cv2.resize(img, (640, 640)) blob = img_resized[:, :, ::-1].transpose(2, 0, 1)[None].astype(np.float32) / 255.0 outputs = sess.run(None, {sess.get_inputs()[0].name: blob}) print(outputs[0].shape)

这个示例展示的是最朴素的预处理流程:缩放到640,把BGR转成RGB,调成CHW顺序,归一化到0~1。ONNXRuntime的输出通常是1x84x8400的矩阵,其中8400是YOLOv8在不同尺度下的候选框数量,84表示4个坐标加80个类别分数,因为导出时把类别数扩展到了COCO的80类。实际使用时需要截取前5个值,再按置信度过滤。如果你只关心warning_line这一类的检测结果,后续代码还要做NMS去重,但这些都不是这份数据集本身要解决的,属于部署细节。

6.3 后续优化建议

地铁站台光照变化剧烈,白天有阳光直射,晚上只有冷光灯,建议在原图上做亮度、对比度增强后再扩充一轮数据。还可以补充雨天、玻璃反光、客流遮挡等极端帧,让模型在恶劣条件下不至于失效。如果现场摄像头角度固定,加上轨道边缘的位置先验做后处理过滤,误检率还会进一步下降。

从那以后我每次拿到数据集,都会先跑一遍第4章的统计脚本,确认框数和类别没问题,才开始训练。这个习惯帮我躲过了很多莫名其妙的训练事故,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询