集装箱缺陷检测:VOC与YOLO双格式数据集解析
2026/9/10 2:07:38 网站建设 项目流程

简介:这是一份面向集装箱表面缺陷检测的标注数据集,涵盖Dent(凹痕)、Hole(穿孔)、Rust(锈蚀)三类缺陷,采用Pascal VOC与YOLO双格式存储,适合用于计算机视觉目标检测模型的训练与基准评测,可服务工业质检场景下的缺陷自动识别研究。压缩包共2000个文件,其中包含1999个xml标注文件和1个使用说明txt文件,整体压缩体积约163.61MB。所有标注均通过labelImg工具逐框绘制,标注类别框数统计为Dent 4943个、Hole 1218个、Rust 3956个,总计10117个目标框,标注信息准确、格式规范,可直接被主流检测框架解析。资源已吸引347人学习/下载,适合需要集装箱或金属表面缺陷样本的开发者、科研人员及算法工程师,可将其作为模型微调、数据增广与性能评估的重要数据基础。

1. 集装箱缺陷检测:为什么用 VOC 与 YOLO 双格式数据集

集装箱表面缺陷里的 Dent(凹陷)、Hole(孔洞)、Rust(锈蚀)是港口和物流场景里最常被算法点名的三类问题。用 labelImg 标注过数据的人都知道,工具默认产出的是 Pascal VOC 的 xml 文件,而 YOLO 系列训练需要 txt 文件,两者互相转换时一不小心就会把归一化坐标写错。这套数据集直接同时提供 jpg、xml、txt,4127 张图像、10117 个边界框,省掉格式转换这一步,也顺带解决了“下载完只有 xml 却不知道对应哪种 txt 格式”的常见尴尬。适合两类人:想快速跑通 yolo 目标检测流程的初学者,以及做工业视觉算法选型、需要在统一基准上对比模型的工程师。官方说明也写得很明确:数据集只保证标注准确且合理,不保证训练出的模型精度,所以更适合当验证集和 benchmark 资源,而不是“跑完就上线”的现成模型包。

2. 数据集内部结构与标注文件字段解析

2.1 目录组织与文件命名规则

把 7z 压缩包解压后,能看到的是firc_jizhuangxiang_647.jpgfirc_jizhuangxiang_647.xmlfirc_jizhuangxiang_647.txt这样的三件套。前缀firc_jizhuangxiang是场景标识,数字编号相当于样本 ID,图片、XML、TXT 共用同一个 ID,按前缀就能对齐。摘要里特别提到“TXT 不包含分割路径”,也就是说里面没有 train/val 子目录,所有文件平铺在一个目录里,这对后续我写校验脚本反而更友好。

标注类别英文名称框数占比
凹陷Dent494348.9%
孔洞Hole121812.0%
锈蚀Rust395639.1%
合计-10117100%

占比按总框数 10117 计算。能看到 Hole 的框数只有 Dent 的四分之一不到,属于典型的类别不均衡分布,后面做训练参数调整时得专门处理。

2.2 VOC XML 字段逐段说明

Pascal VOC 格式是目标检测里最常见的中间格式之一。labelImg 保存的 XML 大概长这样:

<annotation> <folder>firc_jizhuangxiang</folder> <filename>firc_jizhuangxiang_647.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>Dent</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>312</xmin> <ymin>244</ymin> <xmax>587</xmax> <ymax>391</ymax> </bndbox> </object> </annotation>

这里最关键的是<size><bndbox>size记录原图宽高,bndbox的四个值全部是像素绝对坐标;name字段是类别名,必须是DentHoleRust三者之一。如果标注时手滑把类别名写成小写,后续转 YOLO 时类名映射就会错乱。truncateddifficult在 labelImg 里默认保存为 0,对这套数据影响不大,但如果你自己补充标注,建议把被遮挡或边缘缺角的样本标记difficult=1,否则训练时模型会把残缺目标当成正常目标学习。

2.3 YOLO TXT 的归一化坐标换算

YOLO 格式每一行是class_id x_center y_center width height,其中中心点和宽高都做了归一化。原始 XML 里的xmin等像素值怎么变成 txt 里的浮点数?下面这段脚本是 labelImg 之外最常用的转换方式:

