
简介这份企鹅目标检测数据集面向计算机视觉入门与算法验证场景适合需要快速搭建小样本检测实验的开发者、学生及教学人员使用。资源以VOC与YOLO双格式提供省去格式转换环节可直接接入主流检测框架进行训练与评估。压缩包共364个文件包含121张jpg图片、121个xml标注文件与122个txt标注文件整体约17.54MBrar打包无需解压密码解压后图片、xml与txt分目录存放便于对照查看标注情况。所有图片均由labelImg人工标注类别统一为penguin标注过程遵循边界框选准确、目标尽量不遗漏、完成后交叉检查一致性等原则可在一定程度上保证标注质量。目前已有206人学习下载适合作为小规模检测任务的练手数据帮助读者快速跑通数据加载、格式解析与模型训练流程也可用于验证标注规范与数据增强策略的实际效果。1. 企鹅数据集 VOC 与 YOLO 双格式120 张标注到底能训出什么手上只有 120 张左右的企鹅图片还要求同时给出 VOC 和 YOLO 两套标注格式这是很多做小样本目标检测的人真实会遇到的起点。企鹅这个类别本身有它的特殊性黑白配色对比强烈但企鹅群聚时个体相互遮挡严重幼崽和成年企鹅的纹理差异大远景个体在 640 分辨率下可能只剩十几个像素。120 张图如果按 8:1:1 切分训练集不到 100 张验证和测试各 12 张左右这个量级用 YOLOv8n 或 YOLOv5s 从预训练权重微调mAP0.5 做到 0.85 以上是现实的但想直接训一个从零开始的模型基本会翻车。VOC 格式和 YOLO 格式的本质区别在于坐标表示VOC 用绝对像素坐标的 xmin、ymin、xmax、ymax存在 XML 里YOLO 用归一化后的中心点 cx、cy 加宽高 w、h存在同名 txt 里且类别用从 0 开始的整数索引。两套格式服务于不同工具链——VOC 兼容 labelImg 直接产出、方便用 py-faster-rcnn 或 mmdetection 读取YOLO 格式是 ultralytics 系列训练时的默认输入。120 张的规模标注本身两三个小时能完成真正花时间的是格式转换和校验。这篇把我自己做小样本企鹅检测时的完整路径写清楚怎么标、怎么转、怎么切分、怎么训、哪里最容易出错。2. 从零标注 120 张企鹅图VOC 产出的操作细节2.1 标注工具选型与目录约定小样本标注不建议用太重的东西。labelImg 虽然老但胜在稳定、直接产出 VOC XML装完就能用。安装命令如下pip install labelImg labelImg打开后左侧选 PascalVOC 模式把图片目录设成JPEGImages标注保存目录设成Annotations。这里有个血泪经验labelImg 默认把 XML 存到图片同级目录如果你不手动改保存路径最后会得到一堆散落的 XML整理起来很烦。建议一开始就按下面这个结构建目录penguin_dataset/ ├── JPEGImages/ # 120 张原图 ├── Annotations/ # VOC XML ├── labels/ # YOLO txt转换后生成 ├── ImageSets/ │ └── Main/ # train.txt val.txt test.txt └── classes.txt # 类别名一行一个类别只设一个penguin。如果图里有帝企鹅、王企鹅、阿德利企鹅的区分需求那 120 张根本不够分每类不到 40 张模型学不出差异。我一般建议小样本先做单类把检测框质量做扎实再考虑细分。2.2 标注框的四个实操原则标注质量直接决定上限120 张的规模下每个框都值得认真对待。第一框要贴紧目标边缘但不要把企鹅脚下的阴影或旁边的冰块框进去YOLO 的回归损失对边界噪声很敏感。第二遮挡超过 50% 的个体直接标difficult或者干脆不标强行标注会让模型学到错误的特征。第三远景小目标如果目测小于 16×16 像素建议放弃YOLOv8 的 P3 特征图 stride 是 8太小的目标在特征图上几乎消失。第四同一张图里企鹅数量超过 30 只时检查一遍有没有漏标漏标比错标危害更大因为模型会把没标的目标当成背景来学。标完 120 张后用下面这段脚本快速统计一下框的分布确认没有异常import os import xml.etree.ElementTree as ET xml_dir penguin_dataset/Annotations widths, heights, counts [], [], [] for f in os.listdir(xml_dir): if not f.endswith(.xml): continue root ET.parse(os.path.join(xml_dir, f)).getroot() objs root.findall(object) counts.append(len(objs)) for obj in objs: bbox obj.find(bndbox) w float(bbox.find(xmax).text) - float(bbox.find(xmin).text) h float(bbox.find(ymax).text) - float(bbox.find(ymin).text) widths.append(w) heights.append(h) print(f图片数: {len(counts)}, 总框数: {sum(counts)}) print(f每图平均框数: {sum(counts)/len(counts):.1f}) print(f框宽 min/mean/max: {min(widths):.0f}/{sum(widths)/len(widths):.0f}/{max(widths):.0f}) print(f框高 min/mean/max: {min(heights):.0f}/{sum(heights)/len(heights):.0f}/{max(heights):.0f})这段脚本遍历所有 XML统计每张图的框数量和框的宽高分布。如果发现最小框宽小于 16 像素说明有大量小目标训练时需要把imgsz调大或者用带 P2 层的模型。如果每图平均框数超过 25120 张图的总框数会到 3000 以上对小样本来说反而容易过拟合需要考虑数据增强。3. VOC 转 YOLO 格式转换脚本与四个边界坑3.1 转换脚本的完整实现VOC 的 XML 存的是绝对坐标YOLO 要的是归一化中心点坐标转换公式是cx (xmin xmax) / 2 / img_wcy (ymin ymax) / 2 / img_hw (xmax - xmin) / img_wh (ymax - ymin) / img_h。下面这个脚本我用了很多次处理了常见的边界情况import os import xml.etree.ElementTree as ET from PIL import Image classes [penguin] # 与 classes.txt 保持一致 xml_dir penguin_dataset/Annotations img_dir penguin_dataset/JPEGImages out_dir penguin_dataset/labels os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue stem os.path.splitext(xml_file)[0] img_path os.path.join(img_dir, stem .jpg) if not os.path.exists(img_path): print(f跳过 {stem}: 找不到对应图片) continue img_w, img_h Image.open(img_path).size root ET.parse(os.path.join(xml_dir, xml_file)).getroot() lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in classes: continue if obj.find(difficult) is not None and obj.find(difficult).text 1: continue 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) # 边界裁剪防止标注越界 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) if xmax - xmin 1 or ymax - ymin 1: continue cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h cls_id classes.index(name) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(os.path.join(out_dir, stem .txt), w) as f: f.write(\n.join(lines))脚本先读图片真实尺寸再逐个解析 XML 里的 object。difficult为 1 的框直接跳过因为 YOLO 训练时没有 difficult 概念留着会污染损失。坐标做了裁剪防止标注时手抖框到图片外面导致归一化后出现负数或大于 1 的值。最后保留 6 位小数精度足够且文件不会太大。3.2 四个容易翻车的边界情况第一个坑是图片格式不统一。labelImg 读的是 jpg但如果你中途混入了 png 或 webpPIL 读出来的尺寸和 XML 里记录的可能不一致转换后框会整体偏移。解决办法是转换前统一用 PIL 重新存一遍 jpg。第二个坑是 XML 里size节点和真实图片尺寸不符。有些标注工具会写错 width 和 height如果你用 XML 里的 size 做归一化框会错位。上面脚本坚持用 PIL 读真实尺寸就是为了绕开这个。第三个坑是类别名大小写和空格。Penguin和penguin在classes.index()里会直接抛异常建议转换前统一strip().lower()。第四个坑是空 txt 文件。如果一张图里所有框都被 difficult 过滤掉了会生成一个空 txt。YOLO 训练时遇到空标签文件会当成负样本如果这样的文件多了模型会学偏。建议转换后统计一下空文件数量超过总数 5% 就要回头检查标注。# 统计空标签文件 find penguin_dataset/labels -name *.txt -empty | wc -l4. 数据集切分与 YOLO 训练配置120 张的调参边界4.1 分层切分与 data.yaml 写法120 张图按 8:1:1 切训练 96 张、验证 12 张、测试 12 张。切分时要注意不能让同一只企鹅的连续帧同时出现在训练和验证集里否则验证指标会虚高。如果图片是从视频抽帧来的按时间顺序切不要随机打乱。切分脚本import os import random random.seed(42) img_dir penguin_dataset/JPEGImages stems [os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(.jpg)] random.shuffle(stems) n len(stems) n_train int(n * 0.8) n_val int(n * 0.1) splits { train: stems[:n_train], val: stems[n_train:n_train n_val], test: stems[n_train n_val:] } os.makedirs(penguin_dataset/ImageSets/Main, exist_okTrue) for name, items in splits.items(): with open(fpenguin_dataset/ImageSets/Main/{name}.txt, w) as f: f.write(\n.join(items)) print(f{name}: {len(items)} 张)固定随机种子 42 是为了可复现。切完后 train 96 张、val 12 张、test 12 张。验证集只有 12 张mAP 的波动会比较大建议训练时多看几个 epoch 的曲线不要只盯最后一个 epoch 的数值。data.yaml 的写法path: ./penguin_dataset train: ImageSets/Main/train.txt val: ImageSets/Main/val.txt test: ImageSets/Main/test.txt nc: 1 names: [penguin]path用相对路径方便整个数据集目录迁移。nc是类别数单类就写 1。names 的顺序必须和转换脚本里的classes列表完全一致否则类别索引会错位。4.2 YOLOv8n 微调的关键参数120 张的规模模型选 YOLOv8n 或 YOLOv5s不要上更大的。预训练权重必须加载从零训练在 96 张图上连收敛都难。训练命令yolo detect train \ datapenguin_dataset/data.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ batch16 \ lr00.001 \ lrf0.01 \ warmup_epochs5 \ patience30 \ augmentTrue \ mosaic0.5 \ degrees10 \ translate0.1 \ scale0.3 \ fliplr0.5 \ projectruns/penguin \ namev8n_120参数逐个说epochs150是因为小样本需要更多轮次才能充分拟合但配合patience30早停防止过拟合。lr00.001比默认的 0.01 小一个量级小样本用大学习率容易震荡。mosaic0.5而不是默认的 1.0因为企鹅群聚场景下 mosaic 四图拼接会制造大量不自然的遮挡0.5 的强度更稳。degrees10和fliplr0.5是安全的几何增强企鹅在水里和冰上的姿态本身就有旋转小幅旋转不会引入错误标签。scale0.3控制缩放范围避免小目标被缩得更小。训练过程中重点看三个指标metrics/mAP50是否在 50 个 epoch 内超过 0.7train/box_loss和val/box_loss的差距是否持续扩大以及metrics/precision和metrics/recall是否严重失衡。如果 val loss 在 30 个 epoch 后开始上升而 train loss 还在降说明过拟合了把mosaic降到 0.3、degrees降到 5 再试。5. 小样本企鹅检测的避坑与排查清单5.1 训练 loss 不降反升现象前 20 个 epoch 正常下降之后 train loss 继续降但 val loss 抬头mAP 卡在 0.6 左右不动。原因96 张训练图对 YOLOv8n 来说仍然偏少模型在记忆训练样本而不是学特征。加上 mosaic 强度过高模型把拼接边界当成了判别依据。解决把mosaic从 0.5 降到 0.2degrees从 10 降到 5同时把weight_decay从默认的 0.0005 提到 0.001。如果还不行冻结 backbone 前 5 层只训 head 部分等 head 收敛后再解冻全部微调。5.2 验证集 mAP 波动超过 0.15现象相邻两个 epoch 的 mAP50 一个 0.82 一个 0.65曲线像心电图。原因验证集只有 12 张每张图的检测结果对整体 mAP 影响权重太大。某一张图漏检两只企鹅mAP 就掉一大截。解决把验证集扩到 20 张从训练集里匀。或者用 5 折交叉验证虽然训练时间翻 5 倍但指标稳定得多。120 张的规模下我一般直接接受波动看最后 10 个 epoch 的滑动平均。5.3 小目标企鹅全部漏检现象近处大企鹅检测正常远处小于 32 像素的企鹅一个都检不出来。原因YOLOv8n 的 P3 特征图 stride 是 8640 分辨率下 32 像素的目标在 P3 上只有 4×4 个格子特征太弱。加上训练集里小目标样本本来就少模型没学到。解决把imgsz从 640 提到 1024小目标在特征图上的占比翻倍。或者换 YOLOv8s 并开启 P2 层但参数量会增加。另一个办法是在数据增强里加copy_paste把小企鹅复制粘贴到大图的不同位置人为增加小目标样本。5.4 类别索引错位导致全图误检现象训练 loss 正常下降但推理时整张图被框成一个巨大的 penguin。原因转换脚本里classes列表和 data.yaml 里names的顺序不一致或者某个 XML 里出现了未定义的类别名转换时被静默跳过导致标签和图片对不上。解决转换后跑一遍校验脚本确认每个 txt 里的类别 id 都在[0, nc-1]范围内且每个 txt 的行数和对应 XML 里的有效 object 数一致。发现不一致就回查 XML 的 name 字段。import os nc 1 label_dir penguin_dataset/labels bad [] for f in os.listdir(label_dir): if not f.endswith(.txt): continue with open(os.path.join(label_dir, f)) as fh: for i, line in enumerate(fh): if not line.strip(): continue cid int(line.split()[0]) if cid 0 or cid nc: bad.append((f, i, cid)) print(f越界类别数: {len(bad)}) for b in bad[:10]: print(b)5.5 推理时置信度阈值怎么定现象默认conf0.25时漏检多调到 0.1 又满屏误检。原因小样本训练的模型置信度分布和 COCO 预训练模型不一样默认阈值不适用。解决在验证集上跑一遍yolo detect val看F1_curve的峰值对应的 conf 值那个就是最优阈值。企鹅数据集我实测下来通常在 0.35 到 0.45 之间。定好之后在推理命令里显式指定yolo detect predict \ modelruns/penguin/v8n_120/weights/best.pt \ sourcepenguin_dataset/JPEGImages \ conf0.4 \ iou0.5 \ saveTrue6. 用 120 张企鹅数据验证标注质量的三个进阶技巧训练跑通只是第一步真正决定这个数据集能不能复用的是标注质量的可验证性。我一般用三个手段交叉检查比单纯看 mAP 靠谱。第一个是可视化回灌。把 YOLO 格式的标签反画回原图肉眼扫一遍有没有框偏移、框漏、框重复。脚本很短import os import cv2 img_dir penguin_dataset/JPEGImages label_dir penguin_dataset/labels out_dir vis_check os.makedirs(out_dir, exist_okTrue) for f in os.listdir(label_dir): if not f.endswith(.txt): continue stem os.path.splitext(f)[0] img cv2.imread(os.path.join(img_dir, stem .jpg)) h, w img.shape[:2] with open(os.path.join(label_dir, f)) as fh: for line in fh: if not line.strip(): continue _, 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(os.path.join(out_dir, stem .jpg), img)跑完后打开vis_check目录重点看三类图企鹅密集的、有遮挡的、远景小目标多的。这三类最容易暴露标注问题。第二个是训练集和验证集的分布对比。把两个集合的框宽高散点图画出来如果验证集的框尺寸分布和训练集差异很大说明切分时没有做分层验证指标不可信。120 张的规模下我一般手动确保验证集里既有大目标图也有小目标图。第三个是拿模型在训练集上的预测结果和标注做 diff。如果模型在训练集上都有大量漏检说明标注本身有问题不是模型能力不够。具体做法是yolo detect predict跑训练集把预测框和标注框做 IoU 匹配IoU 低于 0.5 的标注框逐个检查。检查项合格标准不合格时的动作空标签文件占比低于 5%回查 XML 是否全被 difficult 过滤最小框宽大于 16 像素提高 imgsz 或放弃该目标类别 id 范围全部在 [0, nc-1]检查 classes 和 names 一致性训练集预测召回高于 0.95标注漏框或框偏移验证集框尺寸分布与训练集接近重新分层切分这套流程走下来120 张企鹅数据从标注到训练到验证大概两天能跑完一轮。我自己的习惯是每做完一个数据集把转换脚本、切分脚本、校验脚本打包成一个prepare.py下次遇到新类别只改classes列表和目录名就能复用。小样本检测的功夫不在模型结构上在数据准备和校验的细节里这一点做企鹅数据集和做工业缺陷检测没有区别。希望帮到你。本文还有配套的精品资源点击获取