
简介一套可直接用于YOLOv3模型训练的苹果照片数据集包含Python预处理代码与标注文件面向从事目标识别与检测的开发者、研究者也适合刚接触目标检测的入门学习者解决数据集整理、尺寸不统一和格式适配等常见痛点。资源包整体约2000个文件以JPG图片、XML标注文件为主另有少量txt说明和py脚本包内收录原始苹果图片414张并提供828张经数据增强、resize和填充处理后的标准图片同时附有labelImg预选框形式标注的XML结果压缩包约90.81MB。目前已有1074人学习浏览热度适中适合正在进行YOLO系列实验或苹果实例检测项目的人员参考复用。配套Python代码可帮助使用者快速完成批量图片缩放、填充与增强逐步生成符合YOLOv3输入格式的样本集借助已标注文件也能直接用于模型训练前的数据划分与验证省去手动整理标签的重复劳动。1. 苹果数据集与 YOLOv3 目标识别一份带 Python 处理代码的 VOC2007 资源做苹果检测的人最头疼的往往不是模型选型而是缺一份标注干净、格式规整的数据集。公开数据集里车牌有 CCPD、遥感舰船有 HRSC2016但苹果这种农作业单类目标反而很难找到直接能喂给 YOLOv3 的资源。这份苹果数据集包含 414 张原始照片、经数据增强和 resize/填充处理后的 828 张图片以及配套的 .xml 标注文件和 Python 处理代码格式按 Pascal VOC2007 组织标注工具是 labelimg 的预选框方式。对正在跑 YOLOv3、或者打算用同一套流程迁移到 YOLOv8 的从业者来说它最大的价值不只是图片本身而是那份「增强 标注同步 格式转换」的代码能帮你把 414 张底图扩成可用的训练集同时避免我自己踩过的那些数据坑。这篇笔记就把数据集结构、处理逻辑、训练配置和排错记录完整拆开讲。2. 数据集拆解与 VOC2007 格式预处理流程和它的设计逻辑2.1 目录结构与文件清单这份资源按 VOC2007 的标准骨架组织拿到手先别急着跑代码把目录结构认一遍比什么都重要。我一般会先把压缩包解压后用tree或直接看文件管理器确认下面这几类内容。路径/文件内容说明JPEGImages/Apple (编号).jpg414 张原始图原始照片未做任何缩放尺寸不一预处理后图片目录828 张增强resize填充后的图每张原图对应一张增强副本数量翻倍Annotations/同名 .xml 文件labelimg 标注结果包含 bndbox 坐标Python 脚本数据增强、格式转换、划分脚本本资源的核心复用资产一个容易被忽略的点资源和很多公开检测数据集不一样它把「原始图」和「预处理图」分开了。这个设计很实用——原始图留给你做自定义增强、换输入尺寸预处理图则保证你拿到手就能直接进训练流程不用自己写 resize 逻辑。做目标检测的都知道后续训练改动输入尺寸、调整增强策略是常态两份数据并存能省很多反复。2.2 labelimg 标注的 .xml 里到底存了什么VOC2007 的标注文件是 XML 格式labelimg 保存的就是这个。打开任意一个 .xml内容结构长这样annotation folderJPEGImages/folder filenameApple (472).jpg/filename source databaseUnknown/database /source size width640/width height480/height depth3/depth /size object nameapple/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin120/xmin ymin80/ymin xmax260/xmax ymax200/ymax /bndbox /object /annotation这里size里的 width/height 是原图的像素尺寸所有 bndbox 坐标都是基于这个尺寸的绝对像素值。转换 YOLO 格式时必须拿这些坐标除以原图宽高而不是除以预处理后的尺寸。name是类别名这份资源里就是 apple。truncated和difficult是 VOC 协议里两个容易忽略的字段truncated 表示目标是否被边缘截断difficult 表示是否因过小或遮挡而难以识别训练时 YOLO 一般直接读 bndbox 和 name这两个字段对训练影响不大但转换脚本里要做好兼容否则遇到缺失字段会直接抛异常。另外注意文件名是Apple (472).jpg这种带括号和空格的格式。Linux 系的 shell 处理这种名字容易踩坑后续脚本里建议统一用 Python 的glob或os.listdir去读别在 bash 里裸跑通配符。2.3 为什么 414 张变成了 828 张数据增强和 fill 的取舍逻辑从数量看414 → 828 就是翻了一倍最常见的做法是每张原图生成一个水平翻转副本。单类目标检测数据量本来就少苹果这种目标形状相对规则、场景变化大的对象翻一倍能显著提升泛化。如果资源里实际用的是旋转或亮度扰动逻辑也是同一条增强带来的多样性必须大于它引入的标注误差。再说 resize 和填充。YOLOv3 默认输入是 416×416如果你的图是 640×480 之类的不等尺寸直接cv2.resize拉成方形会造成目标变形。苹果是圆形的横向拉伸成椭圆后模型学到的框宽高比就全乱了。正确做法是等比例缩放 灰色填充letterbox先按比例把长边缩放到 416短边补灰边。这份资源里明确写了「resize 和填充」说明走的正是 letterbox 路线这是处理非正方形输入的标准操作YOLOv8 的数据加载器内部也是这么做的。关于增强策略我的建议是复制一份做水平翻转是最安全的选择因为翻转时只有 x 坐标做w - x变换y 坐标不变不容易出错。而任意角度旋转、裁剪这类强增强虽然能提升模型对遮挡和姿态的鲁棒性但 bndbox 需要做完整的仿射变换甚至改写成多边形脚本复杂度高一个量级标注稍有偏差模型直接学歪。真要上强增强用 imgaug 的 BoundingBoxes 同步变换别手写坐标换算。3. 处理代码实战VOC 转 YOLO、数据集划分与增强脚本的落地写法3.1 VOC 标注转 YOLO txt脚本与换算公式YOLOv3 原生训练用的是 txt 格式标注每行代表一个目标格式是类别id x_center y_center width height四个坐标值都是归一化到[0,1]的浮点数。VOC 的 xml 转成这个格式核心就是坐标换算。下面是我常用的转换脚本直接放在资源目录下跑import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_map): tree ET.parse(xml_path) 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): name obj.find(name).text if name not in class_map: continue cls_id class_map[name] box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) x_center (xmin xmax) / 2.0 / w y_center (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}) stem os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, stem .txt), w) as f: f.write(\n.join(lines))坐标换算逻辑YOLO 的 x_center/y_center 是归一化中心点坐标所以先取 bndbox 的中间点再除以图像宽高width/height 同理。除的是原始宽高 w/h这一点必须和 xml 里size块保持一致。class_map 用来做类别名到 id 的映射单类数据集就是{apple: 0}用 id 而不是名字YOLO 的 obj.names 文件按行索引对应。跑完后抽查几个 txt坐标值都应该在 0 到 1 之间如果出现大于 1 的数基本可以断定是 xml 里 xmax 或 ymax 写错了。3.2 数据集划分训练集、验证集和文件清单生成darknet 训练时通过 train.txt 和 valid.txt 拿到图片路径列表每行一个绝对路径。划分的原则是原始图和它的增强副本必须进同一个集合不能一张在训练集、一张在验证集否则就是数据泄漏验证指标会虚高。所以划分操作要在增强流程之后做而不是先划分再增强。import glob import random import os images sorted(glob.glob(JPEGImages_aug/*.jpg)) random.seed(42) random.shuffle(images) split_idx int(len(images) * 0.8) train_files images[:split_idx] val_files images[split_idx:] def write_path_list(path_list, out_file): with open(out_file, w) as f: for p in path_list: f.write(os.path.abspath(p) \n) write_path_list(train_files, train.txt) write_path_list(val_files, val.txt)这里random.seed(42)固定随机种子保证每次划分结果一致方便复现。0.8/0.2 的训练验证比是检测任务的常用默认值。如果你是拿这份数据做比赛或调参对比建议把划分好的两个 txt 文件备份一份因为重新跑脚本会打乱顺序调参前后对比就看不出是数据变化还是参数变化了。特别注意glob.glob的路径如果包含空格和括号返回的字符串本身没问题但写入 txt 后darknet 在 C 层读文件按换行符分割空格和括号不影响解析这个实测没问题。真正要防的是在 bash 里写for f in JPEGImages_aug/*.jpg这种循环带括号的文件名会被 shell 解释出问题所以能用 Python 就别用 shell。3.3 数据增强脚本水平翻转、亮度扰动与标注同步这是整个资源里最容易出错的部分。很多人增强图片后直接把 xml 复制一份坐标完全没变训练出来的框全部偏移。下面是安全的水平翻转增强带完整的标注同步import cv2 import os import glob import xml.etree.ElementTree as ET xml_dir Annotations img_dir JPEGImages out_img_dir JPEGImages_aug out_xml_dir Annotations_aug os.makedirs(out_img_dir, exist_okTrue) os.makedirs(out_xml_dir, exist_okTrue) for xml_path in glob.glob(os.path.join(xml_dir, *.xml)): stem os.path.splitext(os.path.basename(xml_path))[0] img cv2.imread(os.path.join(img_dir, stem .jpg)) h, w img.shape[:2] flipped cv2.flip(img, 1) tree ET.parse(xml_path) root tree.getroot() for obj in root.iter(object): box obj.find(bndbox) xmin int(box.find(xmin).text) xmax int(box.find(xmax).text) # 水平翻转后xmin 变成 w - 原xmaxxmax 变成 w - 原xmin box.find(xmin).text str(w - xmax) box.find(xmax).text str(w - xmin) cv2.imwrite(os.path.join(out_img_dir, stem _flip.jpg), flipped) tree.write(os.path.join(out_xml_dir, stem _flip.xml))核心是翻转后的坐标变换新 xmin w - 原xmax新 xmax w - 原xmin。这个w必须是原图的宽不能是 resize 后的宽。y 坐标完全不动所以不需要改 ymin/ymax。如果还要做亮度扰动建议叠加在翻转副本上比如cv2.convertScaleAbs(flipped, alpha1.2, beta20)alpha 控制对比度倍数beta 控制亮度偏移量这类扰动不改变坐标安全无副作用。关于增强脚本我有一条血泪经验跑完增强后一定要随机抽 10 对原图、增强图把标注框画回图上肉眼检查。脚本逻辑写错是一回事labelimg 原始标注本身就可能有漏标和错标增强只会把错误同步放大。你可以用cv2.rectangle画框检查这一步花不了三分钟但能救回一整天的训练时间。4. 用这份数据跑通 YOLOv3目录组织、cfg 修改与训练参数设置4.1 常见做法先按 darknet 的目录约定重排数据darknet 对数据目录没有硬性要求但约定俗成的结构是单独建一个数据目录把图片、txt 标注、names 文件和 data 配置都归拢到一起。按 3.3 的增强脚本输出是 JPEGImages_aug 和 Annotations_aug 两个目录先转换成 txt 再统一移动mkdir -p yolov3_data/backup mv JPEGImages_aug yolov3_data/images # 把 3.1 转换出的 txt 文件也归入 data 目录 mv txt_labels yolov3_data/labels这样做的目的很直接后续训练命令里的路径不会乱七八糟backup 目录专门存训练过程中的权重备份默认每迭代 10000 次存一次。darknet 在 Windows 和 Linux 下的路径分隔符处理不一致建议全部用相对路径或在 obj.data 里写绝对路径避免跨平台时报文件找不到。4.2 obj.names 与 obj.data 的写法这是 YOLOv3 训练绕不开的两个配置文件。obj.names 只有一行因为这是单类数据集appleobj.data 指向训练集、验证集和配置文件classes 1 train /绝对路径/yolov3_data/train.txt valid /绝对路径/yolov3_data/val.txt names /绝对路径/yolov3_data/obj.names backup /绝对路径/yolov3_data/backupclasses 1必须和 obj.names 行数一致和 cfg 文件里的 classes 也要一致。三个地方对不上训练要么直接报错要么 loss 异常。train 和 valid 指向 3.2 生成的 txt文件名是 train.txt 和 val.txt 都没问题但 obj.data 里字段名是 train/valid不要写成训练集字段名。4.3 cfg 文件必改参数filters、classes 和 random训练前要基于 yolov3.cfg 复制一份自己的配置照着这份数据改三个位置。先看 filters每个 yolo 层之前的卷积层 filters 数等于3 × (classes 5)。单类数据集就是3 × (1 5) 18。YOLOv3 有三个 yolo 层每个 yolo 层前面都有一个[convolutional]层需要改搜索 filters255 的地方全部改成 18。参数原值改为说明filters3 处255183 × (classes 5)单类18classes3 处801对应 obj.data 的 classesrandom11 或 01多尺度训练开显存不够改 0random 这个参数很多人忽略。random1 时每个 batch 会动态改变输入尺寸320~608 之间随机能显著提升模型对不同尺度目标的适应能力代价是显存占用暴涨。如果你的显卡只有 8GB建议改成 0固定 416×416 训练否则很容易 OOM。4.4 训练命令与超参数取值参考用 darknet 的官方训练命令加载 darknet53.conv.74 预训练权重做迁移学习./darknet detector train yolov3_data/obj.data yolov3_data/yolov3-custom.cfg darknet53.conv.74 -mapdarknet53.conv.74是 darknet 官方提供的分类预训练权重用它初始化骨干网络只训练检测头收敛速度能快好几倍。-map参数会在验证集上每 4 个 epoch 计算一次 mAP用来观察模型是否真的在变好。单类小数据集的超参数我一般这样设参数取值说明batch64每批次样本数显存不够降到 32 并调大 subdivisionssubdivisions16把大 batch 拆成小块喂给显卡16 适合 8GB 卡learning_rate0.001迁移学习用这个起步比默认 0.01 稳妥max_batches8000单类 4000~8000 足够看 loss 是否进入平台期momentum / decay0.9 / 0.0005darknet 默认值改动机率小这套数据和配置文件同样能平滑迁移到 YOLOv8把 txt 标注按 ultralytics 的目录结构整理成 images/ 和 labels/然后yolo detect train datavoc.yaml modelyolov8n.pt就能跑。很多做 YOLOv8 训练自己数据集的人卡在标注格式转换这一步这份资源的 VOC 转 YOLO 脚本正好补上这个环节。5. 避坑排查这套流程里最容易翻车的五个细节5.1 现象训练 Loss 一直不降训练日志里 loss 从 20 掉到 2 之后就卡住不动甚至偶尔反弹。原因单类目标数据集太小模型很快拟合了训练集进入过拟合状态另一个常见原因是 learning_rate 太大loss 在最优值附近震荡。苹果这种目标外观变化有限的场景loss 卡在 1.x 是正常的不需要追求降到 0.1 以下。解决先把 learning_rate 从 0.001 降到 0.0001看 500 次迭代内有没有下降趋势其次确认数据增强是否只有翻转和亮度扰动如果增强种类过于单一加上随机裁剪和 HSV 扰动再训。最后看验证集 mAP如果 mAP 还在升而 loss 不降模型没坏是 loss 曲线进入平台期耐心等就好。5.2 现象检测框整体偏到图的角落或缩成一团训练没崩但预测时所有框都挤在某个区域或者框的尺寸明显不对。原因90% 的情况是标注转换时归一化坐标算错了。最常见是把 x_center 直接除以了预处理后的宽比如 416而标注本身是原始像素坐标归一化到[0,1]必须除原图宽高。如果原图是 640×480预处理缩放到 416两张图的宽不一样除错就直接把坐标算飞了。另一个曾让我翻车的原因是增强后的 xml 里 width/height 没改成增强图尺寸导致坐标错位。解决转换后抽一个 txt 看数值是否在[0,1]区间更直观的方法是写几行代码把 bndbox 画回图上看框是不是盖住苹果。这一步能做绝不跳过我已经因为偷懒吃过一次大亏。5.3 现象train.txt 路径报错或图片加载不出来darknet 训练刚开始就报Cant open image或读了 0 张图。原因train.txt 里写的是相对路径而 darknet 的工作目录和相对路径基准不一致。另外多数用户在 Windows 上生成的路径分隔符是\Linux 下训练时解析会出问题。还有文件名里的空格和括号在某些环境下会被误解析。解决统一在 obj.data 里写绝对路径txt 文件里也用绝对路径路径分隔符统一用手工替换replace(\\, /)。如果用的命令是./darknet detector train注意从项目根目录执行darknet 会把相对路径拼到当前工作目录上所以别边 cd 边换路径。5.4 现象增强后 XML 和图片数量对不上增强脚本跑完发现图片 828 张但 xml 只有 700 多或者有 xml 找不到同名图片。原因原始数据里可能有个别图片本身没有标注比如所有苹果都被遮挡或太远labelimg 保存时就没生成 xml或者增强脚本对每张图生成了副本但读取 xml 时遇到空文件抛异常中断导致后半程没跑完。labelimg 这类工具偶尔也会写出文件名和实际图片名大小写不一致的 xml。解决处理前先做一轮一致性检查——遍历所有 jpg检查同名 xml 是否存在遍历所有 xml反过来检查图片。缺失的直接删掉或单独挑出来人工补标。增强脚本里加个容错遇到解析失败的 xml 打印文件名并continue不要中断整个流程。5.5 现象resize 后苹果明显被拉伸变形预处理图里苹果从圆形变成椭圆训练出来的模型预测框宽高比也跟着变形。原因直接用cv2.resize(img, (416, 416))把不规则尺寸硬拉到正方形没有保持原始宽高比。YOLOv3 对变形图像不会报错但模型学到的目标形状特征是错的迁移到真实场景时框就不准了。解决用 letterbox 方案等比例缩放到 416×416 的短边填充。缩放比例取min(416/w, 416/h)填充后图里苹果还是圆的。注意数据增强脚本里如果用了翻转和 resize先翻转再 letterbox顺序反了坐标又得重算。做完之后随机看几张预处理图确认没有可见的拉伸。6. 进阶验证把训练好的模型拿到真实苹果照片上先过这三道关模型训完先在资源自带图片上跑一遍推理验证流程通顺只是第一步。我做目标检测的习惯是把手里的苹果照片分成三组一组拿来训练、一组做验证、一组专门「假装没看见」。最后一组图片不参与任何训练步骤只用来做最终测试这样得出的精度才有参考价值。如果手头还能拍到傍晚逆光、枝条遮挡、苹果紧挨着的照片混进测试组里比看的 mAP 更能反映真实场景的体感精度。第一关是尺度适配。苹果目标在照片里的大小跨度很大近景特写可能占图的一半果园远处一整棵树上的苹果可能只有十几个像素。YOLOv3 对小于 32×32 的目标基本无感所以测试时遇到小目标集中图把输入尺寸从 416 提到 608 再训一轮通常比调 NMS 阈值见效更快。第二关是负样本。只给模型看「有苹果的图」它会倾向于把一切红绿色块都当成苹果。建议额外收集几十张没有苹果的果园、天空、树干照片放进验证集观察误检率这个操作基本不增加训练成本但对落地体验提升很大。第三关是后处理阈值。darknet 默认置信度阈值 0.25对单类检测来说太松我会调到 0.5 以上。如果目标有遮挡导致检出率下降可以同时调低 NMS 的 IoU 阈值到 0.4两个框叠在一起时只保留得分高的那个框会干净很多。我曾经有一版模型在验证集 mAP 0.92拿进果园一测全是偏移框后来发现是增强副本泄漏进验证集导致指标虚高。从那以后我每次改数据流程或增强脚本强制抽 10 张训练图和 10 张验证图把标注框画回去肉眼检查同步性确认无误才开训练成了雷打不动的习惯。希望帮到你。本文还有配套的精品资源点击获取