简介:这份农作物多类别目标检测数据集面向农业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(f"XML标注: {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(parents=True, exist_ok=True) output_label_dir.mkdir(parents=True, exist_ok=True) # 找出包含目标类别的所有样本 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_id=3, target_count=500, output_img_dir='./dataset_balanced/images', output_label_dir='./dataset_balanced/labels' )这段代码的核心逻辑是先找出所有包含目标类别的标注文件,然后复制到新目录,再随机重复复制直到达到目标数量。target_class_id是你要过采样的类别 id,target_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_height
import 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(parents=True, exist_ok=True) 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)这段脚本的关键点在于边界裁剪和无效框过滤。农作物图片里经常出现标注框超出图片边界的情况,如果不裁剪,归一化后的坐标会大于 1,YOLO 训练时会直接报错。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、.bmp,YOLO 训练时如果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(parents=True, exist_ok=True) 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}", quality=95) 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_ratio=0.7, val_ratio=0.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(parents=True, exist_ok=True) lbl_out.mkdir(parents=True, exist_ok=True) 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_dataset,train写images/train,val写images/val。
4.2 训练命令与必调参数
ultralytics 的训练入口非常简洁,一条命令就能启动:
yolo detect train \ data=./crop_dataset/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=./runs/crop_det \ name=exp01逐个说参数。model=yolov8n.pt用的是 nano 版本,参数量最小,适合先跑通流程;如果显存够,可以换成yolov8s.pt或yolov8m.pt。imgsz=640是输入分辨率,农作物叶片目标通常不大,640 够用,如果小目标多可以提到 1024,但显存占用会翻倍。batch=16在 8GB 显存上比较稳妥,12GB 以上可以开到 32。lr0=0.01是初始学习率,YOLOv8 默认用 SGD 时这个值比较合适,如果用 AdamW 要降到 0.001。patience=20表示 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太高。农作物数据集如果图片分辨率本身很大(比如 4000x3000),YOLO 会先 resize 到imgsz,但如果batch也大,显存峰值会超。
解决:先把batch降到 8 或 4,用yolo detect train ... batch=4跑通。如果还不行,把imgsz从 640 降到 512。另外可以开启amp=True(默认开启),混合精度训练能省不少显存。
5.3 现象:验证集 mAP 很高但实际推理时漏检严重
原因:验证集和训练集分布太接近,模型过拟合了。农作物数据集如果按随机划分,同一块田的图片可能同时出现在训练集和验证集里,导致验证指标虚高。
解决:按田块或按拍摄日期划分数据集,而不是随机划分。如果数据集没有田块信息,至少按图片文件名前缀分组,确保同一组的图片只出现在一个 split 里。
5.4 现象:某些类别始终检测不到
原因:该类别实例数太少,或者标注质量差。农作物数据集里杂草类通常标注最粗糙,因为杂草种类多、形态杂,标注员容易漏标或错标。
解决:先统计每个类别的实例数,少于 100 的类别考虑合并或剔除。如果必须保留,用过采样加 mosaic 增强。另外检查该类别的标注框是否普遍偏小,YOLO 对小目标的检测能力有限,可以尝试提高imgsz。
5.5 现象:推理时图片被拉伸变形
原因:YOLO 默认的 letterbox 会保持宽高比并填充灰边,但如果data.yaml里没有正确设置,或者推理时用了rect=False,图片会被直接 resize 到正方形。
解决:推理时用yolo detect predict ... rect=True,保持宽高比。训练时 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', split='val') # 打印每个类别的 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()会把混淆矩阵保存成图片。参数split='val'指定用验证集评估,如果想用测试集就改成split='test'。
我自己的习惯是每次训练完先看混淆矩阵,如果发现某个类别的误判率超过 30%,就回到数据集里把这类样本单独拎出来看一遍。十次里有八次是标注问题,剩下两次才是模型容量不够。农作物数据集尤其如此,田间场景的标注一致性比模型结构重要得多。希望帮到你。
本文还有配套的精品资源,点击获取