药丸缺陷检测数据集:VOC与YOLO格式转换及YOLOv8训练实战指南 简介这份药丸表面缺陷检测数据集以Pascal VOC和YOLO两种主流格式整理覆盖污染、裂纹、合格品三个类别共2759张真实图片与2798个标注框类别分布均衡适合入门目标检测或构建药品质检视觉模型时直接训练、验证与迁移学习。压缩包内共2000个文件其中1049个xml标注文件与951个yolo格式txt文件分别对应VOC和YOLO框架的标注需求配套jpg原图整体约411.78MB按类别前缀命名便于筛选与拆分。数据集使用labelImg工具按矩形框规则标注标注规范一致可减少二次清洗成本。目前已有218人浏览学习对于缺少缺陷样本、需要快速验证检测算法或完成课程设计的开发者是一份可直接投入训练的数据资产后续也可基于此扩展多类别识别。1. 药丸缺陷检测数据集2759张双格式标注能不能直接喂给YOLO训练做制药质检的同行应该都有体会药丸这类小目标缺陷检测最难的不是模型选型而是数据。产线上拍出来的图片标注要画到崩缺、裂纹、污渍这些细节一个人一天标不了几百张还容易前后不一致。标题里的这个药丸缺陷检测数据集把这一步省掉了——2759张、3个类别同时给了Pascal VOC的XML和YOLO的txt两套标注压缩成7z分发。双格式在工程里很实用VOC适合在LabelImg里继续精修和可视化YOLO可以直接丢给YOLOv8训练省掉转换脚本。这篇文章把解压、核对、转换、训练前体检和踩坑点过一遍目标是拿到.7z后一个下午跑出第一版模型。2. 从VOC的XML到YOLO的txt两种标注格式的换算关系与选型理由很多同学拿到双格式数据集第一反应是YOLO的txt最金贵XML是冗余备份。真实工程里恰恰相反XML才是母版txt是派生物。因为XML里存的是像素绝对坐标图片缩放、裁剪后坐标仍然可以追溯而txt的归一化坐标一旦图片被预处理改动就必须重新计算。这一章把两种格式的读法和换算讲清楚后面所有脚本都建立在这一点上。2.1 VOC的XML里到底存了什么Pascal VOC的标注是一个XML文件文件名与图片名一致放在annotations目录下。核心结构长这样annotation folderJPEGImages/folder filenamepill_0001.jpg/filename size width1920/width height1080/height depth3/depth /size object namechip/name truncated0/truncated difficult0/difficult bndbox xmin482/xmin ymin360/ymin xmax510/xmax ymax388/ymax /bndbox /object /annotation先看object节点一个object代表一个缺陷框一张药丸图里可能同时有崩缺和裂纹两个缺陷那就写两个object。bndbox里的四个值分别是左上角和右下角的像素坐标单位是px从0开始计数。filename标签在大多数工具里只是给可视化用的真正决定XML对应哪张图的是文件名本身——标注文件和图片放不同目录靠文件名去掉后缀后一一对应。size节点保存原始图片的宽高转YOLO格式时必须读它不能自己写死。truncated表示目标是否被图像边界裁切difficult表示难例。这两个字段在YOLO训练里一般直接被忽略但如果后续转COCOdifficult1的实例通常要走过滤逻辑。另外要注意如果XML里出现多个object但对应图片上肉眼根本找不到那么多缺陷多半是标注时的误操作这种样本留在训练集里等于给模型喂错题宁可删掉。2.2 YOLO的txt为什么是归一化坐标YOLO格式下每张图片对应一个同名txt文件每一行代表一个目标格式是固定的五列0 0.2583 0.3463 0.0146 0.0259 1 0.7200 0.4100 0.0312 0.0568第一列是类别id从0开始后面四列分别是目标中心点的x、y坐标和框的宽、高全部除以图片自身的宽高做了归一化。拿上面XML里的框举例图片是1920×1080xmin482、ymax388那么x_center (482 510) / 2 / 1920 ≈ 0.2583 y_center (360 388) / 2 / 1080 ≈ 0.3463 框宽 (510 - 482) / 1920 ≈ 0.0146 框高 (388 - 360) / 1080 ≈ 0.0259这个换算里最容易翻车的是除法对象归一化除以的是这张图片自身的宽高不是训练时的输入尺寸640×640。之前见过有人把坐标全除以640结果框全部错位模型训练半天mAP一直为0。反过来从txt还原像素坐标时要乘以原始图片的宽高而不是乘以模型输入尺寸否则可视化时框的位置对不上。2.3 两份格式都要不是多此一举同时给VOC和YOLO两套标注对使用方最大的价值是能互相校验。一个最朴素的检查方法打开一张图片对应的XML数一下object数量再打开同名txt数一下行数。两边一致说明这份标注没丢框对不上说明其中一套在制作或拷贝过程中出了问题必须查清楚再用。另一个原因是工具链的兼容性。LabelImg、MMDetection、Roboflow这些工具对VOC XML解析得最顺打开可以直接精修YOLO生态的训练脚本则是直接读txt两者覆盖面刚好互补。最后txt是归一化坐标一旦后续做图像裁切、拼接坐标全部失效XML是绝对像素坐标留一份在手里永远有后悔药。所以我拿到这种双格式数据集时会先信XML自己从XML重新生成一份txt而不是直接信任压缩包里的txt这一步能过滤掉很多二次处理带来的格式问题。3. 用PyCharm在本地跑通YOLO解压7z、核对标注与转换脚本格式看懂了接下来动手。这一章按正常拿到数据集的顺序走先解压7z再装环境再做文件核对最后把VOC重新转换成YOLO格式。全程只需要一个PyCharm和一个终端。3.1 先把7z解开Linux与Windows两条路7z压缩率高但解压前强烈建议先测一遍压缩包完整性。压缩包在传输过程中哪怕坏了一个字节解压出来的XML都可能打不开而这种损坏在没跑训练前很难发现。# Linux 下先测试完整性再解压到指定目录 sudo apt-get install -y p7zip-full 7z t 药丸的缺陷检测数据集VOCYOLO格式2759张3类别.7z 7z x 药丸的缺陷检测数据集VOCYOLO格式2759张3类别.7z -o./pill_dataset7z t是test模式只校验CRC不释放文件7z x才真正解压-o参数指定输出目录注意-o后面不要画蛇添足加空格。Windows用户建议装7-Zip 21.07以上版本在PowerShell里把7z.exe所在目录加入PATH后命令和Linux一致。解压完成后先别急着写代码用文件管理器看一眼目录结构VOC风格的目录一般叫JPEGImages和AnnotationsYOLO风格的目录一般叫images和labels后面脚本里的路径按实际目录改。3.2 在PyCharm里创建一个干净的YOLO环境训练脚本放在PyCharm里跑断点调试方便。新建虚拟环境时用conda还是venv都可以关键是包要装对顺序。conda create -n pill python3.10 -y conda activate pill pip install ultralyticsultralytics这个包会连带安装torch、torchvision和opencv-python理论上一条命令就能跑YOLOv8。但如果你有NVIDIA显卡我建议分两步走先按PyTorch官网给出的命令安装和CUDA版本匹配的torch再pip install ultralytics。否则pip可能自动装一个CPU版torch训练速度慢一个量级还不报错。装完之后在PyCharm的Settings → Project → Python Interpreter里把解释器切到pill环境新建脚本就能直接跑了。3.3 先做一次文件核对图片、XML、txt三边数量对齐拿到数据集第一件事不是开训是数文件。2759张图片就应该有2759个可用标注文件任何一个对不上后面都是白跑。import os from collections import Counter root pill_dataset # 解压后的根目录按实际路径改 img_dir os.path.join(root, images) # 图片目录 xml_dir os.path.join(root, annotations) # VOC标注目录 txt_dir os.path.join(root, labels) # YOLO标注目录 imgs [f for f in os.listdir(img_dir) if f.endswith((.jpg, .png, .jpeg))] def base_names(path): return {os.path.splitext(f)[0] for f in os.listdir(path)} base_imgs base_names(img_dir) base_xmls base_names(xml_dir) base_txts base_names(txt_dir) print(图片数量:, len(base_imgs)) print(缺XML的图片:, len(base_imgs - base_xmls)) print(缺txt的图片:, len(base_imgs - base_txts)) print(有XML但没有图片:, len(base_xmls - base_imgs)) print(有txt但没有图片:, len(base_txts - base_imgs)) # 统计txt里的类别分布看id和数量是否符合预期 cls_counter Counter() for t in os.listdir(txt_dir): with open(os.path.join(txt_dir, t)) as f: for line in f: line line.strip() if line: cls_counter[int(line.split()[0])] 1 print(txt类别分布:, dict(cls_counter))脚本逻辑是用文件名集合做差集快速找出“孤儿文件”。输出应该看到图片数量等于2759缺XML和缺txt都等于0类别的id范围在0到2之间。如果发现缺标注的图片数量很大先怀疑目录路径配置错了如果是个别缺失把对应文件名打印出来多半是制作数据集时漏拷贝直接剔除比补标更省时间。3.4 用XML重新生成YOLO的txt一份通用转换脚本以XML为基准重新生成txt等于把两份格式做了一次平行校验。脚本本身不难难在类别映射和除法细节。import os import glob import xml.etree.ElementTree as ET # 类别映射先跑统计脚本确认XML里的实际类别名再改这里 CLASS_MAP {chip: 0, crack: 1, stain: 2} # 示例以实际为准 xml_dir annotations out_dir labels_yolo os.makedirs(out_dir, exist_okTrue) for xml_path in glob.glob(os.path.join(xml_dir, *.xml)): tree ET.parse(xml_path) root tree.getroot() size root.find(size) W int(size.find(width).text) H int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_MAP: print(f[警告] {xml_path} 出现未知类别: {name}) continue # 宁可丢框也不要写错类别id cls_id CLASS_MAP[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) # 归一化除以原图宽高不是除以640 cx (xmin xmax) / 2.0 / W cy (ymin ymax) / 2.0 / H bw (xmax - xmin) / W bh (ymax - ymin) / H lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) base os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, base .txt), w) as f: f.write(\n.join(lines))几个参数必须说清楚。CLASS_MAP必须写死不要用程序自动给类别编号自动编号看着方便但每次运行顺序可能不同训练和推理时类别名对应不上这个错极难排查只能重来。坐标归一化除的是W和H这两个从XML里读出来的原图尺寸任何写死尺寸的做法都是错的。6位小数足够用1920宽的图上误差不到1个像素。遇到未知类别时打警告并跳过比把未知类别静默并进已有id安全得多跑完看终端输出如果警告很多回XML目录里检查这些object到底是什么。4. 训练前给数据集做体检类别分布、划分比例与小目标尺寸转换完不代表能直接开训。工业缺陷检测和通用目标检测最大的区别是缺陷样本本身就少类间数量可能差一个量级。这一章做的三件事能把训练翻车的概率降掉一半。4.1 统计每个类别的框数别让模型对某一类视而不见3个类别听上去很均衡实际上一看分布可能发现其中一个类只有不到100个框。YOLOv8默认对类别是平权的某个类样本太少训练后这个类的AP往往惨不忍睹。from collections import Counter import os txt_dir labels_yolo empty_txts [] cnt Counter() for txt in os.listdir(txt_dir): path os.path.join(txt_dir, txt) with open(path) as f: lines [l.strip() for l in f if l.strip()] if not lines: empty_txts.append(txt) # 有图无标注需要人工确认 for line in lines: cnt[int(line.split()[0])] 1 print(各类别框数:, dict(cnt)) print(空txt数量:, len(empty_txts))框数统计结果决定你的训练策略如果某个类别明显偏少最简单有效的手段不是调loss权重而是把包含该类别的那几张图片在训练目录里多复制几份让模型每个epoch多看几次。复制时注意要和标签文件一起复制文件名做一下区分。对于空txtYOLO会把对应图片当背景样本参与objectness训练本身不是错误但你要确认这些图确实是干净背景而不是漏标了缺陷。漏标背景图里如果真的有缺陷模型会学着把缺陷当背景这在产线上是致命的。4.2 按8:1:1划分train/val/test并写出data.yaml划分数据集用随机洗牌就够了不需要花哨的分层采样。2759张图8成约2200张训练1成验证1成留作最终测试。注意test集一旦留下来调参过程中就不要碰它。import os, random, shutil random.seed(42) src_images images src_labels labels_yolo # 目标目录结构按YOLO习惯来 split_dirs { train: (data/images/train, data/labels/train), val: (data/images/val, data/labels/val), test: (data/images/test, data/labels/test), } for img_d, lbl_d in split_dirs.values(): os.makedirs(img_d, exist_okTrue) os.makedirs(lbl_d, exist_okTrue) names [os.path.splitext(f)[0] for f in os.listdir(src_images) if f.endswith((.jpg, .png))] random.shuffle(names) n_train int(len(names) * 0.8) n_val int(len(names) * 0.1) split { train: names[:n_train], val: names[n_train:n_train n_val], test: names[n_train n_val:], } for split_name, items in split.items(): img_d, lbl_d split_dirs[split_name] for base in items: shutil.copy(os.path.join(src_images, base .jpg), img_d) src_txt os.path.join(src_labels, base .txt) if os.path.exists(src_txt): shutil.copy(src_txt, lbl_d) print(split_name, len(items))拷贝而不是移动是为了保留原始数据做重新划分。图片后缀假设是jpg如果你的数据集是png把代码里的.jpg改掉。划分完成后训练目录里应当出现data.yamlpath: ./data train: images/train val: images/val test: images/test names: 0: chip 1: crack 2: stainnames里的id顺序必须和CLASS_MAP完全一致。然后就能跑训练了yolo detect train datadata.yaml modelyolov8n.pt epochs100 imgsz640先用yolov8n这种最小的权重跑通流程确认数据加载没问题再换大模型。验证集控制在200到400张足够看出模型好坏2759张按1/10划分大约270张正合适。4.3 图片尺寸和缺陷框大小直接决定imgsz怎么设制药产线上拍出来的图往往是大分辨率单颗药丸只占画面一小块。如果把1920×1080的图直接压到640×640一个小裂纹在模型眼里只剩两三个像素神仙模型也学不出来。import cv2, os import numpy as np img_dir images txt_dir labels_yolo sizes [] for txt in os.listdir(txt_dir): base os.path.splitext(txt)[0] img_path os.path.join(img_dir, base .jpg) if not os.path.exists(img_path): continue img cv2.imread(img_path) H, W img.shape[:2] # 必须用图片真实尺寸还原 with open(os.path.join(txt_dir, txt)) as f: for line in f: parts line.strip().split() if len(parts) 5: continue _, cx, cy, bw, bh parts sizes.append([float(bw) * W, float(bh) * H]) sizes np.array(sizes) print(框数量:, len(sizes)) print(平均框尺寸: %.1f x %.1f px % (sizes[:, 0].mean(), sizes[:, 1].mean())) print(小于32px的框占比: %.2f % ((sizes.max(axis1) 32).mean()))统计结果直接指导参数如果大部分框小于64×64训练时把imgsz提高到1024或1280别再用默认的640。显存不够就减小batch配合梯度累积。mosaic增强对中大型目标友好但对小目标反而是负担——四张图拼一起小目标被缩得更小。遇到小目标数据集我一般会把mosaic关掉或降到0.5再试通常比硬上大模型更见效。5. 药丸缺陷检测避坑清单4条标注与训练的血泪经验数据集训练出问题90%是数据本身的问题不是模型的问题。以下四条是我在类似缺陷检测数据集上真实踩过的坑每条按现象、原因、解决三步讲完建议对照自己的流程查一遍。5.1 训练刚开始就报class out of range或All labels are empty现象yolo detect train命令下去几秒钟内报错提示某个txt里class id超出范围或者说labels为空。原因VOC转YOLO时类别映射用了自动编号。程序遍历XML时class id按遇到类别的顺序分配第一张图只有chipchip就是0第二张图多了crackcrack变成1一旦某张图的类别顺序不同id就对不上。更隐蔽的情况是XML里混进了一个脏类别名比如带了空格或中文转换脚本给它分配了新id直接超界。解决CLASS_MAP写死不接受自动编号。训练前先检查txt里实际出现的最大类别idpython -c import glob; idsset(); [ids.update([int(l.split()[0]) for l in open(f) if l.strip()]) for f in glob.glob(labels_yolo/*.txt)]; print(sorted(ids))输出应该是[0, 1, 2]。出现3或者更大一定有一个未知类别没被映射到回去查XML。空txt文件不会报错但会以背景图参与训练你要确认这些图确实干净别把漏标的缺陷带进训练。5.2 Windows下解压7z后文件名乱码不是数据集坏了是编码没对上现象7z在Windows下用老版本7-Zip解压图片正常XML文件名变成一堆乱码双击打不开。或者解压到一半提示CRC错误文件不完整。原因压缩包是在Linux下用UTF-8文件名打包的老版本7-Zip按本地代码页GBK去解析UTF-8文件名自然乱码。CRC错误则说明压缩包传输过程中有损坏任何标注文件都可能不完整。解决换7-Zip 21.07以上版本对UTF-8文件名的支持已经处理得比较好。更稳的做法是解压前先跑一遍7z t验证完整性CRC报错就别继续解压了重新获取压缩包。如果已经解压出乱码文件名不要在文件管理器里手动改名——扩展名容易被吃掉。用一段Python脚本打印真实文件名再做批量重命名import os # 解决思路listdir拿到的是原始字节解码后的真实文件名 # 把乱码名映射到正常名然后os.rename for f in os.listdir(annotations): print(repr(f)) # 先看真实内容5.3 mAP很高但实际预测把传送带阴影也当缺陷现象验证集mAP到了0.9模型看起来没问题拿到产线新图上推理把传送带边缘、光照阴影、甚至完整的好药丸全框出来了。原因两个层面的问题。第一标注框可能过松把缺陷周围一圈完整区域也框进去了模型学到的不是“缺陷”而是“药丸区域”。第二数据集背景太单一全是传送带模型把背景纹理当成了判别特征换一个光照条件就翻车。解决先可视化抽查训练样本看标注框是否紧贴缺陷边缘框太松就重新标。训练数据里加负样本——放一批干净的、没有缺陷的背景图到训练目录不加txt文件让模型学会“没有框别输出”。推理时confidence阈值从默认的0.25提到0.5误检通常会显著下降。如果这两步做完还误检跑一次混淆矩阵看误检主要落在哪个类别上对症处理。5.4 loss曲线后期震荡、小缺陷漏检别急着加算力先查imgsz和mosaic现象训练到后期loss下降到0.04左右开始震荡验证集上大块崩缺全部检出细小的裂纹类缺陷几乎全漏。看起来像模型容量不够加了大模型反而更慢。原因药丸图像被resize到640后细小缺陷只剩几个像素mosaic增强又把图进一步缩小小目标在增强环节直接被抹掉。另一个因素是这批数据里小框本身数量少模型一个epoch见不到几次学不动。解决按4.3的脚本统计框尺寸如果小框占比高把imgsz设为1280batch降到8。把mosaic降到0.5或关掉防止小目标被二次缩小。训练时把loss曲线拆开看box_loss、cls_loss、dfl_loss三部分小目标漏检通常会看到cls_loss震荡——类别信息学不稳。这时优先调整数据增强而不是换模型效果立竿见影。6. 验证转换结果的最快方式可视化标注脚本与anchor的进阶调法训练前最后一个动作也是我最喜欢先做的一个动作随便抽两张图把txt标注画上去眼睛看一遍。import cv2 img_path images/pill_0001.jpg txt_path labels_yolo/pill_0001.txt class_names {0: chip, 1: crack, 2: stain} img cv2.imread(img_path) H, W img.shape[:2] # 还原坐标必须用图片真实尺寸 with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) 5: continue cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) x1 int((cx - bw / 2) * W) y1 int((cy - bh / 2) * H) x2 int((cx bw / 2) * W) y2 int((cy bh / 2) * H) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, class_names.get(cls_id, ?), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(check.jpg, img)这段脚本的逻辑是把归一化坐标乘回原图宽高还原成像素框。如果画出来的框位置和缺陷实际位置对不上说明txt的坐标基准和图片尺寸不一致这种情况下无论怎么调模型都是白搭。我每次拿到新数据集都会先跑一遍这个检查比看任何统计数字都直观。模型训练完成后如果还想做对比实验可以换YOLOv5或YOLOv7试一轮。这两个版本是anchor-based的训练脚本会自动聚类anchor跑完看一眼聚类出的9组先验框宽高和你统计的缺陷框分布做个对比如果聚类结果和实际框尺寸差异很大说明数据集里的框本身就存在大量过松标注回到数据层面重新清理而不是调anchor参数。YOLOv8这类anchor-free模型没有这个机制调参重心放在imgsz、mosaic和损失权重上就行。我自己刚拿到这个数据集时也犯过“打开txt一看坐标正常就直接训练”的错后来画框才发现类别顺序全乱了。现在我的习惯很死板解压后先校验转换后必可视化划分完再看一遍统计三步走完才轮到训练脚本。这套流程放在训练脚本之前不是保守是省时间。希望帮到你。本文还有配套的精品资源点击获取