☰
X光安检目标检测实战:VOC/COCO/YOLO格式转换与YOLO训练避坑指南
2026/10/2 2:40:55 网站建设 项目流程

简介:这是一套面向目标检测学习者的X光安检数据集,包含五千张真实安检场景图片,由LabelImg完成标注,标注框质量较高,并同步提供VOC、COCO、YOLO三种格式标签,可直接用于YOLO系列模型的训练与评估。压缩包共两千个文件,以XML标签文件为主,另有少量HTML教程、TXT说明和Python脚本,整体约三百二十一MB,目录按三种标签格式分放,便于按需调用。配套资源还包括YOLO环境搭建与训练案例教程,覆盖Windows和Linux两个平台,可根据自己的数据修改案例完成迁移训练;附带的三个划分脚本能灵活生成训练集、验证集与测试集,也能生成ImageSets标注文件,方便不同工况下的数据准备。已有289人学习下载,适合需要X光安检数据集,或希望快速上手YOLO自定义训练的学习者和研究者,也可作为课程设计或算法论文实验的数据基础。

1. X光安检数据集拿到手先别急着训:三种标签和划分脚本到底帮你省了什么

做X光安检的目标检测,最烦的不是模型选型,而是数据整理:安检机出来的图对比度低、遮挡严重,得先把数据格式摆平才能训。一个完整的YOLO目标检测X光安检数据集,往往带5000张图片、VOC/COCO/YOLO三种格式标签,以及现成的划分脚本和训练教程,这些都是绕开前期脏活的“后悔药”。拿到手你可以直接干正事——检查标注、调超参数、分析漏检。适合正在做安检机违禁品识别、快递包裹检测,或拿公共数据集练手的同学。

2. VOC、COCO、YOLO三种标签格式:先看清差异再动手

2.1 VOC的XML和COCO的JSON怎么读

做视觉的人对这三个名字不陌生,但真把5000张X光安检图的标签从VOC换到COCO再换到YOLO,很多人会在一堆括号和字典之间翻车。先说VOC。VOC格式的每个XML对应一张图像,结构是这样:

<annotation> <folder>JPEGImages</folder> <filename>000001.jpg</filename> <size> <width>800</width> <height>600</height> <depth>3</depth> </size> <object> <name>knife</name> <bndbox> <xmin>210</xmin> <ymin>154</ymin> <xmax>334</xmax> <ymax>211</ymax> </bndbox> </object> </annotation>

重点:bndbox 里四个值都是原始像素坐标,和图像分辨率耦合。你把图缩放到640×640做训练时,这个框不会自动跟着变。另外,一个XML里可以有多个<object>节点,对应一张图里的多个可疑物。

COCO则是另一个路子,整份标签是一或几个JSON文件。打开后能看到三大块:images(每张图的id和文件名)、annotations(每个标注框的image_id、category_id、bbox)、categories(类别id和名称的对照表)。一个标注的典型结构是:

{ "id": 35, "image_id": 7, "category_id": 2, "bbox": [210, 154, 124, 57], "area": 7068, "iscrowd": 0 }

这里 bbox 的 4 个值是 [x, y, width, height],同样基于原始像素。读取它们用不到第三方库,Python 自带的 json 和 xml.etree 就够。常见坑是:COCO 的 category_id 不一定从 0 开始,VOC 的 name 是字符串,两边对不上的时候最容易出事。

写个最短脚本把两种格式读成统一的记录列表,方便后续统计和换格式时用:

import json import xml.etree.ElementTree as ET def parse_voc(xml_path): root = ET.parse(xml_path).getroot() w = int(root.find("size/width").text) h = int(root.find("size/height").text) boxes = [] for obj in root.findall("object"): cls = obj.find("name").text bb = obj.find("bndbox") x1 = float(bb.find("xmin").text) y1 = float(bb.find("ymin").text) x2 = float(bb.find("xmax").text) y2 = float(bb.find("ymax").text) boxes.append((cls, x1, y1, x2, y2)) return w, h, boxes def parse_coco(json_path): with open(json_path) as fp: data = json.load(fp) img_map = {img["id"]: img for img in data["images"]} cat_map = {cat["id"]: cat["name"] for cat in data["categories"]} records = [] for ann in data["annotations"]: if ann["iscrowd"]: continue img = img_map[ann["image_id"]] x, y, bw, bh = ann["bbox"] records.append((img["file_name"], img["width"], img["height"], cat_map[ann["category_id"]], x, y, x + bw, y + bh)) return records

