冬虫夏草检测数据集:从Pascal VOC标注到YOLO格式转换实战
2026/9/14 18:10:39 网站建设 项目流程

简介:一份面向目标检测任务的数据集资源,核心内容为冬虫夏草生长检测。包含613张实地拍摄的草地冬虫夏草图片,以及相同数量的XML标注文件,标注工具为LabelImg,矩形框记录左上与右下角坐标,标注类别为单一冬虫夏草。除图片与标注外,还附带类别JSON文件与可视化Python脚本,脚本可随机读取一张图片并自动绘制边界框,保存到当前目录,无需修改即可运行,方便快速核对标注效果。整套资源共1229个文件,以JPG图片和XML标注为主,另含少量JSON、PNG与PY脚本,压缩包整体约80.12MB。已有226人学习下载。该数据集可直接用于训练YOLOv5等目标检测模型,XML标注格式具通用性,便于转换为COCO或YOLO格式;配合作者在博客分享的YOLOv5改进实战,可帮助开发者系统完成从数据准备、模型训练到效果优化的完整流程,同时为自定义数据集的标注与验证提供范例,适合农业智能化与野生菌类监测等应用场景。

1. 野外单类检测数据集的结构与使用思路

做目标检测的人通常不缺模型,缺的是干净可用的数据。这份冬虫夏草生长检测数据集一共 613 张图片和 613 个 XML 标签文件,标注对象只有一类别:草地上刚生长出来的冬虫夏草,标注工具是 LabelImg,输出形式是 XML 里的矩形框,记录的是左上角和右下角坐标。613 张对单类检测来说不算大,但对野外采集场景来说,已经足够支撑 YOLOv5、YOLOv8 这类模型做迁移学习,关键是怎么把这批 XML 用起来。数据集还附带了一个可视化脚本和一个类别 JSON 文件,前者随机取图就能画框预览,后者用于标注类别到索引的映射。本文从 XML 解析、可视化验证、JSON 类别配置、YOLO 训练格式转换到增强训练时容易翻车的点,完整过一遍这个资源的使用链路。

2. 数据集目录结构与 XML 矩形框标注格式解析

2.1 拿到解压包后先看什么

解压后的核心内容不是图片,而是每张图同名的 XML 文件。图片名形如image (78).jpg,对应的标注文件就是image (78).xml,这种命名方式在 LabelImg 导出时非常常见,注意文件名里的括号在 Shell 脚本中需要转义或用引号包裹,批量操作时很容易在这里踩坑。

├── images/ │ ├── image (78).jpg │ ├── image (71).jpg │ └── ... ├── labels/ │ ├── image (78).xml │ ├── image (71).xml │ └── ... ├── classes.json └── visualize.py

如果解压后没有classes.json,打开任意一个 XML 文件看<object><name>节点的值,单类别数据集通常就是这个类别名本身。

2.2 XML 标注格式的中心坐标换算

LabelImg 输出的 XML 采用 Pascal VOC 格式,核心是<object>节点下的<bndbox>,里面存的是xmin, ymin, xmax, ymax四个像素坐标,分别对应目标框的左上角和右下角。注意这个坐标系原点在图片左上角,x 轴向右,y 轴向下,和 OpenCV、PIL 的坐标系统一致,不需要翻转。

import xml.etree.ElementTree as ET def parse_voc_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) boxes = [] for obj in root.findall('object'): name = obj.find('name').text bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) boxes.append({ 'label': name, 'xmin': xmin, 'ymin': ymin, 'xmax': xmax, 'ymax': ymax, 'w': xmax - xmin, 'h': ymax - ymin }) return width, height, boxes

这段代码是后续所有预处理的基础。widthheight必须与图片真实尺寸一致,否则在归一化坐标时整体偏移。这里boxes收集了每个实例的宽高信息,后面统计目标尺寸分布、检查标注是否越界都会复用。

2.3 LabelImg 导出时容易被忽略的两个字段

除了<bndbox>,XML 里还有<folder><filename><path>三个路径相关字段。LabelImg 在导出时,<filename>写的是图片文件名,<path>写的是标注时图片的绝对路径。如果是换电脑或者数据集整体移动过目录,<path>就是错的,但它不影响训练,因为 YOLO 训练流程是拿图片文件路径去解析,不是读 XML 里的 path。这时候容易犯的错是用glob去匹配 XML 名然后再去拼接图片路径,导致括号和空格处理出错。

