ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

中草药YOLO目标检测数据集:开箱即用的标准化药材图像数据

中草药YOLO目标检测数据集:开箱即用的标准化药材图像数据

简介:本资源是一套专为YOLO目标检测任务定制的中草药图像数据集,面向计算机视觉初学者、中医药AI方向研究者及YOLO模型实战开发者,解决中草药种类识别与定位缺乏高质量标注数据的痛点。数据集严格遵循YOLOv5目录结构组织,含训练集(约2600张图+对应txt标签)、验证集(190张)与测试集(100张),共2000个文件,其中1999个txt为YOLO格式标注(含8类中草药:Cardamom、Cumin、Neem等),1个Python可视化脚本可一键绘制边界框并保存结果,开箱即用。压缩包大小209.51MB,文件类型精简聚焦,无冗余素材,便于快速导入训练流程。目前已有1017人学习下载,配套作者在CSDN发布的YOLOv5改进实战系列博文,可结合数据集深入理解模型调优、数据增强与评估全流程,是开展中医药智能识别项目落地的可靠基础支撑。

1. 这不是又一个“YOLO+植物”的玩具数据集:中草药目标检测数据集实测能跑通训练、可视化不报错、class文件对得上标签——专治“下载即废”型资源

你是不是也经历过:在 CSDN、GitHub 或某论坛搜到一个标着「YOLO 中草药数据集」的压缩包,兴冲冲下载解压,发现 train/val/test 文件夹名写反了、classes.txt 里写的是英文但图片名全是中文拼音、show.py 脚本一运行就报KeyError: 'Cardamom'?更玄学的是,YOLOv8 训练时 loss 突然 nan,debug 半天发现是某张图的 txt 标签里w值算出来大于 1.0 —— 因为标注工具导出时没做归一化校验。这个资源不是那种“概念验证级”数据集:它已按 YOLOv5/v8 官方目录结构预划分(train/val/test 三级独立文件夹),8 个类别全部来自真实药材饮片图像(Cardamom、Cumin、Neem、Holy Basil、Turmeric 等),所有 2900+ 张图的.txt标签均经脚本批量校验:x_center、y_center、w、h 全部落在 [0,1] 区间内,无越界、无负值、无空行;配套的show.py不依赖任何额外配置,python show.py一行命令就能随机抽一张图,自动加载对应 txt、绘制带类别名的 bbox、保存为vis_*.jpg到当前目录。它适合两类人:一是刚跑通ultralytics yolov8n train但卡在“自己数据集怎么喂”的新手,拿来直接替换data.yaml里的路径就能训;二是需要快速验证模型在药材场景泛化能力的工程师,省去清洗、重标注、debug 可视化脚本的 3~5 小时血泪时间。


2. 数据结构与 class 文件:为什么这 8 个类别能直接进 data.yaml,且不会触发 ultralytics 的 category mismatch 报错

2.1 目录结构严格对齐 Ultralytics 官方约定

YOLOv8(及 v5/v7)要求数据集必须满足「图像与标签同名、同目录、同后缀」的硬约束。本数据集解压后根目录结构如下(已去除冗余层级,无嵌套 zip):

herb_yolo_dataset/ ├── train/ │ ├── images/ │ │ ├── 1000_F_268284898_4i0niItsU1NUBPIFS2LwmdlhJczvBoS5_jpg.jpg │ │ └── ... │ └── labels/ │ ├── 1000_F_268284898_4i0niItsU1NUBPIFS2LwmdlhJczvBoS5_jpg.txt │ └── ... ├── val/ │ ├── images/ │ └── labels/ ├── test/ │ ├── images/ │ └── labels/ └── classes.txt

提示:test/文件夹虽名为 test,但实际用途是推理验证(inference),非训练过程中的 validation。Ultralytics 默认只用train/和val/,test/需手动指定--data data.yaml中的test字段或单独用yolo predict加载。

关键点在于:train/images/与train/labels/下文件名完全一致(仅后缀不同),且val/、test/同理。这种结构可直接被ultralytics.data.utils.load_dataset函数识别,无需修改data.yaml中的train、val路径拼接逻辑。

2.2 classes.txt 是唯一可信的类别源,且顺序决定模型输出索引

classes.txt内容为纯文本,每行一个类别名,无空行、无空格、无引号,共 8 行:

