ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

鸟类识别数据集转YOLO目标检测实战:从CUB-200-2011到训练部署

鸟类识别数据集转YOLO目标检测实战:从CUB-200-2011到训练部署 简介这份鸟类识别数据集专为目标检测与深度学习场景设计聚焦Chestnut Munia、Zebra Dove、Collared Kingfisher等10个鸟类类别共覆盖16287幅图像样本兼顾模型训练对数据规模与类间多样性的需求。资源同时提供YOLO与VOC两种格式的标注信息txt标签配合类别yaml配置可直接接入YOLOv5YOLOv10、Faster RCNN、SSD等主流框架训练集、验证集、测试集已预先划分目录结构清晰省去手动标注与格式转换环节。压缩包共2000个文件以1999个txt标签文件为主体另含1个yaml类别配置文件总大小167.9MB体积精简便于快速下载与本地部署。yaml中已写入10个类别的名称列表切换模型时无需再逐一修改标签标签文件命名保留原始图像特征方便与素材对应。目前已有483人学习下载适合刚进入目标检测领域的初学者、需要扩充鸟类样本的算法工程师以及从事鸟类监测、生态统计等项目的开发者。1. 鸟类识别数据集做目标检测先把“能分类”和“能检测”这条分界线划清楚我第一次拿 CUB-200-2011 做鸟类识别数据集时第一个错觉是“数据都齐了直接训练就行”。翻目录才发现标签全在 txt 里每张图属于哪一类、边界框在哪都要自己解析。鸟类识别数据集做目标检测落地第一步就是把以分类为初衷的数据转成目标检测需要的“图片 归一化坐标”格式再交给 YOLO 这类检测框架。这一步做不做输出完全不同分类模型回答“画面里有鸟、大概哪一种”目标检测回答“鸟在哪、几只、个体多大”。野外相机监测、机场驱鸟、城市鸟类调查要的是后者。这篇笔记按“选型、转换、训练、验证、排错”的顺序把流程和踩过的坑一次讲完读者可以按目录跳到正在卡壳的段落不用从头读到尾。2. 主流鸟类识别数据集怎么选从 CUB-200-2011 的 txt 标注转成 YOLO 格式数据是目标检测项目的燃料但真正决定工作量的不是图片数量而是标注格式。公共鸟类数据集的标注大多数不是为检测设计的有的给分类标签有的给属性有的只有零星 bbox。拿到手先做三件事确认 bbox确认类别定义确认图像来源。这三样直接决定你要不要自己补标以及转换脚本怎么写。2.1 选数据集看三样东西bbox 有没有、类别粒度、拍摄来源先把最常用的几个鸟类识别数据集摆在一起对比。表格里没有排名只有适合什么场景。数据集标注形式类别规模适合做什么CUB-200-2011分类标签 bbox 属性标注200 类 / 千余张级细粒度验证、检测模型入门NABirds分类 bbox 部件关键点约 555 类北美鸟类细分种群iNaturalist以分类标签为主部分带框数万类预训练、开放类别识别Bird1445以分类图像为主资料中常按多模态数据处理约 1445 类类别跨度大的细粒度识别实验CUB 是“做目标检测最省事的起点”因为它的 bbox 已经给好了不需要从零标框。200 个类对应 200 种北美常见鸟比如黑脚信天翁、黄莺这类细粒度差异明显的物种既有挑战性又能跑得动。NABirds 的 bbox 质量更细类别更多但它偏向北美区域做国内或东南亚鸟种时覆盖度不够。iNaturalist 适合做预训练或当负样本来源它的大类非常多直接拿来做检测意味着你要自己补标几千张图成本极高。Bird1445 这类数据则经常出现在“多模态检索 识别”的工程里先确认它有没有 bbox 再谈训练。选型顺序我一般这么定做算法验证用 CUB做产品原型用 CUB NABirds 混合做真实野外部署时再用自己的拍摄数据做领域迁移。iNaturalist 不作为首选检测训练集但可以作为分类头的预训练数据。2.2 把 CUB-200-2011 转成 YOLO 格式解析 txt 并归一化坐标CUB 的标注分布在四个 txt 文件里images.txt 记录图片 id 和相对路径image_class_labels.txt 记录图片 id 和类别号bounding_boxes.txt 记录图片 id 和 bbox 的 x、y、宽、高train_test_split.txt 记录图片 id 属于训练还是测试。YOLO 需要的格式则是每张图对应一个同名 txt每行是“类别号 中心点 x 中心点 y 宽 高”所有数值都除以图像宽高做归一化。下面这段脚本就是把 CUB 转成 YOLO 标签的核心逻辑我在多个项目里复用过大半。# cub_to_yolo.py # 用法示例: python cub_to_yolo.py --root /data/CUB_200_2011 --out ./cub_yolo from pathlib import Path import argparse from PIL import Image def load_pair(path): 把 CUB 的 id value 两列 txt 读成 dict mapping {} with open(path) as f: for line in f: parts line.strip().split() if len(parts) 2: mapping[int(parts[0])] parts[1] return mapping def main(): ap argparse.ArgumentParser() ap.add_argument(--root, requiredTrue, helpCUB_200_2011 数据集根目录) ap.add_argument(--out, defaultcub_yolo, help输出目录) args ap.parse_args() root Path(args.root) images load_pair(root / images.txt) classes load_pair(root / image_class_labels.txt) bboxes {} with open(root / bounding_boxes.txt) as f: for line in f: parts line.strip().split() idx int(parts[0]) x, y, w, h float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) bboxes[idx] (x, y, w, h) out_labels Path(args.out) / labels out_images Path(args.out) / images out_labels.mkdir(parentsTrue, exist_okTrue) for idx, rel_path in images.items(): if idx not in bboxes: continue x, y, w, h bboxes[idx] img_path root / images / rel_path with Image.open(img_path) as im: W, H im.size # 越界保护CUB 的标注偶发超出图像边界 x max(0.0, x) y max(0.0, y) w min(float(W) - x, w) h min(float(H) - y, h) if w 1 or h 1: continue # 中心点转换并归一化到 0~1 cx (x w / 2.0) / W cy (y h / 2.0) / H bw w / W bh h / H # CUB 的类别号从 1 开始YOLO 要求从 0 开始 cls_id int(classes[idx]) - 1 line f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n (out_labels / (Path(rel_path).stem .txt)).write_text(line) # 图片同步复制进输出目录保留原二级目录结构 dst out_images / rel_path dst.parent.mkdir(parentsTrue, exist_okTrue) if not dst.exists(): dst.symlink_to(img_path.resolve()) if __name__ __main__: main()这段脚本的关键点在于CUB 类号从 1 开始YOLO 从 0 开始不减 1 会导致所有类别标签错位一位归一化用的图像宽高必须和实际载入训练时的宽高一致不同库读图时如果有旋转修正坐标就会对不上越界保护不是可选项CUB 的 bbox 是人工标的偶尔超出图像边缘不截断会让检测训练时 loss 出现 NaN。运行完检查一下输出目录。每个 label 文件都应该能对应到一张图片并且 txt 里不超过 5 个浮点数。这一步检查只要几分钟能省掉后面训练时的数小时排查。2.3 划分 train/val用自带的 split而不是随机打乱CUB 自带 train_test_split.txt这是最稳妥的划分方式直接照用它就行。如果你用的是 NABirds它也自带官方划分。这两类数据基于同一批图反复切分才会出问题。如果是自己攒的鸟类数据那就要注意“随机打乱是陷阱”。假设一个场景里连拍了几十张同一只鸟姿态接近、背景几乎不变按图片随机切分后训练集和验证集里可能同时出现同一场景验证指标虚高野外部署时立刻打回原形。我一般按“拍摄事件”分桶给每张图的文件名前缀编号或者按子目录名作为组。把同一组的图片全部进 train 或全部进 val才能模拟真实场景。# group_split.py # 按图片所在子目录作为“事件组”划分避免同一场景同时出现在 train 和 val from collections import defaultdict from pathlib import Path def group_split(img_root, val_ratio0.2): groups defaultdict(list) for img in Path(img_root).rglob(*.jpg): group_key img.parent.name groups[group_key].append(img) keys sorted(groups.keys()) val_keys set(keys[:max(1, int(len(keys) * val_ratio))]) train_list, val_list [], [] for key in keys: if key in val_keys: val_list.extend(groups[key]) else: train_list.extend(groups[key]) return train_list, val_list这段代码用“目录名”当分组边界多数拍摄项目天然按日期和地点归档直接对得上。如果原始文件没有目录那就从文件名里提取时间戳或地点前缀。这个“按组划分”的习惯在野生动物监测里比随机划分稳得多。3. 用 YOLOv8 训练自己的鸟类识别数据集YAML 配置、训练命令和指标格式转换完就要进入模型训练了。这一章直接拿 YOLOv8 说事因为它的命令行接口最省事数据格式要求就是上一章转出来的 YOLO txt不引入额外依赖。YOLOv5 也有不少人用但新项目里我默认 v8迁移成本和坑都少一些。3.1 为什么选目标检测 YOLO而不是直接用图像分类模型只看“图里有没有鸟”用分类模型就够了但鸟类识别在真实场景里几乎都伴随着“在哪”的问题监测相机拍到一只鸟就要求同时输出位置和置信度。分类模型做不到多目标计数更不能告诉你两只鸟靠在一起时哪只是哪只。YOLO 相比两阶段检测器优势是快、部署简单对边缘设备友好。野外相机通常装在树桩或铁塔上供电和算力都有限单阶段检测器是最现实的选择。YOLOv8 的预测头还带了对象置信度和类别置信度方便后续做难例筛选。训练自己的数据集时从头训练不可取都会用 COCO 或者 YOLOv8 官方预训练权重做迁移学习。对鸟类这类细粒度小类别任务预训练模型在高维特征上的泛化能力很关键后面只需用鸟类识别数据集做微调即可。3.2 编制 birds.yaml 和目录结构YOLOv8 训练时需要一个 YAML 文件描述数据集。目录结构建议统一成“images/train、images/val、labels/train、labels/val”。上一章转换脚本只产出了单层 images 和 labels这里还需要按划分移动文件。先写 YAML# birds.yaml path: /data/cub_yolo # 数据集根目录后续 train/val 都相对它 train: images/train # 训练图片目录会自动找同级 labels/train val: images/val # 验证图片目录 nc: 200 # 类别数量 names: 0: Black_footed_Albatross 1: Laysan_Albatross ...一个很实用的做法是用脚本生成 names避免手写 200 行出错。CUB 的 classes.txt 里已经按顺序罗列了类名直接读出来拼进 dict 即可。# make_yaml.py import yaml from pathlib import Path with open(classes.txt) as f: names [line.strip() for line in f if line.strip()] data { path: /data/cub_yolo, train: images/train, val: images/val, nc: len(names), names: {i: name for i, name in enumerate(names)}, } with open(birds.yaml, w) as f: yaml.safe_dump(data, f, allow_unicodeTrue)注意 YAML 里 names 是 dict 而非 list如果写成 listUltralytics 在部分版本里也能解析但 dict 形式最稳。类别数量 nc 和 names 长度不一致时训练会直接报错没有任何商量余地所以类名顺序的事最好交给脚本。3.3 训练命令与必调参数转换完成、YAML 就位剩下的就是一顿命令行调用# 训练鸟类检测模型 yolo taskdetect modetrain \ modelyolov8n.pt \ databirds.yaml \ epochs150 imgsz640 batch16 \ device0 patience20 \ projectruns namebird_v1 \ pretrainedTrue先说几个我踩过坑后才固定的参数。imgsz640 是最常用分辨率但对小目标鸟来说 640 可能不够这一步在显存允许时可以升到 1280代价是训练时间大约变为 2 到 3 倍。batch16 是默认值如果显存只有 8Gbatch8 才是稳定线。patience20 是早停轮数验证 mAP 连续 20 轮没提升就停省时间也防过拟合。pretrainedTrue 默认是从 YOLOv8 的预训练权重加载。如果你只想学检测头、保留更低的显存占用可以加 freeze10冻结主干前 10 层。这个操作对小数据集很有效CUB 这种每类才几十张图全部参数微调容易在特征层面过拟合。如果想更快上手用 yolov8n.pt 作为 baseline等验证集指标上不去再切 yolov8m.pt。换更大的模型之前先确认问题是否出在数据侧否则模型越换越玄学。3.4 先看 mAP50 还是 mAP50-95鸟类检测的合理指标训练结束时的验证输出里最值得注意的是两个指标mAP50 和 mAP50-95。mAP50 表示 IoU 阈值 0.5 下的平均精度比较宽松mAP50-95 则是在 0.5 到 0.95 之间取十个阈值求平均要求预测框得贴合得很准。鸟类目标不是矩形刚体翅膀张开、尾羽岔开边界框标注天然有噪声所以 mAP50 往往容易达到 0.7 以上mAP50-95 却可能只有 0.4。如果 mAP50 不错但 mAP50-95 很低说明你的框整体贴合度差优先去查标注框是否过大过小而不是急着换模型。如果两个值都低先回看 bbox 坐标转换是否有误。对细粒度鸟类识别来说框的精准度还会影响后续分类分类头拿到的裁剪区域如果把旁边树枝也算进去细粒度特征就会被污染。所以我的习惯是 mAP50-95 优先。4. 训练完怎么看效果可视化、置信度与混淆矩阵训练完的模型不能只看 val 数字还要落到“在一批真实图片上到底找不找得准”。我每次跑完第一轮先做可视化检查再看混淆矩阵最后用大图滑窗验证真实场景的漏检率。4.1 用 detect predict 快速做可视化检查最简单的方式是直接调用 YOLOv8 的 predict 命令yolo taskdetect modepredict \ modelruns/bird_v1/weights/best.pt \ sourcetest_images \ conf0.25 iou0.45 \ saveTruesource 可以是指向单张图的文件也可以是指向图片文件夹的目录。conf0.25 是默认置信度阈值如果漏检多把 conf 下调到 0.1这时候模型会吐出一堆低置信度框这些渲染成图片后一眼就能看出是背景误检还是真鸟漏检。iou 控制 NMS 的框合并力度同位置多个框时 iou 越高越容易合并0.45 是常规值。可视化图输出在 runs/detect/predict 下。我通常把置信度阈值调低跑一遍重点看三种图图上没框的属于漏检框在树枝、水面上的属于误检同一只鸟两个框的属于 NMS 参数没调好。先修这三类问题比调模型更实在。4.2 用 val 模式输出混淆矩阵与各类 APpredict 只能定性量化要靠 val 模式yolo taskdetect modeval \ modelruns/bird_v1/weights/best.pt \ databirds.yaml \ splitval plotsTrueplotsTrue 会生成 confusion_matrix.png 和各类的 P-R 曲线文件存在 runs/val 里。鸟类识别最典型的杀器就是混淆矩阵多个相似物种会不会互相串类看矩阵右上角的非对角线数值就知道了。如果黑脚信天翁有一大部分被判成了 Laysan 信天翁说明这两个类特征太接近。这时候把这两类单独拉出来看样本通常会发现数据不平衡或者标注时本身就把两者标混了。混淆矩阵能帮你从“模型挺准”的错觉里跑出来看清真正卡脖子的是哪几对类别。4.3 大图上验证把“模型能不能在真实场景找到鸟”变成可量化指标训练集图片多是单鸟特写真实部署却是整张大图鸟只占画面一角。这时候要写一个滑窗推理脚本把大图切成小块送进模型再把框恢复到原图坐标。把它当作一次完整的小验证能直接暴露小目标漏检问题。# sliding_eval.py # 将大图滑窗切割后推理再合并回原图坐标用于人工检查 import cv2 from ultralytics import YOLO model YOLO(runs/bird_v1/weights/best.pt) def sliding_predict(img_path, window640, stride640): im cv2.imread(img_path) H, W im.shape[:2] results [] for y in range(0, H, stride): for x in range(0, W, stride): crop im[y:ywindow, x:xwindow] res model(crop, conf0.25, iou0.45)[0] for b in res.boxes: cx, cy, bw, bh b.xywh[0].tolist() # 当前 crop 内的坐标 results.append((x cx, y cy, bw, bh, float(b.conf), int(b.cls))) return results if __name__ __main__: boxes sliding_predict(field_camera_01.jpg) print(f共检测到 {len(boxes)} 只鸟目标)滑窗步长等于窗口尺寸时不重叠速度最快但鸟若恰好在接缝处容易被截断所以验证时我建议 stride 取窗口一半比如 window640、stride320。这个脚本每跑完一张图生成的结果就能单独核对画面里有多少只鸟、识别出了几只、漏的都在远处还是近处。把漏检的截下来就是下一轮难例挖掘的素材。5. 鸟类识别目标检测数据集的 5 个实战坑现象、原因与解决这一章写我在这类数据集上反复遇到的坑。每条都是“现象、原因、解决”的结构按真实优先级排列值得收藏起来对照。5.1 边界框越过图像边界训练时报 bbox 越界现象转换脚本跑完YAML 也写好了训练开始不到几十轮日志里冒出类似“assertion failed”或者 loss 变成 NaN 的报错。原因CUB 或 NABirds 的 bbox 是人工标注的个别框左上角是负数或者宽高超出图像尺寸这类噪声在目标检测里会被当成无效样本严重时直接让损失计算崩掉。解决转换阶段必须对 bbox 做 clamp。我在转换脚本里加了x max(0.0, x)、w min(W - x, w)这套保护逻辑并把宽高小于 1 像素的样本跳过。跑完转换后顺手做一个扫描脚本数一遍是否有坐标越界或类别 id 越界能省下后面排查训练崩溃的时间。5.2 相似物种类别混淆单类 AP 低得扎眼现象整体 mAP50 在 0.7 以上但打开 val 输出的每个类别 AP几个形态相近的类别 AP 只有 0.2 左右。预测图里黑脚信天翁常被标成 Laysan 信天翁。原因鸟类识别数据集的细粒度特性决定了相似种之间的视觉差异小如果每个类别样本数量又少模型很难学到稳定的判别特征。加上标注时的人为误差猫头鹰、隼这类类内差异大的属种更容易混。解决先看混淆矩阵确认是哪几对在混再决定是合并类别还是增加数据。产品逻辑只关心“有没有什么鸟”时把高度混淆的类合并成一个元类是最省事的做法。必须区分物种时优先去补这几对类别的样本并给训练加上类别权重让少量类别的 loss 在反向传播中占比更高。5.3 同一场景图片同时进了训练和验证指标虚高现象验证集 mAP50 高达 0.9信心满满地拿野外新拍的图片去测漏检率却明显更高真实表现和指标完全对不上。原因数据集来自连续拍摄同一只鸟在同一背景下有几十帧随机切分时这些相似帧被分到了 train 和 val 两边。模型背下了背景的纹理验证时同时考了同一张卷子。解决回到第 2.3 节提到的分组划分按文件名前缀、目录名或拍摄时间把同一事件内的图片全部归到同一侧。这个坑在下线前发现还能救上线后用户现场翻车返工成本就不是几个小时能算完的了。5.4 小目标漏检远处的鸟真的就十几个像素现象低置信度阈值也跑不出远处的鸟画面上只有一个小小的黑点人眼能看出来是鸟模型完全没有响应。原因目标像素太少。训练时 imgsz640一只在画面角落的鸟实际就几十个像素下采样后有效特征几乎丢光。直接把整图缩进网络里那些小鸟就像背景噪声。解决两种常规方案。第一训练时增大 imgsz 到 1280但显存和训练时长显著增加适合离线训练。第二推理时用滑窗或切块把大图分区域放大再合并结果这是野外相机部署最常用的手段。另外增强训练数据的 Mosaic 也有效把小目标拼进大图让网络多见到这种尺度。5.5 标注工具格式混乱类别 id 和类名对不上现象项目里流传下来的数据集有的图片带 VOC 格式 xml有的是 YOLO txt还有一堆 json类别 id 完全乱套训练起来 val 结果根本没法解释。原因不同标注工具默认格式不同。有人用 LabelImg 存 xml有人用 X-AnyLabeling 导出 json还有人直接手改 txt导致同类别在不同文件里 id 不一样甚至同一个类名大小写都不一致。解决统一用 YOLO txt 作为中间格式先写一个扫描脚本把 xml、json 全部转成 txt然后核对 classes.txt 是否和 YAML 的 names 一致。推荐标注工具时我一般用 X-AnyLabeling 或 Label Studio都支持导出自定义格式也比 LabelImg 维护得勤快。无论用哪个入训练流程之前先跑一遍格式校验脚本类别数、文件对数、字段数都要过一道。6. 难例挖掘把已有鸟类识别数据集再榨出一轮精度模型第一轮训练完成后最有性价比的动作不是换更大的模型而是难例挖掘。你手头已经有一个能跑出框的模型它就是最好的数据筛选器。先把 val 集和一批没标注的真实图片喂给模型把置信度在 0.3 到 0.7 之间、且和真实标注 IoU 低于 0.5 的框全捞出来。这些难例通常包含三类被遮挡的鸟、姿态怪异的鸟、背景酷似鸟的树桩。把这三类图片放进一个新目录人工快速复核校准标注后加入训练集。这一步我一般做两轮第一轮加难例第二轮加上一轮新训练后新增的难例。如果想把这个过程自动化一点可以用 predict 的输出直接导出一个难例清单# hard_example_filter.py # 从验证集推理结果里收集置信度中等的错误框输出到 hard_examples/ 供人工复核 from ultralytics import YOLO from pathlib import Path model YOLO(runs/bird_v1/weights/best.pt) val_dir Path(datasets/field_raw) out_dir Path(hard_examples) out_dir.mkdir(exist_okTrue) for img_path in val_dir.glob(*.jpg): result model(str(img_path), conf0.3, iou0.5)[0] for box in result.boxes: conf float(box.conf) # 中等置信度 模型拿不准正是要重点复核的难例 if 0.3 conf 0.7: out_dir.joinpath(img_path.name).write_bytes(img_path.read_bytes())这个阈值范围不是死的野外场景背景复杂我会放宽到 0.2 到 0.8 之间保证不遗漏难负样本。人工复核时把误检图单独放进“负样本目录”下轮训练时作为背景类样本加入能明显抑制树枝和水面的乱框。难例挖掘做完后用新数据微调原模型通常能让 mAP50-95 再涨 3 到 5 个点。这个数字对于基线模型来说不算巨大但它是靠数据侧增量换来的不增加推理成本。我现在拿到任何新的鸟类识别数据集第一件事就是先跑一遍验证和混淆矩阵再看难例集里能不能捞出值得补标的样本。做目标检测数据集的功夫一半在转换脚本里另一半在难例挖掘这轮循环里。希望帮到你。本文还有配套的精品资源点击获取
返回列表