集装箱缺陷检测数据集:4127张VOC+YOLO双格式,3类缺陷实战指南
简介本资源为集装箱缺陷检测数据集面向从事工业质检、目标检测算法训练与课程实践的研究者及开发者可用于训练和验证集装箱表面缺陷识别模型。数据集采用Pascal VOC与YOLO双格式标注包含4127张jpg图片每张图片均配有对应的xml与txt标注文件标注类别为Dent、Hole、Rust三类总框数达10117个其中Dent 4943框、Hole 1218框、Rust 3956框使用labelImg按矩形框规则完成标注。压缩包为7z格式共2000个文件以1999个xml标注文件和1个说明txt为主整体约163.61MB目录结构清晰便于直接接入主流检测框架进行训练与评估。目前已有348人学习下载适合需要快速获取缺陷检测数据、开展模型对比实验或完成课程项目的读者参考使用。1. 集装箱缺陷检测数据集4127 张 VOCYOLO 双格式3 类缺陷怎么落地做集装箱表面质检的同行大概率都遇到过这个尴尬算法框架搭好了模型结构也调通了结果卡在数据上——网上能找到的公开缺陷数据集要么类别不对口要么格式只有 VOC 没有 YOLO要么标注质量参差不齐光清洗就耗掉一周。这次拆的这份集装箱缺陷检测数据集4127 张 jpg 图片配套 4127 个 Pascal VOC 格式 xml 和 4127 个 YOLO 格式 txt3 个类别 Dent凹陷、Hole破洞、Rust锈蚀总标注框 10117 个。标注工具是 labelImg规则就是矩形框没有分割路径的 txt属于典型的检测任务数据包。它解决的核心问题很直接省掉从原始图到标注再到格式转换的前半程让你把时间花在模型和调参上。适合做工业质检、港口物流视觉、表面缺陷检测的从业者也适合想跑通 YOLO 全流程的新手拿来练手。下面按「数据长什么样 → 怎么读进来 → 怎么训 → 坑在哪」的顺序拆一遍。2. 数据集结构与标注分布先看清 10117 个框的底牌拿到一个 7z 压缩包别急着解压完就往训练脚本里塞。先搞清楚目录结构、文件命名规则和类别分布这三件事决定了后面要不要做重采样、要不要改 data.yaml、要不要处理空标注。2.1 目录组织与文件对应关系从项目正文给出的文件列表看根目录下有一个「使用前必读.txt」然后是大量firc_jizhuangxiang_数字.xml文件。这种命名方式说明图片和标注是同名不同后缀配对的firc_jizhuangxiang_647.jpg对应firc_jizhuangxiang_647.xml和firc_jizhuangxiang_647.txt。解压后典型结构是这样dataset/ ├── 使用前必读.txt ├── JPEGImages/ # 4127 张 jpg │ ├── firc_jizhuangxiang_647.jpg │ └── ... ├── Annotations/ # 4127 个 VOC xml │ ├── firc_jizhuangxiang_647.xml │ └── ... └── labels/ # 4127 个 YOLO txt ├── firc_jizhuangxiang_647.txt └── ...实际压缩包里可能没有分文件夹而是全部平铺在一个目录这很常见。平铺的话写脚本按后缀分流即可。关键点是xml 数量、txt 数量、jpg 数量三者必须都是 4127任何一个对不上说明有文件丢失或命名不一致先补齐再往下走。2.2 三个类别的框数分布与影响摘要里给了明确的框数统计这个信息比图片数量更重要因为它直接反映类别不平衡程度类别英文名框数占比凹陷Dent494348.9%破洞Hole121812.0%锈蚀Rust395639.1%合计—10117100%Dent 和 Rust 加起来占了近 88%Hole 只有 12%。这意味着如果直接训练模型会明显偏向 Dent 和 RustHole 的召回率大概率拉胯。常见做法是要么在 loss 里给 Hole 加权要么对 Hole 做过采样要么用 mosaic、copy-paste 这类增强手段补 Hole 的样本。我一般会先跑一版 baseline看混淆矩阵里 Hole 被误判成什么再决定用哪种策略。另外注意10117 个框分布在 4127 张图上平均每张约 2.45 个框。这个密度不算高说明大部分图里目标不多训练时 batch 可以适当开大一点但要注意有些图可能只有 1 个框甚至个别图框数偏多做 anchor 聚类时别用默认的 COCO anchor。2.3 VOC 与 YOLO 双格式的取舍这份数据同时给了 VOC xml 和 YOLO txt很多人会纠结用哪个。我的建议是训练直接用 YOLO txt验证和可视化用 VOC xml。原因很简单YOLO 系列读 txt 是原生支持速度快而 xml 里保留了图片宽高、目标尺寸等原始信息做数据分析和可视化时更方便。YOLO txt 每行格式是class_id x_center y_center width height全部归一化到 0~1。VOC xml 里是绝对像素坐标xmin ymin xmax ymax。两者可以互转但要注意归一化时的除数是图片实际宽高不是固定值。如果图片尺寸不统一转换脚本必须逐张读取尺寸不能写死。提示解压后先跑一遍完整性校验确认 jpg、xml、txt 三者数量一致且文件名能一一对应再开始任何训练操作。3. 从压缩包到可训练解析、校验与格式转换实操这一章是整篇的核心操作区。目标是把一个刚解压的 7z 目录变成 YOLO 能直接吃的训练集中间包含解析、校验、划分、配置文件生成四步。每一步都给可抄的代码。3.1 解压与文件完整性校验7z 在 Linux 下用7z x解压Windows 下用 7-Zip 右键解压即可。解压后第一件事不是训练是校验。下面这段脚本做三件事统计三类文件数量、检查同名配对、找出孤立文件。import os from collections import Counter root dataset # 解压后的根目录 exts {.jpg: [], .xml: [], .txt: []} for f in os.listdir(root): name, ext os.path.splitext(f) if ext.lower() in exts: exts[ext.lower()].append(name) # 1. 数量统计 for ext, names in exts.items(): print(f{ext} 文件数: {len(names)}) # 2. 同名配对检查以 jpg 为基准 jpg_set set(exts[.jpg]) xml_set set(exts[.xml]) txt_set set(exts[.txt]) missing_xml jpg_set - xml_set missing_txt jpg_set - txt_set orphan_xml xml_set - jpg_set orphan_txt txt_set - jpg_set print(f缺 xml 的图: {len(missing_xml)}) print(f缺 txt 的图: {len(missing_txt)}) print(f孤立 xml: {len(orphan_xml)}) print(f孤立 txt: {len(orphan_txt)}) # 3. 打印前几个异常方便定位 print(示例缺 xml:, list(missing_xml)[:5]) print(示例孤立 txt:, list(orphan_txt)[:5])逻辑说明以 jpg 为基准集合用集合差集找出缺失和孤立文件。参数上root改成你实际解压路径即可。如果输出里missing_xml或missing_txt不为 0说明数据包在传输或解压时出了问题必须先把这些图剔除或补齐否则训练时 dataloader 会直接报错中断。3.2 解析 VOC xml 提取类别与框信息校验通过后用 xml 做一次类别分布复核确认摘要里的 4943/1218/3956 是否对得上。这一步同时能发现标注里的异常框比如宽高为 0、坐标越界。import xml.etree.ElementTree as ET from collections import Counter xml_dir dataset/Annotations class_counter Counter() box_counter Counter() abnormal [] for f in os.listdir(xml_dir): if not f.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, f)) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) for obj in root.findall(object): cls_name obj.find(name).text.strip() class_counter[cls_name] 1 bbox obj.find(bndbox) xmin int(float(bbox.find(xmin).text)) ymin int(float(bbox.find(ymin).text)) xmax int(float(bbox.find(xmax).text)) ymax int(float(bbox.find(ymax).text)) # 异常框检查 if xmax xmin or ymax ymin: abnormal.append((f, cls_name, zero_area)) elif xmin 0 or ymin 0 or xmax w or ymax h: abnormal.append((f, cls_name, out_of_bound)) print(类别分布:, dict(class_counter)) print(异常框数量:, len(abnormal)) print(异常示例:, abnormal[:5])逻辑说明ET.parse逐文件解析findall(object)拿到每个标注目标。类别名用strip()去掉可能的首尾空格这一步很关键因为 labelImg 有时会留下空格导致类别名对不上。异常框分两类零面积和越界。零面积框在训练时会产生 NaN loss越界框会被某些框架裁剪或直接报错。发现异常后要么修正坐标要么把对应图从训练集剔除。参数上xml_dir指向你的 Annotations 目录。如果类别分布和摘要给的数字有出入比如 Dent 不是 4943那说明这份数据可能被二次编辑过需要重新评估。3.3 划分训练集/验证集并生成 data.yamlYOLO 训练需要 train/val 两个列表和一份 data.yaml。划分比例常见 8:2 或 9:1工业缺陷数据我一般用 8:2保证验证集有足够样本覆盖三个类别。下面脚本按类别分层抽样避免验证集里 Hole 太少。import os, random, shutil from collections import defaultdict random.seed(42) root dataset img_dir os.path.join(root, JPEGImages) lbl_dir os.path.join(root, labels) out_dir yolo_dataset # 按类别分层读取每个 txt 的主类别 cls_to_imgs defaultdict(list) for f in os.listdir(lbl_dir): if not f.endswith(.txt): continue name os.path.splitext(f)[0] with open(os.path.join(lbl_dir, f)) as fp: lines fp.readlines() if not lines: continue # 取该图出现最多的类别作为分层依据 from collections import Counter c Counter([int(l.split()[0]) for l in lines if l.strip()]) main_cls c.most_common(1)[0][0] cls_to_imgs[main_cls].append(name) train_list, val_list [], [] for cls, names in cls_to_imgs.items(): random.shuffle(names) split int(len(names) * 0.8) train_list names[:split] val_list names[split:] # 建立 YOLO 目录结构 for sub in [images/train, images/val, labels/train, labels/val]: os.makedirs(os.path.join(out_dir, sub), exist_okTrue) for name in train_list: shutil.copy(os.path.join(img_dir, name .jpg), os.path.join(out_dir, images/train, name .jpg)) shutil.copy(os.path.join(lbl_dir, name .txt), os.path.join(out_dir, labels/train, name .txt)) for name in val_list: shutil.copy(os.path.join(img_dir, name .jpg), os.path.join(out_dir, images/val, name .jpg)) shutil.copy(os.path.join(lbl_dir, name .txt), os.path.join(out_dir, labels/val, name .txt)) print(ftrain: {len(train_list)}, val: {len(val_list)})逻辑说明先按每张图的主类别分组再在组内按 8:2 切分这样验证集里三个类别都有覆盖。random.seed(42)保证可复现。复制而非移动保留原始数据不动方便后面换划分比例重跑。生成 data.yamlpath: ./yolo_dataset train: images/train val: images/val nc: 3 names: [Dent, Hole, Rust]注意names的顺序必须和 txt 里的 class_id 对应。labelImg 导出 YOLO 格式时类别 id 是按你标注时的类别列表顺序生成的如果顺序和这里不一致训练出来的类别会全错。核对方法随便打开一个 txt看第一列的数字再对照 xml 里的 name确认映射关系。注意如果 txt 里的 class_id 从 0 开始且只有 0/1/2 三个值说明映射正常如果出现 3 或更大值说明类别数不止 3 个数据包可能混入了其他类别。4. 训练配置与参数调优3 类缺陷的 YOLO 落地细节数据准备好了接下来是训练。这一章不讲 YOLO 原理只讲针对这份数据的具体参数怎么设、为什么这么设、以及训练过程中看什么指标。4.1 模型选型与输入尺寸3 类缺陷、4127 张图属于中小规模数据集。模型不用上太大的YOLOv8n 或 YOLOv8s 就够再大容易过拟合。如果追求精度且显存充足可以上 YOLOv8m但要做强增强。输入尺寸方面集装箱缺陷的 Dent 和 Rust 通常面积较大Hole 可能偏小建议用 640×640 起步如果 Hole 召回一直上不去再试 960×960。训练命令以 YOLOv8 为例yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs150 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ patience30 \ augmentTrue \ mosaic1.0 \ mixup0.1 \ copy_paste0.1 \ projectruns/container_defect \ nameexp1参数说明epochs150配合patience3030 轮没提升就早停避免浪费时间。lr00.01是初始学习率lrf0.01是最终学习率系数余弦退火到初始的 1%。mosaic1.0开启马赛克增强对小目标 Hole 有帮助。mixup0.1和copy_paste0.1是额外增强copy_paste 对缺陷检测特别有效因为它能把缺陷区域复制到其他图上相当于免费扩充 Hole 样本。4.2 类别不平衡的处理策略前面说过 Hole 只有 1218 个框占比 12%。除了 copy_paste还有两个手段一是 loss 加权二是过采样。YOLOv8 默认的分类 loss 没有类别权重接口但可以在数据集层面做。最简单的方法是把含 Hole 的图在 train 列表里重复 2~3 次让模型每轮多见几次 Hole。# 在生成 train_list 后对含 Hole 的图做过采样 hole_imgs [] for name in train_list: with open(os.path.join(lbl_dir, name .txt)) as fp: if any(line.startswith(1 ) for line in fp): hole_imgs.append(name) # 重复 2 次即总共出现 3 次 train_list_oversampled train_list hole_imgs * 2 print(f过采样后 train: {len(train_list_oversampled)})逻辑说明line.startswith(1 )判断该图是否含 Holeclass_id1。重复后训练集从约 3300 张变成约 4000 张Hole 的出现频率明显提升。注意不要重复太多次否则模型会对 Hole 过拟合把其他类误判成 Hole。另一个手段是调整验证指标。训练时重点看metrics/mAP50-95和每个类别的metrics/mAP50如果 Hole 的 mAP 明显低于另外两类说明不平衡问题没解决回去加过采样或增强。4.3 训练过程监控与早停判断YOLO 训练会输出一堆指标新手容易看花眼。重点盯三个box_loss、cls_loss、mAP50。box_loss下降说明框回归在收敛cls_loss下降说明分类在收敛mAP50上升说明整体在变好。如果box_loss震荡不降可能是学习率太大或 anchor 不匹配如果cls_loss降不下去多半是类别不平衡或标注有噪声。早停的判断标准连续 30 轮mAP50-95没有提升就停。但要注意YOLO 的早停是基于 fitness 指标不是单纯 mAP。如果验证集太小fitness 波动大可能提前停。这时候可以把patience调到 50或者手动跑满 epoch 再选最佳权重。训练完成后用验证集跑一次评估yolo detect val \ modelruns/container_defect/exp1/weights/best.pt \ datadata.yaml \ imgsz640 \ conf0.25 \ iou0.5conf0.25是置信度阈值iou0.5是 NMS 的 IoU 阈值。这两个值直接影响召回和误报的平衡。工业场景通常更看重召回可以适当降低 conf 到 0.15~0.2宁可多报不可漏报。5. 避坑与排查标注、格式、训练里的 5 个真实翻车点这一章全是血泪经验。数据集用起来出问题90% 不是模型的问题是数据和配置的问题。下面 5 条按「现象 → 原因 → 解决」写每条都是实际踩过的。5.1 类别名带空格导致训练报 KeyError现象训练启动后报KeyError: Dent 或类别名找不到或者训练能跑但类别全乱。原因labelImg 在某些版本里导出 xml 时name标签内容会带首尾空格比如Dent而不是Dent。YOLO 解析时按字符串精确匹配带空格的类别名对不上 data.yaml 里的names。解决在解析 xml 的脚本里统一strip()或者批量清洗 xml 文件。清洗命令import os, xml.etree.ElementTree as ET xml_dir dataset/Annotations for f in os.listdir(xml_dir): if not f.endswith(.xml): continue path os.path.join(xml_dir, f) tree ET.parse(path) root tree.getroot() changed False for obj in root.findall(object): name obj.find(name) if name.text ! name.text.strip(): name.text name.text.strip() changed True if changed: tree.write(path, encodingutf-8, xml_declarationTrue)5.2 txt 里的 class_id 与 data.yaml 顺序不一致现象训练 loss 正常下降但验证时所有 Dent 被预测成 Rust或者混淆矩阵完全错位。原因labelImg 导出 YOLO 格式时class_id 是按标注时的类别列表顺序生成的。如果标注时先标了 Rust 再标 Dent那 Rust 的 id 可能是 0Dent 是 1。而 data.yaml 里写的是[Dent, Hole, Rust]顺序对不上。解决先确认 txt 里的 id 分布再反推映射。打开几个 txt看第一列的数字对照对应 xml 里的 name。如果发现 id 0 对应的是 Rust就把 data.yaml 改成[Rust, Hole, Dent]或者写脚本把 txt 里的 id 重映射。# 假设实际映射是 0-Rust, 1-Hole, 2-Dent # 目标映射是 0-Dent, 1-Hole, 2-Rust id_map {0: 2, 1: 1, 2: 0} lbl_dir dataset/labels for f in os.listdir(lbl_dir): if not f.endswith(.txt): continue path os.path.join(lbl_dir, f) with open(path) as fp: lines fp.readlines() new_lines [] for line in lines: parts line.strip().split() if parts: parts[0] str(id_map[int(parts[0])]) new_lines.append( .join(parts) \n) with open(path, w) as fp: fp.writelines(new_lines)5.3 图片尺寸不统一导致归一化坐标错乱现象训练时框的位置明显偏移或者验证可视化里框画到了图片外面。原因YOLO txt 里的坐标是归一化的归一化除数是图片实际宽高。如果转换脚本写死了 640×640而实际图片是 1920×1080坐标就全错了。解决转换时必须逐张读取图片尺寸。用 PIL 或 OpenCV 读from PIL import Image import os img_dir dataset/JPEGImages for f in os.listdir(img_dir): if not f.endswith(.jpg): continue with Image.open(os.path.join(img_dir, f)) as im: w, h im.size # 用 w, h 做归一化不要写死如果已经生成了错误的 txt只能重新从 xml 转一遍确保归一化除数正确。5.4 空标注图导致 dataloader 报错现象训练到某一步突然报IndexError或ZeroDivisionError定位到某张图。原因有些图可能没有标注对应的 txt 是空文件。YOLO 默认会跳过空 txt但如果配置里skip_empty没开或者空 txt 被当成有效样本就会出问题。解决先统计空 txt 数量决定是剔除还是保留。工业数据里纯背景图也有价值可以保留但要在 dataloader 里正确处理。YOLOv8 默认会跳过空标签图如果报错检查 txt 是否真的是空文件而不是只有换行符。empty [] lbl_dir dataset/labels for f in os.listdir(lbl_dir): if f.endswith(.txt): with open(os.path.join(lbl_dir, f)) as fp: if not fp.read().strip(): empty.append(f) print(f空标注文件: {len(empty)})5.5 验证集划分不均匀导致指标虚高现象验证集 mAP 很高但实际部署时效果差很多。原因随机划分时验证集可能恰好抽到了容易的图或者某个类别在验证集里几乎没有。比如 Hole 只有 1218 个框随机抽 20% 可能只抽到 200 个评估结果波动大。解决用分层抽样确保验证集里三个类别的框数比例和整体一致。前面 3.3 的脚本已经按主类别分层但更严格的做法是按框数分层。如果验证集 mAP 波动超过 5 个点说明验证集太小或不均衡考虑增大验证集比例到 25%。提示每次改完数据或配置先跑 1~2 个 epoch 看 loss 是否正常下降再跑完整训练。别一上来就 150 epoch浪费算力。6. 进阶技巧用 VOC xml 做数据增强与难例挖掘训练跑通只是第一步真正拉开差距的是数据层面的迭代。这份数据同时给了 VOC 和 YOLO 两种格式很多人只用 YOLO txt 训练把 xml 扔在一边其实浪费了。xml 里保留了图片宽高、目标绝对坐标做数据增强和难例分析时比 txt 方便得多。6.1 基于 xml 的离线增强YOLO 自带的在线增强mosaic、mixup已经很强但有些增强适合离线做比如针对 Hole 的 copy-paste。用 xml 读原图把 Hole 区域裁剪出来再随机贴到其他没有 Hole 的图上同时更新 xml 和 txt。这样能定向扩充 Hole 样本比单纯过采样更有效。import xml.etree.ElementTree as ET from PIL import Image import random, os def crop_hole(img_path, xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() with Image.open(img_path) as im: for i, obj in enumerate(root.findall(object)): if obj.find(name).text.strip() ! Hole: continue bbox obj.find(bndbox) xmin int(float(bbox.find(xmin).text)) ymin int(float(bbox.find(ymin).text)) xmax int(float(bbox.find(xmax).text)) ymax int(float(bbox.find(ymax).text)) patch im.crop((xmin, ymin, xmax, ymax)) patch.save(os.path.join(out_dir, fhole_{os.path.basename(img_path)}_{i}.png)) # 遍历所有含 Hole 的图裁剪出 Hole 区域 for f in os.listdir(dataset/Annotations): if f.endswith(.xml): name os.path.splitext(f)[0] xml_path os.path.join(dataset/Annotations, f) img_path os.path.join(dataset/JPEGImages, name .jpg) crop_hole(img_path, xml_path, hole_patches)逻辑说明这段脚本把所有 Hole 区域裁剪成小图存到hole_patches。然后写另一个脚本随机选一张无 Hole 的图把 patch 贴上去同时生成新的 xml 和 txt。注意贴的时候要避开已有目标区域否则框会重叠。贴完后的坐标要重新计算归一化除数用新图的宽高。6.2 用验证集做难例挖掘训练完一版模型后用验证集跑推理把误报和漏报的图挑出来人工复核标注。常见做法是用yolo detect predict输出带框的图然后按置信度排序低置信度的真阳性、高置信度的假阳性都是重点。yolo detect predict \ modelruns/container_defect/exp1/weights/best.pt \ sourceyolo_dataset/images/val \ conf0.1 \ save_txtTrue \ save_confTrue \ projectruns/predict \ nameval_checkconf0.1故意调低让模型多输出一些框方便看漏报。save_txtTrue会把预测结果存成 txt格式和标注一致可以直接和标注对比。写个脚本算每个类别的 TP/FP/FN找出 Hole 漏报最多的图回去检查标注是否有遗漏或框不准。6.3 一个具体技巧用 Rust 的框数反推标注一致性Rust 有 3956 个框Dent 有 4943 个两者数量接近但 Rust 通常面积更大、边界更模糊标注一致性容易出问题。我一般会抽 50 张含 Rust 的图把 xml 里的框画出来肉眼检查是否有漏标或框过大。如果发现某张图的 Rust 框明显偏少说明标注员可能把锈蚀当成了背景。这种图如果留在训练集里会教模型忽略 Rust导致召回下降。具体操作用 xml 读框用 OpenCV 画到图上存到rust_check目录人工过一遍。发现问题的图要么重新标要么从训练集剔除。这个步骤花不了半小时但能避免后面反复调参的玄学问题。从那以后我每次拿到新数据集都强制走一遍「数量校验 → 类别分布复核 → 抽样可视化 → 空标注检查」这四步再开始训练。希望帮到你。本文还有配套的精品资源点击获取