ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

土豆病害检测数据集实战:VOC转YOLO、标签校验与类别不平衡处理

土豆病害检测数据集实战:VOC转YOLO、标签校验与类别不平衡处理

简介:这份文档面向农业计算机视觉研究与土豆病害识别开发者,围绕8908张图像、5类土豆状态构建的VOC与YOLO双格式检测数据集展开说明,可用于早期病害筛查、品质分级等目标检测模型训练场景。压缩包共1个docx文件,约3.71MB,以文字形式汇总数据集结构、类别定义、标注规范与使用注意事项,便于在正式训练前评估数据是否匹配任务需求。文档列出正常土豆、损伤、缺陷、真菌性病害与萌芽五类,合计30854个矩形标注框,其中正常土豆9868框、真菌性病害7674框,并交代jpg、xml、txt三类文件的对应关系及labelImg标注流程,附图片预览与标注示例,帮助读者核对类别分布、标注粒度与样本覆盖情况。已有68人学习,适合需要快速了解该数据集构成、增强情况与适用边界的研究者和工程人员参考。

1. 土豆病害检测数据集:8908 张图、30854 个框,先看清它的边界

拿到一份农业视觉数据集,我第一件事不是跑训练,而是翻标注。这份土豆病害检测数据集标称 8908 张 jpg、8908 个 xml、8908 个 txt,VOC 与 YOLO 双格式并存,5 个类别合计 30854 个矩形框。数字看着漂亮,但真正决定它能不能用的,是类别定义和框的分布——这决定了你训出来的模型是能分病害,还是只会认“这是个土豆”。

它适合三类人:做农业视觉课题需要现成交叉格式数据的研究者、想在 YOLO 系模型上快速验证小目标密集检测的工程师、以及要交付一个能跑通的病害识别 Demo 的开发者。不适合指望“下载即高精度”的人,数据集本身明确不保证精度,这一点必须提前摆到桌面上。

2. 五个类别与框数分布:先算清这份数据集的账

2.1 类别定义决定任务难度

先把五个类别摊开看。Damaged potato(机械损伤)、Defected potato(外观缺陷)、Diseased-fungal potato(真菌性病害)、Potato(正常)、Sprouted potato(发芽)。这五类里,前三类属于“病害/损伤”,第四类是负样本基准,第五类是储藏期问题。注意,这是一个目标检测任务,每张图上可能同时出现多个类别的土豆,模型输出的是框和类别,不是整图分类标签。

类别名中文含义框数占比
Potato正常土豆986831.98%
Diseased-fungal potato真菌性病害767424.87%
Damaged potato机械损伤485015.72%
Sprouted potato发芽450914.61%
Defected potato外观缺陷395312.81%
合计—30854100%

平均每张图 3.46 个框,说明多数图是多目标场景,不是单目标抠图。这种密度对 YOLO 系列很友好,但也意味着小目标比例可能不低——土豆在画面里占比小的时候,框会非常密。

选型理由:如果你的任务只需要区分“正常/异常”,五类合二为一即可,直接改 data.yaml 的 nc=2,不用重标。如果要细分病害类型,就必须接受 Defected potato 只有 3953 个框这个事实,它是弱类,后文会讲怎么补。

2.2 单类框数与图片数的关系

数据集没有给“每类出现在多少张图里”,只有框数。这个差别很关键:9868 个正常土豆框可能集中在 4000 张图上,也可能散在 8000 张图上。你要自己统计,方法在 3.2 节给脚本。我一般会先跑一遍分布,如果某类图片覆盖率低于 15%,训练时就要考虑过采样。

提示:数据集说明里提到“有增强”,但没给增强前后的对照。拿到手先看图片尺寸是否统一、是否存在明显翻转/裁剪痕迹,这直接影响你划分验证集的方式——如果增强图与原图混在一起,随机划分会导致验证集泄漏。

2.3 格式说明与不含内容

VOC 侧给的是 jpg+xml,YOLO 侧是 jpg+txt,不包含分割路径的 txt 文件,也没有 ImageSets/Main 那套划分文件。也就是说,train/val 的切分要自己做,别指望解压就有现成的 train.txt。标注工具是 labelImg,规则是矩形框,没有多边形、没有关键点。这一点要在项目立项时就写进假设,否则后面写 dataloader 会因为找不到分割文件报错。

3. 从 VOC 到 YOLO:目录结构、转换校验与训练接入

3.1 目录怎么摆,先立规矩

拿到压缩包解压后,常见结构是图片一个目录、xml 一个目录、txt 一个目录。我一般会重排成标准 YOLO 结构,避免训练脚本里到处写绝对路径:

# 建立标准结构,名称按项目自定 mkdir -p potato/{images,labels} # 图片统一放 images,txt 统一放 labels,文件名 stem 必须一致 # 假设原始目录为 raw/jpg 与 raw/txt for f in raw/jpg/*.jpg; do base=$(basename "$f" .jpg) cp "$f" potato/images/"$base".jpg cp raw/txt/"$base".txt potato/labels/"$base".txt done

