简介:这份安检X光危险物品识别检测数据集面向安检图像识别、目标检测方向的算法工程师与高校研究者,可用于训练和验证X光场景下的违禁品检测模型。数据同时提供Pascal VOC与YOLO两种标注格式,包含jpg原图及一一对应的xml、txt标注文件,无需额外转换即可直接接入主流检测框架。压缩包共约2000个文件,以1999个xml标注文件和1个说明txt为主,整体约350.67MB,标注覆盖罐头、玻璃瓶、塑料瓶、喷雾罐、瑞士军刀、刀片、匕首、刀具、剪刀等12个类别,贴近真实安检场景中的危险物品形态。目前已有852人学习下载,适合用于模型训练、类别扩展实验与算法对比评测,也可作为安检智能识别项目的预训练数据基础。
1. 安检 X 光危险物品识别:9551 张 VOC+YOLO 双格式数据集到底能干什么
安检机每天吐出成千上万张灰度透视图,刀具、剪刀、打火机、压力罐、锂电池这些违禁品混在行李里,靠人眼盯屏幕,漏检和疲劳是迟早的事。这个标题指向的,就是给这类场景准备的一份目标检测数据集:9551 张 X 光图像,12 个危险物品类别,同时提供 VOC 和 YOLO 两套标注格式。它解决的不是"模型怎么设计",而是"我上哪找一批带框的安检图,直接开训"。适合两类人:一类是想做安检危险品识别 demo 或课程设计的同学,另一类是手里有安检业务、想先跑个基线看看可行性的工程师。VOC 和 YOLO 双格式意味着你不用自己写转换脚本,Pascal VOC 的 XML 和 YOLO 的 txt 标签都在,省掉最容易出错的一步。12 类、9551 张这个量级,单卡就能训,不挑硬件,是典型的"小而全"落地数据集。
2. 先搞清楚 VOC 和 YOLO 两套标注到底差在哪
2.1 同一张图,两种标签文件的字段对照
VOC 格式的标注是每张图一个 XML,框的坐标用绝对像素值,左上角加右下角两个点表示。YOLO 格式是每张图一个 txt,每行一个目标,格式是类别索引 中心x 中心y 宽 高,而且这四个值全部归一化到 0 到 1 之间。很多人第一次转格式翻车,就是忘了归一化,或者把中心点坐标当成左上角坐标写进去,训出来的框整体偏移。
| 项目 | VOC (XML) | YOLO (txt) |
|---|---|---|
| 文件粒度 | 一图一 XML | 一图一 txt |
| 坐标表示 | 绝对像素 xmin,ymin,xmax,ymax | 归一化 cx,cy,w,h |
| 类别表示 | 字符串 name | 整数索引 class_id |
| 类别映射 | 写在每个 XML 里 | 靠 classes.txt 或 data.yaml 顺序 |
| 空图处理 | 无 object 节点 | 空 txt 文件 |
这张表建议存下来,转换脚本写错的时候对着看,八成能定位问题。
2.2 12 类危险物品的类别定义要先对齐
数据集给了 12 个类别,但 VOC 里是字符串,YOLO 里是 0 到 11 的整数。这两套映射必须严格一致,否则同一个"剪刀"在 VOC 里叫 scissors,在 YOLO 里索引是 3,你训练时 data.yaml 里第 3 位写成了 knife,模型学出来的就是错的。常见做法是先跑一遍统计,把所有 XML 里的 name 去重列出来,人工确认顺序,再生成 YOLO 的 classes 列表。
import os import xml.etree.ElementTree as ET from collections import Counter # 统计 VOC 标注里出现的所有类别名及数量 def count_voc_classes(xml_dir): 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'): name = obj.find('name').text.strip() counter[name] += 1 return counter if __name__ == '__main__': # 把这里换成你的 VOC 标注目录 result = count_voc_classes('./Annotations') for name, num in result.most_common(): print(f'{name}: {num}')这段代码的作用是把 VOC 目录里所有类别名和出现次数打出来。参数只有一个xml_dir,指向 Annotations 文件夹。跑完你会得到一份真实分布,如果某个类别只有几十个框,后面训练要单独处理,别指望它和上千框的类别一个效果。类别顺序一旦定下来,就写进 data.yaml,后面所有环节都引用它,不要中途改。
3. 从 VOC 转 YOLO:转换脚本和四个必查边界
3.1 转换脚本逐行拆解
VOC 转 YOLO 的核心就三件事:读 XML 拿宽高、把绝对坐标转归一化中心点、按类别名查索引写 txt。下面这份脚本可以直接抄,注意路径和类别列表要改成你自己的。
import os import xml.etree.ElementTree as ET # 类别顺序必须和 data.yaml 完全一致 CLASSES = ['knife', 'scissors', 'lighter', 'battery', 'pressure_tank', 'spray', 'hammer', 'wrench', 'powerbank', 'zippo', 'bullet', 'firecracker'] def voc_to_yolo(xml_path, out_txt_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) lines = [] for obj in root.findall('object'): name = obj.find('name').text.strip() if name not in CLASSES: continue # 类别不在列表里直接跳过,避免索引错乱 cls_id = CLASSES.index(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) # 归一化中心点与宽高 cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h # 越界裁剪,防止个别标注超出图像范围 cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) bw = min(max(bw, 0.0), 1.0) bh = min(max(bh, 0.0), 1.0) lines.append(f'{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}') with open(out_txt_path, 'w') as f: f.write('\n'.join(lines)) if __name__ == '__main__': xml_dir = './Annotations' out_dir = './labels' os.makedirs(out_dir, exist_ok=True) for f in os.listdir(xml_dir): if f.endswith('.xml'): voc_to_yolo(os.path.join(xml_dir, f), os.path.join(out_dir, f.replace('.xml', '.txt')))逻辑说明:先读图像宽高,再把每个框的绝对坐标换算成归一化中心点和宽高。CLASSES列表的顺序就是最终类别索引,必须和训练配置一致。越界裁剪那几行是后悔药,有些标注框会超出图像边界,不裁的话 YOLO 训练时可能报坐标异常。参数上,xml_dir是 VOC 标注目录,out_dir是输出 txt 目录,两个目录名按你实际解压后的结构改。
3.2 转换后必须做的四项校验
转完不是就完事了,下面四项不查,训练阶段一定出玄学问题。
第一,数量对齐。XML 文件数应该等于 txt 文件数,差一个都说明有文件没转成功。第二,空标签检查。有些图确实没有目标,对应 txt 是空的,这是正常的,但如果大量 txt 为空,说明类别名没匹配上,全被continue跳过了。第三,坐标范围。随便抽几个 txt,确认所有值都在 0 到 1 之间,出现大于 1 的就是没归一化。第四,类别分布。统计每个 class_id 出现的行数,和 2.2 里 VOC 统计的数量对一遍,对不上就是映射错了。
# 数量对齐检查 ls Annotations/*.xml | wc -l ls labels/*.txt | wc -l # 找出空标签文件 find labels -name "*.txt" -empty | wc -l # 抽查坐标是否越界(输出应为空) awk '$2>1 || $3>1 || $4>1 || $5>1' labels/*.txt这三条命令是血泪经验攒出来的,尤其最后一条,越界坐标在训练时不一定立刻报错,但会让 loss 异常震荡,查半天查不出来。
4. 用 YOLOv8 把这份数据集跑起来
4.1 目录结构和 data.yaml 怎么写
YOLOv8 对目录结构有固定要求,图片和标签要分开放,且路径要能对应上。推荐结构如下:
dataset/ images/ train/ val/ labels/ train/ val/ data.yamldata.yaml 是训练的入口配置,写错一个字段就跑不起来。
path: ./dataset train: images/train val: images/val nc: 12 names: 0: knife 1: scissors 2: lighter 3: battery 4: pressure_tank 5: spray 6: hammer 7: wrench 8: powerbank 9: zippo 10: bullet 11: firecrackernc是类别数,必须和 names 的长度一致。names 的顺序必须和转换脚本里的 CLASSES 一模一样,这是最容易翻车的地方,顺序错了模型照样能训,但预测出来的类别全是错的,而且 loss 看起来还挺正常,属于典型黑匣子问题。
4.2 训练命令和关键参数怎么设
yolo detect train \ data=dataset/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/security \ name=xray_v1参数说明:model用 yolov8n 起步,9551 张图用 nano 版本足够跑通,想提精度再换 s 或 m。imgsz=640是默认值,X 光图里小目标多的话可以提到 800,但显存要跟上。batch=16在 8G 显存上比较稳,爆显存就降到 8。patience=20是早停,20 轮没提升就停,省时间。lr0=0.01是初始学习率,小数据集不建议调太大,容易震荡。
训练完看 results.png 里的 mAP50 和 mAP50-95,再看混淆矩阵。如果某个类别一直学不好,先回去查这个类别的框数量是不是太少,而不是急着改模型。
4.3 划分训练验证集时别踩的坑
9551 张图不能随便随机切。安检图往往来自连续拍摄,相邻帧高度相似,纯随机划分会让验证集里出现和训练集几乎一样的图,mAP 虚高,上线就露馅。常见做法是按采集批次或时间划分,同一批次的图要么全进训练,要么全进验证。如果数据集没给批次信息,至少用哈希或文件名前缀做分组划分,别用random.shuffle一刀切。
import os import random from sklearn.model_selection import GroupShuffleSplit # 假设文件名前缀代表采集批次,如 batch01_0001.jpg files = [f for f in os.listdir('images') if f.endswith('.jpg')] groups = [f.split('_')[0] for f in files] gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(files, groups=groups)) print('train:', len(train_idx), 'val:', len(val_idx))这段用 GroupShuffleSplit 按批次分组划分,保证同一批次的图不会同时出现在训练和验证里。test_size=0.2是验证集比例,groups是每张图所属批次。如果你的文件名没有批次信息,这一步可以先跳过,但要知道验证指标可能偏乐观。
5. 训练和验证阶段最容易翻车的几个点
5.1 现象:loss 正常下降但 mAP 一直是 0
原因:data.yaml 里的 names 顺序和转换脚本 CLASSES 顺序不一致,或者路径写错导致标签根本没加载上。YOLO 找不到标签时不会报错,会把所有图当负样本训,loss 照样降。
解决:先跑一次yolo detect train看日志里打印的类别数和名称,和你的预期对一遍。再抽查一张图的 txt,确认 class_id 在 names 范围内。路径问题用绝对路径先验证一遍。
5.2 现象:某个类别召回率极低,几乎检不出
原因:这个类别的框数量太少,或者框普遍偏小。X 光图里子弹、打火机这类小目标,640 分辨率下可能只剩十几个像素,模型很难学。
解决:先统计各类别框数,少于 200 的类别考虑过采样或单独增强。分辨率提到 800 或 1024,小目标会明显改善。也可以在训练时开 mosaic 增强,但要注意 X 光图拼接后语义会变怪,效果不一定好,得试。
5.3 现象:验证集 mAP 很高,换一批新图就崩
原因:训练验证集划分时相邻帧泄漏,验证集和训练集太像,指标虚高。这是最隐蔽的坑,看指标根本发现不了。
解决:按批次或时间重新划分,确保验证集是模型没见过的采集条件。如果条件允许,留一批完全不同来源的图做测试集,那个数字才可信。
5.4 现象:训练中途 BN 层报错或 loss 变 NaN
原因:batch 太小或者学习率太大,BN 统计量不稳定。小数据集上 batch=4 以下很容易出这个问题。
解决:batch 至少提到 8,或者改用 yolov8 默认的优化器配置,把 lr0 降到 0.001 再试。如果显存实在不够,用梯度累积模拟大 batch,别硬扛小 batch。
5.5 现象:混淆矩阵里两个类别互相混
原因:这两个类别在 X 光图里形态接近,比如不同材质的刀具,或者类别定义本身有重叠。
解决:先确认标注时这两类是否真的分得清,如果人眼都难分,模型更分不了,考虑合并类别。如果确实可分,增加这两类的样本量,或者在 data.yaml 里调整类别顺序让它们不相邻(对某些损失函数有微弱影响)。根本上还是靠数据,别指望改网络结构解决标注问题。
6. 把这份数据集用出上限:几个进阶技巧
数据集本身是固定的,能拉开差距的是怎么用。第一个技巧是预训练权重选择,yolov8n.pt 是 COCO 预训练的,和 X 光灰度图域差异大,但底层边缘纹理特征仍然有用,直接微调比从头训快得多。如果你能找到其他 X 光或灰度图预训练权重,替换上去通常能再涨几个点。
第二个技巧是针对性增强。X 光图的特点是灰度、低对比、物品重叠严重。常规的 HSV 增强意义不大,因为图本来就没色彩。更有效的是 CLAHE 直方图均衡、随机伽马变换、以及模拟物品重叠的 cutmix。下面这段是 YOLOv8 自定义增强的入口思路:
# 在 ultralytics 的 augment 配置里,重点调这几个 # hsv_h: 0.0 灰度图不需要色调增强 # hsv_s: 0.0 饱和度同理 # hsv_v: 0.4 亮度增强保留,模拟不同穿透强度 # degrees: 10.0 小角度旋转,安检物品朝向随机 # translate: 0.1 # scale: 0.5 # fliplr: 0.5 左右翻转安全 # flipud: 0.0 上下翻转对安检图不合理,物品不会倒挂参数解释:灰度图把 hsv_h 和 hsv_s 关掉,避免无意义的色彩扰动。hsv_v 保留,模拟不同行李密度下的亮度变化。flipud 关掉,因为上下翻转会造出物理上不存在的姿态,属于无效增强甚至有害。
第三个技巧是推理阶段的处理。安检场景对漏检容忍度低,可以在推理时降低置信度阈值,比如从 0.25 降到 0.15,再用 NMS 的 iou 阈值控制重叠框。代价是误报变多,实际部署时要根据业务权衡。我一般会先跑一批真实图,画一条置信度-召回曲线,找到业务能接受的平衡点,而不是拍脑袋定 0.25。
最后一个习惯:每次改完配置或增强策略,固定用同一批验证图跑一遍,记录 mAP 和各类别指标,别凭感觉说"好像好了点"。数据集就这一份,实验记录才是你真正的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取