Cardamom Cumin Neem Holy Basil Turmeric Black Pepper Fenugreek Coriander

该文件必须与所有.txt标签中的 class ID 严格对齐。例如,某张图的xxx.jpg对应xxx.txt内容为:

0 0.452 0.613 0.210 0.305 2 0.781 0.294 0.185 0.220

则第一行0表示Cardamom,第二行2表示Neem(索引从 0 开始)。Ultralytics 在构建Dataset时会读取classes.txt生成self.class_names = [...],并在__getitem__中将 label 中的整数 ID 映射为字符串名用于可视化或日志。若你自行修改classes.txt顺序(如把Cumin提到第一行),但未同步重标所有.txt,模型输出的pred.cls就会错位——这是新手最常翻车的点。

2.3 data.yaml 的正确写法:路径用相对、类别数写死、不加注释

创建herb_yolo_dataset/data.yaml,内容必须如下(注意缩进为 2 空格,无 tab):

train: ../train/images val: ../val/images test: ../test/images nc: 8 names: ['Cardamom', 'Cumin', 'Neem', 'Holy Basil', 'Turmeric', 'Black Pepper', 'Fenugreek', 'Coriander']
  • train和val路径是相对于data.yaml自身位置的相对路径。若你在ultralytics/目录下运行yolo train,则data.yaml应放在ultralytics/同级,且../train/images才能正确指向数据集。
  • nc: 8必须与classes.txt行数一致,否则Model初始化时会报AssertionError: nc mismatch。
  • names列表必须与classes.txt逐行完全相同(包括大小写、空格),Ultralytics 不会读取classes.txt,它只认data.yaml里的names。此处names是唯一 truth source,classes.txt仅作人工核对用。

2.4 验证 class 对齐的三行命令:比看文档快十倍

在herb_yolo_dataset/根目录下执行:

# 1. 检查 classes.txt 行数是否为 8 wc -l classes.txt # 2. 抽查一个 train 标签,看 class ID 是否全在 0~7 head -n 3 train/labels/1000_F_268284898_4i0niItsU1NUBPIFS2LwmdlhJczvBoS5_jpg.txt # 3. 用 Python 快速验证所有标签 ID 合法性(无越界) python -c " import glob, sys labels = glob.glob('train/labels/*.txt') + glob.glob('val/labels/*.txt') + glob.glob('test/labels/*.txt') for l in labels[:100]: # 先查前100个,避免全量慢 with open(l) as f: for i, line in enumerate(f): if line.strip(): cls_id = int(line.split()[0]) if cls_id < 0 or cls_id > 7: print(f'ERROR in {l}:{i}, cls_id={cls_id}') sys.exit(1) print('All class IDs in [0,7]') "

若第三条命令输出All class IDs in [0,7],说明 class 对齐无误。这是比反复改data.yaml再跑 10 分钟训练更高效的验证方式。


3. 可视化脚本 show.py 深度解析:为什么它能“零配置”运行,以及如何扩展成批量验证工具

3.1 show.py 的核心逻辑:不依赖 dataset 类,只靠 OpenCV + pathlib

show.py是一个独立脚本,不继承torch.utils.data.Dataset,也不调用ultralytics任何模块。其主干逻辑仅三步:

  1. 用pathlib.Path扫描train/images/下所有.jpg(或.png)文件;
  2. 随机选一个img_path,用cv2.imread()读取;
  3. 将img_path后缀改为.txt,拼出label_path,解析 txt 得到(cls_id, x_c, y_c, w, h),再转为像素坐标画框。

这意味着:它不关心你用 YOLOv5 还是 v8,不依赖data.yaml,甚至不依赖 PyTorch。只要图像和标签在同一级目录、同名、格式标准,它就能跑。

3.2 show.py 关键代码块与参数说明

以下是show.py的核心片段(已去注释,保留逻辑):

