企鹅数据集VOC与YOLO双格式:120张图小样本目标检测全流程

发布时间:2026/9/23 18:17:03
企鹅数据集VOC与YOLO双格式:120张图小样本目标检测全流程 简介这份企鹅目标检测数据集面向计算机视觉入门者与需要小型样本练手的算法开发者用于验证检测模型在单一类别场景下的训练与推理效果。资源包共364个文件以121张jpg图片、121个xml标注文件和122个txt标注文件为主压缩包约17.54MBrar格式无需解压密码解压后图片、VOC标注与YOLO标注分目录存放可直接导入labelImg或主流检测框架查看。图片约120张单张1-500KB标注类别统一为penguin标注过程遵循边界框选准确、目标不遗漏、多轮一致性检查等原则xml与txt一一对应省去格式转换步骤。目前已有206人学习下载适合快速搭建数据加载流程、调试标注解析代码或作为课程实验素材帮助读者把精力集中在模型结构与训练策略上。1. 企鹅数据集 VOC 与 YOLO 双格式120 张标注到底能训出什么手上只有 120 张左右的企鹅图片还要求同时给出 VOC 和 YOLO 两套标注格式这事听起来像是课程作业但我实际拿它跑过一轮检测之后发现它恰好是检验「小样本目标检测流程是否跑通」的最低成本方案。企鹅这个类别本身有天然优势黑白配色对比强烈、轮廓清晰、姿态变化集中在站立、行走、俯卧几种背景大多是冰面、岩石或水面类内差异比行人、车辆小得多。120 张图按 8:1:1 切分训练集不到 100 张听起来寒酸但对于单类别、特征鲜明的目标YOLO 系列在几十张量级上就能给出可用的收敛曲线。这篇文章面向两类人一是手里已经有一批企鹅图、想知道怎么标、怎么转、怎么训的从业者二是想拿一个干净的小数据集把 YOLO 训练、验证、推理整条链路走一遍的新手。VOC 格式负责兼容老工具链和部分评测脚本YOLO 格式负责直接喂给 ultralytics 系训练器两套格式同源同标注转换过程本身就是一次对标注质量的复核。下面按「标注 → 转换 → 训练 → 排错 → 提效」的顺序拆开讲参数和坑都落到具体命令上。2. 从零标注 120 张企鹅图工具选择与 VOC 格式落地2.1 为什么先标 VOC 再转 YOLO而不是直接标 YOLO常见做法是直接用 labelImg 切到 YOLO 模式标完就是class x_center y_center w h的归一化 txt。但我一般会先标成 VOC 的 XML再写脚本转 YOLO原因有三个。第一VOC 的 XML 里保留了图片宽高、绝对像素坐标和类别名信息是冗余的转换时可以做校验比如发现某个框的 xmax 超过图片宽度能立刻定位到是哪张图标错了。第二很多老的可视化脚本、评测工具、数据增强库仍然吃 VOC先有 XML 等于留了一份「后悔药」。第三YOLO 的归一化坐标一旦算错肉眼很难发现而 VOC 的绝对坐标对着图一看就知道框偏没偏。120 张图的量级多这一步转换成本极低收益是标注质量可控。labelImg 的安装不复杂但版本要选对。用 pip 装的话pip install labelImg1.8.6 labelImg启动后左侧工具栏切到 PascalVOC 模式也就是默认模式。Open Dir选图片目录Change Save Dir选 XML 输出目录两个目录建议分开避免图片和标注混在一起。快捷键W画框CtrlS保存当前D下一张A上一张。类别名统一填penguin不要一会儿penguin一会儿Penguin大小写不一致在后续转换时会变成两个类这是血泪经验。2.2 标注规范120 张图里必须统一的四个细节小数据集最怕标注不一致模型学到的边界会飘。以下四点是我在企鹅数据上踩过坑之后固定下来的规范。第一框的紧贴程度。企鹅的鳍状肢在站立时贴着身体行走时张开如果一部分图把鳍算进框、一部分不算模型对宽度的回归会震荡。统一标准是以躯干和头部的外轮廓为准鳍状肢张开时包含到鳍尖贴身时不额外扩框。第二遮挡处理。企鹅群居经常出现一只挡另一只。原则是遮挡超过 50% 的目标不标低于 50% 的按可见轮廓标不要凭想象补全被挡部分。120 张图里如果遮挡样本太多建议单独统计一下比例超过三成就要考虑补充无遮挡图。第三截断目标。图片边缘只露出一半的企鹅如果可见面积小于整体的 30%直接跳过不标大于 30% 的按可见部分标框。这条规则要和遮挡规则区分开截断是画面边界造成的遮挡是目标之间造成的。第四最小尺寸。像素面积小于 32×32 的目标不标YOLO 下采样到 P3 特征图是 8 倍太小的目标在训练中基本是噪声。企鹅数据集里如果有远景小企鹅要么裁图放大重标要么直接放弃。标完 120 张之后做一次自检用脚本统计每个 XML 的框数量、类别分布、坐标越界情况。下面这段代码可以直接跑import os import xml.etree.ElementTree as ET xml_dir annotations_xml stats {total_boxes: 0, images: 0, bad_coord: [], classes: {}} for f in os.listdir(xml_dir): if not f.endswith(.xml): continue stats[images] 1 tree ET.parse(os.path.join(xml_dir, f)) root tree.getroot() size root.find(size) w, h int(size.find(width).text), int(size.find(height).text) for obj in root.findall(object): name obj.find(name).text stats[classes][name] stats[classes].get(name, 0) 1 bbox obj.find(bndbox) xmin int(bbox.find(xmin).text) ymin int(bbox.find(ymin).text) xmax int(bbox.find(xmax).text) ymax int(bbox.find(ymax).text) stats[total_boxes] 1 # 坐标越界或框无效的图记录下来 if xmin 0 or ymin 0 or xmax w or ymax h or xmax xmin or ymax ymin: stats[bad_coord].append(f) print(图片数:, stats[images]) print(总框数:, stats[total_boxes]) print(类别分布:, stats[classes]) print(异常文件:, stats[bad_coord])逻辑说明遍历 XML 目录解析每张图的宽高和每个 object 的 bbox累计框数和类别分布同时检查坐标是否越界或框宽高是否为负。参数说明xml_dir换成你的 XML 目录bad_coord列表里的文件必须逐张打开确认通常是画框时拖出了图片边界或者 xmin/xmax 填反了。120 张图跑完正常应该看到penguin一个类别总框数在 150 到 300 之间如果框数低于 120说明有图漏标。3. VOC 转 YOLO 格式转换脚本与四个边界坑3.1 转换脚本归一化坐标怎么算才不出错YOLO 格式每行是class_id x_center y_center width height全部归一化到 0 到 1。转换的核心是把 VOC 的绝对坐标(xmin, ymin, xmax, ymax)先算中心点和宽高再分别除以图片宽高。下面这个脚本我用了很多次直接改路径就能跑import os import xml.etree.ElementTree as ET xml_dir annotations_xml img_dir images out_dir labels_yolo classes [penguin] # 类别顺序必须固定训练时一致 os.makedirs(out_dir, exist_okTrue) for f in os.listdir(xml_dir): if not f.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, f)) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in classes: continue cid classes.index(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 / w y_center (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h # 裁剪到 [0,1]防止浮点误差越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) bw min(max(bw, 0.0), 1.0) bh min(max(bh, 0.0), 1.0) lines.append(f{cid} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}) txt_name os.path.splitext(f)[0] .txt with open(os.path.join(out_dir, txt_name), w) as fp: fp.write(\n.join(lines))逻辑说明解析每个 XML取图片宽高对每个 object 算归一化中心点和宽高按class_id x y w h写入同名 txt。参数说明classes列表的顺序就是训练时的类别索引必须和后续 data.yaml 里的 names 顺序完全一致:.6f保留六位小数YOLO 训练器对精度不敏感但保留足够位数方便排查裁剪到 [0,1] 是防止 xmax 恰好等于图片宽度时归一化结果出现 1.000001 这种值ultralytics 会直接报错。3.2 四个边界坑转换后必须抽查的项第一个坑图片宽高和 XML 里的 size 不一致。有些图被工具重新保存过实际像素变了但 XML 还是旧的 size转换出来的框会整体偏移。解决办法是转换时用 PIL 重新读一遍图片实际宽高和 XML 的 size 对比不一致的记录下来。from PIL import Image img Image.open(os.path.join(img_dir, f.replace(.xml, .jpg))) real_w, real_h img.size if real_w ! w or real_h ! h: print(尺寸不一致:, f, real_w, real_h, w, h)第二个坑文件名对不上。XML 叫penguin_001.xml图片叫penguin_001.jpeg扩展名不一致导致训练时找不到图。转换后要检查 labels 目录和 images 目录的文件名主干是否一一对应。第三个坑空标注文件。某张图里所有目标都被规则过滤掉了生成的 txt 是空的。YOLO 训练器遇到空 txt 会当作负样本这是合理的但如果空文件太多说明标注规则太严要回头调整。第四个坑类别名有空格或特殊字符。penguin带尾随空格转换时name not in classes判断失败这个框被静默丢弃。转换后统计一下总框数和 VOC 阶段的自检结果对比数量对不上就是有框被丢了。3.3 目录结构YOLO 训练器认的布局ultralytics 系训练器对目录结构有固定要求常见做法是dataset/ images/ train/ val/ test/ labels/ train/ val/ test/ data.yaml120 张图按 96/12/12 切分。切分脚本用随机种子固定避免每次跑结果不一样import os, random, shutil random.seed(42) files [f for f in os.listdir(images) if f.endswith(.jpg)] random.shuffle(files) n len(files) train, val, test files[:int(n*0.8)], files[int(n*0.8):int(n*0.9)], files[int(n*0.9):] for split, items in [(train, train), (val, val), (test, test)]: for f in items: shutil.copy(fimages/{f}, fdataset/images/{split}/{f}) shutil.copy(flabels_yolo/{f.replace(.jpg,.txt)}, fdataset/labels/{split}/{f.replace(.jpg,.txt)})data.yaml 内容path: ./dataset train: images/train val: images/val test: images/test nc: 1 names: [penguin]nc是类别数企鹅数据集就是 1names的顺序必须和转换脚本里的classes一致。这两处不一致是新手最常见的翻车点训练不报错但类别全乱。4. 120 张图训练 YOLO参数怎么设、曲线怎么看4.1 环境与启动命令小数据集的关键参数环境搭建按 ultralytics 官方方式即可不展开。直接说训练命令120 张图的量级和常规大数据集参数差别很大yolo detect train \ datadataset/data.yaml \ modelyolov8n.pt \ epochs300 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ warmup_epochs5 \ cos_lrTrue \ close_mosaic30 \ patience50 \ seed42 \ projectruns/penguin \ nameexp1参数说明modelyolov8n.pt选 nano 版本120 张图用大模型必然过拟合nano 的容量刚好。epochs300比常规的 100 大小数据集需要更多轮次才能收敛但配合patience50早停实际不会跑满。batch16在 8G 显存上够用显存小就降到 8。lr00.01是初始学习率小数据集不要用默认的 0.01 以上容易震荡。cos_lrTrue余弦退火配合lrf0.01让学习率从 0.01 降到 0.0001。close_mosaic30是最后 30 轮关闭 mosaic 增强mosaic 对小数据集提升明显但训练末期关掉能让模型适应真实分布。warmup_epochs5前 5 轮预热防止初期梯度爆炸。4.2 数据增强小数据集的救命稻草与过拟合边界120 张图不靠增强基本训不出泛化能力。ultralytics 默认开启的增强包括 mosaic、随机缩放、随机平移、HSV 抖动。我一般会额外调几个参数degrees10.0 \ translate0.1 \ scale0.5 \ fliplr0.5 \ mosaic1.0 \ mixup0.1 \ hsv_h0.015 hsv_s0.7 hsv_v0.4degrees10.0旋转角度企鹅在自然场景里不会大角度倾斜10 度够用设太大反而引入不真实样本。scale0.5缩放范围让模型适应不同距离的企鹅。fliplr0.5水平翻转企鹅左右对称翻转是安全的。mixup0.1混合增强小数据集上能提升鲁棒性但比例不要高0.1 到 0.2 之间。hsv_h/s/v控制色调、饱和度、亮度抖动冰面反光场景下亮度抖动可以适当加大到 0.4。注意增强不是越多越好。如果训练集 loss 持续下降但验证集 mAP 在 50 轮后不再提升说明增强过头或者模型开始过拟合这时候要么减少增强强度要么提前停。4.3 看曲线判断该不该继续训训练日志里重点看三条线。train/box_loss和train/cls_loss持续下降是正常的如果 50 轮后还在 0.5 以上说明学习率太大或者标注有问题。val/box_loss和val/cls_loss如果开始上升而过拟合。metrics/mAP50是主指标120 张图单类别正常能到 0.85 以上如果卡在 0.5 左右先查标注一致性再查类别索引对不对。一个反直觉的现象小数据集上mAP50-95通常比mAP50低很多因为框的定位精度难做高。企鹅数据集上 mAP50 到 0.9 而 mAP50-95 只有 0.6 是正常的不要因为这个去调参先把 mAP50 稳住。5. 企鹅检测的避坑与排查五条血泪记录5.1 训练 loss 不降mAP 一直是 0现象启动训练后 box_loss 在 1.0 附近不动mAP50 始终为 0。原因最常见的是 data.yaml 里的names和转换脚本的classes顺序不一致或者 labels 目录路径写错训练器读不到标注把所有框当背景。解决先跑一遍yolo detect train的 dry run或者手动检查dataset/labels/train/下 txt 文件是否非空再核对 data.yaml 的nc和names。5.2 验证集 mAP 高但推理时框全偏现象训练日志里 mAP50 到 0.9但拿测试图推理框整体偏移或大小不对。原因XML 里的 size 和图片实际尺寸不一致转换时用了错误的宽高做归一化。解决回到 3.2 节的尺寸校验脚本把所有不一致的图重新标注或修正 XML 的 size再重新转换。5.3 同一只企鹅被检出多个框现象推理结果里一只企鹅身上叠了两三个框。原因NMS 的 IoU 阈值默认 0.7对于重叠目标或者标注时框画得松的情况NMS 压不干净。解决推理时调低iou参数比如yolo detect predict iou0.5或者在训练时提高标注紧贴度。如果训练集里就有重复框模型会学到重复检测必须回头清理标注。5.4 置信度门限调不好漏检和误检两头堵现象默认conf0.25时漏检多调到 0.1 又出现大量误检。原因小数据集训出的模型置信度分布比较平没有明显的高低分界。解决在验证集上跑一遍不同 conf 的 PR 曲线选 F1 最高的点。实操上可以先yolo detect val conf0.001输出所有预测再用脚本扫 0.1 到 0.5 的阈值找漏检和误检的平衡点。企鹅数据集上我一般落在 0.2 到 0.3 之间。5.5 换机器或换版本后结果对不上现象同一份数据同一份代码换台机器训练 mAP 差 0.1 以上。原因随机种子没固定、CUDA 版本差异、ultralytics 版本差异都会影响。解决训练命令里固定seed42和deterministicTrue记录 ultralytics 版本号换环境时先跑一遍基线对比。小数据集对随机性特别敏感同一份数据不同种子 mAP 波动 0.05 到 0.1 是正常的不要因为一次结果差就大改参数。6. 把 120 张企鹅图用到极致半自动标注与增量迭代120 张图训出的模型最实际的进阶用法不是继续调参而是拿它当半自动标注器去标剩下的企鹅图。流程是用训好的best.pt对新图片批量推理输出 YOLO 格式的 txt人工在 labelImg 里加载这些预标注只做修正不做从零画框。这一步能把单张图的标注时间从两三分钟压到三十秒以内。批量推理并导出 YOLO txt 的命令yolo detect predict \ modelruns/penguin/exp1/weights/best.pt \ sourcenew_images/ \ conf0.25 \ iou0.5 \ save_txtTrue \ save_confTrue \ projectruns/pseudo \ nameround1save_txtTrue会在输出目录生成 labels 文件夹每张图一个 txt格式和训练用的一致。save_confTrue会把置信度写在每行末尾人工复核时优先看低置信度的框。注意伪标注的 txt 里多了一列置信度直接拿去训练会报错需要写脚本去掉最后一列import os for f in os.listdir(runs/pseudo/round1/labels): path os.path.join(runs/pseudo/round1/labels, f) with open(path) as fp: lines fp.readlines() cleaned [ .join(line.split()[:5]) for line in lines] with open(path, w) as fp: fp.write(\n.join(cleaned))修正后的伪标注和原始 120 张图合并再训一轮mAP 通常能再涨几个点。这个循环可以迭代两到三轮但要注意伪标注的错误会被模型学进去形成误差累积。我的习惯是每轮只把置信度高于 0.6 的框直接采纳0.3 到 0.6 之间的必须人工确认低于 0.3 的直接丢弃重标。另一个值得做的验证是跨场景测试。120 张图如果全来自同一片冰面、同一时间段模型换到另一片海域的企鹅图上可能直接崩。我一般会留 10 张完全不同背景的图不参与训练专门做最终验证。如果这 10 张上 mAP 掉到 0.5 以下说明数据多样性不够优先补图而不是调模型。最后说个习惯每次训完模型把 data.yaml、训练命令、ultralytics 版本、mAP 结果记在一个文本文件里和权重放一起。小数据集的实验重复性差没有记录的话两周后自己都说不清哪个权重对应哪组参数。这个习惯帮我省过很多次返工。希望帮到你。本文还有配套的精品资源点击获取