☰
YOLO杂草与作物数据集实战:从数据清洗到训练评估的完整指南
2026/9/28 5:30:00 网站建设 项目流程

简介:这份数据集面向目标检测与农业视觉应用场景,适合正在学习YOLO系列算法,或需要训练杂草、作物识别模型的开发者与研究人员。压缩包约137.26MB,共2000个文件,主体为xml格式的VOC标注文件,每张图像对应一个独立标注;同时提供yolo格式的txt标注与data.yaml配置文件,txt中按类别索引、归一化中心点坐标及宽高记录目标框信息。数据集已完成训练集与验证集划分,VOC和YOLO两套标签并存,方便在检测框架间切换;适配YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10、YOLO11等主流版本,可直接用于模型训练、验证和测试。对于希望快速上手目标检测流程,或开展作物分行、杂草识别等农业AI项目的读者,这套资源省去了数据采集、标注和目录整理的重复劳动,拿到后即可按YOLO工程惯例调用。目前已有103人学习下载,适合作为从入门到实操的对照样本。

1. 2722张带标签的杂草与作物图像,为什么值得你认真把它做成一个YOLO项目

做农田视觉的人,手机里多少存着几个别人发来的数据集压缩包。真实场景里,这种“yolo算法-杂草和作物数据集-2722张图像带标签”的zip,比你在论文附录里找到的公开数据集更常见——它是上一个项目的交付物、一次田间采集的成果,或者外包标注团队交付的半成品。拿到之后直接丢进训练脚本的人不在少数,但我建议你先别急着跑模型:2722张图对一个二分类检测任务来说不算多,可它恰好卡在“迁移学习能带得动”的量级上。用对方法,这几个小时你就能得到一个可用于变量喷洒或除草机器人避让的杂草检测器;用错方法,你会被一个看起来很高、换块地就崩的mAP骗掉两周时间。

这条技术路线适合三类人:做精确农业的算法工程师、搞除草机器人视觉的学生、以及刚接手农场数据集但没系统做过YOLO训练的人。接下来我从解压到评估,按一线干活顺序讲清楚每一步该看什么、改什么、防什么。

2. 解压与标签核对:训练前两小时必须做对的几件事

2.1 先盘点文件构成,用脚本而不是肉眼确认标签状态

我拿到任何标注数据包的第一反应,不是打开文件夹浏览缩略图,而是先看扩展名统计。经过网盘转存、压缩软件二次打包、甚至被微信传输改名之后,zip里的文件结构经常面目全非。常见的情况是:

  • 图像是jpg或png,标签是同名txt,整体目录形态符合YOLO惯例;
  • 标签是xml,说明标注工具导出成了VOC格式;
  • 标签是json,可能是Labelme或COCO样式,需要单独解析。

先在自己机器上建一个工作目录,解压后跑一个最简单的统计命令:

mkdir -p ~/weed_work && cd ~/weed_work unzip "../yolo算法-杂草和作物数据集-2722张图像带标签-杂草-作物.zip" find . -type f | sed 's/.*\.//' | sort | uniq -c

命令逻辑不复杂:find列出所有文件,sed 's/.*\.//'截出扩展名,sort | uniq -c按扩展名计数。输出会明确告诉你zip里有多少jpg、多少txt、多少xml。如果txt数量与图像数量对不上,说明存在漏标或空标签,这个后面要重点排查。

接下来统计标签内容。我习惯把所有YOLO格式的txt读一遍,看类别ID分布和标注行数,这一步能提前暴露类别失衡和空标注文件:

from pathlib import Path from collections import Counter root = Path('.') txt_files = list(root.rglob('*.txt')) print('txt 标签文件数量:', len(txt_files)) class_counter = Counter() empty_files = [] total_boxes = 0 for txt in txt_files: lines = [line.strip() for line in txt.read_text().splitlines() if line.strip()] if not lines: empty_files.append(txt) continue for line in lines: parts = line.split() if len(parts) == 5: class_id = int(parts[0]) class_counter[class_id] += 1 total_boxes += 1 print('类别 ID 分布:', dict(class_counter)) print('标注框总数:', total_boxes) print('空标签文件数量:', len(empty_files))

这个脚本的价值在于把“标签质量”量化。比如发现类别0有2100个框、类别1只有120个框,那后续训练时就要对少样本类别做处理;发现几十个空txt文件,就得判断这些是“背景负样本”还是标注遗漏。YOLO训练协议里,空标签文件代表该图没有目标,可以保留,但你必须确认它们是有意采集的负样本,而不是丢标签。

