简介:面向翻越栏杆/围栏检测场景的专用目标检测数据集,由真实场景图片组成,共1680张JPG照片,每张均配套Pascal VOC XML与YOLO TXT两种主流标注格式,可直接替换进现有YOLO、Faster R-CNN、SSD等检测训练流程,适用于计算机视觉入门者、安防巡检算法工程师以及需要异常行为样本的团队。压缩包以zip格式分发,整体约100.25MB,共2000个文件,内含图片、xml标注、txt标注和说明文档,无需额外转换即可使用。目前已有590人次学习下载。数据集中共标注两类目标:climbing(翻越/攀爬动作)与person(行人),总共2454个矩形框,其中climbing框2060个、person框394个;标注均通过labelImg人工绘制,规则统一细致,并加入了部分增强图片,有助于提升模型在复杂环境下的泛化能力,支撑翻越围栏识别、行人行为分析等具体落地任务。
1. 为什么说“翻越栏杆”数据集比通用检测数据集更考验训练策略
第一次看到这份1680张的翻越围栏/栏杆数据集时,我最先注意到的不是标注格式,而是类别分布:climbing有2060个框,person只有394个框,比例超过5:1。这就是安防行为识别场景的典型形态,事件样本少、背景样本多,而且发布方明确标注“含部分增强图片”。对想用yolov8训练自己的数据集的工程师来说,这组数据比COCO那种均衡数据集更有练手价值,因为你要同时解决格式转换、类不平衡、增强图带来的伪影三个问题。
数据本身是Pascal VOC和YOLO两种格式并存,1680张jpg各带一个xml和一个txt,标注工具是labelImg,两类目标分别是climbing和person,总框数2454。直接用这类数据训练时,最大的坑不是模型不会收敛,而是val mAP虚高、实际视频流里误报频繁。适合的人群很明确:做周界防护、园区安全、行为识别研究的开发者,以及想拿真实非均衡数据验证目标检测训练流程的人。
2. VOC与YOLO并存的数据集:XML坐标转换、归一化与反查校验
拿到数据集先看格式。每个jpg对应一个.xml和一个.txt,这就是“同一份标注两种表达”的数据集。很多从KITTI标注转YOLO入手的开发者,第一次看到VOC时习惯找现成的转换脚本,但脚本里的坑通常不在解析xml,而在类别顺序和归一化基准。发布包里除了说明.txt和一系列以xyxr_ship_开头的文本文件(这类文件通常是发布方用来记录原图划分或类别清单的,不是标注文件),核心内容是jpg、xml、txt三件套,实际训练时不要被多余文件干扰,只看xml与jpg的配对关系即可。
2.1 两种格式的根本差异:单位、坐标系与语义
VOC的xml记录的是真实像素坐标:xmin、ymin、xmax、ymax,对应左上角和右下角。而YOLO的txt记录的是相对图片宽高的归一化结果:class_id、x_center、y_center、w、h,全是float。以某个xml文件为例,一个典型目标是这样存储的:
<object> <name>climbing</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>112</xmin> <ymin>86</ymin> <xmax>319</xmax> <ymax>428</ymax> </bndbox> </object>对应的YOLO标签是:0 0.337 0.401 0.291 0.476。换算关系是:x_center = (xmin+xmax)/2 再除以图片宽度,y_center = (ymin+ymax)/2 再除以图片高度;w和h同理除以宽和高。这里最容易被忽略的是,归一化的基准是原图尺寸,不是labelImg预览窗口的尺寸。
| 属性 | VOC XML | YOLO txt |
|---|---|---|
| 坐标单位 | 像素 | 归一化(0~1) |
| 表示方式 | xmin/ymin/xmax/ymax | x_center/y_center/w/h |
| 是否依赖图片尺寸 | 是 | 否 |
| 类别标识 | 字符串 | 整数索引 |
| 读取方式 | xml.etree.ElementTree | 每行五个float |
2.2 手写VOC转YOLO脚本:避免工具链更新导致格式漂移
我一般不用网上“一键转换”的GUI工具,而是用下面的脚本,因为训练集每个类别索引要跟data.yaml对齐。类别顺序一旦变了,txt里第一个数字的含义就全变了。
import xml.etree.ElementTree as ET from pathlib import Path def voc2yolo(xml_path: Path, out_dir: Path, class_names: list[str]): root = ET.parse(xml_path).getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_names: continue cls_id = class_names.index(name) b = obj.find("bndbox") xmin = float(b.find("xmin").text) ymin = float(b.find("ymin").text) xmax = float(b.find("xmax").text) ymax = float(b.find("ymax").text) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") out_dir.mkdir(parents=True, exist_ok=True) out_path = out_dir / (xml_path.stem + ".txt") out_path.write_text("\n".join(lines), encoding="utf-8") if __name__ == "__main__": voc2yolo(Path("annotations/00001.xml"), Path("labels"), ["climbing", "person"])这段代码的逻辑说明:用xml.etree.ElementTree解析文件,先读size节点下的width和height,这是转YOLO的前提;类别名称在class_names列表中的位置,就是输出txt的第一个整数;归一化时除的是原图宽高,而不是自己resize后的尺寸,很多转换错误都出在这里。对于被增强过的图片,xml里的size也要从对应jpg重新读取,不要信任缓存。如果某些xml中出现了difficult=1的物体,建议保留并单独统计,不要直接丢弃,否则会影响后续混淆矩阵的计算口径。
2.3 反推校验:从txt回到像素坐标
转换完不能只看文件数量,要看坐标有没有越界、宽高是否为0。我一般写一个反向脚本抽查:
with open("labels/00001.txt", "r") as f: for line in f: cls_id, x_c, y_c, w, h = map(float, line.split()) img_w, img_h = 640, 480 # 实际应从jpg读取 x1 = int((x_c - w / 2) * img_w) y1 = int((y_c - h / 2) * img_h) x2 = int((x_c + w / 2) * img_w) y2 = int((y_c + h / 2) * img_h) print(cls_id, x1, y1, x2, y2)这里用640和480作为示例宽高,实际使用时要替换成对应jpg的真实尺寸。如果反推的框明显超出图像范围,说明原始xml中记录的尺寸与jpg的EXIF方向字段冲突,最常见的是旋转拍照的手机图片。处理办法是先把所有jpg统一重写为无EXIF方向的图片,再转标签,否则后期用yolo训练时图像会被自动旋转,而坐标不会跟着转。这个坑在含增强图片的数据集里更容易被放大,因为增强流程通常不保留EXIF信息。
提示:转换后统计每个txt里的坐标范围,如果出现负数或大于1的值,优先排查某个xml的xmax误写成了矩形宽度,这种情况在手工标注时很常见。
3. 用YOLOv8训练1680张VOC+YOLO数据集的完整流程
3.1 目录结构与data.yaml设计
YOLOv8训练自己的数据集的目录习惯是images放图、labels放txt,train/val分两个子目录。以这份数据集为例,我最终整理成:
datasets/railing/ images/train/*.jpg images/val/*.jpg labels/train/*.txt labels/val/*.txt1680张图,我一般按8:1:1划分训练/验证/测试。但本数据集样本量不大,测试集保留150张左右更能反映真实场景,剩下1530张用于训练和验证。然后写data.yaml:
path: datasets/railing train: images/train val: images/val nc: 2 names: ["climbing", "person"]注意names顺序必须和转换txt时使用的列表一致,否则标签全部错位。如果之前用labelImg打开过,xml里会存类名,不存在顺序问题;但当你重写转换脚本时,顺序就由你自己决定了,这个顺序要一直保持到data.yaml。
3.2 训练命令与关键参数分析
训练命令我习惯写成多行,便于回看参数:
yolo detect train \ data=datasets/railing/data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ patience=20 \ mosaic=0.5 \ close_mosaic=10 \ scale=0.5 \ fliplr=0.3 \ flipud=0.0 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4 \ cls=1.0 \ box=7.5参数说明:model=yolov8s.pt比n更适合小数据集,因为s的容量足以拟合行为特征,又不像l那样容易过拟合;imgsz=640是对1080p监控图像的一个折中,原图信息量大的话可以用imgsz=960,但要付出更多显存与训练时间;mosaic在本数据集上必须限制在0.5以下,因为数据集本身含增强图,mosaic会进一步人为构造“极端”样本;close_mosaic=10表示最后10个epoch关闭mosaic,让模型适应正常构图,否则val的mAP在训练末尾会突然下跌;cls=1.0保留默认,box=7.5是YOLOv8默认值。
针对person样本太少的问题,YOLOv8官方train参数里没有直接的按类权重,所以一般用损失分支权重或数据采样来干预。我后文会具体讲,训练时先保持默认,跑一轮再看val曲线再调。
3.3 硬件要求与loss观察
训练需要GPU吗?严格说需要。实测GTX 1660 Super 6GB可以跑yolov8s + batch=8 + imgsz=640,CPU跑150轮不现实。观察yolo损失函数变化时,重点看box_loss和cls_loss是否同步下降。如果cls_loss降到很低但val的P曲线剧烈抖动,说明增强图造成了训练集与验证集分布漂移;如果val loss在30轮以后回升,说明模型开始记住增强图的纹理伪影,此时优先把mosaic调到0.3、epochs降到100,并增加close_mosaic的epoch数。
4. 含增强图像的数据集如何清洗:重复检测、类别统计与loss调节
4.1 识别增强图的信号
发布方既然说明“含部分增强图片”,就不能假装它们不存在。增强图通常表现为:水平翻转后文字方向相反、hsv偏色、同一目标多张构图完全一致但尺寸缩放。我处理这份数据集的第一步是算感知哈希:把每张图resize到固定尺寸,计算dHash,再按汉明距离找重复组。
from PIL import Image import imagehash def dhash(img_path: str) -> str: return str(imagehash.dhash(Image.open(img_path).convert("L").resize((9, 8))))然后对1680张图两两比较汉明距离,距离小于5的基本就是增强副本。如果只是镜像翻转,dHash的汉明距离会比较大,可以在找相似结果时再用ImageOps.mirror对其中一个做翻转再比较。找到重复组之后,选择保留哪张取决于你想要的多样性:保留原图、删除翻转增强图,可避免模型把“翻越方向”学偏。删除时要同步删除xml和txt,不能只删图片。
4.2 统计框的宽高比和位置分布
我一般会在训练前跑一个统计脚本,输出每类目标的宽高比中位数、中心点热区,看看数据是否过于集中在画面中央。翻越栏杆场景里,目标集中在围栏附近,这是合理的,但如果验证集也来自同一场景,就会高估模型泛化能力。这个数据集的person框只有394个,远少于climbing的2060个,所以在数据集中person更像是“路人/误闯者”,而climbing才是真正要报警的行为。
| 类别 | 框数 | 占比 | 训练建议 |
|---|---|---|---|
| climbing | 2060 | 84% | 作为主检测类别 |
| person | 394 | 16% | 保留但不能让它主导模型 |
如果直接训练,模型会把person当作困难负样本,最后推理时看到“站立的人”常常不输出框,因为在loss里它被反复压低了。这时一种常见做法是:在数据加载时对person做dropout,每批随机丢掉20%的person框,或者复制climbing样本两遍。这个不用改yolo源码,用PyTorch的WeightedRandomSampler就可以实现。
4.3 调节损失函数与增强策略
如果想在不动代码的情况下干预,就把cls提高到1.5,同时把fliplr从0.3降到0.15。为什么:person样本少但和climbing外观高度相似,水平翻转会让人物左右手互换,对“攀爬姿态”的影响远大于对普通检测目标的影响。反过来,如果模型漏检较多,就把scale=0.5改成scale=0.8,让目标大小变化更剧烈,迫使模型学到“距离远时的小尺寸目标”。
另外要注意:如果数据集里含旋转增强,那些旋转角度超过30度的图片并不适合这个任务,因为翻越栏杆的行为语义有重力方向。遇到生成式增强图,最好的处理是直接删掉而不是留着。最后重新划分train/val时,要确保增强图和原图不同时落在验证集,否则val指标虚高,部署到新场景立刻打回原形。
5. 护栏攀爬检测的验证与推理调优:混淆矩阵、TTA与连续帧处理
5.1 用混淆矩阵定位误报来源
训练结束后先在验证集上跑指标:
yolo detect val model=runs/detect/train/weights/best.pt data=datasets/railing/data.yaml split=val运行后会在输出目录生成confusion_matrix.png。重点看两类:person被预测成climbing的比例,和climbing被预测成person的比例。对安防场景而言,我更关心person→climbing,这个比例高于10%就不适合直接上监控,因为行人路过会触发大量误报。导致这个结果的根本原因是两类样本比例悬殊,模型倾向于把不确定的人形都归到样本量大的climbing里。
5.2 用TTA与NMS稳定单帧输出
推理时可以用TTA提升召回:
yolo predict model=runs/detect/train/weights/best.pt source=test_video.mp4 imgsz=640 conf=0.25 iou=0.45 augment=True参数说明:conf=0.25是安防场景的下限,低于它会刷出大量低置信度person框;augment=True会同时用原图和翻转图推理再合并,单帧耗时增加约一倍,但recall提升明显;iou=0.45控制NMS的抑制力度,翻越栏杆这类有遮挡的场景可以适当降到0.4,减少相邻框合并。如果只做CPU部署,可以加device=cpu,速度取决于视频解码器。
5.3 连续帧处理:避免单帧闪烁
栏杆攀爬是一个过程动作,单帧检测抖动很大。常见做法是在推理后串一个轻量追踪器,例如ByteTrack,对同一个ID连续3帧以上保持climbing置信度大于0.3再触发告警。用ultralytics的track模式:
yolo track model=runs/detect/train/weights/best.pt source=test_video.mp4 tracker=bytetrack.yaml persist=Truetracker=bytetrack.yaml表示使用ByteTrack配置。此时如果同一个track_id在连续多帧里都是climbing,就认为事件成立,避免行人路过时误报。最后提一个组合技巧:TTA和跟踪尽量不要同时开,先关掉augment再跑track,否则检测框位置会因TTA产生轻微偏移,导致同一个目标的track_id频繁切换,反而更抖。
本文还有配套的精品资源,点击获取