农作物多类别目标检测数据集:从解压到YOLO训练全流程避坑指南

发布时间:2026/10/8 13:44:47
农作物多类别目标检测数据集:从解压到YOLO训练全流程避坑指南
简介这份农作物多类别目标检测数据集面向农业AI开发者、农机视觉算法工程师及农业院校科研人员用于解决农田场景下作物种类识别与定位的模型训练需求。数据集覆盖香蕉、豆类、茄子、辣椒、黄瓜、大蒜、生姜、玉米、洋葱、豌豆、菠萝、马铃薯、水稻、高粱、番茄、小麦等16类主要经济作物涵盖谷物、蔬菜与经济作物三大类别并特别包含豆类与豌豆、不同茄科作物等易混淆样本便于训练高精度区分模型。资源包共2000个文件以957张jpg图像与1041个txt标注文件为主另含1个yaml数据配置和1份docx说明文档压缩包约73.86MB标注严格遵循YOLO格式可直接用于YOLOv5/v7/v8等主流框架的迁移学习与微调。目前已有107人学习下载适合快速搭建农田巡检、智能除草与自动化收割等视觉感知模块。1. 农作物多类别目标检测数据集从拿到压缩包到跑通训练中间隔着多少坑你从某个渠道拿到一个叫「农作物多类别目标检测数据集.zip」的压缩包解压之后大概率会看到一堆图片配一堆标注文件类别可能是玉米、小麦、水稻、大豆、杂草这类田间常见目标。问题在于这个压缩包不会告诉你标注格式是 VOC 的 XML 还是 YOLO 的 txt不会告诉你类别 id 从 0 还是 1 开始更不会告诉你图片里有多少张是重复的、多少张是坏图。目标检测这个方向数据集的质量直接决定模型上限而农作物场景又有它自己的特殊性叶片遮挡、光照剧烈变化、同类作物不同生长期的形态差异极大。这篇笔记就是围绕这个压缩包把从解压到跑通 YOLO 训练的完整链路拆开讲适合刚拿到数据集准备做农作物检测的新手也适合想看看别人怎么处理农业数据集的熟手。2. 先搞清楚压缩包里装的是什么农作物检测数据集的格式与类别体系2.1 三种常见标注格式的识别方法拿到一个目标检测数据集第一件事不是急着写训练脚本而是搞清楚标注格式。农作物多类别数据集常见的标注格式有三种Pascal VOC 的 XML、YOLO 的 txt、以及 COCO 的 json。识别方法很简单看标注文件的后缀和目录结构。如果解压后看到Annotations文件夹里全是.xml文件每个 XML 里包含object标签里面有name和bndbox那就是 VOC 格式。如果看到labels文件夹里全是.txt每行是class_id x_center y_center width height这种归一化坐标那就是 YOLO 格式。如果看到一个大的.json文件里面有images、annotations、categories三个顶层字段那就是 COCO 格式。用下面这段脚本可以快速扫描整个数据集统计标注格式和类别分布import os import xml.etree.ElementTree as ET from collections import Counter def scan_dataset(root_dir): 扫描数据集根目录识别标注格式并统计类别分布 stats { xml_count: 0, txt_count: 0, json_count: 0, image_count: 0, class_counter: Counter(), empty_annotations: 0 } image_exts {.jpg, .jpeg, .png, .bmp} for dirpath, dirnames, filenames in os.walk(root_dir): for f in filenames: ext os.path.splitext(f)[1].lower() if ext in image_exts: stats[image_count] 1 elif ext .xml: stats[xml_count] 1 # 解析 XML 提取类别名 tree ET.parse(os.path.join(dirpath, f)) root tree.getroot() objects root.findall(object) if not objects: stats[empty_annotations] 1 for obj in objects: name obj.find(name).text stats[class_counter][name] 1 elif ext .txt: stats[txt_count] 1 elif ext .json: stats[json_count] 1 return stats if __name__ __main__: result scan_dataset(./农作物数据集) print(f图片总数: {result[image_count]}) print(fXML标注: {result[xml_count]}, TXT标注: {result[txt_count]}, JSON标注: {result[json_count]}) print(f空标注文件数: {result[empty_annotations]}) print(类别分布:) for cls_name, count in result[class_counter].most_common(): print(f {cls_name}: {count})这段脚本的逻辑是遍历整个目录树按扩展名分类统计。class_counter用Counter对象记录每个类别出现的次数most_common()按频次降序输出。empty_annotations统计的是没有object标签的 XML 文件这类文件对应的图片通常是纯背景图训练时可以选择保留或剔除。参数方面root_dir指向解压后的数据集根目录脚本会自动递归所有子目录。2.2 类别不平衡与长尾分布的处理策略农作物数据集有一个很现实的问题类别分布往往极度不平衡。比如玉米图片可能有 2000 张而某种杂草只有 80 张。这种长尾分布直接训练会导致模型对少数类几乎无响应。常见做法有三种过采样少数类、欠采样多数类、以及在损失函数层面做加权。我一般会先统计每个类别的实例数然后按下面的规则决定策略如果最多类和最少类的比例超过 10:1就考虑过采样少数类如果超过 50:1单纯过采样会导致过拟合需要配合数据增强。YOLO 训练时可以通过cls损失权重来调整但更直接的办法是在数据集配置文件里复制少数类图片并做增强。import shutil import random from pathlib import Path def oversample_minority(img_dir, label_dir, target_class_id, target_count, output_img_dir, output_label_dir): 对指定类别进行过采样复制图片和标注到新目录 img_dir Path(img_dir) label_dir Path(label_dir) output_img_dir Path(output_img_dir) output_label_dir Path(output_label_dir) output_img_dir.mkdir(parentsTrue, exist_okTrue) output_label_dir.mkdir(parentsTrue, exist_okTrue) # 找出包含目标类别的所有样本 matched [] for label_file in label_dir.glob(*.txt): with open(label_file, r) as f: lines f.readlines() for line in lines: parts line.strip().split() if parts and int(parts[0]) target_class_id: matched.append(label_file.stem) break print(f包含类别 {target_class_id} 的样本数: {len(matched)}) # 先全部复制一遍 for stem in matched: for ext in [.jpg, .png, .jpeg]: src_img img_dir / f{stem}{ext} if src_img.exists(): shutil.copy(src_img, output_img_dir / f{stem}{ext}) break shutil.copy(label_dir / f{stem}.txt, output_label_dir / f{stem}.txt) # 再随机复制直到达到目标数量 current len(matched) while current target_count: stem random.choice(matched) new_stem f{stem}_aug_{current} for ext in [.jpg, .png, .jpeg]: src_img img_dir / f{stem}{ext} if src_img.exists(): shutil.copy(src_img, output_img_dir / f{new_stem}{ext}) break shutil.copy(label_dir / f{stem}.txt, output_label_dir / f{new_stem}.txt) current 1 print(f过采样完成目标类别样本数从 {len(matched)} 增加到 {current}) # 使用示例将类别 3 过采样到 500 张 oversample_minority( ./dataset/images, ./dataset/labels, target_class_id3, target_count500, output_img_dir./dataset_balanced/images, output_label_dir./dataset_balanced/labels )这段代码的核心逻辑是先找出所有包含目标类别的标注文件然后复制到新目录再随机重复复制直到达到目标数量。target_class_id是你要过采样的类别 idtarget_count是期望达到的样本数。注意过采样后的图片文件名加了_aug_后缀避免冲突但实际训练时这些图片内容是完全一样的所以最好配合在线数据增强如 YOLO 自带的 mosaic、mixup来增加多样性。提示过采样比例不要超过原始数量的 5 倍否则模型会严重过拟合到少数类的特定样本上。3. 把农作物数据集转成 YOLO 格式转换脚本与四个边界坑3.1 VOC 转 YOLO 的完整脚本如果你拿到的农作物数据集是 VOC 格式需要转成 YOLO 才能用 ultralytics 训练。转换的核心是把 XML 里的绝对坐标xmin, ymin, xmax, ymax转成归一化的中心点坐标和宽高。公式是x_center (xmin xmax) / 2 / img_widthy_center (ymin ymax) / 2 / img_heightwidth (xmax - xmin) / img_widthheight (ymax - ymin) / img_heightimport os import xml.etree.ElementTree as ET from PIL import Image from pathlib import Path def voc_to_yolo(xml_dir, img_dir, output_dir, class_list): 将 VOC 格式标注转换为 YOLO 格式 class_list: 类别名称列表索引即类别 id xml_dir Path(xml_dir) img_dir Path(img_dir) output_dir Path(output_dir) output_dir.mkdir(parentsTrue, exist_okTrue) class_to_id {name: idx for idx, name in enumerate(class_list)} converted 0 skipped 0 for xml_file in xml_dir.glob(*.xml): tree ET.parse(xml_file) root tree.getroot() # 获取图片尺寸 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) # 如果 XML 里没有尺寸信息从图片读取 if img_w 0 or img_h 0: img_path img_dir / f{xml_file.stem}.jpg if not img_path.exists(): img_path img_dir / f{xml_file.stem}.png with Image.open(img_path) as im: img_w, img_h im.size lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_to_id: skipped 1 continue 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)) ymin max(0, min(ymin, img_h)) xmax max(0, min(xmax, img_w)) ymax max(0, min(ymax, img_h)) # 跳过无效框 if xmax xmin or ymax ymin: skipped 1 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 cls_id class_to_id[name] lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if lines: with open(output_dir / f{xml_file.stem}.txt, w) as f: f.write(\n.join(lines)) converted 1 print(f转换完成: {converted} 个文件, 跳过 {skipped} 个无效标注) # 使用示例 class_list [玉米, 小麦, 水稻, 大豆, 杂草] voc_to_yolo(./Annotations, ./JPEGImages, ./labels, class_list)这段脚本的关键点在于边界裁剪和无效框过滤。农作物图片里经常出现标注框超出图片边界的情况如果不裁剪归一化后的坐标会大于 1YOLO 训练时会直接报错。class_to_id字典把类别名映射到从 0 开始的整数 id这个顺序必须和后续data.yaml里的names列表完全一致否则模型学到的类别会全部错位。3.2 四个容易翻车的边界问题第一个坑是类别名大小写不一致。同一个数据集里可能同时出现Corn和corn如果不做统一处理会被当成两个类别。解决办法是在构建class_list之前先做一次全量扫描把所有类别名转成小写并去重。第二个坑是图片和标注文件名不匹配。有些数据集的图片是IMG_001.jpg标注是IMG_001.xml这没问题但有些数据集图片是001.jpg标注是img_001.xml直接按 stem 匹配就会丢数据。处理办法是写一个匹配函数用模糊匹配或者手动建立映射表。第三个坑是标注文件里的类别名有空格或特殊字符。比如玉米 (corn)这种带括号和空格的名称在 YOLO 的data.yaml里会引发解析错误。建议在转换阶段就把类别名规范化为纯中文或纯英文不要混用。第四个坑是图片格式不统一。数据集里可能混着.jpg、.png、.bmpYOLO 训练时如果data.yaml里指定的路径模式不匹配会漏掉部分图片。最稳妥的做法是转换时统一转成.jpg用 PIL 批量处理。from PIL import Image from pathlib import Path def unify_image_format(img_dir, output_dir, target_ext.jpg): 统一图片格式为 jpg img_dir Path(img_dir) output_dir Path(output_dir) output_dir.mkdir(parentsTrue, exist_okTrue) for img_file in img_dir.iterdir(): if img_file.suffix.lower() in {.jpg, .jpeg, .png, .bmp}: with Image.open(img_file) as im: # 转成 RGB 再保存避免 PNG 的 RGBA 通道导致训练报错 im.convert(RGB).save(output_dir / f{img_file.stem}{target_ext}, quality95) print(f格式统一完成输出目录: {output_dir}) unify_image_format(./JPEGImages, ./images_unified)注意PNG 转 JPG 时一定要先convert(RGB)否则 RGBA 四通道图片在 YOLO 数据加载阶段会直接抛异常这个坑我踩过不止一次。4. 用 YOLOv8 跑通农作物检测训练配置文件与关键参数4.1 data.yaml 的写法与路径陷阱YOLO 训练的第一步是写data.yaml。这个文件告诉 ultralytics 去哪里找训练集、验证集以及类别有哪些。一个典型的农作物检测data.yaml长这样path: /home/user/crop_dataset train: images/train val: images/val test: images/test nc: 5 names: 0: corn 1: wheat 2: rice 3: soybean 4: weed这里最大的坑是path和train的拼接逻辑。ultralytics 会把path和train拼在一起形成完整路径所以train写相对路径时不要以/开头。另外names的顺序必须和转换脚本里的class_list完全一致差一个位置所有类别都会错。如果数据集没有划分训练集和验证集需要自己写脚本按比例划分。常见做法是 8:1:1 或 7:2:1农作物数据集建议验证集比例不低于 15%因为田间场景的多样性很高验证集太小会导致评估指标波动大。import random import shutil from pathlib import Path def split_dataset(img_dir, label_dir, output_dir, train_ratio0.7, val_ratio0.2): 按比例划分训练集、验证集、测试集 img_dir Path(img_dir) label_dir Path(label_dir) output_dir Path(output_dir) # 收集所有有标注的图片 samples [] for label_file in label_dir.glob(*.txt): stem label_file.stem for ext in [.jpg, .png, .jpeg]: img_file img_dir / f{stem}{ext} if img_file.exists(): samples.append((img_file, label_file)) break random.seed(42) random.shuffle(samples) n len(samples) n_train int(n * train_ratio) n_val int(n * val_ratio) splits { train: samples[:n_train], val: samples[n_train:n_train n_val], test: samples[n_train n_val:] } for split_name, items in splits.items(): img_out output_dir / images / split_name lbl_out output_dir / labels / split_name img_out.mkdir(parentsTrue, exist_okTrue) lbl_out.mkdir(parentsTrue, exist_okTrue) for img_file, label_file in items: shutil.copy(img_file, img_out / img_file.name) shutil.copy(label_file, lbl_out / label_file.name) print(f{split_name}: {len(items)} 个样本) split_dataset(./images_unified, ./labels, ./crop_dataset)random.seed(42)保证每次划分结果一致方便复现。划分完成后data.yaml里的path指向./crop_datasettrain写images/trainval写images/val。4.2 训练命令与必调参数ultralytics 的训练入口非常简洁一条命令就能启动yolo detect train \ data./crop_dataset/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ project./runs/crop_det \ nameexp01逐个说参数。modelyolov8n.pt用的是 nano 版本参数量最小适合先跑通流程如果显存够可以换成yolov8s.pt或yolov8m.pt。imgsz640是输入分辨率农作物叶片目标通常不大640 够用如果小目标多可以提到 1024但显存占用会翻倍。batch16在 8GB 显存上比较稳妥12GB 以上可以开到 32。lr00.01是初始学习率YOLOv8 默认用 SGD 时这个值比较合适如果用 AdamW 要降到 0.001。patience20表示 20 个 epoch 验证指标不提升就早停农作物数据集如果增强做得好通常 80-120 个 epoch 收敛。训练过程中重点看三个指标box_loss是否稳定下降、mAP50是否在上升、cls_loss是否出现震荡。如果cls_loss震荡剧烈大概率是类别不平衡导致的需要回到第 2 章做重采样。如果mAP50卡在某个值不动检查验证集里是否有训练集没出现过的类别。提示第一次训练建议先用 10 个 epoch 跑一遍小规模验证确认数据加载、类别映射、损失计算都没问题再开完整训练。直接上 100 epoch 发现数据有问题浪费的是几个小时。5. 农作物检测训练避坑五条血泪经验5.1 现象训练 loss 正常下降但 mAP 始终为 0原因类别 id 映射错位。data.yaml里的names顺序和标注文件里的class_id不一致模型学到的类别和评估时的类别对不上。这种情况 loss 会正常下降因为模型确实在学东西只是学错了标签。解决用第 2 章的扫描脚本重新统计标注文件里的类别 id 分布和data.yaml逐项比对。最稳妥的做法是转换脚本和data.yaml用同一个class_list变量生成。5.2 现象训练到一半突然报 CUDA out of memory原因batch设太大或者imgsz太高。农作物数据集如果图片分辨率本身很大比如 4000x3000YOLO 会先 resize 到imgsz但如果batch也大显存峰值会超。解决先把batch降到 8 或 4用yolo detect train ... batch4跑通。如果还不行把imgsz从 640 降到 512。另外可以开启ampTrue默认开启混合精度训练能省不少显存。5.3 现象验证集 mAP 很高但实际推理时漏检严重原因验证集和训练集分布太接近模型过拟合了。农作物数据集如果按随机划分同一块田的图片可能同时出现在训练集和验证集里导致验证指标虚高。解决按田块或按拍摄日期划分数据集而不是随机划分。如果数据集没有田块信息至少按图片文件名前缀分组确保同一组的图片只出现在一个 split 里。5.4 现象某些类别始终检测不到原因该类别实例数太少或者标注质量差。农作物数据集里杂草类通常标注最粗糙因为杂草种类多、形态杂标注员容易漏标或错标。解决先统计每个类别的实例数少于 100 的类别考虑合并或剔除。如果必须保留用过采样加 mosaic 增强。另外检查该类别的标注框是否普遍偏小YOLO 对小目标的检测能力有限可以尝试提高imgsz。5.5 现象推理时图片被拉伸变形原因YOLO 默认的 letterbox 会保持宽高比并填充灰边但如果data.yaml里没有正确设置或者推理时用了rectFalse图片会被直接 resize 到正方形。解决推理时用yolo detect predict ... rectTrue保持宽高比。训练时 ultralytics 默认就是 letterbox不需要额外设置。如果发现推理结果框位置偏移检查推理时的imgsz是否和训练时一致。6. 用混淆矩阵和 PR 曲线验证农作物检测模型的真实水平训练跑完之后runs/crop_det/exp01/目录下会生成一堆评估图表其中最有价值的是混淆矩阵和 PR 曲线。很多人只看mAP50就结束了但这两个图能告诉你模型到底哪里不行。混淆矩阵的横轴是预测类别纵轴是真实类别。对角线越深越好非对角线上的值表示误判。农作物检测里最常见的误判是玉米和杂草混淆因为幼苗期的玉米和某些杂草形态非常接近。如果混淆矩阵显示玉米被大量预测成杂草说明这两个类别的特征区分度不够需要增加难例样本或者调整类别定义。PR 曲线Precision-Recall Curve展示的是不同置信度阈值下精确率和召回率的权衡。曲线下的面积就是 AP所有类别 AP 的平均就是 mAP。看 PR 曲线时重点关注曲线是否平滑如果某个类别的曲线急剧下降说明该类别在高召回率时精确率崩得很快实际部署时需要把置信度阈值调高。from ultralytics import YOLO import matplotlib.pyplot as plt # 加载训练好的模型 model YOLO(./runs/crop_det/exp01/weights/best.pt) # 在验证集上评估 metrics model.val(data./crop_dataset/data.yaml, splitval) # 打印每个类别的 AP print(每个类别的 AP50:) for i, name in enumerate(metrics.names.values()): print(f {name}: AP50{metrics.box.ap50[i]:.4f}) # 绘制混淆矩阵ultralytics 会自动保存到 runs 目录 # 也可以手动调用 metrics.confusion_matrix.plot(save_dir./runs/crop_det/exp01/)这段代码调用model.val()在验证集上跑评估返回的metrics对象包含box.ap50数组索引对应类别 id。metrics.confusion_matrix.plot()会把混淆矩阵保存成图片。参数splitval指定用验证集评估如果想用测试集就改成splittest。我自己的习惯是每次训练完先看混淆矩阵如果发现某个类别的误判率超过 30%就回到数据集里把这类样本单独拎出来看一遍。十次里有八次是标注问题剩下两次才是模型容量不够。农作物数据集尤其如此田间场景的标注一致性比模型结构重要得多。希望帮到你。本文还有配套的精品资源点击获取