资讯动态

2240张香蕉图:从VOC到YOLO格式转换与YOLOv8训练实战

发布时间:2026/9/15 2:44:34 来源:尧图企业网站定制
简介这是一份面向目标检测入门与香蕉识别场景的专用数据集适合计算机视觉学习者、算法工程师用于训练香蕉检测模型或验证检测流程。压缩包约2000个文件以VOC格式的XML标注文件为主体配合TXT说明文件整体容量361.72MB便于下载与解压。已有428人学习具备一定的参考价值。数据内容上包含2243张香蕉图片并提供了对应的VOC格式XML与YOLO格式TXT标注标注类别统一为banana共标记9195个目标框标注过程使用labelImg完成矩形框标注规则清晰可直接用于YOLO系列、Faster R-CNN等目标检测模型的训练与评估免去自行整理和转换标注格式的环节。1. 2240 张香蕉图足够把 VOC 和 YOLO 两套格式的流转练扎实很多人手里攒了一批图片第一反应就是直接丢进训练脚本里跑结果十有八九卡在数据上标注是 XML 的训练脚本要 txt图片 2400 张标注只有 2100 个训练起来 loss 不降最后才发现类别列表顺序对不上。2240 张的香蕉数据集正好卡在一个微妙的体量上——它比 MNIST 那种玩具集复杂又比 COCO 那种全量集轻得多足够完整走一遍真实标注 → 格式整理 → 训练 → 验证的流程又不至于让人在等训练时失去耐心。它的价值不是把 mAP 刷到多高而是把 VOC 的 XML 和 YOLO 的 txt 两套标注体系彻底对比清楚顺手沉淀出一个通用预处理流程。之后换果实、换零件、换车型骨架完全不用动。这篇文章按从业者做数据预处理的常规路线展开先讲格式差异再给转换脚本最后落到用训练结果反查标注质量。2. VOC 和 YOLO 格式的字段差异先看懂再动手2.1 VOC 的 XML 标注与目录约定VOC 格式源自 PASCAL VOC 比赛后来被 LabelImg、CVAT 等工具广泛支持成了一个事实标准。一个 VOC 数据集通常分成四个目录JPEGImages 放原图Annotations 放与图片同名的 XML 标注ImageSets/Main 放划分好的 train.txt 和 val.txt里面每一行是图片文件名不带扩展名。Layout 和 SegmentationClass 在目标检测场景里可以忽略。XML 内部的结构比较直观annotation folderJPEGImages/folder filenamebanana_001.jpg/filename size width640/width height480/height depth3/depth /size object namebanana/name bndbox xmin120/xmin ymin98/ymin xmax430/xmax ymax380/ymax /bndbox /object /annotation关键在size和bndbox坐标是像素级整数原点在图像左上角x 向右、y 向下。一个object对应一个标注框多类别就多写几个name不同的 object。size里的宽高必须和真实图片一致不然后面转 YOLO 时坐标全部偏移。用 CVAT 导出 VOC 时也会得到类似结构但文件夹名可能不叫 Annotations需要留意。2.2 YOLO 的 txt 标注与归一化坐标YOLO 格式只有一个 txt 文件加一个类别清单txt 与图片同名放在 labels 目录下。每一行代表一个目标0 0.4297 0.4979 0.4844 0.5875从左到右依次是类别索引、归一化后的中心点 x、中心点 y、归一化后的宽 w、归一化后的高 h。归一化是指除以图片原始宽高所有数值都落在 0 到 1 之间。这样做的好处是训练时无论把图片缩放到 640 还是 1280标注都不用改模型内部会按比例换算。类别索引不是字符串而是 classes.txt 里的行号。比如 classes.txt 只有一行 banana那么索引就是 0如果还有一行的确良 bakerybakery 就是 1。这个顺序一旦定了就不能随意调整否则训出来的模型类别标签全是错的。CVAT 导出 YOLO 格式时通常自带一个 obj.names 或 classes.txt里面按行排列类别名顺序就是索引顺序拿到手先看这个文件。2.3 两种格式的相互转换与容易踩的坑转换公式本身非常机械唯一的数学工作是从左上右下换成中心宽高VOC 字段YOLO 字段转换公式xmin、xmaxx_center(xmin xmax) / 2 / widthymin、ymaxy_center(ymin ymax) / 2 / heightxmax - xminw(xmax - xmin) / widthymax - yminh(ymax - ymin) / heightobject/nameclass_id在类别列表中的索引实际踩坑点集中在五个地方。第一宽高必须用 XML 里size的值不要重新用 PIL 或 OpenCV 读图一是慢二是部分手机拍摄的图片带 EXIF 旋转信息读出来的宽高可能和标注时的坐标系不一致。第二坐标必须归一化整数 428 写成 0.67 才能跨分辨率迁移。第三一张图如果没有标注XML 里没有 object转换后应该生成一个空 txt而不是不生成文件否则训练时 YOLO 会因为图片没有对应标签跳过或者报错。第四类别列表顺序要固定banana 是 0后面加类别只能往后追加。第五越界框XML 里的坐标如果超出了图片边界转换后会出现大于 1 或小于 0 的值常见于半自动标注工具需要裁剪或过滤。这个转换逻辑和 KITTI 标注转 YOLO 是同一套公式只是 KITTI 的坐标用浮点数、类别名是 Car/Pedestrian 之类换成自己的类别列表即可。3. 把 2240 张散图整理成标准目录并完成 VOC→YOLO 转换3.1 原始数据检查与三项一致性校验拿到原始数据第一件事不是写转换脚本而是做数据体检。我一般先跑三个命令看家底# 统计图片数量和标注数量 find . -maxdepth 2 -name *.jpg | wc -l find . -maxdepth 2 -name *.xml | wc -l # 找出没有配对 xml 的图片 for img in images/*.jpg; do f${img%.jpg} [ -f annos/$f.xml ] || echo missing: $img done # 统计文件名大小写不一致的情况 find . -maxdepth 2 -name *.JPG | head -20第一个命令里的-maxdepth 2限制递归深度到两层wc -l统计行数第二个命令里${img%.jpg}是 bash 的变量后缀移除把images/banana_001.jpg变成images/banana_001再拼上annos/和.xml检查配对文件是否存在。重点看三项图片数是否等于标注数、有没有只标了一半的断头情况、jpg 和 JPG 是否混用。Linux 下文件名大小写敏感banana_001.jpg和banana_001.JPG会被当成两个文件如果混用训练时小白容易在加载阶段报图片找不到。发现大小写混用先统一再继续cd images for f in *.JPG; do [ -f $f ] mv $f ${f%.JPG}.jpg done这组命令把大写扩展名批量改成小写。注意mv之前加了一个存在性判断防止扩展名不匹配时报错。3.2 目录标准化和 8:2 划分训练验证集数据体检通过后把所有文件规整成标准 VOC 目录同时做训练验证划分。这一步用 Python 更方便控制随机种子和比例import random, shutil from pathlib import Path IMG_SRC Path(raw_images) XML_SRC Path(raw_xml) OUT Path(banana_voc) random.seed(42) (OUT / JPEGImages).mkdir(parentsTrue, exist_okTrue) (OUT / Annotations).mkdir(parentsTrue, exist_okTrue) (OUT / ImageSets/Main).mkdir(parentsTrue, exist_okTrue) imgs sorted(IMG_SRC.glob(*.jpg)) sorted(IMG_SRC.glob(*.JPG)) imgs [p for p in imgs if (XML_SRC / f{p.stem}.xml).exists()] random.shuffle(imgs) train_ratio 0.8 n_train int(len(imgs) * train_ratio) train_files, val_files imgs[:n_train], imgs[n_train:] for p in train_files val_files: shutil.copy(p, OUT / JPEGImages / p.name) shutil.copy(XML_SRC / f{p.stem}.xml, OUT / Annotations / f{p.stem}.xml) def write_list(path, files): with open(path, w) as f: for p in files: f.write(p.stem \n) write_list(OUT / ImageSets/Main/train.txt, train_files) write_list(OUT / ImageSets/Main/val.txt, val_files) print(ftrain{len(train_files)}, val{len(val_files)})说明几个参数random.seed(42)固定随机种子保证每次脚本跑出的划分一致方便复现实验glob(*.jpg) glob(*.JPG)兼容两种扩展名shutil.copy是复制不是移动原始数据保留一份出问题能回滚p.stem取不带扩展名的文件名写入 train.txt 的正是这个。如果 2240 张里你想把验证集加大到 3:7改train_ratio 0.7即可K 折交叉验证的话把这个循环套一层就行。3.3 VOC 转 YOLO解析 XML 并计算归一化坐标目录整理完写转换脚本。这个脚本是数据集预处理的通用模板换数据集只用改 CLASSES 列表import xml.etree.ElementTree as ET from pathlib import Path import shutil CLASSES [banana] VOC_ROOT Path(banana_voc) YOLO_ROOT Path(banana_yolo) for split in [train, val]: txt_file VOC_ROOT / ImageSets/Main / f{split}.txt out_label_dir YOLO_ROOT / split / labels out_img_dir YOLO_ROOT / split / images out_label_dir.mkdir(parentsTrue, exist_okTrue) out_img_dir.mkdir(parentsTrue, exist_okTrue) for line in txt_file.read_text().splitlines(): xml_path VOC_ROOT / Annotations / f{line}.xml if not xml_path.exists(): continue tree ET.parse(xml_path) root tree.getroot() img_w float(root.find(size/width).text) img_h float(root.find(size/height).text) out_lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASSES: continue 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 / img_w y_center (ymin ymax) / 2 / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h out_lines.append( f{CLASSES.index(name)} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f} ) (out_label_dir / f{line}.txt).write_text(\n.join(out_lines)) img_file list((VOC_ROOT / JPEGImages).glob(f{line}.*)) if img_file: shutil.copy(img_file[0], out_img_dir / img_file[0].name)这段代码的核心是root.find(size/width)直接从 XML 里取原始宽高不触发额外的图像 IO处理 2240 张也就几秒。CLASSES.index(name)做字符串到索引的转换保证 txt 第一列是整数 0 而不是 banana。输出保留 6 位小数足够还原像素级框位置。没有 object 的图会生成空 txtYOLO 训练脚本能正常处理空标签文件。类别不在 CLASSES 里的 object 被跳过这一步同时起到了过滤脏数据的作用。3.4 转换结果自检把 txt 画回图上看一眼转换完必须验证别直接开训。最有效的办法是把 txt 里的归一化坐标画回图上肉眼抽查:import cv2 from pathlib import Path def draw_yolo(img_path, label_path, out_path, classes): img cv2.imread(str(img_path)) ih, iw img.shape[:2] for line in Path(label_path).read_text().splitlines(): parts line.split() if len(parts) ! 5: print(fbad line: {label_path} - {line}) continue cid, xc, yc, bw, bh map(float, parts) x1 int((xc - bw / 2) * iw) y1 int((yc - bh / 2) * ih) x2 int((xc bw / 2) * iw) y2 int((yc bh / 2) * ih) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, classes[int(cid)], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(str(out_path), img) for img_path in list(Path(banana_yolo/val/images).glob(*.jpg))[:5]: stem img_path.stem draw_yolo(img_path, Path(fbanana_yolo/val/labels/{stem}.txt), Path(fcheck_{stem}.jpg), [banana])draw_yolo把归一化坐标乘回原图宽高画出矩形和类别名checked后缀图片保存的是前 5 张验证集的结果。打开图片重点看两件事框是否贴合香蕉轮廓有没有框跑到图像外边去。如果坐标出现负数或大于 1 的值说明源标注有越界框需要回到 XML 修如果出现宽高为 0 的框说明标注退化成了一个点这种样本要直接剔除。抽样复核后再做全量统计把宽高异常的 txt 文件列出来处理。4. 用 YOLOv8 训练香蕉检测模型配置、超参数与常见报错4.1 数据 YAML 和目录结构对齐 yolov8 训练自己的数据集的规范Ultralytics YOLOv8 对数据目录有一套硬性约定。转换脚本输出的 banana_yolo 目录结构如下banana_yolo/ ├── train/ │ ├── images/ │ │ ├── banana_001.jpg │ │ └── ... │ └── labels/ │ ├── banana_001.txt │ └── ... └── val/ ├── images/ └── labels/注意 labels 目录和 images 目录必须同级YOLO 训练时会自动在 images 的兄弟目录里找 labels不需要在配置里写标签路径。然后写数据集配置文件# banana.yaml path: /absolute/path/to/banana_yolo train: train/images val: val/images nc: 1 names: [banana]path写绝对路径最省心train和val是相对path的路径nc必须和names列表长度一致。这里最容易翻车的点验证集路径写错了不会报错只是训练完 mAP 一直为 0因为模型根本没在 val 上评估过。单类别数据集的names只写一行 banana顺序和之前转换脚本里的 CLASSES 完全一致。4.2 训练命令与关键超参数环境装好后最小训练命令一行起yolo detect train databanana.yaml modelyolov8n.pt epochs100 imgsz640 batch16 patience20 workers4 projectbanana_runs namebaseline首次使用yolov8n.pt会自动下载 COCO 预训练权重2240 张图在 8GB 显存上用 n 模型大约 20 分钟一轮跑完。几个关键参数按这个表调参数作用2240 张时的建议model网络规模n/s/m/l/x先用yolov8n.pt跑通再换yolov8s.ptimgsz训练分辨率640香蕉是大目标没必要上 1280batch每批图片数受显存限制16OOM 就降到 8epochs最大迭代轮数100配合早停一般 60 轮内收敛patience多少轮没提升就自动停20防止过拟合lr0初始学习率默认 0.01loss 震荡可降到 0.005workers数据加载线程数4CPU 核少就降到 2训练顺序的优先级值得强调先把数据质量调好再调分辨率然后才是模型大小和学习率。很多人一上来就上yolov8x.pt2240 张数据训 100 轮结果 mAP 还不如 n 模型因为小数据撑不起大模型的容量。AMD 显卡这类非 CUDA 环境也能跑只是得用 CPU 或 ROCm 版推理速度慢一个量级小规模验证还是能接受的。4.3 训练监控损失曲线和断点恢复训练开始后盯着banana_runs/baseline/results.png看四个子图box_loss、cls_loss、dfl_loss 和 metrics 的 precision、recall、mAP50、mAP50-95。需要知道的是 YOLOv8 的损失函数构成box_loss 用的是 CIoU 加 DFL负责框位置的回归cls_loss 是 BCE负责分类。所以如果看到 box_loss 在 0.05 附近小幅抖动是正常的关键要看 mAP50 是否随 epoch 上升、mAP50-95 是否平滑增长。只要曲线还在涨就不要仓促停掉。训练中断后继续不要用 best.pt 重训用这个命令yolo detect train databanana.yaml modelbanana_runs/baseline/weights/last.pt resumeTrueresumeTrue会从 last.pt 里恢复 epoch 计数、优化器状态和学习率调度器位置效果等同于暂停后继续比拿 best.pt 从头训稳定得多。4.4 四个高频报错的定位方法训练过程中最常遇到的四个问题按出现频率排No labels found in /path/banana_yolo/train/labels目录名拼错txt 文件为空或者 labels 没有和 images 同级。先确认train/labels存在且非空。CUDA out of memory把 batch 减半或者 imgsz 从 640 降到 512。2240 张图、batch 16 imgsz 640 用 8GB 显存跑 yolov8n 是够的。类别索引越界yaml 里 nc1但某个 txt 文件里出现了索引 1 或 2说明转换脚本的 CLASSES 顺序和 yaml 的 names 没对齐或者混入了别的数据集标签。训练正常但验证集 mAP 和 recall 全程为 0labels 和 images 根本没配对文件名对不上。回到 3.4 节把验证集标签画回图上看一遍对比文件名。以上问题里第一类和第四类的根源几乎都在数据准备阶段而不是训练本身——这也是为啥花一整章篇幅在格式转换和自检上。5. 用混淆矩阵和漏检图反查标注质量训练完成后val 结果目录里除了权重还会生成confusion_matrix.png和results.png。2240 张的单类别香蕉数据混淆矩阵只有 banana 和 background 两类重点看两个格子background 被误判为 banana 的假阳性以及 banana 漏成 background 的假阴性。假阳性多说明背景里有黄色物体干扰标注或者数据增强过度假阴性多说明存在漏标——某些角落的小串香蕉根本没有框。这一步比调参收益大得多。但混淆矩阵只能给统计结论不能告诉你具体是哪张图。用一个小脚本把预测结果和真实标注比对把可疑样本自动收集起来from ultralytics import YOLO from pathlib import Path import shutil model YOLO(banana_runs/baseline/weights/best.pt) val_img_dir Path(banana_yolo/val/images) bad_dir Path(bad_cases) bad_dir.mkdir(exist_okTrue) conf_thresh 0.25 iou_thresh 0.5 for img_path in sorted(val_img_dir.glob(*.jpg))[:50]: results model.predict( str(img_path), confconf_thresh, iouiou_thresh, verboseFalse ) pred_count len(results[0].boxes) label_path val_img_dir.parent / labels / f{img_path.stem}.txt if not label_path.exists(): continue gt_count len(label_path.read_text().splitlines()) ratio pred_count / gt_count if gt_count else 0 if ratio 0.5 or ratio 1.8: shutil.copy(img_path, bad_dir / img_path.name) print(fsuspect: {img_path.name} gt{gt_count} pred{pred_count})脚本里conf0.25控制置信度阈值阈值调低能召回更多框但也会引入假阳性这里只做筛选不用作最终指标iou0.5是 NMS 的 IoU 阈值。判定逻辑很简单预测框数量不到标注的一半大概率是漏检预测框数量接近标注的两倍大概率是把背景误识别成目标。被挑进bad_cases目录的图片不要急着删逐个打开看是目标被遮挡、目标过小还是真的漏标。把漏标的框补回去重新跑一遍 3.3 的转换、4.2 的训练你会看到 mAP50 提升比换 backbone 或调 loss 权重更直接。这才是一个 2240 张的小数据集该发挥的作用——把数据闭环打磨跟手。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价