2.2 标签格式统一与文件名对齐,VOC转换写成一个脚本

如果统计结果显示标签是VOC的xml,第一步不是训练,而是把它转成YOLO的txt。YOLO的标签一行五列:类别ID、归一化中心点x、归一化中心点y、归一化宽、归一化高。VOC里存的是左上角和右下角像素坐标,两者之间要做一次换算。我一般会写一段转换脚本,并保留原xml做备份:

import xml.etree.ElementTree as ET from pathlib import Path voc_dir = Path('annotations') yolo_dir = Path('labels') def voc_to_yolo(xml_path, out_path, class_names): 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'): name = obj.find('name').text if name not in class_names: print(f'未知类别 {name},跳过') continue class_id = class_names.index(name) box = obj.find('bndbox') x1 = float(box.find('xmin').text) y1 = float(box.find('ymin').text) x2 = float(box.find('xmax').text) y2 = float(box.find('ymax').text) cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f'{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}') out_path.write_text('\n'.join(lines)) # 按实际任务里的类别顺序写,顺序一旦确定不要随意改 class_names = ['crop', 'weed'] for xml_path in voc_dir.rglob('*.xml'): out_path = yolo_dir / xml_path.with_suffix('.txt').name voc_to_yolo(xml_path, out_path, class_names)

这段脚本里的类名顺序值得单独说:id从0开始连续排列,固定后不要改。如果你先跑了半轮训练,再把crop和weed互换顺序,前面load的权重标签就全部错位了,训练出的模型会表现得像是没见过数据一样。这个坑我踩过一次,损失的是整整一天时间,而报警信号只是loss值一直不降。

另外,文件名对齐也要顺手检查。YOLO的默认约定是图像img_001.jpg对应标签img_001.txt,但网传数据包里经常出现img_001(1).jpg这种重命名痕迹。我的建议是写一行循环,把所有文件名做一次规范化,统一成纯英文小写加数字,避免中文路径和空格进入训练管线:

for f in $(find . -type f \( -name '*.jpg' -o -name '*.txt' \) ); do dir=$(dirname "$f") base=$(basename "$f") newname=$(echo "$base" | tr 'A-Z ' 'a-z_') mv "$f" "$dir/$newname" 2>/dev/null || true done

这样处理后,后续所有脚本都按路口文件名处理,不会因为空格或中文编码半夜报错。等到后面用YOLO训练,你会感激这一步的。

提示:如果数据集里同时存在空标签文件和正常标签文件,空标签代表的负样本图像不要随手删,先保留。对农业场景来说,空地块图像是压制误检的关键数据。

3. 用YOLOv8在杂草与作物数据上跑通训练:目录布局、最小命令与关键参数

3.1 把Zip包整理成YOLO标准目录结构

YOLO官方默认的目录结构是images/train、images/val、labels/train、labels/val四件套。很多数据包交付时是images和labels两级目录,训练集和验证集还没分,需要手动整理。用ln关联目录或直接拷贝都可以,我喜欢先建标准目录再移动:

mkdir -p dataset/images/train dataset/images/val mkdir -p dataset/labels/train dataset/labels/val # 将全部图像与标签放入一个大目录后再做划分 python split_data.py

这个split脚本自己写的话,建议加入按路径前缀分组的功能,而不是简单的随机抽取。随机划分会在第四章详细讲为什么危险。在这里只用一个大原则:同一块地、同一次拍摄序列的图片,尽量只出现在一个集合里。

3.2 写data.yaml并执行最小训练命令

数据整理到位后,配置data.yaml。这是Ultralytics YOLOv8读取数据入口的唯一来源,路径和类名写错,后续全是空转:

path: /home/you/weed_work/dataset train: images/train val: images/val names: 0: crop 1: weed

注意path这个字段建议写绝对路径。开发机上用相对路径勉强能跑,但换一台机器、换一个工作目录启动Jupyter或shell脚本时,YOLO会自动拼接当前目录,经常出现“明明文件在那里,却报图片不存在”的玄学问题。排错半天到头来就是路径问题。

训练命令也简单,ultralytics装好后一行启动:

pip install ultralytics yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=60 \ imgsz=640 \ batch=16 \ lr0=0.001 \ patience=20 \ device=0

命令行里的参数直接影响训练成功率和最终精度,我按杂草项目的特点解释几个:

