简介:本数据集面向公共场所安全监控、危险物品识别等计算机视觉任务,适合目标检测模型训练与算法验证,尤其适用于安防场景下的多类别物品检测研究。资源采用Pascal VOC与YOLO双格式标注,包含1431张jpg图片及对应的xml、txt标注文件,标注工具为labelImg,以矩形框方式完成六类目标标注:billete、knife、monedero、pistol、smartphone、tarjeta,总框数1497个,其中knife类别框数最多达1035个。压缩包为7z格式,共2000个文件,含1431个xml与569个txt,整体约126.77MB,目录结构清晰,便于直接接入主流检测框架进行训练与评估。目前已有330人学习下载,可为安防检测、危险品识别等课题提供现成的标注数据基础,帮助读者省去数据采集与标注环节,快速开展模型实验与效果对比。
1. 1431 张图、6 个类别:这份公共场所危险物品检测数据集到底能干什么
如果你正在找一份能直接跑通 YOLO 训练、又带 VOC 双格式标注的小规模危险物品数据集,这份 1431 张图片、6 个类别、1497 个标注框的资源值得先看一眼。它覆盖了 billete(纸币)、knife(刀具)、monedero(零钱包)、pistol(手枪)、smartphone(手机)、tarjeta(卡片)六类目标,全部用 labelImg 画矩形框,同时提供 Pascal VOC 的 xml 和 YOLO 的 txt 两套标注。换句话说,你不需要自己写格式转换脚本,拿到就能分别喂给 YOLOv5/v8 或者任何吃 VOC 的老框架。
它的定位很明确:不是那种几十万张的工业级数据集,而是一个能让你在单卡上几小时内跑完一轮实验的轻量包。适合做公共场所安检场景的原型验证、课堂演示、或者作为你自建数据集前的格式参考。1431 张的体量意味着你用它来验证数据增强策略、损失函数改动、或者 efficient head 这类结构微调时,迭代成本很低。但也要提前说清楚:类别分布并不均衡,knife 有 1035 个框,monedero 只有 18 个,这种长尾分布会直接影响训练时的类别权重设置,后面会专门讲怎么处理。
2. 拆开压缩包:VOC 与 YOLO 双格式的目录结构与字段含义
2.1 文件清单与命名规则
解压后你会看到 jpg 图片、对应的 xml 文件、以及同名的 txt 文件,三者数量都是 1431。另外还有一批 firc_ 开头的 txt 和一个「使用前必读.txt」。firc_ 系列通常是图片的分组或索引文件,具体用途以「使用前必读.txt」里的说明为准,不要想当然地当成标注文件去解析。图片命名没有统一前缀,所以划分训练集和验证集时不能靠文件名排序,得用脚本随机切分或者按 firc 分组切分。
VOC 格式的 xml 里,每个目标对应一个<object>节点,包含<name>、<bndbox>下的 xmin/ymin/xmax/ymax。YOLO 格式的 txt 每行是class_id x_center y_center width height,全部归一化到 0~1。两套标注的类别顺序必须一致,否则你混用时会出现类别错位。常见做法是先用脚本统计一遍两边的类别名和数量,确认对齐后再开始训练。
2.2 类别分布与长尾问题
| 类别名 | 标注框数 | 占比 |
|---|---|---|
| billete | 249 | 16.6% |
| knife | 1035 | 69.1% |
| monedero | 18 | 1.2% |
| pistol | 73 | 4.9% |
| smartphone | 94 | 6.3% |
| tarjeta | 28 | 1.9% |
knife 一类占了将近七成,monedero 和 tarjeta 加起来不到 50 个框。这种分布下,如果你直接用默认的类别权重训练,模型会强烈偏向 knife,小类别几乎学不到。我一般会先算一遍每个类别的框数,然后要么在 loss 里加类别权重,要么对小类别做过采样。YOLOv8 的cls损失本身不带自动加权,需要你自己在 dataset 的get_labels或者 sampler 里处理。
2.3 用脚本核对标注一致性
在训练之前,先跑一段校验脚本,确认每张图都有对应的 xml 和 txt,且类别 id 映射正确。下面这段代码遍历目录,输出缺失文件和类别统计:
import os from collections import Counter from xml.etree import ElementTree as ET img_dir = "images" xml_dir = "annotations_xml" txt_dir = "labels_yolo" imgs = {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(".jpg")} xmls = {os.path.splitext(f)[0] for f in os.listdir(xml_dir) if f.endswith(".xml")} txts = {os.path.splitext(f)[0] for f in os.listdir(txt_dir) if f.endswith(".txt")} print("缺失 xml:", imgs - xmls) print("缺失 txt:", imgs - txts) cls_counter = Counter() for f in os.listdir(xml_dir): if not f.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, f)) for obj in tree.findall("object"): cls_counter[obj.find("name").text] += 1 print("VOC 类别统计:", cls_counter)这段脚本做了三件事:用集合差集找出缺标注的图片、遍历 xml 统计每个类别的框数、把结果打印出来。参数上只需要改三个目录路径。如果你发现某个类别在 VOC 和 YOLO 里的数量对不上,说明转换时出过错,得回去检查类别映射表。常见做法是把类别名按字母序排好,生成一个classes.txt,两边都用这个顺序。
3. 从 VOC 到 YOLO:转换脚本、类别映射与训练配置
3.1 自己写一遍转换脚本的必要性
虽然数据集已经提供了 YOLO 格式的 txt,但你还是应该自己写一遍转换脚本。原因有两个:一是你要确认类别 id 的映射顺序和你的data.yaml一致;二是你可能会做数据增强后再生成新的标注,那时候需要批量转换。下面是一个标准的 VOC 转 YOLO 脚本:
import os import xml.etree.ElementTree as ET classes = ["billete", "knife", "monedero", "pistol", "smartphone", "tarjeta"] cls_to_id = {c: i for i, c in enumerate(classes)} def convert(xml_path, out_path, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in cls_to_id: continue 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) xc = (xmin + xmax) / 2.0 / img_w yc = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_to_id[name]} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines))关键参数是classes列表的顺序,它决定了 txt 里每行第一个数字代表哪个类别。img_w和img_h必须从对应 jpg 读取,不能硬编码,因为数据集里图片尺寸可能不统一。转换完成后,随便抽几张图用可视化脚本画框,确认坐标没有偏移。
3.2 data.yaml 与训练超参设置
YOLOv8 的data.yaml写法如下:
path: ./danger_dataset train: images/train val: images/val nc: 6 names: ["billete", "knife", "monedero", "pistol", "smartphone", "tarjeta"]nc必须等于 6,names的顺序和转换脚本里的classes完全一致。训练时,针对长尾分布,我一般会把lr0设到 0.001 而不是默认的 0.01,因为小数据集上大学习率容易让模型只记住 knife。batch设 16 或 32,取决于你的显存。epochs可以先跑 100 轮看曲线,如果 val 的 mAP 在 50 轮后还在涨,再加到 200。
还有一个容易忽略的点:close_mosaic参数。默认是最后 10 轮关闭 mosaic 增强,但小数据集上 mosaic 会进一步加剧类别不平衡,因为 knife 的图片被拼来拼去,小类别更没机会。我通常会把close_mosaic设成 20 甚至 30,让模型在后半程看到更多原始分布。
3.3 训练命令与日志观察
yolo detect train data=data.yaml model=yolov8n.pt epochs=150 imgsz=640 batch=16 lr0=0.001 close_mosaic=20跑起来之后,重点看三个指标:metrics/mAP50、metrics/mAP50-95、以及每个类别的metrics/mAP50分项。如果 knife 的 mAP 很高但 monedero 一直是 0,说明小类别完全没学到,需要回去加类别权重或者做过采样。日志里还会输出instances数量,确认每轮的训练实例数和你的标注总数对得上。
4. 避坑与排查:标注、类别与训练中的五个血泪教训
4.1 现象:训练时 loss 突然变 NaN
原因:学习率过大,或者某张图的标注框宽高为 0。这个数据集里 monedero 只有 18 个框,如果其中某个框的 xmax 等于 xmin,归一化后宽度为 0,YOLO 在计算 loss 时会出问题。
解决:在转换脚本里加一行判断,if xmax <= xmin or ymax <= ymin: continue,把无效框过滤掉。同时把lr0降到 0.001 以下再试。
4.2 现象:验证集 mAP 很高,但实际推理时框全偏了
原因:训练时用了 letterbox 填充,但推理时没有保持同样的预处理。YOLO 默认会做 letterbox,但如果你自己写推理脚本,忘了把坐标映射回原图尺寸,框就会整体偏移。
解决:直接用 ultralytics 的model.predict,它内部处理了坐标还原。如果必须自己写,记住 letterbox 的缩放比例和 padding 值,反算回去。
4.3 现象:类别 id 对不上,knife 被识别成 pistol
原因:VOC 的 xml 里类别名是字符串,YOLO 的 txt 里是数字 id。如果你手动改过classes.txt的顺序,但没重新生成 txt,就会错位。
解决:永远用同一个classes列表生成 txt 和data.yaml,不要中途改顺序。校验脚本里加一步:随机抽 10 张图,把 xml 的类别名和 txt 的 id 映射回去对比。
4.4 现象:小类别 monedero 和 tarjeta 的召回率始终为 0
原因:框数太少,模型在训练中几乎看不到正样本。加上 mosaic 增强后,小类别被进一步稀释。
解决:对小类别做过采样,把包含 monedero 和 tarjeta 的图片复制多份,或者在 dataloader 里给它们更高的采样权重。另一个办法是冻结 backbone,只训练 head,减少模型对小类别的遗忘。
4.5 现象:训练到一半显存爆了
原因:batch设太大,或者imgsz设成了 1280。1431 张图里有些图片分辨率较高,放大到 1280 后显存占用翻倍。
解决:先用imgsz=640跑通,确认流程没问题再尝试更大尺寸。如果显存不够,把batch降到 8,或者用yolov8n而不是yolov8m。
5. 进阶技巧:用分层采样和类别权重把长尾拉平
5.1 分层采样:让每个 batch 都见到小类别
YOLO 默认的 dataloader 是随机打乱,小类别可能连续几个 batch 都不出现。我一般会写一个自定义 sampler,保证每个 batch 里至少包含一张含 monedero 或 tarjeta 的图片。实现方式是按图片的类别标签分组,然后从每组轮流采样。下面是一个简化版:
import random from torch.utils.data import Sampler class BalancedSampler(Sampler): def __init__(self, labels, batch_size=16): self.batch_size = batch_size self.groups = {} for idx, lbls in enumerate(labels): key = tuple(sorted(set(lbls))) self.groups.setdefault(key, []).append(idx) self.keys = list(self.groups.keys()) def __iter__(self): while True: batch = [] for key in self.keys: if self.groups[key]: batch.append(random.choice(self.groups[key])) while len(batch) < self.batch_size: key = random.choice(self.keys) batch.append(random.choice(self.groups[key])) yield from batch[:self.batch_size] def __len__(self): return sum(len(v) for v in self.groups.values())这段代码按类别组合分组,每个 batch 先从每组抽一张,不够的再随机补。参数batch_size要和训练时的 batch 一致。注意这个 sampler 是无限迭代的,需要配合max_steps或者epochs控制训练长度。
5.2 类别权重:在 loss 里给小类别加杠杆
YOLOv8 的v8DetectionLoss里,cls分支用的是 BCEWithLogitsLoss。你可以手动传入pos_weight,让 monedero 和 tarjeta 的损失权重更高。常见做法是按框数的倒数开根号来设权重:
| 类别 | 框数 | 权重(倒数开根号归一化) |
|---|---|---|
| billete | 249 | 0.63 |
| knife | 1035 | 0.31 |
| monedero | 18 | 2.35 |
| pistol | 73 | 1.17 |
| smartphone | 94 | 1.03 |
| tarjeta | 28 | 1.88 |
把权重传给 loss 函数时,注意顺序要和data.yaml的names一致。如果不想改源码,也可以在数据集层面做重采样,效果类似但更简单。
5.3 验证方法:用混淆矩阵和 PR 曲线确认长尾是否改善
训练完之后,跑一遍yolo detect val,然后看混淆矩阵。如果 monedero 的列里大部分预测都落到 knife 或背景上,说明模型还是没学会区分。这时候可以进一步加大权重,或者把 monedero 的图片单独拿出来做一轮微调。PR 曲线里,小类别的曲线如果贴近坐标轴,说明召回率极低,需要继续调整采样策略。
从那以后我每次拿到这种长尾数据集,都会先跑一遍类别统计,把权重和采样策略定下来再开训,不然跑完 150 轮才发现小类别全灭,那才是真的后悔药都没得吃。希望这份拆解能帮到你,拿到数据后先校验、再转换、最后调采样,三步走完再开训,能省下不少返工时间。
本文还有配套的精品资源,点击获取