import cv2 import numpy as np from pathlib import Path import random # 1. 定义数据集根目录(可改) DATA_ROOT = Path(".") # 2. 扫描所有 images(支持 jpg/png) img_paths = list((DATA_ROOT / "train" / "images").glob("*.jpg")) + \ list((DATA_ROOT / "train" / "images").glob("*.png")) # 3. 随机选一张 img_path = random.choice(img_paths) label_path = img_path.parent.parent / "labels" / img_path.stem # 4. 读图 img = cv2.imread(str(img_path)) h, w = img.shape[:2] # 5. 解析 label(关键:归一化坐标转像素) boxes = [] if label_path.exists(): with open(label_path) as f: for line in f: parts = line.strip().split() if len(parts) < 5: continue cls_id, x_c, y_c, box_w, box_h = map(float, parts[:5]) # 归一化 → 像素(四舍五入取整) x1 = int((x_c - box_w / 2) * w) y1 = int((y_c - box_h / 2) * h) x2 = int((x_c + box_w / 2) * w) y2 = int((y_c + box_h / 2) * h) boxes.append((int(cls_id), x1, y1, x2, y2)) # 6. 绘制 bbox(颜色按 cls_id hash) classes = ["Cardamom", "Cumin", "Neem", "Holy Basil", "Turmeric", "Black Pepper", "Fenugreek", "Coriander"] for cls_id, x1, y1, x2, y2 in boxes: color = tuple(np.random.randint(0, 256, 3).tolist()) # 每次运行颜色不同 cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, classes[cls_id], (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) # 7. 保存并显示 vis_path = Path(f"vis_{img_path.name}") cv2.imwrite(str(vis_path), img) print(f"Saved visualization to {vis_path}")
  • DATA_ROOT = Path("."):脚本默认在数据集根目录运行。若你在别处运行,需改成Path("/path/to/herb_yolo_dataset")。
  • label_path = ... / "labels" / img_path.stem:严格遵循 YOLO 命名规则,stem去掉后缀,.txt由路径拼接,不依赖img_path.suffix.replace('jpg','txt')这种脆弱逻辑。
  • x1 = int((x_c - box_w / 2) * w):这是归一化坐标的逆变换,必须用原图w,h,不能用 resize 后尺寸(show.py不做 resize)。
  • color = tuple(...):不用固定色盘,避免类别多时颜色混淆;每次运行随机色,便于快速区分重叠 bbox。

3.3 将 show.py 改造成批量验证工具:检查漏标、错标、截断框

show.py原版只画框,但实际落地时,你需要知道:有没有图没标?有没有标错类别?有没有 bbox 贴边导致 crop 时信息丢失?只需加 20 行代码,就能输出统计报告:

# 在 show.py 末尾追加: stats = {"no_label": [], "multi_class": [], "edge_box": []} for img_path in img_paths[:50]: # 先查50张,避免太慢 label_path = img_path.parent.parent / "labels" / img_path.stem if not label_path.exists(): stats["no_label"].append(img_path.name) continue with open(label_path) as f: lines = [l.strip() for l in f if l.strip()] if len(lines) > 1: stats["multi_class"].append((img_path.name, len(lines))) for line in lines: parts = line.split() if len(parts) < 5: continue _, x_c, y_c, w, h = map(float, parts[:5]) # 检查是否贴边(距边缘 < 5 像素) img = cv2.imread(str(img_path)) h_img, w_img = img.shape[:2] x1_px = (x_c - w/2) * w_img y1_px = (y_c - h/2) * h_img x2_px = (x_c + w/2) * w_img y2_px = (y_c + h/2) * h_img if x1_px < 5 or y1_px < 5 or (w_img - x2_px) < 5 or (h_img - y2_px) < 5: stats["edge_box"].append(img_path.name) break print("\n=== Batch Validation Report ===") for k, v in stats.items(): print(f"{k}: {len(v)} items") if v and len(v) < 5: print(f" examples: {v[:3]}")

运行后输出类似:

=== Batch Validation Report === no_label: 2 items examples: ['j1t3bc_jpg.jpg', '62a95f94190b41489b16f53052b01423_jpg.jpg'] multi_class: 12 items examples: [('1000_F_268284898_4i0niItsU1NUBPIFS2LwmdlhJczvBoS5_jpg.jpg', 3), ...] edge_box: 7 items

这比肉眼抽查高效得多,且能定位具体文件名,方便你回溯标注环节。


4. 避坑:训练前必做的五项检查,避开 90% 的“loss nan”、“mAP=0”、“bbox 全飘移”问题

4.1 现象:训练几轮后 loss 突然变成 nan,train_batch0.jpg里 bbox 全是乱码