imgsz=640:杂草和大田作物的俯视图像通常来自无人机或行间相机,目标尺度偏小。如果标注框的绝对像素普遍小于32像素,640下会进一步缩小。先跑一组640拿baseline,再对比一组960或1280。显存够就优先用大尺寸。

batch=16:batch大小跟显存直接挂钩。8GB显存跑YOLOv8n、640输入,batch=16基本是上限;更强的模型如YOLOv8m直接减半。发现训练进程被CUDA out of memory杀掉,第一个动作是把batch调小,别去动imgsz。

lr0=0.001:迁移学习起步常用值。如果你发现loss曲线前几个epoch剧烈震荡,甚至出现nan,降到0.0005再试。有的人喜欢从0.01开始,但那是COCO随机初始化的大模型玩法,农业数据集样本少,开大学习率很容易让预训练权重崩掉。

如果不想用命令行,Python脚本的写法也很常见:

from ultralytics import YOLO model = YOLO('yolov8n.pt') results = model.train( data='data.yaml', epochs=60, imgsz=640, batch=16, lr0=0.001, cos_lr=True, patience=20, workers=4, device=0, )

这段里面cos_lr=True是按余弦曲线衰减学习率,样本量不大时通常比阶梯式衰减稳定,收敛更平缓。patience=20表示验证集metric连续20个epoch不提升就早停,避免无效等待。算力紧张时可以设小一点,但小于10容易在精度还在爬升时就被打断。

3.3 类别不均衡的简单处理:给少样本类别补数据

农业数据集的常见病是类别极端不均衡。杂草数据包如果只框了300个草目标、作物框却有1500个,模型当然会倾向学作物。此时我建议先不换复杂模型,直接从训练数据下手:

from pathlib import Path from collections import Counter label_dir = Path('dataset/labels/train') counter = Counter() for txt in label_dir.rglob('*.txt'): lines = [line for line in txt.read_text().splitlines() if line.strip()] if lines: counter[int(lines[0].split()[0])] += 1 print('当前各类别标注框数:', dict(counter))

然后对数量明显少的类别的图像做复制。复制图像的同时复制同名txt,放回训练目录。这个操作本质是过采样,不改变标注内容,只提高少样本类参与训练的频率:

import shutil from pathlib import Path image_dir = Path('dataset/images/train') label_dir = Path('dataset/labels/train') target_cls = 1 # 假设类别1是杂草 target_count = 1200 for txt in label_dir.rglob('*.txt'): lines = [line for line in txt.read_text().splitlines() if line.strip()] if not lines: continue if int(lines[0].split()[0]) == target_cls: img_path = image_dir / txt.with_suffix('.jpg').name if not img_path.exists(): continue for i in range(3): shutil.copy(img_path, image_dir / f'{txt.stem}_dup{i}.jpg') shutil.copy(txt, label_dir / f'{txt.stem}_dup{i}.txt')

注意过采样倍数别太大。一个类别复制个三倍还行,复制的图像数量超过原数据量两倍以上,模型容易记住重复图像的长相,在验证集上表现诡异。更稳妥的方式是对这些少样本图像做数据增强,比如随机旋转、翻转、调亮度,这样生成的重复样本和原图有差异,泛化性会好一点。但增强方式要符合农业图像规律,无人机俯视图像不能用垂直翻转,否则植物的朝向就错了,杂草在行间的几何位置关系会被破坏。

4. 杂草与作物YOLO训练最常翻车的5个坑,逐个给解法

4.1 验证集mAP高得像作弊,换块地就崩

现象:训练日志里mAP50一路爬到0.95以上,你满心欢喜直接部署,结果在隔壁田块实测时漏检一片。回来看验证集图像,发现验证集里有一半图像跟训练集长在同一块田、同一天拍摄,甚至相邻帧。

原因:随机划分数据集时,同一采集批的近邻图像被同时分进了训练集和验证集。模型在训练时已经见过同类背景,相当于考试题目光透过来,验证分自然高。

解决:按地块或按拍摄批次分组后再划分。代码里我把文件路径按前两级目录分组,组为单位抽20%做验证集:

