首页 / 资讯中心 / 文章详情

中草药目标检测数据集实战:YOLO数据划分、标签与可视化避坑指南

中草药目标检测数据集实战:YOLO数据划分、标签与可视化避坑指南 ★ FEATURED ARTICLE
简介面向中草药图像目标检测场景这份YOLO格式数据集可为目标检测入门者、算法工程师及中药智能化项目提供可直接训练的数据基础无需额外转换即可接入YOLOv5及系列改进模型。数据按YOLOv5目录规范组织共8个类别涵盖Cardamom、Cumin、Neem等常见中草药材并附带完整的classes类别文件标注采用相对坐标格式类别、x_centre、y_centre、w、h便于直接训练与推理。训练集约2600张图片与对应txt标签验证集约190张测试集约100张划分清晰便于复现训练与评估流程。资源包共2000个文件包含1999个txt标注文件与1个Python可视化脚本压缩包约209.51MBshow.py脚本可直接读取任意图片并绘制边界框无需修改配置即可快速核查标注质量适合数据清洗与效果展示。目前已有1019人学习使用对需要标准化中草药检测数据集的读者是一份开箱即用的实用资源。1. 中草药目标检测卡在数据上一套 YOLO 数据集的三个必备件做中草药图像目标检测的人十有八九不是被模型难住的是被数据收拾过的。一个做智慧药房识别场景的开发者曾跟我抱怨网络换了好几种调参调到头秃最后的结论竟是验证集和训练集发生了数据泄漏导致线上效果全线崩盘。这个方向本身就很磨人薄荷和留兰香长得像金银花的花蕾和叶片在俯拍下边界模糊光照从暖黄到阴天冷白变来变去。模型真正能学到什么往往在你收集图片、标框、分目录的那一天就决定了。标题里提到的三样东西——划分好的数据集、类别class文件、数据可视化脚本——恰好组成了一个能直接喂给 YOLO 训练的最小数据集闭环。搞清楚它们的组织逻辑和坑在哪里比换一个更大的 backbone 划算得多。这条笔记就是围绕这套闭环把目录结构、划分脚本、类文件生成、可视化质检和常见翻车点一次说清适合刚准备拿中草药图像训练自己的 YOLO 检测模型、却还在数据集阶段打转的人。2. YOLO 数据集目录与标注格式中草药图像为什么必须按这套组织2.1 镜像目录结构images 与 labels 必须同生共死我见过太多半路接手的数据集图片堆在一个文件夹txt 标签堆在另一个文件夹训练时靠人肉猜路径。YOLO 系训练框架对数据集目录的默认约定非常统一图片放在 images 下标签放在 labels 下两边用 train/val/test 做同样的子目录划分。标准结构长这样herb_yolo/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── classes.txt └── data.yamllabels 和 images 目录的层数、命名必须完全对齐一个训练图片images/train/mentha_001.jpg对应的标签只能是labels/train/mentha_001.txt。不要求文件内容顺序一致但前缀必须一摸一样否则训练框架在labels里找不到同名 txt会直接把这图片当成无目标样本跳过最后你就是得到一堆看起来没问题的空标签。data.yaml 里需要写清楚 train/val 路径、类别数量和类别名列表这是 YOLO 读取数据集的路由信息。一个最小的 data.yaml 长这样train: herb_yolo/images/train val: herb_yolo/images/val nc: 4 names: [leaf_mint, leaf_basil, flower_chrysanthemum, fruit_wolfberry]我一般会建议 test 目录也建上哪怕刚开始不为测试而划分。原因很简单等模型训完你需要一份完全没有参与过验证的图片来做最终盲测。如果一开始没有留 test后面再补往往会因为文件分散而随手抓几张破坏原来的数据边界。中草药图像采集成本高、类别分布天然不均衡前期就别省这个目录。2.2 标注格式归一化坐标里藏着大多数低级错误YOLO 的每个 txt 标签文件每一行代表一个目标框标准字段是五段式类别 id、框中心点 x、框中心点 y、框宽、框高。注意这里的坐标不是一个整数像素值而是经过归一化的相对值范围在 0 到 1 之间。很多中草药数据集标注工具默认输出的是像素坐标直接拿来当 YOLO 格式用是新手踩得最多的坑。归一化的公式不难中心点归一化坐标等于像素中心点除以图片宽或高框宽高同理。例如一张 640×640 的图片上某片叶子的像素框是 x100, y150, w300, h80那么归一化结果是x_center (100 300 / 2) / 640 0.390625 y_center (150 80 / 2) / 640 0.296875 width 300 / 640 0.46875 height 80 / 640 0.125对应的标签行就是0 0.390625 0.296875 0.46875 0.125其中开头的 0 表示类别 id与 classes.txt 的行号严格对应。也就是说 classes.txt 第一行写的类别名对应 id0第二行对应 id1以此类推。一旦你在排类别名时不加小心这个编号就会整体错位模型训练时学到的语义和推理时对不上。中草药图像的标注还有一个特定问题由于叶片形态细碎用像素坐标在自定义训练脚本里看似没问题一旦模型内部做 letterbox 缩放所有坐标都会被整体拉伸和填充像素坐标就全错位。与其在训练配置里反复加偏移校验不如一开始就把所有标签统一转成归一化坐标。2.3 中草药的细粒度特性决定了标注对象要统一提到中草药目标检测有一个绕不开的选择题到底框叶片、框花、框果实还是框整株这个问题直接影响类别文件怎么写和模型能不能收敛。我的经验是大多数场景下框「器官」而不是框「整株」。薄荷整株在田间拍摄时枝条互相遮挡一个框里往往混入两三种植物而单片叶子的边缘清晰、目标小但一致性强。框整株会导致框的宽高比极不稳定、目标内部大量混入背景模型学出的特征是关于「一坨绿色」的而不是关于「薄荷」的。所以我在整理中草药数据集时先在类别定义上达成一个约定要么统一标叶片要么统一标花/果实不要同一个类别里一半标叶一半标全株。以金银花为例如果同时标注它的花蕾、叶片和整株藤蔓建议拆成三个类别名而不是混在一个 label 里。这样做的好处一是可视化的时候能直观看到类间差异二是后续做数据扩充时可以对小类别单独调增强强度。记住一个朴素原则宁可类别多一点、每个类别边界清楚也不要类别少而含义模糊。3. 从原始图片到划分好的数据集train/val/test 划分脚本与 class 文件生成3.1 划分脚本按文件名随机划分并复制文件拿到一批中草药图片和对应的 YOLO 标签后第一步不是立刻训练而是把它划分成 train/val/test 三份。我习惯用 Python 脚本做这个事而不是在文件管理器里手动拖因为脚本可重复执行且能保证 images 与 labels 同步移动。下面这个脚本是这套流程的骨架import random import shutil from pathlib import Path random.seed(42) # 固定随机种子保证可复现 images_dir Path(raw_images) # 原始中草药图片目录 labels_dir Path(raw_labels) # 原始 YOLO 标签目录 out_root Path(herb_yolo) # 输出根目录 ratios {train: 0.8, val: 0.15, test: 0.05} # 只保留既存在图片又存在同名标签的样本 pairs [ (img_path, labels_dir / f{img_path.stem}.txt) for img_path in images_dir.glob(*.jpg) if (labels_dir / f{img_path.stem}.txt).exists() ] random.shuffle(pairs) for i, (img, lbl) in enumerate(pairs): if i len(pairs) * ratios[train]: split train elif i len(pairs) * (ratios[train] ratios[val]): split val else: split test dest_img out_root / images / split / img.name dest_lbl out_root / labels / split / lbl.name dest_img.parent.mkdir(parentsTrue, exist_okTrue) dest_lbl.parent.mkdir(parentsTrue, exist_okTrue) shutil.copy2(img, dest_img) shutil.copy2(lbl, dest_lbl)这段脚本的核心逻辑是先构造「图片到同名标签」的配对列表只保留两边都存在的文件然后打乱顺序按比例切分成三段。copy2会保留文件元信息如果你确定原始目录不会再动也可以换成shutil.move。我建议在中草药这种数据量不大的场景下用复制而不是移动因为一旦划分后想改比例手里还有原始底稿不至于想去恢复被打散的文件夹。参数上ratios里的比例不是死规定。数据量只有几百张时test 占 5% 大概只有二三十张做一次盲测还够用数据量到几千张时test 可以适当提高到 10%。唯一不变的是 train 和 val 的比例不能太接近 9:1否则 val 集太小每次训练出来的 mAP 波动能吓死你。3.2 别急着按图片随机切先按采集批次分 group直接按图片随机划分是最省事的方式但也是最危险的方式。中草药图像的采集往往是按「批次」进行的同一片田、同一天、同一株植物连续拍几十张。如果这些相似度极高的连续帧被随机拆进了 train 和 valval 里就会出现大量与 train 极为相似的图片模型评估结果虚高。等到你换个环境部署模型立刻被打回原形。一个简单可靠的改进是给每张图加一个「批次前缀」。如果你的原文件名已经带了日期或采集区编号例如20240501_plot3_mentha_001.jpg那么划分前先按前缀分组再把整个组放进同一份数据集而不是逐张随机。常见做法是这样from collections import defaultdict groups defaultdict(list) for img_path, lbl_path in pairs: prefix img_path.name.split(_)[0] _ img_path.name.split(_)[1] groups[prefix].append((img_path, lbl_path)) group_keys list(groups.keys()) random.shuffle(group_keys) for i, prefix in enumerate(group_keys): if i len(group_keys) * ratios[train]: split train elif i len(group_keys) * (ratios[train] ratios[val]): split val else: split test for img, lbl in groups[prefix]: # 复制逻辑与上一段一致 pass这样划分后同一植株的连续帧不会同时出现在 train 和 val 里val 的分数才具备真实参考价值。如果你的原始文件名没有清晰的批次标识还有一个笨办法按目录划分把同一目录下的图片视为同一批次直接用目录名做 group key。3.3 从现有标签自动生成 classes.txtclass 文件是数据集最容易出低级事故的环节。它本质上就是一个每行一个类别名的文本文件名字可以任意起但顺序必须和标签里的数字 id 一致。手写这个小文件本身不复杂复杂的是当你有上百类中草药时人工维护顺序几乎必然出错。所以我会写一个小脚本从标签目录里「统计实际出现过的类别 id」再映射到一个维护好的 id→名称表中自动生成 classes.txtfrom pathlib import Path # 手动维护 id 到名称的映射这是唯一需要人工确认的地方 id_to_name { 0: leaf_mint, 1: leaf_basil, 2: flower_chrysanthemum, 3: fruit_wolfberry, } max_id max(id_to_name.keys()) with open(herb_yolo/classes.txt, w) as f: for i in range(max_id 1): f.write(id_to_name.get(i, funknown_class_{i}) \n)这段脚本的逻辑很直白遍历一个固定的 id 范围把每个 id 对应的类别名逐行写入 classes.txt。这里不建议直接从标签 txt 里自动反推类别名因为标签文件里只有数字 id没有名称自动反推最终还是要靠一份映射表。把映射表显式维护在脚本里比对一遍等于在训练前多做一次人工校验。写完 classes.txt 后还应该顺手用一个 counter 统计每个 id 在标签文件里出现的次数和 id_to_name 里的类别数量做对比。如果一个 id 出现在标签里但没出现在映射表里脚本生成时会用 unknown_class_ 兜底但这个警告不能忽略需要回到标注工具里确认是不是类别定义漏了。4. 数据可视化脚本把标注画回图上眼睛就是第一道质检4.1 用 OpenCV 叠加检测框反归一化与颜色区分数据可视化脚本是标题里最容易被人忽略、实际上又最救命的一块。标注文件是纯数字你看不到「框是否画歪了」「类别是否标反了」。把标注画回原图上几秒钟就能发现坐标越界、类名错位、目标压根没框准这些问题。一个最小可用的可视化脚本长下面这样import cv2 from pathlib import Path class_names [ leaf_mint, leaf_basil, flower_chrysanthemum, fruit_wolfberry, ] # 为每个类别分配一个固定颜色方便肉眼区分 color_map { 0: (0, 255, 0), 1: (0, 165, 255), 2: (255, 0, 255), 3: (0, 255, 255), } def draw_yolo_labels(image_path: Path, label_path: Path) - None: img cv2.imread(str(image_path)) if img is None: print(f[错误] 无法读取图片: {image_path}) return h, w img.shape[:2] with open(label_path, r, encodingutf-8) as f: lines [line.strip() for line in f if line.strip()] for line in lines: parts line.split() if len(parts) ! 5: print(f[警告] 标签格式不正确: {label_path}: {line}) continue cls_id int(parts[0]) x_c, y_c, bw, bh map(float, parts[1:]) # 从归一化坐标反算像素坐标 cx, cy x_c * w, y_c * h box_w, box_h bw * w, bh * h x1 int(cx - box_w / 2) y1 int(cy - box_h / 2) x2 int(cx box_w / 2) y2 int(cy box_h / 2) color color_map.get(cls_id, (255, 255, 255)) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) label_text f{class_names[cls_id]} cv2.putText(img, label_text, (x1, max(10, y1 - 4)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) out_path image_path.parent / (image_path.stem _viz.jpg) cv2.imwrite(str(out_path), img) print(f已生成: {out_path})这个脚本的重点在反归一化那段所有坐标乘以真实宽高才能回到像素空间。画框之前还应该顺手做一道边界检查例如检测到x1小于 0 或x2大于 w就说明标签出界了打印警告并继续。把这种检查做在可视化脚本里你就在浪费像素眼力前自动拦截了大量病态标签。实际操作时我会对 train 和 val 目录各随机抽 20 张图做可视化然后把生成结果放到一个visualized/文件夹里过一眼。中草药图像有一个特点叶片通常密集且互相遮挡如果可视化时发现大量框只有整张图的一半大或者一个框里包含了三片叶子说明标注框的粒度已经偏离了检测目标。这类问题看数值统计看不出看图一眼就能发现。4.2 类别分布与目标尺寸统计用图表发现数据病除了画框另一个不得不做的可视化是类别分布统计。中草药数据集天然不平衡薄荷叶可能标了 5000 个框枸杞果实可能只有 200 个框。模型面对这种分布往往会倾向于把所有目标都预测成多数的那个类。与其等训练完再调 loss 权重不如在划分阶段就用脚本把统计图和长宽比散点图画出来import matplotlib.pyplot as plt from collections import Counter from pathlib import Path label_root Path(herb_yolo/labels/train) all_cls [] ratios_list [] for label_file in label_root.glob(*.txt): with open(label_file, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls int(parts[0]) w float(parts[3]) h float(parts[4]) all_cls.append(cls) ratios_list.append((w, h)) counter Counter(all_cls) print(类别分布) for cls_id, cnt in sorted(counter.items()): print(f class {cls_id}: {cnt}) plt.figure(figsize(12, 5)) plt.subplot(1, 2, 1) plt.bar(counter.keys(), counter.values()) plt.xlabel(class id) plt.ylabel(box count) plt.title(class distribution) plt.subplot(1, 2, 2) plt.scatter( [r[0] for r in ratios_list], [r[1] for r in ratios_list], alpha0.15, s5, ) plt.xlabel(normalized width) plt.ylabel(normalized height) plt.title(box size distribution) plt.tight_layout() plt.savefig(dataset_stats.png, dpi150)这里用Counter统计每个类别出现的框总数用散点图展示所有框的归一化宽高。散点图的价值在于能一眼看出有没有「异常巨人框」例如宽高都接近 1.0 的点意味着有人把整张图当成了一个框这类标签基本等于噪声还有大量集中在左上角的超小框说明标注颗粒度太细对中草药这类边角模糊的目标反而会引入大量难样本。我会顺手计算一个平均每张图的框数如果在 1 到 3 之间说明大多数图片是稀疏场景如果平均超过 10说明模型要面对密集遮挡。这个数字直接决定你后续是优先调 NMS 阈值还是优先做强数据增强。可视化脚本不只是给眼睛看的它是给后续所有训练决策提供依据的。4.3 把可视化脚本变成批处理质检工具单张看不够你可能一次要检查几百张。把这套可视化逻辑封装成批处理函数再配合一个「抽查比例」参数就是一个皮实的数据质检工具。常见做法是遍历目录下所有图片每隔 N 张抽一张可视化同时把错误行收集起来写进一个error_report.txt。这样你不需要打开图片文件夹也能在十分钟内拿到全数据集的健康报告。import random from pathlib import Path def batch_visualize(img_root: Path, label_root: Path, sample_rate: float 0.1): img_paths list(img_root.glob(*.jpg)) random.seed(0) sample random.sample(img_paths, int(len(img_paths) * sample_rate)) for img in sample: lbl label_root / f{img.stem}.txt if not lbl.exists(): continue draw_yolo_labels(img, lbl)sample_rate设成 0.1 就是抽 10%数据量大的时候够用数据量小的时候建议直接设 1.0。注意这里随机抽样会带来一定的偶然性所以我会在第一次使用新数据时把 sample_rate 调高确认没有系统性问题后再降到 0.1 做常态化巡检。5. 避坑排查中草药 YOLO 数据集最容易翻车的五个细节5.1 训练 loss 不降mAP 趋近于 0现象是模型训练了很久loss 曲线像条死鱼。最后发现数据集本身没问题但类别文件顺序和标注里的 id 对不上。原因是标注工具里定义类别的顺序和 classes.txt 里的顺序不一样导致 id0 的真实类别是薄荷叶模型却按 id0 映射成了苦菊叶从头到尾都在学错误标签。解决方法是训练前打印 classes.txt 和 val 标签里出现的前 10 个 id逐个比对语义更稳的做法是维护一份 id_to_name 映射表用脚本自动生成 classes.txt而不是手工抄写。5.2 可视化时框全部跑偏甚至超出图片边界现象是脚本画出的框位置黏在图像左上角或者宽高比例明显不对。原因多半是标签文件里写的是像素坐标比如100 150 300 80而不是归一化坐标也可能是类别 id 超出类别总数导致索引越界。解决方法是给可视化脚本加一道校验所有坐标必须在 0 到 1 之间宽高必须大于 0。一旦发现非法值打印出具体文件名和行内容然后回到标注输出环节重新生成标签。坐标问题必须消灭在训练之前因为训练时的损失函数不会因为你喂了像素坐标就报错它只会帮你把模型带偏。5.3 val 集表现很好实际部署却全乱套现象是 val mAP 稳定在 0.8 以上换到真机上识别率直接腰斩。原因多数是划分阶段发生了数据泄漏同一株植物或同一批次的连续帧同时进入了 train 和 val导致评估分数虚高。解决方法是回到第 3.2 节说的方式按采集批次或目录分组划分。中草药图像的采集往往是连拍模式相邻几帧之间的背景、光照几乎不变这种泄漏比随机切分更隐蔽也更致命。我自己在这个问题上吃过一次亏从那以后凡是新数据集先按文件名前缀分组再划分宁可牺牲一点点训练样本也要保证 val 集真的独立。5.4 训练时报找不到匹配的标签文件现象是 YOLO 训练一开始日志里大量提示图片没有对应标签训练集的有效样本数骤减。原因是图片后缀不统一有人用.jpg有人用.JPG还有人用.png标注软件生成的文件却统一用.txt但你在配对时用了后缀反转的逻辑导致标签文件名找错了。解决方法是统一图片后缀并严格按照「图片 stem .txt」的方式构造标签路径。在划分脚本里做一次存在性检查比训练时报错再回头排查高效得多。5.5 类别严重不均衡小类别完全学不出来现象是多数类别识别效果不错但像枸杞果实这类小目标类别recall 几乎为 0。原因是框数量差距悬殊模型把所有预测倾向都贡献给了多数类。解决方法是先从可视化分布图里确认不均衡系数然后对小类别做过采样或针对性数据增强例如对果实局部做随机裁剪缩放不要在划分阶段删少数类样本因为中草药小类别本来就稀有删了等于直接放弃那一类。还有一个玄学但有效的做法在训练配置里调整类别权重让 minority class 的 loss 占比更大避免被淹没在前景主导的梯度里。提示以上这些坑不是一次同时出现的它们往往一前一后冒出来。我的习惯是每个坑解决后把对应检查项写进可视化脚本形成一套固定的数据体检流程下次换新数据集时就不用从零开始踩。6. 用自检脚本收尾训练前确认数据集能读通在把数据交给 YOLO 训练之前我最后还会跑一个全量自检脚本。它的意义不是找出所有语义错误而是快速排查「缺标签、空标签、格式错误、越界坐标」这几类机械性错误确保训练框架能把数据完整吃进去。from pathlib import Path def validate_dataset(img_root: Path, label_root: Path): missing 0 empty 0 invalid 0 total 0 for img in img_root.glob(*.jpg): total 1 lbl label_root / f{img.stem}.txt if not lbl.exists(): missing 1 continue lines lbl.read_text(encodingutf-8).strip().splitlines() if len(lines) 0: empty 1 continue for line in lines: parts line.split() if len(parts) ! 5: invalid 1 continue try: cls_id, x_c, y_c, w, h map(float, parts) except ValueError: invalid 1 continue if not (0 x_c 1 and 0 y_c 1 and 0 w 1 and 0 h 1): invalid 1 print(f总图片数: {total}) print(f缺少标签: {missing}) print(f空标签: {empty}) print(f格式或范围错误: {invalid})这个脚本只有几行但每次换数据集我都会先跑一遍。缺少标签和空标签在很多框架里不会直接报错它们只是被默默跳过你会误以为自己训练了好几千张实际上有效样本少得可怜。格式错误通常来自标注软件导出的 tab 空格混用解析时容易被忽略。跑完自检后我再做一次小规模实验训练只跑几个 epoch不看精度只看能不能顺利加载数据和完成前几步迭代。这样一套流程走下来再烂的数据也能提前暴露八成问题。说句实在话中草药图像检测的难点从来不是模型结构。同样是做目标检测通用场景下你有大量成熟数据集可以参考但中草药这种细粒度、遮挡多、类别分布偏的领域数据集的每一个细节都会被模型无限放大。我自己的习惯是划分脚本、类别文件、可视化脚本这三件套一定要保留成可复用的工程模板而不是每次新项目都临场写。这样后面拿到新一批中草药图片只需要替换类别名和路径十分钟就能产出一份结构健康、可训练的数据集。这个流程我反复用了很多次从设计之初就省掉的排错时间比后期调参省得更多希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站