☰
YOLO树叶检测实战:1000张图、三种标签格式与训练避坑指南
2026/10/5 4:03:41 网站建设 项目流程

简介:这份资源面向从事目标检测学习与课程实践的学生、教师及算法入门者,提供一套真实场景下的树叶分类目标检测数据集,可直接用于YOLO系列模型的训练与验证。压缩包共约2000个文件,以1000个xml标注文件、990个txt标签文件为主,另含少量html教程页面、py脚本与yaml配置文件,整体约27.93MB,体积轻便便于快速下载与本地部署。数据经labelimg标注,标注框质量较高,同时提供voc、coco和yolo三种格式标签,分别存放于不同文件夹,省去格式转换环节。资源还附赠数据集划分脚本,可按需生成训练集、验证集与测试集,并配套Windows与Linux环境搭建、训练案例教程,帮助读者从环境配置到模型训练完整跑通流程。目前已有471人学习下载,适合作为课程设计、毕业设计或目标检测入门练手的数据基础。

1. 树叶分类检测数据集:1000 张图、三种标签格式和一套划分脚本到底怎么用

手上接到一个树叶识别的小项目,客户扔过来一个压缩包,名字长得像绕口令:YOLO树叶分类目标检测数据集(含1000张图片)+对应voc、coco和yolo三种格式标签+划分脚本+训练教程.rar。这种资源在目标检测圈子里很常见,但真正让人头疼的不是模型选哪个,而是拿到手之后发现标签格式对不上、划分脚本跑不通、训练时类别数写错。树叶分类这个场景本身不算复杂,难点在于叶片重叠、背景杂乱、同类叶片形态差异大,1000 张图的量级刚好卡在“够用但不够富裕”的区间。这篇文章面向的是手里已经拿到类似数据集、准备用 YOLO 跑通训练和推理的从业者,从目录结构、三种标签格式的差异、划分脚本的写法,到 YOLOv8/v11 训练参数和避坑,一步步拆开讲。新手能照着命令跑,熟手能直接看参数边界和踩坑记录。

2. 树叶数据集目录结构与三种标签格式的取舍

2.1 拿到压缩包先别急着解压训练,先看清目录

很多人拿到 .rar 之后直接解压,看到 images 和 labels 就开始写 data.yaml,结果训练时发现标签全是 VOC 的 XML,YOLO 根本不认。常见做法是先列一遍目录树,确认图片数量、标签格式、是否有划分文件。一个典型的树叶数据集解压后大概长这样:

# 查看压缩包内容,不解压 unrar l YOLO树叶分类目标检测数据集.rar # 解压后进入目录,列出两级结构 tree -L 2 -d leaf_dataset/

预期输出类似:

leaf_dataset/ ├── images/ # 1000 张 jpg/png ├── annotations/ # VOC 格式 XML ├── labels/ # YOLO 格式 txt ├── coco/ # COCO 格式 json ├── split.py # 划分脚本 └── train_tutorial.md

这里的关键是:images 目录下可能混放了所有图片,而 labels 目录下可能只有部分图片有对应 txt。先统计一下图片和标签的数量是否一致,这是后面排查“训练时找不到标签”的第一步。

# 统计图片数量 find leaf_dataset/images -type f \( -name "*.jpg" -o -name "*.png" \) | wc -l # 统计 YOLO 标签数量 find leaf_dataset/labels -type f -name "*.txt" | wc -l # 统计 VOC 标签数量 find leaf_dataset/annotations -type f -name "*.xml" | wc -l

如果三个数字不一致,说明数据集本身有缺失,需要先补齐或剔除无标签图片。我一般会写个脚本把没有对应标签的图片移出去,避免训练时反复报 warning。

2.2 VOC、COCO、YOLO 三种格式到底差在哪

树叶分类检测里,三种格式的核心差异在于坐标表示和文件组织方式。VOC 用 XML,每个目标一个<object>节点,坐标是左上角和右下角的绝对像素值;COCO 用单个 JSON,所有图片的标注集中管理,坐标是 [x, y, width, height] 绝对像素;YOLO 用每张图一个 txt,每行一个目标,格式是class_id x_center y_center width height,全部归一化到 0-1。

