☰
YOLO小样本作物杂草检测:数据处理与切图推理实战
2026/10/9 3:44:12 网站建设 项目流程

简介:面向YOLO系列目标检测算法使用者的作物与杂草数据集,包含200张原始图像与完整标注,已按常见比例划分好训练/验证集,并提供data.yaml配置文件,可直接在YOLOv5、v7、v8、v9、v10、v11等主流版本中调用,适合农业信息化、智能除草等场景的算法研究与实践。压缩包共601个文件,核心由200张jpg原图、200个txt标签(YOLO格式)和200个xml标签(VOC格式)组成,外加1个yaml配置文件,整体185.56MB;两种标签格式分别置放,方便对比不同标注规范或迁移旧有代码。txt标签遵循<类别> <中心x> <中心y> <宽> <高>的归一化坐标格式,类别从0开始编号,可直接送入大多数YOLO训练脚本,省去手动转换环节。目前已有106人学习/下载,数据整体质量较好,覆盖多种农田杂草形态;对正在准备数据集或需要快速验证检测效果的开发者而言,能显著减少标注与格式适配的时间成本,是开展作物杂草检测实验的高性价比选择。

1. 作物杂草 YOLO 数据集的“小样本”开局,第一步不是训练

一拿到这种“yolo算法-作物杂草数据集-200张图像带标签-.zip”压缩包,绝大多数人的第一反应都是解压、把图片路径塞给训练脚本,然后盯着 loss 曲线等结果。这也是小样本目标检测里最经典的翻车姿势:训练 loss 一路下滑、验证 mAP 挺好看,模型一到真实田间就漏检成片。作物杂草检测的难点从来不在网络结构,而在作物与杂草形态高度相似、幼苗目标小、田间光照和遮挡千变万化,200 张图能覆盖的变化实在有限。

这份 zip 里只有 200 张带标签图像,意味着你手里的数据本质是一份标注样板,而不是直接能“收割”精度的训练资源。它的价值在于让你在一个能复现的规模上,把作物杂草检测从数据规约、标注转换、模型训练到部署验证的全链路跑通;等方案可行了,再按同样的流程补充样本或接入更大数据。这也是很多刚入门 yolo 学习的人最容易走偏的地方——总想靠换模型结构涨点,却忽略了对小数据集来说,数据处理才是决定成败的杠杆。

下面我按自己处理这类小样本农业数据的顺序来讲:解压验数、转换标注、划分增强、YOLOv8 训练调参、避坑排查,最后再给一个离线可用的切图推理技巧。无论你是正在做农业视觉、植保无人机,还是目标检测课程项目,照着这套流程,都能把这份数据用到靠谱的 mAP 上。

2. 解开 zip 先验数据:200 张带标签图像的文件规约与类别盘点

2.1 先看目录结构:图像、标注、类别说明是不是齐的

拿到压缩包的第一步不是训练,而是解压并带着疑问看目录。常见的作物杂草数据包布局无非这么几种:图像和 txt 标签躺在同一目录;图像、标注各占一个文件夹;还有直接打包 VOC 风格的 JPEGImages 和 Annotations。布局不同,后面的数据加载和转换逻辑也不一样。先花几分钟盘清楚,能省掉之后一整天的重标时间。

# 解压到指定目录,避免文件散落在当前路径 mkdir -p crop_weed unzip "yolo算法-作物杂草数据集-200张图像带标签-.zip" -d crop_weed cd crop_weed # 统计扩展名分布:图像、标注、类别文件一目了然 find . -type f | sed 's/.*\.//' | sort | uniq -c # 清理 mac/windows 系统自带的隐藏垃圾 rm -rf __MACOSX

unzip -d指定解压目录,是为了防止压缩包内文件过多时当前目录被塞满;find配合sed和uniq -c则是把扩展名频次打印出来。正常情况会看到 200 个 jpg、200 个 txt 这类对得上的数字。如果看到的是 200 张 jpg 配 90 个 txt,说明标注缺失过半,后面就得先决定补标还是丢样本,而不是闷头训练。

这里还要注意一个工程细节:压缩包的文件名和内部路径经常会带中文或空格,解压命令一定要给 zip 文件名加上引号;项目所在路径我一般会固定在crop_weed_yolo这种纯英文、无空格的目录下。原因是 YOLO 的数据加载器对 unicode 路径支持并不稳定,训练到一半报FileNotFoundError的玄学问题,大半出在这里。

2.2 逐行读 YOLO 标签:五个数字、归一化坐标和容易忽略的细节

