竹子缺陷检测数据集构建指南:从标注到YOLO训练全流程
发布时间:2026/10/2 3:27:15来源:尧图网络
简介竹子缺陷检测数据集是一份面向目标检测任务的标注数据资源适合使用YOLO系列、Faster Rcnn、SSD等深度学习模型训练竹子幼芽缺陷检测场景覆盖Bud、Sprouted_Bud、Damage_Bud三个类别。压缩包内共2000个文件包括1999个txt标注文件和1个yaml配置文件整体大小77.18MBtxt文件采用YOLO格式记录目标框位置与类别yaml文件则定义类别名称与配置直接服务于模型训练前的数据读取与标签解析。目前已有357人学习下载。数据集包含2953张图片对应的标注信息类别划分贴近实际种植监测需求txt标注文件命名与图像一一对应方便按目录整理训练集与验证集yaml配置让YOLO系模型开箱即用。无论用于教学实验还是实际缺陷检测项目都能为数据准备环节节省时间。1. 竹子缺陷检测数据集为什么目标检测不能跳过数据这一关竹板材厂的质检工位前一张竹片从传送带滑过工人要在两三秒内判断出节疤、虫眼、霉变、开裂这几类缺陷分别在哪、有多严重。人眼会疲劳标准会因人而异于是很多产线开始想用目标检测模型替代人工初检。可模型不是天生就会认竹子缺陷的它吃的每一口「知识」都来自数据集——这就是竹子缺陷检测数据集要解决的问题把缺陷的位置和类别变成一张张带标注框的图片喂给YOLO这类目标检测模型。这篇笔记按我做过类似工业视觉数据集的流程来讲从缺陷分类、采集规范到标注格式、训练参数和踩坑记录适合正在做竹木加工质检、或者想自己从零构建一个目标检测数据集的人。2. 缺陷类别与采集规范一张竹板上到底有哪些东西要框2.1 先把缺陷类别定清楚活节、死节、虫眼、霉变、开裂的边界做目标检测数据集的第一步不是拍照而是定类别。类别的定义直接决定标注一致性和模型上限。竹子缺陷检测常见的类别有五类活节、死节、虫眼、霉变、开裂。活节是竹节处还连着竹青、颜色偏绿或偏黄、边缘平滑的节疤死节则是节疤已经干枯、颜色发黑或发褐、和周围竹材有明显分界。如果产线还关心竹条侧边的损伤可以再加一类「磕碰伤」但我不建议一上来就分太细五类以内是最好标、最好训练的区间。为什么类别边界要单独说因为标着标着就会出现分歧同一个深色节疤A标注员标「死节」B标注员标「霉变」。这不是标注员不认真而是两类在视觉上确实有重叠区域。我的做法是先做一份带示例图的标注规范把「死节和霉变的判定优先级」写死节疤形状完整、边界清晰、且周围竹纤维未变色时归死节颜色呈放射状晕染、边界模糊时归霉变。这份规范不用很长但要配上典型图和边界案例图让所有标注员对照着标。2.2 采集设备和光照参数手机拍的和工业相机拍的差距在哪采集设备方面如果你的目标是产线部署建议直接用产线拟用的工业相机拍摄分辨率建议不低于500万像素这样一张竹板上最小的虫眼可能只有十几个像素才不会丢失。如果只是做算法预研、验证流程用手机后置摄像头也能起步但要注意保持拍摄距离固定不要一会近一会远——距离变了缺陷的尺度就变了模型会误以为「大节疤」和「小节疤」是两种东西。光照是竹子缺陷检测里最容易被低估的变量。竹材表面有反光强光直射会产生高光区域把霉变和正常纹理之间的对比度吃掉光线不均匀又会让同一张竹片的两端色温不一致。我一般会这样设置采集条件用两个LED条形灯以45度角从左右两侧打光照度控制在800到1200勒克斯相机镜头与竹片表面垂直拍摄距离在30到50厘米之间固定下来。这个参数组合的特点是能把节疤的边缘阴影和霉变的颜色晕染都保留下来同时不产生镜面反射。采集时把竹片放在纯色深灰或黑背景上方便后期如果要做背景替换增强。采集数量上记住一个底线每个类别至少要有300到500个实例框。注意我说的是「实例框」不是「图片张数」——一张竹片上往往有十几个节疤所以如果每类有100张图、每张平均4个实例那这一类就有400个实例框够用。缺陷样本实在不够的类别比如霉变宁可先跑一批只有三类的小模型也不要硬把五类都塞进去训练类别不平衡的后果到第5章细说。3. 标注与格式转换把竹子缺陷整理成YOLO能吃的格式3.1 用目标检测常用标注工具把缺陷框出来labelImg还是labelme标注工具的选择取决于你要的框形态。竹子缺陷里节疤、霉变、开裂基本都是近似矩形或可以用矩形包住的形状虫眼虽然是圆形但矩形框也足够表达位置所以用labelImg就够了。它是目标检测常用标注工具里上手最快的打开图片画框选类别保存下一张。labelme虽然支持多边形标注更精细但标注成本高竹节疤这种边缘不规则的区域反而容易标得参差不齐。打开labelImg后先把类别文件准备好一行一个类名比如live_node dead_node insect_hole mildew crack这一步对应的是在labelImg里点击「Change Save Dir」旁边的类别列表。类名建议全小写加下划线不要用中文也别写「defect1」这种看不出含义的名字否则后面分析混淆矩阵时你会看不懂哪一类是哪一类。标注时的操作规范有两条一是框要紧贴缺陷可见边缘比缺陷外轮廓大上2到3个像素即可不要为了省事把整段竹节都框进去二是如果一个缺陷被另一个缺陷部分遮挡各标各的框不要合并模型学习的是独立缺陷的视觉模式。3.2 把labelImg的XML转成YOLO的txt转换脚本与四个参数细节labelImg默认保存为Pascal VOC格式的XML文件而YOLO训练需要的是每张图对应一个同名的txt文件每一行代表一个框。转换逻辑不复杂但参数细节容易错。下面这个脚本是我常用的转换方式import os import xml.etree.ElementTree as ET from glob import glob # 类别顺序要和训练时的data.yaml保持一致 CLASSES [live_node, dead_node, insect_hole, mildew, crack] def convert_xml_to_yolo(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) txt_name os.path.splitext(os.path.basename(xml_path))[0] .txt lines [] for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in CLASSES: continue cls_id CLASSES.index(cls_name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # YOLO格式要求归一化的中心点坐标和宽高 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 # 防止边缘框越界后数值大于1 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(w, 1.0) h min(h, 1.0) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(os.path.join(out_dir, txt_name), w) as f: f.write(\n.join(lines)) if __name__ __main__: os.makedirs(labels, exist_okTrue) for xml_file in glob(annotations/*.xml): convert_xml_to_yolo(xml_file, labels)这段脚本有三个地方要特别说明。一是CLASSES列表的顺序就是类别ID训练时的data.yaml里names必须按这个顺序写否则模型输出的类别会错位——这是最常见的低级错误一旦names顺序对不上训练出来的模型在推理时会把节疤认成虫眼。二是宽高的归一化用的是图片原始宽高如果你的数据集里混有不同分辨率的图片这不是问题因为每一张都按自己的宽高归一化。三是加了越界截断防止标注框边缘刚好落在图片边界时计算出的数值略大于1导致训练报错。3.3 转换后先做一次目录结构体检转换完成后不要急着训练先检查目录结构。按YOLO系列的惯例数据集目录是这样的bamboo_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamlimages和labels下的train、val、test三个子目录必须一一对应任何一张只在images里而没有对应txt的图片训练时会被跳过反过来一张只有txt没有图片的样本等于白标了。我最常犯的错是转换后忘了把images和labels按相同比例划分到子目录结果train里有300张图labels/train里只有280个txt训练时YOLO不报错但莫名其妙少了20张图的数据。检查手段很简单用一行命令统计两边的文件数是否一致for d in train val test; do echo $d: images$(ls images/$d | wc -l) labels$(ls labels/$d | wc -l) done这里ls统计的是文件数量如果发现不一致去labels里找缺失的对应txt基本就是这一张图的XML解析出问题了比如类别名写了中文或多了空格被脚本里的if cls_name not in CLASSES过滤掉了。把这类问题图挑出来修XML比重新标注一整批要省钱得多。3.4 用可视化脚本抽查标注质量格式转换通过不等于标注正确。我每次转换完都会写一个简单的OpenCV脚本把YOLO格式的txt画回图片上人工扫一遍。import cv2 def draw_yolo_boxes(img_path, txt_path, class_names): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: for line in f.readlines(): cls_id, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[int(cls_id)], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img class_names [live_node, dead_node, insect_hole, mildew, crack] img draw_yolo_boxes(images/train/001.jpg, labels/train/001.txt, class_names) cv2.imwrite(check_001.jpg, img)这个脚本本身没什么技术含量价值在于把「标注框和缺陷的真实边界是否贴合」这件事变成肉眼可见。我一般会从每个类别里随机抽20张图做这种可视化检查重点看两类问题一是大缺陷只标了局部比如一根贯穿半张竹片的裂纹只标了一小段——目标检测模型会学出一堆半截框二是小缺陷框过大比如虫眼旁边的正常纹理也被包进去——模型会把这些纹理特征一起学进虫眼这个类。标注质量直接决定模型上限这一步花掉的时间会在训练阶段加倍省回来。4. 划分数据集与训练前的预处理参数怎么设才不翻车4.1 按「竹片」划分而不是按「缺陷框」划分一个容易忽略的原则数据集划分看起来是基础操作但竹子缺陷检测有个特殊问题同一张竹板上的多个节疤之间高度相似如果把同一张图片的缺陷框同时分到训练集和验证集模型在验证时就是开卷考试——它已经见过同一块竹板的纹理和光照了val的mAP会虚高等上线到新产线立刻打回原形。正确的做法是按图片竹片划分保证同一张竹片的所有框只出现在一个集合里。划分脚本如下核心是用文件名做去重import os import random from glob import glob from collections import defaultdict random.seed(42) image_files glob(images_raw/*.jpg) # 按文件名前缀去重同前缀的图和框必须进同一个集合 samples defaultdict(list) for img in image_files: prefix os.path.splitext(os.path.basename(img))[0] samples[prefix].append(img) prefixes list(samples.keys()) random.shuffle(prefixes) train_ratio, val_ratio 0.8, 0.1 n_train int(len(prefixes) * train_ratio) n_val int(len(prefixes) * val_ratio) train_prefixes set(prefixes[:n_train]) val_prefixes set(prefixes[n_train:n_train n_val]) test_prefixes set(prefixes[n_train n_val:]) # 按划分结果移动文件 for prefix, paths in samples.items(): if prefix in train_prefixes: dest train elif prefix in val_prefixes: dest val else: dest test for p in paths: os.rename(p, fimages/{dest}/{os.path.basename(p)})这段脚本里的random.seed(42)不是玄学而是让每次运行的结果可复现——如果你调整了数据集内容想重新划分固定seed可以让新旧结果的差异只来自数据本身。比例上8:1:1是常规配置但如果你的缺陷类别分布很不均匀比如虫眼样本特别少可以考虑把val的比例降到0.05保证train里能多留几个虫眼样本。注意划分必须在转换YOLO格式之前或者之后同步进行确保images和labels的移动方式完全一致否则就回到了3.3里文件数对不上的问题。4.2 训练前的最后检查空标签、越界框、类别数量统计划分完成后真正训练前还有一道检查工序。我见过太多人划分完直接开训练跑了一夜发现数据集里有一批空标签或者类别全部错位。这个统计脚本可以一次性把这些常见问题暴露出来import os from glob import glob stats {c: 0 for c in [live_node, dead_node, insect_hole, mildew, crack]} empty_labels [] for txt in glob(labels/train/*.txt) glob(labels/val/*.txt): with open(txt) as f: lines f.readlines() if len(lines) 0: empty_labels.append(txt) continue for line in lines: cls_id int(line.split()[0]) stats[list(stats.keys())[cls_id]] 1 print(类别分布:, stats) print(空标签数量:, len(empty_labels))这个脚本输出的类别分布有两个用途。一是看有没有类别完全没标上——尤其是转换脚本里类别名写错时某类会被静默过滤统计结果里对应类就是0。二是看类别不平衡的程度最好的类别和最差的类别如果相差10倍以上训练时就要考虑类别权重或者先少训几类。空标签文件本身就是脏数据训练时不会报错但会让模型从一张没有目标的图里学会「预测为空」所以发现空标签直接删掉对应图片或者重新补标。最后给出一个最常见的YOLO训练命令以yolov8n为例你也可以换yolov5s、yolov11n参数逻辑一致yolo detect train \ databamboo_dataset/data.yaml \ modelyolov8n.pt \ epochs200 \ imgsz640 \ batch16 \ patience50 \ projectbamboo_run \ nameexp_01对应的data.yaml里names顺序必须和转换脚本的CLASSES一致names: [live_node, dead_node, insect_hole, mildew, crack]。训练参数里最关键的是imgsz640——竹材表面缺陷尺度差异极大虫眼可能只有20个像素如果缩到320虫眼就彻底消失了patience50是早停轮数连续50轮val mAP不提升就自动停止防止后面几百轮纯属浪费时间。这个命令跑完才是检验数据集真正质量的时刻。5. 竹子缺陷检测的5个常见坑现象、原因与解法5.1 标注框偏移导致mAP卡在0.3上不去现象训练loss正常下降val的mAP50却卡在0.3到0.4之间无论怎么调epochs和batch都不动。原因标注框整体偏移常见于快速标注时框没有贴紧缺陷边缘导致模型学到的特征是「缺陷一段背景」。这个问题在竹节疤这种大色块缺陷上表现不明显在虫眼和细裂纹上会被放大因为小缺陷的框偏移一点IOU就掉得厉害。解决用3.4的可视化脚本按类别抽查优先看虫眼和开裂这两类小目标的框。发现问题后不要全局重标只修正偏移明显的图修正完重新转换格式再训一次。如果偏移是系统性的所有框都偏右上方可以用脚本批量校正坐标偏移不要手工一张张挪。5.2 霉变样本太少导致类别被「吃掉」现象训练集里节疤有2000个框霉变只有60个框结果模型在验证集上对霉变的recall接近0。这不是模型笨是它在训练时发现「预测什么都是节疤」就能拿到很低的loss。原因目标检测的数据集天然存在长尾分布缺陷出现的频率本身就不一样。采集时想凑齐霉变样本往往很难因为霉变竹片本来就少。解决有两种有效思路。一是把霉变单独做一轮小样本微调先用全部数据训练基础模型再用霉变样本为主的子集哪怕只有100张图微调10到20轮避免灾难性遗忘的同时恢复对少样本类别的敏感度。二是加针对性的数据增强对霉变样本做随机亮度扰动和HSV增强因为霉变的颜色分布范围本来就广这类增强能让模型学到「颜色晕染」这个共性特征。5.3 一个缺陷被标了两个框模型输出的框会互相打架现象训练和验证都正常但推理时同一个节疤位置模型输出了两个高置信度框NMS怎么都压不掉。原因标注阶段同一缺陷被不同标注员各标了一次两个框高度重叠但又不完全一样模型把这种「一个目标两个框」的模式学进去了导致推理时如果目标出现在两者中间位置NMS的阈值设置不合适就会保留两个框。解决这类问题要在标注规范里明确「重叠超过50%的框必须合并或删除」。已经标好的数据可以用脚本做一次批量去重计算所有框的IOU对同一张图上IOU大于0.7的框保留置信度高的或面积更大的那个。跑完这个清洗脚本再统计一次类别分布你会发现很多隐性脏数据一起被清掉了。5.4 过拟合训练集mAP高、验证集mAP低现象训练集上的mAP到了0.9验证集只有0.6且随着训练轮数增加两者差距还在拉大。原因竹子缺陷数据集往往只有几百到一两千张图模型容量远大于数据量于是开始死记硬背训练集的光照、背景甚至标注框的位置。这在数据采集阶段就埋下了隐患——如果采集时只在一条产线、一种光照下拍模型学到的就是「那条产线的光照模式」。解决先从数据增强入手。YOLO的增强参数里hsv_h0.02、hsv_s0.7、hsv_v0.5是常用的颜色增强幅度再把degrees10小角度旋转、translate0.1、scale0.5调出来。如果用了增强后验证集mAP还是低去检查是否训练集和验证集来自同一批竹片——如果是说明你的val本质上不算是「新数据」需要重新按4.1的划分逻辑采集一批独立数据。5.5 部署现场换光照就翻车现象模型在测试集上mAP有0.85一上产线准确率掉到0.6现场工程师反馈「白天还好晚上开灯就识别不准」。原因数据集的采集条件太单一模型依赖了光源角度和色温这些与缺陷无关的环境特征。竹子表面反光强光照一变霉变的色温和节疤的阴影完全不一样。解决这是数据集层面的问题靠调参救不了。补采一批不同光照条件下的数据是正路产线如果白天晚上都运行就白天和晚上各采一批如果条件不允许退而求其次在训练时把hsv_v的随机范围调大比如hsv_v0.8再配合随机亮度扰动能稍微缓解但治标不治本。做工业项目时数据采集方案里一定要写清楚「覆盖光照变化」这个要求否则后面返工的成本很高。6. 验证与进阶用mAP和混淆矩阵找出漏检的缺陷类型训练完成后先跑一次验证集评估别急着去改训练参数。YOLO的val命令会输出一张柱状图和一个表格里面逐类列出了precision、recall、mAP50、mAP50-95。看的时候有个顺序先看每类的mAP50看哪类最低再看recallrecall低的类别说明「漏检」precision低的类别说明「误检」。比如开裂的recall只有0.4说明模型一半以上的裂纹没框出来虫眼的precision低说明框出来的十个里有几个是假的。这两个指标分别指向不同的修法recall低的去补数据或调低置信度阈值precision低的去清洗标注或调高阈值。混淆矩阵也值得单独看。YOLO的val输出里带有混淆矩阵图行是真实类别列是预测类别。最值得关注的是对角线之外的高亮格子——如果dead_node和mildew之间有大量互相混淆基本就是标注规范里两类边界没划清回到2.1去补边界案例图如果insect_hole被漏检到background那一列里说明虫眼过小可以尝试把imgsz从640提到960或者给虫眼单独做切片训练。这一步的价值在于把「模型表现不好」这个模糊的感觉拆成「哪一类、漏检还是误检、原因在哪」三个具体问题。进阶一点的做法是难例挖掘。把val里所有预测错误的图导出按置信度从高到低排序置信度高却预测错比如0.8置信度把节疤认成开裂的图说明存在模型已经学会但关联错误的特征置信度低却漏检的图说明模型的注意力分配有问题。这些难例不要丢加进训练集里再做一轮微调比盲目增加同类型数据有效得多。如果数据量足够也可以在难例集上专门训练一个二分类模型做瑕疵复审但那已经是产线方案的范畴了。我做竹子这类细长工件的数据集习惯是每次改完标注或数据分布都先跑一轮短训练50轮看趋势确认mAP在涨再跑全量200轮。这个习惯帮我避免了很多次「跑了一整夜发现是数据集坏了」的尴尬。希望这篇笔记能让你在构建自己的竹子缺陷检测数据集时少走几步弯路一次把数据这关过扎实。本文还有配套的精品资源点击获取
网站建设高端定制企业官网