☰
X光安检目标检测:YOLO数据集格式转换与训练避坑指南
2026/10/7 1:38:27 网站建设 项目流程

简介:面向YOLO系列目标检测训练的X光安检图像数据集,收录1000张真实安检场景的高质量图片,数据场景丰富,由LabelImg人工精细标注,标注框质量可靠;同步提供VOC(xml)、COCO(json)、YOLO(txt)三种主流标签格式,分别存放于独立文件夹,可直接导入现有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_ok=True) 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_ok=True) os.makedirs(dst_lab, exist_ok=True) 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 \ data=D:/airport_dataset/split/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ patience=20 \ workers=4 \ project=./runs \ name=xray_airport

六个必调参数逐个说。model选预训练权重,1000张图的数据量不建议从随机初始化开始训,用yolov8n.pt或yolov8s.pt做起点,前者约310万参数,后者约1100万,按你的显存选,6G显存用n,8G以上可以考虑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预训练权重,可以写pretrained=False,但我不建议在小数据集上这么干。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检测阶段默认的iou=0.7,你可以在推理时显式改成iou=0.45或更低,代价是附近会出现一些重复框,但对小目标召回率的提升立竿见影。训练阶段的nms参数不需要动,推理时改就行了。

5.2 COCO预训练权重迁移过来效果奇差:灰度图让通道语义失效

现象:用yolov8n.pt在X光数据上微调50个epoch,mAP@0.5连0.5都不到。

原因:COCO训练数据的通道语义是RGB,X光是单通道灰度透射图,物体呈现的颜色、纹理和自然图像完全不同。backbone前几层学的是RGB空间的边缘和颜色组合,迁移过来的参数在灰度图上用处有限,甚至会起反作用。

解决:微调时冻结backbone前几层,让后层重新适应X光的灰度特征。Ultralytics里可以用freeze=10参数冻结前10层。如果冻结后效果还是不行,就换思路:把X光图在三通道上复制成伪RGB,虽然本质还是灰度,但至少保留了完整输入维度。这个方法不保证一定提升,但值得一试——准确说这属于玄学范畴,不同数据集上表现差异很大。

5.3 验证集指标好看,测试集漏检率翻倍

现象:val的mAP@0.5到0.85,test的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.9,x_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( source="D:/airport_dataset/split/test/images", conf=0.25, # 先放低阈值,跑出全部候选框 save=True, project="./runs/predict", name="test_conf_demo" )

跑完后逐一翻看输出图,观察低置信度区间里到底有多少真正的目标混在误检里。如果0.25阈值下误检框满天飞,说明模型本身不够自信,要把阈值升到0.4以上;如果0.25阈值下真目标还是经常没框出来,那问题在模型不在阈值,回头去调训练参数。这里没有绝对正确的数字,每批数据和每个类别的合适阈值都不同。

6.2 TensorRT导出与单卡并发粗估

安检场景跑实时视频流,模型推理速度直接决定硬件预算。常见做法是把YOLO模型导出成TensorRT的engine格式,在NVIDIA显卡上把推理速度压到极致。

yolo export model=runs/xray_airport/weights/best.pt format=engine device=0 \ imgsz=640 half=True workspace=4

half=True开启FP16精度,速度几乎翻倍但精度损失很小,安检场景完全够用。workspace=4分配4GB显存给构建期使用,导出完成后运行期只占约1GB。导出成功后,拿真实的X光图片或视频流测试单帧推理耗时。一张T4或类似算力的卡,用YOLOv8n的engine跑640分辨率,单帧耗时通常能压到2到5毫秒之间。按“一路1080p 25fps”算,每路需要每40毫秒完成一帧检测,留50%余量的话按80毫秒预算,那基本可以估算:2毫秒的推理耗时对应40路余量,5毫秒对应16路——实际还要算上解码、前后处理。记住一个原则:不要按理论峰值算并发,跑一次实际测试最靠谱。

说实话,做X光安检项目这几年,我最大的感受是数据质量对结果的影响远大于模型选型。YOLO在安检场景里已经是验证过无数次的成熟方案,真正让项目失败的都是标注不一致、划分不严谨、阈值拍脑袋这些基础环节。把本文提到的格式校验、划分检查、类别统计这三件事做扎实,模型的性能至少不会翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询