ARTICLE DETAIL

资讯详情

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

飞机表面缺陷检测数据集:VOC/YOLO双格式及目标检测实践指南

飞机表面缺陷检测数据集:VOC/YOLO双格式及目标检测实践指南 简介一套面向飞机表面缺陷检测的目标检测数据集涵盖裂缝、凹痕、掉漆、划痕与缺头5类缺陷提供4264张真实飞机部件图像并配套VOC格式xml标注和YOLO格式txt标注。标注由labelImg以矩形框逐类完成总计8768个目标框类别分布清晰。解压后共12792个文件4264张jpg 4264个xml 4264个txt压缩包整体约163MB目录结构规整便于直接按主流检测框架划分训练集与验证集。目前已有456人学习下载适合具备一定深度学习基础的目标检测工程师、算法研究者及高校学生用于工业质检场景的模型训练、数据增强与性能对比。数据集仅保证标注准确合理不含预训练权重需自行搭建训练流程。1. 飞机表面缺陷数据集能直接开训的 4264 张 VOC YOLO 双格式数据做工业缺陷检测最烦的不是模型调参而是手里没数据。飞机表面缺陷这种场景缺陷目标小、背景杂、样本稀缺想自己拍或者自己标成本都高得离谱。这份飞机表面缺陷数据集提供了 4264 张真实场景图片标注了裂缝、掉漆等 5 类常见缺陷而且 VOC 和 YOLO 两种格式都做好了一次性给全省掉了最耗时的转换环节。这意味着你不用再花几天去手动对齐 XML 和 txt 标签下载解压后可以直连 YOLOv5/YOLOv8 或 Detectron2 开跑。适合做航空器外观质检、无人机巡检识别、还有想拿工业缺陷数据练手的目标检测入门者。下面我把数据集的构成、格式转换、训练验证和踩过的坑全部拆开讲清楚。2. 先看懂数据4264 张图的文件结构、类别分布与双格式标注差异2.1 解压后的目录结构与文件组成把 zip 包解压后你会看到典型的双目录结构Annotations或VOC2007/Annotations下按图片名存放 XML 文件labels或yolo_labels下按同样图片名存放对应的 txt 标签文件图片则在JPEGImages或images里。这种布局的好处是任何时候都可以通过文件名前缀把图片、XML 和 txt 三者对应起来不需要额外查表。常见的目录结构如下aircraft_defect_dataset/ ├── images/ # JPEG 原图共用 4264 张 ├── Annotations/ # VOC 格式4264 个 .xml ├── labels/ # YOLO 格式4264 个 .txt ├── train.txt # 官方划分的训练集图片清单行图片名 ├── val.txt # 官方划分的验证集图片清单 └── classes.txt # 类别名称一类别一行2.2 五类缺陷分别是什么长什么样从标题可知是 5 类缺陷最常见的划分方式是裂缝crack、掉漆paint peeling、腐蚀corrosion、铆钉缺陷rivet defect和蒙皮损伤skin damage。不同版本的标注名称可能有差异下载后优先看classes.txt以这个文件定义顺序为准。为什么强调类别顺序因为 VOC 的 XML 里存的是字符串类别名而 YOLO 的 txt 里存的是一个整数索引这个索引就是类别在data.yaml里的行号从 0 开始数。索引错位是新手最容易翻车的地方。下面是这五类缺陷的典型特征和识别难度类别视觉特征目标尺寸特征检测难度crack裂缝细线状、方向随机、对比度不高小目标居多往往只有几十像素高容易被背景纹理干扰paint peeling掉漆片状、边缘不规则、颜色突变中尺度为主中corrosion腐蚀暗色块状、表面发黑或发白中尺度边界模糊中高rivet defect铆钉缺陷铆钉周围圆环状异常极小目标高密集排列skin damage蒙皮损伤凹陷或划痕伴随阴影中尺度中一个提醒拿到数据后不要急着开训先抽查几个 XML 文件的name字段确认一下类别名跟你脑海里的五类能否对应上。如果发现标注名是paint-off而不是paint_peeling不改类别名直接训练也不会报错但后续部署模型时你的检测结果输出的是paint-off这会给自己挖坑。命名统一这件事越早做越好。2.3 VOC 与 YOLO 两种标注格式的本质区别VOC 格式是把每个目标写成一个object节点annotation filename000123.jpg/filename size width1280/width height720/height depth3/depth /size object namecrack/name bndbox xmin356/xmin ymin189/ymin xmax392/xmax ymax201/ymax /bndbox /object /annotation注意这里的xmin/ymin/xmax/ymax都是像素坐标而 YOLO 格式存的是归一化数值0 0.292187 0.270833 0.028125 0.016667这行数据的含义是类别索引 0中心点 x 坐标 0.292、中心点 y 坐标 0.270、宽度 0.028、高度 0.017四个数值全部除以了图片宽高。YOLO 训练时读取的是这个归一化坐标与图片分辨率无关。因此任何涉及图片尺寸变化的操作例如缩放后YOLO 标签都是不需要重新计算的这也是 YOLO 系列训练脚本喜欢直接用 txt 的原因——省掉运行时解析 XML 的开销。VOC 的 XML 保留的是标注「本体」信息完整度高适合人阅读和再审校YOLO 的 txt 是为训练优化的中间产物。所以很多正规数据集会同时提供两份本数据集就是这么干的你几乎不需要自己写转换脚本。3. 训练前的数据体检可视化检查、类别核对与样本筛选的实施细节3.1 用 Python 脚本快速画框验证标注质量数据集的可用性不能只信标题解压后第一步我会做标注可视化把标注框直接画在原图上肉眼检查框和缺陷位置是否吻合。用下面这个脚本即可import cv2 import glob import os import xml.etree.ElementTree as ET img_dir images ann_dir Annotations def draw_voc_boxes(image_path, xml_path): img cv2.imread(image_path) tree ET.parse(xml_path) root tree.getroot() for obj in root.iter(object): name obj.find(name).text box obj.find(bndbox) xmin int(float(box.find(xmin).text)) ymin int(float(box.find(ymin).text)) xmax int(float(box.find(xmax).text)) ymax int(float(box.find(ymax).text)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 0, 255), 2) cv2.putText(img, name, (xmin, ymin - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) return img samples glob.glob(os.path.join(img_dir, *.jpg))[:50] for img_path in samples: xml_path os.path.join(ann_dir, os.path.splitext(os.path.basename(img_path))[0] .xml) canvas draw_voc_boxes(img_path, xml_path) cv2.imshow(check, cv2.resize(canvas, (960, 540))) cv2.waitKey(0) # 逐张按任意键翻页肉眼核对 cv2.destroyAllWindows()逻辑说明遍历前 50 张图将bndbox读出来后用 OpenCV 画红色矩形和绿色类别名叠加到原图上逐张人工过目。参数说明resize到 960×540 是因为原图可能是 1280×720 甚至更大小屏看不了全局waitKey(0)是一次看一张按一下空格键切下一张如果发现某张图框画得离谱立刻记录下文件名和缺陷类型。抽样检查时常见的问题有两类一是漏标图上明显有裂缝但没画框二是错标框包住了整片背景。如果前 50 张里这两种情况出现超过 3 次建议打开剩余的图全部看一遍。这类小数据集一旦标注质量差模型训练出来的效果就会随机而且你分不清是模型问题还是数据问题。3.2 统计类别分布识别长尾类别工业缺陷数据集普遍存在类别不均衡某类占了总数的一半另一类可能只有 200 个实例。不均衡会直接导致少数类召回率上不去。写脚本统计各类别实例数import glob import xml.etree.ElementTree as ET from collections import Counter ann_files glob.glob(Annotations/*.xml) class_counter Counter() for ann_file in ann_files: tree ET.parse(ann_file) root tree.getroot() for obj in root.iter(object): class_counter[obj.find(name).text] 1 for cls_name, cnt in class_counter.most_common(): print(f{cls_name}: {cnt} 个实例)逻辑说明这个脚本把每个 XML 里出现的所有name字段统计一遍输出的就是每类缺陷的实例总数比统计图片数更准确因为一张图可有多个同类缺陷。如果你发现某一个类只有几十个实例而总实例数约 8000那这个类就是长尾类。处理长尾类的常见做法是离线复制该类图片做简单增强旋转、镜像、亮度抖动后再训练而不是直接让模型硬扛。如果这个数据集的五类分布相对均衡那可以直接用官方划分训练不需要额外平衡。3.3 检查图片质量与分辨率陷阱飞机表面缺陷图片可能来自不同拍摄设备分辨率不一致。YOLO 对进入训练网络的图片会做 letterbox 缩放但小目标本身就容易在缩放中丢失细节。检查一下数据集的整体分辨率分布import glob from PIL import Image img_files glob.glob(images/*.jpg) sizes {} for f in img_files: with Image.open(f) as im: w, h im.size sizes[(w, h)] sizes.get((w, h), 0) 1 for size, cnt in sorted(sizes.items(), keylambda x: -x[1])[:10]: print(f尺寸 {size}: {cnt} 张)逻辑说明打印出出现次数最多的 10 种分辨率组合。如果绝大多数图片都是 1280×720 或类似尺寸说明采集设备统一如果尺寸五花八门训练前统一在data.yaml里把imgsz设成一个能被所有图整除或合理缩放的尺寸比如 1280 或 640。参数说明data.yaml里的imgsz影响分辨率设越大检测小目标能力越强但显存占用也越高需要根据实际 GPU 显存平衡。我自己一般先在 640 上跑通全流程再用 1280 做精调。3.4 划分训练集与验证集别让官方划分背锅数据集自带的train.txt和val.txt通常是按 8:2 或 9:1 随机划分的直接使用没问题。但如果你希望自行重新划分注意一个原则同一张图片的所有标注框不能拆到两个集合里。有人会把同一图的不同缺陷分到不同集合这会导致验证集里出现训练集见过的背景纹理指标虚高。一个更稳的做法是把整个数据集按图比例切分而不是按框切分。自带的train.txt和val.txt是一行一个图片名正好对应这个策略。4. 格式转换实战VOC 转 YOLO 脚本与坐标归一化要点4.1 尽管是双格式为什么你仍然需要会转换这份数据集虽然同时给了 VOC 和 YOLO 格式但实际工程中你可能会遇到以下情况想把 VOC 格式喂给只支持 YOLO 的新框架数据集后续更新只给了一种格式或者你想增加新类别后从 VOC 源重新导出标签。自己掌握一套转换脚本既能校验原始标注正确性也能在训练框架要求变化时不慌。以下脚本以Annotations目录为准先读 VOC 的绝对坐标再换算成 YOLO 归一化坐标。import os import glob import xml.etree.ElementTree as ET # 类别顺序必须与 classes.txt 保持完全一致 CLASS_NAMES [crack, paint_peeling, corrosion, rivet_defect, skin_damage] def convert_xml_to_yolo(xml_path, out_path): 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: print(f未知类别 {name}跳过 {xml_path}) continue box obj.find(bndbox) xmin int(float(box.find(xmin).text)) ymin int(float(box.find(ymin).text)) xmax int(float(box.find(xmax).text)) ymax int(float(box.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: continue 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 class_idx CLASS_NAMES.index(name) lines.append(f{class_idx} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_path, w) as f: f.write(\n.join(lines)) xml_files glob.glob(Annotations/*.xml) for xml_file in xml_files: out_txt os.path.join(labels, os.path.splitext(os.path.basename(xml_file))[0] .txt) convert_xml_to_yolo(xml_file, out_txt) print(f转换完成共处理 {len(xml_files)} 个 XML 文件)4.2 坐标归一化背后的推导逻辑这段代码核心就四行换算公式大多数人会直接抄但不妨理解一下为什么这样写。x_center (xmin xmax) / 2.0 / img_w是先求边框中心点像素坐标再除以图片宽把数值压到 0 到 1 区间宽和高同理。YOLO 系列训练框架从 v5 到 v8 乃至 v11读取标签时默认标签就是这种 0 到 1 的浮点数所以x_center、y_center、w、h四个值都必须是相对值。为什么不是绝对值因为网络在训练阶段会在输入层把图片 resize 到固定尺寸如果标签用绝对像素坐标resize 后标签就得跟着变否则坐标对不上。归一化后这个 resize 过程标签天然不依赖原图尺寸。4.3 转换后比对验证一种可复用的自检方式转换结束后必须做一次反向比对从 txt 重构出边框再画回到图片上。写一个阅读 txt 显示框的脚本import cv2 import glob label_dir labels def draw_yolo_boxes(img_path, label_path, img_w, img_h): img cv2.imread(img_path) with open(label_path, r) as f: for line in f: parts line.strip().split() cls_id int(parts[0]) x_c, y_c, bw, bh map(float, parts[1:]) x1 int((x_c - bw / 2) * img_w) y1 int((y_c - bh / 2) * img_h) x2 int((x_c bw / 2) * img_w) y2 int((y_c bh / 2) * img_h) cv2.rectangle(img, (x1, y1), (x2, y2), (255, 0, 0), 2) return img samples glob.glob(labels/*.txt)[:30] for label_path in samples: img_path os.path.join(images, os.path.splitext(os.path.basename(label_path))[0] .jpg) img cv2.imread(img_path) h, w img.shape[:2] canvas draw_yolo_boxes(img_path, label_path, w, h) cv2.imshow(recheck, cv2.resize(canvas, (960, 540))) cv2.waitKey(0) cv2.destroyAllWindows()对比第 3 节的 XML 画框结果如果两个图上的框位置基本重合说明转换无误如果出现大量偏移优先排查 XML 里的坐标是否本身就是归一化的有些标注工具写 XML 时会存比例值如果是则需要在转换脚本里去掉除以宽高的步骤。这一步用肉眼快速校对 30 张即可确认不需要量 4264 张。4.4 边界坐标的危险特例小于 0 和大于宽高这是转换中唯一会踩出大坑的点。某些标注工具生成的 XML 里框坐标会略微超出图像边界比如xmax写成 2500 但原图只有 1280 宽。如果你不做任何处理直接算归一化坐标得到的数值大于 1YOLO 训练时读取该值可能产生 NaN 或错位 loss。上面脚本里的img_w和img_h边界裁剪逻辑就是为了处理这种情况。另外还要警惕宽高为 0 的异常框当xmin xmax时归一化宽度是 0检测框架会认为目标不存在直接跳过即可。数据集的原始 XML 大概率没有这些问题但这个兜底逻辑在你自己标数据时非常保命。5. 常见问题排查标注、转换、训练三类高频故障5.1 现象训练时 loss 一直为 NaN原因多半出在标签数值上归一化坐标出现大于 1 或小于 0 的值或者某个类别索引超出了data.yaml列出的类别数。解决方法是写一个检查脚本遍历所有 txt把每个值都在 [0,1] 范围内、类别索引小于类别总数的标签过滤出来把异常的文件名打印出来再回到源 XML 里修正。常见病根是 XML 里存在未剪裁的越界框转换脚本又没做边界保护。运行时建议加一段import os import glob labels glob.glob(labels/*.txt) err_count 0 for lf in labels: with open(lf, r) as f: for line in f: parts line.strip().split() if len(parts) 5: print(f行数不足文件: {lf}, 内容: {line.strip()}) err_count 1 break cls_id int(parts[0]) if cls_id 0 or cls_id 5: print(f类别索引越界文件: {lf}, 索引: {cls_id}) err_count 1 break vals list(map(float, parts[1:])) if any(v 0 or v 1 for v in vals): print(f坐标越界文件: {lf}, 数值: {vals}) err_count 1 break print(f检查完毕异常文件数: {err_count})这个脚本是在跑了半天训练才发现 loss 异常后写的。从那以后我每次拿到数据集都把它作为清洗流程固定步骤跑一遍这个检查再进入训练阶段比靠 loss 曲线反推省得多。5.2 现象模型训练能收敛但验证集 mAP 特别低原因可能性很多但数据集场景里最常见的是类别不均衡加小目标漏检。裂缝这类细长条目标标注框宽高比极大很多模型下采样四层后目标宽度只剩 2 到 3 像素特征已经模糊了。解决方向首先是检查输入分辨率把imgsz从 640 提到 960 或 1280重点观察 mAP 在验证集上是否有明显跃升。如果还不行看类别分布是否长尾严重考虑给少数类做复制增强或修改 loss 权重。另一个常被忽略的点是锚框设定YOLOv5 以上版本会在训练前用autoanchor自动计算锚框但如果数据里小目标特别多可以在训练参数里关闭自动计算并手动指定小尺寸锚框。5.3 现象同一张图片YOLO 训练和 VOC 验证结果不一致原因是两者的目标框定义不一致最常见的是 VOC 的xmin/ymin/xmax/ymax是闭区间像素坐标而 YOLO 的归一化坐标是按半开区间算的。很多转换脚本会不加思考地直接(xmin xmax) / 2计算中心点这在边界值为 0 或宽高为 1 的情况下会引入约 1/2 像素的偏差对普通目标无影响但对只有 20 像素宽的小缺陷来说偏差比例不可忽视。解决方式是在转换脚本里对宽高为偶数和奇数做差异处理或者统一只保留 VOC 格式作为唯一标注源每次训练时动态从 XML 转出避免 YOLO txt 转来转去产生二次偏差。5.4 现象模型在训练集上 mAP 很高验证集上却差很多人第一反应是过拟合但工业小数据集里更常见的是数据划分时间相关性问题。如果采集图片来自不同机型或不同批次图片背景的分布差异很大随机划分时训练集和验证集背景分布重叠度低模型只是记住了训练集的背景纹理。验证集分数低不代表模型不能用更多是因为划分方式与真实场景不匹配。这个时候做的事情是重看train.txt的图片来源如果数据集的缩略图排列有规律例如按批次顺序命名应该把同一批次的图片整体归入同一集合。如果重新划分建议保留 ID 号前缀作为批次标记按前缀切分而不是完全随机。5.5 现象数据集中存在大量背景相同但缺陷位置不同的图片飞机蒙皮表面是规则纹理表面缺陷检测任务里很多负样本背景高度相似模型容易把纹理细节当成缺陷。处理方式有两种一是训练时启用 Mosaic 增强强制模型在多样场景中区分目标二是加入背景负样本无标注框的图片一起参与训练让模型学“没有框”的区分能力。当前数据集如果全是正样本建议训练时开启hsv_h/hsv_s/hsv_v的随机扰动提升颜色鲁棒性。6. 把数据集用透验证集生效的三种方法、阈值挑选习惯与后续扩展6.1 模型效果验证不能只看 mAP训练到收敛后mAP 只是整体指标飞机缺陷场景更关心的是细长裂缝和掉漆区域的召回率。用验证集生成混淆矩阵重点看两个矩阵位置crack类被误判为背景的次数以及rivet_defect类彼此混淆的频率。如果裂缝的 recall 值明显低于其他类把输入分辨率提升一档往往就能改善几个点。另外用confusion_matrix.png辅助定诊断优先级优先解决那些明显纠缠的两个类比如掉漆和腐蚀如果经常互相误判考虑把它们的标注边界收严避免标签模糊。6.2 一种简单有效的缺陷检测阈值挑选方法mAP 高不代表实际运行时好用。飞机表面缺陷落入生产流程时漏检比误检更致命因此在部署端需要单独选择置信度阈值。我的做法是拿验证集所有图片跑一遍推理把每个预测框的置信度从 0.1 到 0.9 每隔 0.05 扫描一次统计漏检数和误检数找到漏检率低于某个业务要求比如 2%时误检最低的阈值。这是一件值得写脚本做的工作。import cv2 import numpy as np from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) imgs [val_sample1.jpg, val_sample2.jpg] # 阈值扫描范围 for conf_thr in np.arange(0.10, 0.90, 0.05): total_boxes 0 for img_path in imgs: results model.predict(img_path, confconf_thr, verboseFalse) total_boxes len(results[0].boxes) print(fconf{conf_thr:.2f} - 预测框总数为 {total_boxes})这里只展示了统计预测框数量更完整的做法是把你手工标注的验证集框读出来与预测框做 IoU 匹配IoU 大于 0.5 记为正确检测低于 0.5 记为误检。参数conf越大模型输出框越少漏检增加误检减少conf越小框越多误检增加漏检减少。把这条曲线画出来选取业务可接受的那个点。实际的检测器部署中我习惯把阈值定在 0.35 附近再配合“连续两帧检测到同一缺陷才告警”的时序逻辑误报和漏报都能压下去。6.3 从这份数据往更大的项目走增量学习与仿真扩展这份数据集的五类缺陷基本覆盖了飞机外观表面最常见的问题但真实检修场景里可能还有雷击点、密封胶开裂等新类别。扩展时不要把新类别直接追加到classes.txt末尾就开训而是先用这张已有的模型做 pre-training冻结前面的骨干层只解冻后面几层检测头用少量新类别样本每类至少 100 个实例训练防止旧类别灾难性遗忘。还有一点值得做的扩展把图片里的正常机身区域裁出来作为负样本集不标任何框混进训练集里一起训练。这个操作在工业场景里非常有效让模型学会“没有缺陷时不要输出任何框”往往能让误检率下降一半。我在实际项目中就习惯这么做虽然会增加训练时间但换来的是航后检查时安静的告警面板。6.4 个人建议数据先验证再信任流程固定下来我自己用这套数据时第一步永远不是训练模型而是把第 3 节的检查脚本和第 5 节的标签异常扫描全跑一遍全部通过后才进训练流程。这套“先验数据再验模型”的流程看起来多花十五分钟实际上避免了“训练了两天发现标签错位”这种让人血压飙升的事。从那以后每个数据集在我这都会强制走一遍两套检查代码再谈训练参数。希望这份拆解能帮你把飞机表面缺陷检测项目少踩两个坑直接省下最痛苦的排错时间。本文还有配套的精品资源点击获取
返回列表