
简介这是一份面向计算机视觉初学者与目标检测实践者的鸭子图像数据集可用于训练和验证 duck 类别的检测模型适合课程作业、算法练手及小样本实验等场景。资源包共1039个文件以346张jpg图片、346个xml标注文件和347个txt标注文件为主图片与标注一一对应压缩包约25.28MB解压后无需密码即可直接查看。所有图片均由labelImg人工标注类别统一为duck标注过程强调边界框选准确、目标不遗漏并通过一致性检查保证质量。包内按图片、VOC格式xml、YOLO格式txt分三个文件夹存放方便直接接入主流检测框架。目前已有130人学习下载可作为快速构建鸭子检测任务的现成数据基础。1. 鸭子数据集 VOC 和 YOLO 格式目标标注 348 张小样本检测到底能不能打手上只有 348 张标注图还想训一个能用的鸭子目标检测模型这事我干过不止一次。结论先放这能打但前提是你得把 VOC 和 YOLO 两套格式的转换、划分、增强和评估链路全部打通否则模型要么学不会要么在验证集上给你演一出“指标很好看、实际全乱框”的玄学戏。鸭子数据集 VOC 和 YOLO 格式目标标注 348 张本质是一个典型的小样本单类或少数类目标检测任务VOC 提供 XML 结构化标注YOLO 吃的是归一化后的 txt 坐标348 张图意味着你必须把每一张的标注质量、类别一致性、训练集/验证集分布都抠到极致。适合谁适合手上有类似规模自建数据集、想跑通 YOLOv8 训练自己数据集全流程的从业者也适合需要把 VOC 老标注迁移到 YOLO 生态的工程师。不适合指望 348 张直接刷 SOTA 的人那是另一条路。2. 从 VOC XML 到 YOLO txt转换脚本与四个边界坑2.1 先看清 VOC 和 YOLO 的坐标逻辑差异VOC 的 XML 里bndbox存的是绝对像素坐标xmin, ymin, xmax, ymax原点在左上角右下角是xmax, ymax。YOLO 的 txt 每行是class_id x_center y_center width height全部归一化到 0~1且x_center和y_center是框中心点相对于图像宽高的比例。很多人第一次转格式翻车就是因为把xmax当成了宽或者忘了减 1 导致的像素偏移。常见做法是宽 xmax - xmin高 ymax - ymin中心 x xmin 宽/2中心 y ymin 高/2再分别除以图像宽高。注意 VOC 里可能存在difficult或truncated标记鸭子数据集如果标注时没统一转换时要么过滤要么保留但必须和训练配置一致。2.2 转换脚本一次跑通 VOC 到 YOLO 的目录映射下面这个脚本我一般会放在数据集根目录下跑输入是 VOC 风格的Annotations和JPEGImages输出是 YOLO 需要的images和labels平行目录。脚本里加了类别映射和图像尺寸校验避免因为某张图读不出来导致整个转换中断。import os import xml.etree.ElementTree as ET from PIL import Image # 类别映射鸭子数据集如果只有一类就写 {duck: 0} CLASS_MAP {duck: 0} VOC_ANN_DIR Annotations VOC_IMG_DIR JPEGImages OUT_IMG_DIR images OUT_LBL_DIR labels os.makedirs(OUT_IMG_DIR, exist_okTrue) os.makedirs(OUT_LBL_DIR, exist_okTrue) for xml_file in os.listdir(VOC_ANN_DIR): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(VOC_ANN_DIR, xml_file)) root tree.getroot() img_name root.find(filename).text img_path os.path.join(VOC_IMG_DIR, img_name) if not os.path.exists(img_path): print(f跳过缺失图像: {img_name}) continue with Image.open(img_path) as im: w, h im.size lines [] for obj in root.findall(object): cls_name obj.find(name).text.strip() if cls_name not in CLASS_MAP: continue bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 边界裁剪防止标注超出图像范围 xmin max(0, min(xmin, w - 1)) xmax max(0, min(xmax, w - 1)) ymin max(0, min(ymin, h - 1)) ymax max(0, min(ymax, h - 1)) bw xmax - xmin bh ymax - ymin if bw 1 or bh 1: continue xc (xmin bw / 2) / w yc (ymin bh / 2) / h nw bw / w nh bh / h lines.append(f{CLASS_MAP[cls_name]} {xc:.6f} {yc:.6f} {nw:.6f} {nh:.6f}) # 复制图像到输出目录 im.save(os.path.join(OUT_IMG_DIR, img_name)) # 写 label 文件空标注也写空文件避免训练时找不到 lbl_name os.path.splitext(img_name)[0] .txt with open(os.path.join(OUT_LBL_DIR, lbl_name), w) as f: f.write(\n.join(lines)) print(转换完成)逻辑说明脚本先解析 XML拿到图像宽高再逐 object 算归一化坐标。参数上CLASS_MAP必须和后续 YOLO 的data.yaml里names顺序完全一致否则类别会错位。边界裁剪那几行是血泪经验有些标注工具会导出xmax等于图像宽度的情况不裁剪的话归一化后可能大于 1训练时虽然不报错但框会飘。空标注文件也要写YOLO 训练时如果某张图没有对应 label会直接报错找不到文件。2.3 四个边界坑坐标越界、类别错位、文件名不匹配、空标注第一个坑是坐标越界。现象是训练 loss 正常下降但验证时框跑到图像外原因就是xmax或ymax等于图像宽高甚至超出解决就是上面脚本里的min裁剪。第二个坑是类别错位。现象是模型把鸭子识别成背景原因通常是CLASS_MAP和data.yaml的names顺序不一致解决是两边都只保留一个类别且 id 从 0 开始。第三个坑是文件名不匹配。现象是转换后 images 里有图但 labels 里没有对应 txt原因是 XML 里的filename和实际图像文件名大小写或后缀不一致解决是在脚本里加os.path.splitext统一处理。第四个坑是空标注。现象是训练时 dataloader 报IndexError原因是某些图没有目标但没写空 txt解决是像上面那样即使lines为空也写一个空文件。3. 348 张怎么划分与增强别让验证集骗了你3.1 训练集/验证集划分8:2 还是 7:3348 张这个量级我一般按 8:2 分训练集 278 张验证集 70 张。但这里有个容易被忽略的点如果鸭子在不同场景水面、草地、岸边分布不均随机划分会导致验证集里某个场景一张都没有指标虚高。常见做法是先按场景或拍摄批次分层再在每层里随机抽 20% 做验证。如果数据集本身没有场景标签那就至少保证划分后训练集和验证集的标注框数量比例接近。可以用下面这段脚本做分层抽样按图像里目标数量分桶。import os import random import shutil LABEL_DIR labels IMG_DIR images TRAIN_RATIO 0.8 random.seed(42) # 按目标数量分桶 buckets {} for lbl in os.listdir(LABEL_DIR): if not lbl.endswith(.txt): continue with open(os.path.join(LABEL_DIR, lbl)) as f: n len([line for line in f if line.strip()]) buckets.setdefault(n, []).append(lbl) train_list, val_list [], [] for n, files in buckets.items(): random.shuffle(files) split int(len(files) * TRAIN_RATIO) train_list.extend(files[:split]) val_list.extend(files[split:]) for phase, files in [(train, train_list), (val, val_list)]: os.makedirs(fdatasets/{phase}/images, exist_okTrue) os.makedirs(fdatasets/{phase}/labels, exist_okTrue) for lbl in files: img_name os.path.splitext(lbl)[0] .jpg shutil.copy(os.path.join(IMG_DIR, img_name), fdatasets/{phase}/images/{img_name}) shutil.copy(os.path.join(LABEL_DIR, lbl), fdatasets/{phase}/labels/{lbl}) print(f训练集 {len(train_list)} 张验证集 {len(val_list)} 张)参数说明TRAIN_RATIO控制比例random.seed保证可复现。分桶逻辑是按每张图的目标数量分组这样验证集里不会全是单目标或全是多目标。注意复制图像时假设后缀是.jpg如果你的鸭子数据集是.png要改成对应后缀。3.2 小样本增强Mosaic、MixUp 和 HSV 的取舍YOLOv8 默认开启 Mosaic 和 HSV 增强对 348 张这种小数据集来说Mosaic 能显著增加背景多样性但也会带来一个问题如果鸭子目标本身很小Mosaic 拼接后目标更小反而难学。我一般会保留 Mosaic 但把mosaic概率从 1.0 降到 0.5同时开启mixup到 0.1 左右。HSV 增强里hsv_h不要太大鸭子羽毛颜色是重要特征色调抖动过猛会让模型学偏。具体在data.yaml同级建一个hyps.yaml覆盖默认超参# hyps.yaml mosaic: 0.5 mixup: 0.1 hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4 degrees: 5.0 translate: 0.1 scale: 0.5 flipud: 0.0 fliplr: 0.5逻辑说明mosaic: 0.5表示一半概率做拼接mixup: 0.1做少量图像混合degrees: 5.0只做小角度旋转因为鸭子通常不会倒着出现。flipud: 0.0关闭上下翻转水面倒影场景除外。这些参数不是死的如果验证集 mAP 波动大优先降mixup和scale。3.3 用 YOLOv8 训练鸭子数据集的完整命令环境配置不展开假设你已经装好ultralytics。在数据集根目录建duck.yamlpath: ./datasets train: train/images val: val/images names: 0: duck然后跑训练yolo detect train dataduck.yaml modelyolov8n.pt epochs200 imgsz640 batch16 patience50 device0参数说明modelyolov8n.pt用 nano 版348 张图用大模型容易过拟合。epochs200配合patience50如果 50 轮验证指标不升就早停。imgsz640是默认值如果鸭子目标普遍偏小可以提到 960但显存要够。batch16根据显存调不够就降。训练过程中重点看metrics/mAP50-95和val/box_loss如果训练 loss 降但验证 loss 升就是过拟合回头降模型规模或加增强。4. 训练中 BN 崩溃与混淆矩阵不唯一排查与避坑4.1 现象训练几轮后 loss 变 NaNBN 层报错这是小数据集训练里最典型的翻车现场。现象是前几轮正常突然 loss 变成 NaN控制台提示BatchNorm相关错误。原因通常有两个一是 batch size 太小比如设成 2 或 4BN 在少量样本上统计量方差过大二是某张图的标注框归一化后宽高为 0 或负数导致梯度爆炸。解决方法是先把batch提到 8 以上如果显存不够就换小模型或降imgsz然后检查所有 label 文件过滤掉宽高小于 0.001 的行。我一般会在训练前跑一遍数据校验脚本把异常标注直接打印出来。4.2 现象混淆矩阵总和不等于验证集图片数YOLOv8 训练完会输出混淆矩阵有人发现矩阵里所有数值加起来和验证集图片数对不上。原因不是 bug而是混淆矩阵统计的是“预测框”和“真实框”的匹配结果一张图可能有多个目标也可能有目标没被匹配上。如果鸭子数据集里有些图没有标注框这些图不会贡献矩阵数值。另外conf阈值也会影响匹配数量。解决方法是看矩阵时结合val的precision和recall一起看不要只盯总和。如果确实想核对可以把conf设为 0.001 再跑一次验证看匹配数是否接近真实框总数。4.3 现象验证集 mAP 很高但实际推理漏检严重这是最让人头疼的“指标骗局”。原因通常是验证集和训练集分布太接近比如验证集里鸭子都是同一角度、同一背景模型只是记住了而不是学会了。解决方法是重新划分验证集确保验证集里有训练集没出现过的背景或角度另外可以拿一批完全没参与训练的图做测试看实际漏检率。如果测试集漏检高优先加数据而不是调模型。348 张的极限大概在 mAP50 0.85 左右再往上就得靠更多数据或更强增强。4.4 现象训练时提示找不到 label 文件现象是FileNotFoundError或No labels found。原因通常是duck.yaml里的train路径写成了绝对路径但实际是相对路径或者 images 和 labels 目录名不匹配。YOLO 默认要求 images 和 labels 平行且 label 文件名和图像文件名一致。解决方法是检查path和train拼接后的实际路径用ls确认 images 下有图、labels 下有同名 txt。如果图像是.jpeg而 label 是.txt没问题但图像后缀和 label 前缀必须一致。4.5 现象GPU 显存够但训练速度极慢现象是nvidia-smi显示显存占用不高但每轮训练时间很长。原因可能是 dataloader 的workers设得太小或者图像尺寸太大导致 CPU 预处理瓶颈。解决方法是把workers提到 8 或 16imgsz如果不需要 640 就降到 512。另外如果数据集放在机械硬盘上复制到 SSD 再训。这些细节不影响精度但影响你迭代的速度。5. 小样本鸭子的进阶技巧从 348 张里再榨出 10 个点如果你已经跑通上面的流程mAP50 卡在 0.8 左右上不去可以试下面几个技巧。第一个是“复制粘贴增强”把训练集里的鸭子目标抠出来随机贴到其他背景图上生成新样本。这个对单类检测特别有效但要注意粘贴时的尺度、光照和边缘融合否则会引入噪声。第二个是“伪标签”先用当前模型对未标注的鸭子图做推理把高置信度的预测框当标签人工过一遍后加入训练集。348 张如果不够这是最低成本的扩增方式。第三个是“冻结 backbone 微调”用 COCO 预训练的 YOLOv8n先冻结前几层训 50 轮再解冻全量训 100 轮小数据集上比直接全量训更稳。验证方法上我习惯在训练完后跑一次yolo detect val把plots设为True看val_batch0_pred.jpg和val_batch0_labels.jpg的对比。如果预测框和真实框位置差得远说明回归分支没学好可以调box损失权重如果类别置信度低调cls权重。最后别迷信 348 张能训出工业级模型它更适合做原型验证和流程跑通。我自己的习惯是每加 100 张新标注就重新跑一次完整训练记录 mAP 变化直到收益递减。希望帮到你。本文还有配套的精品资源点击获取