parse_voc 返回宽高加框列表,parse_coco 返回统一记录。这个脚本不负责转换,而是先把标签读进来做一次“体检”:统计每个类别的框数量、框是否越界、宽高是否小于0。很多标签包打开时看着文件都在,一统计才发现某个类别一张都没有,或者坐标已经超出图像边缘。

2.2 YOLO的txt为什么是归一化坐标

YOLO 自成一派,每张图像配一个同名txt,每行是一个框:

2 0.3125 0.5333 0.1240 0.1233

四个数值分别是 class_id、中心点x、中心点y、宽、高,全部除以图像宽高做归一化。所以任意分辨率输入都能直接用,训练时缩放图像不破坏标签语义。class_id 是谁由 data.yaml 里的 names 字段决定,常见的是和 classes.txt 顺序一致,比如:

0 gun 1 knife 2 pliers 3 scissors 4 wrench

注意 YOLO 这种格式的分辨率信息只能从图片本身取,所以训练时图片和txt必须严格同名且一一对应。这个特性也带来一个隐雷:如果一个txt里出现负数坐标或者超过1的宽高,图片缩放后框就飘到画面外,但YOLO训练程序不会直接崩,只会把loss算得越来越怪。下面这个检查脚本值得在每个回合前跑一遍:

import os label_dir = "labels" bad_files = [] for f in os.listdir(label_dir): p = os.path.join(label_dir, f) with open(p) as fp: for line in fp: parts = list(map(float, line.split())) if len(parts) != 5: bad_files.append(f) break cid, cx, cy, bw, bh = parts if not (0.0 <= cx <= 1.0 and 0.0 <= cy <= 1.0 and 0.0 < bw <= 1.0 and 0.0 < bh <= 1.0): bad_files.append(f) break print("异常txt数量:", len(bad_files))

这个脚本把“归一化坐标必须在 [0,1] 区间、宽高必须为正”的约束写死,一旦发现异常直接报名字。X光安检数据里如果出现这类异常,多半是转换脚本里把除以宽写成了除以高,或者把像素坐标当成了归一化坐标直接写进来。

2.3 用20行脚本从VOC XML转YOLO txt,顺便验标签

数据集已经带了三种格式,正常情况下不需要自己转。但你训练前大概率会做数据清洗——删掉不清晰的图、合并可疑类别、修正个别错框,这时候重新导出标签就绕不开转换。从VOC转YOLO的要点有两个:类别名要映射到整数id,像素坐标要除以图宽高得到归一化中心点与宽高。最小实现如下:

