
简介面向农业计算机视觉研究与土豆病害识别开发者的数据集说明文档聚焦生长、储存环节中损伤、缺陷、真菌病害、萌芽与正常土豆的识别需求帮助读者在选型、标注理解与模型训练准备阶段快速判断数据可用性。压缩包共1个docx文件约3.71MB以文档形式集中整理数据集格式、五类名称、框数分布、标注工具、重要说明与图片预览便于按模块查阅。文档对应数据集含8908张图像五类为损伤土豆、缺陷土豆、真菌性病害土豆、正常土豆和萌芽土豆提供Pascal VOC的xml与YOLO的txt双格式标注共30854个框其中正常土豆9868框、真菌性病害7674框另含数据增强提示。已有68人学习适合需要复核类别覆盖、标注规模、VOC/YOLO衔接细节以及精度声明与使用边界的研究者与开发者参考。1. 土豆病害检测数据集8908 张双格式标注先搞清楚它能干什么农业巡检场景里最耗时间的环节通常出在标注模型训练本身反而没那么费时间。这份土豆病害检测数据集给的是 8908 张 jpg、8908 个 VOC xml、8908 个 YOLO txt五个类别合计 30854 个矩形框标注工具是 labelImg图片做过增强。类别覆盖正常土豆、损伤、缺陷、真菌性病害、萌芽五种状态基本对应分选线下料口能拍到的那几类情况。它适合两类人一是要快速搭一个土豆分选或仓储巡检原型的开发者二是需要现成标注做迁移学习预训练的算法同学。注意提供方明确写了不对训练精度作保证这意味着后处理、阈值、场景适配仍然要自己扛。下面按“拆包核对 → 类别分析 → 转换训练 → 排错 → 验收”走一遍。2. 拆包先看结构VOC 与 YOLO 三件套的对齐校验和坐标系差异拿到压缩包别急着写训练脚本。这种双格式数据集最常见的崩法不是模型结构选错而是三件套对不齐——图片有、xml 少一个、txt 多一个训练时框架不报错只是悄悄少读几百张图最后指标怎么调都上不去。先把目录理清楚再用脚本把 8908 组文件挨个点名。2.1 压缩包解压后应该看到什么按正文描述这份数据集只包含 jpg 图片、VOC 格式 xml、YOLO 格式 txt不包含分割路径的 txt 文件。也就是说没有train.txt、val.txt这类索引清单划分要自己做。我一般会先把目录整理成下面这样再动手potato/ ├── images/ # 8908 张 jpg ├── annotations/ # 8908 个 VOC xml ├── labels/ # 8908 个 YOLO txt └── preview/ # 图片预览与标注示例如果原始包是 jpg 和 xml 混在一个文件夹里先写个脚本把扩展名分流别手工拖拽。文件名主干stem是后面所有对齐操作的唯一键任何重命名都要保证 jpg、xml、txt 三者同步否则校正起来很麻烦。有个细节容易被忽略交付方提到“数据集有增强请认真查看图片预览”。增强图通常会以新文件名混进同一目录文件名可能带_aug、_copy之类的后缀也可能完全随机。先翻十几张预览图确认增强方式是大致旋转、翻转、亮度扰动还是把目标贴到不同背景上。这一步决定后面 3.2 节怎么处理重复场景。2.2 用一段脚本核对 8908 组三件套下面这段不依赖任何第三方库纯标准库跑完。它的作用是输出三个集合的差集差集为空才算对齐。import os from pathlib import Path # 按实际解压路径改这三行 IMG_DIR Path(potato/images) XML_DIR Path(potato/annotations) TXT_DIR Path(potato/labels) # 用文件名主干作为唯一键忽略扩展名大小写 imgs {p.stem for p in IMG_DIR.iterdir() if p.suffix.lower() in (.jpg, .jpeg)} xmls {p.stem for p in XML_DIR.iterdir() if p.suffix.lower() .xml} txts {p.stem for p in TXT_DIR.iterdir() if p.suffix.lower() .txt} print(jpg:, len(imgs), xml:, len(xmls), txt:, len(txts)) # 三个方向都查一遍避免只看单向缺失 print(有图无xml:, len(imgs - xmls), sorted(imgs - xmls)[:10]) print(有图无txt:, len(imgs - txts), sorted(imgs - txts)[:10]) print(有xml无图:, len(xmls - imgs), sorted(xmls - imgs)[:10]) print(有txt无图:, len(txts - imgs), sorted(txts - imgs)[:10])脚本逻辑很简单分别扫描三个目录用Path.stem取主干。参数上唯一需要改的是三个路径常量。跑完如果jpg、xml、txt都打印 8908且四个差集都是 0说明三件套一一对应。如果出现“有图无 txt”优先怀疑两件事一是这张图在 VOC 里本来就是负样本不含任何目标xml 里没有 object 节点转 YOLO 时被跳过二是转换脚本中途崩过。负样本要不要保留取决于你的业务——分选线上空图很多留一部分当背景样本能压误检但如果验证集里空图占比过高mAP 会被拉低这点在第 6 章会再提。2.3 VOC 的绝对坐标和 YOLO 的归一化坐标差在哪两套格式描述的是同一个框但写法完全不同。VOC 的 xml 里存的是绝对像素坐标xmin/ymin/xmax/ymax左上角到右下角单位是像素YOLO 的 txt 每行五个数class cx cy w h其中后四个全部除以了图宽图高落在 0 到 1 之间cx cy是框中心点。这两个体系的差异会带来三类实际故障。第一类是边界框贴边VOC 允许xmax等于图宽转成归一化后cx w/2刚好等于 1.0某些框架会因浮点误差算出 1.0000001 而把框裁掉一小截稳妥做法是转换后统一 clip 到[0, 1]。第二类是 1 像素差VOC 的框是闭区间宽高按xmax - xmin算出来会比实际少 1 像素几千个框累加后对小目标有感知但一般不影响训练知道有这回事即可。第三类是坐标系原点labelImg 默认左上角原点如果你后续接了某个要求右下角原点的工具链转换时要额外做一次翻转不要想当然。验证两者是否一致最省事的办法是抽一张图用 PIL 把 YOLO 的归一化坐标还原成像素框和 xml 里的数值对一遍误差在 2 像素以内就算通过。这一步花五分钟能省掉后面一整天的怀疑。3. 五类 30854 个框类别长尾、增强痕迹与数据划分的取舍3.1 先把 30854 个框按类别数出来正文已经给了每类框数Damaged potato 4850、Defected potato 3953、Diseased-fungal potato 7674、Potato 9868、Sprouted potato 4509合计 30854。平均每张图约 3.46 个框说明大部分图片是多目标场景不是单果摆拍。从分布看Potato正常占 32%Defected potato只占 12.8%最大类与最小类相差约 2.5 倍。这个比例在农业数据集里算温和的不需要上来就做重采样但有两个后果要提前知道一是模型会天然偏向把模糊目标判成Potato因为它的先验概率最高二是Defected potato的召回率通常最难做上去验收时要单独看这一类的 AP不能只看总体 mAP。统计脚本可以直接从 xml 里数顺带把每类框的宽高分布也打出来import xml.etree.ElementTree as ET from collections import Counter, defaultdict from pathlib import Path XML_DIR Path(potato/annotations) cnt Counter() sizes defaultdict(list) for xml_path in XML_DIR.glob(*.xml): root ET.parse(xml_path).getroot() size root.find(size) W float(size.find(width).text) H float(size.find(height).text) for obj in root.findall(object): name obj.find(name).text.strip() box obj.find(bndbox) w float(box.find(xmax).text) - float(box.find(xmin).text) h float(box.find(ymax).text) - float(box.find(ymin).text) cnt[name] 1 sizes[name].append((w / W, h / H)) # 归一化后便于跨分辨率比较 for name, n in cnt.most_common(): ws [s[0] for s in sizes[name]] hs [s[1] for s in sizes[name]] print(f{name:28s} 框数{n:5d} 平均宽{sum(ws)/len(ws):.3f} 平均高{sum(hs)/len(hs):.3f})代码里的W、H从每个 xml 的size节点读不要用全局常量因为数据集可能混了不同分辨率的图。归一化宽高能直观看出目标尺度如果多数框的归一化宽高都在 0.5 以上说明是大目标特写imgsz640够用如果大量框小于 0.1就得考虑提高输入分辨率或者切图训练。3.2 增强图的痕迹同一场景重复出现会带来什么原始数据做过增强这是好事也是隐患。好处是样本多样性上去了模型对光照、角度的鲁棒性会好一些。隐患在于如果增强是离线做好的、和原图放在同一个目录那么同一颗土豆、同一个背景可能出现四五次只是旋转角度或亮度不同。这会直接影响数据划分。你要是纯随机按 8:2 切分同一场景的“原图”进了训练集、“旋转版”进了验证集验证指标会明显虚高部署到新产线上立刻掉点。这种虚高特别有迷惑性因为 loss 曲线很漂亮你会以为模型学到了病害特征其实它记住的是背景纹理。识别办法有两个。肉眼法随机抽 30 张缩略图拼成一张九宫格看如果发现背景高度雷同、只是角度不同基本可以确认。脚本法是算感知哈希分组用 PIL 自己实现一个 dHash不引入额外依赖from PIL import Image from pathlib import Path import itertools def dhash(path, size8): img Image.open(path).convert(L).resize((size 1, size), Image.LANCZOS) px list(img.getdata()) bits [] for row in range(size): for col in range(size): left px[row * (size 1) col] right px[row * (size 1) col 1] bits.append(1 if left right else 0) return int(.join(bits), 2) hashes {p.stem: dhash(p) for p in Path(potato/images).glob(*.jpg)} def hamming(a, b): return bin(a ^ b).count(1) # 阈值 5 是经验值越小越严格先小样本试别一上来全量两两比 groups [] stems list(hashes) seen set() for s in stems: if s in seen: continue group [s] seen.add(s) for t in stems: if t not in seen and hamming(hashes[s], hashes[t]) 5: group.append(t) seen.add(t) groups.append(group) print(总图数, len(stems), 近似分组数, len(groups)) print(最大组大小, max(len(g) for g in groups))hamming距离阈值 5 是偏宽松的起点8x8 的 dHash 一共 64 位阈值越小越严格。全量两两比较是 O(n²)8908 张跑起来会慢实际用的时候先按文件名前缀或目录粗分组再在组内算哈希。分好组之后划分数据集时以“组”为单位整组进训练或验证不要拆开。3.3 划分训练验证集按图分还是按场景分承接上一节结论是有增强的数据集划分单位必须是场景组不是单张图。做法是把 3.2 得到的groups打乱按 8:1:1 切保证同一组的图只出现在一个 split 里。如果嫌哈希麻烦还有个降级方案看文件名。很多增强流程会给增强图加统一后缀比如原图IMG_0001.jpg、增强图IMG_0001_aug1.jpg那就按主干前缀分组简单可靠。前提是你要先翻预览图确认命名规律别凭猜测写正则。划分比例上验证集不要低于 8%。五个类别里有长尾验证集太小会导致Defected potato只有几十个框AP 波动非常大今天 0.62 明天 0.48你会误以为是训练不稳定。我的习惯是训练 80%、验证 12%、测试 8%测试集切完就锁起来调参阶段一次都不碰留到最后验收用。4. 从 xml 到 txt转换脚本、data.yaml 与训练参数怎么落4.1 写一个可复现的 VOC→YOLO 转换脚本虽然数据集自带 txt但只要你改过类别名、筛过图、或者要重新划分就得自己转一遍。下面这个脚本把 VOC 转 YOLO同时做三件事过滤不在类别表里的脏标注、对越界坐标做 clip、跳过无目标的空图可选保留。import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image # 顺序必须和 data.yaml 的 names 完全一致改顺序等于改标签含义 CLASSES [ Damaged potato, Defected potato, Diseased-fungal potato, Potato, Sprouted potato, ] CLS2ID {c: i for i, c in enumerate(CLASSES)} XML_DIR Path(potato/annotations) IMG_DIR Path(potato/images) OUT_DIR Path(potato/labels_v2) OUT_DIR.mkdir(parentsTrue, exist_okTrue) KEEP_EMPTY True # 空图是否生成空 txt当背景样本用 for xml_path in sorted(XML_DIR.glob(*.xml)): root ET.parse(xml_path).getroot() size root.find(size) W, H float(size.find(width).text), float(size.find(height).text) if W 0 or H 0: print(异常尺寸跳过, xml_path.name) continue lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLS2ID: print(未知类别跳过, name, xml_path.name) continue b obj.find(bndbox) x1 float(b.find(xmin).text) y1 float(b.find(ymin).text) x2 float(b.find(xmax).text) y2 float(b.find(ymax).text) # 先 clip 再归一化避免浮点误差把坐标推出 [0,1] x1, x2 max(0.0, min(x1, W)), max(0.0, min(x2, W)) y1, y2 max(0.0, min(y1, H)), max(0.0, min(y2, H)) if x2 - x1 1 or y2 - y1 1: continue # 宽高为 0 的退化框直接丢 cx (x1 x2) / 2 / W cy (y1 y2) / 2 / H w (x2 - x1) / W h (y2 - y1) / H lines.append(f{CLS2ID[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) if not lines and not KEEP_EMPTY: continue (OUT_DIR / f{xml_path.stem}.txt).write_text(\n.join(lines), encodingutf-8)逻辑说明脚本以 xml 为主驱动一张图对应一个 txt。CLASSES的顺序是硬约束CLS2ID由它派生任何一行都不允许单独改动。坐标先 clip 到图像范围再归一化是为了处理标注时手抖画到图外的框退化框宽或高小于 1 像素直接丢因为归一化后会变成 0训练时可能触发除零或无效 anchor。参数上需要你改的只有四个路径常量和KEEP_EMPTY。KEEP_EMPTYTrue会把无目标的图写成空 txt这在 YOLO 里是合法的背景样本但要注意如果一个 batch 里全是空 txtloss 会异常偏低训练日志看起来像卡住了这时候检查一下空图比例超过 15% 就考虑降采样。转换完成后再跑一遍 2.2 的对齐脚本把TXT_DIR指向labels_v2确认输出数量与图片一致。这个动作重复一次不亏转换脚本写错一个缩进就能悄悄少生成几百个文件。4.2 data.yaml 的 names 顺序必须和 txt 索引锁定YOLO 的 txt 只存类别索引不存类别名所以data.yaml里的names列表顺序就是标签含义的唯一来源。索引 0 在 txt 里是 0在 yaml 里必须还是Damaged potato。这个对应关系一旦错位训练照样能跑、loss 照样下降但模型学到的语义全乱这是最典型的黑匣子故障。# potato.yaml path: /data/potato # 数据集根目录绝对路径最稳 train: images/train val: images/val test: images/test names: 0: Damaged potato 1: Defected potato 2: Diseased-fungal potato 3: Potato 4: Sprouted potato写 yaml 时有三个注意点。第一path建议写绝对路径相对路径是相对于运行命令时的当前目录不是相对于 yaml 文件很多人在这里翻车。第二names里的类别名带空格和连字符yaml 里用引号包起来更保险虽然这里不加引号也能解析。第三train、val指向的是图片目录框架会自动把路径里的images替换成labels去找标注所以目录命名最好遵循这个约定省得再配labels字段。4.3 训练命令与一组保守可用的超参环境里装好检测框架后一条命令就能起训。下面这组参数是我在 8k 量级、五类、单卡场景下比较常用的起点不是最优解但复现性好不容易出问题yolo detect train \ datapotato.yaml \ modelyolov8s.pt \ imgsz640 \ epochs100 \ batch16 \ workers8 \ patience30 \ lr00.01 \ lrf0.01 \ close_mosaic10 \ cacheram \ projectruns/potato \ namebaseline逐项说清楚。model换成你环境里已有的权重文件小模型先跑通再换大模型别一上来就上大 backbone8k 张图容易过拟合。imgsz640对应大多数产线相机的下采样尺寸如果 3.1 节统计出目标普遍偏小可以提到 960 再试。batch16是 8G 显存下的安全值显存够就往上加batch 太小 BN 统计不稳。patience30表示 30 轮验证指标没提升就早停配合 100 epoch 上限实际往往 60 到 80 轮就停了。close_mosaic10在最后 10 轮关掉马赛克增强让小目标回归真实分布这一步对最终 mAP 通常有零点几个点的正向影响。cacheram把图片缓存进内存8908 张 jpg 大概几个 G内存够就开能明显减少数据加载瓶颈内存紧张就改cachedisk或去掉。训练启动后先盯前 10 轮。看三件事train/box_loss是否稳定下降、val/box_loss有没有发散、每类的metrics/mAP50是不是从第二轮开始就有非零值。如果某一类从头到尾 AP 都是 0八成是 4.2 的 names 错位或者这一类在验证集里样本太少回到 2.2 和 3.1 复查。5. 避坑排查这份数据集最容易翻车的五个地方5.1 训练 loss 正常下降但推理结果几乎全是 Potato现象训练日志一切正常验证集 mAP50 能到 0.7 上下但拿真实图片推理框出来的几乎都标成Potato病害类极少出现。原因Potato这一类占了 9868 个框是Defected potato的 2.5 倍。交叉熵在类别不平衡时会偏向高频类加上真菌性病害和缺陷在低分辨率下纹理接近模型走了捷径。另一个可能是验证集里Potato的图片占比过高导致指标看起来还行。解决先按 3.1 的脚本确认验证集每类框数如果最小类少于 200 个框先补验证集而不是改模型。训练侧可以开类别权重或者用 copy-paste 增强补长尾类推理侧按类别分别调置信度阈值病害类阈值压低到 0.25正常类提到 0.5用混淆矩阵验证调整效果。5.2 mAP 虚高换一批图就崩现象验证集 mAP50 到 0.85换一个批次的实拍图漏检率直接过半。原因3.2 说的增强泄漏。同一场景的原图和增强图被随机分到了训练集和验证集验证集对模型来说是“见过的背景”指标虚高。也可能验证集和训练集来自同一次采集光照、背景完全一致。解决按 dHash 分组重划数据集保证同组不跨 split再额外留一批不同时间段采集的图做测试集哪怕只有两三百张。验收时以这个独立测试集为准验证集指标只用于调参。5.3 标注框贴边被裁小目标训练时消失现象训练时提示某些标签越界或者可视化时发现图像边缘的目标框少了一截。原因VOC 的xmax允许等于图宽转 YOLO 归一化后cx w/2在浮点下可能略大于 1部分框架的数据增强比如随机裁剪、仿射变换会把这个框判为非法直接丢弃。解决用 4.1 脚本里的 clip 逻辑先把坐标夹到[0, W]、[0, H]再归一化并在写出前检查w 0 and h 0。增强阶段把随机裁剪的边界留出 5% 余量别贴着边裁。5.4 同一张图同时出现在训练集和验证集现象对齐脚本显示文件数都对但训练到后期验证 loss 突然掉到很低mAP 跳变。原因增强图文件名不同、内容近似随机划分时被拆散或者你复制过目录同一个 stem 在两个 split 里各存了一份。解决划分前先做一次全局 stem 去重跨 split 取交集非空就报错退出。把这段检查写成训练前的固定步骤比事后排查省事得多。5.5 中文路径或空格路径导致读图失败现象训练启动即报错提示找不到图片或者读到 0 张图。原因数据集解压到了带中文或空格的目录部分底层库对路径编码处理不一致data.yaml里的相对路径又叠了一层当前目录问题。解决数据根目录统一用纯英文、无空格、层级浅的路径比如/data/potato。yaml 里的path写绝对路径。如果必须用中文路径先小批量跑yolo detect val验证读图链路别等训练到第 10 轮才发现。6. 验收与进阶用单类 AP 和分层小样本判断要不要继续投入6.1 别只看 mAP50先看每类召回和混淆矩阵农业检测场景的代价不对称漏掉一颗病害土豆可能污染一整箱把正常土豆误判成病害最多多切一刀。所以验收时优先级是每类召回 每类 AP 总体 mAP50。跑一次验证把逐类结果拉出来yolo detect val \ modelruns/potato/baseline/weights/best.pt \ datapotato.yaml \ splittest \ conf0.001 \ iou0.6 \ plotsTrueconf0.001是为了让 PR 曲线覆盖全阈值区间方便后面手动挑工作点iou0.6是常规匹配阈值如果目标密集、重叠多可以降到 0.5 再看一遍。输出目录里的混淆矩阵要重点看两处Diseased-fungal potato有多少被误判成PotatoDefected potato有多少被判成Damaged potato。这两组混淆如果占比超过 20%说明类别定义在视觉上不够可分要么合并类别要么补更多区分性样本光调参解决不了。指标关注点我的参考线每类 Recall病害漏检病害三类均不低于 0.80每类 AP50长尾类是否学动最小类不低于 0.65背景误检率空图误报不高于 10%推理耗时产线节拍单图 640 输入低于 30ms这张表不是标准是我自己的止损线。低于参考线时先回去查数据划分和类别错位再考虑换模型。6.2 一个省时间的做法分层抽样 800 张先跑通全量 8908 张跑 100 轮单卡动辄几小时。我的习惯是先按类别框数比例分层抽 800 张跑 15 轮专门用来验证三件事标签索引有没有错位、数据管道读图是否正常、增强有没有把目标搞没。这 800 张里保证每个类别至少 60 个框Defected potato这种小类按比例上浮。抽样脚本按 3.1 统计出的每类框数做比例分配抽完单独生成一份potato_mini.yamlpath指向 mini 目录。15 轮跑完看验证集的可视化结果如果框的位置和类别都对得上再切回全量。这一步大概二十分钟能挡掉八成低级错误。我自己有过一次教训直接上全量训了四个小时最后发现data.yaml的names顺序和转换脚本的CLASSES不一致模型把萌芽当成了正常loss 曲线却漂亮得很。从那以后我每次起训前都强制走一遍“对齐脚本 800 张分层抽样 可视化抽检”三关过了才开全量。这套流程看着笨但比事后复盘省时间。希望帮到你。本文还有配套的精品资源点击获取