集装箱缺陷检测数据集:VOC与YOLO双格式实战指南
简介本资源为面向工业质检与计算机视觉方向的集装箱缺陷检测数据集适用于目标检测模型训练、算法验证及课程实践可帮助开发者解决集装箱表面缺陷样本不足、标注格式不统一的问题。数据集共1476张jpg图片每张均配有对应的VOC格式xml标注与YOLO格式txt标注标注工具为labelImg采用矩形框方式涵盖Deframe、Dent、Hole、Rusty、Scratch五类缺陷总标注框数达4227个其中Rusty与Scratch样本较为丰富。压缩包内共2000个文件以1476个xml与524个txt为主整体约163.76MB目录结构清晰便于直接接入YOLO或Pascal VOC训练流程。目前已有494人学习下载适合从事缺陷检测、工业视觉及深度学习目标检测的中级学习者快速开展实验与模型调优。1. 1476张集装箱缺陷图为什么值得你花一个下午跑通集装箱表面缺陷检测这件事真正卡住大多数团队的从来不是模型结构而是数据。你手里如果只有几百张手机拍的锈蚀照片再怎么调 YOLO 的 anchor 和损失函数mAP 也上不去。这个标题里的「1476张5类缺陷 VOCYOLO 格式」之所以值得关注是因为它一次性把目标检测缺陷检测数据集最费时间的两个环节——标注格式和类别定义——替你走完了。VOC 和 YOLO 双格式意味着你既可以用 Pascal VOC 那套 XML 流程做数据清洗和可视化也可以直接喂给 ultralytics 系的 YOLOv8/v11 训练不用自己写转换脚本。5 类缺陷通常是锈蚀、凹陷、裂缝、污渍、变形这类集装箱表面高频问题1476张的体量属于「小样本但够跑通 baseline」的区间适合做目标检测入门、验证 pipeline、或者作为工业质检方向的预研起点。如果你正在找一份能直接下载、解压、改路径就能开训的集装箱缺陷数据这篇就是按这个目标写的。2. 拆开这个数据集5类缺陷、VOC与YOLO双格式到底怎么对应2.1 集装箱缺陷的5个类别为什么这样分拿到一份缺陷检测数据集第一件事不是急着训练而是搞清楚类别定义背后的物理含义。集装箱表面缺陷在工业场景里通常按「成因 形态」划分锈蚀是电化学腐蚀导致的片状或点状变色区域凹陷是机械撞击后的几何形变裂缝是应力集中产生的线状断裂污渍是油污或残留物覆盖变形则是整体或局部的结构扭曲。这5类里锈蚀和污渍在低分辨率下容易混淆凹陷和变形在边界框标注上容易重叠这是后面训练时 loss 震荡的主要来源。从目标检测的角度看这5类的尺度差异很大裂缝可能是细长条长宽比极端锈蚀和污渍可能是大面积不规则区域凹陷和变形则介于两者之间。这意味着你在设置 anchor 或者用 YOLO 的自动 anchor 时不能只盯着 COCO 那套默认值。1476张图分摊到5类平均每类不到300个实例属于典型的小样本多类别场景数据增强策略比模型选型更关键。提示先别管模型把每一类的实例数量统计出来。如果某一类少于150个实例训练时就要考虑过采样或者针对性增强否则该类别的 recall 会明显偏低。2.2 VOC的XML和YOLO的txt到底差在哪VOC 格式每张图对应一个 XML 文件里面记录了图片尺寸、每个目标的类别名和边界框的左上角、右下角坐标。YOLO 格式每张图对应一个 txt 文件每行是「类别索引 中心x 中心y 宽 高」全部归一化到 0 到 1 之间。两者的核心差异不只是坐标表示还有类别索引的映射关系——VOC 用字符串类别名YOLO 用从0开始的整数索引这个索引顺序必须和你的 data.yaml 里的 names 列表严格一致否则训练出来的模型会把锈蚀认成裂缝。另一个容易翻车的点是坐标越界。VOC 的 XML 里偶尔会出现 xmax 大于图片宽度、或者 ymin 为负数的情况这通常是标注工具导出时的精度问题。直接转 YOLO 会导致归一化后的值超出 0 到 1训练时虽然不会报错但那些框会变成无效样本白白浪费算力。我一般会在转换脚本里加一步 clamp 操作把坐标强制裁剪到图片范围内。2.3 用Python把VOC转成YOLO并做完整性校验下面这段脚本做三件事解析 VOC XML、转成 YOLO txt、同时校验坐标越界和类别缺失。你只需要把 voc_dir 和 out_dir 换成自己的路径。import os import xml.etree.ElementTree as ET from pathlib import Path # 类别顺序必须和 data.yaml 里的 names 完全一致 CLASSES [rust, dent, crack, stain, deform] CLASS_TO_ID {c: i for i, c in enumerate(CLASSES)} def convert_voc_to_yolo(voc_dir, out_dir): voc_dir Path(voc_dir) out_dir Path(out_dir) out_dir.mkdir(parentsTrue, exist_okTrue) bad_files [] for xml_file in voc_dir.glob(*.xml): tree ET.parse(xml_file) 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.findall(object): cls_name obj.find(name).text.strip() if cls_name not in CLASS_TO_ID: bad_files.append((xml_file.name, f未知类别: {cls_name})) continue bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 坐标裁剪防止越界导致归一化异常 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) if xmax xmin or ymax ymin: bad_files.append((xml_file.name, 无效框: 宽或高为0)) continue cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{CLASS_TO_ID[cls_name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) if lines: out_file out_dir / (xml_file.stem .txt) out_file.write_text(\n.join(lines), encodingutf-8) if bad_files: print(f发现 {len(bad_files)} 个问题文件:) for name, reason in bad_files[:10]: print(f {name}: {reason}) else: print(转换完成未发现异常) if __name__ __main__: convert_voc_to_yolo(./VOCAnnotations, ./labels)这段代码的关键参数有三个CLASSES 列表的顺序决定了 YOLO 的类别索引必须和后续 data.yaml 里的 names 一一对应坐标裁剪那四行是防止 XML 里的脏数据污染训练集最后的 bad_files 收集逻辑让你知道哪些文件被跳过了而不是默默丢掉。跑完之后检查一下输出的 txt 文件数量是否和图片数量一致如果少了说明有图片没有对应的标注或者标注全被过滤了。2.4 划分训练集验证集时别用随机种子糊弄很多人划分数据集就是 random.shuffle 一刀切这在缺陷检测里是个隐患。集装箱图片往往来自同一批拍摄同一批里可能有连续帧或者同一角度的多张图随机划分会导致训练集和验证集里出现高度相似的样本验证指标虚高实际部署时翻车。我一般会按图片的文件名前缀或者拍摄批次做分组划分保证同一批次的图片只出现在训练集或验证集其中一边。import random from pathlib import Path from collections import defaultdict def split_dataset(img_dir, label_dir, out_dir, train_ratio0.8): img_dir Path(img_dir) label_dir Path(label_dir) images sorted(img_dir.glob(*.jpg)) sorted(img_dir.glob(*.png)) # 按文件名前缀分组假设前缀代表拍摄批次 groups defaultdict(list) for img in images: prefix img.stem.split(_)[0] groups[prefix].append(img) group_keys list(groups.keys()) random.seed(42) random.shuffle(group_keys) split_idx int(len(group_keys) * train_ratio) train_groups set(group_keys[:split_idx]) for split_name in [train, val]: (out_dir / split_name / images).mkdir(parentsTrue, exist_okTrue) (out_dir / split_name / labels).mkdir(parentsTrue, exist_okTrue) for prefix, imgs in groups.items(): split_name train if prefix in train_groups else val for img in imgs: label label_dir / (img.stem .txt) if not label.exists(): continue # 这里用软链接或复制按你的磁盘空间决定 target_img out_dir / split_name / images / img.name target_label out_dir / split_name / labels / label.name target_img.write_bytes(img.read_bytes()) target_label.write_bytes(label.read_bytes()) print(f训练集组数: {len(train_groups)}, 验证集组数: {len(group_keys) - len(train_groups)}) if __name__ __main__: split_dataset(./images, ./labels, ./dataset, train_ratio0.8)分组划分的逻辑是先按文件名前缀把图片归类再以组为单位随机分配到训练集或验证集。这样同一批次的图片不会跨集出现验证指标更接近真实部署表现。train_ratio 设0.8是常规做法但如果你的总组数少于20组建议调到0.7保证验证集有足够的组数覆盖不同场景。3. 用YOLOv8跑通第一个集装箱缺陷检测baseline3.1 data.yaml的四个字段一个都不能错YOLO 训练的第一步是写 data.yaml这个文件只有四个关键字段但每一个写错都会导致训练失败或者类别错乱。path 是数据集根目录train 和 val 是相对于 path 的训练集和验证集图片路径names 是类别名列表。names 的顺序必须和前面转换脚本里的 CLASSES 完全一致差一个位置模型学到的就是错的映射。path: /home/user/container_defect_dataset train: train/images val: val/images names: 0: rust 1: dent 2: crack 3: stain 4: deform注意names 里用数字加冒号的写法是 YOLOv8 的要求不要写成列表形式。如果你用的是 YOLOv5列表和字典两种写法都支持但为了兼容性建议统一用字典。3.2 从零训练还是加载预训练权重1476张图属于小样本从零训练几乎不可能收敛到可用的精度。我一般会加载 COCO 预训练的 yolov8s.pt 或者 yolov8m.pt前者参数量小、训练快后者精度更高但需要更多显存。集装箱缺陷的纹理和 COCO 里的日常物体差异较大但底层边缘和纹理特征仍然可以迁移冻结 backbone 训练几轮再解冻全量微调是小样本场景的常规操作。yolo detect train \ data./data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ patience20 \ freeze10 \ project./runs \ namecontainer_baseline参数说明epochs 设100是起步值配合 patience20 做早停验证集 loss 连续20轮不降就停imgsz640 是 YOLOv8 的默认输入尺寸如果你的图片分辨率远大于640可以调到1280但显存占用会翻倍freeze10 表示冻结前10层小样本场景下这能防止 backbone 被少量数据带偏lr0 和 lrf 分别是初始学习率和最终学习率因子0.01 到 0.0001 的衰减对大多数缺陷检测任务够用。3.3 训练过程中该盯哪几条曲线训练启动后runs/container_baseline 目录下会生成 results.csv 和一系列曲线图。新手最容易犯的错是只看 mAP 不看 loss。实际上box_loss 和 cls_loss 的下降趋势比 mAP 更能反映模型是否在正常学习。如果 box_loss 震荡剧烈通常是学习率太大或者 batch size 太小如果 cls_loss 下降很慢可能是类别不平衡或者标注质量有问题。另一个关键指标是验证集的 recall。集装箱缺陷检测在实际部署时漏检的代价往往比误检高所以 recall 比 precision 更值得关注。如果某一类的 recall 明显低于其他类回去检查那一类的实例数量和标注质量大概率是样本太少或者标注不一致。4. 集装箱缺陷检测的避坑与排查清单4.1 训练loss不降反升现象训练开始几轮后 box_loss 或 cls_loss 持续上升mAP 接近0。原因通常是学习率过大或者 data.yaml 里的路径写错导致加载了空标签。先检查 labels 目录下是否有对应的 txt 文件再确认 txt 里的类别索引是否在 names 范围内。如果路径和标签都没问题把 lr0 降到0.001再试。4.2 某一类缺陷完全检测不到现象验证时某一类的 mAP 为0其他类正常。原因大概率是该类实例数量过少或者该类在标注时被大量漏标。先统计每一类的实例数如果少于100个用过采样或者 copy-paste 增强补充。同时抽查该类图片的标注确认没有把锈蚀标成污渍这类类别混淆。4.3 验证集指标很高但实际图片检测效果差现象验证集 mAP 到0.8以上但拿新图片测试时漏检严重。原因通常是训练集和验证集划分时没有按批次分组导致验证集图片和训练集高度相似。回到第2.4节的划分脚本按文件名前缀分组重新划分再训练一次对比。4.4 推理时框重叠严重现象同一张图里同一个缺陷被多个框重复检测。原因是 NMS 的 iou 阈值设得太高或者模型对某些类别的置信度普遍偏低。在推理时把 iou 阈值从默认的0.7降到0.5同时把 conf 阈值从0.25提到0.4能过滤掉大部分重叠框。如果仍然严重说明训练时正样本分配有问题检查 anchor 是否匹配缺陷的尺度分布。4.5 图片分辨率差异大导致训练不稳定现象数据集中既有800x600的小图也有4000x3000的大图训练时 loss 波动明显。原因是 YOLO 在训练时会统一 resize 到 imgsz大图被压缩后小缺陷几乎消失。解决办法是在数据清洗阶段把分辨率差异过大的图片筛掉或者统一缩放到一个中间尺寸再送入训练。我一般会把短边小于640的图片直接排除保证缺陷在 resize 后仍然可见。5. 把1476张图用到极致小样本下的增强与验证技巧小样本缺陷检测的核心矛盾是模型容量大了过拟合容量小了学不到特征。1476张图跑 YOLOv8s 已经接近这个数据量的上限再大的模型就需要更强的正则化。我常用的增强组合是 Mosaic MixUp 随机旋转 亮度对比度扰动其中 Mosaic 对小样本特别有效因为它在一张图里拼接四张图变相增加了每个 batch 看到的实例数量。但 Mosaic 的关闭时机很关键YOLOv8 默认在最后10轮关闭 Mosaic这个设置对缺陷检测同样适用因为开启 Mosaic 时边界框的上下文被破坏关闭后模型能更好地学习真实场景下的缺陷形态。验证阶段我习惯做两件事一是用 t-SNE 把 backbone 输出的特征降维可视化看5类缺陷在特征空间里是否分得开如果锈蚀和污渍混在一起说明模型还没学到区分性特征需要针对性增加这两类的对比样本二是用混淆矩阵看类别间的误判方向如果裂缝大量被误判为变形检查这两类的标注边界是否清晰裂缝的细长框和变形的矩形框在标注时容易互相包含。from ultralytics import YOLO import numpy as np from sklearn.manifold import TSNE import matplotlib.pyplot as plt model YOLO(./runs/container_baseline/weights/best.pt) # 提取验证集特征这里用模型中间层输出 features [] labels [] for result in model.predict(source./dataset/val/images, streamTrue, saveFalse): # 取 backbone 最后一层特征做全局平均池化 feat result.boxes.data[:, :4].mean(dim0).cpu().numpy() if len(result.boxes) 0 else np.zeros(4) features.append(feat) labels.append(result.boxes.cls.cpu().numpy() if len(result.boxes) 0 else np.array([-1])) # 注意这里只是示意实际特征提取需要 hook 模型中间层 # 更可靠的做法是用 model.model 的 forward hook 拿 backbone 输出上面这段代码只是示意特征提取的思路实际做 t-SNE 需要用 PyTorch 的 hook 机制拿 backbone 的中间层输出而不是用检测框坐标。检测框坐标做 t-SNE 只能看框的分布看不到类别间的特征差异。如果你不想折腾 hook一个更简单的替代方案是看验证集的混淆矩阵YOLOv8 训练完成后会自动生成 confusion_matrix.png直接看那个图就能判断类别间的混淆方向。最后一个习惯每次训练完把 best.pt 在验证集上跑一遍把漏检和误检的图片单独挑出来看。漏检的图里往往藏着标注遗漏误检的图里往往有背景干扰。把这两类图整理出来要么补标要么加到训练集的负样本里下一轮训练就能看到明显提升。这个迭代过程比调参更有效也是我从多次翻车经历里总结出来的最实在的一条。希望帮到你。本文还有配套的精品资源点击获取