简介面向小麦田地自动除草场景的杂草检测数据集包含约一千四百张已完成标注的田间图像及对应的YOLO格式标签文件覆盖八类杂草标签可供计算机视觉学习者、智慧农业开发者和目标检测方向的研究者直接用于模型训练、验证与改进。包内已经完成训练集和验证集划分并附带可视化脚本运行后即可快速预览图像与标注框的叠加效果便于检查数据质量。整个资源共两千个文件包括五百八十五张JPG原始图像、一千四百一十四个TXT标签文件和一个Python工具脚本压缩包整体约276MB目录结构清晰可直接接入YOLOv5、YOLOv8等常见检测框架使用。目前已有101人学习下载。数据采集自不同光照和拍摄角度的小麦田覆盖多种常见杂草类型既可用于杂草识别、精准施药等农业视觉任务的算法验证也能作为目标检测模型改进实验的基准数据集帮助读者省去自行采集与标注的周期和时间成本。1. 小麦田地杂草检测手上有标注数据才是第一步做农田目标检测的工程师应该都有体会模型选型、调参这些事反而是简单的真正卡住进度的永远是数据。YOLO 系列跑起来很快但标注框质量、类别分布、光照环境这些问题直接决定训练出来的模型是能用还是翻车。这份小麦田地杂草图像数据集大约有 1400 张已标注图像和对应标签类别数 8 类用的是 YOLO 标注格式来源是不同时间、不同角度拍摄的田块影像文件名里能看到拍摄日期和 crop 信息。适合两种人一是刚入门目标检测、想拿真实农田数据练手的学生二是已经在做植保无人机或田间监测系统、需要快速验证检测思路的开发者。它最大的价值不是让你跳过标注环节而是给你一个可以直接跑通流程的起点。2. 读懂这份数据集的标注格式classes 文件是第一个关键点2.1 从文件名信息反推采集环境这批数据的文件名包含拍摄日期和时间比如camden_wheat_radish1_45_20210421_11-33-07_crop.jpg这个命名里radish1表示地块编号45可能是拍摄角度crop说明原图已经被裁剪过。这类命名信息在训练时容易被忽略但实际上很有用如果你的验证集和训练集来自同一天同一块地模型的泛化能力会被严重高估。我一般会建议先按文件名里的日期把数据分一下组再做训练集和验证集的划分而不是直接随机切分。至于图像的原始尺寸这批数据没有在简介里明说。YOLO 标注文件里只存相对坐标所以即使图像尺寸不统一也不影响训练但在预处理时最好统计一下所有图像的尺寸分布统一 resize 到某个尺度。常见做法是取所有宽高的中位数或者直接按 YOLOv5 默认的 640x640 来处理。2.2 classes 文件决定了标签索引的映射关系数据集包含 8 个类别分别是 RAD1、RAD2、RAD3、RAD4、RA1、RD2、RAD23、RDA3。这些编号看起来像杂草种类缩写但具体对应什么植物数据集的 classes 文件里会有明确顺序。YOLO 标注格式的 txt 文件第一列是类别索引从 0 开始计数所以 classes 文件的顺序绝对不能随意改动。举个例子如果classes.txt里的顺序是RAD2、RAD3、RAD4、RAD1、RD2、RAD23、RDA3、RA1那么索引 0 对应的就是 RAD2索引 7 对应的是 RA1。一旦训练脚本和数据集的 classes 顺序不一致模型输出的类别就全是乱的而且这个错很难发现因为 Loss 还是会下降但预测结果是对不上的。2.3 YOLO 标注文件的实际内容长什么样打开一个与 .jpg 同名的 .txt 文件里面是若干行文本每行由 5 个数组成格式为class_id x_center y_center width height。这里的 x_center、y_center、width、height 都是相对于图像宽高的比例值取值在 0 到 1 之间。0 0.521484 0.435547 0.083203 0.115234 2 0.472656 0.328906 0.036719 0.047656第一行表示图像中心位置在横向约 52.1%、纵向约 43.6% 处有一个宽度约占 8.3%、高度约占 11.5% 的 RAD2 杂草目标。这种相对坐标的标注方式有两个好处一是图像缩放后不需要重新计算标注框二是在多尺度训练时可以直接沿用。3. 把 YOLO 标注数据跑通训练流程从数据校验到 YOLOv5 实战3.1 先做数据完整性校验这一步不能省拿到这批数据后不要急着训练先写一个扫描脚本校验三件事每张图像是否有对应 txt 文件、txt 内容格式是否正确、类别索引是否超范围。很多标注数据在拷贝过程中会产生缺失文件或者某些框的坐标值出现越界。import os from pathlib import Path def validate_yolo_dataset(img_dir, label_dir, num_classes8): img_files list(Path(img_dir).glob(*.jpg)) list(Path(img_dir).glob(*.png)) issues [] for img_path in img_files: label_path Path(label_dir) / (img_path.stem .txt) if not label_path.exists(): issues.append(fMissing label: {label_path}) continue with open(label_path, r) as f: lines f.readlines() for idx, line in enumerate(lines): parts line.strip().split() if len(parts) ! 5: issues.append(fBad line {idx1} in {label_path}: {line.strip()}) continue cls_id, x, y, w, h map(float, parts) if cls_id num_classes or x 0 or y 0 or w 0 or h 0: issues.append(fOut of range in {label_path}: {line.strip()}) if x w / 2 1.0 or y h / 2 1.0 or x - w / 2 0 or y - h / 2 0: issues.append(fBbox beyond image bounds: {label_path}: {line.strip()}) if issues: print(fFound {len(issues)} issues, first 20:) for issue in issues[:20]: print(issue) else: print(fAll {len(img_files)} images passed validation.) return issues这里检查了坐标越界的情况因为 YOLO 训练时会把这些越界的框直接过滤掉导致有效样本数量打折。如果发现大量框越界宁可先修正数据也不要直接去调训练参数。3.2 划分训练集和验证集按日期分组更合理数据简介里提到已经做了训练集和验证集的划分但我个人习惯拿到后重新分一次。原因是这批数据的文件名里带日期字段比如20210421和20210514如果随机划分同一时刻拍摄的图像会同时出现在训练集和验证集里导致验证指标虚高。import random from pathlib import Path import shutil random.seed(42) image_dir Path(images) label_dir Path(labels) train_img_dir Path(train/images) train_lbl_dir Path(train/labels) val_img_dir Path(val/images) val_lbl_dir Path(val/labels) for d in [train_img_dir, train_lbl_dir, val_img_dir, val_lbl_dir]: d.mkdir(parentsTrue, exist_okTrue) all_images list(image_dir.glob(*.jpg)) # 按文件名中的日期分组 date_groups {} for img in all_images: # 假设文件名形如 camden_wheat_radish1_45_20210421_11-33-07_crop.jpg parts img.stem.split(_) for p in parts: if len(p) 8 and p.isdigit(): date_groups.setdefault(p, []).append(img) break train_imgs [] val_imgs [] for date, img_list in date_groups.items(): random.shuffle(img_list) split_point int(len(img_list) * 0.8) train_imgs.extend(img_list[:split_point]) val_imgs.extend(img_list[split_point:]) for img in train_imgs: shutil.copy(img, train_img_dir / img.name) lbl label_dir / (img.stem .txt) if lbl.exists(): shutil.copy(lbl, train_lbl_dir / lbl.name) for img in val_imgs: shutil.copy(img, val_img_dir / img.name) lbl label_dir / (img.stem .txt) if lbl.exists(): shutil.copy(lbl, val_lbl_dir / lbl.name) print(fTrain: {len(train_imgs)}, Val: {len(val_imgs)}) print(Date groups:, {d: len(v) for d, v in date_groups.items()})按日期分组再按 8:2 划分的好处是验证集能反映模型在不同光照、不同生长阶段下的表现更接近真实部署场景。如果某个日期的图像数量特别少比如只有几张可以把这个日期整体放训练集避免验证集出现极端分布。3.3 配置 YOLOv5 数据集文件并启动训练YOLOv5 需要一份dataset.yaml来指定训练集和验证集的路径、类别数和类别名。把数据集组织成如下结构wheat_weed_dataset/ ├── train/ │ ├── images/ │ └── labels/ ├── val/ │ ├── images/ │ └── labels/ └── dataset.yamldataset.yaml的内容是train: /path/to/wheat_weed_dataset/train/images val: /path/to/wheat_weed_dataset/val/images nc: 8 names: [RAD2, RAD3, RAD4, RAD1, RD2, RAD23, RDA3, RA1]启动训练时可以用 YOLOv5s 作为预训练模型因为数据量只有 1400 张用太大的模型容易过拟合。python train.py --img 640 --batch 16 --epochs 100 \ --data /path/to/wheat_weed_dataset/dataset.yaml \ --weights yolov5s.pt --name wheat_weed_exp这里 batch size 取 16 是考虑到农田图像里目标较小太大的 batch 会导致 BatchNorm 统计不稳定。训练过程如果发现 GPU 显存不够可以把 batch 降到 8 或 4。迭代次数 100 是这个数据量下比较合理的起点如果 50 轮后 Loss 已经基本收敛可以直接早停。3.4 用 show 脚本可视化标注框数据集简介里提到运行 show 脚本可以可视化数据。这类脚本一般会随机读取若干张图像并把标注框画上去输出成带框的新图片。在训练前做可视化很重要因为能直接用眼睛判断标注质量。import cv2 import random from pathlib import Path def show_yolo_labels(image_dir, label_dir, classes, sample_count9): img_files list(Path(image_dir).glob(*.jpg)) sampled random.sample(img_files, min(sample_count, len(img_files))) cols 3 rows (len(sampled) cols - 1) // cols canvas None for i, img_path in enumerate(sampled): img cv2.imread(str(img_path)) h, w img.shape[:2] label_path Path(label_dir) / (img_path.stem .txt) with open(label_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id, x_c, y_c, bw, bh map(float, parts) cls_id int(cls_id) x1 int((x_c - bw / 2) * w) y1 int((y_c - bh / 2) * h) x2 int((x_c bw / 2) * w) y2 int((y_c bh / 2) * h) color (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, classes[cls_id], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) if canvas is None: canvas img else: canvas cv2.hconcat([canvas, img]) if (i 1) % cols else cv2.vconcat([canvas, img]) cv2.imshow(YOLO Labels, canvas) cv2.waitKey(0) cv2.destroyAllWindows() classes [RAD2, RAD3, RAD4, RAD1, RD2, RAD23, RDA3, RA1] show_yolo_labels(path/to/images, path/to/labels, classes)这段脚本只是核心逻辑的示意数据集中自带的 show 脚本会更完整。可视化时重点看两类问题一类是框明显比目标大一圈或小一圈另一类是同一类别的框在不同图片中的尺度差异是否合理。如果杂草较小但有上千张图可以考虑做切图预处理。4. 数据体积小怎么办从增强策略到类别不平衡处理4.1 1400 张图像其实不够即使加预训练仍需要增强策略1400 张图像对 YOLOv5s 这类模型来说属于偏小的数据集虽然用 COCO 预训练权重初始化可以大幅缩小训练所需数据量但农田场景和自然图像的分布差异较大还是需要通过数据增强来扩充有效样本。YOLOv5 自带了一些增强选项比如 HSV 扰动、随机翻转、尺度变化这些开箱即用但不够针对性。农村田野场景有一个非常典型的特点杂草形状相近而背景纹理差异大土块、作物叶片、残茬都会干扰检测。我一般会在训练配置里显式开启 mosaic 增强和 mixup 增强因为这能让模型学会在更复杂的背景中区分目标。# hyp.scratch.yaml 中相关部分 mosaic: 1.0 # 训练时 100% 概率使用 mosaic 增强 mixup: 0.2 # 20% 概率做 mixup hsv_h: 0.015 # 色调扰动幅度比默认的 0.1 调小避免颜色失真 hsv_s: 0.5 # 饱和度扰动 hsv_v: 0.4 # 明度扰动需要特别注意 hsv_h 参数。农田图像的杂草颜色是分类和检测的重要依据如果色调扰动太大模型会学到错误的颜色关联。我吃过这个亏默认的 0.015 偏保守但调到 0.1 后模型在真实农田图像上的误检率明显上升。建议从 0.01 到 0.02 之间测试。4.2 类别不平衡是杂草数据集的普遍痛点看 8 个类别 RAD1 到 RDA3它们在实际田块中的分布往往差异很大。如果某些类别只占总量的一小部分模型会倾向于把这类杂草预测成其他类别导致召回率很低。训练前先统计各类别的框数量很有必要。import os from collections import Counter from pathlib import Path def count_bboxes_by_class(label_dir): counter Counter() total_boxes 0 for lbl_path in Path(label_dir).glob(*.txt): with open(lbl_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id int(float(parts[0])) counter[cls_id] 1 total_boxes 1 return counter, total_boxes label_dir path/to/labels counter, total count_bboxes_by_class(label_dir) print(fTotal bboxes: {total}) for cls_id in range(8): print(fClass {cls_id}: {counter.get(cls_id, 0)} boxes)如果发现某类少于 50 个框应该优先考虑针对性增强而不是直接加大训练轮数。常用做法是把该类图像复制几份并加上随机裁剪和翻转困难的是同时保留标注文件的同步修改。4.3 选择 YOLOv5s 还是 YOLOv8n数据量决定模型容量这批数据有三个可选路线YOLOv5s、YOLOv8n、或者 YOLOv5m。数据量约 1400 张时我推荐优先试 YOLOv8n推理速度快且不容易过拟合如果检测效果不够再上 YOLOv5m同时配合更强的正则化和 Early Stopping。YOLOv5 的改进方向里有一个常见的操作是替换 Backbone 为轻量化结构但数据量这么小的前提下改动 Backbone 收益有限不如先把基础流程跑通拿到 baseline。有一个细节值得注意YOLOv9、YOLOv10 系列虽然新但它们的默认训练配置是按大规模数据集调过的在小数据集上未必比 v5/v8 稳。而且很多改进脚本基于 v5 结构网上能搜到大量 v5 的改进案例更利于后续调试。5. 避坑指南杂草检测数据训练的五条血泪经验5.1 标注框的类别索引与 classes 文件顺序不一致训练跑完验证时RAD1 和 RA1 的预测结果经常混在一起。原因是数据集的 classes 文件顺序可能是RAD2,RAD3,RAD4,RAD1,RD2...但我一开始按字母序排列了names导致训练时模型学到的索引映射和标注文件的索引完全错位。解决方式是在训练前用脚本统一输出 classes 顺序到 dataset.yaml 里并且手动抽查几张训练数据确认可视化框的标签是否和原图一致。5.2 背景中其他杂草被误识别为目标如果图像里存在数据集中未标注的杂草种类模型会把它们当成已知类预测这在田间很常见。解决方式取决于标注数据的来源如果原图确实包含未知杂草建议用较差置信度的检测结果反推并补充标注如果无法补充标注就在推理阶段提高置信度阈值比如从 0.25 提到 0.45会明显降低误检。5.3 白天不同时段光照差异导致模型白天可用、黄昏失效这次数据的文件名里有 11-33 和 14-52 这样的时间字段说明不同图像采集时间跨度很大。如果验证集恰好集中在中午图像模型在早晨和傍晚的泛化能力无法得到检验。建议把文件名带日期和时间的图像按时间段分组保证验证集出现至少两个时段的图像再计算 mAP才能看出真实水平。5.4 图像裁剪不当导致目标被切断从crop文件名可以判断这批图是裁剪得到的裁剪时某些杂草可能被框沿切断。这类切断目标在检测时有两种可能一是被模型当作完整目标检出但框偏小二是直接漏检。如果可视化后发现大量目标贴着图像边界建议在预处理时用一种常见的做法——图像不加 padding 直接保留边界框YOLO 训练时会自动忽略边界外的部分但需要注意总体的有效框数量。5.5 训练 Loss 正常但 mAP 始终为 0一个容易忽略的坑训练脚本自动生成的labels.jpg文件展示了目标在网格中的分布情况注意观察小目标是否大量集中在某些位置然后检查 dataset.yaml 的nc是否写成了类别总数加一。有一次我把背景也当成一类强制加了进去导致验证时所有预测的类别索引都偏移了一位mAP 就直接归零。这个教训是每次改 dataset.yaml 后先用一张图跑detect.py看输出再决定是否继续调参。6. 推理阶段的两个实用技巧置信度阈值与滑动窗口检测6.1 先校准置信度阈值再谈 mAP训练完的模型直接拿默认置信度阈值 0.25 去跑田间图像效果往往不佳——杂草检测任务中误检代价远高于漏检。我一般会在验证集上画置信度-召回率曲线或者用验证集跑一次批量推理统计不同阈值下的 F1 分数选一个平衡点。对于较高危杂草的检测场景宁可调高阈值保证准确率。import torch from pathlib import Path model torch.hub.load(ultralytics/yolov5, custom, pathruns/exp/weights/best.pt) # 对验证集做不同阈值的推理找F1最高的阈值 best_f1 0 best_conf 0.25 for conf in [0.15, 0.25, 0.35, 0.45, 0.55]: results model(val_img_dir, conf_thresconf) stats results.pandas().xyxy[0] # 取第一张图的统计此处只做示意 precision stats[confidence].gt(conf).mean() if len(stats) else 0 # 实际应统计全验证集的 P/R/F1这里只说明阈值扫描思路 print(fconf{conf}, raw detections {...})6.2 稠密小目标场景用滑窗推理代替直接全图检测在有大量小杂草的田块图像中直接全图推理往往效果不佳。此时更好的做法是把原图切成 640x640 的块每一块单独推理再按坐标还原到原图。切块时要保留一定重叠度常见做法是重叠 10% 到 20%避免目标被切断。推理完毕后保留置信度最高的框用 NMS 融合。import cv2 import numpy as np def sliding_window_detect(model, img_path, window_size640, overlap0.2, conf_thres0.35): img cv2.imread(img_path) h, w img.shape[:2] step int(window_size * (1 - overlap)) detections [] for y in range(0, h - window_size 1, step): for x in range(0, w - window_size 1, step): crop img[y:y window_size, x:x window_size] results model(crop, conf_thresconf_thres) for *xyxy, conf, cls_id in results.xyxy[0].tolist(): x1, y1, x2, y2 [int(v) for v in xyxy] detections.append((x x1, y y1, x x2, y y2, conf, int(cls_id))) # 合并重叠框取置信度最高的保留 filtered [] for det in sorted(detections, keylambda d: d[4], reverseTrue): keep True for prev in filtered: # 简单 IOU 计算重叠过大则丢弃 iou compute_iou(det[:4], prev[:4]) if iou 0.5: keep False break if keep: filtered.append(det) return filtered在实际部署时滑窗推理的速度会慢一些可以结合显卡显存调节批量大小每块图同时推理多个窗口来提速。对我来说这类数据集最好的落地方式不是一次性追求高精度而是先用它把数据预处理和训练流程走通再在自采数据上做迁移学习。从那以后我每次接手一个带标注的 YOLO 数据包都会强制先跑一遍可视化脚本、类别分布统计和文本标注格式校验确认这三关过了才允许模型进入训练阶段。有标注数据不代表能直接躺赢数据本身的校验和管理功力决定了下限希望这些经验能帮你在小麦杂草检测这条路上少踩几个坑。本文还有配套的精品资源点击获取