格式文件组织坐标类型类别表示适合场景
VOC每图一个 XML绝对像素 xmin,ymin,xmax,ymax字符串类别名传统检测框架、标注工具导出
COCO单 JSON绝对像素 x,y,w,h数字 category_id多任务、分割、评估
YOLO每图一个 txt归一化中心点+宽高数字 class_idYOLO 系列训练

树叶数据集的类别通常不多,比如“银杏叶、枫叶、梧桐叶”三类,但有些数据集会把“叶片”和“叶柄”分开标,导致类别数比预期多。拿到 COCO JSON 后第一件事是看 categories 字段,确认类别数和顺序,因为 YOLO 训练时 data.yaml 里的 names 顺序必须和 class_id 对应,否则推理结果全是错的。

import json with open('leaf_dataset/coco/annotations.json', 'r') as f: coco = json.load(f) # 打印类别信息 for cat in coco['categories']: print(cat['id'], cat['name']) # 统计每类目标数量 from collections import Counter cat_counter = Counter(ann['category_id'] for ann in coco['annotations']) print(cat_counter)

这段代码的作用是确认类别 id 和名称的映射关系。参数上注意:COCO 的 category_id 不一定从 0 开始,也不一定连续,而 YOLO 的 class_id 必须从 0 开始连续。如果 COCO 里是 1、2、3,转 YOLO 时要减 1。树叶数据集里如果出现“背景”类,通常要删掉,因为 YOLO 不把背景当正类。

2.3 格式转换:VOC 转 YOLO 的脚本与边界处理

如果数据集只给了 VOC 格式,需要自己转 YOLO。转换的核心是读取 XML 里的 size 和 object 坐标,做归一化。下面这个脚本处理了树叶数据集常见的几个边界:图片宽高为 0、坐标越界、类别名不在预设列表里。

import os import xml.etree.ElementTree as ET # 类别列表,顺序决定 class_id CLASSES = ['ginkgo', 'maple', 'phoenix'] # 按实际数据集修改 def voc_to_yolo(xml_path, img_w, img_h, output_path): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in CLASSES: continue # 跳过未定义类别 cls_id = CLASSES.index(name) bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) # 裁剪到图像边界 xmin = max(0, min(xmin, img_w)) xmax = max(0, min(xmax, img_w)) ymin = max(0, min(ymin, img_h)) ymax = max(0, min(ymax, img_h)) # 跳过无效框 if xmax <= xmin or ymax <= ymin: continue # 归一化 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(output_path, 'w') as f: f.write('\n'.join(lines))

逻辑说明:先按 CLASSES 列表过滤类别,不在列表里的目标直接丢弃,避免 class_id 错乱。坐标裁剪是必须的,树叶数据集里经常有标注框超出图片边缘的情况,不裁剪会导致归一化后坐标小于 0 或大于 1,YOLO 训练时虽然不报错,但会引入噪声。参数上,img_w和img_h从 XML 的<size>节点读取,不要硬编码。如果 XML 里没有 size 信息,需要用 PIL 或 OpenCV 读图片获取。

提示:转换完成后随机抽 5 张图用 labelImg 或 CVAT 可视化检查,确认框的位置和类别都对。我见过太多转换后类别偏移一位的情况,训练 loss 降不下去,排查半天才发现是 class_id 从 1 开始了。

3. 划分脚本怎么写:训练集、验证集、测试集的比例与随机种子

3.1 为什么不能直接随机 split,要先按类别分层

树叶数据集的类别分布往往不均衡,比如银杏叶有 600 张,枫叶只有 200 张,梧桐叶 200 张。如果直接random.shuffle然后按 8:1:1 切,验证集里可能一个银杏叶都没有,导致评估指标失真。常见做法是按类别分层抽样,保证每个类别在训练集、验证集、测试集里的比例一致。