逻辑说明:YOLO 系训练器靠“图片路径替换 images 为 labels、后缀换 txt”来找标签,stem 一旦对不上就静默丢样本,这是最常见的坑。参数说明:目录名 images/labels 是约定,不用改;basename去掉后缀保留 stem,是保证对齐的关键。

3.2 校验脚本:先证明标签没坏

在训练前跑一遍完整性检查,比训到一半发现 loss 不降要省时间得多。下面这段扫描标签目录,检查类别 id 范围、坐标归一化、空文件与孤儿标签:

import os from pathlib import Path img_dir = Path("potato/images") lbl_dir = Path("potato/labels") NC = 5 # 类别数,与 data.yaml 一致 bad_cls, bad_coord, empty, orphan = [], [], [], [] for lbl in lbl_dir.glob("*.txt"): stem = lbl.stem if not (img_dir / f"{stem}.jpg").exists(): orphan.append(stem) # 有标签没图片 continue lines = lbl.read_text().strip().splitlines() if not lines: empty.append(stem) # 空标签,可能是纯背景图 continue for ln in lines: p = ln.split() if len(p) != 5: bad_coord.append((stem, ln)); continue cid = int(p[0]) if cid < 0 or cid >= NC: bad_cls.append((stem, cid)) xs = list(map(float, p[1:])) if any(v < 0 or v > 1 for v in xs): bad_coord.append((stem, ln)) print("orphan:", len(orphan), "empty:", len(empty)) print("bad_cls:", len(bad_cls), "bad_coord:", len(bad_coord))

逻辑说明:先查图片存在性,再查每行字段数与类别 id 合法性,最后查坐标是否落在 0~1。参数说明:NC=5必须和后面 data.yaml 的 nc 对齐;空标签文件不是错误,纯背景图在检测任务里合法,但要统计数量,占比过高说明数据里有大量无目标图。

跑完你会得到几个数:孤儿标签数、空标签数、越界坐标数。孤儿标签直接删或补图,越界坐标通常是标注时拖出了边界,可以裁剪到 [0,1] 而不是丢弃整张。

3.3 data.yaml 与训练命令

# potato.yaml path: /abs/path/potato train: images/train val: images/val names: 0: Damaged potato 1: Defected potato 2: Diseased-fungal potato 3: Potato 4: Sprouted potato

注意类别 id 的映射顺序必须与 txt 里的数字严格一致。这份数据集只给了类别名列表,没给 id 映射,转换脚本按列表顺序 0~4 写入是最常见做法;如果你手上的 txt 是别人转的,务必用上一节脚本抽样打印几行,对照 xml 里的 name 反查,别默认它一定对。

划分训练/验证集时按 8:2 或 7:3,按图片 stem 划分,绝不要按行划分,否则同一张图的不同框会同时进训练和验证,指标虚高。抽样命令:

# 打乱后切分,stem 级别 ls potato/images/*.jpg | shuf --random-source=<(yes) > all.txt head -n $(( $(wc -l < all.txt) * 8 / 10 )) all.txt > train.txt tail -n +$(( $(wc -l < all.txt) * 8 / 10 + 1 )) all.txt > val.txt

然后启动训练,YOLOv8 系常见写法:

yolo detect train data=potato.yaml model=yolov8s.pt epochs=100 imgsz=640 batch=16

参数说明:imgsz=640是默认,如果原图分辨率高且土豆在图中占比小,可以上到 960 甚至 1280,但显存要跟上;batch按显存调,24G 卡上 640 分辨率跑 s 模型 16 一般没问题。训练日志里重点看每类的 mAP50,别只看总 mAP。

4. 类别不平衡与增强陷阱:这份数据集的三个暗坑

4.1 弱类不是靠调学习率救的

Defected potato 3953 个框,对比正常类 9868,比例约 1:2.5,还不算极端失衡,但在多类检测里足够让弱类召回上不去。我一般不会一上来就改 loss 权重,而是先做两件事:一是统计弱类在多少张图里出现,二是看弱类的框尺寸分布。如果弱类框普遍偏小,问题在分辨率而不在采样。

补法上,优先用按类过采样:复制含弱类的图片进训练集,同时复制对应标签,文件名加后缀避免覆盖。不要对验证集做任何过采样,否则指标不可信。

import shutil, random from pathlib import Path lbl_dir = Path("potato/labels") img_dir = Path("potato/images") WEAK = {1} # Defected potato 的类别 id targets = [] for lbl in lbl_dir.glob("*.txt"): for ln in lbl.read_text().splitlines(): if ln and int(ln.split()[0]) in WEAK: targets.append(lbl.stem); break for stem in targets: for i in range(2): # 复制两份,按需调 shutil.copy(img_dir / f"{stem}.jpg", img_dir / f"{stem}_os{i}.jpg") shutil.copy(lbl_dir / f"{stem}.txt", lbl_dir / f"{stem}_os{i}.txt")

逻辑说明:按 stem 找出含弱类的图,复制图片和标签,stem 加后缀保证一一对应。参数说明:WEAK集合按你实际的类别 id 填;复制份数从 1~2 起步,过量会让模型过拟合这批图。