import os import glob xml_dir = 'labels' img_dir = 'images' xml_files = glob.glob(os.path.join(xml_dir, '*.xml')) for xml_path in xml_files: basename = os.path.splitext(os.path.basename(xml_path))[0] img_path = os.path.join(img_dir, basename + '.jpg') width, height, boxes = parse_voc_xml(xml_path) # 后续转 YOLO 格式时,img_path 是实际存在的那张图

basename直接取自 XML 文件名,这样无论filenamepath字段怎么脏,都不会影响匹配结果。

3. 数据可视化脚本的原理与边界框绘制实现

3.1 随机抽图验证标注质量

这份资源自带的visualize.py设计思路很直接:从数据集里随机挑一张图片,解析同名 XML,用 OpenCV 绘制矩形框并保存到当前目录。为什么要做这一步?因为 XML 标注是纯数字,人无法直觉判断位置是否正确。画框预览的性价比极高,框的位置偏了、尺寸是 0、坐标超出了图片边界,一张图就能暴露问题。

import os import cv2 import random import xml.etree.ElementTree as ET image_dir = 'images' xml_dir = 'labels' all_images = [f for f in os.listdir(image_dir) if f.lower().endswith('.jpg')] chosen = random.choice(all_images) stem = os.path.splitext(chosen)[0] img = cv2.imread(os.path.join(image_dir, chosen)) H, W = img.shape[:2] tree = ET.parse(os.path.join(xml_dir, stem + '.xml')) root = tree.getroot() for obj in root.findall('object'): name = obj.find('name').text bbox = obj.find('bndbox') xmin = int(float(bbox.find('xmin').text)) ymin = int(float(bbox.find('ymin').text)) xmax = int(float(bbox.find('xmax').text)) ymax = int(float(bbox.find('ymax').text)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, name, (xmin, ymin - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) out_path = 'visualized_' + chosen cv2.imwrite(out_path, img) print(f'saved: {out_path}')

运行这个脚本前,把image_dirxml_dir指向解压后的实际目录即可。cv2.rectangle 的第三、四个参数是左上角和右下角,与 XML 里 bndbox 的语义一一对应,所以不需要换算。颜色如果是 (0, 255, 0) 绿色,在绿色草地里对比度可能略低,可以改成红色 (0, 0, 255) 或者蓝色,看实际图再调。

3.2 坐标越界检查:可视化脚本之外的批量验证

可视化脚本每次只能看一张随机图,要系统性地检查 613 张图是否全部规范,建议补一个批量越界扫描。所谓越界,指的是 xmax 大于图片宽度或者 ymax 大于图片高度,这种情况通常发生在标注后期手动微调框时不小心拉出了边界,训练时如果不处理,YOLO 在计算损失时会直接报错。

bad = 0 for xml_path in xml_files: width, height, boxes = parse_voc_xml(xml_path) img_path = xml_path.replace('labels', 'images').replace('.xml', '.jpg') if not os.path.exists(img_path): print(f'missing image: {img_path}') bad += 1 continue img = cv2.imread(img_path) H, W = img.shape[:2] for box in boxes: if box['xmin'] < 0 or box['ymin'] < 0 or box['xmax'] > W or box['ymax'] > H: print(f'{xml_path}: box out of bounds') bad += 1 print(f'total issues: {bad}')

这段代码里真正的检查是对WH与 XML 的<size>字段做对比,如果发现漏检,一般不是 XML 写错了,而是图片被压缩过但没有重新标注。所有坐标必须落在[0, W][0, H]区间内,不满足就说明标签不可信。

4. 类别 JSON 文件与 YOLO 训练格式转换

4.1 JSON 类别文件到底在配置什么

这里的数据集类别为冬虫夏草 1 类,也就是classes.json里只有一个类别名。理解 JSON 文件的作用要站在 YOLO 训练流程的角度:YOLOv5/YOLOv8 训练时,数据集的类别 ID 从 0 开始编号,标签文件里的每一行是class_id x_center y_center width height,第一个数字就是类别索引。JSON 文件在这里充当类别名与索引的唯一映射。

{ "categories": [ {"id": 0, "name": "cordyceps"} ] }

如果拿到的classes.json结构是简单的数组["cordyceps"],本质也一样,训练时把它转成 YOLO 要求的 yaml 即可。注意类别名的拼写要全局统一,如果你决定用 "cordyceps",那 XML 里<name>节点的值也被映射成0,两边的严格对应关系不能错。

4.2 XML 转 YOLO TXT 的完整脚本

YOLOv5/YOLOv8 默认不会直接读 Pascal VOC 格式的 XML,需要先转成每张图一个.txt标签文件,放在与图片同名的labels目录下,这是使用这个数据集时最关键的一步,没有之一。

import os import glob def voc_to_yolo(xml_path, out_dir, class_names): width, height, boxes = parse_voc_xml(xml_path) stem = os.path.splitext(os.path.basename(xml_path))[0] out_txt = os.path.join(out_dir, stem + '.txt') lines = [] for box in boxes: if box['label'] not in class_names: raise ValueError(f"unknown class: {box['label']}") cls_id = class_names.index(box['label']) x_center = (box['xmin'] + box['xmax']) / 2.0 / width y_center = (box['ymin'] + box['ymax']) / 2.0 / height w = box['w'] / width h = box['h'] / height lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_txt, 'w') as f: f.write('\n'.join(lines) + '\n') class_names = ['cordyceps'] os.makedirs('yolo_labels', exist_ok=True) for xml_path in glob.glob('labels/*.xml'): voc_to_yolo(xml_path, 'yolo_labels', class_names)

