简介:这份资源面向计算机视觉入门与进阶学习者,以及需要开展目标检测实战的开发者,提供了一套可直接用于YOLOv3训练的苹果数据集及配套Python处理代码。包内包含414张苹果原始图片、经数据增强与resize填充后扩充至828张的预处理图片,以及对应的XML标注文件,标注采用labelimg预选框形式完成,可直接对接YOLOv3的数据读取流程。资源共2000个文件,以1648个jpg图像、820个xml标注为主,另含少量txt说明与3个py脚本,压缩包约90.81MB,目录结构清晰,便于按原始数据、增强数据与代码模块分别检索。借助这套素材,读者能够省去自行采集与标注的成本,快速跑通数据增强、格式转换与模型训练链路,并对照脚本理解图像填充、标注解析等关键环节,适合作为目标识别课程的实验素材或检测项目的起步数据。目前已有1073人学习下载。
1. 苹果照片数据集配 Python:从 VOC2007 标注到可训练样本的完整链路
手里有一份VOC2007.zip,里面装的是苹果照片和对应的 XML 标注,想拿它训练一个能识别苹果的检测模型,却卡在「解压之后不知道从哪下手」——这是很多做农业视觉、果蔬分拣、零售称重项目的朋友都会遇到的场景。苹果照片数据集本身不复杂,难的是把 VOC2007 这套老而稳的标注格式,变成 YOLO、SSD、Faster R-CNN 都能直接吃的输入。Python 在这里扮演的角色不是「跑个脚本」这么简单,它要完成解压校验、XML 解析、坐标换算、数据集划分、可视化抽检这一整条流水线。这篇内容面向三类人:刚配好 Python 环境想找个真实数据集练手的新手、做果蔬检测需要快速验证 baseline 的算法工程师、以及要把标注数据接进自己训练框架的工程同学。下面按「先看懂 VOC2007 结构,再写解析代码,最后避坑」的顺序讲透。
2. VOC2007 标注结构拆解:苹果照片数据集里到底存了什么
2.1 目录树与三个核心文件夹
拿到VOC2007.zip解压后,标准结构长这样:
VOC2007/ ├── Annotations/ # 每张图对应一个 XML,存目标框和类别 ├── ImageSets/ │ └── Main/ # train.txt / val.txt / trainval.txt / test.txt ├── JPEGImages/ # 原始照片,文件名与 XML 一一对应 └── SegmentationClass/ # 分割掩码,检测任务用不到苹果照片数据集通常只用到前三个。Annotations里的 XML 是核心,它记录了图片尺寸、每个苹果的边界框bndbox(xmin/ymin/xmax/ymax)和类别名name。ImageSets/Main下的 txt 文件只是文件名列表,不带路径和后缀,很多人第一次读会在这里翻车。JPEGImages里图片格式可能混着.jpg和.JPG,Linux 下大小写敏感,写代码时要么统一转小写,要么用glob忽略大小写匹配。
2.2 一个 XML 的字段含义与坐标系
打开任意一个苹果的 XML,关键片段如下:
<annotation> <folder>VOC2007</folder> <filename>apple_0001.jpg</filename> <size> <width>500</width> <height>375</height> <depth>3</depth> </size> <object> <name>apple</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>112</xmin><ymin>96</ymin> <xmax>308</xmax><ymax>290</ymax> </bndbox> </object> </annotation>size里的宽高是原图尺寸,bndbox用的是左上角原点、x 向右、y 向下的像素坐标,且是 1-based(从 1 开始计数)。这一点极其关键:VOC 的坐标是 1-based 闭区间,而 YOLO 要求的是 0-based 归一化中心点加宽高。差一个像素在训练时不会报错,但会让框整体偏移,mAP 掉几个点你都找不到原因。difficult字段表示该目标是否难以识别,VOC 官方评测时会忽略 difficult=1 的样本,但训练时是否保留要看你的策略——苹果数据里如果存在大量遮挡果,建议保留训练、评测时再过滤。
2.3 为什么选 VOC2007 而不是直接上 COCO
苹果这类单一品类数据集,目标数量少、类别只有一到两个,用 COCO 那套 JSON 标注属于杀鸡用牛刀。VOC2007 的优势在于:XML 可读性强,Python 标准库xml.etree.ElementTree就能解析,不依赖 pycocotools;社区里几乎所有检测框架都保留了对 VOC 格式的导入接口;文件体积小,苹果照片数据集通常几百张到几千张,解压和遍历都是秒级。常见做法是先用 VOC 格式把 pipeline 跑通,确认模型能收敛,再考虑要不要转成 COCO 做多类别扩展。
3. 用 Python 解析苹果照片数据集:从 XML 到训练列表
3.1 环境准备与依赖确认
先确认 Python 版本和必要库。VOC 解析本身只需要标准库,但可视化抽检会用到 OpenCV 和 matplotlib:
python --version # 建议 3.8 及以上 pip install opencv-python matplotlib numpy如果你用的是 PyCharm 或 VSCode,把解释器指到同一个环境即可。xml.etree.ElementTree是内置的,不需要额外装。这里不涉及任何需要联网下载的模型权重,纯本地处理。
3.2 解析单个 XML 并提取目标框
下面这段代码把一张图的 XML 读成结构化数据,是整条流水线的基础:
import xml.etree.ElementTree as ET from pathlib import Path def parse_voc_xml(xml_path): """解析单个 VOC XML,返回图片尺寸和目标列表""" tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) objects = [] for obj in root.findall('object'): name = obj.find('name').text.strip() difficult = int(obj.find('difficult').text) box = obj.find('bndbox') # VOC 坐标是 1-based,转成 0-based xmin = int(box.find('xmin').text) - 1 ymin = int(box.find('ymin').text) - 1 xmax = int(box.find('xmax').text) - 1 ymax = int(box.find('ymax').text) - 1 objects.append({ 'name': name, 'difficult': difficult, 'bbox': [xmin, ymin, xmax, ymax] }) return {'width': width, 'height': height, 'objects': objects} if __name__ == '__main__': info = parse_voc_xml('VOC2007/Annotations/apple_0001.xml') print(info['width'], info['height']) for o in info['objects']: print(o['name'], o['bbox'])逻辑说明:ET.parse把 XML 加载成树,find取单个子节点,findall取所有同名节点。坐标减 1 是必须的,因为 VOC 从 1 开始计数,而后续所有计算都基于 0-based。difficult单独取出来,方便后面按需过滤。参数上唯一要注意的是name字段可能有前后空格,.strip()不能省,否则类别名对不上。
3.3 批量遍历并生成训练清单
单张解析只是验证,真正要的是把整个Annotations目录扫一遍,生成带路径的清单文件:
import xml.etree.ElementTree as ET from pathlib import Path import random def build_dataset(root_dir, val_ratio=0.2, seed=42): root = Path(root_dir) ann_dir = root / 'Annotations' img_dir = root / 'JPEGImages' records = [] for xml_file in sorted(ann_dir.glob('*.xml')): stem = xml_file.stem # 兼容 .jpg 和 .JPG img_path = None for ext in ('.jpg', '.JPG', '.jpeg', '.png'): candidate = img_dir / (stem + ext) if candidate.exists(): img_path = candidate break if img_path is None: print(f'[跳过] 找不到图片: {stem}') continue info = parse_voc_xml(str(xml_file)) if not info['objects']: continue # 无目标的负样本,按需保留 records.append({ 'image': str(img_path), 'width': info['width'], 'height': info['height'], 'objects': info['objects'] }) random.seed(seed) random.shuffle(records) n_val = int(len(records) * val_ratio) val_set = records[:n_val] train_set = records[n_val:] return train_set, val_set if __name__ == '__main__': train, val = build_dataset('VOC2007') print(f'训练集 {len(train)} 张,验证集 {len(val)} 张')逻辑说明:glob('*.xml')遍历所有标注,stem拿到不带后缀的文件名,再用多个扩展名去JPEGImages里找对应图片,这一步解决了大小写和后缀不一致的问题。random.seed(42)固定随机种子,保证每次划分结果一致,方便复现实验。val_ratio默认 0.2,苹果数据量小的时候可以调到 0.15,把更多样本留给训练。无目标的负样本默认跳过,如果你的场景需要降低误检,可以把它们保留进训练集但标注为空。
3.4 转成 YOLO 格式的归一化坐标
如果你打算用 YOLO 系列训练,需要把 VOC 的绝对坐标转成归一化的中心点加宽高:
def voc_to_yolo(bbox, img_w, img_h): """VOC [xmin,ymin,xmax,ymax] -> YOLO [cx,cy,w,h] 归一化""" xmin, ymin, xmax, ymax = bbox cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 裁剪到 [0,1],防止标注越界 cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) return [cx, cy, w, h]逻辑说明:中心点用(xmin+xmax)/2再除以宽高,宽高直接除以对应尺寸。最后的裁剪是后悔药——苹果数据集里偶尔会有标注框超出图片边界的脏数据,不裁剪会导致 YOLO 训练时 loss 异常。类别索引需要你自己维护一个{'apple': 0}的映射表,写 label 文件时用索引而不是类别名。
4. 数据校验与可视化抽检:别让脏标注混进训练集
4.1 三类必须拦截的脏数据
苹果照片数据集在人工标注阶段容易留下三类问题:框的xmin >= xmax或ymin >= ymax,坐标超出图片尺寸,以及类别名拼写不一致(比如apple和Apple混用)。这三类问题不会让代码报错,但会让模型学到错误的分布。校验逻辑应该放在解析之后、写清单之前:
def validate_record(rec): """返回 (是否合法, 原因)""" w, h = rec['width'], rec['height'] for obj in rec['objects']: xmin, ymin, xmax, ymax = obj['bbox'] if xmin >= xmax or ymin >= ymax: return False, f"框退化: {obj['bbox']}" if xmin < 0 or ymin < 0 or xmax > w or ymax > h: return False, f"框越界: {obj['bbox']} 图片 {w}x{h}" if obj['name'] != obj['name'].strip(): return False, f"类别名有空格: '{obj['name']}'" return True, 'ok'逻辑说明:退化框和越界框直接判非法,类别名带空格判非法。实际用的时候可以把非法记录写进一个bad_cases.txt,人工复核后再决定是修正还是丢弃,不要静默跳过。
4.2 用 OpenCV 画框抽检
数字校验只能查格式,框画得对不对还得靠眼睛。随机抽 20 张画框保存,是投入训练前最划算的一步:
import cv2 import random def visualize_samples(records, out_dir, num=20): Path(out_dir).mkdir(parents=True, exist_ok=True) samples = random.sample(records, min(num, len(records))) for i, rec in enumerate(samples): img = cv2.imread(rec['image']) if img is None: continue for obj in rec['objects']: xmin, ymin, xmax, ymax = obj['bbox'] cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, obj['name'], (xmin, ymin - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(f'{out_dir}/check_{i:03d}.jpg', img)逻辑说明:cv2.rectangle用 0-based 坐标直接画,和解析结果一致。putText的 y 坐标减 5 是为了让文字不压住框线。抽检时重点看三种情况:框是否紧贴苹果边缘、有没有漏标、多个苹果时框有没有重叠错位。这一步花十分钟,能省下后面几小时的调参玄学。
4.3 统计类别分布与框尺寸
训练前还应该看一眼类别数量和框的尺寸分布,苹果数据集如果只有一类,那分类头可以设得很小;如果框普遍偏小,anchor 尺寸要相应调整:
import numpy as np def dataset_stats(records): all_w, all_h, count = [], [], 0 for rec in records: for obj in rec['objects']: xmin, ymin, xmax, ymax = obj['bbox'] all_w.append(xmax - xmin) all_h.append(ymax - ymin) count += 1 all_w, all_h = np.array(all_w), np.array(all_h) print(f'总目标数: {count}') print(f'框宽 中位数 {np.median(all_w):.1f} 均值 {all_w.mean():.1f}') print(f'框高 中位数 {np.median(all_h):.1f} 均值 {all_h.mean():.1f}') print(f'框面积占比中位数: {np.median(all_w * all_h) / (500 * 375):.3f}')逻辑说明:中位数比均值更能反映典型框尺寸,因为个别超大框会把均值拉偏。面积占比用了一个假设的图片尺寸,实际应该用每张图自己的宽高算完再取中位数。这些统计值直接决定你选 YOLOv5n 还是 YOLOv5s,以及 anchor 要不要重新聚类。
5. 避坑与排查:苹果照片数据集处理中最容易翻车的五件事
5.1 现象:训练 loss 正常下降但 mAP 极低
原因:VOC 坐标 1-based 没有转 0-based,或者转 YOLO 时忘了归一化。框整体偏移一两个像素在 loss 上看不出来,但 IoU 匹配会大面积失败。解决:在解析函数里强制减 1,转 YOLO 时强制除以宽高,并在可视化抽检时确认框贴合。
5.2 现象:部分图片读进来是 None
原因:cv2.imread遇到中文路径或损坏文件会返回 None,而JPEGImages里可能混有非图片文件。解决:读图后判空跳过,同时用Path.suffix.lower()过滤扩展名,只保留.jpg/.jpeg/.png。
5.3 现象:类别名对不上,训练时提示 unknown class
原因:XML 里apple和Apple混用,或者带前后空格。解决:解析时统一.strip().lower(),并在生成 label 前用集合打印所有出现过的类别名,人工确认一遍。
5.4 现象:验证集和训练集有重复图片
原因:ImageSets/Main里原本的划分和随机划分叠加,或者同一张图有多个 XML。解决:以文件名 stem 为唯一键去重,划分前先set一遍,不要依赖目录里没有重复。
5.5 现象:小目标苹果检测效果差
原因:苹果在图中占比小,默认 anchor 偏大。解决:用dataset_stats看框尺寸中位数,如果普遍小于 32 像素,在 YOLO 配置里增加小尺寸 anchor,或者把输入分辨率从 416 提到 640。
6. 把苹果数据集接进训练框架:一个可复用的转换脚本骨架
前面几章把解析、校验、可视化都拆开了,实际项目里我会把它们串成一个命令行脚本,一次跑完从解压到生成 YOLO 目录的全流程。骨架如下:
import argparse from pathlib import Path def main(): parser = argparse.ArgumentParser() parser.add_argument('--voc_root', default='VOC2007') parser.add_argument('--out_root', default='datasets/apple') parser.add_argument('--val_ratio', type=float, default=0.2) args = parser.parse_args() train, val = build_dataset(args.voc_root, args.val_ratio) out = Path(args.out_root) for split, records in (('train', train), ('val', val)): img_out = out / 'images' / split lbl_out = out / 'labels' / split img_out.mkdir(parents=True, exist_ok=True) lbl_out.mkdir(parents=True, exist_ok=True) for rec in records: stem = Path(rec['image']).stem # 复制图片 import shutil shutil.copy(rec['image'], img_out / f'{stem}.jpg') # 写 YOLO label lines = [] for obj in rec['objects']: cls_id = 0 # 单类 apple cx, cy, w, h = voc_to_yolo(obj['bbox'], rec['width'], rec['height']) lines.append(f'{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}') (lbl_out / f'{stem}.txt').write_text('\n'.join(lines)) # 生成 data.yaml yaml_text = f"""path: {out.resolve()} train: images/train val: images/val nc: 1 names: ['apple'] """ (out / 'data.yaml').write_text(yaml_text) print('转换完成,data.yaml 已生成') if __name__ == '__main__': main()逻辑说明:脚本把图片复制到images/train和images/val,label 写到对应labels目录,最后生成 YOLO 训练需要的data.yaml。cls_id写死为 0 是因为苹果数据集通常单类,多类的话维护一个name_to_id字典即可。坐标保留 6 位小数足够,再多是浪费。shutil.copy会实际占用磁盘,数据量大时可以改成软链接。
参数上,--val_ratio控制验证集比例,--out_root决定输出位置。跑完之后用yolo detect train data=datasets/apple/data.yaml model=yolov8n.pt epochs=100 imgsz=640就能起训。第一次跑建议epochs=10先验证 pipeline 通不通,确认 loss 在降、验证集能出框,再拉长训练。
一个我踩过的坑:data.yaml里的path必须用绝对路径,相对路径在不同工作目录下启动训练会找不到图片,报错信息还很隐晦。另外names列表的顺序必须和 label 里的cls_id严格对应,单类时无所谓,多类时错一位整个模型就废了。
这套流程我在几个果蔬检测项目里反复用过,从 VOC2007 到 YOLO 格式,核心代码不到 200 行,但每一步的边界条件都得抠清楚。苹果照片数据集本身不复杂,复杂的是标注里的脏数据和格式转换时的坐标陷阱。把可视化抽检当成习惯,每次换数据集都先画 20 张看看,比事后调参省心得多。希望帮到你。
本文还有配套的精品资源,点击获取