简介面向YOLO系列目标检测算法的咖啡叶片检测数据集共包含一千张带标签图像已划分好训练集、验证集与测试集适配YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10和YOLO11等主流目标检测框架适合深度学习初学者及农业智能识别项目直接使用。压缩包内共两千个文件含有九百九十九张JPG叶片图像、一千个TXT格式标签文件以及一个用于配置类别与路径的YAML文件整体约24.53MB。标签采用标准YOLO格式每行记录类别索引与归一化后的中心点坐标及宽高也可转换为VOC格式以适配其他检测框架。目前已有五十七人学习下载。数据集可用于咖啡叶片状态识别、病害检测和长势分析等场景下载后即可直接开始模型训练、验证与测试省去手动标注和划分数据集的流程是快速上手YOLO系列算法实践与算法对比的良好基础数据。1. 咖啡叶片检测更像一道“送分题”1000 张图能不能跑出能用的模型农业目标检测里叶片类任务一直是被低估的“新手友好区”。和行人、车辆这种类别多、尺度变化大的场景不一样咖啡叶片检测的目标相对固定——叶子轮廓、病斑区域、枯叶边界背景是种植园土壤或枝干干扰项有限。这套 1000 张带标签的 YOLO 格式数据集恰好踩在“数据量够用、标签已划分、格式直接喂给 ultralytics”这个点上。它对熟练工来说是个能快速验证训练管线的物料包对刚入门 YOLO 系列的人来说则是绕开“自己标数据、自己分 train/val”这两件最耗时间的事。下面我会从目录结构、标签换算、训练配置到翻车细节把它拆开讲清楚。2. 拆开数据集目录结构、文件对应关系和那段“只给文件名”的隐患2.1 拿到压缩包之后先别急着解压训练我先说一个真实经历有一次我拿到一个叶片数据集压缩包里只有一堆 JPG 和一个叫 labels 的文件夹我直接扔给 YOLOv8 去训练结果报了 “AssertionError: train dataset not found”。原因就是数据集根目录里缺少 images 和 labels 的同名组织方式ultralytics 的默认加载逻辑是前缀匹配。所以第一步永远是先解压、再走一遍目录结构。这套咖啡叶片数据集压缩包内图片命名是img_063_368.jpg这种连续编号标签是同名.txt。通常 YOLO 系列的目录组织如下coffee-leaf-dataset/ ├── images/ │ ├── train/ │ │ └── img_063_368.jpg │ ├── val/ │ │ └── img_063_369.jpg │ └── test/ │ └── img_063_367.jpg └── labels/ ├── train/ │ └── img_063_368.txt ├── val/ │ └── img_063_369.txt └── test/ └── img_063_367.txt摘要里已经说明了“数据集已经划分好”也就是说 images 和 labels 下各自都有 train/val/test 三份。这在训练管线里能省掉很多事——你不用自己写train_test_split.py去切分也不用手动数正负样本比例。我一般会用一个 bash 命令快速核对图片和标签是否成对cd coffee-leaf-dataset for split in train val test; do img_count$(ls images/$split/*.jpg 2/dev/null | wc -l) lbl_count$(ls labels/$split/*.txt 2/dev/null | wc -l) echo $split: $img_count images, $lbl_count labels done这段脚本的关键在于遍历三个子集分别统计 JPG 和 TXT 的数量。数值相等只是最基础的体检不相等时先看是哪个 split 出了问题再用comm -3对比文件名差异。这里有个容易被忽略的前提文件名前缀必须完全一致包括下划线、数字位数任何一位对不上ultralytics 都会在训练时报 file not found。2.2 为什么“只给文件名”会让人踩坑以及如何自动核对正经给到用户手里的数据集理应是完整目录。但如果压缩包解压后你只看到img_063_368.jpg这类文件名列表而没有目录树那大概率是 README 或者 metadata 里另有说明。我拆过不少数据集最稳妥的做法是先看有没有 dataset.yaml 或 classes.txt那个文件会写明类别数和类别名。如果标签文件是 YOLO 格式每个 TXT 的第一列就是类别索引。注意索引从 0 开始也就是说如果你的类别清单是[leaf, disease_spot]那么0代表叶片1代表病斑。这里非常容易错位尤其是从别的数据集迁移过来时类别排序一变模型训练就废了。我通常会写一个快速检查脚本把每张图的标签第一列打出来看分布import os from collections import Counter label_root labels/train class_counter Counter() total_boxes 0 for fname in os.listdir(label_root): if not fname.endswith(.txt): continue with open(os.path.join(label_root, fname), r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f[警告] {fname} 行格式异常: {line.strip()}) continue class_id int(parts[0]) class_counter[class_id] 1 total_boxes 1 print(类别分布:, dict(class_counter)) print(总标注框数:, total_boxes)这段脚本有两个检查点一是行内字段数必须是 5class, x_center, y_center, width, height多一个少一个都说明标签损坏二是统计每个类别索引出现的次数如果类别索引出现缺失或者分布极度不均衡比如类别 0 有 9000 个框、类别 1 只有 3 个框那么训练时就要考虑类别权重或数据增强策略否则模型会直接把类别 1 忽略掉。参数说明label_root要按你的实际路径改total_boxes可以顺带估算单张图的平均目标数如果均值小于 0.5说明很多图是背景图这类图在训练里会加剧“背景误检为叶片”的问题后面训练时需要注意负样本比例。3. 读懂归一化标签YOLO 格式与 VOC 格式互转的完整思路3.1 归一化坐标的换算逻辑关系着你会不会做出一个“看起来能用、一细看全偏”的标注咖啡叶片数据集的标签格式是class x_center y_center width height注意这四个坐标全是相对值范围 0 到 1。举个具体的例子一张 640×640 的图片里一个叶片标注框左上角在像素 (160, 128)右下角在 (480, 512)。那么框宽度 480 - 160 320相对宽度 320 / 640 0.5框高度 512 - 128 384相对高度 384 / 640 0.6中心点 x (160 480) / 2 / 640 0.5中心点 y (128 512) / 2 / 640 0.5所以这一行标签就是0 0.5 0.5 0.5 0.6。这个换算逻辑是所有 YOLO 系列算法yolov5 到 yolov11通吃的区别只在于读取代码的框架封装不同但标签文件规范始终一致。很多翻车现场都出在分辨率上图片实际尺寸不是 640而是 1280但你按 640 去还原像素坐标画出来的框全偏到左上角。YOLO 格式的好处是与图像尺寸解耦同一份标签可以在任何分辨率下训练。坏处是调试时不直观普通人看到 0.5 0.5 根本不知道框在哪。所以把 YOLO 转成 VOC 做可视化是判断标注质量最快的路径。3.2 写一个转换脚本YOLO 转 VOC顺便可视化验证下面这段代码把 YOLO 标签转成 VOC 的 Pascal XML并画出标注框。VOC 格式的坐标是绝对像素所以在转换前必须拿到图片的真实宽高这是最容易遗漏的一步import os import cv2 from xml.etree.ElementTree import Element, SubElement, ElementTree def yolo_to_voc(image_path, label_path, xml_out_path, class_names): img cv2.imread(image_path) h, w img.shape[:2] root Element(annotation) folder Element(folder) folder.text os.path.basename(os.path.dirname(image_path)) root.append(folder) filename Element(filename) filename.text os.path.basename(image_path) root.append(filename) size SubElement(root, size) SubElement(size, width).text str(w) SubElement(size, height).text str(h) SubElement(size, depth).text 3 with open(label_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) x_center, y_center, box_w, box_h map(float, parts[1:]) x_min int((x_center - box_w / 2) * w) y_min int((y_center - box_h / 2) * h) x_max int((x_center box_w / 2) * w) y_max int((y_center box_h / 2) * h) obj SubElement(root, object) SubElement(obj, name).text class_names[cls_id] bndbox SubElement(obj, bndbox) SubElement(bndbox, xmin).text str(max(0, x_min)) SubElement(bndbox, ymin).text str(max(0, y_min)) SubElement(bndbox, xmax).text str(min(w, x_max)) SubElement(bndbox, ymax).text str(min(h, y_max)) tree ElementTree(root) tree.write(xml_out_path, encodingutf-8)这段代码的关键参数是x_min (x_center - w_box/2) * w这里有三个坑第一个x_center和box_w是相对图宽的必须以实际读取到的w为准不能用固定的 640 去乘。第二个计算出的值要int()取整而且必须用max(0, ...)和min(w, ...)做边界裁剪因为有些标注框会略微超出图像边界不裁剪的话后续转成 mask 或做裁剪训练时会出现负数坐标。第三个class_names这个列表必须和数据集的类别清单严格对应建议直接读classes.txt而不是手打。参数说明image_path是图片绝对路径label_path是对应 TXTclass_names是类别名列表顺序要和标签索引对齐xml_out_path是输出的 VOC 文件路径。转换完后再用 matplotlib 或 cv2.rectangle 画框如果能正确覆盖叶片轮廓说明标签没问题可以直接进入训练。4. 训练自己的咖啡叶片检测模型从 YOLOv8 到 YOLOv10 的配置全过程4.1 数据配置 yaml 和数据集划分重排ultralytics 框架下训练的第一步不是写模型结构而是写一个数据描述 yaml。这个 yaml 决定了模型读哪里的图片、哪些类别、有几类。对于这套咖啡叶片数据集如果类别是叶片leaf和病斑disease两类数据配置长这样# dataset.yaml path: /path/to/coffee-leaf-dataset train: images/train val: images/val test: images/test nc: 2 names: 0: leaf 1: diseasepath是数据集根目录的绝对路径train/val/test是相对path的目录这里有个隐藏要求images/train路径下只放图片对应标签在labels/train下ultralytics 会自动按images替换为labels去找标签文件。如果你把标注文件和图片混放在同一个目录必须在路径里体现出来否则训练直接报找不到标签。nc是类别总数必须和names列表长度一致这个是 YOLOv8 后引入的强校验不一致直接抛异常。然后我一般会用 ultralytics 自带的验证命令先把数据集的图片和标签匹配检查跑一遍这一步非常快但能拦住大部分低级错误yolo detect val modelyolov8n.pt datadataset.yaml注意这里用的是预训练权重的验证模式不是训练。它会尝试加载数据集遍历每张图片的标签路径并且在控制台打印All 1000 images match labels之类的提示。如果图片和标签对不上它就会列出具体的缺失文件。这个过程等价于一个体检比直接训练再报错要省时间得多。4.2 训练、验证、导出 ONNX 的完整命令训练命令我一般会按下面的模板来yolo detect train \ modelyolov8n.pt \ datadataset.yaml \ imgsz640 \ epochs100 \ batch16 \ lr00.01 \ patience20 \ projectcoffee_leaf_run \ nameexp1 \ device0参数含义如下modelyolov8n.pt是预训练权重n 是 nano 版本参数最少、速度最快适合先跑通流程。imgsz640是输入分辨率对叶片这种中小目标640 是性价比平衡点。lr00.01是初始学习率对微调场景我一般不改除非 loss 震荡明显。patience20是早停参数验证集 mAP 连续 20 轮不涨就自动停止能省训练时间。训练结束后验证命令直接用yolo detect val \ modelruns/coffee_leaf_run/exp1/weights/best.pt \ datadataset.yaml \ imgsz640 \ conf0.25conf0.25是置信度阈值低于这个值的预测框会被丢弃。验证集指标一般看 mAP50 和 mAP50-95前者做快速判断后者衡量框位精度。这里有个细节实际部署时阈值可以提高到 0.4 或 0.5因为验证集上的 0.25 阈值会把很多模糊框也算进去到了真实场景全变成误检。如果你想把模型部署到摄像头或者边端设备导出 ONNX 是常见操作yolo export modelruns/coffee_leaf_run/exp1/weights/best.pt formatonnx imgsz640 opset12导出过程注意opset12是为了兼容性如果你用的推理框架是 OpenVINO 或者 TensorRT 的老版本opset 太高会不兼容。导出的 ONNX 文件可以直接用 Python 的 onnxruntime 加载也可以转成 OpenVINO 的 IR 格式跑 CPU。4.3 换 YOLOv10 训练时的差异点如果你的环境已经装了ultralytics的最新版YOLOv10 的权重文件是yolov10n.pt训练命令几乎不用变。但有几个差异YOLOv10 在推理时默认不输出 NMS 结果它用的是one-to-many训练、one-to-one推理的结构所以conf阈值在验证时更敏感。我实际跑下来YOLOv10 的 mAP50 通常和 YOLOv8 持平或略高但推理速度几乎翻倍因为省掉了 NMS 延时。还有一个注意点YOLOv10 的类别数如果和预训练权重不一致加载时会自动裁掉最后一层并重新初始化这时你会在终端看到一条警告some metrics may be missing。这不算报错但意味着前几个 epoch 的 loss 会偏大是正常现象。不要看到 loss 高就急着调学习率给它 10 个 epoch 稳定期。5. 避坑记录咖啡叶片数据集训练里最容易翻车的五个细节5.1 图像尺寸不一致导致标签偏置现象训练 loss 掉得很快但验证集 mAP50 卡在 0.6 上不去可视化检测框时发现框总是偏大或偏小。原因这套数据集的图片并非统一分辨率。有些图的宽高比是 3:4有些是 16:9。训练时imgsz640会强制 resize而标签坐标是归一化的理论不受影响但如果模型在某个尺度上的特征分布不均匀小图上标注的小目标就会被拉成大目标导致回归分支学习到错误的偏移量。解决训练前做一次统一缩放把图片按最长边缩放到 640短边补齐到 640 再训练。ultralytics 的 dataloader 自带 letterbox 逻辑但默认的 padding 颜色是灰色114, 114, 114对绿色叶片场景干扰不大真正要检查的是原图是否有 EXIF 旋转信息手机或相机拍的图带旋转角度直接读会横竖颠倒标签对不上。建议训练前用 Python 脚本把所有图片重新保存一遍自动应用 EXIF 旋转再同步重写标签坐标。5.2 标签越界画框时直接报负数或者超出图像尺寸现象验证时部分图片的预测框特别奇怪有的框在图像边缘外也有的框坐标直接是负值画出来图形在左上角重叠。原因原始标注是从其他格式转换过来的转换时没有做边界裁剪有些叶片在图片边缘被截断标注框把延伸出去的部分也框进去了坐标因此变成负数或大于 1。解决写脚本对所有标注做边界裁剪。对每个归一化坐标先乘上实际宽高再做clip(0, w)和clip(0, h)然后重新转回归一化。用 3.2 节的转换脚本就能顺带完成这个操作。但要注意裁剪后有可能出现宽或高变为 0 的退化框这类框直接删除如果一个框裁剪后面积剩余不到原来的 30%也建议删掉因为它带来的监督信号是残缺的比起让模型“脑补”一个看不见的半截叶片不如让它不学这个样本。5.3 训练时提示 no labels in dataset数据没被加载进去现象yolo detect train启动后终端打印的Number of images是 0 或者远小于实际数量。原因数据集根目录配置错误。我看到过两种常见操作导致这个问题。一种是在 dataset.yaml 的path里直接写了images/train的完整路径然后又写了train: images/train导致最终路径变成双重拼接另一种是 label 文件用了.txt以外的后缀比如.txt~或带空格的名字ultralytics 不认。解决把 dataset.yaml 里的path指向数据集压缩包解压后的根目录train写相对路径。同时检查 label 文件命名确保格式是img_063_368.txt文件名里不要有多余字符。一个小技巧是直接在终端ls labels/train/ | head -5看看实际文件名是什么再对比代码里的路径。5.4 叶片目标小mAP50 虚高但实际推理框偏大现象验证集 mAP50 有 0.85但部署到另一台机器上的摄像头画面里叶片框总是框住整株树而不是单张叶片。原因mAP50 衡量的是 IoU 是否超过 0.5它对抗偏移不敏感。对咖啡叶片来说很多叶片是紧密相邻的模型可能只学到“绿色区域就是叶片”的粗粒度特征没有学会分开每一个叶片实体。这个问题在小目标检测领域很常见本质是特征分辨率不够。解决把imgsz从 640 提到 832 或者 1024输入分辨率增大后小目标在特征图上有更多像素。同时可以把mAP50-95作为主指标来选模型。如果分辨率和训练时长都受限另一个办法是把标签里过大的叶片框过滤掉——把宽或高超过图像尺寸 60% 的标的去除因为那种目标大概率是“整片叶子簇”而非单叶片模型学了反而容易在推理时把整个绿色区域框进来。5.5 类别不平衡病斑样本极少模型完全不预测这一类现象训练完测试能正确框出所有叶片但病斑一个都预测不出来验证集上这一类的 recall 是 0。原因数据集里病斑样本太少模型在cls损失里被叶片类淹没。这是农业数据集的通病因为叶片容易标病斑需要专家参与样本少且切片形状高度多变。解决先确认标签里两个类别的框数量用 2.2 节的 Counter 脚本。如果比例超过 10:1可以考虑三种方案一是训练时设置cls0.5提高分类损失的权重二是对病斑类做简单复制粘贴的数据增强把病斑区域从原图中裁剪出来粘贴到空白背景图上顺便做尺度缩放和亮度扰动三是干脆把病斑检测拆成二阶段任务先用模型跑叶片检测再用一个独立的分类器判断每张叶片有没有病斑。二阶段方案看起来费事但对单类样本量不足 50 张的数据集效果往往比硬训练一个多分类检测器好得多。6. 把精度再往上顶一截数据增强、迁移学习和置信度后处理模型训练到 mAP50 有 0.85、mAP50-95 有 0.68 之后再往上提就需要动训练策略和推理策略了。我先说数据增强ultralytics 在超参数文件里默认开启了 mosaic、hsv_h、hsv_s、hsv_v 这些增强但对咖啡叶片场景真正有用的是mosaic1.0和mixup0.2。Mosaic 把四张图拼成一张模型被迫学习“叶片不会满屏都是”的空间分布对减少误检很有帮助。Mixup 则混合两张图的像素和标签对叶片这种纹理复杂的目标相当于免费扩充了背景多样性。我调训时一般把hsv_h从默认的 0.015 调到 0.03因为咖啡叶片在不同光照下颜色差异明显色相扰动大一点反而提升泛化但别超过 0.05否则叶片颜色会变成紫色模型学到的就是错误颜色特征。迁移学习方面别小看预训练权重里的浅层特征。叶片边缘、纹理、绿色背景的卷积核在 COCO 上已经学得不错。我有时会固定 backbone 的前 10 层权重只训练 neck 和 head 部分具体做法是在 ultralytics 里没有直接参数控制但可以用freeze10指定冻结前 10 层。这个参数在训练初期收敛很快10 个 epoch 后解冻再全量训练能避免一开始就破坏浅层特征。对 1000 张这种小数据集这种方式比从头训练普遍高 2 到 3 个点的 mAP。置信度后处理是个容易被忽略的点。YOLO 默认给的置信度是“框内存在目标的可能性”但实际部署里真正决定用户体验的是“这个框要不要显示给用户”。我一般会做一次校准把验证集上预测结果的置信度分布拉出来找一个阈值把假正例控制在可接受范围内。比如叶片检测任务50 张图里可以容忍 1 个误检那就把置信度阈值提高到 0.55如果场景里不允许漏检那就降到 0.2。咖啡叶片生产环境我一般调到 0.45视觉上既能覆盖大多数枯叶和病斑又不会把整棵树的轮廓都框出来。最后分享一个习惯。我从那次“标签坐标像素和归一化混淆”翻车之后每拿到一个数据集都会强制走一遍脚本体检先用 Counter 统计类别分布再用 yolo_to_voc 抽 20 张图可视化最后才写 dataset.yaml。这套动作下来训练必报错的概率降了至少一半。这次这套咖啡叶片数据集我希望你拿到手后也先走这一遍确认标签格式和图片尺寸匹配了再训。数据本身的质量决定了模型能走到哪一步——1000 张图跑一个 baseline 验证管线没问题想要真正用在田间识别还得靠你手里的增广策略和调参耐心。希望帮到你。本文还有配套的精品资源点击获取