转换的核心是坐标从像素值变为相对图片宽高的比例值,而且必须把左上右下的表示转为边界框中心点坐标加宽高。为什么要用中心点而不是直接用 xmin 和 ymin?因为 YOLO 的回归目标是对 anchor 的偏移量,中心点加宽高的编码方式与模型输出头对齐,迁移学习时直接套用 MS COCO 的 anchor 尺寸还是要靠这个格式。归一化时统一除以宽度和高度,x 轴除以宽,y 轴除以高,不能混用,否则框会上下左右漂移。

4.3 数据集目录组织与训练配置

转出来的 YOLO 格式还需要按训练框架的目录规范摆放,一般会合成这样的结构:

目录/文件作用
images/train/训练图片
labels/train/训练标签
images/val/验证图片
labels/val/验证标签
data.yaml类别名与路径配置

划分比例建议按 8:2 或者 9:1,613 张图片规模下验证集 120 张左右比较合适。对应的data.yaml如下:

train: images/train val: images/val nc: 1 names: ['cordyceps']

这里nc: 1对应单类别,names的索引顺序必须与转换脚本里的class_names一致,否则训练时类别名和标签数字对不上。没有按这个结构整理的数据集,train 路径找不到图时 YOLOv5 会在 DataLoader 阶段报错,而不是训练中途才报,检查起来相对容易。

5. 单类别数据集增强训练时的隐藏风险与验证技巧

5.1 标注框重叠问题:mosaic 增强的天然克星

单类别数据集做 mosaic 增强时有个非常隐蔽的坑。Mosaic 会把四张图拼在一起,如果两张图里的冬虫夏草刚好拼到相邻区域,增强后生成的新标签框可能重叠或相交,模型学到的边界就变成"多个目标可以共享同一个框",反向拉低精确率。常见做法是在训练时调低 mosaic 概率:

# hyp.scratch-low.yaml 或训练命令中 mosaic: 1.0 # 默认 1.0,单类目标重叠频繁时可降到 0.5 mixup: 0.0 # 单类别小数据集建议关掉 mixup

冬虫夏草在草地上密集分布时,这种重叠更明显。训练完跑验证集 mAP 时如果发现 recall 高但 precision 持续走低,第一嫌疑就是增强阶段产生的框重叠,而不是模型容量不够。

5.2 用边界框统计判断 anchor 是否合理

613 张图片这个规模,anchor 大小直接决定小目标能否被召回。统计所有标注框的面积分布,如果框普遍很小,就要缩小默认 anchor 或开启 autoanchor:

python train.py --data data.yaml --cfg yolov5s.yaml --batch-size 16 --epochs 200 --hyp hyp.scratch-low.yaml

训练前自动计算的数据集 anchor 是基于你这份 613 张图的真实框分布重新聚类的,比 COCO 预训练 anchor 更有针对性。如果关闭 autoanchor,锚盒多大,模型的感受野就偏向多大,小目标容易漏检。

统计工具可以简单复用parse_voc_xml返回的wh,画一个散点图或直接打印分位数。冬虫夏草目标通常只占全图的 1% 到 3%,看到这样的分布不用惊讶,这是这类野外小目标数据集的常态。

5.3 可视化输出图的快速验收方法

跑完训练后,最终验证还是得看效果。用detect.py对验证集出图时,可以加--save-txt把检测结果也导出,重点检查两类假阳性:一是背景中的草茎被误检成冬虫夏草,这是样本背景多样性还不够;二是两根相邻目标被框成一个,这是 NMS 阈值设置偏低导致相邻高 IoU 框没有正确合并。通过对比可视化脚本生成的标注图和 detect 输出的预测图,能直观看出问题在标签侧还是模型侧。这个步骤不要跳过,它是最低成本的错误定位手段。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询