
简介这份数据集面向目标检测与农业视觉应用场景适合正在学习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 \ datadata.yaml \ modelyolov8n.pt \ epochs60 \ imgsz640 \ batch16 \ lr00.001 \ patience20 \ device0命令行里的参数直接影响训练成功率和最终精度我按杂草项目的特点解释几个imgsz640杂草和大田作物的俯视图像通常来自无人机或行间相机目标尺度偏小。如果标注框的绝对像素普遍小于32像素640下会进一步缩小。先跑一组640拿baseline再对比一组960或1280。显存够就优先用大尺寸。batch16batch大小跟显存直接挂钩。8GB显存跑YOLOv8n、640输入batch16基本是上限更强的模型如YOLOv8m直接减半。发现训练进程被CUDA out of memory杀掉第一个动作是把batch调小别去动imgsz。lr00.001迁移学习起步常用值。如果你发现loss曲线前几个epoch剧烈震荡甚至出现nan降到0.0005再试。有的人喜欢从0.01开始但那是COCO随机初始化的大模型玩法农业数据集样本少开大学习率很容易让预训练权重崩掉。如果不想用命令行Python脚本的写法也很常见from ultralytics import YOLO model YOLO(yolov8n.pt) results model.train( datadata.yaml, epochs60, imgsz640, batch16, lr00.001, cos_lrTrue, patience20, workers4, device0, )这段里面cos_lrTrue是按余弦曲线衰减学习率样本量不大时通常比阶梯式衰减稳定收敛更平缓。patience20表示验证集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()), kint(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每个格子负责预测固定尺度的目标。当输入图像缩小到64030像素的小目标在特征图上的响应已经弱到几乎不可分辨尤其伴随遮挡和叶片重叠的情况。解决两个方向选一个。显存允许时直接加大imgsz到960或1280很多小目标问题靠这一步就能改善。显存不够就把原图切块每块提高目标相对尺寸。切块训练时每块图像独立标注推理时再用nms合并结果。切块的具体做法是先把原始大图切成4片分别作为一张图训练同时把对应位置的标注归一化到各切片坐标系中。过程麻烦但无人机大田数据常需要这一招。4.3 图像EXIF旋转导致标签错位loss降不下去现象训练了20个epochloss一直在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后训练速度惨不忍睹。原因很多教程默认batch32甚至64完全不照顾8GB显存用户。YOLO训练框架在显存不足时不会自动回退只会硬报错。解决先按batch8试能跑再往上加。模型先别换YOLOv8n在显存占用和速度上都最稳。若batch8还报错排除一下是不是开了多线程数据加载导致内存峰值太高把workers从8降到2。此外开启自动混合精度能省不少显存yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs60 \ imgsz640 \ batch8 \ ampTrue \ device0ampTrue就是混合精度训练在绝大多数情况下不掉精度对农业数据尤其值得开。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(datadata.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 modelruns/detect/train/weights/best.pt \ sourcedataset/images/val \ save_txtTrue \ save_confTrue \ conf0.25然后重点检查预测结果中置信度0.25到0.5之间的检测框。这些框往往对应草叶重叠、阴影遮挡、叶片边缘模糊的真实分布也是模型能力边界所在。判断标准很简单人眼能不能认出这里的杂草能认出而模型丢了这个目标说明特征学习的空间还不够人眼也认不出那就暂时不要强求模型。6. 用验证集反推数据缺口开启下一轮标注迭代6.1 把漏检图像做成一个“难例回填”清单模型训练到可以用之后工作还没完。对农业项目来说模型一定会在某个光照、某个生育期翻车而这个翻车点往往对应着数据集的分布缺口。我的做法是把验证集推理结果中所有假阴性的图像copy到一个单独目录重新标注或追加标注定期扩充回训练集。不需要一次标很多每周追加50到100张模型的边界就会一点一点往前推。如果数据包里只有2,722张图不要指望一轮训练就把所有问题解决。第一轮训练后检查出来的空标签、误标和大目标漏标才是这个包真正的价值——它们指向的要么是标注质量问题要么是场景覆盖不足继续迭代的基础就在这里。6.2 保持一个固定测试集别随手改随机种子我见过最多的效率杀手是同学改随机种子重新划分数据。今天用seed42明天改成2024训练结果翻来覆去没法定量比较。正确做法是选定一个划分后固定下来锁死在data.yaml对应的目录里后续所有代码改动、参数调整都以这个固定划分做对比基准。这样每轮迭代的差异才真实反映模型变化而不是数据波动带来的假象。我自己习惯把每个训练轮次的预测结果保存一份到带时间戳的文件夹方便周末统一回看。做过三轮之后你会发现哪个类别的AP涨了、哪批漏检消失了、哪些图始终学不会比任何训练日志都清楚。项目做到后期最有用的经验就是这些按周累积的验证记录。希望这个2830张图像的数据包迭代思路能帮到你照这条路线走做出能下地的杂草检测模型并不遥远。本文还有配套的精品资源点击获取