import os import random from collections import defaultdict from sklearn.model_selection import train_test_split def split_dataset(label_dir, img_dir, output_dir, train_ratio=0.8, val_ratio=0.1, seed=42): random.seed(seed) # 收集每张图的类别集合 img_classes = {} for txt_file in os.listdir(label_dir): if not txt_file.endswith('.txt'): continue img_name = txt_file.replace('.txt', '') with open(os.path.join(label_dir, txt_file)) as f: classes = set(line.split()[0] for line in f if line.strip()) img_classes[img_name] = list(classes) # 按主类别分组 class_to_imgs = defaultdict(list) for img_name, classes in img_classes.items(): for c in classes: class_to_imgs[c].append(img_name) train_imgs, val_imgs, test_imgs = [], [], [] for cls, imgs in class_to_imgs.items(): imgs = list(set(imgs)) # 去重 train, temp = train_test_split(imgs, test_size=val_ratio + (1 - train_ratio - val_ratio), random_state=seed) val, test = train_test_split(temp, test_size=0.5, random_state=seed) train_imgs.extend(train) val_imgs.extend(val) test_imgs.extend(test) # 去重,避免多类别图片被重复划分 train_imgs = list(set(train_imgs)) val_imgs = list(set(val_imgs) - set(train_imgs)) test_imgs = list(set(test_imgs) - set(train_imgs) - set(val_imgs)) # 写入文件 for split_name, split_imgs in [('train', train_imgs), ('val', val_imgs), ('test', test_imgs)]: with open(os.path.join(output_dir, f'{split_name}.txt'), 'w') as f: for img in split_imgs: f.write(f"{img}\n") print(f"train: {len(train_imgs)}, val: {len(val_imgs)}, test: {len(test_imgs)}")

逻辑说明:先统计每张图包含的类别,按类别分组后分别做 train_test_split,最后合并去重。参数上seed固定为 42 保证可复现,train_ratio和val_ratio按需调整。树叶数据集如果某类样本少于 50 张,建议只做 train/val 两分,测试集从验证集里抽,否则测试集太小没有统计意义。

3.2 划分后的目录组织与 data.yaml 写法

划分脚本输出的是三个 txt 文件,里面是图片文件名(不带扩展名)。YOLO 训练时可以直接用这些 txt 作为索引,也可以把图片和标签复制到 train/val/test 三个子目录。我一般倾向于后者,因为 Ultralytics 的 YOLO 对目录结构有默认约定,复制过去省去改代码的麻烦。

# 按划分文件复制图片和标签 for split in train val test; do mkdir -p leaf_dataset/splits/$split/images mkdir -p leaf_dataset/splits/$split/labels while read img; do cp leaf_dataset/images/$img.jpg leaf_dataset/splits/$split/images/ cp leaf_dataset/labels/$img.txt leaf_dataset/splits/$split/labels/ done < leaf_dataset/splits/$split.txt done

对应的 data.yaml:

path: /absolute/path/to/leaf_dataset/splits train: train/images val: val/images test: test/images names: 0: ginkgo 1: maple 2: phoenix

注意path要写绝对路径,Ultralytics 在不同版本里对相对路径的处理不一致,写绝对路径最稳。names的顺序必须和转换脚本里的 CLASSES 完全一致,否则训练出来的模型会把银杏叶识别成枫叶。

注意:如果数据集里图片是 png 格式,复制时记得改扩展名,或者在 data.yaml 里不写扩展名让 YOLO 自动匹配。我遇到过 jpg 和 png 混放的情况,YOLO 默认只找 jpg,导致一半图片被忽略。

4. YOLOv8/v11 训练树叶检测模型的参数与显存控制

4.1 从预训练权重开始,不要从零训

树叶数据集只有 1000 张图,从零训练几乎不可能收敛到可用精度。常见做法是加载 COCO 预训练的 yolov8n.pt 或 yolo11n.pt,冻结 backbone 前几层,只微调检测头。Ultralytics 的命令行接口很直接:

yolo detect train \ data=leaf_dataset/splits/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ warmup_epochs=3 \ patience=20 \ device=0 \ project=leaf_runs \ name=exp1

参数说明:imgsz=640是 YOLO 的标准输入尺寸,树叶目标通常不大,640 够用;如果叶片在图中占比很小,可以提到 1024,但显存翻倍。batch=16在 8GB 显存上跑 yolov8n 没问题,如果 OOM 就降到 8 或 4。lr0=0.01是初始学习率,微调时如果 loss 震荡厉害,降到 0.001。patience=20表示 20 个 epoch 验证指标不提升就早停,树叶数据集容易过拟合,早停能省时间。

4.2 显存不够时的三个降级方案

