ARTICLE DETAIL

资讯详情

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

YOLOv5鸟类检测数据集实战:从VOC格式转换到模型训练全流程

YOLOv5鸟类检测数据集实战:从VOC格式转换到模型训练全流程 简介这份鸟类检测数据集面向计算机视觉与目标检测开发者基于PASCAL VOCtrainval2012提取仅含“bird”单一类别专用于YOLOv5目标检测模型的鸟类检测训练与评估。压缩包共2434个文件包含811张jpg图像、812个txt标注文件与811个xml标注文件其中txt文件记录边界框坐标与类别IDxml文件采用VOC标准格式提供更丰富的目标信息可满足不同训练框架的标注需求。资源总大小约90.55MB下载后可直接开展数据预处理、模型配置、训练调优及部署验证。已有2126人学习下载非常适合研究YOLOv5目标检测算法、尝试迁移学习或扩展鸟类识别应用场景的开发者。借助这套数据读者能够系统实践从数据标注解析、格式转换、模型配置到训练调优与评估的完整流程掌握数据集构造与YOLOv5训练调参方法为后续研究或项目开发打下坚实基础。1. 分拣YOLOv5鸟类检测数据集先搞清楚你拿到的这份VOC任务拿到这份“yolov5 鸟类检测 bird鸟类检测数据集”解压后你会看到大量类似2009_000206.jpg的图片以及成对出现的.xml和.txt文件。这个命名风格太典型了它就是从 PASCAL VOC2012 里单独把bird这一个类别抽出来的子集类别只有一类标注里不会出现 person、car 等干扰项。它的价值在于你不需要在 20 类里跟你目标物纠缠而是拿到一个干净的单类数据集直接用来跑通 YOLOv5 训练自己的数据集。这适合刚接触目标检测的从业者把图像预处理、标签格式转换、训练、评估到部署的链路完整走一遍也适合老手拿它当算法对比的基准先验证一套超参数到底能不能收敛。2. txt 与 XML 两套标注先统一成 YOLO 格式再谈训练拿到这个压缩包第一步不是急着开训练而是把标注格式理顺。YOLOv5 要求的是每个图片对应一个同名的.txt标签文件每行格式是类别id 中心x 中心y 宽 高坐标全部归一化到 0 到 1 之间。原始数据集里给的却是 PASCAL VOC 风格的.xml和普通文本.txt这两种格式跟 YOLOv5 的预期都不一样直接丢进训练脚本必报错。2.1 先分清 XML 标签和普通 TXT 标签的差异XML 遵循 PASCAL VOC 的规范包含图片名、图片尺寸、通道数每个object节点里还有类别名和bndbox边界框。你可以把它理解成结构化描述信息最全。annotation filename2009_000206.jpg/filename size width500/width height375/height depth3/depth /size object namebird/name bndbox xmin82/xmin ymin113/ymin xmax360/xmax ymax303/ymax /bndbox /object /annotation这份 XML 里能读到图片绝对路径下的文件名2009_000206.jpg也能直接拿到500x375的尺寸转换起来最稳。而普通 txt 的格式则简陋得多每一行由图片名、左上角坐标、右下角坐标和类别ID拼成如下2009_000206.jpg 82 113 360 303 0注意这里的坐标沿用的是 VOC 的 x1 y1 x2 y2 风格也就是左上角和右下角的绝对像素位置。后面转换时两者都必须换算成 YOLO 格式。我的建议是优先用 XML 作为标注唯一来源普通 txt 只作为参考因为 XML 里带尺寸信息不会出现坐标在图片外这种低级脏数据。2.2 转换脚本XML 和 TXT 统一变成 YOLO 标签文件转换脚本的做法很固定解析XML拿坐标再用 OpenCV 读一次图片拿到真实宽高后计算归一化值。不要信任 XML 里写的 width/height 字段我在实际项目中遇到过多次 XML 尺寸和真实图片尺寸对不上的情况用真实图片尺寸更稳。import os import cv2 import xml.etree.ElementTree as ET from pathlib import Path CLASSES [bird] # 单类别id 固定为 0 def voc_xml_to_yolo(xml_path, img_dir, out_dir): tree ET.parse(xml_path) root tree.getroot() img_name root.find(filename).text img_path os.path.join(img_dir, img_name) img cv2.imread(img_path) if img is None: print(skip missing image:, img_path) return h, w img.shape[:2] # 从真实图片读取宽高避免标注尺寸漂移 lines [] for obj in root.iter(object): name obj.find(name).text if name not in CLASSES: continue # 只保留 bird 类 bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 防御性处理坐标倒挂时强制修正 xmin, xmax min(xmin, xmax), max(xmin, xmax) ymin, ymax min(ymin, ymax), max(ymin, ymax) # 转 YOLO中心点 宽高全部除以图片宽高做归一化 cx ((xmin xmax) / 2) / w cy ((ymin ymax) / 2) / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) label_out os.path.join(out_dir, Path(img_name).stem .txt) with open(label_out, w, encodingutf-8) as f: f.write(\n.join(lines))逻辑说明这段脚本先把 XML 里的filename拼出完整图片路径用 OpenCV 读取后得到真实高度和宽度随后遍历所有object过滤掉非 bird 类别再把xmin ymin xmax ymax换算成 YOLO 需要的中心坐标和宽高并除以图片尺寸完成归一化。坐标倒挂的防御处理非常关键PASCAL VOC 标注偶尔会出现 xmin 大于 xmax 的情况不修正的话训练时 loss 会直接飘成 NaN。参数说明CLASSES列表目前只有 bird 一个类别所以类别 id 固定写死为 0如果你后续用这份数据集扩展多类别比如把personbird都加进去只需扩充列表然后把lines.append里的硬编码0改成CLASSES.index(name)。out_dir建议直接指向 YOLOv5 项目下的labels/train目录省得后面再搬一次文件。普通 TXT 的转换脚本类似但因为 TXT 不携带图片尺寸读取图片这一步更是免不了。如果图片尺寸读不到直接把这个样本剔除。import os import cv2 from pathlib import Path def plain_txt_to_yolo(txt_path, img_dir, out_dir): with open(txt_path, r, encodingutf-8) as f: rows f.read().strip().splitlines() for row in rows: parts row.strip().split() if len(parts) ! 6: continue # 约定格式图片名 x1 y1 x2 y2 class_id img_name, x1, y1, x2, y2, cls_id parts img cv2.imread(os.path.join(img_dir, img_name)) if img is None: continue h, w img.shape[:2] cx ((float(x1) float(x2)) / 2) / w cy ((float(y1) float(y2)) / 2) / h bw abs(float(x2) - float(x1)) / w bh abs(float(y2) - float(y1)) / h label_out os.path.join(out_dir, Path(img_name).stem .txt) with open(label_out, a, encodingutf-8) as f_out: f_out.write(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n)这段脚本用abs把宽高变成正值避免坐标倒挂直接生成负数宽高。需要留意的是如果图片里有多只鸟普通 txt 会写成多行追加写模式能保证所有框都落进同一个 label 文件。2.3 转换后的校验先查坐标边界和文件对齐关系转换脚本写完不能直接开训还得做一轮检查。我一般会跑一次批量校验统计所有 label 文件中的坐标是否全部落在 0 到 1 之间同时检查图片和 label 是否完全同名。这一步能挡掉大部分脏数据。常见做法是用 glob 拿到全部 jpg 和 txt比对文件主干的集合是否一致。另一个容易被忽略的点是同一张图片如果同时存在 xml 和 txt转换过程会产生两个同名 label 文件后写的会覆盖先写的。我处理这类数据集时的原则是只保留 XML 解析结果txt 从存储里挪走避免训练时标注被静默覆盖。然后是可视化抽查我习惯随机画三张图看框和鸟的位置是否吻合这一步能发现标签串位这种坐标没错但框错位的问题。import cv2 import glob for img_path in sorted(glob.glob(images/train/*.jpg))[:3]: label_path img_path.replace(images/, labels/).replace(.jpg, .txt) img cv2.imread(img_path) h, w img.shape[:2] for line in open(label_path, r, encodingutf-8): cls_id, cx, cy, bw, bh line.strip().split() x1 int((float(cx) - float(bw) / 2) * w) y1 int((float(cy) - float(bh) / 2) * h) x2 int((float(cx) float(bw) / 2) * w) y2 int((float(cy) float(bh) / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, cls_id, (x1, y1 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imwrite(fcheck_{img_path.split(/)[-1]}, img)可视化脚本把归一化坐标还原成像素坐标画框并在左上角写类别 id。如果框明显偏离鸟身说明源标注本身有问题不要带病进训练。这一步虽然土但比任何精度检查都好用。3. 数据预处理与训练配置按 YOLOv5 的脾气组织目录和 YAML标注格式转换完成接下来要把物料按 YOLOv5 的目录习惯摆好。YOLOv5 的数据加载逻辑是读一个 YAML 文件里的train和val路径再根据路径去拿图片和同名 label。所以目录结构、YAML 内容、图片和 label 的同名对齐这三件事直接决定训练能不能正常启动。3.1 目录结构和数据集 YAML 怎么搭一个标准的 YOLOv5 本地数据集目录长这样图片和 label 分成 train 和 val 两个子集注意 label 目录名必须叫labels图片目录叫images这个约定不能改。bird_datasets/ ├── images/ │ ├── train/ │ │ ├── 2009_000206.jpg │ │ └── ... │ └── val/ ├── labels/ │ ├── train/ │ │ ├── 2009_000206.txt │ │ └── ... │ └── val/ ├── train.txt ├── val.txt └── bird.yamlbuild 目录就是移动文件把images里 80% 的图片挪进images/train剩余 20% 挪进images/vallabel 文件按照同名原则跟随图片各归其位。这一步做好后写 YAML 数据配置文件# bird.yaml train: ./images/train val: ./images/val nc: 1 names: [bird]这里的train和val路径是相对路径相对于你执行训练命令时所在的目录。如果你用的是绝对路径换机器后只要路径一变就得重新改 YAML我吃过这个亏在本机训练到一半换到服务器上结果训练启动时一直报找不到图片。所以我现在写 YAML 一律用相对路径。nc: 1表示类别数只有一个names列表里也只有bird。如果以后扩展类别两个字段必须同步更新否则训练脚本会在标签加载时报警告。3.2 训练前的数据自检写完成目录和 YAML 后我会先跑一段快速统计确认图片数和 label 数大致对齐并计算一下每张图的平均框数量。这个数字能直观反映数据集的标注密度。import glob train_imgs glob.glob(images/train/*.jpg) train_labels glob.glob(labels/train/*.txt) print(train images:, len(train_imgs)) print(train labels:, len(train_labels)) total_boxes 0 for lp in train_labels: n len(open(lp, encodingutf-8).read().strip().splitlines()) total_boxes n print(total boxes:, total_boxes)这段统计的意义在于发现空 label 文件。如果某个 label 文件内容为空训练时 YOLOv5 会视该图片为负样本单类数据集里这类负样本如果占比过高模型很容易学成什么都检测不到。统计完成后我一般会把空 label 对应的图片一并从 train 集合里移除。3.3 YOLOv5 训练启动与参数语义环境是另一件头疼事。yolov5 官方仓库的requirements.txt列了 torch、opencv-python 等依赖我建议直接新建一个 conda 环境。conda create -n yolov5 python3.8 -y conda activate yolov5 pip install torch torchvision pip install -r requirements.txt参数说明Python 3.8 是对 PyTorch 兼容性最稳的版本torch和torchvision的版本必须配套直接用pip install torch torchvision会拉取默认配套版本requirements.txt里缺什么补什么不建议在这个环节手动改版本号除非你明确知道某个版本有坑。训练命令用下面这行python train.py --data bird.yaml --weights yolov5s.pt --img 640 --batch-size 16 --epochs 100参数说明--weights yolov5s.pt是 COCO 预训练权重用它当作迁移学习起点收敛速度远快于从零训练--img 640是训练输入尺寸这个值决定显存占用和检测精度小鸟目标建议后面调大到 1280--batch-size 16是 NVIDIA 3070 或 V100 级别显卡的常见选择显存不够就降到 8--epochs 100对单类数据集够用我一般会加一个--patience 20连续 20 轮验证集 mAP 不上升就自动早停节省时间。训练启动后第一屏会输出模型结构参数量和各层信息。真正要盯的是每轮结束打印的PRmAP0.5和mAP0.5:0.95。如果第一个 epoch 结束后 mAP 还是 0不要慌前面几个 epoch 主要是让模型适配新数据集的标注维度通常第 5 到第 10 个 epoch 之后指标会开始跳动。另外一个细节是超参数文件data/hyps/hyp.scratch-low.yaml里的默认数据增强。单类数据集里随机翻转fliplr: 0.5不会产生类别语义问题但hsv_h这类色彩扰动对鸟类这类外观敏感的目标建议把色调扰动调小一点否则鸟的羽毛颜色被扭曲后模型更容易把绿色背景误判成目标。这个文件里的参数直接改 YAML 即可训练时会自动读取。4. 训练过程与模型评估别只看 lossmAP 才是单类任务的北极星模型启动训练之后你会在终端看到大量日志。很多新手只看 loss 曲线但 loss 下降并不代表检测准确。对单类目标检测来说mAP 的参考价值远高于 loss尤其是 mAP0.5:0.95 这个严格指标。4.1 训练日志里那些字段怎么读YOLOv5 训练时每个 epoch 结束会打印一张指标表核心字段含义如下表字段含义我的评判习惯PPrecision所有预测框中真正是鸟的比例单类任务里最好到 0.9 以上RRecall真实鸟中有多少被召回低于 P说明漏检比误检更严重mAP0.5IoU 阈值等于 0.5 时的平均精度训练阶段性参考容易虚高mAP0.5:0.95不同 IoU 阈值下的平均精度更严格最终上线看这个val/box_loss边界框回归损失数值下降趋势比绝对值重要通常我在训练前期只看 mAP0.5 有没有从 0 往上走这说明模型学到东西了。到第 30 个 epoch 以后重点看 mAP0.5:0.95 和 R 的变化如果 P 很高但 R 一直上不去就是典型的漏检后面要从输入尺寸和数据增强两方面解决。4.2 val.py 验证模型到底能不能上线训练完的模型用best.pt验证路径在runs/train/exp/weights/best.pt。所谓 best 是按验证集 mAP 选的不是按训练 loss 选的。python val.py --data bird.yaml --weights runs/train/exp/weights/best.pt --img 640验证完成后终端会输出最终的 P、R、mAP同时跑出一批预测样本图放在runs/val/exp里。我一般会去翻这些图重点看两类错误一是漏检真实存在的鸟没有框二是虚检把树枝、石头、阴影框成鸟。框画得准但置信度不高的情况可以调低验证时的置信度阈值再看一次。python val.py --data bird.yaml --weights runs/train/exp/weights/best.pt --img 640 --conf-thres 0.1 --iou-thres 0.45--conf-thres 0.1让模型把置信度大于 10% 的框都画出来--iou-thres 0.45控制 NMS 合并阈值。这两个参数直接影响验证结果里的假阳性数量跑验证时用低置信度能暴露更多漏检问题但最终部署时通常会把这个阈值调回 0.25 以上来压误检。4.3 单类数据集的超参数调优顺序单类任务和 80 类 COCO 的调优顺序不同。我调过几次后发现对只有 bird 一个类别的任务超参数影响从大到小排列是输入分辨率 学习率 数据增强强度 模型容量。第一优先是输入分辨率。从 640 提到 1280 通常能让小鸟目标 mAP 提升 5 到 8 个点代价是显存占用近乎翻倍训练速度也相应变慢。第二是学习率YOLOv5 默认 lr0 是 0.01单类数据集上我习惯把它降到 0.003 起步因为数据模式简单过大的学习率容易在初期优化方向震荡。第三才是数据增强尤其hsv_h这类颜色扰动减小到 0.01 以下往往对鸟类这类颜色敏感目标更友好。模型容量yolov5s在这个任务上已经够用除非你想在极小目标上继续压榨性能才需要换yolov5m或yolov5l。另外单类数据集极易过拟合。一个常见现象是训练 loss 一路下降但验证 mAP 在第 40 个 epoch 后开始回落这说明模型把训练集里的背景特征也记进去了。处理办法最直接的就是早停训练命令里加--patience 20让它自己判断如果你还想再压榨一点可以在hyp.scratch-low.yaml里把mosaic改到 1.0 并配合mixup 0.2让模型见更多异构图。5. 鸟类检测避坑标注错位、小目标漏检与数据泄漏排查数据集和训练链路跑通只是第一步真正让你熬夜的是各种诡异的训练故障。这一章内容来自我实际踩过的坑每一条都花了至少半天才定位列出来希望你别绕远路。5.1 报错AssertionError: Label class X exceeds nc1是什么原因现象训练刚启动几秒终端直接抛AssertionError提示 label 里的类别 id 超过了 1。原因数据集中某个 label 文件里出现了类别 id 大于等于 1 的行。虽然这个数据集是从 VOC 里提取的单类 bird但我在解码 rar 时发现过漏网的 txt 文件里混入了其它类别的行可能是标注工具生成时没有完全过滤干净。解决写一段脚本扫描所有 label 文件把类别 id 不小于nc的行打印出来定位文件。这类脏行可以直接删除或者用 XML 重新生成一遍 label 再覆盖。我在实际项目里用 XML 重新生成来兜底因为 XML 里name字段清楚地标着类别名过滤逻辑不容易出错。5.2 验证集 mAP 很高实拍一张照片却什么都检测不到现象训练时 mAP0.5 到 0.95 都到了 0.8 以上但把训练集里任意一张图拎出来做推理画不出框或者置信度极低。原因这是数据泄漏的典型特征。如果 train 和 val 划分时没有做随机洗牌或者同一个场景的多张连续帧同时进了训练集和验证集模型会把验证样本的语境背下来而不是学到鸟的外观特征。PASCAL VOC 本身是按场景组织的场景与场景之间的视觉差异很大划分集合时不打乱验证集里就会出现和训练集高度相似的图片。解决划分 train/val 时必须先用随机数打乱图片列表再切片。我一般用random.shuffle或sklearn的train_test_split并且固定随机种子保证可复现。划分完了还要按图片聚类检查一遍不要出现同一背景的多张连续帧分属不同集合的情况。5.3 远距离小鸟漏检严重近处的大鸟检测很好现象模型对近处大鸟识别没问题但场景里远处树梢上只有几十像素的小鸟完全不检测。原因640 输入尺寸下小目标在特征图上的有效像素可能不到 10 个。YOLOv5s 的浅层特征图分辨率有限小目标的语义信息在多层下采样后基本丢失。这个数据集的 VOC 图片里很多鸟都是远距离小目标漏检并不意外。解决先把--img调到 1280输入分辨率越高小目标在下采样前保留的像素就越多。如果显存不够可以在训练脚本里启用--rect让图片保持原始宽高比减少无效缩放。再不行就引入 SAHI 这类切片推理工具推理时把大图切成小图分别检测再合并结果代价是推理时间成倍增加。5.4 训练 loss 一开始就是 NaN或者前几个 epoch 直接爆炸现象训练日志里box_loss或cls_loss变成nan有时第一个 epoch 就出现有时第 10 个 epoch 突然出现。原因最常见的是 label 文件里有坐标超级大的值比如归一化后的cx跑到 3.7或者出现负数宽高。还有一种可能是学习率过大单类数据模式单一优化器在二维损失面上震荡幅度大把梯度推向了数值不稳定区域。解决先用脚本扫描 label 坐标范围全部限制在 0 到 1 之间超范围的样本直接剔掉。坐标没问题时把--epochs调大是没用的应该把学习率降到 0.001 甚至 0.0003 重新训练。我在单类任务上碰到 loss NaN十次有八次是坐标脏数据剩下两次是学习率太高。5.5 训练时图片数和 label 数差很多模型训练质量突然下降现象训练统计数据里 train images 有 500 张但 train labels 只有 380 个文件差出来的 120 张图完全没有 label 文件。原因这个数据集同时存在 XML 和 TXT 两套标注而我在此前清理时只把 XML 转出的 label 留在了目录里某些图片在转换过程中因为 XML 内容为空或者解析失败被跳过了。YOLOv5 加载逻辑里没有 label 文件的图片会被当作背景负样本参与训练背景比例过大模型自然朝什么都检不到的方向偏移。解决逐个比对图片主文件名和 label 主文件名没有 label 的图片从训练目录移出。更稳的做法是统计每个 label 文件的行数所有空 label 文件对应的图片一起剔除。这个校验必须在每次转换后强制执行不要默认上一轮做过了就跳过。6. 进阶把模型导出为 ONNX以及给暗光场景补数据的习惯模型训练到 mAP 达标只是起点真正让它产生价值的是拿到真实场景里跑。这里我讲一个最常用的进阶动作用 ONNX Runtime 做端侧推理以及一个我坚持了很久的暗光数据补充习惯。导出 ONNX 格式只需要一条命令yolov5 官方的 export 脚本会直接打包好python export.py --weights runs/train/exp/weights/best.pt --include onnx --img 640导出后得到best.onnx用它推理不再需要 PyTorch摆脱了模型权重对训练框架的依赖在纯 CPU 环境下也能跑起来。我一般再用onnxruntime做一次推理验证import cv2 import onnxruntime as ort sess ort.InferenceSession(best.onnx) img cv2.imread(test.jpg) # 这里省略 resize、归一化、letterbox 等预处理 # 输出张量经后处理解析出坐标和置信度在边缘设备上跑 OpenVINO 或者 TensorRT 导出前我都会先用 ONNX 做一次标准验证确认模型推理结果和 PyTorch 推理结果差异小于 1%再继续做深度倍量化或 INT8 量化。这一步虽然繁琐却能省掉后端部署时排查问题的两个晚上。另一个让我受益多年的习惯是无论原始数据集多干净我都会在训练结束后拿一批真实夜间或者逆光场景图做补充标注再丢进训练集重训一轮。鸟类检测的应用场景很少是实验室里的正光环境树荫下的暗光鸟体、逆光剪影里的轮廓这些才是野外监控最常遇到的难点。红外夜视场景下我会先把图像做一次自适应直方图均衡化再辅以少量暗光样本训练。从那以后我每次拿到新数据集都会强制走一遍流程先可视化校验标注再跑一轮快速训练验证数据加载最后才决定要不要投入完整训练周期。希望这套流程能帮到你。本文还有配套的精品资源点击获取
返回列表