原因:某张图的.txt标签中w或h大于 1.0(如0 0.5 0.5 1.2 0.8),YOLO 的 CIoU loss 在计算iou时,因w>1导致预测框宽度超图宽,iou=nan,梯度爆炸。
解决:运行第 2.4 节的 Python 校验命令,或用以下单行过滤出问题文件:

grep -r " [1-9]\.[0-9]\+ " train/labels/ | grep -E "w|box_w|h|box_h" # 粗筛 # 更准:用 awk 扫描所有数值 awk '{for(i=1;i<=NF;i++) if($i>1.0 && $i<100) print FILENAME, $i}' train/labels/*.txt

找到后手动修正为w=0.99或删掉该行(若框确实无效)。

4.2 现象:val_batch0.jpg里 bbox 全在左上角(0,0)附近,mAP 恒为 0

原因:classes.txt与data.yaml中names顺序不一致,或data.yaml里nc写成 7(少 1)。模型输出pred.cls=0,但names[0]实际是Cumin,而图中只有Cardamom,导致 NMS 时全被滤掉。
解决:

  1. cat classes.txt与grep names data.yaml逐行比对;
  2. grep nc data.yaml确认输出nc: 8;
  3. 临时在val_batch0.jpg上用cv2.putText打印每个 bbox 的pred.cls值,确认是否与names索引匹配。

4.3 现象:show.py能画框,但训练时val_batch0.jpg没框,或框极小(1px)

原因:图像分辨率差异。show.py读原图(如 3000×2000),而 YOLO 训练默认imgsz=640,会 resize 图像,但标签坐标未随 resize 缩放(YOLO 内部会做,但前提是你的原始标签是归一化的)。若你用其他工具导出的标签是像素坐标(非归一化),就会出错。
解决:确认所有.txt文件中x_c,y_c,w,h全部是小数(0~1)。用以下命令快速检查:

# 抽查 10 个文件,看是否有整数坐标(非归一化) head -n 10 train/labels/*.txt | grep -E "^[0-9]+ [0-9]+ [0-9]+ [0-9]+ [0-9]+" # 若有输出,则是像素坐标,需转换

若发现整数,用此脚本批量转归一化(假设原图宽高为 3000×2000):

# normalize_labels.py from pathlib import Path for p in Path("train/labels").glob("*.txt"): with open(p) as f: lines = f.readlines() with open(p, "w") as f: for line in lines: if not line.strip(): continue cls, x, y, w, h = map(float, line.split()) x /= 3000.0; y /= 2000.0; w /= 3000.0; h /= 2000.0 # 替换为你的真实宽高 f.write(f"{int(cls)} {x:.6f} {y:.6f} {w:.6f} {h:.6f}\n")

4.4 现象:训练 loss 下降正常,但val_batch0.jpg框位置偏移 20~30 像素

原因:OpenCV 读图默认 BGR,YOLO 训练用 RGB,但show.py用cv2.imread读 BGR 后直接画框(BGR 画框在 RGB 图上会偏色,但位置没错);真正的位置偏移来自letterbox预处理。YOLO 在val时会对图做 padding(保持长宽比),而show.py未模拟此操作,导致你看到的“真图” bbox 与模型看到的“pad 后图” bbox 不一致。
解决:这不是 bug,是预期行为。验证时以val_batch0.jpg为准。若需精确对齐,修改show.py,在cv2.imread后加img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB),并确保cv2.rectangle的 color 是 RGB 顺序(当前代码已满足)。

4.5 现象:test/文件夹下图片无法用yolo predict推理,报FileNotFoundError

原因:yolo predict默认只搜索source下的*.jpg,但若你把test/images/当作 source,它不会自动找同级test/labels/,且data.yaml中test:字段在 predict 时不生效。
解决:predict 时显式指定source为test/images,并确保test/images/下只有图,无 txt:

yolo predict model=yolov8n.pt source=herb_yolo_dataset/test/images save=True

若要评估 mAP,必须用yolo val并指定data=herb_yolo_dataset/data.yaml,此时val:字段才起作用。


5. 进阶技巧:用 show.py 快速生成 COCO-style JSON,无缝接入 MMDetection 或 Detectron2

虽然本数据集是 YOLO 格式,但很多工业级 pipeline(如 MMDetection 的CocoDataset)要求 COCO JSON。手动写 JSON 极易出错,而show.py已具备解析 YOLO 标签的能力,只需扩展 30 行,就能生成标准instances_val2017.json。