import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_path, class_list): tree = ET.parse(xml_path) root = tree.getroot() w_img = int(root.find("size/width").text) h_img = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_list: continue cls_id = class_list.index(name) box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) x_center = (xmin + xmax) / 2.0 / w_img y_center = (ymin + ymax) / 2.0 / h_img bw = (xmax - xmin) / w_img bh = (ymax - ymin) / h_img lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines))

这段代码的转换逻辑是:先把xminxmax求平均得到像素中心点,再除以图像宽,得到归一化中心;宽度用xmax - xmin再除以宽。四个计算结果都应该落在 0 到 1 之间才算合法。参数class_list的顺序必须和后续训练 yaml 里的names完全一致,如果顺序写反,比如把Hole放在索引 0,那么所有的Dent框都会被当成Hole来训练。训练时 YOLO 会按 txt 中的归一化值,换算回原图分辨率计算 IoU,如果你除以的是 resize 后的尺寸而不是原图size,所有框都会系统性偏移,这是最容易踩的坑。

3. 训练前先校验:图像-标注一致性检查与边界框统计

3.1 一致性校验脚本

拿到数据先别急着开训。我在工业项目里的习惯是,把解压后的数据过一遍自动化校验,避免标注文件和图片 key 不齐导致训练时 dataloader 报错。一个最小校验脚本如下:

import os from pathlib import Path import xml.etree.ElementTree as ET data_dir = Path("firc_jizhuangxiang") imgs = {p.stem: p for p in data_dir.glob("*.jpg")} xmls = {p.stem: p for p in data_dir.glob("*.xml")} txts = {p.stem: p for p in data_dir.glob("*.txt")} miss = [] for stem in imgs: if stem not in xmls or stem not in txts: miss.append(stem) print("missing annotation:", len(miss)) for stem, xml_path in list(xmls.items())[:5]: tree = ET.parse(xml_path) width = int(tree.findtext("size/width")) height = int(tree.findtext("size/height")) for obj in tree.findall("object"): xmin = int(obj.findtext("bndbox/xmin")) ymin = int(obj.findtext("bndbox/ymin")) xmax = int(obj.findtext("bndbox/xmax")) ymax = int(obj.findtext("bndbox/ymax")) if xmin < 0 or ymin < 0 or xmax > width or ymax > height: print("out of bound:", stem, obj.findtext("name"))

这里miss列表只统计缺失标注的图片,不统计多余标注,因为 labelImg 偶尔会在误操作时留下空 xml,这种文件不影响训练但会降低有效样本数。后段的边界检查只抽了前 5 个 xml,实际批量校验时不要切片,遍历全部 4127 个文件。如果输出大量out of bound,先看这类框是否位于图像边缘,有些相机畸变会导致标注框略超出画面,YOLO 训练时会出现images with box out of bounds的警告,数量少时可以过滤,数量多就要考虑修正坐标。

3.2 统计每个类别的框数与图片分布

除了校验,还需要统计脚本看类别数量以及单张图片的目标数。摘要里已经给了总框数,但拿到真实文件后仍要自己验证一遍,并观察是否存在单张图片框数特别多的离群样本。

from collections import Counter counter = Counter() per_image_count = [] for xml_path in data_dir.glob("*.xml"): tree = ET.parse(xml_path) objs = tree.findall("object") per_image_count.append(len(objs)) for obj in objs: counter[obj.findtext("name")] += 1 print(counter) print("max boxes per image:", max(per_image_count))

运行后counter理应输出Dent=4943, Hole=1218, Rust=3956。如果不一致,说明压缩包里的某些 xml 和摘要描述有版本偏差,或者存在重复标注文件,这时就要回源头确认。per_image_count的最大值如果超过 30,需要留意这张图是不是一个集装箱全景挤了几十个目标,这类样本在小 batch 训练里容易产生较大的梯度波动,可以单独抽出来放到 val 集。

只看 xml 还不够,严谨起见应对比 xml 里object数量与 txt 非空行数。下面的 bash 循环能快速核对全部文件:

for f in *.xml; do n_xml=$(grep -c "<object>" "$f") base=${f%.xml} n_txt=$(wc -l < "$base.txt") if [ "$n_xml" -ne "$n_txt" ]; then echo "$base mismatch: xml=$n_xml txt=$n_txt" fi done

grep -c "<object>"统计的是包含该字符串的行数,labelImg 生成的 xml 中每个<object>单独占一行,所以结果是可信的。这条命令适合在解压目录内执行,如果出现 mismatch,通常是有人手工改过 txt 但没有同步 xml。

