简介:这份杂草与作物目标检测数据集专门面向yolo系列算法开发者与农业智能化研究人员,包含2722张已标注图像,覆盖杂草和作物两类目标,可直接用于模型训练、验证与测试。数据集已预先划分好训练集、验证集,并附带data.yaml配置文件,适配yolov5、yolov7、yolov8、yolov9、yolov10、yolo11等主流版本,省去自行整理与格式转换的麻烦。压缩包共2000个文件,以xml标签文件为主,同时提供yolo格式的txt标签与voc格式的xml标签两种存储方式,分别位于独立文件夹,标签内容包含目标类别、中心点坐标、宽高比例等标准字段,便于不同框架直接读取。包体大小约137.26MB,整体结构清晰,下载后即可加载数据集配置开始训练,也可结合可视化工具核对标注质量。目前已有102人学习下载,适合需要快速获取高质量农业检测数据的入门及进阶开发者。
1. 2722张带标签的杂草-作物图像,能训出什么样的YOLO模型
杂草识别是精准农业里最典型的视觉任务之一,目标是在田间图像中区分作物与杂草,为定点喷洒或机械除草提供感知输入。这个zip包里的数据量很明确:2722张图像、带标签、类别只有杂草和作物两类。你可能会觉得2722张太少,但实际上这个量级对于二分类检测来说,属于“起步充分、落地欠火候”的状态——用它训一个YOLOv8n做产品原型完全够用,但想直接扛住不同光照、不同土壤背景、不同生长周期的田间视频流,还得靠后续的半监督迭代。这篇内容会把从解压zip到模型部署验证的全流程拆开,重点讲清楚标签格式的处理、数据增强怎么做才不伤小目标、训练时哪些参数对应这个数据规模的陷阱,以及最后怎么判断模型真的能用而不是只活在验证集里。
2. 从zip到YOLO能吃的目录:标签格式与数据整理
2.1 先确认这2722张图的标签到底是什么格式
“带标签”是个含糊的说法。YOLO系列模型原生消费的是txt格式的标注文件,每行一个目标,格式为class_id x_center y_center width height,坐标是相对图像宽高的归一化值。但网上流传的数据集经常是VOC的XML或者COCO的JSON,甚至可能是LabelMe的JSON。拿到zip后的第一件事,不是急着训练,而是解开压缩包,摸清楚图像和标签的对应关系。
常见做法是写一个探查脚本,遍历目录结构并统计标签文件类型。我一般会同时输出目录树的前两层以及每个子目录下文件数量,重点确认三个信息:图像是jpg还是png、标签是xml/json/txt中的哪一种、是否每个图像都有对应的标签文件。
import os from collections import Counter from pathlib import Path dataset_root = Path("weed_crop_dataset") ext_counter = Counter() missing_label = [] for img_path in dataset_root.rglob("*"): if img_path.suffix.lower() in {".jpg", ".jpeg", ".png", ".bmp"}: ext_counter[img_path.suffix.lower()] += 1 # 判断同名的txt标签是否存在 txt_path = img_path.with_suffix(".txt") xml_path = img_path.with_suffix(".xml") json_path = img_path.with_suffix(".json") if not (txt_path.exists() or xml_path.exists() or json_path.exists()): missing_label.append(str(img_path)) print("图像格式分布:", dict(ext_counter)) print("缺标签的图数量:", len(missing_label)) for p in missing_label[:10]: print(p)这段代码先把所有图像文件找出来,统计格式分布,再逐个检查同名的三种常见标签文件是否存在。如果缺失数量很大,说明标签命名与图像名不对应,需要进一步看标签文件的内部引用字段,而不是盲目补文件。如果缺失很少,大概率是标注时漏标的边角样本,直接删掉或单独归类都行。
这一环节最常见的坑是:图像文件名形如IMG_20230415_093021.jpg,而标签名是IMG_20230415_093021.txt,表面看没问题,但某些数据集会在文件名里混入中文或空格,Windows下解压会改变编码,导致训练时OpenCV读不到图片。处理这类数据集的第一步规范,就是统一重命名,全部转成000001.jpg这种纯数字格式。
2.2 把VOC或COCO标签转换成YOLO的txt格式
如果探查下来发现标签是XML格式,需要做格式转换。VOC的XML会记录每个目标的name和bndbox坐标,转换逻辑是把xmin/ymin/xmax/ymax换算成归一化的中心点坐标和宽高。转换前先收集所有类别名字,确定类别索引的映射关系,避免后面写着两个类,实际标签里却冒出第三个类名来。
import xml.etree.ElementTree as ET from pathlib import Path def voc2yolo(xml_path, out_dir, class_map): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_map: continue cls_id = class_map[name] box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") out_name = Path(xml_path).stem + ".txt" with open(Path(out_dir) / out_name, "w") as f: f.write("\n".join(lines)) class_map = {"weed": 0, "crop": 1} xml_dir = Path("labels_xml") txt_out = Path("labels_yolo") txt_out.mkdir(exist_ok=True) for xml_file in xml_dir.glob("*.xml"): voc2yolo(xml_file, txt_out, class_map)转换后必须做一次反向校验:随机抽取50张图,把txt里的坐标画回图上,肉眼确认框的位置是否贴合目标。这一步不能省,因为有些数据集里XML的坐标单位不是像素而是百分比,转换后会出现框彻底偏移的情况。画框检查可以用OpenCV的rectangle函数,一次生成一张拼接网格图,几十张图扫一遍就够。
2.3 数据划分:分层抽样保住杂草类别的占比
2722张图按7:2:1划分为训练集、验证集和测试集很常见,但要注意:如果杂草只出现在部分图像中,随机划分可能导致验证集里杂草目标数量极少,mAP评估失真。更稳的做法是按“图像中是否含杂草”做分层抽样,先分出含杂草和不含杂草两个集合,再各自按比例划分,这样能保证三个子集里杂草图像的比例接近原始分布。
mkdir -p dataset/images/{train,val,test} mkdir -p dataset/labels/{train,val,test} python - <<'EOF' import random from pathlib import Path import shutil random.seed(42) img_root = Path("images") label_root = Path("labels_yolo") all_imgs = list(img_root.glob("*.jpg")) weed_imgs = [] no_weed_imgs = [] for img in all_imgs: txt = label_root / (img.stem + ".txt") if not txt.exists(): continue has_weed = any(line.startswith("0 ") for line in txt.read_text().splitlines()) (weed_imgs if has_weed else no_weed_imgs).append(img) def split_imgs(imgs): random.shuffle(imgs) n = len(imgs) return imgs[:int(n*0.7)], imgs[int(n*0.7):int(n*0.9)], imgs[int(n*0.9):] for split_name, split_imgs in zip(["train", "val", "test"], map(split_imgs, [weed_imgs, no_weed_imgs])): pass EOF上面脚本的思路是先把图像按是否含杂草分成两个池子,再各自划分。实际运行时你需要把两个池子的结果合并写入各自的子集目录,代码里我把这段用pass占位了,完整版里会逐个shutil.copy图像和标签文件。seed固定为42后,每次运行划分结果一致,后续对比实验才不会因为数据分布漂移干扰结论。分层抽样对杂草这类长尾分布尤其重要——如果原数据里杂草图像只占三成,随机划分中某一折可能只剩两成,模型对杂草的召回率波动会非常剧烈。
3. 训练前必调的三件事:图像尺寸、标签均衡与数据增强策略
3.1 统计目标尺寸分布,决定imgsz怎么设
2722张图的训练规模不大,所以输入分辨率的选择对效果影响很大。如果图像里杂草占的面积小,比如在无人机俯拍图中杂草只有几十个像素,那么直接缩放到640x640会把小草变成几个像素的噪点,模型根本学不到纹理特征。训练前先用脚本统计所有标注框的像素宽度和高度分布,看看中位数和百分位数落在哪里。
from pathlib import Path import numpy as np boxes = [] for txt in Path("labels_yolo").glob("*.txt"): for line in txt.read_text().splitlines(): parts = line.split() w = float(parts[3]) h = float(parts[4]) boxes.append([w, h]) boxes = np.array(boxes) print("归一化宽度 中位数: {:.4f}, 90分位: {:.4f}".format( np.median(boxes[:, 0]), np.percentile(boxes[:, 0], 90))) print("归一化高度 中位数: {:.4f}, 90分位: {:.4f}".format( np.median(boxes[:, 1]), np.percentile(boxes[:, 1], 90)))如果90分位的框宽高都小于0.1,说明大量目标是中、小目标,训练时imgsz应设为832甚至1024而不是默认的640。代价是显存占用变大,2722张图迭代100轮的时间会拉长,但针对小目标的数据集,提升输入分辨率常常比换更大的模型更直接。反之,如果90分位的框宽高都在0.3以上,说明目标占比大,640就够,强行增大分辨率只会带来过拟合风险。
3.2 类别不均衡:杂草少不是问题,问题是怎么采样
两个类别的目标数量如果差距超过3倍,训练时模型会倾向于把不确定目标预测成样本多的那一类。YOLOv8自带focal loss缓解类别不均衡,但对极端比例帮助有限。更可控的做法是过采样:把含杂草的图像在训练集里多复制几份,或者在数据加载层面设置每个batch强制包含一定比例的杂草图像。
另一个实用技巧是调整loss里类别损失的权重。Ultralytics的配置里没有直接暴露cls_loss_weight参数,但可以通过自定义数据集采样器实现。常见做法是写一个自定义Dataset,在__getitem__里以概率p决定返回杂草图像还是作物图像,人为控制每个epoch里两类图像的出现频率接近1:1。如果不想动代码,最简单的方式是直接用mosaic增强——把4张图拼成一张,其中至少一张是含杂草的图,相当于变相把杂草样本的参与频率提高了。
3.3 数据增强参数怎么配才不伤杂草小目标
YOLOv8的增强参数集中在hyp.yaml里,对杂草数据集需要重点调整的是scale和hsv_h。scale=0.5表示训练时图像缩放范围是0.5到1.5倍,这对大目标无感,但对小目标草株来说,缩到0.5倍后目标可能只有十几个像素,标注框还在但语义信息已经丢了。杂草检测场景我一般把scale压到0.3,mosaic=1.0保持开启,translate=0.1控制平移幅度,fliplr=0.5左右翻转保留,上下翻转flipud关掉——因为作物种植行方向固定,上下翻转会让模型学到错误的几何先验。
| 参数 | 默认值 | 杂草数据集建议 | 理由 |
|---|---|---|---|
| scale | 0.5 | 0.3 | 避免小目标缩放后不可辨 |
| fliplr | 0.5 | 0.5 | 左右翻转不破坏行的方向 |
| flipud | 0.0 | 0.0 | 上下翻转会颠倒种植行方向 |
| hsv_h | 0.015 | 0.02 | 田间光照变化大,适当加强色调扰动 |
| translate | 0.1 | 0.1 | 保持默认,平移不改变目标尺度 |
| mosaic | 1.0 | 1.0 | 拼接增强提高小目标上下文 |
这些参数在Ultralytics里可以直接通过训练命令传hyp路径,也可以写到yaml文件里。
4. 用YOLOv8在2722张图上训练杂草检测模型
4.1 写好data.yaml:类别顺序、路径和中文名陷阱
YOLO训练的第一步是准备数据集配置文件。杂草数据集需要明确两个类别的索引顺序,训练脚本、标签文件和这个yaml三者必须完全一致,否则模型训练完做推理时会把类别名对应错。
path: /home/user/weed_crop_dataset train: images/train val: images/val test: images/test names: 0: weed 1: croppath字段用绝对路径,避免相对路径在不同工作目录下解析出错。names的键值顺序必须和标签txt文件里的第一个数字一致。有个容易忽略的细节:如果图像路径里含中文目录名,即使训练能跑通,OpenCV在某些版本的编码处理下会读不到图,导致训练集实际数量少于预期。拿到数据集先看路径,有中文就全部改掉。验证数据配置是否正确的最快方式,是运行yolo detect train前先用一行命令检查:
python -c "from ultralytics import YOLO; model = YOLO('yolov8n.pt'); results = model.val(data='data.yaml')"如果配置有问题,这一步会直接报错或警告,不会等到训了半天才发现数据没加载。
4.2 训练命令与关键参数:从n模型起步,先看能不能收敛
2722张图属于小规模数据,首选yolov8n或yolov8s。直接上yolov8l大概率过拟合,而且训练时间长,不利于快速迭代验证标签质量。初始训练参数我常用:
yolo detect train \ model=yolov8n.pt \ data=data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ patience=20 \ seed=42 \ augment=True \ cache=Trueepochs=100配合patience=20,意思是如果连续20个epoch验证集mAP没提升就早停。lr0=0.01是YOLOv8的默认初始学习率,对小数据集来说可以接受,如果发现loss前10个epoch震荡不降,改成0.005重试。cache=True显存够时建议开启,把图像加载进内存,省去每轮epoch的磁盘IO——2722张jpg图全部载入大约占用几个GB内存,性价比很高。
训练过程中要盯着两个指标:train/box_loss是否平稳下降,以及val/mAP50有没有在早停前出现平台期。如果box_loss一直降但mAP卡住不动,优先怀疑标签噪声太大,比如杂草和作物在图像中本就难以区分,标注员的框位置飘忽不定。另一个常见现象是mAP50很高但mAP50-95很低,说明框的位置基本正确但交并比不够高,这类问题到部署阶段会表现为检测框轻微抖动,需要从标签质量下手,而不是盲目加大训练轮数。
4.3 混淆矩阵能告诉你哪些错误不可接受
训练结束后的confusion_matrix.png是杂草模型最值得看的一张图。对于喷洒类应用,把作物误检成杂草导致除草剂打到作物上,和把杂草漏检导致草没除掉,两者代价完全不同。混淆矩阵里如果crop被大量预测成weed,说明两类的外观特征在模型看来有重叠,此时需要回到数据层面检查是不是标签本身就标错了,或者增加更多不同生长阶段的图像。如果weed大量漏检成背景,则是正样本不足的问题,优先考虑过采样或使用更小的conf_thres重新评估。
训练结束后用测试集做一次完整的指标评估,记住要保留生成最优模型的权重文件best.pt而不是last.pt。last.pt是最后一个epoch的权重,早停情况下它的指标往往比best低一截,部署时选错文件会白白损失几个点的准确率。
5. 部署前的一个关键验证:按目标尺寸分层统计召回率
训练完别急着接摄像头。杂草检测的落地难点在于目标尺度变化极大——刚出苗的草只有十几个像素,长到分蘖期可能比作物还大。通用mAP把大小目标混在一起算,一个模型mAP50有0.9,不代表它对小目标可用。把测试集推理结果按标注框面积分层统计召回率,才是真正暴露模型短板的方法。
from ultralytics import YOLO from pathlib import Path import numpy as np model = YOLO("runs/detect/train/weights/best.pt") img_dir = Path("dataset/images/test") area_ratio = [] is_tp = [] for img_path in sorted(img_dir.glob("*.jpg")): results = model.predict(str(img_path), conf=0.25, verbose=False) txt_path = Path("dataset/labels/test") / (img_path.stem + ".txt") gt_boxes = [] for line in txt_path.read_text().splitlines(): parts = line.split() w = float(parts[3]) * 640 h = float(parts[4]) * 640 gt_boxes.append((w * h)) pred_boxes = [] if len(results) > 0 and results[0].boxes is not None: for box in results[0].boxes.xyxy.cpu().numpy(): pred_boxes.append((box[2] - box[0]) * (box[3] - box[1])) for area in gt_boxes: area_ratio.append(area / (640 * 640)) is_tp.append(any(_calc_iou(box, area) for box in pred_boxes))这段代码只是框架,完整的逻辑需要计算预测框和真实框的交并比,并判断是否大于0.5。运行后把area_ratio分成三档:小于0.01的算小目标,0.01到0.05算中目标,大于0.05算大目标,逐档统计召回率。如果小目标召回率明显低于其他两档,说明输入分辨率不够或标注框小于10像素难以学习,此时应改用imgsz=960重训,或者引入滑窗推理——把原图切成四块分别推理再合并结果。最后还要做一次真实场景的连拍视频测试,采集连续帧观察检测框是否抖动,杂草检测最终是要驱动执行机构的,稳定的边界框比偶尔刷高几个点的mAP更值得追求。
本文还有配套的精品资源,点击获取