简介:本资源为地铁警戒线检测数据集,面向从事目标检测算法训练与验证的开发者、学生及研究人员,可用于地铁站台安全监控场景下的警戒线识别模型训练与测试。压缩包共2000个文件,包含1125张jpg图片、1125个VOC格式xml标注文件及875个YOLO格式txt标签文件,整体约116.34MB,图片清晰且未做数据增强,标注均为矩形框,标签种类仅warning_line一类,总框数达1635个,可直接用于YOLO与VOC双格式的模型训练流程。目前已有159人学习下载,适合需要快速搭建地铁警戒线检测实验、验证算法效果或进行课程设计、毕业设计的读者参考使用。数据集仅提供准确合理的标注,不对训练所得模型或权重文件的精度作任何保证,使用者可结合自身需求进行划分与调优。
1. 地铁警戒线检测数据集:1125 张 yolo+voc 双格式到底能跑出什么
地铁站台最怕的不是人多,而是有人越过那条黄线。做视觉检测的同行都清楚,警戒线本身是个细长目标,跟轨道、地砖缝、盲道纹理高度相似,模型很容易把一条地砖缝当成警戒线报警,或者把被行人遮挡半截的警戒线漏掉。这个数据集标题里写得很直白:1125 张、yolo 和 voc 双格式,指向的就是「站台越线检测」这个具体任务。它解决的是从零标注成本高、格式不统一、样本量不够这三件事。适合谁?适合手上已经有 YOLO 训练流程、想快速验证警戒线检测可行性的工程师,也适合做轨道交通安防 Demo 的学生。1125 张不算大,但足够跑通一个 baseline,关键是格式已经给你转好了,省掉最烦的那一步。
2. 先搞清楚 1125 张里有什么:类别、分布与格式选型
拿到一个检测数据集,第一件事不是急着训练,而是先摸清它的底细。1125 张这个量级,如果类别单一、场景集中,能训出可用的模型;如果场景跨度大、标注质量参差,那就要先做清洗。地铁警戒线检测的难点在于目标形态:它是一条线,不是一个块,标注框往往是扁长的矩形,长宽比可能到 10:1 甚至更极端。这种目标对 anchor 设计和 NMS 都不友好,所以先看数据再定策略。
2.1 警戒线检测的类别定义与标注形态
常见做法是把类别定成单类,比如warning_line或者yellow_line,因为任务本质是「有没有越线」,不需要区分线的颜色或材质。标注形态上,警戒线通常用矩形框把整条可见线段框住,被行人遮挡的部分一般不单独标,而是框住可见部分。这里有个坑:如果标注时把被遮挡的线段也强行框进去,框里会混入大量行人像素,模型学到的特征就脏了。我一般会先抽查 30 到 50 张,看标注框是不是紧贴线段边缘,有没有把地砖缝误标进去。
判断标注质量有个简单办法:统计所有框的长宽比分布。警戒线的长宽比中位数应该在 5 以上,如果出现大量接近 1:1 的框,大概率是误标或者标成了别的目标。可以用下面这段脚本快速统计。
import os import xml.etree.ElementTree as ET # 统计 VOC 格式标注框的长宽比分布 def stat_voc_ratio(anno_dir): ratios = [] for f in os.listdir(anno_dir): if not f.endswith('.xml'): continue tree = ET.parse(os.path.join(anno_dir, f)) for obj in tree.findall('object'): bbox = obj.find('bndbox') w = float(bbox.find('xmax').text) - float(bbox.find('xmin').text) h = float(bbox.find('ymax').text) - float(bbox.find('ymin').text) if h > 0: ratios.append(w / h) ratios.sort() n = len(ratios) print(f"总框数: {n}") print(f"长宽比中位数: {ratios[n//2]:.2f}") print(f"长宽比 < 2 的框占比: {sum(1 for r in ratios if r < 2)/n:.2%}") stat_voc_ratio('./annotations')这段代码遍历 VOC 的 XML 文件,提取每个目标框的宽高,算出长宽比。中位数低于 3 就说明标注可能有问题,需要人工复查。< 2的占比超过 20% 基本可以判定有大量误标,得回去清洗。
2.2 YOLO 与 VOC 双格式的差异与选用
VOC 格式是每张图对应一个 XML,里面存绝对坐标;YOLO 格式是每张图对应一个 txt,存归一化的中心点和宽高。两者信息等价,但训练时用哪个取决于你的框架。Ultralytics 系的 YOLO 直接吃 txt,省掉转换;如果要用 Detectron2 或者 MMDetection,VOC 或 COCO 更顺手。这个数据集两个都给了,说明作者考虑到了不同框架的用户。
选型建议:如果你用 YOLOv5/v8/v11,直接用 txt 格式,别转来转去,转换过程容易出坐标越界、类别索引错位的问题。如果你要做对比实验,比如同时跑 YOLO 和 Faster R-CNN,那就用 VOC 格式统一转 COCO,因为 COCO 的生态工具最全。这里有个细节:YOLO 的类别索引从 0 开始,VOC 的类别名是字符串,转换时一定要维护一张映射表,否则会出现「训练时 loss 正常下降,推理时全标成背景」的玄学问题。
| 格式 | 单图标注文件 | 坐标类型 | 适用框架 |
|---|---|---|---|
| VOC | XML | 绝对坐标 | MMDetection、Detectron2 |
| YOLO | TXT | 归一化中心点+宽高 | Ultralytics YOLO 系列 |
2.3 1125 张的划分策略与最小验证集
1125 张怎么分?常见做法是 8:1:1,即 900 训练、112 验证、113 测试。但警戒线检测有个特殊性:同一段站台连续拍摄的帧之间高度相似,如果随机划分,训练集和验证集里会出现几乎一样的图,验证指标虚高。正确做法是按视频片段或拍摄时段划分,保证验证集里的场景在训练集里没出现过。我一般会先按文件名前缀分组,同组只进一个集合。
如果数据里没有明显的分组信息,退而求其次:按时间戳或拍摄角度做分层抽样。验证集至少要有 100 张,否则 mAP 波动会很大,今天 0.85 明天 0.78,你都不知道是模型改了有用还是随机种子在作祟。测试集可以留到最后再动,训练过程中只看验证集。
提示:划分完先跑一遍「空标注检查」,确认每张图的 txt 或 xml 里至少有一个目标框,空标注图会让训练时的正样本比例失真。
3. 用 YOLO 格式跑通第一个 baseline:配置、训练与指标解读
数据摸清楚了,接下来就是跑通。这一章按「改配置 → 启动训练 → 看指标 → 调参」的顺序走,每一步都给可抄的命令和参数说明。目标不是刷到最高 mAP,而是先得到一个能用的模型,知道瓶颈在哪。
3.1 数据配置文件与类别映射的写法
YOLO 训练需要一个 yaml 描述数据路径和类别。假设目录结构是images/train、images/val、labels/train、labels/val,配置文件这样写:
# data_warning_line.yaml path: ./dataset # 数据集根目录 train: images/train # 训练图相对路径 val: images/val # 验证图相对路径 nc: 1 # 类别数,警戒线单类 names: 0: warning_line # 类别名,必须和标注时的索引对应path是根目录,train和val是相对路径,YOLO 会自动去对应的labels目录找同名 txt。nc和names必须和标注文件里的类别索引严格一致,索引从 0 开始。如果标注时用了 1 作为警戒线索引,这里就要改成nc: 2并且把 0 留给背景或者空类,否则训练时所有框都会被当成背景,loss 卡在 0.7 下不去。
改完配置先做一次 dry run,用极小的 epoch 确认数据能读进去:
yolo detect train data=data_warning_line.yaml model=yolov8n.pt epochs=1 imgsz=640 batch=8这条命令用 YOLOv8n 跑 1 个 epoch,目的是验证数据管道通不通。如果报「No labels found」,八成是路径写错或者 txt 文件名和图片名不一致。跑通后再换正式参数。
3.2 训练命令与关键超参:imgsz、batch、anchor
正式训练我一般从 YOLOv8s 起步,n 太小欠拟合,l 以上在 1125 张上容易过拟合。命令如下:
yolo detect train \ data=data_warning_line.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ mosaic=1.0 \ close_mosaic=20 \ patience=30 \ project=runs/warning_line \ name=exp1逐个说参数。imgsz=640是输入分辨率,警戒线是细长目标,分辨率太低线会糊成一片,640 是底线,有条件上 960 或 1280,但显存要够。batch=16在 8G 显存上跑 640 分辨率差不多是上限,显存不够就降到 8,但 batch 太小 BN 统计不稳,可以开accumulate补偿。lr0=0.01是初始学习率,lrf=0.01是最终学习率系数,余弦退火到 0.0001。mosaic=1.0是马赛克增强,对细长目标有帮助,但最后 20 个 epoch 要关掉(close_mosaic=20),让模型在真实分布上收敛。patience=30是早停,验证指标 30 轮不升就停,省时间。
anchor 方面,YOLOv8 是 anchor-free 的,不用手动设。如果你用的是 YOLOv5,需要重新聚类 anchor,因为默认 anchor 是 COCO 的,长宽比偏正方形,对警戒线不友好。聚类命令:
python utils/autoanchor.py --data data_warning_line.yaml --img-size 640它会输出一组新的 anchor 尺寸,替换到配置文件里。不聚类也能跑,但召回率会低几个点。
3.3 看指标:mAP50 够不够,召回率为什么比精度重要
训练完看results.csv,重点看三个指标:mAP50、mAP50-95、recall。警戒线检测里,recall 比 precision 重要,因为漏检一条越线可能出安全事故,误检一条顶多让安保多看两眼。mAP50 到 0.85 以上算可用,0.9 以上算不错。如果 mAP50 高但 recall 低,说明模型保守,可以调低置信度阈值。
验证命令:
yolo detect val model=runs/warning_line/exp1/weights/best.pt data=data_warning_line.yaml conf=0.25 iou=0.5conf=0.25是置信度阈值,iou=0.5是 NMS 的 IoU 阈值。警戒线框扁长,NMS 时 IoU 容易算低,导致同一条线出多个框。如果发现重复框多,把iou调到 0.3 到 0.4,让 NMS 更激进。反过来如果漏检多,调高iou到 0.6。
推理单张图看效果:
from ultralytics import YOLO model = YOLO('runs/warning_line/exp1/weights/best.pt') results = model.predict('test_station.jpg', conf=0.3, iou=0.4, imgsz=960) for r in results: for box in r.boxes: print(box.xyxy, box.conf, box.cls)imgsz=960推理时比训练分辨率高,能提升细长目标的召回,代价是速度慢。conf=0.3比验证时略高,减少误报。打印出的xyxy是绝对坐标,conf是置信度,cls是类别索引。
4. 从 VOC 转 YOLO 再到 COCO:转换脚本与四个边界坑
虽然数据集号称双格式,但实际用的时候经常需要自己再转一道,比如从 VOC 转 YOLO 做数据增强,或者转 COCO 喂给 MMDetection。转换本身不难,坑都在边界情况上。这一章给一套可复用的转换脚本,再把四个最常见的翻车点讲清楚。
4.1 VOC 转 YOLO 的完整脚本与坐标归一化
import os import xml.etree.ElementTree as ET # VOC 转 YOLO:绝对坐标 -> 归一化中心点+宽高 def voc_to_yolo(xml_dir, img_dir, out_dir, class_map): os.makedirs(out_dir, exist_ok=True) for f in os.listdir(xml_dir): if not f.endswith('.xml'): continue tree = ET.parse(os.path.join(xml_dir, f)) root = tree.getroot() # 从 XML 里读图片宽高,不要用 PIL 再读一遍,避免尺寸不一致 size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text) lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in class_map: continue cls_id = class_map[name] bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) # 裁剪到图像边界内,防止越界 xmin = max(0, min(xmin, img_w)) xmax = max(0, min(xmax, img_w)) ymin = max(0, min(ymin, img_h)) ymax = max(0, min(ymax, img_h)) if xmax <= xmin or ymax <= ymin: continue # 跳过无效框 cx = (xmin + xmax) / 2 / img_w cy = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out_name = os.path.splitext(f)[0] + '.txt' with open(os.path.join(out_dir, out_name), 'w') as fw: fw.write('\n'.join(lines)) voc_to_yolo('./annotations', './images', './labels', {'warning_line': 0})逻辑说明:先读 XML 里的size拿图片宽高,不要用 PIL 重新读图,因为有些数据集图片被压缩过,实际尺寸和 XML 记录的不一致,用错尺寸会导致归一化坐标偏移。然后对每个框做边界裁剪,防止标注时手抖写出负坐标或超出宽高的值。最后归一化到 0 到 1 之间,保留 6 位小数足够。class_map是类别名到索引的映射,不在映射里的类别直接跳过,避免引入无关目标。
4.2 转换后必做的三项校验
转完不能直接用,先校验。第一项:坐标范围检查,所有值必须在 0 到 1 之间,出现负数或大于 1 说明裁剪逻辑有漏。第二项:框面积检查,归一化后的w*h如果小于 0.0001,说明框太小,可能是标注噪声,训练时会被忽略但会干扰 anchor 统计。第三项:图片与标签一一对应,图片有但标签没有的,要么是负样本要么是漏标,负样本可以留,漏标要补。
import os # 校验 YOLO 标签:坐标范围、框面积、文件对应 def validate_yolo(img_dir, label_dir): img_names = {os.path.splitext(f)[0] for f in os.listdir(img_dir)} label_names = {os.path.splitext(f)[0] for f in os.listdir(label_dir)} print(f"图片数: {len(img_names)}, 标签数: {len(label_names)}") print(f"有图无标签: {len(img_names - label_names)}") print(f"有标签无图: {len(label_names - img_names)}") bad = 0 for f in os.listdir(label_dir): if not f.endswith('.txt'): continue with open(os.path.join(label_dir, f)) as fr: for line in fr: parts = line.strip().split() if len(parts) != 5: bad += 1 continue _, cx, cy, w, h = map(float, parts) if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 < w <= 1 and 0 < h <= 1): bad += 1 elif w * h < 0.0001: bad += 1 print(f"异常标注行数: {bad}") validate_yolo('./images', './labels')这段脚本输出图片和标签的数量差,以及异常标注行数。有图无标签的数量如果超过总数的 5%,要确认是不是漏标;异常行数超过 1% 就要回去清洗。
4.3 转 COCO 时最容易错的两个字段
如果要用 MMDetection,得转 COCO。COCO 的 JSON 结构里,images、annotations、categories三个字段必须对齐。最容易错的是image_id和category_id。image_id必须是整数且唯一,不能用文件名;category_id必须从 1 开始,0 是保留给背景的。很多人转完发现训练时类别全乱,就是category_id从 0 开始了。
另一个坑是bbox格式。COCO 的bbox是[x, y, width, height],绝对坐标,不是中心点。从 YOLO 转 COCO 时要先反归一化再算宽高,顺序错了框会整体偏移。转换脚本网上很多,但建议自己写一遍,把每个字段的含义搞清楚,比调库靠谱。
5. 避坑与排查:警戒线检测里那些让你怀疑人生的瞬间
这一章全是血泪经验。警戒线检测看着简单,实际跑起来问题一堆,而且很多问题不是模型的问题,是数据和流程的问题。下面五条是我踩过的坑,按「现象 → 原因 → 解决」写,你对号入座。
5.1 训练 loss 正常下降但验证 mAP 一直是 0
现象:训练时 box_loss 和 cls_loss 都在降,但验证集 mAP 始终是 0,推理出来全是背景。原因:类别索引错位。标注文件里的类别索引和 yaml 里的names对不上,模型学到的正样本其实是背景。解决:打开一个标注 txt,看第一列的数字,和 yaml 里的names键对比。如果 txt 里是 1 而 yaml 里只有 0,要么改 txt 要么改 yaml。改完重新训练,别在旧权重上继续。
5.2 同一条警戒线被框出三四个重叠框
现象:推理图上一条线有好几个框叠在一起,置信度还都不低。原因:NMS 的 IoU 阈值太高,扁长框之间的 IoU 本来就低,0.5 的阈值压不住。解决:推理时把iou降到 0.3 到 0.4,或者在训练时加overlap_mask相关的后处理。如果还不行,检查标注里是不是同一条线被标了多次,这种重复标注会让模型学到重复输出。
5.3 模型把地砖缝和盲道识别成警戒线
现象:没有警戒线的区域频繁误报,尤其是地砖接缝和盲道。原因:负样本不足。1125 张里如果大部分图都有警戒线,模型没见过「没有警戒线」的场景,就会把相似纹理当成目标。解决:加入负样本图,即画面里没有警戒线的站台图,标注为空 txt。负样本比例控制在 10% 到 20%。另外可以在数据增强里加随机裁剪,让模型看到更多局部纹理。
5.4 换了个站台场景,模型直接失效
现象:在验证集上 mAP 0.9,换一段站台视频跑,漏检一半以上。原因:数据分布不匹配。训练集里的光照、角度、地砖颜色和实际场景差异大,模型过拟合到了训练场景。解决:做场景分层,训练集里尽量覆盖不同光照(白天、夜晚、灯光)、不同角度(正对、斜视)、不同站台材质。如果实在没有,用颜色抖动和亮度变换做增强,但效果有限,最好还是补数据。
5.5 训练到一半显存爆了
现象:前几十个 epoch 正常,突然报 CUDA out of memory。原因:mosaic增强在后期关闭时,图片尺寸变化导致显存峰值波动,或者验证时 batch 太大。解决:把batch降一档,或者开amp混合精度训练。另外close_mosaic的轮数不要设得太晚,留出足够的显存余量。如果还爆,检查是不是有图片分辨率异常大,比如混进了 4K 图,统一 resize 到训练分辨率。
6. 把 1125 张用到极致:小数据集上的增强、蒸馏与部署前验证
1125 张在检测任务里算小数据集,想榨出更高性能,光靠调参不够,得在数据增强和模型策略上做文章。这一章讲三个进阶方向:针对细长目标的增强、用大模型蒸馏小模型、以及部署前的量化验证。最后说一个我自己的习惯。
6.1 针对细长目标的增强策略
通用增强里,mosaic和mixup对警戒线有帮助,但random_perspective要慎用,透视变换会把直线变斜,而实际场景里警戒线基本是水平或接近水平的,过度透视会让模型学到错误的几何先验。我一般把degrees设为 0 到 5,perspective设为 0。另外可以加一个自定义增强:随机遮挡线段的中段,模拟行人遮挡,提升模型对部分可见目标的召回。
import random import cv2 import numpy as np # 自定义增强:随机遮挡警戒线中段,模拟行人遮挡 def random_occlude(image, boxes, occlude_ratio=0.3): h, w = image.shape[:2] for box in boxes: x1, y1, x2, y2 = map(int, box[:4]) # 只处理扁长框 if (x2 - x1) / max(1, (y2 - y1)) < 3: continue seg_w = int((x2 - x1) * occlude_ratio) if seg_w < 5: continue ox = random.randint(x1, max(x1, x2 - seg_w)) # 用周围像素填充遮挡区域,模拟行人 patch = image[y1:y2, ox:ox+seg_w].copy() patch = cv2.GaussianBlur(patch, (15, 15), 0) image[y1:y2, ox:ox+seg_w] = patch return image这个函数对每个扁长框随机选一段做高斯模糊,模拟行人经过时的遮挡效果。occlude_ratio控制遮挡比例,0.3 表示遮住 30% 的长度。注意只对长宽比大于 3 的框做,避免影响其他目标。增强后的图再送进训练管道,能明显提升遮挡场景下的召回。
6.2 用大模型蒸馏小模型的实操路径
如果部署端算力有限,必须用小模型,可以用大模型蒸馏。做法是先用 YOLOv8l 在 1125 张上训一个教师模型,然后用它的输出软标签指导 YOLOv8n 学生模型。蒸馏损失是学生输出和教师输出的 KL 散度,加上原本的检测损失。关键参数是温度系数T,一般设 2 到 4,T越大软标签越平滑,学生学到的类别间关系越多。蒸馏能让小模型 mAP 提升 2 到 5 个点,代价是训练时间翻倍。
6.3 部署前的量化与阈值标定
模型训完别急着上线,先做量化验证。用 ONNX 导出后做 INT8 量化,看 mAP 掉多少。警戒线检测对量化比较敏感,因为细长目标的特征响应弱,量化误差容易把它抹掉。如果掉超过 3 个点,就退回 FP16。阈值标定要在实际场景的视频上做,取一段有越线和无越线的视频,画 precision-recall 曲线,选 recall 0.95 对应的 conf 阈值,宁可误报不可漏报。
| 量化方式 | 模型大小 | 推理速度 | mAP 变化 |
|---|---|---|---|
| FP32 | 基准 | 基准 | 基准 |
| FP16 | 约 50% | 提升 30% | 掉 0.5 以内 |
| INT8 | 约 25% | 提升 60% | 掉 1 到 3 |
部署前还有一个必做项:用实际站台的监控视频跑一遍,统计误报率和漏报率。实验室指标再好,现场光照一变就可能翻车。我自己的习惯是,任何检测模型上线前,至少用三段不同时段的视频各跑 10 分钟,人工核对每一帧的报警,把误报和漏报的 case 截下来,回头补进训练集。这个循环做两三轮,模型才敢真正用起来。希望帮到你。
本文还有配套的精品资源,点击获取