3.3 常见误用:直接开训前的隐患

很多新手会直接扔给 YOLO 训练,但我见过最多的三个问题:第一是数据集没有预划分 train/val,默认把全部图片用于训练,最后得到一个虚高的 mAP;第二是 xml 和 txt 并存,Ultralytics 的 dataset yaml 只读取 txt,如果之前有人把 xml 转换成 txt 时搞混了归一化顺序,代码不会报错,模型却什么都学不到;第三是类别名大小写问题,XML 里是Dent,TXT 里是0,yaml 里names必须按对应顺序写。

现象可能原因处理方式
训练时 FileNotFoundError图片存在但 txt 缺失用脚本补齐或删除该样本
验证时 mAP 接近 0class 顺序与 yaml 不一致检查 class_list 顺序
loss 正常但预测全部为空图像分辨率与 imgsz 差异过大观察 resize 后小目标比例
训练警告 out of bounds标注坐标超出原图尺寸裁剪或移除该框

这个表格是我每次迁移数据集时都会贴在笔记里的检查清单。训练前的十分钟校验,能省掉训练后两小时的排错时间。

4. 基于 YOLOv8 训练集装箱缺陷检测模型:从 YAML 到损失曲线

4.1 数据集 YAML 配置与 train/val 划分

YOLOv8 通过 YAML 文件找到图片和标签。这个数据集原始目录没有划分,需要先整理成images/trainlabels/trainimages/vallabels/val结构。我一般用固定随机种子的 Python 脚本来做,保证每次划分结果一致:

import random from pathlib import Path import shutil random.seed(42) src = Path("firc_jizhuangxiang") for split in ["train", "val"]: (Path("dataset") / "images" / split).mkdir(parents=True, exist_ok=True) (Path("dataset") / "labels" / split).mkdir(parents=True, exist_ok=True) stems = list(src.glob("*.jpg")) random.shuffle(stems) val_size = int(len(stems) * 0.15) for i, img in enumerate(stems): split = "val" if i < val_size else "train" shutil.copy(img, f"dataset/images/{split}/{img.name}") shutil.copy(img.with_suffix(".txt"), f"dataset/labels/{split}/{img.with_suffix('.txt').name}")

这里用shutil.copy而不是move,目的是保留原始文件,避免脚本写错时无法回到初始状态。随机种子固定为 42,复现实验结果时不用重新随机。val_size取 4127 的 15%,约 619 张图用于验证,其余 3508 张用于训练,三个类别在 train 和 val 中的比例会基本保持一致,不需要额外做分层采样。

对应 yaml 文件:

path: /path/to/dataset train: images/train val: images/val names: 0: Dent 1: Hole 2: Rust

path建议写绝对路径,避免不同机器上相对路径解析出错。names顺序必须和 txt 第一列编号一致:Dent 是 0,Hole 是 1,Rust 是 2。如果把Hole放到索引 0,那么所有标注为 Dent 的框都会被当成 Hole 参与训练,验证时 mAP 会直接崩掉。

4.2 训练参数与类别不均衡处理

完成划分后,执行训练:

yolo detect train model=yolov8n.pt data=container_defect.yaml epochs=120 imgsz=640 batch=16 patience=20 fl_gamma=0.5 project=container_defect

model=yolov8n.pt作为基线不是因为它精度最高,而是因为 n 尺寸最快,能先验证数据集本身能否收敛;后续再换 s/m。imgsz=640与原始图片分辨率匹配,这套数据大部分是接近 1920×1080 的集装箱图像,缩到 640 会损失一部分小目标信息,但能明显降低显存占用。batch=16在中等显存下可用,如果爆显存就降到 8,并相应增加 epoch。patience=20是早停轮数,前 20 轮 loss 没有下降就停止,避免无效空转。fl_gamma=0.5是 YOLOv8 的 focal loss 参数,用来控制难易样本权重:Hole 只有 1218 框,相对 Dent 属于少样本类,调成 0.5 会让模型更关注分类置信度低的难例,但不要设到 1.0 以上,否则训练初期梯度震荡很严重。

