简介:面向目标检测与YOLO模型训练的中草药图像数据集,适合初学者或研究者快速上手YOLOv5。数据按YOLOv5文件夹结构保存,标注为类别加中心点坐标与宽高的相对坐标,共8个类别(如Cardamom、Cumin、Neem等),并划分训练、验证、测试集:训练集约2600张,验证集约190张,测试集约100张,可直接用于训练与评估。资源共2000个文件,主体为1999个txt标注文件与1个Python可视化脚本,压缩包约209.51MB;txt对应图片标注,脚本可随机读取图片绘制边界框并保存,无需修改即可运行。目前已有1000余人学习下载。配套提供类别class文件,目录清晰,省去自行清洗划分时间,适合作为中草药目标检测起步数据或YOLO练习。
1. 中草药目标检测,先别急着训练:数据集才是第一道坎
做中草药图像目标检测,绝大多数人上手第一件事就是找模型、调参、跑训练,结果折腾半天发现 AP 值惨不忍睹,回头查数据才发现标注错位、类别索引混乱、训练集和验证集互相“串门”。真正的瓶颈不在 YOLO 的网络结构,而在喂给 YOLO 的数据集本身。这个项目标题其实给了完整的答案:一份划分好的中草药图像数据集、一个类别 class 文件、一套数据可视化脚本。它要解决的不是“怎么把模型训出来”,而是“怎么把数据准备成 YOLO 真正能吃的样子,并且在训练之前就把烂数据揪出来”。适合正在用 YOLOv5/v8 做中草药识别、刚拿到一批药材照片不知道怎么组织成数据集、或者训练出来 AP 忽高忽低找不到原因的从业者。
2. 把中草药照片变成 YOLO 能吃的格式:目录结构与坐标换算
2.1 YOLO 标签的四字段含义与坐标归一化计算
YOLO 系列(v5/v8 都一样)不认常见的 x1y1x2y2 绝对像素坐标,而是要求归一化后的中心点坐标加宽高。每一行对应一个目标实例,结构是:class_id x_center y_center width height。class_id 是类别索引,从 0 开始,对应 class 文件里的行号;x_center 和 y_center 是目标中心点相对图片宽高的比例;width 和 height 是目标框宽高相对图片宽高的比例,全部归一化到 0~1 之间。
为什么要归一化?因为中草药数据集的照片分辨率差异极大——微距特写可能是 6000×4000,手机随手拍是 1920×1080,如果不归一化,模型预处理时就要为每种分辨率重新缩放,而归一化之后模型完全不关心输入图片的绝对像素值。一个具体的例子:一张 1920×1080 的甘草叶片照片,人工标注框左上角在 (960, 540)、右下角在 (1440, 810),中心就是 (1200, 675),框宽 480、高 270,归一化后得到:
0 0.625 0.625 0.25 0.25换算公式本身很简单,从绝对坐标到归一化坐标:
x_center = (x1 + x2) / 2 / image_width y_center = (y1 + y2) / 2 / image_height width = (x2 - x1) / image_width height = (y2 - y1) / image_height反向换回像素坐标,在可视化脚本里会用到:
x1 = int((x_center - width / 2) * image_width) y1 = int((y_center - height / 2) * image_height) x2 = int((x_center + width / 2) * image_width) y2 = int((y_center + height / 2) * image_height)这块的坑主要在三点:一是直接用整数坐标算中心点,会造成 1~2 像素的误差,对中草药里大量小目标(枸杞干果、金银花单朵)是能感知到的;二是忘了除以图片宽高,标签里出现大于 1 的值,训练时边框计算直接乱套;三是坐标顺序写反,把 x_center 写成了 y_center,可视化时所有框全部方向错乱。我一般会在新建数据集时先写一个扫描脚本,检查所有标签文件的数值范围:合法的 x_center、y_center、width、height 必须在 0 到 1 之间,越界的直接打印路径,人工确认后再继续。
2.2 划分好的数据集:images 与 labels 双目录结构与文件命名对齐
一个规范的、划分好的中草药 YOLO 数据集,目录结构通常长这样:
chinese_herb_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── classes.txt └── data.yaml这种 images/labels 双目录结构是 YOLO 系列的默认约定,不是个人习惯。训练时 YOLO 会根据 data.yaml 里指定的图片路径,自动在同级目录下找 labels 目录,图片在 images/train 里,标签就默认在 labels/train 里,不能自己乱放。
data.yaml 是划分配置的入口,YOLOv8 加载数据集时读的就是它:
train: ./chinese_herb_dataset/images/train val: ./chinese_herb_dataset/images/val test: ./chinese_herb_dataset/images/test nc: 5 names: ['gancao', 'gouqi', 'jinyinhua', 'dangshen', 'huangqi']这里有个高频坑:train 路径写成相对路径时,一旦训练脚本里改动过工作目录,路径解析就会失败,直接报 FileNotFoundError。我习惯在 data.yaml 里写绝对路径,换机器时多改几个字符串,但排错时间省下来远高于这点麻烦。
文件命名有一条硬规则:图片和标签的主文件名必须完全一致,只是扩展名不同。比如 images/train/gancao_001.jpg 对应 labels/train/gancao_001.txt。如果标注工具导出时命名规则不一致,YOLO 训练不会报错,只会静默忽略那张没有标签的图,你的训练数据量会在不知不觉中缩水。这是个很阴的问题,因为很多人看数据没报错就以为没问题,直到训练完才发现某类别的 AP 起不来。
“划分好的数据集”从代码上看就是一次 shuffle 加三分。下面这个脚本把一个目录下的图片和对应标注按 8:1:1 分成训练、验证、测试三份:
import os import random import shutil random.seed(2024) # 固定随机种子,保证划分结果可复现 src_images = "raw_images" # 原始图片目录 src_labels = "raw_labels" # 原始标签目录(YOLO格式) dst_root = "chinese_herb_dataset" train_ratio, val_ratio = 0.8, 0.1 image_files = [f for f in os.listdir(src_images) if f.lower().endswith(('.jpg', '.png', '.jpeg'))] random.shuffle(image_files) n = len(image_files) n_train = int(n * train_ratio) n_val = int(n * val_ratio) splits = { "train": image_files[:n_train], "val": image_files[n_train:n_train + n_val], "test": image_files[n_train + n_val:] } for split_name, files in splits.items(): os.makedirs(os.path.join(dst_root, "images", split_name), exist_ok=True) os.makedirs(os.path.join(dst_root, "labels", split_name), exist_ok=True) for fname in files: stem = os.path.splitext(fname)[0] src_img = os.path.join(src_images, fname) src_lbl = os.path.join(src_labels, stem + ".txt") shutil.copy(src_img, os.path.join(dst_root, "images", split_name, fname)) if os.path.exists(src_lbl): shutil.copy(src_lbl, os.path.join(dst_root, "labels", split_name, stem + ".txt")) else: print(f"[警告] {fname} 缺少对应标签文件")逻辑说明:先读全部图片文件名,固定随机种子后 shuffle,避免每次运行得到不同划分;再按比例切出三个文件列表;最后逐个把图片和同名标签复制到目标目录。缺失标签时打印警告但不中断运行,方便回头统计漏标情况。
参数说明:train_ratio=0.8、val_ratio=0.1,测试集自动取剩余的 0.1。如果总样本数小于 300,建议改成 0.7/0.15/0.15,因为验证集太小会导致 mAP 波动很大,同一份代码跑两遍结果差异明显。另外注意这里用的是shutil.copy,也就是复制而不是移动文件——保留原始目录这个习惯非常重要。后续要是发现划分有问题,随时可以重新切,不用后悔。
3. 类别 class 文件与数据集划分比例:参数怎么设才不翻车
3.1 class 文件一行一个类名:顺序决定训练索引,别乱排
classes.txt 就是标题里说的“类别 class 文件”,每行一个类别名。比如:
gancao gouqi jinyinhua dangshen huangqi关键规则是:每一行的行号就是训练时这个类别对应的整数索引,第一行是 0,第二行是 1,依此类推。标签文件里每行开头的数字就是这个索引。说得直接点,classes.txt 的排列顺序直接决定了所有标签文件中数字的语义。
这意味着什么?如果你中途改了 classes.txt 的顺序,比如把 jinyinhua 从第三行提到第一行,那么所有标注成 2 的标签在训练时都会变成 gancao,整个模型的类别输出彻底错位。典型翻车场景是:数据集做到一半发现少了一个类别,随手往末尾补一行,或者为了看着整齐按拼音排序了一下,结果所有标签索引全部作废。
一个保守但有效的习惯:class 文件从建立第一天就定死,后期新增类别只往末尾追加,绝对不去动已有行的顺序。另外,classes.txt 里的类别名尽量用拼音或英文,不要用中文。不是说 YOLO 不支持中文类别名,而是后续可视化脚本、评估脚本对中文的编码处理不一致,同一个名字在 Windows 命令行和 Linux 终端下显示完全不同,排查起来非常费劲。
如果你用 YOLOv8,也可以不在单独文件里写类别,直接在 data.yaml 的 names 字段定义:
names: 0: gancao 1: gouqi 2: jinyinhua 3: dangshen 4: huangqi但单独交付 classes.txt 有一个不可替代的好处:标注工具(LabelImg、CVAT、labelme)可以直接加载这个文件作为预定义类别列表,标注员不需要手动输入类别名,从根源上避免类别拼写不一致。而且很多开源目标检测项目只认 classes.txt、不认 data.yaml,两个文件都准备好,兼容性最好。
3.2 训练/验证/测试划分:按比例随机还是按采集批次分
第 2 章的划分脚本是纯随机 shuffle,适合单一批次采集、光照和背景差异不大的场景。但中草药数据集有个特点:图片往往来自多个批次。比如一批在晴天户外拍,光照充足;一批在室内补光灯下拍,背景是黑板;一批用手机随手拍,分辨率低、画面模糊。如果纯随机划分,同一株药材的不同角度照片可能同时出现在训练集和验证集里,评估结果虚高——模型不是在泛化,而是在“背答案”。
更稳的划分方式是按采集批次分组。假设你的原始数据按批次文件夹组织,每个文件夹内部是一个拍摄批次,那划分策略就变成了选哪些批次进训练集、哪些批次进验证集:
import os import shutil batch_root = "herb_batches" # 子文件夹代表不同采集批次 dst_root = "chinese_herb_dataset" train_batches = ["batch_01", "batch_02", "batch_03", "batch_04"] val_batches = ["batch_05"] test_batches = ["batch_06"] for split_name, batches in [("train", train_batches), ("val", val_batches), ("test", test_batches)]: os.makedirs(os.path.join(dst_root, "images", split_name), exist_ok=True) os.makedirs(os.path.join(dst_root, "labels", split_name), exist_ok=True) for batch in batches: batch_img_dir = os.path.join(batch_root, batch, "images") batch_lbl_dir = os.path.join(batch_root, batch, "labels") for fname in os.listdir(batch_img_dir): stem = os.path.splitext(fname)[0] src_img = os.path.join(batch_img_dir, fname) src_lbl = os.path.join(batch_lbl_dir, stem + ".txt") dst_img = os.path.join(dst_root, "images", split_name, fname) dst_lbl = os.path.join(dst_root, "labels", split_name, stem + ".txt") shutil.copy(src_img, dst_img) if os.path.exists(src_lbl): shutil.copy(src_lbl, dst_lbl) else: print(f"[警告] {batch}/{fname} 缺少标签")逻辑说明:脚本不做任何随机打乱,直接把指定的批次文件夹整体复制到对应的划分目录。每次运行结果完全一致,可复现性比随机划分好很多。因为代码里不会产生随机数,也就不存在设不设 seed 的问题。
参数说明:train_batches、val_batches、test_batches 的分组完全凭业务判断。我的经验是把光照最刁钻(强逆光、弱光)和背景最杂乱(地面、树叶缝隙)的批次放到验证集和测试集,训练集只保留条件相对正常的批次。原因是让模型先学会把类别的共性特征抓住,再去面对恶劣条件,训练损失曲线会平滑很多;反过来如果训练集全是恶劣光照,模型会花大量迭代去学光照不变性,而不是学药材本身的纹理和形状特征。
如果你只是想快速验证流程通不通,随机划分就够了;等流程跑通,再按批次重新划分一次做正式训练。这个先后顺序不冲突,用随机划分出的模型 mAP 只能作为参考,不能作为最终结论。
4. 数据可视化脚本:把每张图的标注画出来,一眼看出问题
4.1 用 OpenCV 把 YOLO 标签画回原图:脚本逻辑与参数说明
光看数字标签发现不了问题,必须把标注框可视化到原图上。这段脚本是我拿到任何新数据集时必跑的第一段代码,它读入图片和同名的 YOLO 标签,反向换算坐标后用 OpenCV 绘制矩形框和类别名称,输出到 vis_output 目录:
import cv2 import os VIS_DIR = "vis_output" IMG_DIR = "chinese_herb_dataset/images/train" LBL_DIR = "chinese_herb_dataset/labels/train" class_names = ["gancao", "gouqi", "jinyinhua", "dangshen", "huangqi"] os.makedirs(VIS_DIR, exist_ok=True) green = (0, 255, 0) # BGR 颜色,绿色框 red = (0, 0, 255) # BGR 颜色,红色文字 for img_name in os.listdir(IMG_DIR): if not img_name.lower().endswith(('.jpg', '.png', '.jpeg')): continue stem = os.path.splitext(img_name)[0] img_path = os.path.join(IMG_DIR, img_name) lbl_path = os.path.join(LBL_DIR, stem + ".txt") img = cv2.imread(img_path) if img is None: print(f"[跳过] 图片无法读取: {img_path}") continue h, w = img.shape[:2] if not os.path.exists(lbl_path): print(f"[警告] 无标签文件: {lbl_path}") continue with open(lbl_path, "r") as f: for line in f: parts = line.strip().split() if len(parts) != 5: print(f"[警告] 非法标签行 {lbl_path}: {line.strip()}") continue cls_id = int(parts[0]) x_center = float(parts[1]) y_center = float(parts[2]) bw = float(parts[3]) bh = float(parts[4]) x1 = int((x_center - bw / 2) * w) y1 = int((y_center - bh / 2) * h) x2 = int((x_center + bw / 2) * w) y2 = int((y_center + bh / 2) * h) x1 = max(0, x1); y1 = max(0, y1) x2 = min(w - 1, x2); y2 = min(h - 1, y2) cv2.rectangle(img, (x1, y1), (x2, y2), green, 2) label_text = class_names[cls_id] if cls_id < len(class_names) else f"cls_{cls_id}" cv2.putText(img, label_text, (x1, max(y1 - 5, 15)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, red, 2) cv2.imwrite(os.path.join(VIS_DIR, img_name), img) print("全部可视化完成,输出目录:", VIS_DIR)逻辑说明:脚本遍历 IMG_DIR 下所有图片,对每张图执行读图、读标签、按行解析、反向归一化、画框、写字、输出。反向坐标换算必须放在画框之前,因为 YOLO 标签里是 0 到 1 的浮点数,OpenCV 必须在像素坐标系上画图。边界裁剪用 max/min 函数把坐标限制在图片范围内,防止标注框超出图像区域导致 cv2.rectangle 崩溃。len(parts) != 5的校验用来拦截空行、多余空格、tab 混用等脏行。
参数说明:颜色是 BGR 顺序,不是 RGB,green=(0, 255, 0) 表示纯绿,你要是把 RGB 的绿色直接传进去,画出来是蓝色。max(y1 - 5, 15)防止类别文字画到图片上边缘之外被截断。字体大小 0.6、线宽 2 适用于 1000 到 3000 像素分辨率的图片,更大分辨率需要按比例放大,后面单独说。
提示:如果你发现某个类别的框在可视化结果里总是明显偏大,先检查这个类别的标注规范是不是和其他类别不一致,比如把整株药材当成一个框、而不是按叶片或果实分别框选。这种情况在多人协作标注时很常见,属于“标注粒度不一致”,不是脚本问题。
4.2 可视化图里常见的三类异常:错框、漏标、类别串位
跑完可视化后,不要急着关掉输出目录,按类别翻一遍图,重点看三类异常。
错框最容易发现:框把背景圈进去了,或者只框住了药材的一半。中草药是非刚性目标,枸杞枝条是弯曲的,金银花一簇有好几朵,矩形框天然无法完美贴合,标注时要做取舍。我的原则是框宁可大一点,也不要漏掉药材边缘,因为 YOLO 靠中心点和回归框定位,中心点偏了远比框大一点影响大。
漏标就是图片里明显有一朵金银花或一颗枸杞干果,但可视化图上没有对应的框。只能回到标注工具里补标,没有捷径。但有个排查技巧:把漏标图按拍摄批次分组,如果集中在某个批次,八成是标注员后半程疲劳了;如果分散在各批次,可能是标注规范没讲清楚,大家对“什么算一个目标”的理解不一致。
类别串位是指框的位置正确,但旁边的文字对不上,比如框里明明是枸杞却写着 gancao。先检查脚本里的 class_names 和 classes.txt 的顺序是否一致,多数是脚本顺序写错了;脚本没问题,那就是标注时选错了类别,需要单独改标签文件里那一行的类别索引。这里要特别提醒:改标签索引之前先备份,改错了还有后悔药,改完了再跑一遍可视化确认,别改个寂寞。
5. 中草药数据集避坑:从标注到划分的五个常见问题
5.1 现象:训练时报错“Label class X exceeds nc=Y”
训练 YOLO 时控制台直接报一个Label class 5 exceeds nc=5,训练中断。
原因:标签文件里出现了索引为 5 的类别,但 data.yaml 里配置的 nc=5,合法索引是 0 到 4。这通常是标注工具的类别列表和 classes.txt 不一致,标注时多选了一个类别;或者某次清理时删掉了 classes.txt 里的一行,却没有去同步已经标好的标签。
解决:先写一个脚本遍历全部标签文件,统计出现过的类别索引集合,和 range(nc) 比对。发现越界索引时,定位到具体标签文件,人工核对是标注错还是样本不该保留,再决定改索引还是删文件。绝对不要批量把 5 改成 4,那会把两种不同的药材混成同一个类别,后续模型怎么训都分不开。
5.2 现象:可视化脚本运行到一半报“assertion failed”
cv2.rectangle 报assertion failed (width > 0 && height > 0),脚本中断在某一张图片上。
原因:某个标签文件里 width 或 height 为 0,或者反向换算后 x2 <= x1、y2 <= y1。这通常是标注工具对极小目标误操作,或者有人手动编辑了标签文件,把坐标改成了同一个值。中草药里小目标特别多——枸杞干果在整张照片里可能只有二三十像素,容易出现这种精度问题。
解决:在可视化脚本里增加面积过滤,反向换算后如果宽度或高度小于等于 0,跳过这个框并打印警告。同时建议在数据预处理阶段就删除这类无效行,因为零面积框的 loss 计算没有意义,留着只会让训练过程产生 NaN。
5.3 现象:训练集 mAP 很高,但验证集 mAP 塌方
训练集 mAP 超过 90%,验证集 mAP 只有 50% 多,差距大得离谱。
原因:大概率是划分策略错了。纯随机 shuffle 时,同一采集批次的同源图片被分到训练集和验证集,验证集 mAP 虚高;反过来如果验证集塌方,通常是某个较难的批次被整体放进了验证集,而训练集完全没见过这种光照和背景,模型的泛化能力在验证集上被真实地暴露出来。
解决:先按采集批次重做划分,让验证集覆盖尽可能多样的条件。如果重划分后还是低,就去检查验证集的可视化图,确认标注框质量和类别分布。排除掉数据问题之后,才轮得上去调模型参数。很多人一上来就调学习率、改网络结构,结果根子在数据划分上,白忙一场。
5.4 现象:数据可视化图上看不清小目标标注
可视化输出图上,枸杞干果的框线太细看不清,多个框重叠之后根本分不清对应关系。
原因:画框线宽固定为 2 像素、字号固定为 0.6,而图片分辨率可能高达 4000×3000,小目标框只有几十像素,整图缩略显示时框线几乎不可见。这不算数据集坏掉,是可视化参数不适配高分辨率图片。
解决:让线宽跟着图片尺寸走,一行代码的事:cv2.rectangle(..., max(2, round(min(h, w) // 500)), ...)。字号同理按max(0.6, min(h, w) / 2000)调整。输出图保存一份缩略版,按 1200px 宽度等比缩放,方便快速扫全图。如果看完一轮还是觉得不清楚,按类别分目录输出,一次只看一个类别的标注分布。
5.5 现象:同一种药材在 class 文件里出现两种写法
统计类别分布时,发现实际类别数比预期多,比如 “gouqi” 和 “gouqi_gan” 其实是同一种枸杞,被当成了两个类。
原因:多人协作标注时,标注工具没有加载预定义的 classes.txt,标注员各自按自己的习惯手动输入类别名。这在团队项目里几乎一定会发生,越是对命名不熟悉的药材越容易出问题。
解决:先从标注工具端强制加载统一的 classes.txt,禁止自由输入。已经标完的数据,用脚本扫描全部标签文件,统计出现的类别索引集合,把多余索引统一成标准类的合法索引。做这个操作前先整体备份 labels 目录,改错了可以快速回滚,别在原始数据上反复横跳。
6. 最后一道防线:训练前跑一个数据体检脚本
数据集的体检脚本,是我每次训练前必跑的最后一段代码。它不画图,只统计四个关键数字:每类的样本数、每张图的平均标注框数、标注框的宽高分布、图片分辨率分布。这四个数字基本决定了 YOLO 训练会不会在早期就出幺蛾子。
import os from collections import Counter IMG_DIR = "chinese_herb_dataset/images/train" LBL_DIR = "chinese_herb_dataset/labels/train" class_names = ["gancao", "gouqi", "jinyinhua", "dangshen", "huangqi"] cls_counter = Counter() box_per_img = [] for img_name in os.listdir(IMG_DIR): stem = os.path.splitext(img_name)[0] lbl_path = os.path.join(LBL_DIR, stem + ".txt") if not os.path.exists(lbl_path): continue with open(lbl_path) as f: lines = f.readlines() box_per_img.append(len(lines)) for line in lines: parts = line.strip().split() if len(parts) == 5: cls_counter[int(parts[0])] += 1 print("类别分布:", dict(cls_counter)) if box_per_img: print("平均每图框数:", sum(box_per_img) / len(box_per_img)) print("最大单图框数:", max(box_per_img))逻辑说明:cls_counter 统计每个类别出现的总次数,直接暴露类别失衡。box_per_img 按图统计框数,能发现异常图——比如某张图有 200 个框而平均只有 5 个,说明这张图把整片药材都框了一遍,模型容易学偏。最后把统计结果打印出来,和自己建数据集时的预期对比。
参数说明:阈值全凭经验。我的个人标准是:单类别框数少于 300,这个类别的 AP 基本不用指望;平均每图框数超过 20,说明要么目标太密集、要么标注粒度太粗,需要重新审视标注策略;最大单图框数超过平均值的 5 倍,需要把那几张图单独拿出来人工核对。
体检跑完再决定要不要训练。这个习惯帮我躲过了大量白训的坑——很多训练完才暴露的脏数据问题,在训练前跑一遍体检就能拦住一大半。希望这些经验对正在准备中草药数据集的你有实实在在的帮助。
本文还有配套的精品资源,点击获取