简介:中草药类型识别检测数据集面向计算机视觉目标检测与图像分类任务,适用于中药品种鉴定、药材质量控制及智能识别系统研发。完整数据集涵盖7976张中草药图片,标注类别达45种,采用Pascal VOC与YOLO两种主流格式,兼顾通用检测框架训练与实时部署需求。本次下载的7z压缩包共2000个文件,其中1999个为VOC格式xml标注文件,另含1个使用前必读txt说明,压缩包整体约481.35MB,便于批量训练与模型迭代。该资源已有578人学习,标注规范、类别丰富,可直接用于YOLO、Faster R-CNN等检测模型的训练与评估。借助这批数据,研究者能快速搭建中草药自动识别系统,提升分类精度与定位效率,亦可作为教学演示、算法对比和论文复现的优质数据源,为中医药信息化提供支撑。
1. 中草药类型识别检测数据集:7976张45类,拿到手先别急着训练
一个做中药饮片质检的朋友拿着这份中草药类型识别检测数据集(VOC+YOLO格式,7976张45类别)问我能不能直接训YOLO。我的第一个反应是:能,但别急着跑 train.py。这类数据集最大的坑从来不在算法,而在格式。同一份数据同时给你VOC的xml和YOLO的txt,看起来是双保险,实际上两套坐标体系、两类归一化方式、类别映射表是否对齐,任何一个环节出错,都会让45类里近一半类别的AP直接归零。这篇文章就针对这类双格式数据集,从目录结构、格式校验、训练配置一路讲到验证集设计和混淆矩阵解读,适合正在自建中药检测数据集,或者刚下载到类似双格式数据集准备训练YOLO的工程师。
2. VOC和YOLO两套格式并存:目录结构、坐标体系和它们的对应关系
2.1 解压后先确认四件套:JPEGImages、Annotations、labels、classes
拿到.7z压缩包,第一步不是打开图片看“标得准不准”,而是把目录树拉出来。常见做法是:
mkdir -p ~/datasets/herbal && cd ~/datasets/herbal 7z x 中草药类型识别检测数据集VOC+YOLO格式7976张45类别.7z tree -L 2 | head -60如果系统没有7z命令,Ubuntu下先装p7zip-full:sudo apt install p7zip-full。解压后要确认的目录项是四个:JPEGImages存原图,Annotations存VOC的xml标注,labels存YOLO的txt标注,classes.txt记录45个类别的名称与顺序。有的数据集会把图片和标注分开放,也有的直接在根目录平铺,但既然标题写了VOC+YOLO双格式,绝大多数情况下就是这套结构。
提示:解压后优先跑一句
find JPEGImages -name '*.jpg' | wc -l,确认图片数量是7976。如果少于这个数,说明压缩包在传输或解压过程中丢了文件,后面训练时的类别均衡统计全是错的。
四件套齐了之后,别急着删Annotations。很多人的习惯是“反正YOLO训练只要txt,xml没用”,这个习惯在自建数据集的迭代场景里会让你后悔——xml里保留着原始标注,txt只是派生物。当你发现YOLO训练效果不对、想回头查某个框到底是怎么画的,xml是最后的后悔药。
2.2 VOC格式的xml标注:bndbox才是核心,name决定类别归属
VOC格式的每个xml对应一张图片,典型结构是:
<annotation> <folder>JPEGImages</folder> <filename>herb_0042.jpg</filename> <size> <width>1280</width> <height>960</height> <depth>3</depth> </size> <object> <name>gouqizi</name> <bndbox> <xmin>240</xmin> <ymin>120</ymin> <xmax>860</xmax> <ymax>720</ymax> </bndbox> </object> </annotation>解读这份xml时要注意三点。第一,<name>标签里的值必须和classes.txt里的某个名字严格一致,多一个空格、大小写不一致都会在转换后被当成“未知类别”丢掉。第二,<bndbox>里存的是绝对坐标,单位是像素,xmin/ymin是左上角,xmax/ymax是右下角,所有数值都是整数。第三,一个xml里可以有很多个<object>节点,每个节点对应图里的一个目标,这就是多目标检测的“多”字落在哪里的答案。
VOC格式的老问题是不统一。有的标注工具存的是左上角+宽高,比如<x>,<y>,<w>,<h>,有的存左上角+右下角,比如上面这套。转换脚本写错一个字段,坐标就全偏移了。所以解析xml时不要自己手写正则去抠数字,直接用xml.etree.ElementTree这种标准库,按节点名取字段,至少能保证结构层面不翻车。
2.3 YOLO格式的txt标注:归一化坐标和类别索引的对应关系
YOLO的txt标注同样是每张图一个文件,文件名和图片名一致,只是后缀从.jpg变成了.txt。文件里每一行代表一个目标,格式是:
class_id x_center y_center width height这里全是相对坐标,取值范围0到1,类别用整数索引而不是字符串。举个例子:
0 0.4297 0.4375 0.4844 0.6250这两个坐标系的换算关系是固定的:
x_center = (xmin + xmax) / 2 / width y_center = (ymin + ymax) / 2 / height box_width = (xmax - xmin) / width box_height = (ymax - ymin) / height其中width和height来自xml里的<size>节点。注意分母是图片的实际尺寸,不是目标框尺寸,很多人第一次转换时在这里除反了。类别索引0对应classes.txt里的第一个名字,索引44对应最后一个名字,所以classes.txt的行顺序直接决定了训练出来的模型输出的是哪个中药名。
2.4 双格式并存的真实问题:标签对得上算运气,对不上是常态
同时提供VOC和YOLO格式,看起来是省了转换这一步,但实际使用中我遇到过好几次两类标签对不上的情况。最典型的是:YOLO的txt文件数比xml少几十个,或者classes.txt里的类别名单和xml里的<name>集合不一致。
我的习惯做法是写一个校验脚本,先不转格式,先把两边对一遍:
ls Annotations/*.xml | wc -l ls labels/*.txt | wc -l grep -h "<name>" Annotations/*.xml | sort | uniq -c | sort -rn cat classes.txt | nl如果两个数量不等,找出差异的具体文件;如果xml里的name集合比classes.txt多,说明有类别没被注册;如果classes.txt里有多余名字,说明有类目在标注中从没出现过。这种“多对一”“一对多”的混乱,在数据集迭代过程中几乎无法避免,所以拿到手先校验、后使用,而不是直接开训。
注意:.7z压缩包解压后,如果路径里带了空格或中文,YOLO在Windows上读取时会间歇性报错。把整个数据集放在纯英文、无空格的路径下是最省事的。
3. 把VOC+YOLO数据集喂给YOLOv8:目录重排、格式校验与data.yaml配置
3.1 先做样本对齐:只保留同时有图片和标注的样本
检测数据集里,图片和标注文件对不上号是最常见的脏数据来源——有的图标注了但标签文件丢了,有的标签文件还在但图片被误删了。无论这份数据集质量多好,训练前都要做一次对齐清洗。
import os from pathlib import Path img_dir = Path("JPEGImages") ann_dir = Path("Annotations") label_dir = Path("labels") img_files = list(img_dir.glob("*.jpg")) + list(img_dir.glob("*.png")) valid = [] for img in img_files: xml = ann_dir / (img.stem + ".xml") txt = label_dir / (img.stem + ".txt") if xml.exists() and txt.exists(): valid.append(img.name) else: print(f"丢弃: {img.name} (缺xml或txt)") print(f"可用样本: {len(valid)} / {len(img_files)}")这段脚本做的事很简单:遍历所有图片,检查同名xml和txt是否同时存在,缺失的打印出来并跳过。逻辑上唯一的注意点是img.stem取的是不含扩展名的文件名,所以jpg和png的同名文件会撞车,如果数据集里同一张图有两种格式,需要在检查时加一个去重条件。
参数上有一个常见坑:有些数据集的图片扩展名是.jpeg,而glob只匹配了.jpg和.png,会漏掉一批。实际使用时把后缀列表补齐就好。跑完这步,你会得到“真正能喂给YOLO的样本数”,后续所有统计都围绕这个数字来。
3.2 VOC转YOLO的脚本:自己写一遍转换逻辑,比用现成工具更稳
虽然数据集自称“VOC+YOLO格式”,但为了排查问题,我仍然会自己跑一遍VOC转YOLO的脚本,然后把生成的txt和数据集自带的txt做对比。这样做的目的是把“格式对不对”变成可验证的事,而不是开盲盒。
import xml.etree.ElementTree as ET from pathlib import Path voc_dir = Path("Annotations") yolo_dir = Path("labels_own") yolo_dir.mkdir(exist_ok=True) img_size_cache = {} def voc_to_yolo(xml_path, out_path): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in class_to_id: print(f"跳过未知类别: {name} in {xml_path.name}") continue 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 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{class_to_id[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") out_path.write_text("\n".join(lines) + "\n") for xml_path in voc_dir.glob("*.xml"): out_path = yolo_dir / (xml_path.stem + ".txt") voc_to_yolo(xml_path, out_path)这个脚本的重点在于class_to_id字典必须从classes.txt按行读取,保证顺序一致:
class_to_id = {name.strip(): i for i, name in enumerate(classes_file.readlines())}转换之后,用diff对比数据集自带的labels和生成的labels_own,逐个文件检查坐标值是否一致。差异超过1e-4说明作者的归一化基准和你理解的不同,这时候不要强行用,先回头核对xml里的<size>是否有一批图写错了宽高。
3.3 yolo训练自己的数据集:写出一份不会报错的data.yaml
data.yaml是YOLO训练的数据集入口,最容易写错的是路径和类别列表。一个可用的配置长这样:
# herbal.yaml path: /home/user/datasets/herbal train: images/train val: images/val nc: 45 names: 0: rencan 1: gouqizi 2: huangqi 3: dangshen 4: baizhu # ... 直到 44写这份配置时有三个容易踩的点。第一,path最好写绝对路径,相对路径在换终端、换工作目录时会找不到文件。第二,train和val指向的是“图片目录”,不是“标签目录”,YOLO会自己根据图片路径找对应的labels目录——默认是同一级目录下的labels文件夹。如果你把图片放在images/train,那标签必须在labels/train,目录名和配对规则是约定俗成的。第三,names列表的顺序必须和classes.txt里的顺序一字不差,因为训练时模型输出的类别索引就是按这个列表来的。
如果不确定路径写没写对,可以先用一句话验证:
python -c "from ultralytics import YOLO; y = YOLO('yolov8s.yaml'); y.train(data='herbal.yaml', epochs=0)"epochs=0不会真正训练,但会触发数据加载流程,如果data.yaml里路径错了,这里就会报错,而不是等训练跑到一半才崩。
3.4 跑通第一轮训练:模型选择、batch和imgsz的取舍
格式都验证完、data.yaml也写对了,就可以开始第一次训练。我一般用这条命令起手:
yolo detect train data=herbal.yaml model=yolov8s.pt epochs=100 batch=16 imgsz=640 device=0 patience=20模型选择上,第一轮不要用yolov8n,虽然它跑得快、显存占用低,但45类这个类别数对n-scale来说容量偏小,最后的结果经常是mAP看着还行,细分类别比如枸杞和红枣这种同色系药材相互混淆。从s开始往上走,如果显存够就m。预训练权重yolov8s.pt会在第一次启动时自动下载,公司内网环境记得提前准备离线权重文件。
batch=16在中等显存卡上是安全值,imgsz=640是速度和精度的平衡点。如果原始图片普遍在1280以上,可以先按640跑通流程,之后再对比一次imgsz=960的结果——中草药识别里有大量小目标(比如枸杞颗粒很小),升分辨率往往能带来3到5个点的AP提升。
precision、recall这些指标在训练完成后会自动打印,但要注意:这些数字是所有类别的均值,45类里如果某几类样本极少,均值会被几十个样本多的头部类别拉高。真正的判断要看每个类别的AP和混淆矩阵,别被平均数字骗了。
4. 训练与推理避坑:45类里一半AP归零的常见原因与排查
4.1 现象:训练正常结束但 val 里近20个类别AP为0,原因:类别极端不平衡
这个数据集有7976张图片、45个类别,平均每类约177张。但实际分布几乎不可能是均匀的——常见的中药材如枸杞、黄芪可能各有六七百张,而某些冷门药材可能只有三四十张甚至个位数。少样本类别的AP为0是最常见的翻车场景。
排查方式是训练前先跑一次标签统计:
grep -o "^[0-9]*" labels/train/*.txt | awk '{count[$1]++} END {for (c in count) print c, count[c]}' | sort -k2 -n看到分布之后,对样本数少于50的类别要做决策:要么收集更多数据,要么对这类图片做离线增强(旋转、亮度扰动、mosaic在训练时虽然会自动做,但本质是“复用”原有样本,信息量不增加),要么干脆合并成更粗粒度的类别。我有一次遇到30多个类别的数据集,直接建议对方把形态相似的类别合并到18类,mAP一下从78涨到87,而且部署时更实用。
4.2 现象:训练时loss偶尔出现NaN或者loss曲线后期震荡,原因:xml里的bndbox坐标超出图像边界
标注框超出图像宽高是人工标注时很容易出现的问题,尤其当标注工具允许画到画布外时。YOLO转换时,xmax大于图片宽度、ymax大于图片高度,除以图片宽高后,框的宽高会大于1或者中心点跑到外边去。训练时模型要预测这种异常目标,损失值直接爆掉。
排查脚本很简单:
for txt in label_dir.glob("*.txt"): for line in txt.read_text().strip().splitlines(): parts = line.split() if len(parts) != 5: print(f"格式错误: {txt.name}: {line}") w, h = float(parts[3]), float(parts[4]) xc, yc = float(parts[1]), float(parts[2]) if xc < 0 or yc < 0 or w <= 0 or h <= 0 or xc > 1 or yc > 1: print(f"越界目标: {txt.name}: {line}")解决方式不是直接删框,而是回到xml里看原始坐标,如果xmax只超了一点点比如几个像素,就clip到图片宽度范围内;如果超了很多或者框本身画错了,就把这个目标去掉,不删整张图。
4.3 现象:标签文件读不出来或者类别名全是乱码,原因:中文类名编码问题
中草药数据集几乎必然涉及中文名。如果classes.txt以UTF-8编码保存,但xml文件是GBK或者反过来,class_to_id匹配时会直接失败,这一整个类别的目标就全被跳过了。还有一种情况是xml的name写的是“枸杞子”,classes.txt里写的是“枸杞”,看着差不多,程序里就是两个字符串。
解决方式是在读取时统一编码并且做一层别名映射表:
alias = {"枸杞子": "gouqizi", "枸杞": "gouqizi", "宁夏枸杞": "gouqizi"}也就是说,给每个最终类别定一个英文内部ID,然后用中文做别名,读取xml时先映射成内部ID,再查找class_to_id。这样即使原始数据里中文叫法不统一,只要别名映射表够全,就不会丢目标。
4.4 现象:训练报错 FileNotFoundError 或者路径拼接后读取到空目录,原因:数据集放在带空格/中文的路径下
Windows上解压.7z到“桌面/中药数据集”这种路径时,YOLO的Dataloader在拼接绝对路径时会把空格截断或者把中文编码转坏,报错形式五花八门——有时候是图片读取失败,有时候是标签文件找不到,还有时候是奇怪的编码异常,看起来跟数据集毫无关系。
这个问题的排查成本最低,也是我推荐第一个检查的项:
# Linux/macOS echo $HOME # Windows PowerShell echo $env:USERPROFILE确认整个数据集路径中没有任何汉字、空格、括号之外的特殊字符。统一放到/home/user/datasets/herbal或者D:\datasets\herbal这种路径下,一次治本。这个问题在几个版本更新后时好时坏,属于典型的“玄学报错”,优先通过路径规避,而不是去翻源码查Dataloader。
4.5 现象:训练到几十个epoch时mAP突然掉到0然后loss变NaN,原因:BN统计量崩了
45类的大类别数加上不干净的标签,训练后期很容易出现BN崩溃。表现是训练曲线看着一切正常,突然某个epoch之后loss失去控制,验证集的mAP瞬间归零。
出现BN崩溃时,优先检查前面几步有没有漏:标签里是否还存在越界坐标?学习率是不是设得过高?如果数据标签没问题,最简单的做法是按顺序尝试三步——把lr从0.01降到0.001,batch增大一倍,然后在训练脚本里加一句amp=False关闭混合精度。中草药检测场景对推理速度没到极端敏感的程度,关闭AMP换来训练稳定是值得的。
注意:检查标签越界永远是第一步,BN崩溃很多时候只是标签异常的连锁反应。
5. 验证集分层抽样、混淆矩阵解读与模型下发的最后一道工序
训练完成后,不要直接拿默认的val结果写在报告里。数据集的官方train/val划分可能已经过时,或者划分时没有考虑类别分布,导致某些冷门类别在验证集里根本没有样本。我的习惯是自己重新做一次分层抽样划分,只抽样不打乱原始图片顺序,保证可复现。
from collections import defaultdict from pathlib import Path import random random.seed(42) label_dir = Path("labels/train") class_to_files = defaultdict(list) for txt in label_dir.glob("*.txt"): classes = set() for line in txt.read_text().strip().splitlines(): classes.add(int(line.split()[0])) for c in classes: class_to_files[c].append(txt.stem) val_files = set() for c, files in class_to_files.items(): sample_n = max(1, int(len(files) * 0.15)) val_files.update(random.sample(files, min(sample_n, len(files)))) with open("val_split.txt", "w") as f: f.write("\n".join(sorted(val_files)))这样写出来的val集合能保证每个类别在验证集里至少出现一次,比随机划分出来的指标更能反映真实水平。
训练完成后,用验证集生成混淆矩阵,重点看对角线以外的密集块。比如枸杞和红枣在外观上都是红色小颗粒,混淆矩阵里这块如果很亮,说明模型学到的是“红色小颗粒”而不是“枸杞和红枣的差异”。处理方式有两种:一是找一批两类都有的局部特写图补进训练集,二是干脆在部署场景里通过上下文规则做二次过滤——比如放在托盘里识别时,用检测到的位置和周边目标做约束。
最后是推理输出映射。模型输出的class_id只是0到44的整数,部署时务必保存一份训练时用的names顺序,别等写推理服务时再对着classes.txt猜。我的经验是单独存一份label_map.json,结构是{"0": "人参", "1": "枸杞子", ...},推理代码直接从json读取,不要硬编码在代码里。这个过程我翻过车:训练时把类别顺序调了一次,忘了同步给推理服务,结果线上模型输出44,前端显示了“枸杞子”,实际应该是最后一类。现在所有项目统一用json做标签映射,这份文件跟着模型权重一起走,模型换版本就换json,两边永远配对。这算是我在这类中药识别项目上最值得说的一条教训,希望帮到你少走这一步弯路。
本文还有配套的精品资源,点击获取