类别不均衡还有另一种做法:加权采样。如果fl_gamma=0.5后 Hole 的召回率仍然低,可以按类别频率给 Hole 对应的图片重复采样几次。但这个数据集本身只有 4127 张,Hole 框数少不代表分布均匀,可能集中在少数几张图上,简单过采样必须保证图片级随机性,否则模型会把同一张图的背景细节背下来,泛化反而更差。

4.3 训练过程观察与过拟合判断

训练时重点看两条曲线:box_losscls_lossbox_loss下降平滑说明 anchor 分配和边界框回归正常;cls_loss在类别不均衡下容易在前 20 轮波动,这是 focal loss 起作用的正常现象。YOLOv8 的损失函数由三部分组成:box 损失用 CIoU 衡量预测框与真实框的重合度,分类损失用 BCEWithLogitsLoss,DFL 损失负责预测边界框的分布。如果 DFL 曲线长时间不降,通常不是数据集问题,而是学习率太高导致边框分布震荡。我在实际训练中碰过典型现象:Rust 的精度很高但 Hole 的召回率只有 40%,原因不是标注错,而是 Hole 框通常较小,缩到 640 后只有 30×30 像素左右,模型对小目标不敏感。解决办法是imgsz=960,或者把增强里的mosaic降到 0.5,降低小目标被裁剪的概率。

最后再强调一句:这个数据集的声明说明确说不保证模型精度,所以不要拿一个 epoch 的 mAP 去横向比较其他资源,也不要因为某类召回率低就怀疑标注质量。它最合理的用法,是当一套标注可信的基准集,用来验证网络结构、增强策略和训参流程。

5. 数据集落地技巧:7z 解压、哈希校验与 COCO 格式导出

5.1 Linux 下用 7z 命令行解压并校验完整性

下载后的文件是 7z 压缩包,Windows 下可以用 7-Zip,Linux 服务器上我直接用命令行:

7z x firc_jizhuangxiang_4127.7z -o./data sha256sum firc_jizhuangxiang_4127.7z

如果7z命令不存在,先安装 p7zip-full。-o参数后面不要有空格,解压路径会直接创建。压缩包比较大的时候,解压前先运行sha256sum记录哈希,解压后再比对一次,能确认文件不是因网络传输截断而损坏。

5.2 快速导出 COCO JSON 做对比实验

有些对比实验框架要求 COCO 格式,可以把 xml 直接转成一个 JSON 文件。核心结构如下:

import json from pathlib import Path import xml.etree.ElementTree as ET coco = { "images": [], "annotations": [], "categories": [ {"id": 0, "name": "Dent"}, {"id": 1, "name": "Hole"}, {"id": 2, "name": "Rust"} ] } ann_id = 0 xml_iter = Path("firc_jizhuangxiang").glob("*.xml") for idx, xml_path in enumerate(xml_iter): coco["images"].append({"id": idx, "file_name": xml_path.with_suffix(".jpg").name}) root = ET.parse(xml_path).getroot() for obj in root.findall("object"): cls_name = obj.findtext("name") cat_id = {"Dent": 0, "Hole": 1, "Rust": 2}[cls_name] b = obj.find("bndbox") x1 = float(b.findtext("xmin")) y1 = float(b.findtext("ymin")) x2 = float(b.findtext("xmax")) y2 = float(b.findtext("ymax")) coco["annotations"].append({ "id": ann_id, "image_id": idx, "category_id": cat_id, "bbox": [x1, y1, x2 - x1, y2 - y1], "area": (x2 - x1) * (y2 - y1) }) ann_id += 1 json.dump(coco, open("instances_container.json", "w"))

这段脚本省略了没有对象的 xml 过滤,也没有做坐标裁剪,但已经能直接导入 detectron2 或 mmdetection 做对比。注意 COCO 的category id从 0 开始还是从 1 开始取决于目标框架:Ultralytics 从 0,原生 COCO 从 1,导出前先查清楚框架要求,不然类别会全部错位。

5.3 用 val 图片做快速预测验证

训练结束后不要只看 mAP,直接抽一张 val 图跑一次推理:

yolo detect predict model=runs/container_defect/weights/best.pt source=dataset/images/val/firc_jizhuangxiang_1028.jpg save_txt=True

把输出的 txt 和原标注 xml 放在一起对比,哪个框漏了、哪个框坐标偏了,比看曲线更直观。这一步能最快发现类别顺序错乱和坐标偏移问题,也是我验证整个训练流程是否真正跑通的标准动作。

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

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

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

立即咨询