药丸缺陷检测数据集实战:VOC/YOLO格式转换与训练避坑指南
简介面向药丸外观质检与工业缺陷检测的实用数据集涵盖污染、裂纹与合格品三个类别共2759张真实药丸图片同时提供Pascal VOC格式的xml标注和YOLO格式的txt标签适用于目标检测、缺陷分类等计算机视觉任务。压缩包大小约411MB文件总数为2000个以xml与txt为主要标注类型不包含分割路径解压后即可配合图片直接使用。所有标注均使用labelImg工具完成遵循矩形框规则污染框904个、裂纹框960个、合格品框934个总计2798个有效标注框类别间框数分布较为均衡。目前已有220人学习使用适合工业质检场景下的缺陷检测模型训练、目标检测算法研发以及相关课程教学实验。借助这份数据用户可省去繁重的数据准备与标注整理工作快速进入模型调优和效果验证阶段也便于在统一的数据集基础上开展不同算法的横向对比研究。1. 药丸的缺陷检测数据集VOCYOLO格式2759张3类别到底能拿来干什么拿到「药丸的缺陷检测数据集VOCYOLO格式2759张3类别」这批数据我首先想到的不是图片数量而是「能不能直接训练出一个能上产线的模型」。工业质检里药丸类缺陷检测是个非常典型的细分场景崩边、缺角、污渍、异色单颗药丸尺寸小产线节拍快人工质检疲劳度高视觉检测几乎是唯一靠谱的出路。2759张图带标注对于一个3类缺陷检测任务来说属于「偏少但可用」的量级关键在于你拿到手之后怎么组织、怎么划分、怎么避开标注格式里那些坑。这篇文章我会按自己实际做同类项目时的流程把数据集的结构、格式转换、训练配置、踩坑经验和数据扩展方式完整过一遍适合正要拿这类数据集练手或者做落地验证的工程师。2. 打开数据集之前2759张图和3个类别到底意味着什么2.1 三类缺陷的常见形态合格品、崩边缺角、表面污渍药丸缺陷检测数据集的3个类别绝大多数情况下对应的是「good、chip、stain」这种组合也就是合格药丸、崩边缺角、表面污渍或异色。崩边缺角这类缺陷的标注难度比普通目标检测高很多因为缺角区域可能只占整颗药丸的几分之一边界模糊有些崩边发生在药丸侧面的弧度上在2D图像里看就是一小条暗色阴影容易和背景阴影混淆。污渍类别则更麻烦药丸表面的糖衣反光、传送带上的灰尘、光源不均匀造成的暗角都可能被误标成污渍标注和模型都会在这类样本上反复摇摆。如果你拿到的数据集类别定义不是这个组合也完全不意外有些厂家会把异色单独拆成第4类有些则把装瓶破损也并进来所以第一件事永远是看类别清单和每类的bbox数量分布而不是直接开训。同一张图片里药丸的密集程度也是重要的先验信息。如果是散粒检测单张图可能有十几粒甚至几十粒药丸每粒药丸都对应一个标注框这类图对标注框的精度要求极高差几个像素在小目标上的AP影响会被放大如果是泡罩板或瓶装检测药丸排列规则背景简单模型相对好收敛。2759张图放在这两个不同场景下实际效果差异会非常大前者可能勉强够用后者则比较宽裕。检查类别平衡度这件事我建议在解压后立刻做一次。直接用脚本统计每个类别出现的框数你会发现大概率存在明显的不平衡合格品框数可能占了大半崩边和污渍的框数只有零头。这个不平衡会直接反映在训练损失和最终mAP上后面第4章会给出对应的处理手段。先看一眼分布心里有个底比训到一半再回头排查省时间得多。2.2 VOC与YOLO两种格式并存一个给人看一个给模型吃这个数据集同时给出了VOC格式和YOLO格式VOC格式的核心是每张图片对应一个XML文件里面记录图片尺寸、目标类别名和真实像素坐标的xmin、ymin、xmax、ymaxYOLO格式则是每张图片对应一个txt文件每行就是一个目标5个数字分别是class_id、x_center、y_center、width、height且坐标全部归一化到0到1之间。两种格式在坐标语义上完全不同混用会出大问题但绝大多数标注工具导出的是VOC格式大多数训练框架吃的却是YOLO格式所以数据集提供两种格式更多是替你省掉了格式转换这第一步。我个人的使用习惯是永远以YOLO格式为基准做训练把VOC格式当作数据校验和人工审阅的辅助形态因为XML里的类别名是人能直接看懂的而YOLO的class_id如果和names映射对不上就是灾难。还要注意一个细节VOC格式里XML文件通常会带difficult和truncated字段有些标注工具会写0有些干脆不生成这两个字段。转换脚本里如果对这两个字段做了解析遇到没有difficult字段的XML会直接报KeyError这不代表数据坏了是你的解析脚本容错不够。同理某些导出工具生成的XML里类别名可能带空格比如chip 肉眼看不出来程序匹配却会失败。处理这类历史包袱的办法是在写转换脚本时统一做strip()并对无法识别的类别名打印警告而不是静默跳过。2.3 解压后第一件事核对图片与标注的四大一致性拿到数据集后不要着急跑训练先用最小成本做一次完整性检查。我自己固定会跑一组检查第一图片文件和标注文件是否一一对应有没有只有图没有标注、或者只有标注没有图的孤儿文件第二每张图的标注框坐标是否都在图像尺寸范围内有没有框跑出画面边界第三类别编号和类别名映射是否和训练预期一致第四图片本身能不能正常解码有些数据集混入0字节损坏文件训练到一半报错会打断整个流程。#!/bin/bash # 在数据集根目录下运行检查图片与标注文件是否一一对应 for img in images/*.jpg; do base$(basename $img .jpg) if [ ! -f labels/$base.txt ]; then echo missing label for $img fi done这段脚本的逻辑很简单遍历所有jpg图片把文件名去掉后缀后去labels目录找同名txt找不到就输出一条告警。实际数据集里图片可能是png、jpeg、bmp混合存在用jpg做后缀匹配会漏掉一部分更稳妥的做法是用find把所有图片文件枚举出来再逐个做basename匹配脚本同理只不过把后缀改成通配符。这类检查脚本不用写得多优雅目的是把「人眼看不出来的缺失」一次性暴露出来。# 检查VOC格式XML里每个目标是否合法 import xml.etree.ElementTree as ET import os xml_dir annotations img_sizes {} # 此处mobile省略为演示实际应从图像文件读取 for name in os.listdir(xml_dir): if not name.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, name)) root tree.getroot() size root.find(size) w float(size.find(width).text) h float(size.find(height).text) if w 0 or h 0: print(fbad size in {name}) for obj in root.findall(object): box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) if x1 0 or y1 0 or x2 w or y2 h or x2 x1 or y2 y1: print(fbad box in {name}: {x1},{y1},{x2},{y2})这段Python会遍历所有XML文件依次检查图像尺寸字段是否合理、每个目标框的坐标是否在图像范围内、矩形宽高是否为正。很多标注工具在标注时允许框略微超出图像边缘导出时也不会自动裁剪这类「越界框」在模型训练时会带来两个隐患一是归一化后坐标可能大于1二是数据增强时如果发生裁剪操作目标可能只剩一小部分模型学到的是残影而不是缺陷。发现越界框之后我一般会在转换阶段做clip修剪而不是直接删掉这个目标因为药丸缺陷本身就是小目标删一个框就等于丢失一条有效样本。3. XML与TXT的相互转换基于VOC和YOLO格式的对账脚本与参数说明3.1 VOC XML的关键字段size和object是唯二真正重要的VOC格式的XML从数据结构上看字段很多但真正在转换时用到的只有两个区域根节点下的size节点记录width、height、depth以及每个object节点里的name和bndbox。读懂这两块整个转换就完成了一大半。size节点里width和height是像素单位而object的bndbox里xmin、ymin、xmax、ymax也都是像素坐标这四个数字共同描述了一个不旋转的矩形框。很多标框工具还支持标注旋转框那种情况会使用rotation字段但这个数据集是标准VOC格式默认没有旋转目标处理起来会简单很多。转换的核心公式只有一组x_center (xmin xmax) / 2 / widthy_center (ymin ymax) / 2 / heightbox_w (xmax - xmin) / widthbox_h (ymax - ymin) / height。这个公式里最容易出错的地方是忘记除以图像宽高导致输出的是像素坐标而不是归一化坐标。另一个容易出错的地方是x_center和box_w的顺序YOLO格式每一行是class_id x_center y_center w h如果你写成class_id x1 y1 x2 y2模型训练时不会报错但精度会一塌糊涂因为所有坐标都错位了。3.2 XML转YOLO的完整脚本目录、类别映射与容错下面是一个我常用的xml转yolo脚本带类别名映射和坐标裁剪可以直接在这个数据集上使用。import xml.etree.ElementTree as ET import os import argparse def xml_to_yolo(xml_path, txt_path, class_names): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w float(size.find(width).text) h float(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in class_names: print(funknown class {name} in {xml_path}) continue cls_id class_names.index(name) box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) x1 max(0, min(x1, w)) y1 max(0, min(y1, h)) x2 max(0, min(x2, w)) y2 max(0, min(y2, h)) if x2 x1 or y2 y1: print(fskip invalid box in {xml_path}) continue xc (x1 x2) / 2 / w yc (y1 y2) / 2 / h bw (x2 - x1) / w bh (y2 - y1) / h lines.append(f{cls_id} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--xml_dir, requiredTrue) parser.add_argument(--txt_dir, requiredTrue) parser.add_argument(--names, nargs, default[good, chip, stain]) args parser.parse_args() os.makedirs(args.txt_dir, exist_okTrue) for fn in os.listdir(args.xml_dir): if not fn.endswith(.xml): continue xml_path os.path.join(args.xml_dir, fn) txt_path os.path.join(args.txt_dir, fn.replace(.xml, .txt)) xml_to_yolo(xml_path, txt_path, args.names)这段脚本做四件事解析XML、按类别名获取class_id、对越界坐标做clip、输出归一化的YOLO文本。参数上只需要指定xml目录、txt输出目录和目标类别名列表。特别要说明的是--names这个参数它的顺序会直接影响class_id的数值训练框架的names配置文件必须与它保持完全一致。比如脚本里我写的默认顺序是good、chip、stain那么训练配置里names也必须是同样顺序一旦改成stain、chip、good模型输出类别就会整体错位这是最隐蔽也最容易发生的低级错误。另外一个值得注意的点是脚本里做了name.strip()处理这是用来消化标注工具产生的尾随空格实际项目里没少被这种坑折磨过。3.3 转换完成后做什么5分钟数据对账检查转换完后不建议直接开始训练先做一轮数值校验重点看三点。第一随机打印几个txt文件的前几行人工确认class_id是不是预期的编号x_center、y_center在0到1之间width和height大于0且小于1。第二把归一化坐标反算回像素坐标叠加到原图上画框可视化检查框的位置是否贴合药丸边缘这一步能发现标注框整体偏移、类别标签张冠李戴等批量问题。第三统计所有txt里每个class_id出现的次数和XML里的类别统计对比验证转换过程没有丢目标。整个检查过程用不了几分钟但能避免训练完发现数据标注大面积错乱的白白浪费。还有一个我在实际项目里养成的习惯保留转换脚本和类别清单到数据集目录下和图片标注放在一起。一个数据集过两三个月重新拿出来用大概率会忘记当初class_id是怎么排的有脚本和清单就能快速复现上下文。如果你是接过别人做了一半的数据集更要搞清楚原始标注是从哪个工具导出的、XML的字段是否被二次处理过这些记录在项目交接时比模型权重还珍贵。4. 用2759张图把检测模型跑起来训练划分、模型选型与参数设置4.1 训练集/验证集划分工业生产数据不能随机切很多人拿到数据集第一步就是train_test_split(random_state42)在学术数据集上这没问题但在药丸缺陷检测这种工业场景里是有风险的。原因是同一批药丸、同一条产线、同一个光照条件下拍出来的图片往往高度相似如果随机划分同一批次的图片可能同时出现在训练集和验证集里模型等于变相见过了验证集的「近似样本」最终验证精度会虚高一上真实产线就现原形。更稳妥的划分方式是按照拍摄批次、文件夹或时间先后分组保证同一个批次的所有图片要么都在训练集要么都在验证集里。import os import random from collections import defaultdict # 按图片所在子目录分组组为单位划分train/val image_dir images groups defaultdict(list) for fn in os.listdir(image_dir): batch fn.split(_)[0] # 假设文件名前缀是批次号 groups[batch].append(fn) all_batches list(groups.keys()) random.seed(2024) random.shuffle(all_batches) split int(len(all_batches) * 0.85) train_batches set(all_batches[:split]) val_batches set(all_batches[split:]) train_files [] val_files [] for batch, fns in groups.items(): if batch in train_batches: train_files.extend(fns) else: val_files.extend(fns) print(ftrain: {len(train_files)}, val: {len(val_files)})这段脚本的关键在于以批次前缀作为分组键而不是以单张图片为最小单位。如果文件名里没有批次信息就退一步用所在文件夹分组效果类似。85%和15%的划分比例在这个数据量下是合理的崩边和污渍类别样本量少验证集里这两个类别可能只有二三十个框这时候就不能只看整体mAP还要单独看每个类别的AP防止模型在小类上彻底失灵。4.2 YOLOv8训练配置数据文件、基座模型与batch参数训练前需要把数据划分结果整理成YOLO生态要求的目录结构即images目录和labels目录各自下辖train和val两个子目录然后写一个数据配置文件指向对应的路径并声明类别名称。path: /data/pills train: images/train val: images/val names: 0: good 1: chip 2: stain这个yaml文件里最容易出错的是path字段的写法如果你的训练命令是在项目根目录执行的这里建议直接用相对当前目录的路径或者绝对路径避免在后台任务里因为工作目录不同导致训练根本找不到图片。names字段的顺序必须和转换脚本里的class_names顺序一致前面第3章强调过这里再重复一遍是因为我真的见过太多训练很久才发现类别错位的案例。训练命令方面我一般会从最小的模型基座开始跑通流程再逐步换更大的模型。yolo detect train datapills.yaml modelyolov8n.pt epochs100 imgsz640 batch16 patience20 projectruns namepills_exp1命令里epochs设100patience设20意思是如果连续20个epoch验证集指标没有提升就提前结束工业场景里这个设置能节省大量时间。imgsz设640是考虑到药丸是小目标如果显存充足我会优先调到960甚至1280通常小目标AP会明显提升代价是训练时间翻倍。batch设16对应大约8GB到12GB显存如果你只有6GB显存batch降到8是一个可用的选择但要注意batch太小会让BN统计量抖动验证集指标波动大遇到这种情况不要急着改学习率先把batch拉回16再说。初期先用yolov8n把全流程跑通然后换成yolov8s或yolov8m看精度增益这是最稳的选型路线。4.3 训练结果怎么看mAP50-95之外更要看单类别的PR曲线训练结束后不少人只看一眼mAP50就下结论这个指标在缺陷检测场景里偏乐观因为崩边缺角这类缺陷在mAP50的IoU阈值下很容易达到但实际落地产线时要求定位精度更高所以mAP50-95更能反映真实水平。更重要的一点是分别查看每个类别的PR曲线特别是崩边和污渍这两个小样本类别。如果chip类别的AP比good低了20个百分点说明模型偏向把绝大多数预测都归为good前面提到的类别不平衡问题已经暴露出来了。解决手段优先顺序是先做基于类别的采样加权训练时dataloader里给chip和stain类别更高的采样概率还不行就加损失函数里的类别权重再不行就回到数据层面做扩增这部分在第6章讲。模型训练到中后期每5个epoch在验证集上做一次可视化推理是很有价值的动作。打印几张包含误检、漏检的图看模型到底把什么当成了崩边是把药丸侧面的光线反射误判了还是把药丸之间的阴影误判了。这些信息直接指导后续的数据处理方向如果误检集中在反光区域就要在训练数据里补充含有反光的负样本如果漏检集中在深色药丸上就要考虑做亮度均衡预处理。很多「玄学」调参问题最后都是靠看可视化图而不是看损失曲线找到答案的。5. 缺陷检测数据集中最常见的5个坑从数据到训练全路线排查5.1 现象训练正常但推理时模型输出的类别整体错位模型训练loss正常下降验证mAP也正常但把模型部署到测试脚本里预测结果第一类永远是第二类的内容第二类又变成第三类整个人工检查结果完全对不上。原因几乎是固定的训练数据的txt里class_id生成时的names顺序和推理时加载的names顺序不一致。比如转换脚本里names是good、chip、stain但部署脚本里加载模型时写成了stain、chip、good。解决方法是统一在模型导出阶段把类别清单固化到模型配置文件里推理时直接从配置文件读取而不是手写在推理脚本里从源头杜绝人为错位。5.2 现象转换后的txt坐标全是0或者是负数训练加载标签时报错提示box坐标非法打开txt一看一行里出现了0.000000或者-0.000002这样的数值。原因通常有两类一类是原XML里某个目标的bndbox坐标本身就是0比如标注工具在导出时生成了空目标另一类是图像尺寸字段写得比真实尺寸大导致归一化后数值被压缩到接近0。解决是在转换脚本里加一个过滤条件width或height小于某个阈值比如3像素的目标直接跳过同时打印警告日志事后去XML里核对原标注。这类坏目标数量一般不多删掉对整体没有任何影响但留着会让训练早期loss剧烈震荡。5.3 现象验证集mAP很高一到产线新批次就疯狂漏检这是工业质检最典型的翻车方式。模型在验证集上mAP50表现极好但实际应用时每批次都出现大量漏检。原因几乎都是数据划分不当同一个批次、同一条传送带上连续拍摄的照片如果随机分到训练集和验证集验证集不过是训练集的「复读」模型记住的是这批照片的共性而不是缺陷的泛化特征。解决方法是回到第4.1节按批次或拍摄时段做分组划分。还有一个辅助手段是刻意做跨条件的验证训练集全用白光源批次验证集用手电筒补光批次检验模型的鲁棒性到底够不够。5.4 现象目标太小模型训练一百轮依然什么都检测不到药丸检测里崩边区域可能就是十几乘十几像素的框在640分辨率的输入图像上这样的小目标经过多次下采样后特征几乎消失。原因不是标注有问题而是模型输入分辨率不够小目标的特征在网络深层被稀释掉了。解决方法是优先把imgsz从640提到960或1280如果显存不够就改用大图切小图策略将原图切成多个patch分别训练推理最后合并检测结果。还有一种常见做法是把网络head层的stride降低但这会改动模型结构实际项目中我通常先用切patch来验证方向是否有效再考虑改结构。5.5 现象三个类别样本量严重不平衡模型把所有目标都预测成good统计下来good类别有几千个框chip类别可能两百个stain类别三百个训练出来的模型几乎不会预测chip和stain因为把一切预测为good已经能拿到很低的loss了。原因是标准交叉熵损失在大类样本主导下会给小类极小的梯度贡献。解决手段按优先级排列先做数据层面的类别平衡采样和简单增强复制然后把损失函数里的类别权重按样本量的倒数配置让模型对小类别的误判产生更大惩罚最后在评测指标上坚持报告每个类别的AP不接受只报总体mAP的评估方式因为总体mAP在小类彻底失效时依然可能看起来不错。6. 从2759张到上万张三条不加新标注的扩数据路线很多人在训练达到瓶颈后进入一个误区寻找更多标注数据。但如果你的任务就是药丸缺陷检测手里这2759张图本身还有很大的扩展空间而且不需要额外花人力标注。我常用的第一条路线是光照与颜色增强药丸表面反光、糖衣颜色差异大在HSV空间对色调、饱和度、明度做强扰动同时叠加随机亮度和对比度调整能把一张图变化出十张不同光线条件下的图这对提升产线光照变化的鲁棒性非常明显。第二条路线是大图切patch加copy-paste合成把原始图片按滑动窗口切出多个patch模型看到的正样本数量等于放大几倍再把带缺陷的药丸小图随机粘贴到不同背景的合格品图片里合成新样本这种方式能人为制造出更多「崩边出现在药丸堆缝隙里」的复杂场景比单纯几何增强有效得多。第三条路线是伪标签半自动筛选用当前表现最好的模型对没有标注的产线图片做预测只取置信度高于0.95且类别为chip或stain的框人工快速扫一遍后并入训练集每次迭代都能扩充几十个到上百个难样本成本几乎可以忽略。我的习惯是每次拿到新一批产线数据不急着训练先把图像叠加上标注框和类别名可视化看一遍观察三类框在亮度、角度、背景上的分布确定数据增强的优先级再决定是先做颜色扰动还是先切patch。这个习惯帮我避过很多模型层面的玄学问题也让我养成了每次训练都保存转换脚本和names映射的好习惯。希望这篇笔记能帮你把这份2759张的数据集一次跑通少走我当年走过的弯路。希望帮到你。本文还有配套的精品资源点击获取