☰
鸟类识别数据集转YOLO目标检测实战:从CUB-200-2011到训练部署
2026/10/1 22:28:36 网站建设 项目流程

简介:这份鸟类识别数据集专为目标检测与深度学习场景设计,聚焦Chestnut Munia、Zebra Dove、Collared Kingfisher等10个鸟类类别,共覆盖16287幅图像样本,兼顾模型训练对数据规模与类间多样性的需求。资源同时提供YOLO与VOC两种格式的标注信息,txt标签配合类别yaml配置,可直接接入YOLOv5~YOLOv10、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", required=True, help="CUB_200_2011 数据集根目录") ap.add_argument("--out", default="cub_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(parents=True, exist_ok=True) 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(parents=True, exist_ok=True) 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_ratio=0.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_unicode=True)

注意 YAML 里 names 是 dict 而非 list,如果写成 list,Ultralytics 在部分版本里也能解析,但 dict 形式最稳。类别数量 nc 和 names 长度不一致时,训练会直接报错,没有任何商量余地,所以类名顺序的事最好交给脚本。

3.3 训练命令与必调参数

转换完成、YAML 就位,剩下的就是一顿命令行调用:

# 训练鸟类检测模型 yolo task=detect mode=train \ model=yolov8n.pt \ data=birds.yaml \ epochs=150 imgsz=640 batch=16 \ device=0 patience=20 \ project=runs name=bird_v1 \ pretrained=True

先说几个我踩过坑后才固定的参数。imgsz=640 是最常用分辨率,但对小目标鸟来说 640 可能不够,这一步在显存允许时可以升到 1280,代价是训练时间大约变为 2 到 3 倍。batch=16 是默认值,如果显存只有 8G,batch=8 才是稳定线。patience=20 是早停轮数,验证 mAP 连续 20 轮没提升就停,省时间也防过拟合。

pretrained=True 默认是从 YOLOv8 的预训练权重加载。如果你只想学检测头、保留更低的显存占用,可以加 freeze=10,冻结主干前 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 task=detect mode=predict \ model=runs/bird_v1/weights/best.pt \ source=test_images \ conf=0.25 iou=0.45 \ save=True

source 可以是指向单张图的文件,也可以是指向图片文件夹的目录。conf=0.25 是默认置信度阈值,如果漏检多,把 conf 下调到 0.1,这时候模型会吐出一堆低置信度框,这些渲染成图片后,一眼就能看出是背景误检还是真鸟漏检。iou 控制 NMS 的框合并力度,同位置多个框时 iou 越高越容易合并,0.45 是常规值。

可视化图输出在 runs/detect/predict 下。我通常把置信度阈值调低跑一遍,重点看三种图:图上没框的,属于漏检;框在树枝、水面上的,属于误检;同一只鸟两个框的,属于 NMS 参数没调好。先修这三类问题,比调模型更实在。

4.2 用 val 模式输出混淆矩阵与各类 AP

predict 只能定性,量化要靠 val 模式:

yolo task=detect mode=val \ model=runs/bird_v1/weights/best.pt \ data=birds.yaml \ split=val plots=True

plots=True 会生成 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, window=640, stride=640): 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:y+window, x:x+window] res = model(crop, conf=0.25, iou=0.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 取窗口一半,比如 window=640、stride=320。这个脚本每跑完一张图,生成的结果就能单独核对:画面里有多少只鸟、识别出了几只、漏的都在远处还是近处。把漏检的截下来,就是下一轮难例挖掘的素材。

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 小目标漏检:远处的鸟真的就十几个像素

现象:低置信度阈值也跑不出远处的鸟,画面上只有一个小小的黑点,人眼能看出来是鸟,模型完全没有响应。

原因:目标像素太少。训练时 imgsz=640,一只在画面角落的鸟实际就几十个像素,下采样后有效特征几乎丢光。直接把整图缩进网络里,那些小鸟就像背景噪声。

解决:两种常规方案。第一,训练时增大 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_ok=True) for img_path in val_dir.glob("*.jpg"): result = model(str(img_path), conf=0.3, iou=0.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 个点。这个数字对于基线模型来说不算巨大,但它是靠数据侧增量换来的,不增加推理成本。我现在拿到任何新的鸟类识别数据集,第一件事就是先跑一遍验证和混淆矩阵,再看难例集里能不能捞出值得补标的样本。做目标检测数据集的功夫,一半在转换脚本里,另一半在难例挖掘这轮循环里。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询