简介:面向目标检测学习者和算法工程师的YOLO树叶分类数据集资源,基于真实场景图像构建,使用LabelImg标注,提供VOC、COCO、YOLO三种主流格式标签,分别存放于独立文件夹,可直接用于YOLO系列模型训练与评测,适配课程设计、毕业设计及工程落地等多种场景。压缩包共2000个文件,主要由1000个xml标注文件、990个txt标签文件及环境配置与训练教程HTML页面、Python划分脚本、yaml配置文件组成,整体大小约27.93MB,目录规划清晰,便于按需调用。资源附赠Linux与Windows双平台YOLO环境搭建和训练教程,以及训练集/验证集/测试集划分脚本,支持自定义数据划分,能够帮助初学者快速完成从环境准备到模型训练的全流程,同时提供训练列表文件,减少上手成本。已有469人学习下载,适合需要高质量标注数据与完整流程指导的目标检测入门者使用。
1. 1000 张树叶图带三格式标签,这个数据集包到底怎么用
拿到一个名为“YOLO树叶分类目标检测数据集(含1000张图片)+对应voc、coco和yolo三种格式标签+划分脚本+训练教程.rar”的压缩包,第一反应通常是解压后对着三个 labels 文件夹发愣:VOC 是一堆 xml,COCO 是一个巨大的 json,YOLO 是一堆 txt。你真正要训练的是 YOLO,但 YOLO 只认 txt;COCO 和 VOC 格式在这一刻像是冗余。其实这三份格式恰恰对应了三条典型工作流:VOC 格式方便用 LabelImg 复查和二次标注,COCO 格式可以直接喂给 mmdetection 或 Detectron2 做对比实验,YOLO 格式才是你马上要拿来训练的那一份。
这个标题面向的对象很明确:要做树叶检测、又不想从零标注的人。树叶检测不是通用目标检测的简单子集,它有几个独有的麻烦——叶片边缘不规则、同株叶片互相遮挡、不同树种叶片尺寸差异巨大、背景里的树干和杂草都是干扰。1000 张图的规模说大不大,说小也不小,关键看你怎么划分、怎么配参数。这篇不走教科书路线,直接从解压后的目录结构讲起,把三种格式的来龙去脉、划分脚本的设计思路、训练时的具体命令和参数、以及最容易翻车的几个坑一次说清。
2. 先吃透三种标签格式:VOC、COCO、YOLO 的数据组织与互转逻辑
2.1 从目录结构看数据集包的设计意图
正常情况下,解压后的目录应该是这样的:
leaves_dataset/ ├── images/ │ ├── leaf_001.jpg │ ├── leaf_002.jpg │ └── ... ├── annotations/ │ ├── xml/ # VOC 格式,每个图片对应一个 xml │ │ ├── leaf_001.xml │ │ └── ... │ ├── coco.json # COCO 格式,所有标注合在一个 json │ └── yolo/ # YOLO 格式,每个图片对应一个 txt │ ├── leaf_001.txt │ └── ... ├── split/ │ └── split.py # 划分脚本 └── train.ipynb # 训练教程如果你拿到的包结构略有出入,比如 yolo 标签直接放在 labels 目录下,也不要紧,核心逻辑一样:图片是一份,标注是同一份语义的不同序列化方式。理解这一点后,你会发现所谓“格式转换”没有技术含量,真正的技术含量在理解三种格式各自 punish 什么、reward 什么。
2.2 VOC 格式:xml 里的坐标是绝对值,修改最直接
VOC(Visual Object Classes)格式源自 PASCAL VOC 挑战赛,每个 xml 文件描述一张图片里的所有目标。核心字段只有几个:
<annotation> <filename>leaf_001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>leaf_diseased</name> <bndbox> <xmin>320</xmin> <ymin>240</ymin> <xmax>860</xmax> <ymax>720</ymax> </bndbox> </object> </annotation>xmin/xmax/ymin/ymax 都是像素绝对值,必须落在图片尺寸范围内。400 × 400 的图上不可能出现 xmax=500 的标注,这种错误只可能在手工标注或脚本转换时生成。所以拿到 xml 标签后,第一个动作应该是写脚本校验边界:
import xml.etree.ElementTree as ET import os xml_dir = "annotations/xml" img_dir = "images" for xml_name in os.listdir(xml_dir): tree = ET.parse(os.path.join(xml_dir, xml_name)) root = tree.getroot() # 解析图片尺寸 size = root.find("size") w, h = int(size.find("width").text), int(size.find("height").text) # 逐个目标检查边界 for obj in root.findall("object"): bbox = obj.find("bndbox") xmin = int(bbox.find("xmin").text) ymin = int(bbox.find("ymin").text) xmax = int(bbox.find("xmax").text) ymax = int(bbox.find("ymax").text) # 越界即报错,不合法目标直接跳过 if xmin < 0 or ymin < 0 or xmax > w or ymax > h: print(f"{xml_name} 边界错误: {xmin},{ymin},{xmax},{ymax}") if xmax <= xmin or ymax <= ymin: print(f"{xml_name} 宽高为负: {xml_name}")这个脚本的检查逻辑并不是多余动作。很多转换脚本在把 COCO 转回 VOC 时,会把整数坐标和浮点坐标混在一起,四舍五入之后正好产生 xmax=xmin 的退化框。YOLO 训练对这种标签完全不报错,但损失函数会一直居高不下。
2.3 COCO 格式:json 里的 id 链是新手最容易被绕晕的地方
COCO 格式把整份数据集的标注塞进一个 json,顶层结构是 info、images、annotations、categories 四个数组。关键在 id 关联关系:
{ "images": [ {"id": 1, "file_name": "leaf_001.jpg", "width": 1920, "height": 1080} ], "annotations": [ { "id": 1, "image_id": 1, "category_id": 1, "bbox": [320, 240, 540, 480], "area": 259200, "iscrowd": 0 } ], "categories": [ {"id": 1, "name": "leaf_diseased"} ] }注意这里 bbox 不再是 xmin/ymin/xmax/ymax,而是[x, y, width, height],即左上角坐标加宽高。COCO 的 categories id 从 1 开始,而 YOLO 的类别 id 从 0 开始,转换时务必减 1。area 字段在树叶数据集里影响不大,但在某些检测框架里会参与 loss 加权,转换脚本生成 area 时最好重新计算宽高乘积,而不是继承原值。
2.4 YOLO 格式:归一化坐标才是训练真正要喂的东西
YOLO 标签的每个 txt 文件里,每行代表一个目标:
0 0.4625 0.4028 0.2813 0.4444五个数字依次是:类别 id(从 0 开始)、x_center / width 的归一化值、y_center / height 的归一化值、bbox 宽度 / 图片宽度、bbox 高度 / 图片高度。全部是 0 到 1 之间的浮点数。归一化意味着标签不依赖图片分辨率:1920 宽的图和 640 宽的图用同一套 txt 都能训练,coco 数据集下载后转成 YOLO 格式也是这个逻辑。
从 VOC 转 YOLO 的核心代码就几行:
def voc_to_yolo(xml_path, out_path, class_map): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text if cls_name not in class_map: # 未注册的类直接跳过 continue cls_id = class_map[cls_name] bbox = obj.find("bndbox") xmin = int(bbox.find("xmin").text) ymin = int(bbox.find("ymin").text) xmax = int(bbox.find("xmax").text) ymax = int(bbox.find("ymax").text) # 转成 YOLO 所需的中心点 + 宽高 归一化 x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines))class_map 是{"leaf_diseased": 0, "leaf_healthy": 1}这种字典。这里有个容易踩的坑:如果 class_map 里漏了某个类,脚本会静默跳过,最后标签总行数比目标数少,但不会报错。所以转换完一定要数一下行数。
2.5 三格式对比表与格式选型建议
| 维度 | VOC (xml) | COCO (json) | YOLO (txt) |
|---|---|---|---|
| 坐标类型 | 绝对值 int | 绝对值 float | 归一化 float |
| 每张图存储 | 独立文件 | 合并在一个 json | 独立文件 |
| 类别 id 起点 | 字符串名 | 1 | 0 |
| 适合工具 | LabelImg 复查 | mmdetection、Detectron2 | YOLO 系列直接训练 |
| 缺陷定位难度 | 低,逐文件可查 | 高,json 结构错误全局崩 | 中,坐标错误不报错 |
实际做项目时,我的习惯是:以 VOC 为中间格式。LabelImg 标注原生出 VOC,脚本转 YOLO 训练,需要对比实验时再转 COCO。这个包里三份都给齐了,省掉了自己写转换的时间,但你不能直接拿来就用——先跑一遍校验脚本,确认三份格式的语义一致,再谈训练。
提示:如果你从 cvat yolo 导出或用 kitti 标注转 yolo,大概率会拿到非归一化的 txt。判别方法很简单:如果某个坐标值大于 1,一定不是标准 YOLO 格式,不要硬着头皮训练。
3. 划分脚本的核心逻辑:不只看比例,还要看叶片的分布均衡
3.1 一副 1000 张图的数据集,划分脚本要考虑什么
划分脚本在整个包里是最不起眼、但直接影响模型泛化能力的文件。train/val/test 的比例通常按 8:1:1 或 7:2:1 来切,比如 1000 张图切成 train 800、val 100、test 100。单纯按比例随机划分,在树叶检测场景下会出问题:同一个树种的叶片纹理高度相似,如果某一种类恰好被集中在某个时间段拍摄(比如上午拍的全部是健康叶,下午拍的全是病叶),随机划分会把“时间段”这个无关变量学进去。
所以划分脚本的价值不止于把文件挪到三个文件夹,至少要做三件事:按文件夹或文件名前缀分源、保证每个类别的图片数量在所有划分中占比一致、记录划分结果到文本文件以便复现。第三点看似多余,实际排查时极其重要。
3.2 能直接用的划分脚本
import os import random import shutil from collections import defaultdict random.seed(42) img_dir = "images" yolo_label_dir = "annotations/yolo" train_dir = "dataset/train" val_dir = "dataset/val" test_dir = "dataset/test" # 结构化数据集,YOLO 训练时直接喂 train 和 val 目录 for d in [train_dir, val_dir, test_dir]: os.makedirs(os.path.join(d, "images"), exist_ok=True) os.makedirs(os.path.join(d, "labels"), exist_ok=True) # 按类别统计图片,保证分层采样 class_to_images = defaultdict(list) for img_name in os.listdir(img_dir): stem = os.path.splitext(img_name)[0] label_path = os.path.join(yolo_label_dir, stem + ".txt") if not os.path.exists(label_path): continue # 读取该图所有类别 with open(label_path) as f: classes_in_img = set() for line in f: cls_id = line.split()[0] classes_in_img.add(cls_id) # 每张图归属第一个类别;多类图片会重复计数 for cls_id in classes_in_img: class_to_images[cls_id].append(img_name) train_ratio, val_ratio = 0.8, 0.1 train_imgs, val_imgs, test_imgs = [], [], [] for cls_id, img_list in class_to_images.items(): # 去重:一张多标签图可能出现在多个类里 img_list = list(set(img_list)) random.shuffle(img_list) n_train = int(len(img_list) * train_ratio) n_val = int(len(img_list) * val_ratio) train_imgs += img_list[:n_train] val_imgs += img_list[n_train:n_train + n_val] test_imgs += img_list[n_train + n_val:] # 复制图片和标签 def copy_files(img_list, target_dir): for img_name in img_list: stem = os.path.splitext(img_name)[0] shutil.copy(os.path.join(img_dir, img_name), os.path.join(target_dir, "images", img_name)) label_src = os.path.join(yolo_label_dir, stem + ".txt") label_dst = os.path.join(target_dir, "labels", stem + ".txt") if os.path.exists(label_src): shutil.copy(label_src, label_dst) copy_files(train_imgs, train_dir) copy_files(val_imgs, val_dir) copy_files(test_imgs, test_dir) # 保存划分记录,方便事后复现 with open("split_record.txt", "w") as f: f.write("train:\n" + "\n".join(train_imgs) + "\n\n") f.write("val:\n" + "\n".join(val_imgs) + "\n\n") f.write("test:\n" + "\n".join(test_imgs) + "\n")这段脚本的要点在于class_to_images的构建方式:不是按文件名前缀猜类别,而是直接读 YOLO 标签文件,从每行第一个数字拿类别 id。这样不管数据集包里图片怎么命名,都能正确分层。随机种子固定为 42 是为了可复现——你换台机器跑,得到一模一样的划分。
3.3 划分完必须做的数量自检
训练前跑一个统计,确认 train/val/test 三个集合中各类别占比接近。如果 val 里某些类只有一两个目标,这个模型的 mAP 波动会非常大,val 曲线看起来像在做布朗运动。
for split in train val test; do echo "=== $split ===" cat dataset/$split/labels/*.txt | awk '{print $1}' | sort | uniq -c done如果发现某个类在 test 里完全没有出现,说明分层采样代码有 bug,或者这个类本身图片太少。1000 张的数据集里,一个类只有 30 张图已经算尾部类别,val 里分到 3 张,好坏全靠运气。这个坑在训练时看不出来,直到你看 per-class mAP 才会意识到——所以划分阶段就要留意。
4. 训练教程的骨架:模型选型、配置文件、命令行参数与损失函数读数
4.1 用哪一代 YOLO 跑树叶检测:v5、v8 还是 v11
“yolo 第几代了”是搜训练教程时最常见的问题之一。截至现在,Ultralytics 主线已经是 YOLOv11,但 yolov5n/yolov5s 的权重文件小、部署生态成熟,在 RK3588 这类边缘设备上依然高频出现。针对树叶检测这种单类目标、1000 张图的小规模任务,我的建议是:
- 首次跑通用 YOLOv8n。nano 模型参数最少,CPU 也能硬跑,迭代一轮验证环境没问题再换大模型。
- 追求精度用 YOLOv8s 或 YOLOv11s。树叶检测不是极简任务,叶片边缘和纹理需要足够的特征表达能力。
- 提前确认部署目标。如果后续要在 AMD 显卡或嵌入式设备上跑,环境配置的坑先踩,模型选型反而次要。AMD 显卡用 DirectML 后端跑 YOLO 时,某些算子的支持度和 CUDA 不一样,训练前先跑一次前向验证。
4.2 数据集配置与训练命令
YOLOv8 以后的数据集配置是一个 yaml 文件:
path: dataset # 数据集根目录 train: train/images # 训练图片相对路径 val: val/images # 验证图片相对路径 test: test/images # 测试图片相对路径 nc: 2 # 类别数量,改成你自己的 names: ['leaf_diseased', 'leaf_healthy'] # 类别名,与标签 id 对应训练命令:
yolo detect train \ model=yolov8n.pt \ data=leaves.yaml \ epochs=100 \ batch=16 \ imgsz=640 \ patience=20 \ save_dir=runs/train/leaves_v8n \ name=leaves_001参数说明:
epochs=100:1000 张图的小数据集,100 轮足够收敛。如果你的显卡显存小,可以降到 60 轮,配合早停。batch=16:imgsz=640 时,16 张图大约需要 10GB 显存,8GB 的卡建议降到 8。patience=20:连续 20 轮 val loss 没有提升就停止,省时间的关键参数。imgsz=640:树叶检测默认用 640。但如果你的图片里叶片普遍偏小,可以试imgsz=1280,小目标召回率会明显上升,代价是显存翻两倍以上。
4.3 从训练日志看损失函数:什么时候该停,什么时候该调
训练日志里最值得盯的四条曲线是train/box_loss、train/cls_loss、val/box_loss、val/cls_loss。小数据集最常见的异常是 val loss 在训练初期就一路走高,但 train loss 正常下降,这是过拟合的典型信号。1000 张图、单类目标,理论上 20 到 30 轮就够;超过 50 轮还在过拟合,先加数据增强而不是加模型容量。
如果你在 Ultralytics 的新版本里启动训练,注意到日志里出现了amp_checks或自动混合精度的相关输出,那是新版默认打开了 AMP。在部分显卡上 AMP 会导致 val loss 抖动,遇到这种情况用amp=False关掉再试一轮,但训练时间会变长。
4.4 训练完成后验证模型的固定动作
训练结束不要急着看results.png,跑一段独立验证:
yolo detect val \ model=runs/train/leaves_v8n/weights/best.pt \ data=leaves.yaml \ split=test \ conf=0.25 \ iou=0.5split=test是关键。很多人默认验证集就是 val,但 val 在训练时被用来早停过,指标偏高。用 test 集跑出来的 map50 才是你可以写进汇报里的数字。树叶检测项目的验收标准通常要求 mAP50 在 0.85 以上,低于 0.7 基本不可用。
5. 训练过程中的疑难杂症与提速技巧
5.1 症状到对策:三个最常见的故障排查
| 症状 | 可能原因 | 处理办法 |
|---|---|---|
Box loss始终在 1.2 以上不降 | YOLO 标签类别 id 与 yaml 不匹配,或坐标归一化错误 | 查看 txt 内容,确认第一个数字在 0~nc-1 范围内 |
训练时报FileNotFoundError | 划分脚本只复制了有标签的图片,但 yaml 里 train 目录仍然索引了全部图片 | 确认 train/images 里的文件名与 train/labels 一一对应 |
| val 集 mAP 忽高忽低 | val 集太小,划分时没有做分层采样 | 检查 val 数据集每个类别的目标数量,不足 30 个目标就重新划分 |
第二个问题的排查方法:一条 find 命令找出来。
for img in dataset/train/images/*.jpg; do label="dataset/train/labels/$(basename ${img%.jpg}.txt)" if [ ! -f "$label" ]; then echo "缺少标签: $img"; fi done5.2 模型加速技巧:预热、冻结与剪枝的取舍
1000 张图的数据集,显存充足时最快的提速手段是冻结主干。前 10 轮用freeze=10只训练检测头,让 loss 先降到一个合理区间,再解冻所有层。原因很简单:树叶数据集的背景和 ImageNet 预训练特征相似,主干不需要大动,直接微调反而容易在小数据集上把预训练特征改坏。
yolo detect train \ model=yolov8s.pt \ data=leaves.yaml \ epochs=60 \ batch=16 \ imgsz=640 \ freeze=10 \ warmup_epochs=3warmup_epochs=3让学习率在前 3 轮从接近 0 慢慢爬升,配合冻结主干,训练稳定性会好很多。这里的代价是总训练时间变长,但对小数据集来说,这样的换时提精是值得的。
5.3 迁移学习的一个正确用法:先在大图上预训练,再回 640 微调
如果叶片尺寸偏小,可以先用 1280 的输入训练 30 轮,然后加载这个权重在 640 下微调 20 轮。这种做法利用了 YOLO 的多尺度训练特性,模型先学会在细节丰富的分辨率下定位叶片,再在低分辨率下统一推理速度。脚本层面只需要把 imgsz 改掉,其余参数保持一致。用迁移学习最忌讳的是把 target 域的标签当成 pre-train 的负样本,所以这里必须加载的是树叶模型自己的last.pt,而不是新的yolov8n.pt。
最后的验证环节,打开runs/detect/val里的val_batch0_pred.jpg,用眼睛看第二行检测框的边缘贴合度。树叶数据集中,边框稍微偏大通常不影响 mAP 计算,但部署到实际场景时会框到背景里的树干,召回率达标但精确率受影响。发现这种情况,推理阶段把conf从 0.25 提到 0.4,比重新训练快得多。
本文还有配套的精品资源,点击获取