import os import xml.etree.ElementTree as ET classes = ["gun", "knife", "pliers", "scissors", "wrench"] def voc2yolo(xml_path, out_dir): root = ET.parse(xml_path).getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in classes: continue bb = obj.find("bndbox") x1 = float(bb.find("xmin").text) y1 = float(bb.find("ymin").text) x2 = float(bb.find("xmax").text) y2 = float(bb.find("ymax").text) cx = ((x1 + x2) / 2) / img_w cy = ((y1 + y2) / 2) / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{classes.index(name)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out_name = os.path.basename(xml_path).replace(".xml", ".txt") with open(os.path.join(out_dir, out_name), "w") as fp: fp.write("\n".join(lines))

转换逻辑本身很直白,但这版脚本把不在 classes 列表里的对象直接忽略。看起来没问题,其实是个隐患:如果原来VOC数据里有个类别你忘了写进classes,它会静默丢框,训练时那个类别就永远学不到。更稳的做法是转换后对比源XML对象数和txt行数,对不上就打印警告。

除了代码,两个参数值得留神。分隔符用空格没问题,但要确保整行没有多余逗号;小数位保留6位对600像素级别的X光图足够,分辨率更高的图也别少于5位,否则同一个框在相邻两次迭代里坐标会有细微抖动,对loss曲线影响不大,但对小目标召回不友好。转换完一定要随机抽五张图做可视化,这一步是绝对的后悔药,具体做法放到第5章。

3. 运行划分脚本:把5000张X光图切成train/val/test的完整过程

3.1 先看清压缩包里的目录结构再动手

解压放进干净目录。解压前建议把.rar复制到工作目录,避免中文文件名在部分Windows终端下解压失败。Linux下用7z x就能解:

mkdir -p ~/xray && cd ~/xray 7z x "YOLO目标检测X光安检数据集(含5000张图片)+对应voc、coco和yolo三种格式标签+划分脚本+训练教程.rar"

解压后先跑一棵目录树,不要急着双击看图片。绝大多数翻车都发生在对目录结构缺乏认知的情况下:

tree -L 3 -d .

常见的数据集打包方式是这个骨架:

xray/ ├── JPEGImages/ # 5000 张原图 ├── Annotations/ # VOC 标签,5000 个 xml ├── labels/ # YOLO 标签,5000 个 txt ├── coco_annotations/ # COCO 标签,train/val/test 三个 json ├── ImageSets/Main/ # VOC 的划分 txt ├── split_dataset.py # 划分脚本 └── classes.txt # 类别清单

注意tree -L 3 -d只显示目录,不显示文件,适合第一眼确认层级。如果压缩包还套了一层外层文件夹,后面所有相对路径都要加前缀,这是训练报错的高频来源。

3.2 划分脚本实操:随机种子、比例和命令

划分脚本的职责是把5000张图按照一定比例切成三份,给VOC生成ImageSets,给COCO生成三个json,给YOLO生成三个txt。命令行一般长这样:

python split_dataset.py --train-ratio 0.8 --val-ratio 0.1 --test-ratio 0.1 --seed 42

如果脚本支持独立传参,常见写法是--train 0.8 --val 0.1 --test 0.1或--ratios 8:1:1。参数名字不用死记,先python split_dataset.py --help看一遍再执行。两个必看的参数是:

  • --seed:固定随机种子。不固定种子,每次跑划分集合都变,同一份实验就没法复现。建议选一个固定数值,比如42或2024,记录在训练笔记里。
  • 划分比例:我一般用8:1:1。数据到了5000张,10%的验证集是500张,足够评估;如果你后面要做模型选择或者早停,验证集留到15%也不心疼。

坑在“随机”两个字。纯随机划分到小类别上容易翻车:比如5000张图里只有150张含gun,随机切10%到测试集,可能正正好好把150张全切到测试集,训练集一个gun都学不到。正规的脚本会在切分前按类别做分层,保证每个类别在三个集合里的占比大致相同。如果自带脚本没有分层逻辑,自己补一段也不难:

import random from collections import defaultdict random.seed(42) imgs_by_cls = defaultdict(list) # 假设 yolo_labels 是 {图片名: [类别id列表]} for img_name, cls_ids in yolo_labels.items(): for ci in set(cls_ids): imgs_by_cls[ci].append(img_name) train_set, val_set, test_set = set(), set(), set() for ci, imgs in imgs_by_cls.items(): n = len(imgs) n_train = int(n * 0.8) n_val = int(n * 0.1) random.shuffle(imgs) train_set.update(imgs[:n_train]) val_set.update(imgs[n_train:n_train + n_val]) test_set.update(imgs[n_train + n_val:])

这段代码的用意是“每个类别单独抽样”,避免小类别被随机波动整类送进测试集。最后所有类别的结果做一次并集,得到最终三个集合。这样训练集中每个类别的样本数都不会太低,测试集也能覆盖各种类。

3.3 划分后的目录怎么对回YOLO训练

划分脚本的输出能直接喂给YOLO吗?对YOLO来说,标准做法是往 data.yaml 的 train/val 字段里填一份txt,每行是一张图片的路径。先看生成的列表文件格式:

head -5 train.txt

假设输出结果是:

/home/user/xray/JPEGImages/000001.jpg /home/user/xray/JPEGImages/000009.jpg /home/user/xray/JPEGImages/000013.jpg /home/user/xray/JPEGImages/000024.jpg

这就是YOLO在训练时实际加载的来源。Ultralytics YOLO 会根据图片路径自动去找同一层的labels目录里的同名txt,所以你的实际路径应保持图片与标签同级。例如原图在images/,标签在labels/,那么 data.yaml 的 train/val 字段可以这么写:

path: /home/user/xray train: images/train val: images/val

YOLO 会自动切到labels/train去找txt。如果你把划分结果直接输出到train.txt,也可以用train: train.txt指向路径列表文件。两种方式都能跑,我更推荐直接给 images 子目录路径,因为少了路径列表文件出错的可能。

4. 按训练教程跑通YOLO:data.yaml和训练命令的关键细节

4.1 data.yaml的path、train、val、names四个关键字段

现在写训练用的 data.yaml。这是整个流程里最容易“看起来对了但跑不动”的文件:

path: /home/user/xray train: images/train val: images/val test: images/test names: 0: gun 1: knife 2: pliers 3: scissors 4: wrench

字段说明:

  • path:数据集的根,写绝对路径最不容易错。相对路径也行,但你执行yolo命令的工作目录一变,相对路径就失效。
  • train/val/test:相对于 path 的图片目录。很多老教程喜欢写train: /home/user/xray/images/train/*.jpg,这种写法在新版Ultralytics里不一定被接受,直接用目录路径更稳。
  • names是字典或者列表,必须和txt标签里的class_id一一对应。这里出错的代价远超过你写错任何一个超参。
  • nc会自动从 names 长度推断,不写也行,但有些 YOLOv5 模板要求显式nc: 5,写上没坏处。

注意:data.yaml 的编码和换行也要留意。在 Windows 下用记事本编辑,极大概率存成带 BOM 头的 UTF-8 或者 CRLF 换行,yaml 解析器报错信息又很含糊。稳妥做法是用 VSCode 打开,把右下角编码切到 UTF-8,文件内容保持纯英文路径和英文类别名。

4.2 模型规模n/s/m/l怎么选

YOLO 同一代模型按规模分 n、s、m、l、x 几档,参数量和计算量依次上涨。选型不是越大越好,安检场景的帧率要求和显存限制很现实。以 YOLOv8 为参考,规模体积大概是这样:

模型参数量说明
yolov8n约3.2M跑通流程用,低显存也能训练
yolov8s约11.2M多数安检任务的基准线
yolov8m约25.9M小目标多时建议直接上这档
yolov8l约43.7M对帧率要求高的设备可能吃紧

新手做法是把 n 先跑通,验证脚本和标签没问题后再换 s 或 m。如果设备是24G显存,且对打火机、刀片这类小目标要求高,直接 m 起步更划算,省得换模型后又要重新调一轮参数。

有人会问:X光和自然图差别这么大,要不要去掉预训练权重完全从零训练?大部分情况下不要。5000张图从零训练YOLO,泛化能力远不如在 COCO 预训练权重上微调。预训练网络前几层学的边缘和纹理特征对X光图同样有效,保留权重,重点训练后面的检测头就够了。

4.3 训练命令与需要动的几个超参数

主命令是:

yolo task=detect mode=train model=yolov8n.pt data=data.yaml \ epochs=300 imgsz=800 batch=16 device=0 cache=True patience=60

拆开来解释:

  • model=yolov8n.pt:预训练权重。第一次运行会自动下载,如果网络环境受限,提前把权重文件放到命令执行的当前目录,程序会优先读本地文件。
  • imgsz=800:对X光安检图,800是我常用的起点。640会让小目标漏检严重,1024显存吃紧。如果你的原始图片本身不到800宽,改到640也行。
  • epochs=300和patience=60:5000张的规模不用硬跑满300轮,靠早停机制:验证集60轮没提升就停。参数值宜大不宜小,设30容易在loss还没收敛时就误停。
  • cache=True:把图片预加载到内存,省每轮读盘时间。5000张800×800的图大概几个GB内存,内存不足就关掉。

训练期间要看的结果都在runs/detect/train/下,至少盯两条曲线:

  • results.csv:epoch、mAP50、mAP50-95、box_loss、cls_loss、dfl_loss 都会落到这张表。
  • confusion_matrix.png和PR_curve.png:这两个文件直观地展示漏检和误检,能看出来哪个类别的召回率偏低。

如果你的loss掉得很慢,先怀疑不是参数,而是标签本身有问题。这时候不要急着换大模型,先按第5章的画框法把标签看一遍,再决定要不要降增强强度。

跑第二次时建议动四个参数:

  • mosaic:默认1.0,对X光这种大包裹图有效,因为多张小图拼接后相当于变相增大单目标占比。但如果发现训练loss和验证loss差异很大,把mosaic降到0.5。
  • scale:X光图里的物体物理比例相对固定,别开太大的尺度增强,scale: 0.3够用。设成0.9会把物体缩成畸形,小目标更难学。
  • lr0:默认0.01,5000张这种中小规模数据集,建议从0.005起步,收敛更稳。
  • hsv_h和hsv_s:灰度X光图不需要颜色增强,直接置0,细节在第5章讲。

5. 避坑:X光安检数据集训练的5个翻车点

5.1 标签和图片对不上:画框验证是最快的后悔药

现象:训练loss正常下降,验证mAP看起来也过得去,但把模型拿来测单张图,框全都歪在目标旁边或者大小不对。

原因:图片和标签错位。VOC转换时如果中途做过一次文件按字符串排序,10.jpg会排到2.jpg前面,标注和图像就错位了。这种错位不会让训练崩溃,只会让模型在错误数据上收敛。

解决:先画框验证。写个脚本随机挑一张图,把YOLO的归一化坐标换算回像素坐标画上去:

import os import cv2 def draw_check(img_path, label_path, out_path): img = cv2.imread(img_path) if img is None: print("图片路径不存在:", img_path) return h, w = img.shape[:2] with open(label_path) as fp: for line in fp: parts = list(map(float, line.split())) if len(parts) != 5: continue cid, cx, cy, bw, bh = parts x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imwrite(out_path, img) draw_check("JPEGImages/000011.jpg", "labels/000011.txt", "check_000011.jpg")

画完肉眼比对框和实际物体是否吻合,比看100个指标都快。建议抽5张不同类别的图检查:大目标一张、小目标一张、多目标一张、只有背景没有目标一张。

5.2 类别id错位导致mAP为0

现象:训练跑了几百轮,loss降到很低,但 mAP50 始终是0,或者偶尔冒个0.01又掉回去。

原因:class_id 和 names 表对不上。比如VOC里 name 是“scissors”的对象,在YOLO txt里写成了第4个类别,而data.yaml的 names 顺序又把“gun”排在第4位。模型标签在图上打了错位的叉,学到的全是噪声。

解决:不要在训练时猜,直接在命令行打印:

python - <<'EOF' import yaml with open("data.yaml") as fp: cfg = yaml.safe_load(fp) print(cfg["names"]) EOF

再看labels目录里任意一个txt的内容,两边的顺序逐项对齐。还有一种常见低级错误:classes.txt 和 data.yaml 不一致,你从别人那儿复制了data.yaml但没改names。预防办法是每次换标签时打印每个 class_id 的样本数分布,哪个id样本数异常少,多半就是错位。

5.3 灰度X光图的增强照搬自然图:把模型练飘了

现象:训练时mAP很好看,但拿到新安检机上漏检明显,或者训练过程loss波动特别大。

原因:X光图是单通道灰度,很多人用OpenCV读取后直接三通道复制,然后用默认增强配置训练。YOLO默认的 hsv_h、hsv_s、hsv_v 参数是给彩色自然图设计的,灰度图上做色调和饱和度扰动,等于给无意义的通道加噪声。

解决:调低颜色类增强参数,只保留少量亮度变化来模拟不同安检机的曝光差异:

hsv_h: 0.0 hsv_s: 0.0 hsv_v: 0.05 fliplr: 0.0 scale: 0.3

hsv_s 直接置0,避免给灰度图添假色彩;hsv_v 保持一个小量,模拟安检机之间亮度不一致;fliplr 要不要开取决于业务:X光包裹左右翻转在物理上是对称的,但如果后面要识别特定朝向摆放的违禁品,关掉更稳。翻转与否不是算法问题,是业务问题。

5.4 小目标召回率低:先查imgsz再看置信度阈值

现象:验证集上整体mAP有0.7,但 recall@0.5 不到0.5,细小的打火机、刀片几乎检不到,只有大件水瓶能检出。

原因:X光图拍的是整个包裹,一件行李缩到640×640后,刀片可能只有几十个像素,接近特征图空间分辨率的极限。和模型参数量的关系不大,主要卡在输入分辨率。

解决:把 imgsz 从640提到960或1024。显存只有12G的话,batch 降到8或4也要保证喂图尺寸。再不行就做切片推理:把原图切四块分别预测再合并结果,但X光图切半张会切碎包裹结构,首选还是加分辨率。

部署时还要注意置信度阈值。训练时默认的conf是0.25,测试时0.25可能漏检太多小目标。安检业务宁可多误报再让后端人工复核,也不愿意漏检,所以这类场景优先保召回率,推理时把conf调到0.15到0.2之间,代价是多一些误报框。

5.5 划分泄漏:同一张图同时出现在train和val

现象:验证集mAP高到0.95,觉得模型稳了,拿去实测效果却明显变差,摆明了“验证即过拟合”。

原因:划分时没做去重。X光安检数据集在采集时经常对同一个包裹多角度拍摄,或者同一个包裹重复过机产生多张几乎一样的图。只按文件名随机划分,完全相同的图可能一张进train、一张进val,验证集水分很大。

解决:划分前先对全量图片做MD5检查,完全相同的图直接去重:

import hashlib, os def md5_file(p, chunk=65536): m = hashlib.md5() with open(p, "rb") as f: while True: data = f.read(chunk) if not data: break m.update(data) return m.hexdigest() img_dir = "JPEGImages" hash_map = {} for name in os.listdir(img_dir): p = os.path.join(img_dir, name) d = md5_file(p) hash_map.setdefault(d, []).append(name) dups = {k: v for k, v in hash_map.items() if len(v) > 1} print("重复图组数:", len(dups))

对MD5相同的组,要么删到只剩一张,要么把它们全部放进同一个划分集合。注意:MD5只能找完全相同的图,经过压缩或裁剪的近似重复要用感知哈希才能识别,但对普通训练流程,MD5已经够先把最大的水分挤掉。

6. 训练完怎么验证:混淆矩阵、mAP和实际过检的推理

6.1 用混淆矩阵确认哪一类被漏检

训练结束后不要只看一张results.png,我的固定顺序是这样:

先打开runs/detect/train/confusion_matrix_normalized.png。横轴是真实类别,纵轴是预测类别,对角线越亮越好。安检任务最怕对角线某个类别暗一块,那说明对应可疑物大量漏检。

再打开PR_curve.png,看每个类别在低置信度时召回率能推到多高。曲线越接近右上角越理想。如果某个类别的曲线整体偏低,就不是阈值问题,而是这类目标本身学得不够,需要补数据或调分辨率。

最后打开results.csv,用文本方式看每轮的 mAP50 和 mAP50-95。mAP50 高而 mAP50-95 低,说明框的位置精度不够,常见原因是标注框边缘本身有噪声;两个指标都低,往回查标签。

6.2 导出ONNX并跑一张没训练过的实拍图

验证的最后一步是导出推理格式,拿一张没进过训练集的实拍图跑通整个链路:

yolo export model=runs/detect/train/weights/best.pt format=onnx opset=12

导出后用同一套权重在Python里做推理:

from ultralytics import YOLO model = YOLO("best.pt") results = model("live_xray.jpg", conf=0.15, iou=0.5) results[0].plot()

推理时把 conf 设到0.15,对安检这种漏检代价高的任务更稳。iou=0.5是NMS的阈值,不是评估用的IoU,保持0.5能减少重叠框扎堆。把输出图存下来再人眼复核一遍,能同时确认预处理、类别名和框坐标都没问题。

我自己每次拿到新数据集,第一件事不是急着训练,而是先把标签画出来看一遍,再跑一次划分脚本核对类别分布。这套习惯救过我太多次,省下的时间远大于多花的半小时。希望这些经验能帮到你,少走几个我曾经踩过的坑。

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

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

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

立即咨询