树叶数据集虽然只有 1000 张,但如果用 yolov8m 或 yolov8l,显存很容易爆。我一般按这个顺序降级:先降 batch,再降 imgsz,最后换更小的模型。下面这个表格是实测的显存占用参考(RTX 3060 12GB,AMP 开启):

模型imgszbatch显存占用训练速度
yolov8n64016~4.2GB快
yolov8s64016~6.8GB中
yolov8m6408~9.5GB慢
yolov8n10248~7.1GB中

如果显存还是不够,可以开启梯度累积,用batch=4配合accumulate=4模拟 batch=16 的效果,但训练时间会拉长。另外cache=True可以把图片缓存到内存,加快数据加载,但 1000 张图缓存后大概占 2-3GB 内存,内存小的机器慎用。

# 显存不足时的保守配置 yolo detect train \ data=leaf_dataset/splits/data.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=8 \ accumulate=2 \ lr0=0.005 \ cos_lr=True \ device=0

cos_lr=True启用余弦退火学习率,对小数据集更友好,避免后期 loss 震荡。accumulate=2表示每 2 个 batch 更新一次权重,等效 batch=16。

4.3 训练过程中的关键指标怎么看

训练日志里重点看三个指标:box_loss、cls_loss 和 mAP50。树叶检测的 box_loss 通常在前 10 个 epoch 快速下降,如果 20 个 epoch 后还在 0.5 以上,说明标注框质量有问题或者学习率太大。cls_loss 反映分类难度,树叶类别之间如果形态相似(比如不同品种的枫叶),cls_loss 会偏高,这时候可以考虑增加类别间的区分度,比如加入叶脉纹理特征。

# 训练结束后用验证集评估 yolo detect val \ model=leaf_runs/exp1/weights/best.pt \ data=leaf_dataset/splits/data.yaml \ imgsz=640 \ batch=16

验证输出里关注 mAP50-95 和每类的 AP。如果某一类 AP 明显低于其他类,大概率是样本量不足,需要针对性补充该类图片或做数据增强。树叶数据集常用的增强包括随机旋转、颜色抖动、马赛克,Ultralytics 默认开启 mosaic,如果叶片重叠严重,可以关掉 mosaic 改用 mixup。

提示:训练时把plots=True打开,结束后会生成混淆矩阵和 PR 曲线,能直观看到哪两类容易混淆。我见过银杏叶和梧桐叶互相误检的情况,后来发现是标注时把两者标反了。

5. 树叶检测避坑:从标签错位到推理翻车的 5 个血泪记录

5.1 现象:训练 loss 正常下降,但推理结果全是乱框

原因:data.yaml 里的 names 顺序和标签文件里的 class_id 不对应。比如转换脚本里 CLASSES 是 ['ginkgo', 'maple', 'phoenix'],但 data.yaml 写成了 ['maple', 'ginkgo', 'phoenix'],模型学到的“0”是银杏叶,推理时却按枫叶输出。解决:训练前用脚本校验一遍,随机抽 10 张图,把标签里的 class_id 映射回类别名,和图片肉眼比对。

5.2 现象:验证集 mAP 很高,但测试集一塌糊涂

原因:划分脚本没有按类别分层,验证集和测试集分布不一致。树叶数据集里如果某类只出现在验证集,测试集里没有,mAP 虚高。解决:用分层抽样重新划分,确保每个类别在三个集合里的比例接近。另外检查是否有同一片叶子的多张照片被分到了不同集合,这会导致数据泄漏。

5.3 现象:训练到一半突然 OOM,之前几个 epoch 都正常

原因:Ultralytics 默认开启 mosaic 增强,某些 batch 里拼接了 4 张高分辨率图片,显存峰值飙升。解决:设置mosaic=0.5降低 mosaic 概率,或者close_mosaic=10在最后 10 个 epoch 关闭 mosaic。另外检查imgsz是否被自动调整,YOLO 在训练时会按 32 的倍数取整,如果原始图片尺寸不规则,可能被放大。

5.4 现象:推理时检测框重叠严重,同一片叶子出多个框

原因:NMS 的 IoU 阈值设得太高,或者模型对重叠目标过拟合。树叶数据集里叶片重叠是常态,NMS 阈值默认 0.7,如果叶片密集,降到 0.5 能减少重复框。解决:推理时加iou=0.5参数,或者训练时增加重叠样本的标注,让模型学会区分相邻叶片。

