ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

VOC转YOLO格式并用YOLOv8训练企鹅目标检测模型

VOC转YOLO格式并用YOLOv8训练企鹅目标检测模型 简介一套面向目标检测任务的企鹅标注数据集适合深度学习初学者、算法练习者及需要小型数据快速验证训练流程的开发者。数据集采用VOC与YOLO两种主流标注格式仅含penguin企鹅一个目标类别使用labelImg完成标注框选严格贴合目标边界并经一致性复查保证质量可直接用于YOLO系列、Faster R-CNN、SSD等常见检测模型的训练与评估。压缩包为rar格式、免密解压共364个文件、约17.54MB内部按jpg原图、VOC格式xml、YOLO格式txt分成三个独立文件夹便于对照学习两种标注格式的差异与转换逻辑。资源已吸引206人学习浏览既可用作入门级目标检测实验数据也可作为理解数据集组织方式和标注格式处理的参考样例。1. 120张企鹅图片怎么变成VOC和YOLO都能用的目标检测数据集很多检测项目不是从COCO这种大而全的数据集起步而是从自己手机上攒的那一百来张照片开始。比如你想做一个企鹅检测器先对着视频抽帧选了120张不同角度、不同光照的企鹅图片用LabelImg一张一张标出了目标框。标完发现自己辛苦生成的VOC格式XML却喂不了YOLO训练脚本。这不是格式偏好问题而是两种坐标体系的设计差异VOC记录的是xmin、ymin、xmax、ymax这样的绝对像素坐标YOLO记录的是归一化后的中心点和宽高。这个标题的核心工作就是把同一批企鹅标注同时维护成VOC和YOLO两种格式保证类别映射和坐标都不出错。下面按工作流顺序展开先拆解VOC的XML结构再写脚本转成YOLO的txt之后配置YOLOv8训练参数最后用一套增强和验证手段守住小数据集的稳定性。2. VOC格式的企鹅数据集里到底存了什么2.1 VOC数据集的目录结构与非必须文件标准VOC数据集目录一般长这样penguin_voc/ ├── JPEGImages/ # 原始图片 ├── Annotations/ # 与图片同名的XML标注 └── ImageSets/ └── Main/ # train.txt、val.txt 文件名列表JPEGImages放图片Annotations放XMLImageSets/Main放训练验证划分的文件名列表。很多教程会忽略ImageSets因为检测脚本可以手动扫描Annotations目录但保留它对后续数据集切分有好处特别是120张这种小数据集手动分一次就够之后每次跑训练都用同一份划分。转换到YOLO格式时真正被依赖的只有JPEGImages和Annotations两个目录。2.2 XML标注的字段逐个拆开打开一张企鹅图片的XML内容是这个样子annotation folderJPEGImages/folder filenamepenguin_001.jpg/filename path/home/user/penguin_voc/JPEGImages/penguin_001.jpg/path source databaseUnknown/database /source size width1920/width height1080/height depth3/depth /size segmented0/segmented object namepenguin/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin310/xmin ymin512/ymin xmax622/xmax ymax844/ymax /bndbox /object /annotation这段XML描述了一张图片里的一个目标框。需要注意的节点有几个size下的width和height是转换格式时的归一化分母必须和真实图片尺寸一致否则所有目标框都会整体偏移。object下的name是类别名拼写必须完全统一多一个空格都会被当成新类别。bndbox里存的是xmin、ymin、xmax、ymax的绝对像素坐标。truncated表示目标是否被截断difficult表示该目标是否难以辨认转YOLO时如果不过滤difficult1的实例很容易给训练集引入噪声。2.3 用Python脚本校验120张VOC标注120张图逐个打开XML检查不现实写个短脚本统计更靠谱import xml.etree.ElementTree as ET from collections import Counter import glob def parse_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() filename root.find(filename).text boxes [] for obj in root.iter(object): name obj.find(name).text.strip() 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)) boxes.append({name: name, bbox: [xmin, ymin, xmax, ymax]}) return filename, boxes xml_files glob.glob(penguin_voc/Annotations/*.xml) counter Counter() bad_count 0 for xml_file in xml_files: filename, boxes parse_voc(xml_file) for b in boxes: counter[b[name]] 1 xmin, ymin, xmax, ymax b[bbox] if xmax xmin or ymax ymin: print(f非法框: {filename} {b}) bad_count 1 print(fXML文件数: {len(xml_files)}) print(f目标框分布: {dict(counter)}) print(f非法框数量: {bad_count})这段脚本做了三件事遍历所有object节点读取目标框统计每个类别的实例数量检查坐标是否出现xmax小于xmin这类反向标注。int(float())是为了兼容部分标注工具会把坐标写成带小数点的字符串。120张图跑完只要输出里没有非法框类别分布也只有penguin一种这份VOC标注才算干净。小数据集经不起脏标注折腾一条错框就可能让mAP掉两三个点。2.4 手工标注时最容易忽略的两个VOC细节第一个坑是类别名不统一。有人第一张图标penguin第二张图标Penguin训练时被当成两个类别最终推理阶段只输出其中一类检测率直接砍半。第二个坑是图片改尺寸后没有更新XML的size节点。常见做法是先裁剪图片再送去标注结果标完又用脚本统一缩放XML里的宽高还停留在缩放前。转换YOLO格式时归一化分母一旦用错全部框都会偏移而且很难肉眼发现。下面是VOC字段在转换YOLO时的优先级速查XML字段转换时是否必需常见问题folder否基本无影响filename是后缀大小写不一致导致匹配失败path否绝对路径换机器后失效size.width/height是图片缩放后未同步更新object.name是类别名拼写或大小写不一致object.bndbox是坐标越界、反向或为0object.difficult建议剔除噪声实例干扰评估3. YOLO格式的归一化标注与VOC到YOLO转换脚本3.1 YOLO的txt标注为什么是五个小数YOLO从v3开始一直沿用同一种标注方式一张图片对应一个同名的txt文件每行五个数依次是类别id、目标中心点x、中心点y、目标宽度、目标高度。这五个数字全部除以图片宽高做了归一化所以都是0到1之间的小数。中心点加宽高的表达方式在不同分辨率下可以直接复用模型推理时再根据输入尺寸乘回来。VOC记录的是像素坐标系的绝对坐标YOLO记录的是相对坐标这就是两种格式必须靠脚本转换的根本原因。0 0.2427 0.6278 0.1625 0.3074这一段txt对应前面XML里的那只企鹅。反算一下xmin约等于1920乘以0.2427减去1920乘以0.1625的一半结果是310和XML里的xmin值对得上。坐标都在图片范围内不依赖具体分辨率这就是YOLO标注的直观含义。3.2 转换脚本怎么写从一个XML到同名txtimport os import xml.etree.ElementTree as ET class_names [penguin] # 类别顺序就是YOLO txt里的id class_to_id {name: i for i, name in enumerate(class_names)} def voc_to_yolo(xml_path, out_dir, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() os.makedirs(out_dir, exist_okTrue) stem os.path.splitext(os.path.basename(xml_path))[0] out_path os.path.join(out_dir, stem .txt) lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in class_to_id: continue cls_id class_to_id[name] 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) x_center ((xmin xmax) / 2.0) / img_w y_center ((ymin ymax) / 2.0) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h x_center min(1.0, max(0.0, x_center)) y_center min(1.0, max(0.0, y_center)) w min(1.0, max(0.0, w)) h min(1.0, max(0.0, h)) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_path, w) as f: f.write(\n.join(lines) \n) voc_to_yolo(penguin_voc/Annotations/penguin_001.xml, penguin_yolo/labels, 1920, 1080)脚本核心是把bndbox的四个角转换成中心点加宽高。x_center取xmin和xmax的均值再除以图片宽度得到归一化中心点横坐标。宽度直接用xmax减xmin除以图片宽度。结尾用min和max把坐标钳制到0到1之间防止标注员手抖把框画到图片外。需要留意的是class_names列表的顺序一旦确定就不要随便改否则已生成的txt里class_id会全部错位。3.3 批量转换并画框回看确认格式没转错120张图不可能手动传参调用批量处理时从图片读取宽高而不是信任XML里的size节点import glob from PIL import Image for xml_path in glob.glob(penguin_voc/Annotations/*.xml): stem os.path.splitext(os.path.basename(xml_path))[0] img Image.open(fpenguin_voc/JPEGImages/{stem}.jpg) img_w, img_h img.size voc_to_yolo(xml_path, penguin_yolo/labels, img_w, img_h)这段代码遍历所有XML用PIL读取真实图片尺寸传给转换函数避免size字段过期导致归一化错误。转换完需要肉眼抽查用OpenCV把YOLO txt画回去import cv2 def draw_yolo(image_path, label_path, class_names): img cv2.imread(image_path) h, w img.shape[:2] with open(label_path) as f: for line in f: cls_id, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[int(cls_id)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img img draw_yolo(penguin_yolo/images/penguin_001.jpg, penguin_yolo/labels/penguin_001.txt, [penguin]) cv2.imwrite(check_001.jpg, img)画框回看时重点检查三点框是否贴着企鹅身体、是否有整体偏移、类别id是不是对应的类别名。随机抽五六张看即可全查太费时间但也没必要。出现偏移时优先怀疑图片读取尺寸和XML size不一致而不是怀疑转换脚本本身。3.4 类别顺序必须锁死用classes.txt保证长期一致如果这个企鹅数据集以后要扩展比如新增chick、egg类别class_names列表一变所有已生成的txt都要重新转换。这里推荐在数据集目录放一个classes.txtpenguin chick转换脚本读取这个文件生成class_to_iddata.yaml里的names也按同一文件生成两个格式共用同一份类别表。120张图的时候体会不到类别错乱有多致命等数据扩到500张再发现类别id对不上那就只能全部重转一遍还要重新训练验证成本远大于一开始就固定好类别顺序。4. 把120张企鹅标签数据喂给YOLOv8并调好参数4.1 按9:1切分train数据集和val数据集120张图没有必要按8:1:1分成train、val、test三份。目标检测的验证集需要保证每个类别有足够多的实例分割太碎会让验证集指标抖动剧烈。常见做法是90%训练、10%验证测试集暂时用验证集代替。切分要固定随机种子import random from pathlib import Path random.seed(42) image_dir Path(penguin_yolo/images) images sorted(image_dir.glob(*.jpg)) random.shuffle(images) val_count max(1, int(len(images) * 0.1)) val_images images[:val_count] train_images images[val_count:] for name, subset in [(train.txt, train_images), (val.txt, val_images)]: with open(fpenguin_yolo/{name}, w) as f: for img in subset: f.write(str(img.resolve()) \n) print(ftrain {len(train_images)}张, val {len(val_images)}张)random.seed(42)保证每次跑脚本得到相同划分避免换了一次切分结果后训练复现不出来。val_count取整数120张会得到12张验证图。脚本只生成txt列表不实际复制图片后续增减图片只要重新运行一遍即可。这里切分后的images目录和labels目录保持同级YOLO会按图片路径自动找到对应标签。4.2 data.yaml和训练命令怎么配Ultralytics的训练入口需要一份data.yamlpath: /absolute/path/to/penguin_yolo train: train.txt val: val.txt nc: 1 names: 0: penguinpath是数据集根目录train和val指向切分脚本输出的txt文件。txt里每一行是一张图片的绝对路径。nc表示类别数量当前只有penguin一个类别。names的索引必须和YOLO txt里的class_id一一对应不能改顺序。Ultralytics会自动把图片路径中的images替换成labels去找同名的txt标签所以labels目录不能放在别的自定义位置。4.3 训练参数表哪些值适合企鹅这种小数据集安装好环境后开始训练pip install ultralytics yolo detect train modelyolov8n.pt datapenguin_yolo/data.yaml epochs120 batch16 imgsz640 patience20 lr00.005 hsv_h0.05这里选YOLOv8而不是更新的YOLO11因为v8的生态最稳文档、导出链和第三方工具更齐全120张图的数据量也用不上模型结构的代际优势。模型权重选yolov8n.pt基于COCO预训练起步比从零训练收敛快得多也不需要重新下载COCO数据集。参数默认值推荐值说明modelyolov8n.ptyolov8n.pt单类别小样本用nano级就够了epochs100120配合early stopping使用batch168或16显存溢出时优先降到8imgsz640640或480企鹅目标大降到480加速明显patience5020小数据集过拟合早早停要敏感lr00.010.005数据少初始学习率降低更稳hsv_h0.0150.05适当增加颜色抖动增强泛化batch16在大多数消费级显卡上都能跑如果报CUDA out of memory就先降batch同时把imgsz降到480。patience20表示验证损失连续20个epoch不降就停止比默认的50敏感适合120张的小规模数据集。lr0调低是因为样本少的时候梯度估计噪声大学习率太猛容易震荡。关于AMD显卡跑YOLO如果本机是A卡又不想折腾ROCm配置可以直接用CPU训练这120张图的数据单epoch耗时几十秒比花时间配环境更划算。4.4 训练成功后先看results.csv再看weights训练结束后runs/detect/train目录下会生成weights/best.pt和weights/last.pt。best是验证集表现最好的权重推理和部署都选best.pt。results.csv里有每个epoch的完整指标先看train/box_loss和val/box_loss两条曲线一起下降属于正常收敛train一直降但val在第30个epoch开始回升就是过拟合信号。YOLOv8的损失函数由box_loss、cls_loss、dfl_loss三部分组成单类别检测时cls_loss降得特别低是正常现象不用额外调整损失权重。5. 小数据集的增强参数设置与两种格式共存的验证技巧5.1 增强别开太猛一组适合企鹅检测的参数120张图最容易出现val_loss快速反弹此时优先改的是数据增强而不是网络结构。企鹅身体左右翻转语义等价上下翻转不自然所以fliplr保持开启flipud维持默认关闭。一个稳妥的参数组合yolo detect train modelyolov8n.pt datapenguin_yolo/data.yaml epochs150 batch16 imgsz640 patience30 hsv_h0.05 hsv_s0.5 hsv_v0.4 degrees0 scale0.5 fliplr0.5degrees0直接关掉旋转企鹅在画面里本身就有各种朝向模型要学的是姿态特征而不是旋转不变性强行旋转增强只会引入不自然的形变。scale0.5让目标尺度在小范围内变化提高对远处小企鹅的响应。fliplr0.5用一半概率水平翻转相当于样本量直接翻倍成本几乎为零。5.2 用一条val命令判断训练是否值得继续训练结束后立刻跑验证yolo detect val modelruns/detect/train/weights/best.pt datapenguin_yolo/data.yaml输出里重点看mAP50和mAP50-95两个指标。120张的单类别企鹅数据mAP50做到0.85以上不奇怪mAP50-95一般能达到mAP50的70%左右。如果mAP50很高但mAP50-95偏低说明框的位置不稳定优先怀疑验证集里存在没标干净的框。把val的12张图重新画框回看一遍经常能找到几处边界偏差。5.3 只维护VOC源标注批量生成YOLO标签最后是一个适合长期维护的工作习惯把VOC的Annotations目录当成唯一真源每次新增图片或修正标注只改XML然后跑一遍批量转换脚本重新生成所有YOLO txt。这样两种格式永远同步不会出现VOC改了一版但YOLO标签还是旧数据的分叉。脚本建议做成命令行入口python voc2yolo.py --voc penguin_voc --out penguin_yolo --classes classes.txt跑完用两条命令对照文件数量ls penguin_voc/Annotations/*.xml | wc -l ls penguin_yolo/labels/*.txt | wc -l两个数字相等再开工。本文还有配套的精品资源点击获取
返回列表