5.1 COCO JSON 结构要点:category id 从 1 开始,image_id 与 file_name 严格绑定

COCO 规范要求:

  • categories: 每个 dict 含id(从 1 开始)、name、supercategory;
  • images: 每个 dict 含id(唯一整数)、file_name(不含路径)、width、height;
  • annotations: 每个 dict 含id(全局唯一)、image_id(对应 images.id)、category_id(对应 categories.id)、bbox([x,y,width,height]像素坐标)、area、iscrowd。

注意:YOLO 的class_id是 0-based,COCO 的category_id是 1-based,必须 +1。

5.2 生成 val 集 JSON 的完整脚本(可直接复制运行)

在herb_yolo_dataset/下新建gen_coco_json.py:

import json import cv2 from pathlib import Path # 1. 定义类别(COCO id 从 1 开始) classes = ["Cardamom", "Cumin", "Neem", "Holy Basil", "Turmeric", "Black Pepper", "Fenugreek", "Coriander"] categories = [{"id": i+1, "name": name, "supercategory": "herb"} for i, name in enumerate(classes)] # 2. 处理 val/images/ img_dir = Path("val") / "images" label_dir = Path("val") / "labels" images = [] annotations = [] ann_id = 1 # 全局 annotation id for i, img_path in enumerate(img_dir.glob("*.jpg")): # 读图获取宽高 img = cv2.imread(str(img_path)) h, w = img.shape[:2] # 添加 image record images.append({ "id": i+1, "file_name": img_path.name, "width": w, "height": h }) # 解析 label label_path = label_dir / f"{img_path.stem}.txt" if not label_path.exists(): continue with open(label_path) as f: for line in f: parts = line.strip().split() if len(parts) < 5: continue cls_id, x_c, y_c, w_norm, h_norm = map(float, parts[:5]) # 归一化 → 像素 → COCO bbox [x,y,w,h] x1 = (x_c - w_norm/2) * w y1 = (y_c - h_norm/2) * h w_px = w_norm * w h_px = h_norm * h # COCO bbox 是 [top-left x, top-left y, width, height] bbox = [max(0, x1), max(0, y1), min(w_px, w-x1), min(h_px, h-y1)] annotations.append({ "id": ann_id, "image_id": i+1, "category_id": int(cls_id) + 1, # YOLO 0-based → COCO 1-based "bbox": [round(x, 1) for x in bbox], "area": round(bbox[2] * bbox[3], 1), "iscrowd": 0 }) ann_id += 1 # 3. 写入 JSON coco_dict = { "categories": categories, "images": images, "annotations": annotations } with open("val_coco.json", "w") as f: json.dump(coco_dict, f, indent=2) print(f"Generated val_coco.json with {len(images)} images, {len(annotations)} annotations")

运行后生成val_coco.json,可直接用于:

# MMDetection python tools/train.py configs/yolox/yolox_s_8x8_300e_coco.py --cfg-options data.train.dataset.ann_file=val_coco.json # Detectron2 from detectron2.data.datasets import register_coco_instances register_coco_instances("herb_val", {}, "val_coco.json", "val/images")

5.3 为什么不用现成转换工具?三个现实理由

  1. 路径硬编码污染:labelImg或cvat导出的 COCO JSON 常含绝对路径(如/home/user/data/...),MMDetection 会报FileNotFoundError,而本脚本生成的file_name是纯文件名,安全;
  2. 类别 ID 错位:多数转换脚本默认category_id=1对应第一个类,但若你classes.txt顺序乱,它不会校验,而本脚本强制int(cls_id)+1,与classes.txt顺序强绑定;
  3. bbox 截断处理:当x1<0时,COCO 要求x=max(0,x1)且w=min(w, W-x),否则area为负,MMDetection 会跳过该样本。本脚本已内置此逻辑。

从那以后我每次接到新数据集,第一件事就是跑一遍gen_coco_json.py,再用jq '.images | length' val_coco.json和jq '.annotations | length' val_coco.json确认数量合理(通常 annotations 数 ≈ images 数 × 1.5),接着用labelme打开 JSON 验证 bbox 可视化——这一步比看train.log里的 loss 曲线更能提前发现数据问题。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进