YOLO 的 txt 标注格式比标注工具导出的 JSON 简单得多,每行一个目标,五个字段依次是:类别 id、归一化中心 x、归一化中心 y、归一化宽、归一化高。注意全是 0 到 1 的浮点数,不是像素坐标。

# 一个典型的 crop_weed 标注文件 0 0.375000 0.284375 0.123437 0.095312 1 0.182031 0.521875 0.090625 0.085938

第一行的含义:类别 0,框中心落在图片宽度 37.5%、高度 28.4% 的位置;框本身占整幅图宽度的 12.3%、高度的 9.5%。换算到 640 输入下,中心约在 (240, 182),框宽约 79 像素、高约 61 像素。归一化坐标意味着标注与原始分辨率解耦,换imgsz时不用重算坐标,这是 YOLO 格式最省心的地方。

但也正因为是浮点数,手工标注的归一化坐标常带着肉眼看不见的错误。我见过不少数据包有中心点写成 1.000002、宽高出现负数、五个字段混用制表符和逗号的情况。写任何处理逻辑前,先直接打印几行原始标注:

head -n 3 labels/*.txt | head -n 30

看到的内容如果五花八门,后面第 3 章的统一转换就有必要;如果全部是规整的五字段,你可以跳过大部分转换步骤,但要额外检查一批分辨率,因为 200 张图里混入不同尺寸的图非常常见。可以用 python 统计一遍所有图片的真实尺寸,避免训练时石破天惊的宽高比问题:

from PIL import Image from pathlib import Path from collections import Counter sizes = Counter() for p in Path("images").glob("*.jpg"): with Image.open(p) as im: sizes[im.size] += 1 print(sizes.most_common(5))

如果输出里出现好几种分辨率,例如 640x640 和 3024x4032 并存,我建议按分辨率把数据分成两份分别处理,而不是强行混训。混训不是说不能跑,而是小目标在缩放到 640 时本来就容易被压没,再混入高分辨率大图,模型会学得非常拧巴。

2.3 类别盘点与映射:200 张图里到底有几个类、分布怎么样

不同来源的 zip 里类别定义差异非常大。有的压缩包只有 crop 和 weed 二分类;有的细分到玉米、大豆、稗草、苋菜;更夸张的会把“泥土”“背景”也设成一类。YOLO 的类别数量直接决定检测头输出维度,中途改类别列表意味着之前所有标注都要重新生成,所以我拿到数据的第一件事永远是统计类别分布。

from collections import Counter from pathlib import Path label_dir = Path("labels") counter = Counter() for txt in label_dir.glob("*.txt"): for line in txt.read_text(encoding="utf-8", errors="ignore").splitlines(): line = line.strip() if not line: continue cls = int(line.split()[0]) counter[cls] += 1 print(counter) # 输出示例:Counter({0: 183, 1: 124, 2: 17})

这段代码不依赖任何第三方库,把所有 txt 里的类别 id 都数了一遍。输出要重点看三件事:类别数是不是比你预期多;有没有某个类别只有十几个框;以及有没有整张图都是空标注的 txt 文件。200 张图、类别数 3 个以内,训练难度相对可控;如果跑出来 5 类以上,每类只有几十个框,我建议直接把低频类别合并进“杂草”大类,别舍不得——小样本数据集宁少类、不多类,类别越少,每个类能分到的训练样本越多,模型越稳。

空标注文件也要单独处理。200 张图里十来张没有框,在真实数据里很常见。处理方式是把它们留在训练集里当负样本,让模型见过“这张图啥也没有”的情况,多少能压掉一点背景误报;但不要放进验证集,因为验证时大量空图会让 mAP 计算变得没有参考价值。

3. 把标注统一成 YOLO 的 txt:格式转换、坐标重算与可视复查

3.1 从 VOC XML 转 YOLO txt:最小脚本与两个高发错误

如果压缩包里的标注其实是 VOC XML,转成 txt 是绕不开的一步。常见做法是写一个一次性转换脚本,把 XML 里的xmin ymin xmax ymax转成归一化的x_center y_center w h。

import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, img_w, img_h, class_map): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter("object"): name = obj.findtext("name").strip() if name not in class_map: continue cls_id = class_map[name] box = obj.find("bndbox") x1 = float(box.findtext("xmin")) y1 = float(box.findtext("ymin")) x2 = float(box.findtext("xmax")) y2 = float(box.findtext("ymax")) x_center = (x1 + x2) / 2.0 / img_w y_center = (y1 + y2) / 2.0 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") return lines class_map = {"crop": 0, "weed": 1} xml_dir = Path("labels_xml") out_dir = Path("labels_yolo") out_dir.mkdir(exist_ok=True) for xml_path in xml_dir.glob("*.xml"): # 实际写代码时,img_w 和 img_h 要从图片或 XML 的 <size> 节点里读 lines = voc_to_yolo(xml_path, img_w=640, img_h=640, class_map=class_map) (out_dir / (xml_path.stem + ".txt")).write_text("\n".join(lines), encoding="utf-8")

第一高发错误就是img_w和img_h写死成 640。图片是 800x600 的时候,你再除以 640,所有框的位置整体偏移,训练出来的模型等于在学一套错的坐标。第二高发错误是class_map的顺序后来变了。今天你把 crop 排 0、weed 排 1,明天又觉得 crop 该排 1,结果数据和模型配置里类别顺序对不上,验证集 mAP 直接是乱的。

3.2 坐标越界检查:哪些标注要修、哪些要删

转换完成后,我会立刻跑一遍坐标合法性检查。YOLO 对边界框的约束比想象中宽松,但中心点越界、宽高为负会直接丢弃目标,这对小样本数据集来说就是白白损失标签。

from pathlib import Path for txt in Path("labels_yolo").glob("*.txt"): bad = [] for i, line in enumerate(txt.read_text().splitlines()): if not line.strip(): continue parts = line.split() if len(parts) != 5: bad.append(f"line{i}: 字段数不对 {len(parts)}") continue c, cx, cy, w, h = parts if not (0 <= float(cx) <= 1 and 0 <= float(cy) <= 1): bad.append(f"line{i}: 中心点越界 cx={cx} cy={cy}") if float(w) <= 0 or float(h) <= 0: bad.append(f"line{i}: 宽高非正数 w={w} h={h}") if float(w) > 1 or float(h) > 1: bad.append(f"line{i}: 宽高超过原图 w={w} h={h}") if bad: print(txt.name, "; ".join(bad))

越界处理要分情况。中心点只略超过边界,比如 0.999 或 1.001,一般是浮点精度或标注误差,我通常直接钳制到合法区间;宽高比原图还大,说明坐标基于的分辨率根本不对,必须删掉重标;整个框大部分在图外,删行比硬算边界可靠。宁可少一个框,也不能让模型学到一个错框,这是小样本训练的铁律。

3.3 可视化复查:把框画回图上,用眼睛做最后一道质检

坐标数值检查只能发现“格式错误”,发现不了“标错的类别”和“框大了小了”这类语义错误。200 张图不多,值得花二十几分钟把标注框画回原图逐张过一遍。

import cv2 from pathlib import Path img_dir = Path("images") label_dir = Path("labels") out_dir = Path("visual_check") out_dir.mkdir(exist_ok=True) for txt in sorted(label_dir.glob("*.txt")): img = cv2.imread(str(img_dir / (txt.stem + ".jpg"))) if img is None: print("缺少图像:", txt.name) continue h, w = img.shape[:2] for line in txt.read_text().splitlines(): parts = line.split() if len(parts) != 5: continue c, cx, cy, bw, bh = map(float, parts) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) color = (0, 255, 0) if int(c) == 0 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, str(int(c)), (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) cv2.imwrite(str(out_dir / (txt.stem + ".jpg")), img)

这段脚本会把每个 txt 对应图片画上彩色框,类别 0 画绿色、类别 1 画红色,输出到visual_check目录。肉眼扫一遍这批图,能同时发现三种问题:框是否完全包住目标、类别 id 是否标反、以及空标注文件是否真的没有目标。这一步的价值远高于调参,我认为它就是小样本项目的后悔药。

4. 用 YOLOv8 把 200 张带标签图像跑通:划分、增强与超参

4.1 训练/验证划分:按地块或拍摄批分,而不是随机抽

很多人在小样本数据集上翻车的第一道坎,不是训练,而是数据划分。随机划分看起来公平,但作物杂草数据里常有连拍图像,同一株杂草在连续几帧中重复出现,随机划分会把近乎重复的样本同时分进训练集和验证集,验证 mAP 虚高得不像话。真实换一块地、换一个光照,模型立刻现原形。

# 按图片文件名的前缀分组,例如 plot01_001.jpg、plot02_001.jpg python - <<'EOF' from pathlib import Path import random, shutil imgs = sorted(Path("images").glob("*.jpg")) groups = {} for p in imgs: g = p.stem.split("_")[0] # 取地块前缀 groups.setdefault(g, []).append(p) random.seed(42) group_names = list(groups.keys()) random.shuffle(group_names) split_idx = int(len(group_names) * 0.8) train_groups = group_names[:split_idx] val_groups = group_names[split_idx:] for g in train_groups: for p in groups[g]: shutil.copy(p, "train/images") shutil.copy(Path("labels") / (p.stem + ".txt"), "train/labels") for g in val_groups: for p in groups[g]: shutil.copy(p, "val/images") shutil.copy(Path("labels") / (p.stem + ".txt"), "val/labels") EOF

这个按组分划的逻辑,本质上是让模型去面对“它没见过的地块”。如果文件名里没有地块前缀,就按拍摄时间段、目录来源或者其他元信息分组;实在没有元信息再退回去用随机划分,但后续一定要在实际场景里补测一次,不能只看验证集数字。

4.2 数据增强参数:200 张图不增强等于直接放弃

在只有 200 张图时,增强不是锦上添花,而是训练能否收敛的生死线。作物幼苗没有明显的“左右语义”,水平翻转不会破坏类别含义,却能让样本多样性翻倍;随机缩放则能模拟植保无人机在不同作业高度看到的尺度变化。这些思路要落到具体参数上:

# crop_weed.yaml path: /your_path/crop_weed_yolo train: train/images val: val/images nc: 2 names: ['crop', 'weed']
yolo detect train \ data=crop_weed.yaml \ model=yolov8s.pt \ imgsz=640 \ epochs=200 \ batch=16 \ optimizer=AdamW \ lr0=0.001 \ fliplr=0.5 \ scale=0.3 \ hsv_h=0.02 hsv_s=0.8 hsv_v=0.6 \ mosaic=1.0 \ patience=30 \ seed=42 \ device=0

参数含义拆开说:fliplr=0.5是水平翻转概率;scale=0.3是缩放比例范围,表示每张图训练时随机缩放到原尺寸的 0.7~1.3 倍;hsv_h不建议调大,作物和杂草的绿色相差异本来就弱,色调抖动太猛会把两者的边界抹掉;mosaic=1.0表示每轮训练有一定比例样本走马赛克拼接。这组参数是我在小样本农业数据上比较常用的起点,而不是抄来的默认配置。

另外,YOLO 内置增强像一个相对固定的黑匣子,它没有专门针对“逆光、阴影、泥土反光”的农业光照退化模型。如果训练效果卡住,常见做法是额外引入 Albumentations 的RandomBrightnessContrast和CLAHE,把亮度、对比度再主动抖一遍。200 张图的数量级下,增强强度宁可激进一点,也不要保守。

4.3 训练超参和损失曲线:小数据集看哪几个关键输出

训练命令本身不复杂,复杂的是知道每个输出意味着什么。我一般从 yolov8s 起步,200 张图用不上大型模型;参数量越大,越容易在几十个 epoch 内把训练集整个背下来。你看到 val 的 loss 曲线从下降转为抬头,就说明开始背了。

训练时要盯两样东西,一个是train/box_loss和val/box_loss的走势,另一个是mAP50。yolo 算法的损失函数里有 box、cls、dfl 好几个分量,小样本场景下我最关注的就是 box_loss——它下降但 val box_loss 反弹,是过拟合的第一个信号;mAP50 从头到尾不涨,就不要反复调超参浪费时间了,先回去看标注和类别定义,大概率是数据本身有硬伤。

早停参数patience=30也不要省,验证集指标连续 30 个 epoch 不涨就自动停,能帮你规避掉过拟合的后半程;seed=42固定随机种子,是为了不同次训练之间能公平对比,这一条容易被忽略,但真对比增强配置时没有固定种子,结论根本没法信。

5. 小样本作物杂草数据集的常见坑与排查

5.1 过拟合:训练 loss 一路下滑,验证 loss 却抬头的翻车现场

现象:前 30 个 epoch 训练 loss 降到 0.02,验证 loss 从 0.1 掉头涨到 0.3,mAP 不再升高。

原因:200 张图里目标形态高度重复,模型把训练集里的个体纹理记住了,没有学到作物和杂草真正的判别性差异。参数量越大、增强越弱、epoch 拉得越长,这种情况就越严重。

解决:先做一次关闭预训练权重的对照实验。如果从零初始化反而更稳定,说明预训练特征被过度微调了;同时把增强强度往上提,scale从 0.3 提到 0.5,hsv_v提高亮度扰动,让网络看不到两张完全相同的图。记住,在小样本上,加增强比加模型容量管用得多。

5.2 小目标漏检:杂草幼苗比指甲盖还小

现象:验证集 mAP50 有 0.8,mAP50-95 却不到 0.3,可视化图里框全打在作物大叶片上,小杂草几乎没检出。

原因:杂草幼苗占图面积比例极小,YOLO 在 640 输入下特征图多次下采样,小目标特征在后几层已经被压没。这是小目标检测的通病,和算法实现关系不大。

解决:第一优先把imgsz提到 1280 或 1536,显存不够就切图训练;第二是检查训练集里有多少目标框宽度或高度小于图像尺寸的 3%,如果占比高,先用带 overlap 的切片把大图切成 640 patch 再训练,相当于人为把小目标放大。具体切法在第 6 章展开。

5.3 类别不平衡:杂草 180 个框,作物只有 30 个框

现象:训练结束后,模型几乎把所有目标都判成杂草,作物类别的 F1 不到 0.4。

原因:类别数量差距超过 5 倍时,分类损失被多数类主导,少数类样本梯度被淹没。

解决:第一优先级是补样本,哪怕找几十张只有作物的无人图像也比降权强;不能补样本就做最简单的过采样——复制少数类图像 2~3 份进训练集,即使图像重复,也能在损失函数里抬高少数类占比。另一个更务实的做法是合并输出,先只做作物和杂草的二分类,把兴趣区域检出来,再通过后处理阈值区分。少类别数,永远是小样本数据的第一选择。

5.4 标注噪声:连拍图像里同一个目标框忽大忽小

现象:训练时某个样本的 loss 始终居高不下,排查后发现同一株杂草在相邻两张图上的框大小差 2 倍,甚至类别相反。

原因:小数据集的标签往往是半自动工具生成的,连拍图像里目标位移、遮挡变化导致标注不一致。这种噪声在 200 张规模里占比很容易超过 5%,而 5% 的错标足以让 mAP 掉 3 到 5 个点。

解决:用一个刚训好的模型去回灌训练集,把“预测框和标注框 IoU 低于 0.5 但置信度很高”的样本单独挑出来让肉眼复查。这种半自动自查比逐张翻图快得多,我的做法是把可疑样本集中到一个to_check文件夹,逐张决定改标签、删样本还是保留。

5.5 工程坑:zip 解压后的中文路径、重名文件和隐藏目录

现象:训练到一半报FileNotFoundError,或者数据加载卡死,排查发现路径里有中文、空格,有的文件解压后带了不可见前缀。

原因:数据加载器对 unicode 路径支持不稳定,压缩包内嵌套同名目录也会让路径错乱。

解决:一开始就把项目路径固定为纯英文无空格;解压后先跑一遍find . -type f | awk -F/ '{print $NF}' | sort | uniq -d检查重名,发现有重名就用编号批量重写文件名。这些事看起来琐碎,但踩过坑的人都知道,它们耽误的时间比调参还多。

6. 把 200 张图用成 2000 张:滑窗切片的实际做法

最后一个技巧,是把高分辨率大图拆成密集小图来训练和推理。这个思路常被叫作滑窗切片,也有人叫 it 切图增强,核心原理完全相同:把 2000x2000 以上的田间大图按步长切出一批 640x640 的 patch,每个 patch 单独送进模型推理,最后把所有 patch 的框映射回原图坐标做一次合并。

切片参数上,patch 尺寸最好和训练时的imgsz保持一致,overlap 设在 0.2,步长为 640 乘以 0.8 即 512 像素。overlap 太低会把目标从中间切碎,太高会让同一个目标在相邻 patch 里重复出框。映射回原图时只需要给每个框加上当前 patch 的偏移量,一行代码的事:

def patch_to_original(boxes, offset_x, offset_y): return [(x1 + offset_x, y1 + offset_y, x2 + offset_x, y2 + offset_y) for x1, y1, x2, y2 in boxes]

我个人的习惯是把它做成“反向增强”:不只推理时切片,训练阶段也把大图按同样 patch 尺寸切成 640 小图再训练,标注框跟着一起切。这样做有两个直接收益,一是小目标在输入层面被放大,二是 200 张图每张切出 9 到 12 个不重叠 patch,样本量相当于扩到 1800+,训练和推理的分布还保持一致。代价是单个 epoch 变长,而且目标恰好骑在 patch 边缘时会被丢掉——这一点我通常接受,因为 overlap 合并时相邻 patch 会把它补回来。

坐标取舍上,我用的规则很简单:目标框中心点落在当前 patch 内就保留,否则丢弃。这个规则比用 IoU 判断省事得多,也够稳。选择合适参数后,整个方案能直接把小目标漏检压下去一个档次,比任何网络结构改动都来得直接。

希望这份从解压到切图推理的流程能帮到你,尤其是那些跟我一样手里数据少、还想把 mAP 再往上提一提的人。还是那句话:小样本的突破口往往不在模型结构,而在你愿不愿意把数据处理和增强做到位。

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

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

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

立即咨询