YOLOv5+DeepSORT飞鸟检测跟踪实战:从模型选型到工程避坑

发布时间:2026/10/13 2:45:18
YOLOv5+DeepSORT飞鸟检测跟踪实战:从模型选型到工程避坑
简介这是一套结合YOLOv5与DeepSORT的飞鸟视觉检测与跟踪完整工程面向目标检测、多目标跟踪及鸟类生态研究方向的开发者与学习者可复现实时检测加稳定跟踪的全流程。包内共244个文件涵盖Python源码、YAML模型配置、预训练权重.pt/.t7、训练可视化日志、Docker部署文件、示例图片与演示视频等py脚本负责推理与训练yaml文件定义网络结构权重可直接用于推理。压缩包约212MB已有824人学习下载。资源基于U-Net架构的YOLOv5实现鸟体定位配合Siamese网络与卡尔曼滤波完成跨帧跟踪并包含多尺度训练、数据增强等关键实现细节。项目把检测与跟踪任务解耦成两个可替换模块便于研究者分别优化和扩展整体目录结构清晰适合理解检测与跟踪算法协同、仿照此类系统搭建个人项目。1. 飞鸟视觉检测和跟踪为什么必须 YOLOv5 加 DeepSORT 双段串联傍晚水面上几十只鸟同时起飞单帧检测器能把每只鸟都框出来但下一秒所有框的编号全乱了——哪只是哪只根本对不上。这个项目解决的就是这个死穴YOLOv5 负责逐帧找到鸟DeepSORT 负责跨帧把同一只鸟认出来并维持编号。它不是单纯的检测Demo而是一套完整的「检测 跟踪」双段管线落地场景包括机场驱鸟、养殖场鸟类计数、候鸟迁徙观测。适合已经有检测基础、想做行为轨迹分析的开发者也适合刚接触跟踪任务但不想从零啃论文的初学者——直接拿这套框架改数据就能跑通。2. YOLOv5 检测端模型选型、数据集标注与训练参数怎么落2.1 模型选型飞鸟这种小目标s、m、l 到底该选哪个YOLOv5 的 s/m/l/x 四档模型本质区别是网络的深度和宽度系数。很多初学者一上来就选 yolov5s理由是速度快结果在飞鸟场景上漏检率直接崩掉。飞鸟目标有两个特殊性第一鸟在画面里通常只有几十个像素属于典型的小目标第二背景极其复杂天空、树枝、水面、建筑外墙都有可能出现模型需要更强的特征提取能力才能把鸟和背景区分开。我实测下来的建议是模型档位速度640输入GPU小目标表现适用场景yolov5s最快约120 FPS漏检较多远处小鸟基本丢快速试跑、验证流程yolov5m中等约80 FPS中等距离可稳定检出大多数飞鸟监控场景yolov5l较慢约45 FPS小目标明显改善精度优先、离线分析yolov5x最慢约25 FPS最好但显存压力大科研级精度验证我一般建议用 yolov5m 起步。如果显存够、对实时性没硬性要求直接上 yolov5l。yolov5s 在飞鸟场景下基本是「能跑但不实用」的状态别被它的速度迷惑。2.2 数据集标注与训练统一类别、锚框设置与训练命令飞鸟检测的数据集质量直接决定后续跟踪的上限。标注时有一个关键决策类别统一用一个bird还是按鸟种拆成多个类。我的建议是——除非你的场景真的需要区分鸟种否则统一用bird单类别。原因很简单DeepSORT 的跟踪效果依赖检测的稳定性类别越多模型分类出错概率越大同一只鸟可能在「麻雀」和「鹬鸟」之间横跳跟踪端会疯掉。标注工具用 labelImg输出 YOLO 格式的 txt 文件。目录结构按 YOLOv5 的标准来dataset/ ├── images/ │ ├── train/ # 训练图片 │ └── val/ # 验证图片 ├── labels/ │ ├── train/ # 对应标注txt │ └── val/ └── bird.yaml # 数据配置文件bird.yaml的内容如下train: dataset/images/train val: dataset/images/val nc: 1 names: [bird]这里nc: 1对应单类别names列表的索引必须和标注文件里的类别 ID 对齐。训练命令我用的是python train.py --img 640 --batch 16 --epochs 300 \ --data bird.yaml --weights yolov5m.pt --hyp hyp.scratch-low.yaml几个关键参数说明--img 640训练输入分辨率。飞鸟目标小可以试 768 或 896 提升小目标检出率但显存占用会明显上涨。8GB 显存跑 640 batch 16 是极限再大就爆显存。--batch 16根据显存动态调整。显存不够就降到 8但训练时间会翻倍。--hyp hyp.scratch-low.yaml这个容易被忽略。飞鸟遮挡多、目标小低数据增强策略比默认的 hyp.scratch.yaml 更稳因为过度的 mosaic 增强会把小鸟切碎让模型学到错误的特征。训练完成后用runs/train/exp/weights/best.pt做推理。注意YOLOv5 默认从最后一轮保存last.pt从验证集最优保存best.pt飞鸟场景一定用best.ptlast.pt往往过拟合了。2.3 检测推理输出置信度、NMS 和输出格式检测端推理命令python detect.py --weights best.pt --source test.mp4 \ --conf-thres 0.25 --iou-thres 0.45 --classes 0--classes 0表示只输出类别 ID 为 0 的目标即 bird这个参数在项目里很关键——如果数据集里混了其他类别的目标不加限制会把无关目标全框出来跟踪端会把这些噪声当成轨迹去追。--conf-thres和--iou-thres的搭配需要调。飞鸟场景我建议conf-thres设在 0.2 到 0.3 之间。太高会把远处的小鸟直接滤掉太低会把云朵、树叶晃动的误检框放进来。iou-thres保持 0.45 就行改动它对结果影响不大。detect.py 的输出是带标注的视频和标签文件但对跟踪来说我们真正需要的是检测框的坐标数据。所以整合时不用 detect.py而是直接用model()的推理结果——这个在后面整合章节展开。3. DeepSORT 跟踪端卡尔曼滤波、级联匹配与参数玄学3.1 跟踪为什么不能只靠检测ID 切换、遮挡与漏检直接对检测框做帧间匹配会出两个问题一是漏检时轨迹直接断掉二是遮挡后重新出现的鸟会被当成新目标。DeepSORT 解决这个问题靠的是两个核心部件卡尔曼滤波负责预测目标下一帧的位置匈牙利匹配负责把预测位置和当前检测框做最优关联。飞鸟场景里 DeepSORT 的「预测」能力格外重要。鸟的飞行速度快帧间位移大如果只靠检测框的 IOU 匹配相邻两帧的框稍微拉开一点就匹配不上。卡尔曼滤波会根据历史运动速度预测出下一帧的期望位置即使检测框有轻微偏移也能通过预测位置拉回关联。3.2 DeepSORT 核心参数max_age、min_hits、iou_threshold 与特征匹配直接看实际操作。初始化 DeepSORT 跟踪器时这些参数我全部调过from deep_sort import DeepSort deepsort DeepSort( model_pathckpt.t7, # ReID 特征提取模型权重 max_dist0.2, # 级联匹配的余弦距离阈值 min_confidence0.3, # 检测框置信度下限 nms_max_overlap1.0, # 检测框 NMS 阈值 max_iou_distance0.7, # IOU 匹配阈值 max_age50, # 轨迹最大存活帧数 n_init3, # 轨迹初始化所需最少匹配次数 nn_budget100 # 外观特征队列长度 )飞鸟场景下这几个参数必须调max_age50默认值通常是 30。鸟飞出画面后再回来是常事max_age太小会导致轨迹刚丢失就被删掉等鸟回来时 ID 已经换了。50 意味着轨迹在目标消失后还能保持 50 帧约 2 秒足够处理大部分短遮挡。n_init3默认 3 没问题。它是「连续匹配到几次才确认这条轨迹」太小会把误检框当成真轨迹太大会让真实轨迹长时间处于未确认状态。max_iou_distance0.7IOU 匹配的阈值越高越宽松。飞鸟帧间位移大默认 0.7 偏严格如果发现同一条轨迹断成两段可以放宽到 0.8。nn_budget100每条轨迹保存最近 100 帧的外观特征。飞鸟的类内差异大但运动模式单调主要靠位置和速度信息匹配外观特征只是辅助。3.3 特征提取与外观匹配ReID 模型选轻量还是重型DeepSORT 的model_path指向的是一个 ReID 模型作用是把检测框裁出来的图像映射成一个特征向量用于计算两帧目标的外观相似度。原版用的是 mars-small128在行人场景表现不错但用在不同物种上效果会打折。飞鸟的长相高度相似同一种鸟之间外观特征差异极小ReID 模型能给到的区分度有限。这块有两个选择一是直接用原版 mars-small128省事推理快但匹配时主要靠运动和位置信息。二是换更强的 ReID 模型比如 osnet特征区分度提升明显但每帧多一次额外的特征提取推理FPS 会掉 15% 到 20%。我的建议是先用原版跑通如果发现「两只鸟交错飞行后 ID 互换了」再考虑换 ReID 模型。不要一上来就上重型模型跟踪器里玄学成分很高先建立基线再逐项优化才是正路。4. 检测与跟踪串联数据流设计、类别过滤与 ROI 边界4.1 整体数据流从检测框到轨迹 ID 的衔接逻辑把这个项目拆开看YOLOv5 和 DeepSORT 是两个独立的模块中间靠一个数据结构衔接。整个流程是读入一帧图像YOLOv5 模型前向推理得到检测框列表。每个检测框做类别过滤和置信度过滤只保留bird类且置信度达标的框。检测框从xyxy格式转换为xywh格式DeepSORT 要求中心点坐标 宽高。调用deepsort.update()把检测框数据喂入内部完成卡尔曼预测 级联匹配 轨迹更新。从返回值中取出轨迹 ID、框坐标和置信度画到画布上。这个流程里最容易出错的是坐标格式和坐标系的统一。YOLOv5 的model()输出的是归一化坐标DeepSORT 需要的是像素坐标中间漏掉反归一化这一步跟踪器会收到一堆接近 0 的小框完全匹配不上。4.2 工程实现整合代码与关键参数直接贴一份我调通的整合代码基于 YOLOv5 的detect.py改的import cv2 import torch import numpy as np # 加载 YOLOv5 模型用训练好的 best.pt model torch.hub.load(yolov5, custom, pathbest.pt, force_reloadTrue) model.conf 0.25 # 检测置信度阈值 model.iou 0.45 # NMS IOU 阈值 model.classes [0] # 只保留 bird 类别 from deep_sort import DeepSort deepsort DeepSort( model_pathckpt.t7, max_dist0.2, min_confidence0.3, max_iou_distance0.7, max_age50, n_init3, nn_budget100 ) def xyxy_to_xywh(det): # YOLO 输出为 xyxy 像素坐标转为 DeepSORT 需要的 xywh x1, y1, x2, y2 det[:4] w, h x2 - x1, y2 - y1 return [x1, y1, w, h] cap cv2.VideoCapture(input.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break # 1. YOLOv5 推理 results model(frame) dets results.xyxy[0].cpu().numpy() # 像素坐标、置信度、类别 # 2. 过滤并转换检测框 bboxes_xywh [] confs [] for det in dets: if det[5] ! 0: # 类别过滤 continue bboxes_xywh.append(xyxy_to_xywh(det)) confs.append(det[4]) # 3. 喂给 DeepSORT if len(bboxes_xywh) 0: tracks deepsort.update( np.array(bboxes_xywh), np.array(confs), frame ) else: tracks deepsort.update([], [], frame) # 4. 绘制跟踪结果 for track in tracks: track_id, x1, y1, x2, y2 track cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, fID:{track_id}, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(Bird Tracking, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码有四个关键点必须说清楚model.conf、model.iou、model.classes这三个属性是 YOLOv5 推理后处理的控制开关。model.classes [0]的作用不是训练时的类别过滤而是推理输出时只保留类别 ID 为 0 的检测框能从源头减少跟踪端的噪声输入。xyxy_to_xywh函数里的格式转换不能省。YOLOv5 的检测框是[x1, y1, x2, y2]左上角和右下角像素坐标DeepSORT 接收的是[cx, cy, w, h]中心点坐标 宽高。写错坐标格式是整合阶段最高频的 bug表现为跟踪器输出为空或者轨迹乱跳。deepsort.update()在无检测框时也要调用传入空列表。卡尔曼滤波需要空帧来完成「预测」步骤——目标当前没有检测到但根据历史运动轨迹预测位置要持续更新这样目标重新出现时才能接上 ID。返回值track是[track_id, x1, y1, x2, y2]注意这里的坐标是整数但 DeepSORT 内部计算用的是浮点画框时如果不转int某些版本的 OpenCV 会报类型错误。4.3 场景约束ROI 区域与轨迹统计边界实际部署时很少对全画面做跟踪。机场驱鸟只需要关注跑道周边养殖场只需要统计投喂区附近的活动。这时候加一个 ROI 遮罩只对进入指定区域的检测框做跟踪。import cv2 import numpy as np # 定义一个多边形 ROI比如跑道端头区域 roi_points np.array([[200, 300], [800, 300], [1000, 700], [100, 700]], dtypenp.int32) def is_inside_roi(x1, y1, x2, y2): cx, cy (x1 x2) / 2, (y1 y2) / 2 # cv2.pointPolygonTest 返回 1 表示在多边形内 return cv2.pointPolygonTest(roi_points, (cx, cy), False) 0 # 在主循环中喂给 deepsort 之前加一层过滤 filtered_bboxes [det for det in dets if is_inside_roi(det[0], det[1], det[2], det[3])]ROI 过滤放在 YOLOv5 推理之后、DeepSORT 更新之前。这样做有两个好处一是减少了跟踪器的计算压力二是避免 ROI 外的背景目标干扰轨迹匹配。注意pointPolygonTest的坐标系要和视频帧对齐用原图分辨率直接画点不要经过缩放。5. 避坑排查飞鸟场景下五个最高频的翻车点5.1 坑一高空远处飞鸟根本检测不到现象视频里肉眼能看到远处有鸟飞过但检测框一直不出现跟踪结果自然是空的。原因飞鸟目标在画面中只有 10 到 20 个像素YOLOv5 的 640 输入分辨率会把这种小目标缩得更小特征图上的响应弱到被 NMS 直接滤掉。解决两条路。一是提高推理分辨率--img 896甚至 1280小目标像素占比变大检出率明显上升代价是推理时间翻倍。二是做图像切片推理——把原图切成 2x2 或 3x3 的块分别推理后合并结果相当于在不增加全局分辨率的情况下放大局部区域。我倾向后者因为切片推理对飞鸟这种稀疏目标场景很友好每块区域的鸟密度低检测精度不会因为目标密集而下降。5.2 坑二跟踪 ID 频繁跳变一只鸟被拆成三段现象同一只鸟匀速飞过画面但画面上显示 ID 从 5 变成 12 又变成 27轨迹断成好几截。原因最常见的是检测端间歇性漏检。某几帧鸟被树枝遮挡检测框消失max_age内如果没接上轨迹就会被标记为删除。等鸟重新出现时DeepSORT 匹配不到旧轨迹只能建新 ID。另一个原因是max_iou_distance太严帧间位移大的时候预测框和检测框的 IOU 低于阈值匹配失败。解决先确认检测端漏检率——把model.conf调低到 0.15 看漏检是否缓解。如果检测稳定再把max_age加到 80max_iou_distance放到 0.8。跟踪参数要配合检测端一起调单独调跟踪器收效甚微。5.3 坑三训练时 loss 不降模型完全学不到特征现象训练到 100 轮box_loss 和 cls_loss 停在某个数值附近波动验证集 mAP 几乎为 0。原因飞鸟数据集里大量图片是「一大片天空 一只小到看不见的鸟」标注框占整图面积的比例极小。YOLOv5 的默认锚框是针对 COCO 数据集设计的在训练初期通过 k-means 重算锚框时如果数据集里大框极少算出来的锚框全偏向小尺寸梯度更新不稳定。解决先检查标注质量——用可视化脚本把标注框画回图片上确认框的边界是否贴合鸟的轮廓。然后在训练命令里加上--noautoanchor参数并手动检查runs/train/exp/anchors.png里锚框分布。如果发现锚框尺寸严重偏移可以强制指定小尺寸锚框在bird.yaml里加anchors字段覆盖默认值。这个操作比较敏感具体数值要根据自己数据集的框尺寸分布来定不要照抄网上任何人的参数。5.4 坑四白天晚上检测效果悬殊傍晚置信度暴跌现象白天正常一到傍晚或阴天检测框开始抖动跟踪 ID 频繁切换。原因飞鸟场景的数据集多半是在白天采集的。傍晚光照弱、对比度低、鸟的轮廓和天空融在一起模型提取不到有效的边缘特征置信度整体下降conf-thres 0.25会滤掉大量真实检测框。解决收集傍晚和逆光场景的数据重新训练或者做数据增强时加大亮度扰动和对比度扰动的权重。如果不想重新训练可以按时间段动态调整model.conf——白天用 0.25傍晚自动降到 0.1。我实际测试过这个「分时段置信度」策略比统一阈值稳定很多代价是有时会把云朵误检进来需要在 ROI 里设置天空区域排除。5.5 坑五批量视频处理时内存爆炸现象跑了几百帧后内存占用持续上涨最后进程被杀掉。原因cv2.VideoCapture读取视频时如果后面处理速度跟不上帧队列会一直累积。加上 YOLOv5 推理每帧都会创建新的 tensor 对象某些版本的 PyTorch 在 CPU 和 GPU 间拷贝数据时存在隐性内存泄漏。解决强制限制读取帧率处理速度是多快就读多快不让帧堆积cap cv2.VideoCapture(input.mp4) fps cap.get(cv2.CAP_PROP_FPS) skip_frames int(fps / 25) # 每 25 帧处理 1 帧 frame_count 0 while cap.isOpened(): ret, frame cap.read() if not ret: break frame_count 1 if frame_count % skip_frames ! 0: continue # 正常推理和跟踪这个处理方式本质是跳帧把视频从 50 FPS 降到 25 FPS 再处理内存压力减半。另外记住在每轮循环末尾调用del frame和torch.cuda.empty_cache()能显著缓解长视频处理时的显存碎片问题。6. 把这套系统推向实用跳帧策略、TensorRT 加速与轨迹行为分析完整跑通检测加跟踪之后下一个问题是怎么让它更快、更实用。速度瓶颈主要在 YOLOv5 推理端。DeepSORT 的卡尔曼滤波和匹配计算量很小ReID 特征提取才是第二瓶颈。我的优化路径是先量化再上 TensorRT。先把 YOLOv5 的 FP16 精度打开——PyTorch 的 AMP 模式在保持精度基本不变的情况下推理速度能提升 40% 左右。如果还嫌慢把模型导出成 TensorRT engine在 Jetson 或带 Tensor Core 的 GPU 上能再快一倍。导出命令是python export.py --weights best.pt --include engine --device 0 --half导完后加载 engine 文件推理方式和 PyTorch 模型完全一致但延迟大幅下降。DeepSORT 的 ReID 特征提取同样可以转 TensorRT但收益没那么大建议先只加速 YOLOv5 部分。精度验证方面单纯看画面「感觉跟踪挺稳」是不够的。我习惯用 MOT 指标里的 IDF1 和 ID Switch 数来量化评估——IDF1 衡量轨迹匹配的准确率ID Switch 统计 ID 跳变次数。做法是把跟踪结果保存成 MOT 格式的 txt然后用公开的评估工具对比真值。跑一遍评估你会吃惊地发现肉眼看着没问题的跟踪结果ID Switch 可能已经上百次了。从那以后我每次做目标跟踪项目都强制自己先跑一遍 MOT 评估再做优化而不是凭肉眼判断好坏。最后的进阶玩法是把轨迹数据用起来。飞鸟检测跟踪的终极价值不在视频里画几个框而在轨迹分析——统计每天经过 ROI 的鸟数量、平均停留时长、飞行方向偏好甚至用每条轨迹的坐标序列做聚类识别出鸟群的主飞行通道。这些分析不需要额外模型用轨迹点的(x, y, frame_id)序列就能完成。做法是把 DeepSORT 输出的轨迹按track_id分组提取每条轨迹的起点、终点、速度和长度存成 CSV后续用 pandas 就能做行为分析。这能做的事很多编排飞行时间、喂食区域热点、驱鸟策略效果评估都在这一步落地。希望帮到你。本文还有配套的精品资源点击获取