yolo detect predict \ model=leaf_runs/exp1/weights/best.pt \ source=test_images/ \ imgsz=640 \ conf=0.25 \ iou=0.5 \ save=True

5.5 现象:模型在训练集上表现完美,换一张新图片就检测不到

原因:过拟合加上数据增强不足。1000 张图对 YOLO 来说偏少,如果类别又多,模型容易记住训练集的背景而不是叶片特征。解决:增加数据增强强度,比如degrees=15随机旋转、hsv_h=0.015色调抖动、flipud=0.5上下翻转。另外可以冻结 backbone 的前 5 层,减少可训练参数。如果还是不行,考虑用半监督方式加入未标注的树叶图片。

6. 用 TensorRT 加速树叶检测:从 640 分辨率到多路视频的实测技巧

训练完模型只是第一步,真正落地时往往要在边缘设备上跑实时检测。树叶分类的场景可能是果园巡检、无人机航拍或者固定摄像头监控,对帧率有要求。我一般会把 PyTorch 权重导出成 TensorRT engine,在 T4 或 Jetson 上跑。导出命令:

yolo export \ model=leaf_runs/exp1/weights/best.pt \ format=engine \ imgsz=640 \ half=True \ device=0 \ workspace=4

half=True启用 FP16 精度,T4 上 640 分辨率的 yolov8n engine 单帧推理大概 2-3ms,理论上能跑 300+ FPS。但实际多路视频时,瓶颈往往在解码和预处理,不在推理。我实测过 T4 上 1080p25 帧的视频,用 TensorRT 跑 640 分辨率检测,单路大概占用 15% GPU,理论上能支持 6-8 路,但加上解码和跟踪,实际稳定在 4-5 路。如果要做多路,建议用 DeepStream 或者自己写批处理,把多路帧拼成一个 batch 送进 engine。

import tensorrt as trt import pycuda.driver as cuda import numpy as np # 加载 engine 并创建执行上下文 logger = trt.Logger(trt.Logger.WARNING) with open('best.engine', 'rb') as f, trt.Runtime(logger) as runtime: engine = runtime.deserialize_cuda_engine(f.read()) context = engine.create_execution_context() # 分配输入输出显存 input_shape = (1, 3, 640, 640) output_shape = (1, 6, 8400) # 4 坐标 + 2 类别,按实际调整 d_input = cuda.mem_alloc(np.prod(input_shape) * 4) d_output = cuda.mem_alloc(np.prod(output_shape) * 4) # 预处理:resize + 归一化 + NCHW def preprocess(img): img = cv2.resize(img, (640, 640)) img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, HWC to CHW img = np.ascontiguousarray(img, dtype=np.float32) / 255.0 return img[np.newaxis, ...] # 推理 def infer(img): inp = preprocess(img) cuda.memcpy_htod(d_input, inp) context.execute_v2([int(d_input), int(d_output)]) out = np.empty(output_shape, dtype=np.float32) cuda.memcpy_dtoh(out, d_output) return out

这段代码的关键是预处理要和训练时完全一致,包括颜色通道顺序、归一化方式、resize 的插值算法。我踩过的坑是训练时用了 letterbox 保持长宽比,推理时直接 resize 导致框偏移。解决方法是把 letterbox 的缩放比例和 padding 记录下来,后处理时还原到原图坐标。

注意:TensorRT engine 和硬件绑定,T4 上导出的 engine 不能直接拿到 Jetson 上用,需要在目标设备上重新导出。另外 TensorRT 版本和 CUDA 版本要匹配,版本不对会报 deserialize 失败。

最后一个技巧:如果树叶检测的类别不多(比如 3-5 类),可以把分类头砍掉,只保留一个二分类的“叶片/非叶片”检测,然后用一个轻量级分类网络做细分类。这样检测模型更小,推理更快,分类网络可以单独优化。我做过一个类似方案,yolov8n 检测 + mobilenetv3 分类,T4 上 640 分辨率能跑到 500+ FPS,精度比端到端多分类只低 1-2 个点。这个思路适合对帧率要求高、类别又多的场景。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询