ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

X光安检目标检测数据集实战:YOLOv8训练与避坑指南

X光安检目标检测数据集实战:YOLOv8训练与避坑指南 简介一份面向安检智能识别与目标检测学习者的X光安检图像标注数据集整体规格覆盖3600张图像、10个类别类别涉及打火机、刀具、剪刀、充电宝、压力容器等常见违禁品并划分出独立测试集可直接用于目标检测模型训练与性能验证。完整数据同时提供VOC格式XML与YOLO格式TXT两类标注本次压缩包为ZIP格式共2000个文件以1999个XML标注文件为主另含1个使用授权说明TXT整体约793.61MB。XML承载目标框与类别信息可接入常见目标检测工作流减少自行标注与格式转换成本授权说明文件也有助于确认数据合规边界。标注由labelImg工具生成全部标注合计9042个目标框10个类别框数从85到2858不等兼顾常见物品与小样本类别便于开展模型迭代、评估与对比验证X光图像中常见的遮挡与重叠目标特征也有助于研究复杂检测场景。当前已有241人学习/下载适合需要安检场景训练数据、开展违禁品识别算法验证的开发者与研究团队。1. 安检X光目标检测数据集3600张图能训出能用的违禁品识别模型吗X光安检目标检测数据集3600张图片、两份标准格式、9042个手工标注框这是我在做安检类项目时最常遇到的资源类型。它不是让人学习的演示数据而是可以直接喂给目标检测框架的训练集每张jpg图片同时配好VOC格式的xml和YOLO格式的txt用labelImg标注共10类违禁品包括打火机、压力罐、刀具、剪刀、充电宝、Zippo油、手铐、弹弓、烟花、指甲油。对做安检算法、物流货物识别、公共安全课题的人来说最关心的不是“文件能不能加载”而是“这些样本够不够训出可用的模型、类别不平衡怎么处理、测试集怎么用”。这份数据里knife和lighter的框分别有2858和2529个而nailpolish只有85个直接训练大概率对小样本类别失效。这篇笔记从格式解析一路讲到训练配置和五个实际踩坑点新手能照着跑通熟手可以直接跳到第5章看边界问题。2. 把VOC和YOLO两种格式吃透目录结构、标注含义与读取脚本拿到手先别急着训练先把格式搞明白。数据集同时包含三种文件jpg图片、VOC格式的xml标注、YOLO格式的txt标注。xml和txt描述的是同一批标注框只是坐标表达方式不同。目标检测领域里VOC格式是给人看的YOLO格式是给模型吃的大多数训练框架只认其中一种所以第一步是确认目录结构第二步是看懂坐标系第三步是做一次文件对齐检查。2.1 打开XML先看什么VOC格式的字段结构与bndbox坐标VOC格式的标注文件是XML根节点是annotation里面包含图片文件名、图片尺寸、物体列表。每个object节点里最关键的是name和bndboxbndbox里有xmin、ymin、xmax、ymax四个整数表示目标外接矩形的左上角和右下角像素坐标。这个结构是labelImg保存xml时的默认写法也是大多数开源检测框架都能解析的标准结构。annotation folderJPEGImages/folder filename500326.jpg/filename size width1280/width height1024/height depth3/depth /size object nameknife/name bndbox xmin120/xmin ymin80/ymin xmax450/xmax ymax520/ymax /bndbox /object object namelighter/name bndbox xmin800/xmin ymin600/ymin xmax860/xmax ymax640/ymax /bndbox /object /annotation上面这段是典型的多目标标注一张X光图里有两件违禁品。size节点的width和height在后续转YOLO格式时必须用到因为YOLO需要把像素坐标归一化到0到1之间。name节点对应类别名可能包含中文或英文训练前必须把类别名映射成整数编号。打开xml先看size和bndbox就能判断这份标注是基于原始分辨率还是resize过的图如果size和实际图片尺寸对不上坐标转换一定会出问题。2.2 YOLO的txt是怎么来的归一化坐标与labelImg的对应关系YOLO格式的txt每一行代表一个目标格式是class x_center y_center width height。前两个数是目标中心点的归一化坐标后两个数是框宽和框高的归一化比例。计算方式很简单x_center (xmin xmax) / 2 / widthy_center (ymin ymax) / 2 / heightwidth和height是图片的实际像素尺寸。labelImg切换到YOLO模式保存时会自动完成这个转换所以这份数据集里的txt可以直接给YOLO系列框架用。import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, class_names): 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) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_names: continue cls_id class_names.index(name) 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) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return lines参数说明class_names是类别列表顺序必须和训练时yaml里的names完全一致。这里用index()方法把类别名转成整数编号如果类别列表顺序错了txt里的编号就会错位训练不会报错但模型会把刀识别成打火机。保留6位小数是必须的因为X光安检图里的小目标比如nailpolish归一化后宽高可能只有0.01量级精度不够会导致框偏移几个像素。3600个xml用这个脚本转换几秒就能跑完。2.3 三种文件怎么对齐写个脚本做一致性校验图片、xml、txt三者的文件名前缀应该一致但实际项目里经常出现缺文件或多余文件的情况。我拿到任何数据集后都会先跑一遍校验脚本找出“有图没标”“有标没图”的文件对。这一步看似基础却能避免训练时遇到空标签、孤儿文件这类磨人的问题。import os from pathlib import Path img_dir Path(images) xml_dir Path(Annotations) txt_dir Path(labels) imgs {p.stem: p for p in img_dir.glob(*.jpg)} xmls {p.stem: p for p in xml_dir.glob(*.xml)} txts {p.stem: p for p in txt_dir.glob(*.txt)} img_stems set(imgs.keys()) print(f图片: {len(imgs)}, xml: {len(xmls)}, txt: {len(txts)}) print(缺xml的图片:, img_stems - set(xmls.keys())) print(缺txt的图片:, img_stems - set(txts.keys())) print(多余xml:, set(xmls.keys()) - img_stems) print(多余txt:, set(txts.keys()) - img_stems)这段脚本用文件名的stem字段比对三份文件。如果缺xml或txt说明标注不完整训练时dataloader会直接跳过或报错。最坑的情况是空txt文件存在但图片没有对应标注某些框架会把这张图当成无目标负样本参与训练导致loss异常。比对时还要注意大小写Windows下文件系统不区分大小写但Linux下500326.JPG和500326.jpg是两个文件这类问题在高性能服务器上训练时特别容易暴露。3. 十个类别分布统计类别不平衡决定你的训练策略训练前必须知道每类有多少框。这直接决定损失函数要不要调权重、数据增强要不要侧重、测试集怎么划分。别凭感觉看目录拿脚本数一遍更清楚。3.1 用脚本数框每类框数与每张图目标数统计每类框数和单图目标数分布可以一次性掌握数据的基本盘。from collections import Counter import glob import xml.etree.ElementTree as ET class_counts Counter() per_image_counts [] for xml_file in glob.glob(Annotations/*.xml): tree ET.parse(xml_file) objs tree.findall(object) per_image_counts.append(len(objs)) for obj in objs: class_counts[obj.find(name).text] 1 print(每类框数:, dict(class_counts)) print(单图目标数分布:, Counter(per_image_counts).most_common())跑完结果和数据集说明完全一致总框数9042。单图目标数大多是2到3个因为安检X光图里常常有多件物品叠放也有一部分图只有1个目标。这个单图目标数会影响你后续的NMS策略物品叠放严重时密集小目标的检测框会互相抑制训练时如果NMS阈值设得太保守容易漏检。类别框数firecrackers141handcuffs275knife2858lighter2529nailpolish85powerbank897pressure1080scissors744slingshot132zippooil301表格里的数字是训练前最该看的东西。knife和lighter合计占了总框数的59%而nailpolish只有85框firecrackers和slingshot也都在150框以下。这不是均匀分布的数据集训练策略必须围绕这个不平衡展开。3.2 类别不平衡对mAP的影响与处理优先级类别不平衡会直接体现在mAP上。高频类别的AP往往能到0.85以上低频类别可能连0.4都不到。原因是模型有强烈先验倾向去预测高频类别遇到不明确的目标时更容易输出knife或lighter而不是nailpolish。处理不平衡的优先级一般是先加强困难类别的数据增强再调整损失权重最后才是改网络结构。对这份数据集我一般会在训练时给nailpolish、slingshot、firecrackers这三类加大损失权重。YOLOv8的class_weights参数可以按框数反比自动计算权重框数少的类别梯度贡献更大。如果你在用YOLOv5或者自定义训练脚本也可以手动给低框数类别乘一个1.5到2.0的系数。还有一点容易被忽略误检往往发生在形态相近的物体之间。lighter和nailpolish在X光下都是小矩形zippooil和pressure都是罐体形状knife和scissors都是长条金属。类别不平衡不只是数量问题难分样本还集中在低频类别上所以训练时不能只盯着总mAP必须按类别看AP否则pian类过低会被均值掩盖。3.3 数据划分训练集/验证集/测试集的正确切法数据集说明里提到包含测试集这很关键。但拿到手后先确认测试集是否独立成目录如果只是混在总目录里需要自己做分层划分。直接random.shuffle会有问题3600张图虽然总量够但nailpolish只有85框纯随机切分很可能让训练集里一个nailpolish框都不剩验证集反而集中了所有小样本这会导致训练时梯度崩溃。一个可用的分层划分方案是按类别分桶再按比例抽取。import random from collections import defaultdict import glob import xml.etree.ElementTree as ET xml_files sorted(glob.glob(Annotations/*.xml)) file_classes {} for f in xml_files: tree ET.parse(f) classes [obj.find(name).text for obj in tree.findall(object)] file_classes[f] set(classes) buckets defaultdict(list) for f, classes in file_classes.items(): for c in classes: buckets[c].append(f) random.seed(42) train_ratio, val_ratio 0.8, 0.1 train_files, val_files, test_files set(), set(), set() for c, files in buckets.items(): random.shuffle(files) n_train int(len(files) * train_ratio) n_val int(len(files) * val_ratio) train_files.update(files[:n_train]) val_files.update(files[n_train:n_train n_val]) test_files.update(files[n_train n_val:]) all_files set(xml_files) orphans all_files - train_files - val_files - test_files test_files.update(orphans) print(ftrain: {len(train_files)}, val: {len(val_files)}, test: {len(test_files)})这段脚本的主要思路是保证每个类别在三个集合里都有样本因为同一张图可能包含多个类别文件会出现在多个桶里最终比例会有少量偏差但比纯随机好得多。最后把孤儿文件塞进测试集保证所有图片都被使用。如果数据集的测试集是作者预先切好的直接沿用作者的划分别重复乱切。4. 用YOLOv8训练安检模型数据配置、参数设置与训练流程数据是YOLO格式最常见的做法是直接用Ultralytics YOLOv8训练。整个过程分为三步写数据集yaml跑训练观察指标。4.1 数据集YAML怎么写路径、类别名与测试集挂载YOLOv8会根据yaml里的path定位数据根目录train/val/test填相对路径指向存放图片的目录。labels目录不需要写进yamlYOLO会自动把images替换成labels来寻找对应的txt文件所以图片和txt的文件名必须完全一致。path: /data/xray_security train: images/train val: images/val test: images/test names: 0: lighter 1: pressure 2: knife 3: scissors 4: powerbank 5: zippooil 6: handcuffs 7: slingshot 8: firecrackers 9: nailpolish这里最关键的约束是names的顺序。txt文件里的类别编号是转换时按class_names列表生成的如果yaml里的names顺序和txt编号对不上模型会把所有的类别标签学错。数据集说明里给出的类别顺序是lighter、pressure、knife、scissors、powerbank、zippooil、handcuffs、slingshot、firecrackers、nailpolish这份yaml完全按这个顺序写。还有一个细节YOLOv8的path支持绝对路径如果你在Linux服务器上训练建议直接写成绝对路径避免相对路径解析问题。4.2 训练参数怎么设imgsz、batch、epochs与类别不均衡修正训练命令用CLI就能跑通不需要额外写脚本。下面这组参数是这份数据集比较靠谱的起点。yolo detect train \ modelyolov8n.pt \ dataxray.yaml \ imgsz640 \ batch16 \ epochs100 \ patience20 \ optimizerAdamW \ lr00.001 \ cos_lrTrue \ class_weightsTrue \ workers8 \ seed42参数说明imgsz用640安检X光图原始尺寸多数在1000像素以上直接缩到640会丢失小目标信息可以先拿640跑通全流程后续有余力再试1280对比效果。batch16是入门级配置如果你的显卡显存大于12G可以改成32或者用batch-1让Ultralytics自动探测上限。epochs设100配合patience20连续20轮验证指标不提升就自动停既防止过拟合又不会浪费算力。class_weightsTrue是必须开的它按类别频率反比调整损失权重能显著改善nailpolish这种低频类别的召回。workers8适合Linux环境Windows下建议改成4否则数据加载可能卡住。训练开始后会先下载yolov8n的预训练权重如果你在离线环境需要提前把yolov8n.pt放好。预训练模型对提升小样本类别的效果很明显不建议从随机权重开始训。4.3 训练中如何盯loss与保存最佳权重每个epoch结束后控制台会输出box_loss、cls_loss、dfl_loss以及验证集mAP50和mAP50-95。要看模型好不好重点盯验证集mAP50-95而不是训练loss。训练loss下降但验证指标停滞是过拟合的信号patience会在20轮内自动停止。训练结束后权重保存在runs/detect/train目录下其中best.pt是验证集mAP最高的权重last.pt是最后一个epoch的权重。做最终评估和部署时一律用best.pt。我用下面这个命令直接跑测试集评估yolo detect val \ modelruns/detect/train/weights/best.pt \ dataxray.yaml \ splittest \ imgsz640这里有一个容易忽略的点默认的yolo detect val只评估yaml里的val路径只有显式加上splittest时才会用测试集。很多人训完只看了验证集指标就下结论导致部署时发现测试集实际效果比预想低很多。5. 避坑与常见问题五个真实踩坑记录这部分是实战中血泪经验换来的每一条都可能让你白跑几小时训练。按现象、原因、解决三步写遇到同类型问题可以直接对照。5.1 图片尺寸不统一导致YOLO训练崩现象训练到一半报错could not determine image size或者某些图被跳过导致一个epoch只用了不到一半数据。原因数据集里的jpg尺寸不统一有的宽1280高1024有的宽800高600还有个别损坏的图片文件头信息缺失或尺寸字段异常YOLO的dataloader读取元数据时就崩了。解决训练前遍历所有jpg用PIL检查尺寸并剔除损坏文件。from PIL import Image from pathlib import Path bad [] for p in Path(images).glob(*.jpg): try: im Image.open(p) im.verify() w, h im.size if w 0 or h 0: bad.append(p) except Exception: bad.append(p) print(损坏图片数量:, len(bad))这个脚本只做验证不修改图片。如果发现坏图需要同时把对应的xml和txt移走保持三个目录对齐。尺寸不统一本身不是问题因为YOLO训练时会自动letterbox缩放真正的坑是坏图和尺寸元数据异常。5.2 XML里有超出图片边界的框坐标归一化后出现负数现象训练时loss正常但验证集画预测框时坐标出现负值或者部分目标一直匹配不上。原因labelImg标注时可能把框拖出图片边界xmax比width大或者ymin为负数。VOC格式只要求xmin小于xmax并没有要求框完全落在图片内部。转成YOLO格式后中心点坐标可能大于1或小于0导致归一化越界。解决在转换脚本里加一步边界裁剪。xmin max(0, xmin) ymin max(0, ymin) xmax min(img_w, xmax) ymax min(img_h, ymax) if xmax xmin or ymax ymin: continue这段代码把坐标限制在图片范围内同时剔除宽高为0或反向的无效框。这里有一个坑要注意不要直接删除越界框有些框只越界一两个像素删掉反而丢失标注先裁剪再按宽高判断要不要保留更符合原始标注意图。5.3 类别编号错位txt类别索引和yaml对不上现象训练能跑通但模型把lighter识别成knife或者PR曲线里某个类别的AP一直是0。原因txt里的类别编号是转换时生成yaml里的names顺序却可能被手改过。比如手写yaml时把knife放到了索引0而txt里第0行实际是lighter。YOLO不会校验类别对应关系它只看索引数字。解决用程序读取txt首行数字统计每个类别编号的框数和数据集说明对照。import glob from collections import Counter cnt Counter() for txt in glob.glob(labels/*.txt): with open(txt) as f: for line in f: parts line.strip().split() if parts: cnt[int(parts[0])] 1 print(sorted(cnt.items()))输出结果应该是[(0, 2529), (1, 1080), (2, 2858), ...]这样的形式对应lighter、pressure、knife。只要发现某个编号的框数和类别对不上优先检查转换脚本里的class_names顺序。这个坑在多人协作时特别常见不同人用labelImg打开时的类别下拉顺序不一样生成的txt编号就不同。5.4 测试集混进训练集数据泄漏的隐蔽形式现象测试集mAP高达0.99但部署在真实X光图上效果很差。原因数据划分时没有按文件名去重测试集和训练集存在重复图片。尤其是同一个包裹的不同角度切片外观高度相似容易同时出现在两个集合里。还有一种隐蔽情况同一张图被复制改名成两个文件名分别进了训练集和测试集。解决训练前对三个集合做文件名交集检查。def read_names(dir_path): return {p.stem for p in Path(dir_path).glob(*.jpg)} train read_names(images/train) test read_names(images/test) dup train test print(重复文件数量:, len(dup))如果发现重复把测试集里属于训练集的那份移出去。对于连续帧数据还要按拍摄时间或包裹编号分组避免同一个物体的不同角度图横跨两个集合。数据泄漏会让你的模型被高估这是我最看重的一条宁可少训练一张图也不能让测试集脏。5.5 标签缺失或空txt导致训练中断现象训练时提示No labels found in ...或者某个epoch出现NaN loss。原因部分图片对应的txt是空文件。labelImg保存时如果某张图没有标注目标会生成大小为0的txt或者预处理时误删了某些标签只留图片。YOLO的dataloader遇到空标签有时直接抛异常有时会让梯度变成NaN。解决先统计空txt数量再决定处理方式。import os from pathlib import Path label_dir Path(labels) empty [p for p in label_dir.glob(*.txt) if p.stat().st_size 0] print(空txt数量:, len(empty)) for p in empty: stem p.stem for ext in [.jpg, .xml, .txt]: fp p.with_name(stem ext) if fp.exists(): os.remove(fp)这段代码会把空txt对应图片和xml一并删除保证三个目录仍然对齐。安检场景下的“没有目标”图片其实有业务价值比如用来测试误检率但那需要单独作为负样本集不能直接混进YOLO训练否则会让模型学会输出空框。所以训练集里直接删除空标签文件是更省事的做法。6. 验证与部署技巧用测试集算mAP再检查四类误检6.1 先跑验证集看PR曲线训练完成后先跑一次yolo detect val看每个类别的PR曲线。YOLOv8会在runs/detect/val下生成PR_curve.png曲线越靠近右上角该类别效果越好。如果某个类别的曲线明显掉在中间比如此类nailpolish说明召回不足需要回看第3.2节的类别不平衡处理。在实际项目里这份数据的大类AP50通常能到0.85以上小类在0.4到0.6之间。如果你发现大类也很低优先检查yaml的names顺序而不是怀疑模型结构。6.2 用测试集做最终评估别拿验证集刷分最后用测试集跑一次完整评估yolo detect val \ modelruns/detect/train/weights/best.pt \ dataxray.yaml \ splittest \ imgsz640 \ save_jsonTruesave_jsonTrue会输出predictions.json里面包含每个检测框的类别、置信度和坐标。我会拿这个json做两个附加检查。第一找出置信度高于0.9但预测错的框这类结果通常指向标注噪声或类别编号错位需要回看第5.3节。第二统计误检集中在哪两类之间最常见的pair是pressure和zippooil都是罐体形状还有knife和scissors的长条金属形状。如果误检严重可以单独对这两个类别多copy原始样本做增强或者把NMS的IoU阈值从默认的0.5降到0.4减少重叠框被错误抑制的概率。关于置信度阈值YOLOv8默认conf阈值是0.25安检场景通常要高一些。我一般从0.45起步宁可漏检一两个小目标也不能让正常物品被误报成违禁品因为现场的人员复检成本远高于算法漏检后的人工复核。从那以后我每次拿到新数据集都会强制先跑一遍第2.3节的一致性校验再开始划分和训练这步流程已经被我焊死在操作清单里希望帮到你。本文还有配套的精品资源点击获取
返回列表