简介:这是一份面向YOLO系列目标检测任务的杂草与作物数据集,适合农业智能化场景下的模型训练与算法验证。资源围绕杂草、作物两类目标提供2722张标注图像,已划分好训练集与验证集,并附带data.yaml配置文件,可直接适配YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10及YOLO11等常见框架。压缩包共2000个文件,以XML格式的标签文件为主,同时提供YOLO格式的TXT标注,两种格式分别存放,便于在VOC与YOLO体系间灵活切换。包体大小约137.26MB,目录结构清晰,标签文件与图像一一对应。数据集覆盖不同田间拍摄角度、光照与杂草密度,可用于学习目标框回归、类别判别及模型调参,也能帮助理解YOLO标注格式中归一化坐标的含义。目前已有103人学习下载,适合刚接触YOLO的初学者快速上手,也适合需要私有数据集验证算法效果的开发者。
1. 杂草作物zip数据集:2722张带标签图像到底能不能直接开训
做除草机器人、无人机植保巡检或者田间杂草识别的工程师,几乎都会碰到同一类东西:一份名为「yolo算法-杂草和作物数据集-2722张图像带标签-杂草-作物.zip」的压缩包。yolo算法跑目标检测,最缺的就是带标签的农业图像数据,这份2722张图像、带标签、zip打包的数据集听起来像是拿到就能训练。但实际经验是——这类zip十有八九不能直接喂给训练脚本:标签可能是VOC格式不是YOLO格式,类别编号对不上你的需求,甚至图像和标签根本对不齐。这篇文章就走一遍标准落地路径:拆包验标、整理成YOLO能吃的目录、跑通最小训练、把参数调明白,最后把最容易翻车的5个坑直接写在明面上。适合刚入手农业视觉的新手照着复现,也适合已经在跑训练但效果不理想的人对照排查。
2. 解压验标:先把2722张图和标签的底细查清楚
2.1 zip解压后先看目录嵌套,别急着拖进训练脚本
拿到zip先不要急着解压到训练目录。第一步永远是把zip放到一个临时目录里解开,看清楚里面到底是什么结构。常见做法是:
mkdir -p dataset_raw unzip "yolo算法-杂草和作物数据集-2722张图像带标签-杂草-作物.zip" -d dataset_raw find dataset_raw -maxdepth 3 -type d | head -n 30-d dataset_raw把解压内容放到独立目录,避免文件散落在当前目录。find加-maxdepth 3只看前三层目录,先确认有没有一层多余的嵌套文件夹——很多zip里还有一层同名目录,直接拿路径训练时容易把路径写错。
解压时有一个高频坑:Windows 上用资源管理器右键「压缩为zip」生成的包,在 Linux 上解压后中文文件名可能乱码。如果发现文件名乱码,改用unzip -O UTF-8指定编码重新解压。另外顺手做一次图像完整性检查,用 Python 逐个确认图像能打开:
from pathlib import Path from PIL import Image root = Path("dataset_raw") bad_images = [] for p in root.rglob("*"): if p.suffix.lower() in (".jpg", ".jpeg", ".png", ".bmp"): try: img = Image.open(p) img.verify() except Exception as e: bad_images.append((p, str(e))) if bad_images: for p, err in bad_images[:10]: print("坏图:", p, err) else: print("全部图像可以正常打开")Image.open延迟加载,img.verify()会真正校验文件完整性。这一步能提前发现中途下载断掉、压缩时损坏的图像,否则训练脚本会在某个 epoch 突然崩溃,而且报错信息往往指向下一张图,很难定位。
2.2 用脚本核对图像与标签的对应关系
图像能打开只是第一步,更重要的事情是验证图像和标签是不是一一对应。这类数据集常见问题是有标签没图像、有图像没标签,或者标签文件名大小写对不上。写一个脚本一次性查清楚。
from pathlib import Path root = Path("dataset_raw") image_exts = {".jpg", ".jpeg", ".png", ".bmp"} images = {} labels = {} for p in root.rglob("*"): if p.suffix.lower() in image_exts: images[p.stem] = p elif p.suffix.lower() == ".txt" and "classes" not in p.name.lower(): labels[p.stem] = p image_stems = set(images) label_stems = set(labels) print("图像数量:", len(image_stems)) print("标签数量:", len(label_stems)) print("有图无标签:", len(image_stems - label_stems)) print("有标签无图:", len(label_stems - image_stems))注意这里用p.stem做集合比较,不看后缀只比较文件名主体。很多数据集里图像是.jpg,标签是.txt,只要主体相同就能对上。如果发现大量「有图无标签」,可能这段数据是纯背景负样本,也可能是标注漏了;如果「有标签无图」,说明压缩包残缺,这两类文件都要单独挑出来处理。
再看一眼标签文件的实际内容,随手抽出三个看一眼:
find dataset_raw -name "*.txt" ! -name "classes.txt" | head -n 3 | xargs -I{} sh -c 'echo "== {}"; head -n 3 "{}"'这个命令的作用是把找到的前三个标签文件内容打印出来。看到形如0 0.52 0.37 0.18 0.23的行,说明是YOLO格式;如果看到<object><name>weed</name>这种尖括号,就是VOC XML混了进来;如果看到{"image_id": ...}那是COCO JSON。标题里写着yolo算法,但zip里实际是什么格式,必须以解压出来的文件为准。
2.3 YOLO标签的五个数字意味着什么:cls cx cy w h
YOLO格式的标签每一行只有五个数字:类别索引、中心点x、中心点y、框宽、框高。这五个数字全部是归一化到0到1之间的浮点数,除以图像宽高。很多人在这上面翻车:把VOC的左上角x1、y1直接当成了cx、cy,或者把像素坐标当成归一化坐标,训练出来的模型mAP从头到尾是0。
cls是整数,从0开始计数。杂草和作物两类的话,常见的约定是0 代表杂草,1 代表作物,或者反过来。类别编号本身没有天然语义,完全看data.yaml里怎么定义。麻烦的是不同数据集对同样两类东西的编号习惯不一样,拿到手必须逐个标签文件统计出现过哪些类别索引:
from pathlib import Path from collections import Counter root = Path("dataset_raw") counter = Counter() bad_lines = [] for p in root.rglob("*.txt"): if p.name == "classes.txt": continue for line_no, line in enumerate(p.read_text().strip().splitlines()): parts = line.split() if len(parts) != 5: bad_lines.append((p, line_no, line)) continue try: cls_id = int(float(parts[0])) values = [float(v) for v in parts[1:]] except ValueError: bad_lines.append((p, line_no, line)) continue if any(v < 0 or v > 1 for v in values): bad_lines.append((p, line_no, line)) counter[cls_id] += 1 print("类别索引分布:", counter) print("异常行数量:", len(bad_lines)) if bad_lines: for p, line_no, line in bad_lines[:10]: print(p, "第", line_no, "行:", line)这个统计同时查了三种异常:行数不是5、数值不能转成浮点、坐标超出0到1范围。类别索引分布能告诉你这份数据集的类别全集——如果统计出来索引是0、1、2,但你以为只有两类,那就说明zip里还藏着第三类你没注意。分类别统计框数量还有助于判断类别是否均衡,这一步在做任何训练之前花两分钟跑完,能省掉后面几天排错时间。
3. 把数据集整理成YOLO能直接训练的目录结构
3.1 统一成images/labels + train/val 的目录规范
yolo算法的主流训练框架(包括YOLOv5和YOLOv8)都约定一套目录:图像和标签分开,各自内部再分train和val。结构如下:
datasets/ └── weed_crop/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml图像放images,标签放labels,二者文件名主体一一对应。train和val的比例按8:2还是9:1取决于数据量,2722张图做二分类检测,8:2是够用的。如果标签和图像本来就在同一目录里,先把它们拆开:
from pathlib import Path import shutil import random src = Path("dataset_raw") dst = Path("datasets/weed_crop") random.seed(42) image_files = sorted([p for p in src.rglob("*") if p.suffix.lower() in {".jpg", ".jpeg", ".png"}]) image_files = [p for p in image_files if (src / (p.stem + ".txt")).exists()] random.shuffle(image_files) split_idx = int(len(image_files) * 0.8) for name, split_files in [("train", image_files[:split_idx]), ("val", image_files[split_idx:])]: img_out = dst / "images" / name lbl_out = dst / "labels" / name img_out.mkdir(parents=True, exist_ok=True) lbl_out.mkdir(parents=True, exist_ok=True) for img_path in split_files: label_path = src / (img_path.stem + ".txt") # 标签与图像同名,直接复制到对应分区的labels目录 shutil.copy(img_path, img_out / img_path.name) shutil.copy(label_path, lbl_out / label_path.name)这段脚本的关键点有两个:一是只复制「图标签同时存在」的样本,避免把残缺样本带入训练;二是用random.seed(42)固定随机种子,让每次划分结果一致,方便复现实验。实际工作中我一般不会直接跑shuffle就完事,还要先按图像文件名的前缀排序,让同一块地、同一架次拍出来的序列帧尽量落到同一个分区,这个下文单独讲。
3.2 写data.yaml:类别数和类别名必须和标签索引对齐
data.yaml 是训练框架读取数据集的入口,内容不多但容易错。格式如下:
path: datasets/weed_crop train: images/train val: images/val nc: 2 names: 0: weed 1: croppath是相对于当前工作目录的数据集根路径,train和val是相对path的图像目录路径。框架会自动把images/train替换成labels/train去找同名标签。nc是类别数量,names的索引顺序必须和第2章统计出来的标签类别索引完全对齐——如果你统计出来标签里 0 是 crop、1 是 weed,那 names 里 0 就必须写 crop,不能按自己喜好对调。
这里最容易出的问题是:zip 里有时带着一份classes.txt,里面的顺序是weed \n crop,你就默认杂草是0。但如果实际标签文件的第一个数字写的是1,那就是作物。所以写完data.yaml之后返回去对比第2章的类别统计结果,确认索引完全一致。
3.3 最小训练命令:先跑30轮验证收敛路径
整理完目录和配置文件,先不要一上来就跑几百轮。用最小的命令验证整条链路通不通:
cd datasets/weed_crop yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=30 \ imgsz=640 \ batch=16yolov8n.pt是纳米版预训练权重,模型小、跑得快,用来验证数据链路最合适。30轮的目的不是拿到好模型,而是确认三件事:训练 loss 在下降、验证 mAP 不是恒为0、没有任何路径报错。跑完以后看 outputs:
yolo detect val \ data=data.yaml \ model=runs/detect/train/weights/best.ptbest.pt是训练过程中在验证集上表现最好的权重,先拿它做验证,确认验证脚本也能正常读出数据和标签路径。如果这个最小链路顺利通过,后面换大模型、加轮数才有意义;如果这步就走不通,问题一定出在前面章节的目录或标注格式上,不用急着调参。
3.4 划分训练验证集时,千万别把同场景帧拆到两边
第3.1节直接随机打乱划分的方式,在普通数据集上没问题,但在杂草数据集上是个隐患。杂草数据往往是一块地连续拍几十张,相邻帧之间背景高度重叠。随机划分时同一场景的相邻帧可能一张进train一张进val,模型在验证集上「见过」几乎一样的内容,验证mAP会虚高。等你把模型部署到另一块没见过的地,性能立刻掉下来。
更稳妥的做法是按文件名的拍摄批次分组采样。很多数据集文件名自带编号或地块前缀,比如field1_001.jpg、field1_002.jpg,划分时以前缀为粒度分桶:
import random from pathlib import Path import shutil from collections import defaultdict random.seed(42) src = Path("dataset_raw") files = sorted([p for p in src.rglob("*") if p.suffix.lower() in {".jpg", ".jpeg", ".png"}]) groups = defaultdict(list) for p in files: # 按第一个下划线之前的字段作为场景ID,具体分隔符以实际文件名为准 scene_id = p.stem.split("_")[0] groups[scene_id].append(p) scene_ids = list(groups.keys()) random.shuffle(scene_ids) split_idx = int(len(scene_ids) * 0.8) train_scenes = set(scene_ids[:split_idx]) for scene in train_scenes: for p in groups[scene]: shutil.copy(p, dst / "images/train" / p.name) shutil.copy(src / (p.stem + ".txt"), dst / "labels/train" / (p.stem + ".txt"))按场景划分后,验证集的mAP才更能反映模型在未知地块上的真实水平。代价是训练数据量减少,但这是值得的——训练指标虚高会让后续所有调参决策失真。
4. yolo算法在杂草场景的调参:imgsz、batch与增强取舍
4.1 imgsz:小目标杂草绕不开的640还是1280
杂草检测和一般目标检测有个显著区别:杂草目标普遍小,一株幼苗在无人机图像里可能只有十几个像素。imgsz=640是YOLO模型的标准输入尺寸,对中大型目标友好,对杂草这种小目标很吃力。把imgsz提到1280,小目标的像素面积变成原来的4倍,检测率提升非常明显。
但1280不是白给的。输入尺寸翻倍,特征图占用显存约翻4倍,训练速度明显变慢。实际项目里我的习惯是:先用640跑通流程,确认数据和标签没问题;再用1280做一次完整训练,对比验证集mAP。如果mAP50提升超过3到5个点,就值得用1280;如果提升很小,说明主要瓶颈不在目标尺寸,而在标注质量或类别混淆,此时加大imgsz只是浪费显存。注意切换imgsz后 batch 要相应调小,否则直接OOM。
4.2 batch:显存允许范围内尽量大,但别让显卡满载到OOM
batch大小影响训练稳定性和速度。batch太小(比如2或4),梯度估计噪声大,loss曲线会剧烈抖动,收敛慢;batch大,梯度更平滑,训练稳,但对显存要求高。杂草数据集一般都很吃显存,因为背景纹理复杂,特征图内存占用大。
经验做法是:单卡训练时把batch从8开始试,看GPU显存占用率。如果占用超过90%,把imgsz或batch降一档。也可以用框架的自动批量:
yolo detect train ... --batch -1-1让框架自动探测显存占用,选择能放得下的最大batch。这个参数对不同显卡很友好,换机器不用手动改。自动batch有时偏保守,但先用它跑稳定,再手动微调。
4.3 epochs和早停:跑多少轮才算真收敛
2822张图跑目标检测,300轮是常见起点。但轮数不是越大越好,跑过头会过拟合,验证集mAP在某个点之后开始下降。主流框架默认带早停(patience参数),默认值一般是100轮左右。意思是验证集指标连续100轮没有提升就自动停。
我一般把patience设成50到80,杂草数据类别简单、收敛快,100轮太浪费。同时要在训练日志里看两个曲线:训练loss持续下降而验证loss开始反弹,就是过拟合的信号;训练loss和验证loss同步下降但mAP还在涨,那就继续跑。不要只盯训练集loss——它低到0.01不代表模型好用,验证集mAP才是唯一可信的标准。
4.4 类别不平衡与数据增强:两个比改网络更有效的招
杂草和作物数据天然不平衡。实际田地里作物可能占大部分像素,杂草只有零星几株;反之在荒废地块,杂草占主体。类别不平衡最直接的影响是模型倾向于把难分样本预测成多数类,比如只识别作物、漏掉杂草。
先看第2章统计的类别分布,如果杂草框数量和作物框数量差距在5倍以上,优先做两件事而不是换模型。第一是简单过采样:把少数类样本(图像级别的,指包含少数类目标的图像)在训练列表里复制两份;第二是调类别损失权重,在data.yaml所在的训练配置里给少数类一个更高的权重系数。数据增强方面,杂草识别最大的干扰来自光照和叶片颜色变化,hsv_h、hsv_s、hsv_v这三项增强对绿色场景非常有针对性。可以把默认值适当调大:
yolo detect train ... \ hsv_h=0.02 \ hsv_s=0.6 \ hsv_v=0.5 \ fliplr=0.5 \ mosaic=1.0hsv_h是色调扰动,控制在正负0.02即可,太大叶片会变色失真;hsv_s和hsv_v是饱和度和明度扰动,模拟不同光照时段。mosaic保持默认开启,它把四张图拼成一张训练,对小目标检测有明显帮助,代价是训练前期loss可能波动稍大。遇到训练不稳定时可以临时把mosaic=0.5降低占比,而不是完全关闭。
5. 杂草和作物数据集训练的5条踩坑记录
5.1 mAP一直是0,先查标签是不是归一化坐标
现象:训练正常跑完,loss也在下降,但验证集mAP从第一个epoch到最后一个epoch都是0。
原因:标签坐标没有归一化,或者坐标轴顺序不对。最常见的是有人把VOC的左上角、右下角像素坐标直接当成了cx、cy、w、h。YOLO格式要求中心点坐标和宽高都除以图像宽高,且中心点在前、宽高在后。像素坐标里cx和cy可能是几百甚至几千,除以宽高后才在0到1区间。
解决:回头跑第2章的统计脚本,看异常行数量。如果坐标值大量超出0到1,写脚本做转换。VOC转YOLO的公式是:cx = (xmin + xmax) / 2 / img_width,cy = (ymin + ymax) / 2 / img_height,w = (xmax - xmin) / img_width,h = (ymax - ymin) / img_height。转换后重新跑验证。
5.2 验证集mAP很高,实拍一测就翻车:数据泄漏
现象:训练时验证集mAP50到了0.95,模型下地拍一段视频,几乎什么杂草都框不出来。
原因:train和val划分时用了纯随机打乱,同一个地块的连续帧同时进了train和val。模型在验证集上见过的场景和训练集太像,评估结果虚高,一到陌生地块就原形毕露。
解决:按3.4节的思路,以场景或拍摄批次为粒度重新划分数据集。划分后验证集mAP会下降一些,这是正常的,说明指标变真实了。项目管理上要把「按场景划分」作为默认规则,不要每次图省事直接shuffle。
5.3 杂草小目标漏检严重:imgsz和切片的问题
现象:模型对近处、较大的杂草框得准,对图像远处的小杂草完全无感,验证时小目标的召回率低得可怜。
原因:这是小目标检测的经典问题。杂草在原始图像里可能只有20×20像素,缩放到640分辨率后只剩几个像素,特征在下采样过程中被吞掉了。YOLO的下采样步长是32,意味着特征图上一个格子对应原图32×32像素区域,小于这个尺寸的目标本身就很难框准。
解决:先试imgsz=1280。如果显存不够或者提升有限,改用第6章的切片推理方案。另外训练时可以用模型内置的多尺度训练参数,让模型在0.5到1.5倍缩放之间随机切换,增强对目标尺寸的适应能力。
5.4 标签里出现超出图像边界的框
现象:训练日志里偶尔出现坐标警告,或者推理时有些框贴在图像边缘,框的中心点落在图像外面。
原因:标注软件允许标注者把框拖到画布外,也可能转换脚本坐标计算失误,导致w或h越过边界。这类框参与训练时会干扰回归损失,让框的预测不稳定。
解决:训练前加一个清洗步骤。把所有标签里坐标小于0或者大于1的框截断到边界,过滤掉宽高小于3像素的超小框:
for line in label_lines: cls_id, cx, cy, w, h = map(float, line.split()) x1 = max(0.0, cx - w / 2) y1 = max(0.0, cy - h / 2) x2 = min(1.0, cx + w / 2) y2 = min(1.0, cy + h / 2) new_w = x2 - x1 new_h = y2 - y1 if new_w < 3 / img_width or new_h < 3 / img_height: continue这里把中心点坐标换算回左上右下角做夹取,再换算回去。过滤阈值3 / img_width按图像的绝对像素来算,3像素以下的框在特征图上几乎不可见,留着只会增加噪声。
5.5 类别不平衡导致模型只认作物不认杂草
现象:模型对作物框得又准又全,对杂草的召回率低,甚至把杂草和背景混为一谈。
原因:训练集里杂草的标注框数量远少于作物,模型在损失函数层面被多数类主导,学不到杂草的区分特征。尤其在杂草幼苗期,杂草和作物都是绿色叶片,特征本来就接近,少数类更吃亏。
解决:先统计两个类别的框数量,把数量少的那一类的图像复制重采样,一般复制到接近1:2以内就不错了,不追求完全均衡,过度过采样容易过拟合。然后在训练参数里给少数类提高损失权重。做完这两步后重新训练,观察每类单独的AP变化,而不是只看整体mAP。
6. 进阶:小目标杂草的切片推理与错分分析
6.1 切片推理的思路
如果你试过imgsz=1280仍然对远处的杂草漏检,下一招是切片推理。思路很简单:训练时仍然用640或者1280的输入,但推理时把大图切成若干小块,每块单独过模型,然后合并结果。切片长度一般取256或320,相邻切片留20%重叠,重叠部分同一目标可能出现两个框,合并时用IoU阈值过滤重复框。重叠比例不能太低,否则目标正好卡在切片边界容易被截断。
切片推理对杂草这种稀疏小目标非常有效,代价是推理时间成倍增加。适合放在无人机巡检后处理的离线流程里,不适合部署在实时性要求高的终端设备上。如果实时性和精度都要,通常是训练一个低阈值模型跑全图粗筛,再对可疑区域单独切片细看。
6.2 用混淆矩阵和PR曲线找错分规律
训练结束后不要只看mAP一个数字。把验证预测结果的混淆矩阵和各类的PR曲线打出来,能看出更多问题:杂草被误分成作物的比例高,说明两类特征重叠严重,优先加强数据增强而不是加深网络;杂草被误分成背景的比例高,说明小目标还是没被召回,优先处理imgsz和切片;作物被误分成杂草的比例高,说明标注样本里可能存在边界情况不清晰,回去修标签比调参更有效。
我自己早期在类似数据集上吃过亏:第一次训练mAP50有0.9,以为已经够用,下地实拍才发现完全是数据泄漏撑起来的虚假繁荣。后来每一次拿到新的杂草数据集,都强迫自己先花半小时做验标和按场景划分,再谈训练。这个习惯让我少走了不少弯路。yolo算法在农业场景里不是跑通就完事,验证集指标要能被陌生地块复现才算数,希望帮到你。
本文还有配套的精品资源,点击获取