简介:这份资源面向计算机视觉入门与进阶学习者,以及需要完成目标检测课程设计或实验项目的开发者,提供了一套可直接用于YOLOv3训练的苹果目标识别数据集及配套Python处理代码。包内共约2000个文件,以1648张jpg图像和820个xml标注文件为主,另含少量txt说明与py脚本,压缩包约90.81MB。图像采用labelimg以预选框形式标注,包含414张原始苹果图片与828张经数据增强、resize和填充处理后的图片,xml文件与图像一一对应,可直接转换为YOLO格式标签。配套Python代码覆盖数据预处理与格式转换环节,省去从零搭建数据管线的成本。目前已有1073人学习下载,适合希望快速跑通YOLOv3苹果检测流程、验证模型效果或作为二次开发起点的读者参考使用。
1. 苹果照片数据集和 Python 处理代码:414 张原图怎么撑起一个 YOLOv3 训练集
手上只有几百张苹果照片,想跑目标检测,第一反应往往是「数据不够」。这份 VOC2007 格式的苹果数据集正好卡在这个痛点上:414 张原始图片,经过数据增强、resize 和填充后扩到 828 张,配套 Python 处理脚本和 labelImg 生成的 xml 标注文件,直接对接 YOLOv3 的训练管线。它不是那种动辄几万张的公开大集,而是一个能让你在本地把「标注 → 增强 → 转格式 → 训练」整条链路跑通的完整小样本工程。适合两类人:一是刚接触目标检测、想找个真实数据集练手的新手,二是需要快速验证某个检测模型在小样本上表现的从业者。下面按「资源里有什么 → 怎么处理 → 怎么转 YOLO 格式 → 坑在哪」的顺序拆开讲。
2. 数据集结构与 Python 预处理脚本:从 414 张原图到 828 张增强样本
2.1 目录结构与文件命名规律
拿到压缩包解压后,典型的目录长这样:
VOC2007/ ├── Annotations/ # xml 标注文件,与图片同名 ├── JPEGImages/ # 原始图片,414 张 ├── ImageSets/ │ └── Main/ # 训练/验证集划分文件 ├── SegmentationClass/ ├── SegmentationObject/ └── preprocess/ # 预处理脚本与增强后图片 ├── augment.py └── output/ # 828 张增强图图片命名是Apple (472).jpg、Apple (600).jpg这种带括号和序号的格式。这个命名本身没问题,但在后续写脚本遍历时要注意:括号在 shell 里是特殊字符,用glob或os.listdir处理时不会出问题,但如果你用 shell 通配符批量操作,记得加引号或者转义。我一般直接用 Python 的pathlib遍历,省得跟 shell 转义较劲。
标注工具是 labelImg,预选框形式,生成的 xml 遵循 VOC2007 标准结构:<filename>、<size>、<object>里的<name>、<bndbox>四个坐标。这套结构是后面转 YOLO 格式的基础,先确认每个 xml 里的<name>字段统一是apple,不然后面类别映射会出乱子。
2.2 数据增强脚本拆解
增强脚本的核心逻辑不复杂,常见做法是用imgaug或albumentations做翻转、旋转、亮度调整,再把原图和增强图一起 resize 到统一尺寸并填充。下面是一个能直接跑的增强脚本骨架:
import cv2 import numpy as np from pathlib import Path import albumentations as A # 输入输出路径 SRC_DIR = Path("VOC2007/JPEGImages") DST_DIR = Path("VOC2007/preprocess/output") DST_DIR.mkdir(parents=True, exist_ok=True) # 增强管线:水平翻转 + 随机亮度 + 轻微旋转 transform = A.Compose([ A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(p=0.5), A.Rotate(limit=15, p=0.5, border_mode=cv2.BORDER_CONSTANT), ], bbox_params=A.BboxParams(format='pascal_voc', label_fields=['labels'])) TARGET_SIZE = 416 # YOLOv3 常用输入尺寸 def letterbox(img, target=416): """等比缩放 + 灰边填充,保持长宽比""" h, w = img.shape[:2] scale = target / max(h, w) nh, nw = int(h * scale), int(w * scale) resized = cv2.resize(img, (nw, nh)) canvas = np.full((target, target, 3), 128, dtype=np.uint8) top = (target - nh) // 2 left = (target - nw) // 2 canvas[top:top+nh, left:left+nw] = resized return canvas, scale, left, top for img_path in SRC_DIR.glob("*.jpg"): img = cv2.imread(str(img_path)) # 原图 resize + 填充 lb_img, _, _, _ = letterbox(img, TARGET_SIZE) cv2.imwrite(str(DST_DIR / f"orig_{img_path.name}"), lb_img) # 增强图 aug = transform(image=img, bboxes=[], labels=[])["image"] aug_img, _, _, _ = letterbox(aug, TARGET_SIZE) cv2.imwrite(str(DST_DIR / f"aug_{img_path.name}"), aug_img)这段代码有几个关键点要说清楚。letterbox是 YOLO 系列的标准预处理方式,等比缩放后补灰边,避免直接 resize 导致目标变形——苹果被拉扁了,模型学到的形状特征就废了。填充值用 128 而不是 0,是因为纯黑边在卷积里容易和真实暗色区域混淆,128 的中性灰更安全。增强部分只做了翻转、亮度、旋转三种,没上 cutout 或 mosaic,因为小样本下过度增强反而会让模型学到噪声。BboxParams里传了空的 bboxes,是因为这个脚本只处理图片,标注的同步变换要在转格式阶段一起做,否则框和图对不上,这是新手最容易翻车的地方。
2.3 增强前后的数量核对
原图 414 张,增强后 828 张,正好是 2 倍。说明脚本对每张原图生成了一张增强图,没有做多倍增强。这个量级对于 YOLOv3 来说偏小,但作为流程验证够用。如果你要实际训练,建议把增强倍数调到 3~5 倍,同时保证验证集只用原图,别把增强图混进 val,否则评估指标会虚高。划分训练验证集时,常见做法是按 8:2 切,414 张原图里拿 330 张训练、84 张验证,增强图只加进训练集。
3. VOC xml 转 YOLO txt:坐标归一化与类别映射的完整脚本
3.1 两种格式的本质差异
VOC 的 xml 存的是绝对像素坐标:xmin, ymin, xmax, ymax,原点在左上角。YOLO 的 txt 存的是归一化后的中心点坐标加宽高:class_id cx cy w h,每个值都在 0~1 之间。转换的核心就两步:把绝对坐标转成中心点加宽高,再除以图片的宽和高做归一化。听起来简单,但图片经过 letterbox 之后,坐标还得跟着缩放和平移一起变,不然框会偏。
3.2 转换脚本与参数说明
import xml.etree.ElementTree as ET from pathlib import Path import cv2 CLASS_MAP = {"apple": 0} # 类别名到 id 的映射,只有一个类 ANNO_DIR = Path("VOC2007/Annotations") IMG_DIR = Path("VOC2007/JPEGImages") LABEL_DIR = Path("VOC2007/labels") LABEL_DIR.mkdir(exist_ok=True) def convert_bbox(size, box): """VOC 绝对坐标 -> YOLO 归一化中心坐标""" dw, dh = 1.0 / size[0], 1.0 / size[1] x = (box[0] + box[1]) / 2.0 y = (box[2] + box[3]) / 2.0 w = box[1] - box[0] h = box[3] - box[2] return x * dw, y * dh, w * dw, h * dh for xml_file in ANNO_DIR.glob("*.xml"): tree = ET.parse(xml_file) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) lines = [] for obj in root.iter("object"): cls_name = obj.find("name").text if cls_name not in CLASS_MAP: continue cls_id = CLASS_MAP[cls_name] bndbox = obj.find("bndbox") box = ( float(bndbox.find("xmin").text), float(bndbox.find("xmax").text), float(bndbox.find("ymin").text), float(bndbox.find("ymax").text), ) cx, cy, bw, bh = convert_bbox((w, h), box) lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_path = LABEL_DIR / (xml_file.stem + ".txt") out_path.write_text("\n".join(lines))convert_bbox里先算中心点和宽高,再乘1/w和1/h做归一化。保留 6 位小数是 YOLO 社区的惯例,精度够用又不会让文件太大。CLASS_MAP只有一个类别apple映射到 0,如果你后面要加其他水果,在这里扩展就行,但记得类别 id 从 0 开始连续,别跳号。脚本会跳过不在CLASS_MAP里的类别,这是有意的——防止标注里混进拼写错误的类别名导致训练时报错。
3.3 转换后的校验方法
转完别急着训练,先做两件事。第一,随机抽 5 个 txt 文件,用 OpenCV 把框画回图上,肉眼看框有没有偏。第二,统计每个 txt 的行数,如果某张图应该有苹果但 txt 是空的,说明 xml 里类别名对不上或者框坐标有问题。校验脚本很短:
import cv2 from pathlib import Path img = cv2.imread("VOC2007/JPEGImages/Apple (472).jpg") h, w = img.shape[:2] for line in Path("VOC2007/labels/Apple (472).txt").read_text().splitlines(): cid, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw/2) * w) y1 = int((cy - bh/2) * h) x2 = int((cx + bw/2) * w) y2 = int((cy + bh/2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite("check.jpg", img)画出来的框如果贴着苹果边缘,说明转换正确;如果框整体偏移或大小不对,回头查 xml 里的size字段和实际图片尺寸是否一致——有些标注工具会写错宽高,这是血泪经验里最常见的一类问题。
4. 避坑与排查:小样本苹果数据集训练时最容易翻车的五件事
4.1 现象:训练 loss 一直不降,mAP 接近 0
原因通常是类别映射错了。YOLOv3 的classes配置和CLASS_MAP对不上,比如配置文件里写了 80 类,实际只有 1 类,模型输出的通道数不匹配,梯度全是乱的。解决:检查yolov3.cfg里yolo层上方的classes=1,以及filters=18(公式是(classes+5)*3),两个数必须同步改。
4.2 现象:框画出来整体偏右下或偏左上
原因是 letterbox 的填充偏移没同步到标注上。原图 resize 加填充后,目标的绝对坐标变了,但 xml 里还是原图坐标。解决:要么在转格式前先把 xml 坐标按 letterbox 的 scale 和 padding 变换一遍,要么干脆不做 letterbox,直接 resize 到正方形——后者会变形,但小样本下影响可控,看你取舍。
4.3 现象:验证集 mAP 很高,实际测试一塌糊涂
原因是增强图混进了验证集。增强图和原图高度相似,验证集里出现增强图等于变相泄露训练数据。解决:划分数据集时只从原图里切验证集,增强图全部归训练集,并且验证集的 txt 只对应原图。
4.4 现象:训练到一半报ZeroDivisionError或框面积为 0
原因是 xml 里有xmin == xmax或ymin == ymax的退化框,归一化后宽高为 0。解决:在转换脚本里加一行过滤,if bw <= 0 or bh <= 0: continue,把无效框丢掉,别让它进训练。
4.5 现象:图片读进来是 None,cv2.imread 返回空
原因是文件名里的括号或空格在某些环境下被截断,或者路径编码不对。解决:用cv2.imdecode(np.fromfile(path, dtype=np.uint8), -1)替代cv2.imread,能绕开中文路径和特殊字符的坑。这个习惯我后来在所有涉及中文或特殊字符路径的项目里都保留着。
5. 从 828 张到可用模型:锚框聚类与训练参数微调
小样本训练 YOLOv3,锚框(anchor)用默认的 COCO 聚类结果往往不合适,因为苹果的宽高比和 COCO 里的通用目标差别大。我一般会用自己的标注跑一遍 k-means,聚出 9 个锚框替换掉 cfg 里的默认值。脚本核心是拿所有归一化后的宽高做聚类:
import numpy as np from pathlib import Path from sklearn.cluster import KMeans wh = [] for txt in Path("VOC2007/labels").glob("*.txt"): for line in txt.read_text().splitlines(): _, _, _, bw, bh = map(float, line.split()) wh.append([bw, bh]) wh = np.array(wh) kmeans = KMeans(n_clusters=9, random_state=0).fit(wh) anchors = kmeans.cluster_centers_ anchors = anchors[np.argsort(anchors[:, 0] * anchors[:, 1])] print("anchors =", [round(a, 4) for pair in anchors for a in pair])聚出来的锚框按面积从小到大排,填进 cfg 的anchors行。注意 YOLOv3 的锚框是相对 grid 的,不是相对整图的,所以这里用归一化宽高聚类后,还要乘以特征图尺寸(比如 13、26、52)才是最终值。这一步容易搞混,我踩过一次,锚框大了十几倍,训练直接不收敛。
训练参数上,小样本要把学习率调低,常见做法是从 0.001 起步,用 step 衰减,batch 设 8 或 16,看显存。迭代次数别照搬大集的几十万次,828 张图跑 2000~4000 轮就够观察趋势了。数据量小的时候,早停比固定轮数更靠谱,验证 loss 连续 10 轮不降就停。
验证模型有没有真的学到东西,别只看 mAP。我习惯抽几张训练时没见过的苹果照片,用训练好的权重跑一遍推理,把框画出来看。如果框能稳定贴住苹果,哪怕 mAP 只有 0.6,这个模型在实际场景里也是能用的;反过来 mAP 0.8 但框飘,多半是验证集划分有问题。从那以后我每次做完小样本训练,都强制走一遍「抽图 → 推理 → 画框 → 肉眼过」这个流程,比盯指标踏实。希望帮到你。
本文还有配套的精品资源,点击获取