from pathlib import Path import random import shutil all_images = list(Path('dataset/images/all').rglob('*.jpg')) for img in all_images: # 假设文件名是 field01_row03_20240711_0001.jpg 这类有明显前缀的结构 group = '_'.join(img.stem.split('_')[:2]) if group not in group_map: group_map[group] = [] group_map[group].append(img) val_groups = set(random.sample(list(group_map.keys()), k=int(len(group_map) * 0.2))) for g, imgs in group_map.items(): for img in imgs: if g in val_groups: shutil.move(str(img), 'dataset/images/val/') shutil.move(str(img.with_suffix('.txt')), 'dataset/labels/val/') else: shutil.move(str(img), 'dataset/images/train/') shutil.move(str(img.with_suffix('.txt')), 'dataset/labels/train/')

这段代码的价值不在调参,而在于把划分单位从“单张图”换成“组”。之后再跑训练,验证集成绩会变得真实,虽然数字会掉下来,但掉了的才是值得信的。

4.2 小目标是检测盲区,imgsz调到640根本不够

现象:验证集mAP50看起来中等,但把预测结果可视化后,发现宽高只有30、40像素的小草压根没被检测到。

原因:YOLO每个格子负责预测固定尺度的目标。当输入图像缩小到640,30像素的小目标在特征图上的响应已经弱到几乎不可分辨,尤其伴随遮挡和叶片重叠的情况。

解决:两个方向选一个。显存允许时直接加大imgsz到960或1280,很多小目标问题靠这一步就能改善。显存不够就把原图切块,每块提高目标相对尺寸。切块训练时每块图像独立标注,推理时再用nms合并结果。切块的具体做法是先把原始大图切成4片,分别作为一张图训练,同时把对应位置的标注归一化到各切片坐标系中。过程麻烦,但无人机大田数据常需要这一招。

4.3 图像EXIF旋转导致标签错位,loss降不下去

现象:训练了20个epoch,loss一直在0.9附近波动,怎么看都不收敛。打开一些图像人工检查,发现部分照片显示方向是正常的,但标签框全跑偏了。

原因:手机或部分行采集相机在拍摄时记录EXIF旋转信息,图片查看器会按方向参数自动摆正,但YOLO读取原始像素矩阵时不做这个事。标注软件摆正后标框,训练代码读的却是旋转前的像素,坐标自然全错。

解决:在训练前把所有图像统一转正,去掉EXIF里的旋转标记:

from PIL import Image, ImageOps from pathlib import Path for img_path in Path('dataset/images/').rglob('*.jpg'): with Image.open(img_path) as im: exif = im.getexif() if exif.get(274, 1) != 1: im = ImageOps.exif_transpose(im) im.save(img_path)

这段处理后图像像素方向固定,YOLO标签的归一化坐标也跟着固定,训练loss就会回到正常下降曲线。转正后的图像建议再抽查一轮,确认作物茎秆方向和标签框一致。

4.4 类别数量悬殊,模型把所有框都预测成作物

现象:验证集mAP虽然不低,但你发现检测结果里几乎没有杂草框,所有置信度高的检测都是作物。

原因:类别不均衡。如果这个zip交付时标注的杂草目标占比本身就很低,模型从多数类中获得的损失主导了梯度方向,少样本类被无视。

解决:除了前面说的过采样,还有一个直接手段是看训练日志里每一类的AP值。Ultralytics训练结束会输出每个类别的precision、recall和AP,哪一类AP为零就知道是哪一类没被学到。这时我建议回看labels目录,逐图确认少样本类的标注框是否真的覆盖了所有可见目标。有些数据集交付时为了赶工,漏标特别多,这不是训练问题,是标注质量问题。

4.5 显存不足报错,速度还慢,不知道先调哪个参数

现象:启动训练就报CUDA out of memory,或者杀进程换小batch后训练速度惨不忍睹。

原因:很多教程默认batch=32甚至64,完全不照顾8GB显存用户。YOLO训练框架在显存不足时不会自动回退,只会硬报错。

解决:先按batch=8试,能跑再往上加。模型先别换,YOLOv8n在显存占用和速度上都最稳。若batch=8还报错,排除一下是不是开了多线程数据加载导致内存峰值太高,把workers从8降到2。此外开启自动混合精度能省不少显存:

yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=60 \ imgsz=640 \ batch=8 \ amp=True \ device=0

amp=True就是混合精度训练,在绝大多数情况下不掉精度,对农业数据尤其值得开。

5. 评估不能只看一个数:用正确的指标和数据划分判断模型能不能下地

5.1 按地块拆验证集是第一步,指标才不会被虚高骗过去

