
简介面向YOLO系列目标检测训练的X光安检图像数据集收录1000张真实安检场景的高质量图片数据场景丰富由LabelImg人工精细标注标注框质量可靠同步提供VOCxml、COCOjson、YOLOtxt三种主流标签格式分别存放于独立文件夹可直接导入现有YOLO训练流程省去格式转换与二次处理的麻烦。压缩包内共2000个文件以xml标签文件与txt标签文件为主体另含html格式的环境搭建与训练案例教程、py格式的数据集划分脚本以及yaml格式的模型配置说明整体约105.65MB目录划分清晰便于按需检索取用。随包附赠Linux与Windows双平台的YOLO环境搭建教程及训练案例指导可按案例修改后应用于自己的数据集同时提供训练集、验证集、测试集三种划分脚本便于灵活调整数据比例。该资源目前已有254人学习下载适合需要快速上手YOLO安检目标检测、兼顾标注规范与训练实操的研究者、学生及竞赛团队。1. X光安检目标检测为什么通用YOLO预训练权重在这里翻车X光安检目标检测和日常的街景检测完全是两码事。行李箱里塞满衣物、充电器、金属支架层层叠叠互相遮挡刀刃换个角度就成了一条亮线液体在灰度图像里和玻璃、陶瓷几乎一个颜色。通用YOLO模型在自然图像上跑得不错一上安检机就原形毕露——这时候你需要的是专门在X光图上训练过的模型而训练的起点就是一套干净的数据集图片、三格式标签、划分脚本和训练教程一起打包的rar方案图片1000张不算多但足够把流程跑通VOC、COCO、YOLO三种标注格式让你不管用什么框架都能直接开工。这篇文章就围绕这套方案的落地展开三种格式怎么互转、划分脚本怎么用、训练参数怎么定、X光场景会踩哪些坑。准备做安检智能判图、或是拿X光图练手的算法工程师和研究生都适用纯小白照着走也能跑通。2. 三种标签格式怎么选VOC、COCO与YOLO的坐标换算2.1 三种标注格式的“脾气”不一样一个绝对像素一个中心归一化先说结论VOC、COCO、YOLO这三种格式本质是同一批框的不同写法差别集中在两点——坐标是绝对像素还是归一化比例、框是用四角表达还是用中心点加宽高表达。VOC格式用XML文件每个文件对应一张图。框的位置写在bndbox里给的是xmin、ymin、xmax、ymax四个绝对像素值左上角原点往右往下增大。COCO格式用一个JSON文件装下整个数据集标注数组里每条记录的bbox是[x, y, width, height]也是绝对像素但表达方式是左上角坐标加上宽高。YOLO格式最直接一张图一个txt文件每行五个数class_id x_center y_center width height其中四个坐标值全部除以图片宽高做了归一化取值在0到1之间。实际项目里最常见的组合是从VOC的XML转到YOLO的txt。因为很多公开数据集和标注工具比如LabelImg默认输出VOC格式而YOLO训练时需要的是txt。换算公式不复杂x_center (xmin xmax) / 2 / img_width y_center (ymin ymax) / 2 / img_height box_width (xmax - xmin) / img_width box_height (ymax - ymin) / img_height注意COCO的宽高是直接用xmax - xmin算出来的不需要除以2。三个格式的索引顺序也不一样YOLO是class_id在前COCO的JSON里类别要单独查categories表VOC的类别名直接写在XML里。我见过太多人转换时把x_center和box_width搞混记住一句话YOLO的前两个数是中心点后两个数是宽高谁除以2这件事最容易翻车。2.2 VOC转YOLO解析XML并完成归一化换算如果你拿到的数据包里只有VOC的XML或者你想自己标一批数据这个脚本可以直接抄。它遍历指定目录下所有XML解析出图片尺寸和每个目标的类别名、边界框然后写出对应的YOLO txt文件。import os import xml.etree.ElementTree as ET # 类别清单顺序决定了 class_id不要随意变动 CLASS_NAMES [knife, gun, lighter, scissors, bottle, other] def voc_xml_to_yolo_txt(xml_path, txt_out_path, class_names): tree ET.parse(xml_path) root tree.getroot() # 图片尺寸必须从 XML 里读不要自己去 opencv 读避免图片被压缩过 img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in class_names: continue # 跳过未定义类别 cls_id class_names.index(cls_name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 坐标换算并做一次越界裁剪 x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(txt_out_path, w) as f: f.write(\n.join(lines)) # 示例批量转换 xml_dir annotations/voc txt_dir labels/yolo os.makedirs(txt_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue base xml_file.replace(.xml, ) voc_xml_to_yolo_txt( os.path.join(xml_dir, xml_file), os.path.join(txt_dir, base .txt), CLASS_NAMES )核心逻辑就两步从size节点拿图片宽高从object节点拿类别和框坐标。类别名到class_id的映射用列表索引完成所以类别清单的顺序一旦定好就不能改否则训练时类别就对不上。min(max(...))那两行做的是越界裁剪有些标注工具会把框拉出图片边界几个像素不裁的话训练时可能报错或者让损失函数震荡。.6f保留六位小数精度完全够用YOLO训练时会做内部归一化不需要更长的精度。COCO转YOLO的思路一样区别在于要先建立image_id到图片文件名、category_id到class_id的两张映射表然后遍历annotations数组。如果数据包里已经带了三种格式那你可以直接跳过转换步骤但我的建议是仍然跑一遍脚本做校验后面会讲为什么。2.3 转换完自检把标签画回图上看一遍格式转换最大的坑不是代码写错而是代码没报错但结果不对。坐标除反了、宽高算成对角线、类别索引错位这些问题在txt文件里肉眼完全看不出来。我的血泪经验是转换完必须把标签画回图片上一张一张看至少抽查30张。import cv2 def draw_yolo_boxes(img_path, txt_path, class_names, save_path): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: lines [line.strip().split() for line in f if line.strip()] for parts in lines: if len(parts) ! 5: print(f[警告] 跳过非法行: {parts}) continue cls_id int(parts[0]) x_c, y_c, bw, bh map(float, parts[1:]) # 反算回绝对像素坐标 x1 int((x_c - bw / 2) * w) y1 int((y_c - bh / 2) * h) x2 int((x_c bw / 2) * w) y2 int((y_c bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (x1, max(y1 - 5, 20)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(save_path, img) print(f已保存: {save_path})这个脚本把YOLO的五个数反算成像素坐标用OpenCV画框并写上类别名。重点看三类问题框是否贴着目标边缘、类别名是否张冠李戴、有没有框跑到图片外面去。X光图有个特殊情况——目标被遮挡时标注员容易把框画得比实际物体大一圈如果你发现大量框都偏大那说明原始标注本身就不干净不是转换的问题。这时候要么重新标注要么在训练时对框做小幅收缩后者后面会讲。3. 划分脚本怎么用随机种子、训练/验证/测试比例与一致性校验3.1 划分的三个原则随机种子、分层抽样、标签同步数据集划分看起来是个人都会但安检场景有个特殊性同一件行李往往拍了多个角度的图如果这些图一部分进了训练集一部分进了验证集那验证指标会虚高因为模型见过同一件行李的另一张照片。1000张图的规模下一张重复就足以让mAP虚高两三个点。三个原则必须同时满足。第一固定随机种子让每次划分结果一致别人复现你的实验时才不会被“随机性”干扰。第二分层抽样如果数据里包含多个类别先按类别统计图片数量保证训练集、验证集、测试集的类别比例大致一致不要出现某类小刀全在验证集里的情况。第三同步移动图片和标签图片和对应txt必须一起走标签丢了模型训练直接报错。这里的“分层”不是严格意义上的按类别分层采样因为一张图可能同时包含多类目标——X光安检图尤其如此一个行李箱里有刀、有打火机、有充电宝。常见的做法是先跑一次类别统计确认每类在三个集合里的分布偏差不超过10%偏差大了就调整随机种子重来。3.2 在Linux和Windows下通用的划分脚本这个脚本把全部图片随机打乱按设定的比例切成三份然后把每张图对应的YOLO格式txt一起复制过去。如果你用的是VOC或COCO格式只需要把标签的后缀名和目录改一下。import os import random import shutil random.seed(42) # 固定随机种子保证可复现 SRC_IMG_DIR dataset/images SRC_LAB_DIR dataset/labels/yolo DST_IMG_DIR dataset/split RATIO {train: 0.7, val: 0.2, test: 0.1} images [f for f in os.listdir(SRC_IMG_DIR) if f.endswith((.jpg, .png, .jpeg))] random.shuffle(images) num_train int(len(images) * RATIO[train]) num_val int(len(images) * RATIO[val]) splits { train: images[:num_train], val: images[num_train:num_train num_val], test: images[num_train num_val:], } for split_name, img_list in splits.items(): dst_img os.path.join(DST_IMG_DIR, split_name, images) dst_lab os.path.join(DST_IMG_DIR, split_name, labels) os.makedirs(dst_img, exist_okTrue) os.makedirs(dst_lab, exist_okTrue) for img_name in img_list: base os.path.splitext(img_name)[0] src_img os.path.join(SRC_IMG_DIR, img_name) src_lab os.path.join(SRC_LAB_DIR, base .txt) dst_img_path os.path.join(dst_img, img_name) dst_lab_path os.path.join(dst_lab, base .txt) shutil.copy2(src_img, dst_img_path) if os.path.exists(src_lab): shutil.copy2(src_lab, dst_lab_path) else: print(f[警告] 缺少标签: {img_name})注意几个细节random.shuffle在random.seed(42)之后执行这就是复现的保障。三份比例不要用85/15/0测试集哪怕只有一份也要单独切开否则你后续的置信度阈值和PR曲线都没得算。图片用copy2保留原始修改时间真出问题排查时能对上原始数据。标签不存在的图片只警告不中断但后面的一致性检查必须单独做一遍。还有一点Windows上跑Python脚本经常遇到“闪退”的情况多半是路径分隔符问题。这个脚本里全部用了os.path.join避免了C:\和/混用的坑。如果你双击bat跑时窗口一闪而过在bat最后加一行pause就能看到报错内容。3.3 划分完先跑一致性检查再开训练划分完成后不要急着训练先跑一遍校验。检查三件事每个txt文件都有对应的图片、txt里每行都恰好是五个字段、类别id没有超出类别清单的范围。前两件事用脚本自动查第三件事肉眼抽查。# 检查有没有标签缺失的图片 for img in dataset/split/train/images/*.jpg; do base$(basename $img .jpg) if [ ! -f dataset/split/train/labels/$base.txt ]; then echo 缺失标签: $img fi done # 统计每类目标数量确认分层效果 for f in dataset/split/*/labels/*.txt; do awk {print $1} $f done | sort | uniq -c第一个循环遍历训练集图片检查同名txt是否存在。第二个命令把所有标签的class_id提取出来做计数输出类似123 0、98 1这样的统计数字代表类别id前面的计数代表图片数。你只需要确认三个集合里各个id的相对比例差不多就行。这一步看起来繁琐但能拦住大概率的数据事故。我自己做过一次划分后直接训练跑到第30个epoch才发现验证集里有一类目标的图片只有张loss曲线一直震荡——就是分层没做好验证集类别分布歪了。4. YOLO训练参数怎么设data.yaml、损失函数曲线与六个必调项4.1 data.yaml类别清单与路径别写错YOLO系列的训练入口是data.yaml不管你是用Ultralytics的YOLOv8还是YOLOv11都是同一个套路。这个文件告诉框架训练集在哪里、验证集在哪里、有几类、每类叫什么。# data.yaml path: D:/airport_dataset/split # 数据根目录建议写绝对路径 train: train/images # 训练集图片相对路径 val: val/images # 验证集图片相对路径 test: test/images # 测试集图片部分版本可不填 names: 0: knife 1: gun 2: lighter 3: scissors 4: bottle 5: other这里最容易翻车的是path配错。path必须是train和val的父目录不是images这一层。我见过有人把path写成D:/airport_dataset/split/train然后训练直接报AssertionError: train dataset not found。另一个高频问题是names的类别数和顺序必须和标注时用的CLASS_NAMES完全一致顺序错了一位整个模型的输出就全乱了。如果你手里是COCO格式的json里面自带categories表建议转成YOLO txt之前就把类别顺序订好然后让所有脚本都用同一个顺序。这个顺序定了就不要再改改一次意味着所有标注文件全部要跟着改工作量你自己体会。4.2 训练命令里的6个必调参数环境配置是纯小白最容易卡住的地方。常见做法是直接在Ultralytics的环境里跑一条命令装完依赖pip install ultralytics。如果你在安装或者运行时报cmdlet不是可识别命令之类的错误多半是Python没加到PATH里重装Python时勾选“Add to PATH”就能解决。yolo detect train \ dataD:/airport_dataset/split/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ patience20 \ workers4 \ project./runs \ namexray_airport六个必调参数逐个说。model选预训练权重1000张图的数据量不建议从随机初始化开始训用yolov8n.pt或yolov8s.pt做起点前者约310万参数后者约1100万按你的显存选6G显存用n8G以上可以考虑s。epochs设100起步1000张的小数据集训练速度很快100个epoch足够看到收敛趋势不要一上来就300过拟合会先来找你。imgsz是推理分辨率X光安检场景建议用640起步如果后面发现小目标检不出来再试960代价是训练和推理时间几乎翻倍。batch受显存约束8G显存跑n模型可以开到32跑s模型建议16。patience是早停参数验证集指标连续20个epoch不提升就自动停小数据集上很实用防止后期过拟合浪费时间。workers设成CPU核数的一半即可设太大反而会因为数据加载瓶颈把GPU饿死。还有一个容易被忽略的pretrained参数。如果你明确不想用COCO预训练权重可以写pretrainedFalse但我不建议在小数据集上这么干。X光图和自然图像差异大COCO权重迁移过来的效果有限但至少backbone学到了边缘、纹理这些底层特征比从零开始强得多。如果发现迁移效果确实不行再考虑冻结backbone前几层做手动迁移后面避坑章会细说。4.3 损失函数曲线怎么读box_loss、cls_loss与dfl_loss训练启动后Ultralytics在控制台实时输出box_loss、cls_loss、dfl_loss三条损失曲线很多人只看mAP就完事。但在数据规模只有1000张的时候mAP刷得再高也可能是过拟合出来的幻觉损失曲线才是判断模型有没有真正学进去的依据。三条曲线的分工是box_loss管检测框和真实框的重合度cls_loss管分类对不对dfl_loss是分布焦点损失管边界框的精确程度。X光场景下最值得盯的是box_loss因为目标遮挡严重框的边界天然模糊box_loss如果一直在0.08以上下不来说明模型对目标边界的学习没到位——加不了epoch解决不了得从标注质量或者输入分辨率入手。正常现象是三个loss在前10个epoch快速下降之后缓慢下降并伴随小幅度震荡。如果你看到cls_loss降到接近0但box_loss还在缓慢上升大概率是过拟合的早期信号这时候patience的早停机制会帮你切断训练。另外一个小技巧训练完翻一下runs/xray_airport/目录下的results.png那个图把三条曲线和mAP都画在一起比你来回翻控制台方便得多。5. X光安检数据集避坑五个实测翻车场景与解决方案5.1 有目标但检不出来重叠行李让NMS把真阳框抑制掉了现象验证集上mAP看着很高但在实际行李图上一把刀明明摆在书包旁边模型就是没框出来。原因X光图里目标叠放严重一个目标的检测框和另一个目标的重叠度很高。下游的NMS按IoU阈值做抑制两个框重叠超过阈值就只留分数高的那个分数低的小目标直接被当成重复框删掉了。这在自然图像里很罕见但在安检图里是家常便饭。解决把NMS的IoU阈值调小。YOLO检测阶段默认的iou0.7你可以在推理时显式改成iou0.45或更低代价是附近会出现一些重复框但对小目标召回率的提升立竿见影。训练阶段的nms参数不需要动推理时改就行了。5.2 COCO预训练权重迁移过来效果奇差灰度图让通道语义失效现象用yolov8n.pt在X光数据上微调50个epochmAP0.5连0.5都不到。原因COCO训练数据的通道语义是RGBX光是单通道灰度透射图物体呈现的颜色、纹理和自然图像完全不同。backbone前几层学的是RGB空间的边缘和颜色组合迁移过来的参数在灰度图上用处有限甚至会起反作用。解决微调时冻结backbone前几层让后层重新适应X光的灰度特征。Ultralytics里可以用freeze10参数冻结前10层。如果冻结后效果还是不行就换思路把X光图在三通道上复制成伪RGB虽然本质还是灰度但至少保留了完整输入维度。这个方法不保证一定提升但值得一试——准确说这属于玄学范畴不同数据集上表现差异很大。5.3 验证集指标好看测试集漏检率翻倍现象val的mAP0.5到0.85test的mAP只有0.6召回率掉了20个点。原因划分脚本的问题通常出在“同源图片泄露”。同一件行李拍的不同角度、不同包裹的相似摆放被随机打乱后分到了train和test两个集合。测试集里出现训练集的“近亲”测试指标就被污染了。另一种可能是出现了空标签图片——安检数据里确实有完全没目标的高压图如果这些图被分到val里它们不参与损失计算但会把mAP拉低。解决划分前按行李编号或拍摄批次做分组同一组全进同一集合。如果数据包里没有编号信息就用文件名的前缀做分组比如bag_001_a.jpg和bag_001_b.jpg都归到bag_001。另外在划分脚本里加一个检查统计每张图txt的行数把行数为0的图片单独列出来避免空图进入评测集干扰指标。5.4 标注框比真实目标大一圈边缘模糊让标注标准失控现象训练出来的模型框总是偏大尤其在目标边缘不清晰的区域检测框比目标外包围盒大了10%左右。原因X光图的边缘天然模糊金属和有机物边界过渡区域宽标注员对“目标边界到底在哪里”有主观判断差异。有人从严贴着目标有人从宽框到半透明外沿。这种标注噪声在训练中会让模型学到“框大一点更安全”的倾向。解决统一标注规范规定边界取“目标外缘不透明区域的外切矩形”然后在训练前对bbox做收缩处理。YOLO格式的txt直接操作即可把w和h各乘0.9x_center和y_center不变相当于把框向中心缩5%。实际操作时建议只收缩到0.95倍收缩太狠反而让边界学偏。5.5 训练时loss降了但PR曲线很难看现象训练曲线显示loss稳步下降但跑测试集时precision和recall曲线失衡准确率0.9时召回率只有0.4。原因类别不平衡。1000张图里如果bottle类占了大头gun类只有几十张模型会倾向把所有目标都预测成多数类loss仍然会降但少数类的召回率几乎为零。解决先看类别统计确认每类最少有多少张图。如果确有类别样本个位数先把这类的图片复制增强水平翻转、随机裁剪、亮度抖动到至少100张或者用weight参数给少数类加loss权重。Ultralytics支持在训练时传class_weights但每次都要生成权重数组比较麻烦更省事的方案是直接用数据增强把少数类补起来。这个操作只能缓解问题想根治还是得补数据。6. 从mAP到上线置信度阈值与单卡并发估算6.1 用测试集画PR曲线选置信度阈值训练完best.pt模型后mAP只是一个参考真正上线前必须做一件事在测试集上跑一遍画出PR曲线选定置信度阈值。安检场景的优先级是“宁可误检一百不能漏检一单”——漏检一把刀意味着安全事故所以阈值通常定在0.3甚至更低换取更高的召回率。from ultralytics import YOLO model YOLO(runs/xray_airport/weights/best.pt) results model.predict( sourceD:/airport_dataset/split/test/images, conf0.25, # 先放低阈值跑出全部候选框 saveTrue, project./runs/predict, nametest_conf_demo )跑完后逐一翻看输出图观察低置信度区间里到底有多少真正的目标混在误检里。如果0.25阈值下误检框满天飞说明模型本身不够自信要把阈值升到0.4以上如果0.25阈值下真目标还是经常没框出来那问题在模型不在阈值回头去调训练参数。这里没有绝对正确的数字每批数据和每个类别的合适阈值都不同。6.2 TensorRT导出与单卡并发粗估安检场景跑实时视频流模型推理速度直接决定硬件预算。常见做法是把YOLO模型导出成TensorRT的engine格式在NVIDIA显卡上把推理速度压到极致。yolo export modelruns/xray_airport/weights/best.pt formatengine device0 \ imgsz640 halfTrue workspace4halfTrue开启FP16精度速度几乎翻倍但精度损失很小安检场景完全够用。workspace4分配4GB显存给构建期使用导出完成后运行期只占约1GB。导出成功后拿真实的X光图片或视频流测试单帧推理耗时。一张T4或类似算力的卡用YOLOv8n的engine跑640分辨率单帧耗时通常能压到2到5毫秒之间。按“一路1080p 25fps”算每路需要每40毫秒完成一帧检测留50%余量的话按80毫秒预算那基本可以估算2毫秒的推理耗时对应40路余量5毫秒对应16路——实际还要算上解码、前后处理。记住一个原则不要按理论峰值算并发跑一次实际测试最靠谱。说实话做X光安检项目这几年我最大的感受是数据质量对结果的影响远大于模型选型。YOLO在安检场景里已经是验证过无数次的成熟方案真正让项目失败的都是标注不一致、划分不严谨、阈值拍脑袋这些基础环节。把本文提到的格式校验、划分检查、类别统计这三件事做扎实模型的性能至少不会翻车。希望帮到你。本文还有配套的精品资源点击获取