ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

水稻虫害检测数据集:VOC与YOLO双格式标注及YOLOv8训练指南

水稻虫害检测数据集:VOC与YOLO双格式标注及YOLOv8训练指南 简介面向水稻虫害智能检测与农业AI应用开发者这份数据集说明文档整理了“水稻虫害检测数据集VOCYOLO格式5212张6类别”的核心信息。资源包共1个文件为docx格式大小约2.08MB以文字说明形式呈现便于快速查阅数据集的格式规范与统计概况。文档中详细介绍了Pascal VOC与YOLO两种标注格式的组合使用方式以及labelImg标注工具的应用明确了5212张jpg图片、5212个XML与5212个TXT标注文件的位置结构并列出6个害虫类别褐飞虱、绿叶蝉、稻纵卷叶螟、稻水象甲、茎螟、螺纹虫总计8104个矩形框。同时给出每类框数分布帮助研究者评估类别平衡程度。目前已有121人学习浏览适合需要快速了解该数据集构成、准备训练数据或进行农业病虫害检测模型开发的读者。文档不保证模型精度但可作为数据选型与预处理阶段的重要参考。1. 水稻虫害检测数据集5212张双格式样本到底能帮你省下多少事做农业检测项目的人第一个坑往往不在模型结构而在数据格式。有人拿到的数据集只有VOC格式的XML得自己写脚本转成YOLO有人只给YOLO的txt想验证标注对不对还得把归一化坐标回算成像素框。这套水稻虫害检测数据集一次给出5212张图像、6个类别、VOC与YOLO两份标注等于把最常见的数据格式问题提前解决了一半。它适合正在做农情监测、植保无人机、病虫害识别或想用YOLO入门学习目标检测的工程师也适合需要一份现成数据来验证模型的从业者。接下来按一条完整落地路径展开先讲两种格式怎么读再讲训练前如何组织目录与配置最后给出转换脚本和四个避坑点。2. VOC与YOLO标注格式在讲什么为什么一份水稻虫害数据要出两份标注2.1 VOC格式的XML像素坐标存储与结构解析VOC全称是PASCAL VOC在目标检测领域已经成为一种通用标注交换格式。它的核心是一个XML文件与同名图片放在一起里面记录图片尺寸、通道数量以及所有目标的类别名称和边界框。以一张1920×1080的田间照片为例假设图中有一只虫害框的左上角与右下角分别是(340,260)和(510,430)XML大致长这样annotation folderimages/folder filenamerice_pest_00001.jpg/filename size width1920/width height1080/height depth3/depth /size object namerice_leaf_roller/name bndbox xmin340/xmin ymin260/ymin xmax510/xmax ymax430/ymax /bndbox /object /annotation这个XML里有四个要点需要单独讲。第一filename字段只存文件名不存完整路径路径完全靠运行时拼接所以目录结构一变所有引用都会失效。第二size节点里的width和height是后面转YOLO格式时的分母如果原始图片被resize过但XML没有同步更新坐标系会整体错位。第三每个object块是一个目标实例同一张图有多个虫害时会出现多个object块类别名以字符串形式存储VOC阶段并不强调顺序。第四bndbox里存的是绝对像素坐标xmin、ymin、xmax、ymax分别对应矩形框左、上、右、下四条边的像素位置取值不能超出图像宽高。VOC格式的优点是人可读、工具支持广labelImg、Labelme、CVAT都能直接导出或识别。缺点是训练框架几乎不原生支持必须在数据加载阶段写XML解析逻辑或者提前转换成YOLO格式。这也是为什么很多数据集发布方会在VOC之外再转一份YOLO标注让两条技术路线的用户都能直接开工。2.2 YOLO格式的TXT归一化坐标与类别索引YOLO格式则是一个txt文件对应一张图每一行表示一个目标共五列类别索引、中心点x、中心点y、框宽、框高。四个坐标值全部相对于图像宽高做了归一化取值范围在0到1之间。把刚才的XML换算成YOLO格式框宽 510 - 340 170像素除以1920约0.0885框高 430 - 260 170像素除以1080约0.1574中心点x (340 510) / 2 425像素除以1920约0.2214中心点y (260 430) / 2 345像素除以1080约0.3194最终txt里这一行写作0 0.2214 0.3194 0.0885 0.1574开头的0是类别索引对应类别清单里的第一个类。这里能看出YOLO格式的关键约束类别不是名字而是序号类别清单的顺序必须全局统一训练配置里写错一个位置整个模型学到的就是错位标签。YOLO格式之所以被广泛接受是因为读取成本低一行一个目标不用解析树结构归一化坐标也让模型不依赖具体分辨率。代价是人眼直接看txt根本看不出框在哪必须回算成像素坐标这也引出后面可视化检查的必要性。2.3 同一份数据为什么给两份标注同步风险与应对思路数据集发行方把VOC与YOLO同时给出通常是标注工具直接导出了VOC再批量转了一份YOLO给训练脚本用。对使用者来说这份双保险并不一定保险因为两份文件是静态保存的一旦标注人员手工改过其中一份另一份不会自动跟着变。我拿到这类数据集的第一件事不是急着训练而是先跑一个差异脚本把每张图的XML与TXT目标数量拉出来对比看看两份标注是否真的对应。import xml.etree.ElementTree as ET from pathlib import Path def voc_object_count(xml_path: Path) - int: root ET.parse(xml_path).getroot() return len(root.findall(object)) def yolo_object_count(txt_path: Path) - int: if not txt_path.exists(): return 0 return len([line for line in open(txt_path, encodingutf-8) if line.strip()])实际项目中更推荐只以其中一份为唯一标准。我一般以YOLO的txt为准因为训练脚本直接读它VOC的XML只在抽检时用用来验证txt里的坐标是否正常。如果两份标注对不上说明发布前没有做批量校验这种数据集的可用度要打个折扣后续训练结果也要谨慎解读。3. 用YOLOv8训练自己的数据集目录组织、data.yaml与首个训练命令3.1 目录重组把VOC与YOLO文件统一进images加labels结构主流的YOLO系训练框架比如YOLOv8对数据目录有一个共同约定图片与标签分开图片放在images下同名txt放在labels下train和val各自分好。很多数据包下载后并没有按这个结构摆放所以第一步永远是重组目录。import random import shutil from pathlib import Path src Path(rice_pest_raw) # 拿到手的数据根目录 dst Path(rice_pest_yolo) # 重组后的目标目录 for split in (train, val): (dst / images / split).mkdir(parentsTrue, exist_okTrue) (dst / labels / split).mkdir(parentsTrue, exist_okTrue) jpgs {p.stem: p for p in src.rglob(*.jpg)} txts {p.stem: p for p in src.rglob(*.txt)} common sorted(jpgs.keys() txts.keys()) random.seed(42) random.shuffle(common) val_count int(len(common) * 0.15) for idx, stem in enumerate(common): split val if idx val_count else train shutil.copy(jpgs[stem], dst / images / split / f{stem}.jpg) shutil.copy(txts[stem], dst / labels / split / f{stem}.txt)脚本的逻辑是先把jpg和txt按主文件名建索引rglob递归扫描不管原始目录套了几层都能找到再求主文件名交集自动丢弃没有标签的图片最后按固定随机种子划分训练集与验证集。这里shutil.copy而不是move目的是保留原始数据包万一后面发现某个环节错了能重新跑一遍等于留了一份后悔药。参数说明15%的验证集比例是我常用的起点5212张图大约对应780张验证图对6个类别来说足够观察主流分布。如果某个类别特别稀少这个比例可能不够最后一章会讲分层抽样。如果数据包已经自带train和val划分就不必跑这个脚本但要额外检查labels目录下是否混入了XML文件YOLO训练器只认txt混入的XML会被当成该图没有标签。3.2 六类别名称与class id映射data.yaml的写法YOLOv8的数据配置放在一个yaml文件里核心内容只有三块根路径path、训练验证集相对路径、names类别清单。典型写法是# rice_pest.yaml path: /绝对路径/rice_pest_yolo train: images/train val: images/val # 6个类别的名字替换为数据包 class.txt 里的实际名称 names: 0: pest_class_0 1: pest_class_1 2: pest_class_2 3: pest_class_3 4: pest_class_4 5: pest_class_5这里的核心点是names里的索引不是给人看的编号而是直接对应txt每行第一个数字。比如某行txt开头是3训练器就会认为这个框属于pest_class_3。如果类别清单顺序是从网上某个项目抄来的而数据包自带的class.txt顺序不同训练照样跑得通验证集mAP也照常计算但最终推理输出的类别名全是错位结果这种错误极难察觉。我拿到一份新数据永远先打开labels目录下任意一个txt看第一行第一个数字再打开对应原图确认框住的虫害与names下标对得上。这一步两分钟的事能省下训练100轮才发现问题的血泪教训。还要提醒yaml里不要写中文文件名部分框架在Windows下读取中文路径容易失败后面避坑章节会细说。3.3 首个训练命令与关键超参选择目录和配置就绪后训练命令很简洁yolo detect train \ datarice_pest.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ patience15 \ projectruns/rice_pest \ namebaseline_s参数这么选的理由model用yolov8s.pt做迁移学习比随机初始化收敛更快更稳显存只有8G就换yolov8n.pt精度会有下降但先把流程跑通最重要。epochs给100配合patience15的早停连续15轮验证集mAP不提升就自动结束不用一直盯盘。imgsz640是入门值水稻虫害中有不少小目标后面排查章节会讲什么时候该提到1280。batch16建立在单卡16G显存的前提下batch翻倍时学习率也要相应上调。device指定GPU多卡时可以写0,1。训练启动后不要只盯训练损失更值得看的是每个epoch结束后的验证集mAP50和mAP50-95。如果50轮后mAP50低于0.5先别急着换模型结构回到第5章按排查顺序找原因大概率是数据或标注问题。4. 从VOC转YOLO格式可复用的转换脚本与4个易踩参数坑4.1 转换脚本逐行拆解XML解析、归一化与越界裁剪虽然这份数据集已经提供了YOLO格式但转换脚本依然值得保留到自己的工具库里——下次换成自采数据或第三方数据时VOC转YOLO仍是最常见的需求。把转换逻辑吃透这份数据就当练习集。import xml.etree.ElementTree as ET from pathlib import Path CLASS_NAMES [pest_class_0, pest_class_1, pest_class_2, pest_class_3, pest_class_4, pest_class_5] def convert_voc_xml_to_yolo(xml_file: Path, out_dir: Path) - bool: if not xml_file.exists(): return False tree ET.parse(xml_file) 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} 在 {xml_file.stem}) continue cls_id CLASS_NAMES.index(name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) cx ((xmin xmax) / 2.0) / img_w cy ((ymin ymax) / 2.0) / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) bw min(bw, 1.0) bh min(bh, 1.0) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) if not lines: return False out_dir.mkdir(parentsTrue, exist_okTrue) (out_dir / f{xml_file.stem}.txt).write_text( \n.join(lines) \n, encodingutf-8) return True这段脚本有四个关键点。第一CLASS_NAMES是全局唯一事实来源转换脚本和训练yaml必须从同一个顺序读取否则类别索引一定错位。第二归一化时必须区分分母x方向的坐标除以img_wy方向的坐标除以img_h混用会导致框整体偏移。第三min和max裁剪不是为了修饰而是处理标注框拖出画布产生的越界值这类值不裁剪会让训练损失变得不稳定。第四坐标保留6位小数已经足够第7位以后对应亚像素级误差对检测框没有实际意义。4.2 转换后如何快速验证画框回看与像素坐标回算转换脚本跑完不能直接开训先做一次可视化抽检。用OpenCV把txt里的归一化坐标回算成像素坐标在原图上画出矩形框和类别名保存到指定目录后人工翻看。import cv2 from pathlib import Path def draw_from_txt(image_path: Path, label_path: Path, class_names, out_path: Path): img cv2.imread(str(image_path)) if img is None: raise ValueError(f图片读取失败: {image_path}) h, w img.shape[:2] with open(label_path, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f[异常] {label_path}: {line}) continue cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (x1, max(y1 - 6, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(str(out_path), img)这里有一个很容易被忽略的细节类别索引转名字时依赖class_names数组画框脚本里的数组顺序必须和训练yaml一致否则你看到的是“绿框配错名”会误判标注质量。抽检数量不必太多每类随机抽10到20张图统一输出到一个logs目录里翻看重点看框是否贴合虫害主体、有没有半个目标被框切掉、有没有两个高度重叠的框指向不同类别。4.3 转换与使用中的4个易踩参数坑第一个坑归一化后宽高大于1。现象是训练loss前期无法下降日志偶尔出现坐标异常警告。原因是标注框拖出图像边界转换脚本没有做裁剪。解决方式就是脚本里加min裁剪同时回到源数据修正治标和治本结合。第二个坑空txt文件。现象是训练时某些图片完全不参与loss计算框架报“No labels found”或静默跳过。原因是原图确实没有目标object块为空转换后txt是0行。解决方式是统计空文件数量如果只有个位数直接剔除对应图片如果很多说明这批数据本身存在漏标需要人工补标。第三个坑中文路径导致读取失败。现象是cv2.imread返回None框架报错找不到图片但文件资源管理器里路径明明存在。原因是OpenCV和部分训练脚本对中文路径支持不完善。解决方式是数据统一放到英文绝对路径下文件名也不要带中文这个习惯能避免大量无意义报错。第四个坑类别映射错乱。现象是模型训练正常推理时输出类别名和实际物体对不上。原因是转换脚本里的CLASS_NAMES和训练yaml里的names顺序不一致。解决方式是把类别清单抽成单独文件比如classes.txt转换和训练都从它读取而不是在两个地方各维护一份列表。5. 训练水稻虫害检测模型注意这4个排查点损失不降、漏检、误检与类别混淆5.1 损失曲线不降先做小数据过拟合实验隔离数据错误与超参问题训练卡住时不要直接把锅甩给模型结构。先看验证集mAP50是否长时间不动再看train loss是否还在降低。yolo损失函数通常拆成box_loss、cls_loss和dfl_loss三部分如果cls_loss一直不降而box_loss正常问题大概率在类别映射或类别不均衡如果box_loss震荡优先查标签坐标质量。排查第一步是小数据过拟合实验。单独建一个只含32到64张图的临时yaml让模型强行去记住这批数据。如果小数据集上loss能快速下降说明数据和标签基本正常问题在超参、学习率或整体样本分布如果小数据集上loss也降不下去那必然是数据错误比如图像与标签没对齐、坐标框异常、类别索引错位。# rice_pest_smoke.yaml path: ./rice_pest_yolo train: images/smoke_train val: images/smoke_train names: 0: pest_class_0 1: pest_class_1 2: pest_class_2 3: pest_class_3 4: pest_class_4 5: pest_class_5Figure out: 这里train和val指向同一个子集是因为我们只要看loss能不能下降不关心泛化指标。训练命令里把patience调大或者直接关掉早停比如patience999epochs给50就够判断。如果这个实验里loss顺利下降再回到全量数据从学习率入手调整。5.2 小目标虫害漏检输入分辨率、锚框策略与切片推理水稻虫害里有一类典型难点是小目标密集稻飞虱、蚜虫这类目标在640分辨率下可能只有二三十个像素漏检几乎必然发生。别先急着换模型先做一次目标尺寸统计把所有标注框的归一化面积分位数算出来。from pathlib import Path import numpy as np stats [] for txt in Path(rice_pest_yolo/labels/train).rglob(*.txt): with open(txt, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue w float(parts[3]) h float(parts[4]) stats.append((w, h, w * h)) arr np.array(stats) print(归一化面积分位数:, np.percentile(arr[:, 2], [10, 50, 90]))如果中位数面积小于0.005意味着在640×640输入下目标边长只有二三十像素这时最直接的调法是imgsz1280训练时间大约翻倍但小目标召回率会明显改善。YOLOv8本身是anchor-free设计不需要像YOLOv5那样重聚类锚框如果用带anchor的模型小目标场景通常把锚框调小、层数调深。对极端小目标光提分辨率还不够可以叠加切片推理把原图切成512×512的重叠块每块独立检测后再把框坐标映射回原图合并结果推理成本上升但漏检明显下降。5.3 误检与背景虚警负样本缺失时不要只调置信度阈值另一种翻车姿势是模型把泥土、枯叶、稻穗误检成虫害conf_thres调到0.7还压不住。原因往往不是阈值太低而是训练集中缺少不含目标的背景图模型没学会“什么都不是”的样本。解决方向是收集一批无虫害的田间背景图加入训练集配空的labels/train/xxx.txt让模型在训练时有机会看到背景图。不建议只动推理阈值把置信度从默认0.25提到0.45确实能压掉一部分虚警但同时会损失真实小目标的召回属于退而求其次。这里有个容易被忽略的点验证集里也要放背景图。如果验证集全是正样本虚警问题会被mAP指标掩盖真正上线时才发现模型对农田背景的误检率高得吓人。5.4 类别混淆与样本不均衡用验证集混淆矩阵定位错分对6类虫害之间出现混淆很正常尤其是幼虫阶段形态接近的类别。先跑一次yolo detect val打开runs/rice_pest/.../confusion_matrix.png看哪两类的互相错分率最高。如果A类被误判成B类而B类样本量远大于A类这不只是特征相似问题还是类别不均衡导致的偏移。解决思路是对稀有类做针对性增强比如提高马赛克增强里掺入该类的概率或者在损失函数里按类别反比加权。更麻烦的情况是标注本身不干净同一只虫在不同图像里分别被标成A类和B类这类错标是模型怎么调都消不掉的。处理方法只有回到标注数据去清洗按图像id列出A、B两类所有框人工复核边界情形。这一步最费时间但也是唯一能根治类别混淆的手段。6. 分层抽样与跨验证集评估把6类别数据集的可信度量化出来前文一直用随机切分train与val对5212张图6个类别的数据随机切分会埋一个隐患占比很低的稀疏类别可能在某次切分里只分到几张验证图评估结果完全随机。我习惯在划分阶段就做分层抽样不是按样本总数分而是按每个类别至少覆盖一定数量来分。核心思路是先统计每张图包含哪些类别把含有最少见类别的图片单独拎出来在这些图片内按比例抽验证集保证稀有类别在验证集里有足够数量再用其余图片补足总体15%的比例。下面是一个简化示例from pathlib import Path from collections import defaultdict import random img_classes defaultdict(set) for txt in Path(rice_pest_yolo/labels/train).rglob(*.txt): with open(txt, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 5: img_classes[txt.stem].add(int(parts[0])) # 假设类别5是样本量最少的先把包含它的图片挑出来 rare_files [k for k, v in img_classes.items() if 5 in v] random.seed(42) rare_val set(random.sample(rare_files, max(1, int(len(rare_files) * 0.2))))真正实现多标签分层分配时要处理一张图包含多个类别的重叠情况代码量会更大这里的思路是先把最少见类别保护起来再用常规切分补齐其余部分。训练时用这套划分结果替换第3章脚本里的random.shuffle部分即可比纯随机切分稳定得多。评估阶段也建议多跑几次。不要只跑一个train/val切分就对外报精度我一般用3个不同random seed生成3份划分分别训练同样配置的模型记录每个类别的AP0.5。如果某个类别在3次实验里波动超过10个百分点说明它的数据在验证集里代表性不足这个类别的精度不能对外承诺需要继续补样本。我自己第一次做这套数据时偷懒用了纯随机切分某个稀有类两次实验AP从0.65掉到0.31还以为是训练翻车后来才发现只是验证集里那个类只分到个位数图片。从此切分永远是分层优先评估永远至少跑三个seed。希望帮到你。本文还有配套的精品资源点击获取
返回列表