
简介中草药类型识别检测数据集面向计算机视觉目标检测与图像分类任务适用于中药品种鉴定、药材质量控制及智能识别系统研发。完整数据集涵盖7976张中草药图片标注类别达45种采用Pascal VOC与YOLO两种主流格式兼顾通用检测框架训练与实时部署需求。本次下载的7z压缩包共2000个文件其中1999个为VOC格式xml标注文件另含1个使用前必读txt说明压缩包整体约481.35MB便于批量训练与模型迭代。该资源已有578人学习标注规范、类别丰富可直接用于YOLO、Faster R-CNN等检测模型的训练与评估。借助这批数据研究者能快速搭建中草药自动识别系统提升分类精度与定位效率亦可作为教学演示、算法对比和论文复现的优质数据源为中医药信息化提供支撑。1. 中草药类型识别检测数据集7976张45类拿到手先别急着训练一个做中药饮片质检的朋友拿着这份中草药类型识别检测数据集VOCYOLO格式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 中草药类型识别检测数据集VOCYOLO格式7976张45类别.7z tree -L 2 | head -60如果系统没有7z命令Ubuntu下先装p7zip-fullsudo apt install p7zip-full。解压后要确认的目录项是四个JPEGImages存原图Annotations存VOC的xml标注labels存YOLO的txt标注classes.txt记录45个类别的名称与顺序。有的数据集会把图片和标注分开放也有的直接在根目录平铺但既然标题写了VOCYOLO双格式绝大多数情况下就是这套结构。提示解压后优先跑一句find JPEGImages -name *.jpg | wc -l确认图片数量是7976。如果少于这个数说明压缩包在传输或解压过程中丢了文件后面训练时的类别均衡统计全是错的。四件套齐了之后别急着删Annotations。很多人的习惯是“反正YOLO训练只要txtxml没用”这个习惯在自建数据集的迭代场景里会让你后悔——xml里保留着原始标注txt只是派生物。当你发现YOLO训练效果不对、想回头查某个框到底是怎么画的xml是最后的后悔药。2.2 VOC格式的xml标注bndbox才是核心name决定类别归属VOC格式的每个xml对应一张图片典型结构是annotation folderJPEGImages/folder filenameherb_0042.jpg/filename size width1280/width height960/height depth3/depth /size object namegouqizi/name bndbox xmin240/xmin ymin120/ymin xmax860/xmax ymax720/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. 把VOCYOLO数据集喂给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的脚本自己写一遍转换逻辑比用现成工具更稳虽然数据集自称“VOCYOLO格式”但为了排查问题我仍然会自己跑一遍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_okTrue) 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.yamldata.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(dataherbal.yaml, epochs0)epochs0不会真正训练但会触发数据加载流程如果data.yaml里路径错了这里就会报错而不是等训练跑到一半才崩。3.4 跑通第一轮训练模型选择、batch和imgsz的取舍格式都验证完、data.yaml也写对了就可以开始第一次训练。我一般用这条命令起手yolo detect train dataherbal.yaml modelyolov8s.pt epochs100 batch16 imgsz640 device0 patience20模型选择上第一轮不要用yolov8n虽然它跑得快、显存占用低但45类这个类别数对n-scale来说容量偏小最后的结果经常是mAP看着还行细分类别比如枸杞和红枣这种同色系药材相互混淆。从s开始往上走如果显存够就m。预训练权重yolov8s.pt会在第一次启动时自动下载公司内网环境记得提前准备离线权重文件。batch16在中等显存卡上是安全值imgsz640是速度和精度的平衡点。如果原始图片普遍在1280以上可以先按640跑通流程之后再对比一次imgsz960的结果——中草药识别里有大量小目标比如枸杞颗粒很小升分辨率往往能带来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.001batch增大一倍然后在训练脚本里加一句ampFalse关闭混合精度。中草药检测场景对推理速度没到极端敏感的程度关闭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两边永远配对。这算是我在这类中药识别项目上最值得说的一条教训希望帮到你少走这一步弯路。本文还有配套的精品资源点击获取