X光安检目标检测实战:VOC/YOLO数据集处理与yolov8训练调参
简介:面向X光安检场景的目标检测数据集,提供3600张真实安检图像,涵盖打火机、刀具、剪刀、充电宝等10类违禁品,并划分测试集,适合目标检测方向的研究者、算法工程师及相关专业学生用于模型训练与效果验证。压缩包内共2000个文件,以Pascal VOC格式xml标注和YOLO格式txt标注为主体,整体大小约793.61MB,采用labelImg标注工具,双格式文件可方便切换主流检测框架。已有241人浏览学习。资源总计包含9042个标注框,各类别框数从几十到两千多不等,类别不均衡明显,可用于验证模型在稀有类别上的表现;同时提供完整测试集划分,便于直接评估检测精度与召回率,是安检智能识别项目落地的实用数据基础。
1. X光安检的目标检测,先解决数据集准备问题
做X光安检的目标检测项目,最耗时间的往往不是调模型,而是数据集准备。眼前这个压缩包解压后同时给出 VOC 和 YOLO 两种标注格式,3600 张原图覆盖 10 个违禁品类别,还单独切好了测试集——这三个信息放在一起,指向的是“拿来即用”:不用先花两周做格式转换,不用自己拍脑袋分训练集,测试集的存在还让你有一个和论文对得上的验收基准。它适合两类人:想快速跑通 yolo 训练流程、验证目标检测效果的入门者,以及要在固定测试集上对比改进收益的算法工程师。下面按我处理安检数据的顺序,把数据校验、训练配置、参数调优和部署验证四件事拆开讲,脚本可以直接抄。
2. 解压先做数据体检:VOC与YOLO双格式怎么对齐
开始训练之前一定先做数据体检。双格式数据集的隐患在于:VOC 的 XML 和 YOLO 的 txt 是两套标注,通常由不同脚本生成,一旦其中一套漏标、错行,训练时你用的是 txt,而检查时看的是 XML,问题会被带进损失函数里才暴露。先花十分钟把两份标注对齐,后面省下的调试时间是以天计的。
2.1 目录结构与双格式对照:XML与txt必须成对
VOC+YOLO 双格式数据集,习惯的目录摆法如下,解压后先核对这个骨架:
XrayData/
├── VOCdevkit/
│ ├── JPEGImages/ # 3600 张 .jpg 原图
│ ├── Annotations/ # 同名 .xml,像素级 bndbox
│ └── ImageSets/Main/ # 官方划分 txt
├── YOLO/
│ ├── images/train|val|test/
│ └── labels/train|val|test/
├── classes.txt # 10 行,一行一个类名
VOC 格式里,每个目标框记录的是左上角和右下角的像素坐标;YOLO 格式每行是“类别索引、归一化中心点 x、归一化中心点 y、归一化宽、归一化高”。以一张 640×480 的图里一个 knife 框为例,XML 里写的可能是 xmin=120 ymin=80 xmax=260 ymax=180 ,对应 YOLO txt 应该是:
1 0.2969 0.2708 0.2188 0.2083
这里的 1 是 knife 在 classes.txt 里的行号。最容易出问题的是有人把 xmin/width 当作中心点直接用,五个数就废了四个。所以我拿到数据集第一件事,不是看十类目标长什么样,而是跑一遍下面的校验脚本,把“XML 有但 txt 没有”“类名不在表里”“坐标越界”一次性筛出来:
import os
import xml.etree.ElementTree as ET
from glob import glob
def audit_xray_pair(xml_dir, label_dir, classes_file):
with open(classes_file, encoding="utf-8") as f:
classes = [line.strip() for line in f if line.strip()]
problems = []
for xml_path in sorted(glob(os.path.join(xml_dir, "*.xml"))):
root = ET.parse(xml_path).getroot()
img_name = root.findtext("filename")
txt_path = os.path.join(label_dir, os.path.splitext(img_name)[0] + ".txt")
if not os.path.exists(txt_path):
problems.append(f"{img_name}: 有 XML,缺 YOLO txt")
continue
for obj in root.iter("object"):
name = obj.findtext("name")
if name not in classes:
problems.append(f"{img_name}: XML 出现未登记类别 {name}")
for line in open(txt_path, encoding="utf-8"):
parts = line.strip().split()
if len(parts) != 5:
problems.append(f"{img_name}: txt 行字段数不为 5")
continue
cid, cx, cy, w, h = int(parts[0]), *map(float, parts[1:])
if cid >= len(classes):
problems.append(f"{img_name}: 类别索引 {cid} 越界")
if not (0 <= cx <= 1 and 0 <= cy <= 1 and w > 0 and h > 0):
problems.append(f"{img_name}: 坐标非法 ({cx},{cy},{w},{h})")
if cx - w / 2 < 0 or cy - h / 2 < 0 or cx + w / 2 > 1 or cy + h / 2 > 1:
problems.append(f"{img_name}: bbox 越出图像边界")
return problems
if __name__ == "__main__":
for p in audit_xray_pair("VOCdevkit/Annotations", "YOLO/labels", "classes.txt"):
print(p)
脚本逻辑分三段:先用 filename 字段把 XML 和 txt 配对,再校验 XML 里的类名是否都在 classes.txt 里,最后检查 YOLO 五个字段的规范性和取值范围。注意倒数第三行的越界判断用的是“中心点加减宽高一半”,不是直接比较 cx 和 w,这个细节区分了“归一化数值合法”和“框真的在图内”。 line.strip().split() 按空白切分,所以 10 个类名里不要带空格,否则一个类名会被拆成两个字段,这类玄学问题后面极难排查。跑完如果 problems 为空,再进入下一步;有报错就逐条修,不要带着脏标注去训练,这是数据文件出问题前的最后一道闸。
2.2 10个类别的常见配置:材质、遮挡与标注噪声
3600 张、10 个类别,平均每类 360 张,但真实的安检数据绝不会均匀分布。我经手过的 X 光安检目标检测数据集,类别配置大致落在下面这个范围:锐器占一半,火种和电子设备占另一半。
| 类别 | 常见英文标签 | 在安检屏上的特征 |
|---|---|---|
| 刀具 | knife | 细长金属高亮,常和钱包、雨伞叠放 |
| 枪支 | gun | 轮廓清晰,但多与电子设备混叠 |
| 剪刀 | scissors | 双环结构,半透明区域多 |
| 钳子 | pliers | 与剪刀视觉上高度接近 |
| 扳手 | wrench | 一端开口,无机物蓝色调 |
| 打火机 | lighter | 小目标,外壳边缘弱 |
| 电池/充电宝 | battery | 矩形,内部密度差异大 |
| 喷雾罐 | spray_bottle | 圆柱形,金属底座反光 |
| 液体瓶 | liquid | 轮廓淡,材质色范围宽 |
| 爆竹/火种 | firecracker | 小目标,经常成串出现 |
这张表不是给你背的,而是提醒三件事。第一,遮挡是常态:包里的东西互相叠,X 光又有穿透性,框的边界在标注时就有分歧,数据噪声比自然图像高一个档次。第二,颜色是材质轴:安检屏上的橙(有机物)、蓝(无机物)、绿(混合物)是伪彩色,不是物体本色,后处理时别用自然图像的色彩逻辑去推。第三,角度任意:一把刀横放竖放斜放都有,旋转增强在这个场景不是可选项而是必需项。第三点的另一个推论是,这类数据集做旋转框目标检测(比如 mmrotate 那套)理论上有优势,但 3600 张直接上旋转框标注成本太高,绝大多数落地项目先用水平框把流程跑通,再谈要不要升级标注方案。
标注工具方面,我一般用 labelImg 直接出 VOC XML,再用现成脚本转 YOLO txt;碰到遮挡严重的框就用 labelme 画多边形,导出后再转矩形,比硬拉矩形框准。目标检测常用标注工具就这两档够了,别一上来就上重型标注平台,3600 张的体量手工刷一遍反而更快。刷的过程中顺手记录每类目标的典型遮挡程度,后面调损失函数权重时用得上。
3. 把3600张跑成yolo训练:划分脚本、YAML与最小命令
数据体检过了,接着就是划分和跑通。这一章解决两个问题:怎么在不碰官方测试集的前提下做出稳定的验证集,以及怎么用最短的命令把 yolov8 训练流程跑起来。
3.1 官方测试集先冻结,再按7:1.5:1.5做分层划分
压缩包带测试集是个好习惯,但也是最大的陷阱:很多人顺手把 test 目录也 glob 进训练,最后指标虚高。我的做法是官方 test 目录连动都不动,从剩余数据里再切出 15% 做验证集,用固定随机种子保证每次结果可复现。
import os
import random
from collections import Counter
random.seed(42)
label_root = "YOLO/labels"
names = [os.path.splitext(f)[0] for f in os.listdir(label_root) if f.endswith(".txt")]
random.shuffle(names)
n = len(names)
cut1, cut2 = int(n * 0.70), int(n * 0.85)
train, val, test = names[:cut1], names[cut1:cut2], names[cut2:]
def class_counter(name_list):
cnt = Counter()
for name in name_list:
with open(os.path.join(label_root, name + ".txt"), encoding="utf-8") as f:
for line in f:
cnt[int(line.split()[0])] += 1
return cnt
print("train 目标数:", dict(class_counter(train)))
print("val 目标数:", dict(class_counter(val)))
print("test 目标数:", dict(class_counter(test)))
随机种子固定成 42,同一份数据任何机器上重跑出来划分一致。7:1.5:1.5 的比例在 3600 张上意味着验证集约 540 张,够用于早停和调参,剩下的约 3060 张喂给训练。数据划分完成的标准不是数量对,而是打印出来的 val 类别分布:如果某个类别在 val 里目标数小于 5,就把 train 里包含该类别的图片挪几张进 val,直到分布不再有空洞。另外,划分完顺手把“图片数量和标注数量”对一遍:train 里有多少张 jpg,就应该有多少个 txt,数量不一致说明有图没标或有标没图,这种错位在双格式拼接时经常发生。
划分结果怎么用有两种方式。一种是物理移动文件,另一种更轻,直接生成图片路径清单,Ultralytics 支持在 YAML 里引用 txt 路径:
find YOLO/images/train -name "*.jpg" > train.txt
find YOLO/images/val -name "*.jpg" > val.txt
find YOLO/images/test -name "*.jpg" > test.txt
我倾向用路径清单,因为训练过程中反复调整划分不用搬文件,改一次清单重跑就行,也更容易在脚本里记录每次划分用的版本。
3.2 yolov8训练自己的数据集:YAML与最小命令
yolov8 训练自己的数据集,核心就一个 YAML 加一条命令。YAML 里路径、目录和 10 个类名按下面对齐,注意 names 的索引必须和 txt 第一位的类别索引一致,这是数据能跑起来的前提:
# xray.yaml
path: /home/user/XrayData
train: train.txt # 或 YOLO/images/train
val: val.txt
test: test.txt
names:
0: gun
1: knife
2: scissors
3: pliers
4: wrench
5: lighter
6: spray_bottle
7: battery
8: liquid
9: firecracker
然后终端里执行:
cd /home/user/XrayData
yolo detect train model=yolov8s.pt data=xray.yaml \
epochs=120 imgsz=960 batch=16 device=0 \
project=runs/xray name=baseline
model=yolov8s.pt 指定从预训练权重起步,第一次运行会联网下载 yolo预训练模型,内网环境就手动把 .pt 文件放到当前目录再指定相对路径。 imgsz=960 是安检场景的关键选择,往下看 4.1 会展开; batch=16 在 16G 显存的卡上足够,显存紧张就降 batch 而不是降 imgsz。跑起来之后先确认终端里打印的类别数和你的 classes.txt 一致,再去盯指标。训练中途断了不用慌,Ultralytics 会在项目目录下存 last.pt 和 best.pt,续跑加 resume=True 就行。V100 这类卡上,3600 张跑 120 轮大概是几小时的量级,别用 CPU 试,会等到怀疑人生。
3.3 增强参数:旋转、贴片与克制着调的颜色
3600 张属于中小体量,增强做得好不好直接决定过拟合程度。X 光场景的增强和自然图像有三个明显差异:可以大方旋转,不要乱动颜色,小目标类需要额外贴片。
yolo detect train model=yolov8s.pt data=xray.yaml \
epochs=120 imgsz=960 batch=16 \
degrees=45 translate=0.1 scale=0.4 fliplr=0.5 flipud=0.2 \
hsv_h=0.01 hsv_s=0.2 hsv_v=0.2 \
close_mosaic=10
degrees=45 配合水平垂直翻转,等效覆盖任意角度,比直接调成 90 更稳,因为 90 度步进会让某些姿态出现重复。 hsv_h 压到 0.01 是我反复踩坑后的取值,原因见 5.4; hsv_s 和 hsv_v 控制在 0.2 以内,只做微调。 close_mosaic=10 表示最后 10 轮关闭马赛克增强,让模型从“见过各种拼贴”回到“只看真实完整图”,对收敛末尾的稳定性帮助明显。稀有小类(比如打火机)如果目标太少,额外用 copy-paste 把它贴到没有该类的包图上再补一份样本,比单纯靠 mosaic 拼图更有效,因为贴片保持了目标真实纹理和 X 光下的材质色。如果你用的是 mmdetection 这类框架,增强逻辑一样,只是参数写进配置文件,核心原则不变:几何往大了做,颜色往小了做。
4. X光安检检测的三个必调参数:分辨率、损失函数与BN崩溃
训练跑通只是起点,指标能不能上去取决于几个参数。这一章讲 X 光安检场景下我每次都会动的三样东西:输入分辨率、损失函数权重、以及小数据集上最容易翻车的 BN 崩溃。
4.1 输入分辨率:640起步,960是安检场景的常用档
自然图像检测很多场景 640 就够,但 X 光安检图里违禁品往往只占包的一小块,刀、打火机这类目标在 640 分辨率下只有十来个像素,特征直接被下采样吃掉。先量化确认一下目标的尺度:
import glob
areas = []
for txt in glob.glob("YOLO/labels/val/*.txt"):
for line in open(txt):
p = line.split()
if len(p) == 5:
areas.append(float(p[3]) * float(p[4]))
avg = sum(areas) / len(areas)
print("目标平均占图比例:", round(avg, 4))
print("中位数:", round(sorted(areas)[len(areas)//2], 4))
这里用归一化宽高相乘得到面积占比。如果均值小于 0.03,说明目标普遍偏小, imgsz=640 会漏掉相当一部分小目标,直接上 imgsz=960 。背后的道理是下采样倍率:YOLO 系列输出的特征图一般是输入的 1/32,640 时是 20×20 的网格,960 时是 30×30,网格更密,小目标的中心点更容易落在某个格子内并被分配为正样本。显存不够就把模型从 yolov8s 降到 yolov8n,保持 960 分辨率,而不是降分辨率保大模型——在安检场景里,分辨率对 mAP 的边际收益通常比模型容量更明显。上 1280 确实还能再涨一点,但训练速度和部署帧率都会明显下降,我一般只在目标平均占比低于 0.02 时才考虑。
4.2 yolo损失函数:box、cls、dfl三个旋钮怎么调
yolo 的训练损失是三个分量的加权和:box 损失负责框回归,算的是预测框和真实框的几何误差;cls 损失负责类别打分,本质是带聚焦机制的二分类损失;dfl 损失负责框分布的聚焦程度,影响小目标的定位精度。Ultralytics 把这三个权重暴露成命令行参数,默认大概在 box=7.5、cls=0.5、dfl=1.5 附近,但 X 光场景要往以下方向改:
yolo detect train model=yolov8s.pt data=xray.yaml \
epochs=120 imgsz=960 batch=16 \
box=8.0 cls=0.8 dfl=1.8
第一个改动方向是提高 dfl。安检图里目标互相叠,框边界不明确,dfl 权重高一点能让模型对小偏移更敏感,定位精度提升明显。第二个方向是提高 cls。10 个类别里小类数量少,类别损失权重不足时,稀有类会倾向被预测成大类,把 cls 从 0.5 提到 0.8 能缓解,但别超过 1.0,否则定位损失被压制,框质量会下降。调整节奏是每次只动一个旋钮,幅度不超过 20%,跑完一轮看 val 的 loss 分量曲线再决定下一步。三个一起乱拧等于什么结论都得不到,还会浪费时间重训。
4.3 yolo训练中bn崩溃:3600张小数据集的nan从哪来
训练中 loss 突然变 nan、mAP 归零、BN 层的 running_mean 发散,这个现象在 3600 张的小数据集上比大样本数据集常见得多。机理不复杂:batch 太小或学习率太高时,BN 在一批样本上统计的均值和方差抖动过大,running stats 被带偏,下一层输入分布彻底乱掉,损失直接浮点溢出。前兆通常是前几个 epoch 的 loss 剧烈跳动,然后某一步直接变 nan。
常见的止损和预防做法是三件事连做:
yolo detect train model=yolov8s.pt data=xray.yaml \
epochs=120 imgsz=960 batch=16 \
lr0=0.002 lrf=0.01 warmup_epochs=10 freeze=10
第一,batch 至少 8,最好 16 以上。显存不够就开梯度累积,别真用 batch=2 硬扛。第二,把 lr0 从默认的 0.01 降到 0.002 附近, warmup_epochs 提到 10,让 BN 统计量在前期慢慢热起来。第三, freeze=10 冻结前 10 层主干,既省显存又让浅层特征先稳定下来,训练中期再解冻。这套组合在这类小数据集上几乎必用。真的出现 nan 也别慌,从 last.pt 续跑,lr0 降一个数量级,比从头再来省时间。
5. X光安检数据集训练避坑:最容易翻车的5个地方
前面讲的是怎么做,这一章讲不能怎么做。以下五条都是我在安检数据集上真实踩过的坑,按“现象 → 原因 → 解决”写,每条都能直接对号入座。
5.1 官方测试集被顺手并进训练
现象:训练时 mAP 一路涨到 0.95,换到现场实拍图性能明显缩水。
原因:数据集的 test 目录就在 YOLO/images 下,很多人用 glob("YOLO/images/*.jpg") 全量加载,测试集被当成训练集用过,模型相当于提前看到了答案。
解决:训练前先数数。 yolo detect train 启动时打印的图像数量如果接近 3600,而你的划分应该是一万两千多的一半,数量对不上就停下来查。用路径清单方式训练时,确认 train.txt、val.txt 里没有任何一行指向 images/test 目录。
5.2 VOC转YOLO的坐标公式写错
现象:loss 正常下降,但可视化检测框全偏到物体一侧,AP 偏低。
原因:把 VOC 的 xmin 除以图像宽度当成中心点 x 用了。XML 给的是左上角和右下角,YOLO 要的是中心点加宽高,公式差一步结果天差地别。
解决:转换时按下面这个函数来,别自己缩写:
def voc2yolo(xmin, ymin, xmax, ymax, W, H):
cx = (xmin + xmax) / 2.0 / W
cy = (ymin + ymax) / 2.0 / H
w = (xmax - xmin) / W
h = (ymax - ymin) / H
return cx, cy, w, h
转完强制做一步可视化抽查,随机抽 20 张图把框画回去,人眼扫一遍比任何数值检查都直观。这一步建议直接写进数据校验流程,而不是临时做一次。
5.3 稀有小类从未出现在验证集
现象:训练 100 轮,某个类的 AP 一直是 0,训练损失却正常。
原因:随机划分下,样本量最小的类可能全部落进训练集,验证集里一个该类目标都没有,验证指标当然恒为 0。我见过有人调了一周损失函数,最后发现是划分的锅。
解决:划分后打印 val 的类别分布,发现空洞就从 train 往 val 挪图。挪的时候连图带 txt 一起挪,别只挪标注。如果小类整体样本都很少,优先在 train 里做该类的 copy-paste 增强,而不是靠挪图制造假平衡。
5.4 颜色增强把材质信息洗没了
现象:训练指标还行,但上真实安检机后,刀具、枪支在橙色有机物背景下大量漏检。
原因:默认的 hsv_h=0.5 会把 X 光伪彩色随机旋转,有机物从橙色变成蓝色,模型再也学不到“橙色=有机物=背景干扰”这条稳定线索。X 光图像里颜色是材质轴,不是装饰轴,自然图像那套大幅度颜色抖动在这里是毒药。
解决:把 hsv_h 压到 0.01,hsv_s 和 hsv_v 不超过 0.2。如果还是漏,直接全部关掉颜色增强,只保留几何增强,损失一点数据量换稳定性。这条也是 3.3 里参数设置的依据。
5.5 离群目标框:超大框与退化点框
现象:训练中某个类 AP 突然下滑,查看标签发现一张图上有个框占了大半个包,或者宽高只有 0.001。
原因:原始标注里把整个行李包误标成了违禁品,或者边界框在截断时被压成一条线。这种离群框会把 anchor 分配和损失计算都带偏,轻则浪费训练资源,重则让该类的定位头学歪。
解决:在数据体检脚本里加两条过滤:过滤归一化宽或高小于 0.005 的框(点框),过滤面积占比大于 0.8 的框(整包框)。过滤前先可视化确认,别误删真实的大目标,比如底座展开的大型喷雾罐本身可能就很大。
6. 部署前验证:先读yolo混淆矩阵,再做ONNX导出
训练结束不等于可以做验收,更不等于能上线。最后这一章讲我部署前必做的两件事:用混淆矩阵确认漏检到底出在哪一类,以及用固定测试集加 ONNX 导出把模型从训练框架里固化出来。
6.1 先读混淆矩阵,再看平均mAP
平均 mAP 会骗人:10 个类别里 9 个很高的平均值,会被一个几乎全漏的类别拖成平庸,反之也会被两三个优势类抬得很高。所以在专用测试集上跑完验证后,第一眼看 confusion_matrix.png :
yolo detect val model=runs/xray/baseline/weights/best.pt \
data=xray.yaml imgsz=960 split=test
跑完在 runs/xray/baseline/ 下会生成混淆矩阵图。重点看主对角线之外哪一格最亮:在 X 光图里,剪刀和钳子、刀和笔形物体经常互相混。定位到了混淆对,下一步才有方向——是补该类样本、调 cls 权重,还是在后处理里按类别设不同置信度阈值。单纯盯着总 mAP 调参,等于闭着眼睛开车。
6.2 ONNX导出与分辨率二选一
验证通过后,导出成 ONNX 到目标设备上测帧率。安检机边缘设备的算力通常比训练卡低一个量级,这一步必须做,不能在训练框架里跑着测性能,标准做法是导出后套上后处理再到真机测:
yolo export model=runs/xray/baseline/weights/best.pt \
format=onnx imgsz=960 opset=12
导出后在同一台目标设备上分别跑 640 和 960 两个分辨率,记录 mAP50 与 FPS,二选一。我的经验是安检场景里 640 到 960 的 mAP 提升通常有 2 到 3 个点,而帧率可能掉三成;如果现场对实时性要求高,优先保 960 的检出能力,因为漏检一次的代价远大于多等 0.1 秒。我现在的习惯是,拿到任何一份 X 光安检目标检测数据集,都先跑一遍第 2 章的校验脚本再谈训练,改一次增强就把 val 类别分布重新打印一次,这个习惯帮我避开了大半无意义的调参。数据这个黑匣子里的灯点亮了,模型才不容易翻车。希望帮到你。
更多推荐



所有评论(0)