4.2 “有增强”是个黑匣子

数据集说明写了“有增强”,但没写增强方式。这带来两个风险:一是增强图可能和原图高度相似,划分时若随机打散,验证集里会出现训练图的近邻,指标虚高;二是增强可能改变了目标形态(比如过度旋转导致框失效),但标签没重算。

应对方式:训练前抽样 50 张看图,重点看是否有同一场景的近似重复;如果有,划分验证集时按“场景/来源”分组,而不是纯随机。这一步没有脚本能替你做,只能人眼看。

注意:不要因为数据集声称已增强就关掉训练时的在线增强。在线增强是每个 epoch 随机的,和离线增强不冲突;关掉反而容易过拟合。

4.3 五个类别的语义重叠

Damaged(机械损伤)和 Defected(外观缺陷)在像素层面经常难分,标注者自己也未必一致。这两类混淆会直接拉低 mAP。我的做法是先训一版,导出混淆矩阵,如果这两类互错率超过 30%,就考虑合并成一个“异常”类再训一版做对比,而不是硬调阈值。

yolo detect val model=runs/detect/train/weights/best.pt data=potato.yaml plots=True

参数说明:plots=True会输出混淆矩阵和 PR 曲线到 runs 目录,重点看对角线之外的块。如果某一类的 PR 曲线在低召回区就掉下去,说明该类样本不足或标注噪声大。

5. 避坑与排查:五条踩过的记录

现象一:训练时提示找不到标签,loss 一直是 nan 或 0。原因:图片和标签的 stem 不一致(比如图片是1.jpg、标签是1.txt但中间多了空格或大小写差异),训练器静默跳过。解决:跑 3.2 的校验脚本,把 orphan 列表打印出来逐个对;批量修名用rename统一小写。

现象二:训练正常但验证 mAP 高得离谱,接近 0.99。原因:增强图与原图混在一起后随机划分,验证集泄漏。解决:按场景分组划分,或先用图片哈希去重再切分。

现象三:Defected potato 的召回始终在 0.3 左右上不去。原因:弱类样本少且框偏小,640 分辨率下特征被压缩。解决:先过采样补到 6000 框量级,再把 imgsz 提到 960 试一版,两者叠加通常能拉到 0.5 以上;仍不行就检查该类标注是否有大量漏标。

现象四:转换后坐标越界,训练报 “non-normalized coordinates”。原因:labelImg 标注时框拖出图像边界,xml 里 xmax 大于图宽。解决:转换脚本里对坐标做 clamp 到 [0,1],而不是整图丢弃;同时统计越界比例,超过 5% 说明标注流程有问题。

现象五:模型在验证集上表现好,换一批实拍图就崩。原因:数据集场景单一(同背景、同光照),域偏移大。解决:训练时开 mosaic、HSV 抖动、随机缩放,验证时另留一组不同来源的图做域外测试,别只信同分布验证集。

6. 把五类合成两类的迁移技巧:先跑通再细分

很多人拿到这份数据集就想直接训五类,结果卡在弱类上反复调参。我的习惯是先做一次类别坍缩实验:把 Damaged、Defected、Diseased-fungal、Sprouted 合并为“异常”,Potato 保留为“正常”,得到一个二分类检测任务。这个版本几乎一定能跑出可用的基线,用来验证数据管线、增强策略、分辨率设置是否正确。

# 五类转二类:0 正常,1 异常 import shutil from pathlib import Path src_lbl, dst_lbl = Path("potato/labels"), Path("potato/labels_bin") dst_lbl.mkdir(exist_ok=True) for lbl in src_lbl.glob("*.txt"): out = [] for ln in lbl.read_text().splitlines(): if not ln: continue p = ln.split() cid = 0 if int(p[0]) == 3 else 1 # 3 是 Potato out.append(" ".join([str(cid)] + p[1:])) (dst_lbl / lbl.name).write_text("\n".join(out))

逻辑说明:只改类别 id,坐标原样保留,图片目录复用。参数说明:3是正常类的 id,按你实际映射改;生成新标签目录后,data.yaml 的nc: 2、names改成两项,训练命令不变。

基线跑通后,记录下 mAP50 和每类召回,再切回五类做对比。如果五类版本的异常类召回明显低于二类版本的异常召回,说明细分带来的类间混淆是主要瓶颈,这时候再去考虑合并语义相近的类,或者补标注,而不是盲目加 epoch。

验证方法上,我一般会固定三件事:固定随机种子、固定验证集划分文件、固定 imgsz。每次只改一个变量,把结果记在表格里,避免“调了一堆参数不知道哪个起作用”。导出模型后,拿十张完全没在数据集里出现过的实拍图做人工过一遍,看漏检是集中在某一类还是均匀分布——集中说明该类样本不足,均匀说明分辨率或增强不够。

从那以后我每次拿到这类农业检测数据集,都强制先跑一遍标签校验、再做一次类别坍缩基线,两个数对上了才敢往五类上堆训练时长。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进