第四章已经强调过不能随机划分,这一节再说透一点。随机划分本质是数据泄漏——同一采集批的图像只有几秒钟间隔,背景近乎相同。如果训练集和验证集同时包含这些图像,验证损失会显著偏低。我和同行交流时发现,很多人训练时就把全部2,722张图放到一个大目录,由YOLO随机分10%验证,最后报告0.9以上的mAP。这数据一上真实农田就露馅。

正确的做法是保证验证集里的任何图像与训练集之间没有“同场景相邻帧”。实际执行时用文件路径分组即可,我还会再叠加一个条件:把每天采集时间作为分组字段,让验证集中至少包含三天不同的拍摄时段。这样早晚光照变化、阴影角度差异在验证时也会被考核到,指标才有一点参考价值。

5.2 mAP50和mAP50-95分开看,结合精确率和召回率一起解读

训练完后终端会打印一组指标,主要看这几个:

from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') metrics = model.val(data='data.yaml') print('mAP50:', metrics.box.map50) print('mAP50-95:', metrics.box.map)

对杂草检测项目,mAP50和mAP50-95的差距能反映框的稳定程度。mAP50高而mAP50-95明显较低,说明检测框大概能覆盖目标,但边界贴合不好。对喷洒场景来说,情有可原;但对需要精确对行的机械除草,你得进一步压边界精度。

我把该关注的内容整理成下面的对照:

现象可能原因农业场景影响
mAP50高、mAP50-95低框定位不稳定,类别识别问题不大除草铲可能切到作物,需要注意边界
precision高、recall低漏检多,模型偏保守漏了杂草,除草效果打折
precision低、recall高误检多,模型把作物当杂草喷洒系统误喷作物,损失直接
weed类AP远低于crop类类别不均衡或漏标除草价值最大的部分反而没做好

很多项目只看一组总mAP就草草收尾,但杂草检测的根本矛盾从来不是“能不能找到草”,而是“能不能不伤作物”。如果precision不够,变量喷洒系统会把作物预算也喷掉,这在地里是最不能接受的故障。

5.3 把最难的那批图挑出来,用人工判断确认模型能力边界

指标只是汇总数字,不能告诉你哪一类杂草在什么光照下失效。我会在验证集跑一次推理,把所有漏检和误检的图按置信度排序单独建目录:

yolo predict model=runs/detect/train/weights/best.pt \ source=dataset/images/val \ save_txt=True \ save_conf=True \ conf=0.25

然后重点检查预测结果中置信度0.25到0.5之间的检测框。这些框往往对应草叶重叠、阴影遮挡、叶片边缘模糊的真实分布,也是模型能力边界所在。判断标准很简单:人眼能不能认出这里的杂草?能认出而模型丢了这个目标,说明特征学习的空间还不够;人眼也认不出,那就暂时不要强求模型。

6. 用验证集反推数据缺口,开启下一轮标注迭代

6.1 把漏检图像做成一个“难例回填”清单

模型训练到可以用之后,工作还没完。对农业项目来说,模型一定会在某个光照、某个生育期翻车,而这个翻车点往往对应着数据集的分布缺口。我的做法是把验证集推理结果中所有假阴性的图像copy到一个单独目录,重新标注或追加标注,定期扩充回训练集。不需要一次标很多,每周追加50到100张,模型的边界就会一点一点往前推。

如果数据包里只有2,722张图,不要指望一轮训练就把所有问题解决。第一轮训练后检查出来的空标签、误标和大目标漏标才是这个包真正的价值——它们指向的要么是标注质量问题,要么是场景覆盖不足,继续迭代的基础就在这里。

6.2 保持一个固定测试集,别随手改随机种子

我见过最多的效率杀手是同学改随机种子重新划分数据。今天用seed=42,明天改成2024,训练结果翻来覆去没法定量比较。正确做法是选定一个划分后固定下来,锁死在data.yaml对应的目录里,后续所有代码改动、参数调整都以这个固定划分做对比基准。这样每轮迭代的差异才真实反映模型变化,而不是数据波动带来的假象。

我自己习惯把每个训练轮次的预测结果保存一份到带时间戳的文件夹,方便周末统一回看。做过三轮之后你会发现,哪个类别的AP涨了、哪批漏检消失了、哪些图始终学不会,比任何训练日志都清楚。项目做到后期,最有用的经验就是这些按周累积的验证记录。

希望这个2830张图像的数据包迭代思路能帮到你,照这条路线走,做出能下地的杂草检测模型并不遥远。

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

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

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

立即咨询