X光安检目标检测数据集与YOLO训练实战:VOC转格式、小目标避坑指南
简介X光安检目标检测数据集面向目标检测算法研究与安检应用开发场景收录3600张真实X光安检图像覆盖打火机、压力罐、刀、剪刀、充电宝、打火机油、手铐、弹弓、鞭炮、指甲油等10类物品总标注框达9042个。数据采用Pascal VOC与YOLO两种主流标注格式由labelImg工具标注格式规范统一可直接用于YOLOv5/YOLOv8、SSD、Faster R-CNN等常见框架训练无需额外转换。数据已划分测试集便于直接评估模型泛化效果。压缩包共2000个文件以XML标注文件为主并附带TXT使用说明整体约793.61MB。各类别框数分布清晰如刀2858框、打火机2529框等便于按需筛选或做类别均衡实验。目前已有241人学习下载适合需要真实安检图像验证检测效果的开发者与科研人员快速入手。1. X光安检目标检测数据集到底解决了什么3600张图、双格式、10类别意味着什么做安检机图像识别的人都有一个共同感受跑模型第一步卡住的往往不是算法而是标好的数据从哪儿来。自己标X光图像成本极高得对着显示器看被遮挡的物体轮廓一个刀具在包裹里可能只露出一半边缘标注员稍微手抖框就偏了。所以当看到这个X光安检目标检测数据集VOCYOLO格式3600张10类别含测试集.zip时第一反应是它把最费人的标注环节替你省掉了而且VOC和YOLO两种格式都给好了不用再写一次格式转换才能喂给训练脚本。3600张对安检这个细分场景不算少10个类别也覆盖了日常安检最常见的违禁品和日用物品最关键的是它自带测试集这意味着你拿到手就能直接跑一个公平的基准分数而不是自己再切一份验证集。这个数据集适合两类人一类是要做安检机识别的算法工程师想快速跑通YOLO训练流程看看预训练权重在自己业务图上的基线效果另一类是搞目标检测模型改进的研究者需要一份干净、公开结构、带固定测试划分的数据集来对比改进前后的mAP变化。压缩包里的VOC格式解决的是可读性问题XML标注用记事本就能看YOLO格式解决的是效率问题ultralytics的训练脚本可以直接消费txt标注不需要中间转换。下文就把这个数据集从解压到训练、再到评估的完整路径拆开讲。2. 类别构成与双格式标注VOC和YOLO分别怎么读、怎么互相转换2.1 10个类别的典型构成与安检场景的检测难点这个数据集标注的10个类别不同版本可能有差异但安检X光场景里高频出现的违禁品和日常物品就那么几种枪支、刀具、打火机、剪刀、液体瓶、充电宝、手机、扳手、钳子、电池。拿到压缩包后第一件事不是解压跑训练而是打开classes.txt或者label_list.txt看类别清单确认类别顺序。这个顺序决定了YOLO训练时每个txt文件里第一个数字的含义顺序错一位整个模型就白训练。安检X光图像和自然图像最大的不同在于物体互相遮挡严重——水杯里插着刀充电宝底下压着打火机密集包裹场景里目标之间几乎没有干净间隔。这给目标检测带来的影响是标注框之间的IoU常常很高NMS在推理时容易把相邻的两个目标合并成一个。另一个典型难点是目标尺度极端打火机、刀片在整张602×402的X光图上只占40×20像素属于典型的小目标YOLO系列模型默认的anchor或anchor-free头在小目标上表现不如专门优化过的版本。所以10个类别里小物件类别打火机、刀片和大物件类别枪支、液体瓶的检测难度不在一个量级上评估时最好分开看每一类的AP。2.2 VOC格式的目录结构与XML标注字段解析VOC格式在这个压缩包里通常表现为JPEGImages文件夹存原图、Annotations文件夹存XML标注有的版本还会带ImageSets/Main目录存放train.txt、val.txt、test.txt的图片名清单。用VOC格式的好处是标注信息完整每个XML文件不仅记录每个目标的坐标框还记录了图片尺寸、目标类别名、甚至难例标志。先用Python脚本解析一个XML文件能看到结构如下import xml.etree.ElementTree as ET tree ET.parse(Annotations/000001.xml) root tree.getroot() # 图片尺寸信息 size root.find(size) print(width:, size.find(width).text, height:, size.find(height).text) # 遍历每个目标 for obj in root.iter(object): name obj.find(name).text bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) print(name, (xmin, ymin, xmax, ymax))这段代码用ElementTree解析XML先读出整张图的宽高再循环取出每个目标的类别和边界框坐标。xmin、ymin是左上角坐标xmax、ymax是右下角坐标单位是像素。XML里的bndbox是绝对坐标不需要归一化这点和YOLO格式完全不同。观察XML时有两点要留意。第一有些工具生成的XML里坐标是浮点数比如xmin120.5在做整数化处理时四舍五入可能让框偏移一两个像素对检测任务影响不大但如果后续要做像素级分割就要保留浮点。第二如果XML里出现difficult1的标注说明这个目标极其模糊甚至被遮挡到人都不太确定训练时要不要过滤掉取决于你的业务场景安检场景里这种难例反而最有价值不该过滤。2.3 YOLO格式的txt标注归一化坐标与类别idYOLO格式的标注存放在labels文件夹下每张图的标注是一个同名txt文件每行代表一个目标格式是class_id center_x center_y width height其中中心点坐标和宽高都是相对图片宽高的归一化数值范围0到1。举个例子一个框在602×402的图上左上角(150, 100)右下角(250, 220)则中心点是(200, 160)宽度100高度120归一化后分别是0.332、0.398、0.166、0.299。从VOC转换到YOLO的脚本几乎是每个目标检测项目都要写一遍的标准工具这里给出一个可以直接用的版本import os import xml.etree.ElementTree as ET # 类别顺序必须和后续训练yaml里的names完全一致 class_mapping { gun: 0, knife: 1, lighter: 2, scissors: 3, liquid: 4, powerbank: 5, phone: 6, wrench: 7, pliers: 8, battery: 9 } def convert_annotation(xml_path, target_dir): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) txt_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(target_dir, txt_name), w) as f: for obj in root.iter(object): cls obj.find(name).text if cls not in class_mapping: continue # 跳过不在映射表里的类别 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) # 防止越界修正后面避坑章节会专门讲 xmin max(0, min(xmin, img_w - 1)) xmax max(0, min(xmax, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) ymax max(0, min(ymax, img_h - 1)) if xmax xmin or ymax ymin: continue cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h cls_id class_mapping[cls] f.write(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n)这段转换脚本里有两个细节值得说。第一个是类别映射表class_mapping它决定了VOC里的字符串类别名转成YOLO的整数id。id从0开始和后续训练yaml文件里的names列表顺序一一对应如果names写成了[knife, gun, ...]那所有box都会对错类别。第二个是越界修正原XML里偶尔会有标注框超出图片边界的情况直接除以宽高后宽高比可能大于1训练时模型会学到错误的目标尺寸分布所以这里强制把坐标clip回[0, img_w-1]范围内过滤掉宽度或高度已经为负的坏框。3. 用这个数据集跑通YOLO训练目录划分、训练命令与关键参数3.1 从压缩包到YOLO训练目录目录结构与train/val/test划分VOC和YOLO格式都给好的数据集省掉了转换步骤但目录结构不一定直接满足ultralytics YOLO的要求。常见做法是重新组织目录让images和labels分开放且图片和标注文件的文件名一致。推荐的文件布局如下dataset/ ├── images/ │ ├── train/ # 约2500张 │ ├── val/ # 约500张 │ └── test/ # 约600张仅做最终评估 └── labels/ ├── train/ ├── val/ └── test/这里有一个容易踩的细节是测试集的位置。很多初学者把test合并进val用这在训练阶段看没问题但会污染模型选型决策因为你在重复用同一批数据调参最终mAP会虚高。这个数据集合里既然带了test集划分就把它留到最后训练和调参只看验证集分数。划分工作可以用脚本完成。如果压缩包里已提供ImageSets/Main下的train.txt、val.txt、test.txt读取文本后批量移动图片和标签即可import os import shutil def split_dataset(img_root, label_root, imgset_dir, dest_root): # 分别读取三个划分的图片名列表 for split_name in [train, val, test]: list_path os.path.join(imgset_dir, f{split_name}.txt) with open(list_path) as f: names [line.strip() for line in f if line.strip()] os.makedirs(f{dest_root}/images/{split_name}, exist_okTrue) os.makedirs(f{dest_root}/labels/{split_name}, exist_okTrue) for name in names: src_img os.path.join(img_root, f{name}.jpg) src_lbl os.path.join(label_root, f{name}.txt) if os.path.exists(src_img): shutil.copy(src_img, f{dest_root}/images/{split_name}/{name}.jpg) else: print(fmissing image: {src_img}) if os.path.exists(src_lbl): shutil.copy(src_lbl, f{dest_root}/labels/{split_name}/{name}.txt) else: print(fmissing label: {src_lbl}) split_dataset(JPEGImages, labels, ImageSets/Main, dataset)这段脚本的核心是按txt列表做复制而不是移动保留原始压缩包结构万一划分出错还能重来。复制过程中打印missing文件很重要因为一个缺失的标签会导致训练时该图片被跳过而你不会第一时间发现。执行完脚本后用find命令统计各子目录文件数确认train/val/test三种划分的数量比例大致符合8:1:1到7:2:1之间的分布如果某个划分数量为0则说明txt列表路径或图片后缀名写错了。3.2 用Ultralytics YOLO训练的最小命令与yaml配置目录整理好之后需要写一个数据集描述yaml文件告诉训练脚本图片路径、标签路径和类别名。常见做法是在dataset目录下新建xray_data.yaml# xray_data.yaml path: /absolute/path/to/dataset train: images/train val: images/val test: images/test names: 0: gun 1: knife 2: lighter 3: scissors 4: liquid 5: powerbank 6: phone 7: wrench 8: pliers 9: battery这里的path建议写绝对路径相对路径在ultralytics里会相对于当前工作目录解析很容易因为启动位置不同而找不到数据集。names的索引顺序必须与labels里txt文件每行开头的class_id一致不能只看类别名文字一致就以为自己配对了要抽查几个labels文件确认id对应的名称。比如打开labels/train/000001.txt第一行若是2 0.356 0.412 0.087 0.051对照yaml应该知道这是lighter打火机如果实际是scissors就要回头查转换脚本生成的顺序。训练命令在不同YOLO版本下略有不同当前主流用法是统一命令行接口yolo detect train \ -model yolov8n.pt \ -data xray_data.yaml \ -imgsz 640 \ -batch 16 \ -epochs 100 \ -cache True \ -project runs/xray \ -name baseline_n这条命令用yolov8n预训练权重初始化在640分辨率下训练100轮。cache True把图片一次性加载进内存3600张图大概占几个GB内存能显著减少每轮读取磁盘的时间。project和name两个参数把训练输出隔离到独立目录跟多个实验对比时不会被覆盖。3.3 imgsz、batch、epochs三个参数在安检场景的具体取值建议imgsz输入分辨率是安检场景里最值得调的参数。3600张图本身分辨率可能在600到1000像素宽如果直接缩到640训练很多小目标比如打火机可能只剩下不到30像素。我的做法是先试640跑通流程再用imgsz960甚至1280做对比实验。大分辨率会让训练显存占用翻倍batch必须相应减半但安检小目标场景下这个trade-off通常是值得的。batch的选择取决于显存大小。以单卡12GB显存为例yolov8n在640分辨率下batch16没问题换yolov8m就要降到batch8或更低。batch过小会导致BN统计量不稳定表现为验证集loss在第10轮后仍然剧烈震荡此时优先减半imgsz而不是硬抗。epochs方面3600张数据规模不算大100轮足够收敛通常在60到80轮时mAP50增长曲线就趋于平缓。关键在于开启早停机制ultralytics默认patience是50轮但对这个小数据集建议设成20避免后期无效训练浪费时间yolo detect train \ -model runs/xray/baseline_n/weights/best.pt \ -data xray_data.yaml \ -imgsz 960 \ -batch 8 \ -epochs 100 \ -patience 20 \ -name scale_960后半程训练直接加载baseline_n的best.pt继续训练效果通常比重新预训练更快达到更好的mAP。这种做法在迁移学习里等价于先在低分辨率下学整体特征、再在高分辨率下细化学小目标对X光安检这种小目标密集的数据集会有明显收益。4. X光安检数据集训练的5个常见坑现象、原因、解决4.1 类别标签错位导致训练loss不降现象训练第一个epoch的loss正常下降但从第5轮开始box_loss明显升高最终mAP50只有0.3左右查每个类别的AP发现gun和knife几乎为零。原因训练yaml里的names顺序和labels里的class_id对不上。比如转换脚本里把knife放在id1但yaml里names[1]写的是gun模型实际在学把刀的框预测成枪的类别两个容易混淆的类别互相竞争loss自然降不下去。解决训练前写个几行的校验脚本读取labels里每个txt的第一列数字做分布统计再对照yaml的names确认每个id都有合理数量的样本且类别名和业务预期一致。这一步花两分钟能省掉一整轮白跑的10个小时。4.2 小目标漏检严重刀具打火机在图上只占几十像素现象验证集整体mAP50有0.7但单独看lighter和battery两个类别的AP50不到0.3预测框要么完全漏掉要么框偏到只包含物体的一半。原因安检X光图里小目标占比高YOLO在640分辨率下的特征金字塔对小目标的召回本身就有限。再加上原始图片分辨率高缩放到640后一个小目标可能只剩8×8像素经过几次下采样后在深层特征图上已经无法分辨。解决优先提升输入分辨率到960或1280并把训练和验证的imgsz设为一致避免训练时看大图、验证时看小图导致的性能落差。如果显存不够就在模型层面换成P2输出层加强的小目标检测头或者使用SAHI切片推理。数据增强方面把mosaic关闭或调低到0.5因为拼接图会进一步缩小单个目标的像素占比。4.3 空标注文件与越界框导致训练中断现象训练到第30轮突然报错提示某个jpg对应的txt内容为空或者gt_bboxes里有坐标值为负数程序直接退出。原因数据集中存在少数图片标注为空比如纯背景的X光图转换脚本如果不生成空txtultralytics训练时读到图片却找不到标签就会报错。越界框则来自原始XML标注不规范xmax写成了大于图片宽度的数值转换脚本如果不做clip操作边界框归一化后宽高比异常训练时loss会变成nan。解决转换脚本里必须对空标注生成一个空txt文件内容为0字节这样训练逻辑会把这张图当作无目标样本处理跳过损失计算。越界修正的逻辑已经在第二章的转换脚本里给出核心是强制把坐标clip回[0, img_w-1]并过滤width或height非正的框。处理完之后批量统计txt的行数逐张检查是否存在空文件用find . -name *.txt -size 0快速定位再人工确认。4.4 类别样本严重不平衡液体瓶样本不到打火机的三分之一现象gum和liquid这类大件物品AP很高knife和lighter的AP中等powerbank和battery的AP低到不可用。看训练日志中每个类别的样本数发现相差超过一个数量级。原因安检场景本身就有长尾分布随手放进包里的充电宝比刻意藏匿的刀具常见得多数据集虽然凑了10个类别但各框数量不平衡。样本量少的类别在训练时贡献的梯度占比太小模型倾向于把它们预测成更常见的背景或相邻类别。解决最简单的方式是启用ultralytics的ClassBalancedDataset采样策略它会按类别频率重新采样构成每个batch。如果效果不够对样本少的类别做离线增强复制粘贴同一类目标到同一张图里或者使用mosaic增强并确保该类目标占比较高。评估时不要只看整体mAP要根据AP per class决定哪些类别真正达到了可用水平否则部署后会在稀有类别上频繁漏报。4.5 测试集与训练集重叠造成mAP虚高现象最后一轮评估test集时mAP50达到0.85但部署到现场新拍的X光图上效果断崖式下跌只有0.4不到。原因压缩包里的test.txt划分被误用进了训练阶段做数据增强或者模型选择又或者数据整理时用glob把所有图片全部复制到train目录导致测试集图片参与训练。另一个隐蔽情况是test和train里的图片可能来自同一个多帧序列相邻帧几乎一样相当于测试集信息提前暴露给了模型。解决训练全过程不要碰test目录只在最终评估推理时使用。如果怀疑数据集中有重复或近重复图片可以用文件名哈希检查完全重复的图片再用感知哈希算法检查近重复。做模型选型时以val集为准test集只在确认最终模型后跑一次跑完把结果记录下来不要反复用test集调参否则测试分数会逐渐失真。5. 用这个数据集做一次完整验证从可视化检查到低阈值评估5.1 把标注画回原图确认每一行txt对应正确的物体训练前用OpenCV把YOLO格式的标注画回图片这是最直观且最省事的质量检查手段。读取labels下的txt文件把归一化坐标乘以图片宽高得到像素坐标再画矩形框和类别名import cv2 import numpy as np labels_dir dataset/labels/val images_dir dataset/images/val class_names [gun, knife, lighter, scissors, liquid, powerbank, phone, wrench, pliers, battery] for txt_name in [000512.txt, 000513.txt]: base_name txt_name.replace(.txt, ) img cv2.imread(f{images_dir}/{base_name}.jpg) with open(f{labels_dir}/{txt_name}) as f: for line in f: parts line.strip().split() cls_id, cx, cy, w, h map(float, parts) h_img, w_img img.shape[:2] x1 int((cx - w / 2) * w_img) y1 int((cy - h / 2) * h_img) x2 int((cx w / 2) * w_img) y2 int((cy h / 2) * h_img) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[int(cls_id)], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(fcheck_{base_name}.jpg, img)先把坐标转换回绝对像素再做画框结果显示如果类别名出现文字和物体明显不符、框明显包裹了多个物体、或者框大部分落在图片外就说明前面的转换或划分环节出了问题。做可视化抽查时不要只挑前几个文件重点看test目录里最低序号和最高序号的图片因为数据集的边界文件最容易出现标注错误。5.2 用val集跑mAP50与低置信度recall评估验证脚本用ultralytics自带的val命令但评估时把conf低阈值打开才有意义。安检场景里漏检一个打火机的代价远大于误报一个所以不能只看默认mAP50要看recall在低置信度下的变化yolo detect val \ -model runs/xray/baseline_n/weights/best.pt \ -data xray_data.yaml \ -imgsz 640 \ -conf 0.05 \ -iou 0.5 \ -save_jsonconf设成0.05会让模型输出所有置信度高于0.05的框recall指标会明显高于默认0.25阈值下的值。如果模型在这个低阈值下依然拉不回某些类别说明该类别的特征学习本身就有问题调后处理参数救不回来。save_json会把每张图的详细预测结果写成JSON文件之后可以用脚本分析哪些图片漏检最多、漏检集中在哪几个类别为后续针对性加强数据提供依据。5.3 值得投入的方向判断与数据补充建议跑完上面几步你就知道这个数据集的下限在哪模型训练流程是否通顺、每个类别的检测基线是多少、在低置信度下能找回多少漏检目标。如果val集的低置信度recall能达到0.85以上但mAP50只有0.6说明模型检出了目标但定位精度不够优先调anchor或回归损失权重如果recall本身只有0.5说明特征学习不足优先加高分辨率训练和数据增强。往这个方向再做一轮imgsz960的实验对比两轮结果基本就能判断这个数据集值不值得继续投入时间微调。我个人处理这类安检X光数据集的习惯是先跑通baseline拿到每一类的AP50分布再针对AP最低的两个类别做补充标注或数据增强。这个数据集的测试集可以作为固定的benchmark后续换模型、换预处理、换损失函数时用同一套评估标准对比。希望这样的验证流程能帮你在X光安检检测这个方向上少走弯路后面真正拿到现场数据做迁移时也能复用这套评估方法。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →