简介:面向计算机视觉与深度学习研究者的蘑菇类型识别检测数据集,包含八千四百三十张真实场景图片,标注类别为二十一种蘑菇,每个样本同时提供Pascal VOC格式的XML标注和YOLO格式的TXT标注,可直接用于主流目标检测算法的训练与验证,省去自行转换格式的麻烦。由于不同蘑菇在形态、颜色、纹理上的差异细微,该数据集天然构成细粒度识别任务,适合检验模型对相似类别的判别能力,也可应用于食品安全中毒蘑菇快速甄别、生态调查中物种分布监测、农业领域品种分类与品质分级等落地场景。压缩包共两千个文件,以XML标注文件为主,另附一份使用前必读说明,整体大小约一百九十三点五六兆字节,结构清晰,便于导入常用深度学习开发环境进行二次处理,也可作为课程设计或算法评测的参考数据。当前已有四百二十九人学习或浏览,配套说明文档对样本分布、目录组织和标注形式给出了必要提示,有助于减少初期数据清洗与整理时间,让使用者更专注于模型调优和识别精度提升。
1. 8430张蘑菇数据集的定位:细粒度目标检测的难点不在算法
在真实项目里,蘑菇识别比车辆检测更考验数据配套:松茸和香菇在颜色、质感上接近,白蘑菇和白玉菇更是只有菌盖弧度差异,模型一旦在训练集中见过太少代表样本,验证集会立刻“翻脸”。这份8430张、覆盖21个类别的蘑菇检测数据集,同时保留了VOC的XML标注和YOLO的TXT标注,对做食用菌分选、野生菌识别或者深度学习课程设计的人来说,省掉的是最贵的整理阶段。而且21类这个规模,刚好能让你用yolov8s这类轻量模型在一张消费级显卡上跑完整流程。下面从解压检查开始,把直接可用的流程写清楚。
2. 目录结构与VOC/YOLO双标注格式的底层差异
2.1 先解压再盘点:7z文件别急着喂给训练脚本
很多人在linux上用7z x解压后直接开始训练,结果目录里多出一层嵌套,data.yaml里的路径全要重写。我的习惯是两步走。第一步先用7z l列内容:
7z l 蘑菇类型识别检测数据集VOC+YOLO格式8430张21类别.7z这条命令只列目录结构不解压,适合先确认压缩包内是否有一个顶层目录。如果有,解压时文件不会散落;如果解压出来文件直接平铺在当前目录,说明压缩包制作时没用目录包一层。第二步再真正解压:
7z x 蘑菇类型识别检测数据集VOC+YOLO格式8430张21类别.7z解压完成后,我一般会检查顶层目录名里有没有空格或者中文。yolov8的data.yaml虽然能处理带空格路径,但ultralytics在读取时偶尔会把含空格的路径截断,报出奇怪的FileNotFoundError,排查起来很浪费时间。建议解压后立刻重命名成纯ASCII目录,例如MushroomDataset。另外,压缩包文件名里的中文不影响解压内容,但放到服务器上容易因locale不同而显示乱码,所以解压后顺手改英文名最省事。
目录盘点用tree看一眼结构,VOC系数据集的典型布局如下:
MushroomDataset/ ├── Annotations/ # VOC格式的XML标注 ├── JPEGImages/ # 8430张原始图片 ├── ImageSets/ │ └── Main/ # train.txt / val.txt 划分文件 └── labels/ # YOLO格式的TXT标注VOC类数据集通常把划分文件放在ImageSets/Main里,先看train.txt和val.txt的行数,能确认训练验证比例是否符合预期。如果之后要跑k折交叉验证,也需要从这里重新分配。顺便说一句,原始XML里的图片尺寸信息不一定和实际图片一致,解压后抽检几十张对比一下,防止压缩传输过程中被二次压缩导致尺寸变化。
2.2 两种格式的换算逻辑:左上右下坐标vs归一化中心坐标
VOC格式的XML里,<bndbox>节点下是xmin、ymin、xmax、ymax四个整数,代表目标框在原始图片上的绝对像素位置;YOLO格式的TXT每行是class_id cx cy w h,其中cx和cy是中心点坐标除以图片宽高后的归一化值,w和h是目标宽高除以图片宽高。转换的核心就是一个除法关系。
如果你用labelimg打标完yolo格式的标,再回头看这份数据的txt文件,会发现有的坐标小数位特别多,这是正常的。但如果自己补标了一批新图,我建议用脚本批量确认坐标是否越界,常见的问题有两个:一是标注框贴近图像边缘时xmax可能写成了图片宽度,二是某些标注工具的坐标从1开始计数,转换后最小值变成负数。
下面这段是我常用来做VOC转YOLO并顺手校验边界的代码:
import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_path, class_map, img_w, img_h): root = ET.parse(xml_path).getroot() lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_map: continue # 忽略不在类别表中的目标 bndbox = obj.find('bndbox') xmin = int(bndbox.find('xmin').text) ymin = int(bndbox.find('ymin').text) xmax = int(bndbox.find('xmax').text) ymax = int(bndbox.find('ymax').text) # 边界裁剪:避免转换后出现小于0或大于1的值 xmin = max(0, min(xmin, img_w - 1)) xmax = max(0, min(xmax, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) ymax = max(0, min(ymax, img_h - 1)) if xmax <= xmin or ymax <= ymin: continue # 过滤退化框,防止后续loss变成nan cx = (xmin + xmax) / 2 / img_w cy = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{class_map[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_path, 'w') as f: f.write('\n'.join(lines))这段代码的要点不止在注释里。第一,class_map的作用是把21个类别的名称映射成从0开始连续整数,如果类别名没在字典里,代码会直接跳过,所以转换后一定要复查txt行数是否和xml里的object数一致。第二,归一化后cx、cy的范围是[0,1],w和h理论上也在[0,1]内,但很多标注框给的xmax等于图片宽度,经除法运算后w会是1.000000,yolov8虽然能容忍,但对回归头的梯度有轻微负面影响。第三,过滤退化框的逻辑不可少,宽或高为0的标注会让损失当场变成nan,这种问题已经浪费过我太多次时间。
2.3 训练前的四类完整性检查
即便是别人整理好的数据集,直接丢给yolov8训练前我也建议跑一遍检查脚本。最常见的问题不是标注错,而是文件对应关系断掉:图片还在、txt被改名;或者txt里混入了空行;或者某种原因下class_id写成了21、22这种越界值,而21类数据集的合法class_id范围是0到20。
我一般用下面这个脚本做批量体检:
import os from pathlib import Path from PIL import Image img_dir = Path('JPEGImages') label_dir = Path('labels') NC = 21 # 类别数 bad_img, missing_label, empty_label, over_cls = [], [], [], [] for img_path in sorted(img_dir.glob('*')): try: Image.open(img_path).load() except Exception: bad_img.append(img_path.name) continue label_path = label_dir / (img_path.stem + '.txt') if not label_path.exists(): missing_label.append(img_path.name) continue lines = [ln for ln in label_path.read_text().splitlines() if ln.strip()] if not lines: empty_label.append(img_path.name) continue max_cls = max(int(ln.split()[0]) for ln in lines) if max_cls >= NC: over_cls.append((img_path.name, max_cls)) print(f"图片总数: {len(list(img_dir.glob('*')))}") print(f"损坏图片: {len(bad_img)}") print(f"缺失标签: {len(missing_label)}") print(f"空标签: {len(empty_label)}") print(f"类别越界: {len(over_cls)}")这个脚本的核心逻辑是逐项解码图片,对yolov8来说图片解码失败是最致命的。缺失标签和空标签会导致训练时出现“image without labels”警告,yolov8会直接删除该样本并在日志里减少训练集数量,虽然不会崩,但会悄悄改变你预期中的训练数据量。类别越界则必须改成和yaml里nc一致的连续编号。
常见问题处理优先级参考下表:
| 问题 | 典型现象 | 处理方式 |
|---|---|---|
| 图片损坏 | PIL/OpenCV读取报错 | 重新导出为jpg或删除 |
| txt缺失 | 日志提示image without labels | 核对xml和txt命名一致性 |
| txt为空 | 训练集数量少于预期 | 回到labelimg补标 |
| class_id越界 | 某类别从不被预测 | 按data.yaml里的names重排编号 |
| 框宽或高为0 | 训练loss出现nan | 在转换脚本里过滤退化框 |
这五类问题里,前两类影响训练数量,后三类直接影响模型能力,处理成本最高的其实是类别越界,因为它不会报错,只在推理阶段表现为某类永远没有输出。跑完这步,数据底子才算干净。
3. 用YOLOv8训练这套蘑菇数据集的完整配置
3.1 重组目录结构并生成data.yaml
yolov8训练自己的数据集时,目录布局的要求是images和labels两级目录,下面再分train和val。很多人的数据集从VOC迁移过来,还是ImageSets那套结构,直接训练会报找不到标签。我的做法是用bash把文件按ImageSets/Main/train.txt里的文件名清单移动过去:
mkdir -p dataset/images/{train,val} dataset/labels/{train,val} while read fname; do fname=$(basename "$fname") fname="${fname%.*}" # 去掉扩展名后,找到同一stem的图片和txt再移动 [ -f "JPEGImages/${fname}.jpg" ] && mv "JPEGImages/${fname}.jpg" "dataset/images/train/" [ -f "labels/${fname}.txt" ] && mv "labels/${fname}.txt" "dataset/labels/train/" done < ImageSets/Main/train.txt注意:如果train.txt里存的是包含路径的完整行,用basename处理后只保留文件名;如果原图是png,把.jpg换成.png即可。脚本里加的存在性判断是为了防止mv输出一堆No such file的报错刷屏。移动比复制省磁盘空间,但如果你还需要保留VOC结构做其他实验,把mv改成cp,代价是磁盘多占用一份空间。
完成目录重组后生成data.yaml。如果你的labels目录里class_id是从0开始连续编号的,参考配置如下:
path: /data/MushroomDataset/dataset train: images/train val: images/val nc: 21 names: 0: white_mushroom 1: shiitake 2: oyster_mushroom 3: enoki 4: king_oyster 5: white_beech 6: brown_beech 7: tea_tree 8: lion_mane 9: flower_mushroom 10: nameko 11: coprinus 12: straw_mushroom 13: wood_ear 14: silver_ear 15: matsutake 16: porcini 17: chanterelle 18: morel 19: termitomyces 20: blewit这里names的顺序就是TXT里class_id的顺序,必须严格对应。yolov8训练自己数据集时最常见的隐蔽错误就是names顺序和标注文件对不上:训练不报错,mAP也能到0.9,但推理阶段“香菇”框里永远显示“口蘑”。如果这份数据集的labels目录内class_id不是从0开始连续,需要先做一次重映射再写yaml。
config里还有一个容易被忽略的点:path建议写绝对路径。相对路径在yolov8早期版本能通过,但跨机器迁移时经常因为工作目录不同而失效。写死绝对路径后,换机器只需要改path一处,其他配置不用动。
关于yolo环境配置,很多人卡在torch和CUDA版本上,解压完数据集先别急着跑训练,用下面这条命令确认GPU可用:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"如果输出False,先解决显卡驱动和CUDA,再回来跑训练。这个步骤跳过的话,训练会在CPU上跑很久,很多人误以为数据集有问题,其实只是算力没用上。
3.2 训练参数怎么定:模型规模、batch和imgsz的权衡
8430张图对yolov8来说不算大,所以不需要一上来就是yolov8x。我通常从yolov8s起步,因为蘑菇目标在图中占比较大,分辨率需求没到遥感那种程度,s的参数量足够拟合21个类。如果后续发现个别品种的mAP50-95明显偏低,再切换yolov8m,而不是直接上x导致过拟合。
参数表如下:
| 参数 | 建议值 | 说明 |
|---|---|---|
| model | yolov8s.pt | 预训练权重从COCO迁移,细纹特征仍可复用 |
| imgsz | 640 | 蘑菇近景占主体,640够用;边缘品种用1024验证 |
| epochs | 150 | 21类细粒度任务150轮是比较稳的区间 |
| batch | 16 | 12G显存下不OOM,batch过小BN不稳定 |
| optimizer | AdamW | 损失面更平滑,细粒度分类不易震荡 |
| patience | 30 | mAP连续30轮不涨自动停,省时间 |
| cache | True | 8430张原图可完全放入内存时显著提速 |
实际训练命令如下:
yolo detect train \ data=/data/MushroomDataset/dataset/data.yaml \ model=yolov8s.pt \ imgsz=640 \ epochs=150 \ batch=16 \ patience=30 \ optimizer=AdamW \ cache=True \ project=run_mushroom \ name=v8s_mix说两个容易踩的细节。第一个是project和name不写的话默认落在run/detect/train,连续跑几次实验目录被覆盖的概率非常大,每次都会提示是否覆盖或新建,节奏会被打断,写两个参数就能在同一个项目目录下看到多条实验记录。第二个是cache=True时首轮训练前会有一个较长的缓存构建过程,这不是卡死,是正在把图片读到内存;如果用的是机械硬盘或者网络挂载盘,训练速度会掉很多,我建议把数据集挪到本地NVMe再cache,9010毫秒级的加载和30秒级的加载差距能直接体现在训练总时长上。
3.3 训练日志里的box_loss/cls_loss/dfl_loss怎么看
yolov8的损失由三部分构成,这也是yolo损失函数设计里最关键的地方。box_loss衡量预测框与标注框的位置偏差,体现回归头的表现;cls_loss衡量分类概率与真实类别的偏差,在21类任务里直接决定品种判对判错;dfl_loss是distribution focal loss,通过对边界坐标分布建模来优化边缘回归,对蘑菇这种伞盖边缘不规则的目标比普通L1损失更稳定。三条loss要配套看才有意义:box和dfl同降说明框在逐渐贴紧轮廓,若cls_loss居高不下,问题大概率出在相似品种的样本代表性上,而不是模型结构。
训练结束后验证命令是这样的:
yolo detect val \ model=run_mushroom/v8s_mix/weights/best.pt \ data=/data/MushroomDataset/dataset/data.yaml \ project=run_mushroom \ name=v8s_val验证过程会输出results.csv,里面有precision、recall、mAP50和mAP50-95四个核心指标。mAP50到0.9以上说明粗粒度识别可行,mAP50-95如果比mAP50低20个点以上,说明框的位置精度还有提升空间,优先查标注框是否贴合蘑菇边缘,再考虑imgsz是否要提到1024。
我通常在日志里看的三个分位点是:第30轮cls_loss是否进入平台期、第60轮box_loss是否还保持下降趋势、最后一次patience触发在哪个epoch。如果patience触发得太早,比如50轮就停,说明学习率下降太快,下次训练把cos_lr打开,或把初始lr从1e-3降到5e-4。
4. 21类样本不均衡与相似品种的调优思路
4.1 统计类别分布,避免长尾类别拖后腿
食用菌数据集天然不平衡:口蘑、香菇这类常见品种样本多,鸡油菌、牛肝菌这类野外品种样本少。深度学习任务里不均衡会导致尾部类别的召回率极低,所以建议先做分布统计。统计脚本很简单:
import os from collections import Counter label_dir = 'dataset/labels/train' counts = Counter() for txt in os.listdir(label_dir): with open(os.path.join(label_dir, txt)) as f: for line in f: line = line.strip() if not line: continue counts[int(line.split()[0])] += 1 total = sum(counts.values()) for cls_id in range(21): print(f"class {cls_id}: {counts[cls_id]} instances, {counts[cls_id]/total:.2%}")运行后重点看两个数字:占比最大的类和占比最小的类之间差多少倍,以及低于平均水平的类有几个。如果尾部类占比不足2%,在batch size固定时每个batch里可能都碰不到这些类,模型对其特征的更新频率极低。
处理不均衡时我一般按场景选方法,参考对照如下:
| 方法 | 适用场景 | 实际注意点 |
|---|---|---|
| Copy-Paste增强 | 尾部类目标易于分离 | 蘑菇成簇生长,粘贴后自然度较好 |
| 降采样头部类 | 头部类超过40% | 同时减少总epoch或加大尾部类权重 |
| loss加权 | 尾部类业务重要性更高 | 需修改ultralytics的cls_loss或自定义Dataset |
| 类别计数日志 | 排查数据标注漏标 | 训练前过一遍,比训练后推断成本低 |
Copy-Paste是细粒度目标检测里性价比很高的做法。比如鸡油菌只有几十个实例,把这几十个目标真实地从原图中抠出来,随机粘贴到其他没有该目标的图片上,相当于把样本量放大几倍。难点是粘贴位置不能和已有目标重叠太多,以及要同步更新边界框,否则会引入噪声。蘑菇类目标边缘比较清晰,比粘贴透明物体效果好很多。
4.2 用混淆矩阵区分错分和漏检
食用菌识别里最典型的问题是花菇和香菇、白玉菇和蟹味菇这类相近品种互相错分。mAP指标只能告诉你“整体还行”,无法定位到底是哪两个类互相干扰。验证结束后,我习惯直接把混淆矩阵调出来:
from ultralytics import YOLO model = YOLO('run_mushroom/v8s_mix/weights/best.pt') val_metrics = model.val(split='val', batch=16) confusion = val_metrics.confusion_matrix.matrix print(confusion.shape) # (21, 21)混淆矩阵的行是真实类别,列是预测类别,对角线上的数值越大越好。看的时候我一般直接打印非对角线上最大的几个值,定位互相竞争的类别对。如果花菇样本经常被预测成香菇,说明这两类的特征在模型看来太接近,这时候调低整体置信度阈值没有意义,反而会把更多的普通香菇误报成花菇。
对这类近似品种,常见做法是回到数据集层面看两组样本的标注差异:伞盖是不是有裂纹、边缘卷曲程度是否一致。如果原图上这两类的视觉差异确实小到人眼都难分辨,那应该考虑降低任务的细粒度要求,或者把这两个类合并成一个大类。如果人眼能分辨而模型不能,那就是训练数据里代表性样本不够,重点补困难样本而不是整体加数据。很多时候补几十张特定光照下的难例,比增加几百张普通样本更有效。
4.3 用数据增强处理树荫和背光场景
野生蘑菇很多长在树荫下,图片整体偏暗、对比度不足,这部分样本在实际推理中又恰恰是最容易漏检的。yolov8自带的mosaic和hsv增强已经在跑,但对光照退化的针对性不够,我会额外在训练前预处理里加一组albumentations增强:
import albumentations as A aug = A.Compose([ A.RandomBrightnessContrast( brightness_limit=0.3, contrast_limit=0.3, p=0.8 ), A.RandomGamma(gamma_limit=(80, 120), p=0.5), A.CLAHE(clip_limit=2.0, tile_grid_size=(8, 8), p=0.3), A.HueSaturationValue( hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=15, p=0.6 ), ], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels'])) # 调用方式: # out = aug(image=img, bboxes=boxes, class_labels=labels)参数设计逻辑:RandomBrightnessContrast模拟阴天和树荫下的明暗变化,亮度范围0.3不会把蘑菇打到过曝;RandomGamma用于背光场景,gamma值80到120调整的是中间调的亮度权重;CLAHE对伞盖纹理的增强最有用,能在不改变整体明暗的情况下让菌褶和白色伞盖的边界更清晰;HueSaturationValue的幅度控制在10和20内,避免让颜色偏移到不真实的程度,香菇和花菇这类靠颜色区分的类最怕颜色增强过头。
这里补一点和yolov8自带增强的配合经验:yolov8的mosaic概率默认是1.0,意味着前十个epoch大部分样本是马赛克拼接的,此时外部增强叠加会产生过于嘈杂的训练图。我的做法是保留yolov8默认mosaic参数,但在自定义数据加载时只对训练集应用albumentations,验证集不做任何增强,这样对比不同增强策略时能分辨效果来源。
5. 推理阶段按置信度分档输出,提升漏检召回
5.1 同图双档预测对比
训练收敛后,直接用一个置信度跑推理是很浪费的。蘑菇检测任务里,很多漏检不是模型没识别到,而是置信度卡在0.3到0.5之间。我通常在同一张图上分别跑conf=0.5和conf=0.25两组,再对比输出框数量和类别分布:
from ultralytics import YOLO model = YOLO('run_mushroom/v8s_mix/weights/best.pt') results_hi = model.predict( source='test/forest_bright.jpg', conf=0.5, iou=0.5, imgsz=640, save=True ) results_lo = model.predict( source='test/forest_bright.jpg', conf=0.25, iou=0.5, imgsz=640, save=True ) hi_boxes = results_hi[0].boxes lo_boxes = results_lo[0].boxes print(f"conf=0.5 检出 {len(hi_boxes)} 个目标") print(f"conf=0.25 检出 {len(lo_boxes)} 个目标")跑完后重点看不只是数量差,而是新增的框集中在哪些类别。如果新增框主要落在白蘑菇、平菇这类常见品种,说明模型对这些类的学习已经充分,阈值可以安全降低;如果新增框全部集中到鸡油菌、羊肚菌这类尾部类,那说明这些类在训练时就没学好,降阈值换来的是误检暴涨,正确思路是回到第4.1节的类别均衡处理,而不是在推理侧迁就。
5.2 conf、iou两个旋钮的分工
很多人在推理侧只会动conf,忽略iou。蘑菇的分布特点是成簇生长:一簇平菇往往有多个实例紧贴在一起,边界框之间高度重叠。此时把iou设成0.7,NMS会把相邻的两个蘑菇判断为同一个目标而合并掉;设到0.4到0.5,才能保留簇内的独立实例。yolo检测调整置信度门限时我建议的操作顺序是:先固定iou=0.5,看conf从0.25到0.5的误差曲线;再固定conf=0.3,调iou从0.3到0.7,找漏检率的最低点。两步分开调比同时乱调可控得多。
实际部署到分选产线时,可以做一个两档输出的增强逻辑:conf大于等于0.5的框直接进执行,conf在0.25到0.5之间的框标记为“待人工复核”,而不是一律丢弃。这样既保住了尾部类的召回,又不会因为降阈值让普通误检直接干扰产线。对这份数据集来说,这一步操作往往比换更大的模型更能提升实际可用性。
本文还有配套的精品资源,点击获取