ARTICLE DETAIL

资讯详情

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

DeepSORT-YOLOv5无人机检测跟踪:从检测框到运动轨迹全链路实战

DeepSORT-YOLOv5无人机检测跟踪:从检测框到运动轨迹全链路实战 简介这份资源面向计算机视觉入门与进阶开发者提供一套基于YOLOv5与DeepSORT的无人机目标检测与多目标跟踪完整实现并附带目标运动轨迹可视化功能可用于无人机监控、智能安防、交通管理等场景的实时识别与持续追踪。压缩包共221个文件约130.46MB以60个py源码、43个yaml配置、34个pyc编译文件为主另含14张jpg与6张png示例图、10个md说明文档、3个pt权重文件及2段mp4演示视频覆盖模型配置、训练脚本与运行示例。目前已有1781人学习下载。项目整合了YOLOv5检测、DeepSORT跟踪与轨迹绘制模块读者可据此理解卡尔曼滤波与匈牙利算法在身份保持中的作用掌握PyTorch框架下的工程组织方式并借助可视化结果分析目标出现、移动与消失的完整过程适合作为课程设计或课题开发的参考方案。1. DeepSORT-YOLOv5无人机检测和跟踪从检测框到运动轨迹一条链路怎么跑通无人机视角下的目标检测和跟踪和地面监控完全是两回事。地面摄像头机位固定背景稳定行人车辆尺度变化有限无人机一升空画面持续平移旋转目标像素面积可能从 30×30 缩到 8×8再加上俯视角度带来的外观畸变检测器帧间抖动一大跟踪器就容易跟丢或者频繁切换 ID。我最初拿 YOLOv5 DeepSORT 直接套无人机视频检测框每帧都在跳轨迹画出来像心电图根本没法用。这套方案要解决的核心问题就一个在无人机动态背景下把 YOLOv5 的逐帧检测结果串成稳定的目标 ID再把每个 ID 的历史位置连成运动轨迹最终在画面上可视化出来。适合谁做无人机巡检、航拍目标计数、低空安防的工程师手里有 YOLOv5 训练好的权重想加一层跟踪和轨迹输出但不想从头造轮子。下面按「检测器怎么配 → DeepSORT 怎么接 → 轨迹怎么画 → 坑在哪」的顺序拆开讲每一步都给可复现的命令和参数。2. YOLOv5 在无人机画面上的检测配置权重、置信度和 NMS 怎么定2.1 无人机场景下 YOLOv5 的选型逻辑YOLOv5 有 n/s/m/l/x 五个规格无人机机载算力通常有限Jetson 系列或者树莓派 5 上跑n 和 s 是首选。我一般用 yolov5s输入尺寸 640×640在 VisDrone 或者自建无人机数据集上 fine-tune 之后mAP0.5 能到 0.45 左右帧率在 Jetson Xavier NX 上大约 18-22 FPS够跟踪用。为什么不直接上 YOLOv8 或者 YOLOv11可以上检测头更强但 DeepSORT 的关联逻辑依赖检测框的稳定性YOLOv5 的输出格式和 DeepSORT 的集成代码更成熟社区里现成的 deepsort-yolov5 仓库大多基于 v5 的输出做解析。如果你已经用 v8/v11 训好了改一下检测结果解析那几行也能接核心是保证输出格式统一为[x1, y1, x2, y2, conf, cls]。训练自己的数据集时无人机数据标注要注意两点小目标单独拉出来看召回遮挡目标别当负样本扔。YOLOv5 的hyp.scratch-low.yaml里box损失权重可以适当调高到 0.06cls保持 0.5因为无人机场景类别通常不多分类损失不用太大。2.2 检测输出的关键参数conf 和 iou 怎么设YOLOv5 推理时两个参数直接决定 DeepSORT 的输入质量# 无人机视频推理输出检测结果供 DeepSORT 使用 python detect.py \ --weights runs/train/exp/weights/best.pt \ --source drone_video.mp4 \ --img-size 640 \ --conf-thres 0.35 \ --iou-thres 0.45 \ --classes 0 2 \ --save-txt \ --project runs/detect \ --name drone_exp--conf-thres 0.35无人机画面里小目标多置信度阈值设太高会漏检设太低会引入大量误检。0.35 是我在 VisDrone 上试出来的平衡点漏检和误检比例大约 1:1.2。如果你做的是车辆跟踪可以降到 0.3做人形跟踪0.4 更稳。--iou-thres 0.45NMS 的 IoU 阈值。无人机俯拍时目标密集比如停车场场景两辆车挨得近IoU 阈值太高会把相邻目标合并。0.45 比默认 0.5 略低能保留更多相邻框。但别低于 0.4否则同一个目标会出多个框DeepSORT 会当成多个目标跟。--classes 0 2只保留特定类别。无人机场景通常只关心人、车、船等少数几类过滤掉无关类别能减少 DeepSORT 的关联计算量也降低 ID 切换概率。--save-txt把检测结果存成 txt每行格式是cls x_center y_center w h conf归一化坐标。DeepSORT 需要的是绝对坐标的[x1, y1, x2, y2]后面要写个转换脚本。注意YOLOv5 的detect.py默认输出归一化坐标DeepSORT 的Detection类需要绝对坐标。转换时用img_width和img_height乘回去别搞反了。2.3 检测结果转 DeepSORT 输入格式的脚本YOLOv5 的 txt 输出不能直接喂给 DeepSORT中间要做一个格式转换。下面这个脚本我用了很久处理单帧检测结果import numpy as np def yolo_to_deepsort(yolo_txt_path, img_w, img_h): 将 YOLOv5 的归一化 txt 转为 DeepSORT 需要的绝对坐标列表 返回: [[x1, y1, x2, y2, conf, cls], ...] detections [] with open(yolo_txt_path, r) as f: for line in f.readlines(): parts line.strip().split() if len(parts) 6: continue cls, xc, yc, w, h, conf map(float, parts[:6]) # 归一化坐标转绝对坐标 x1 (xc - w / 2) * img_w y1 (yc - h / 2) * img_h x2 (xc w / 2) * img_w y2 (yc h / 2) * img_h # 边界裁剪防止越界 x1, y1 max(0, x1), max(0, y1) x2, y2 min(img_w, x2), min(img_h, y2) detections.append([x1, y1, x2, y2, conf, int(cls)]) return np.array(detections) # 调用示例 img_w, img_h 1920, 1080 dets yolo_to_deepsort(runs/detect/drone_exp/labels/frame_001.txt, img_w, img_h) print(f转换后检测框数量: {len(dets)})逻辑说明YOLOv5 的 txt 每行是cls xc yc w h conf其中xc, yc, w, h都是相对于图像宽高的归一化值。DeepSORT 的Detection需要[x1, y1, x2, y2]绝对坐标所以先反归一化再裁剪边界。conf和cls保留后面做类别过滤和置信度加权用。参数说明img_w和img_h必须和 YOLOv5 推理时的输入尺寸一致但注意 YOLOv5 内部会做 letterbox 缩放如果你直接读原始视频帧宽高用原始分辨率如果读的是detect.py保存的图片用图片实际尺寸。这个坑我踩过坐标偏了 20 个像素跟踪框一直飘。3. DeepSORT 接入 YOLOv5级联匹配和卡尔曼滤波的参数调法3.1 DeepSORT 的核心机制为什么比 SORT 多一层外观特征DeepSORT 在 SORT 的基础上加了一个外观特征提取网络把每个检测框对应的图像区域过一个 ReID 模型得到一个 128 维的特征向量。匹配时不仅看卡尔曼滤波预测的位置和马氏距离还看外观特征的余弦距离。无人机场景下目标外观变化剧烈光靠位置匹配很容易串 ID外观特征能拉回来一部分。DeepSORT 的匹配流程分两步第一步用外观特征做级联匹配优先匹配最近几帧都出现的目标第二步用 IoU 匹配剩下的。级联匹配的max_dist默认 0.2nn_budget默认 100这两个参数在无人机场景下要调。max_dist控制外观距离阈值越小越严格。无人机目标小ReID 特征本身就不稳定设 0.2 会导致很多正确匹配被拒。我一般调到 0.3给外观特征更多容错。nn_budget是每个轨迹保留的历史特征数量无人机画面变化快保留太多旧特征反而干扰调到 50 更合适。3.2 卡尔曼滤波参数过程噪声和观测噪声怎么配DeepSORT 的卡尔曼滤波用匀速模型状态向量是[x, y, a, h, vx, vy, va, vh]其中a是宽高比h是高度。过程噪声协方差矩阵Q和观测噪声协方差矩阵R在deep_sort/kalman_filter.py里定义# deep_sort/kalman_filter.py 中的参数调整 class KalmanFilter: def __init__(self): ndim, dt 4, 1. self._motion_mat np.eye(2 * ndim, 2 * ndim) for i in range(ndim): self._motion_mat[i, ndim i] dt # 观测噪声无人机检测框抖动大调高 R self._std_weight_position 1. / 20 # 默认 1./20 self._std_weight_velocity 1. / 160 # 默认 1./160_std_weight_position控制位置观测噪声默认 1/20。无人机检测框每帧抖动可能到 5-10 像素这个值要调小比如 1/30让滤波器更相信预测而不是观测。_std_weight_velocity控制速度噪声默认 1/160无人机目标运动速度快可以调到 1/200让速度估计更平滑。调参效果在自建的无人机车辆跟踪视频上默认参数 ID 切换次数 47 次调完_std_weight_position1/30和max_dist0.3之后降到 19 次。代价是跟踪框响应稍微滞后但轨迹连续性明显提升。3.3 完整跟踪脚本YOLOv5 检测 DeepSORT 关联 轨迹绘制下面是把检测和跟踪串起来的核心代码基于deep_sort的Tracker类import cv2 import numpy as np from deep_sort import DeepSort from yolo_to_deepsort import yolo_to_deepsort # 上一节的转换函数 # 初始化 DeepSORT deepsort DeepSort( model_pathdeep_sort/deep/checkpoint/ckpt.t7, max_dist0.3, # 外观距离阈值无人机场景调高 min_confidence0.35, # 低于此置信度的检测不参与匹配 nms_max_overlap0.45, # NMS 阈值和 YOLOv5 保持一致 max_iou_distance0.7, # IoU 匹配阈值 max_age30, # 轨迹丢失后保留帧数 n_init3, # 连续命中多少次才确认轨迹 nn_budget50 # 每个轨迹保留的特征数 ) # 轨迹历史字典{track_id: [(cx, cy), ...]} trajectories {} MAX_TRAIL_LEN 60 # 每条轨迹最多保留 60 个点 cap cv2.VideoCapture(drone_video.mp4) frame_idx 0 while cap.isOpened(): ret, frame cap.read() if not ret: break frame_idx 1 img_h, img_w frame.shape[:2] # 读取 YOLOv5 检测结果假设已提前跑完 detect.py txt_path fruns/detect/drone_exp/labels/frame_{frame_idx:06d}.txt dets yolo_to_deepsort(txt_path, img_w, img_h) if len(dets) 0: # DeepSORT 更新 bbox_xywh np.array([[ (d[0]d[2])/2, (d[1]d[3])/2, d[2]-d[0], d[3]-d[1] ] for d in dets]) confs np.array([d[4] for d in dets]) classes np.array([d[5] for d in dets]) outputs deepsort.update(bbox_xywh, confs, classes, frame) if len(outputs) 0: for out in outputs: x1, y1, x2, y2, track_id, cls out cx, cy int((x1 x2) / 2), int((y1 y2) / 2) # 更新轨迹 if track_id not in trajectories: trajectories[track_id] [] trajectories[track_id].append((cx, cy)) if len(trajectories[track_id]) MAX_TRAIL_LEN: trajectories[track_id].pop(0) # 画检测框和 ID 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) # 画所有轨迹 for tid, points in trajectories.items(): for i in range(1, len(points)): cv2.line(frame, points[i-1], points[i], (0, 0, 255), 2) cv2.imshow(DeepSORT-YOLOv5 Drone Tracking, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明deepsort.update()接收bbox_xywh中心点宽高、置信度、类别和当前帧图像返回[x1, y1, x2, y2, track_id, cls]格式的跟踪结果。轨迹字典按track_id存历史中心点每帧追加超过MAX_TRAIL_LEN就丢弃最旧的点防止轨迹线太长糊成一片。参数说明max_age30表示轨迹丢失后保留 30 帧无人机目标被遮挡或者飞出画面再回来30 帧内还能接上。n_init3表示连续 3 帧匹配上才确认轨迹能过滤掉一闪而过的误检。nn_budget50控制特征库大小无人机场景外观变化快50 够用太大反而拖慢匹配。提示deepsort.update()的frame参数必须传原始 BGR 图像DeepSORT 内部会裁剪检测框区域做 ReID 特征提取。如果传灰度图或者缩放后的图特征质量会下降。4. 运动轨迹可视化从散点连线到热力轨迹图4.1 轨迹平滑为什么原始中心点连线会抖YOLOv5 检测框每帧都在跳直接连中心点轨迹线像锯齿。我一般做两层平滑第一层用滑动平均窗口大小 5第二层用三次样条插值把轨迹点加密到每帧 2 个点线条更顺。from scipy.interpolate import make_interp_spline import numpy as np def smooth_trajectory(points, window5): 滑动平均 样条插值平滑轨迹 if len(points) 3: return points # 滑动平均 arr np.array(points, dtypefloat) kernel np.ones(window) / window x_smooth np.convolve(arr[:, 0], kernel, modevalid) y_smooth np.convolve(arr[:, 1], kernel, modevalid) # 样条插值加密 t np.arange(len(x_smooth)) t_new np.linspace(0, len(x_smooth)-1, len(x_smooth)*2) spl_x make_interp_spline(t, x_smooth, kmin(3, len(x_smooth)-1)) spl_y make_interp_spline(t, y_smooth, kmin(3, len(y_smooth)-1)) return list(zip(spl_x(t_new).astype(int), spl_y(t_new).astype(int)))逻辑说明np.convolve做滑动平均modevalid会丢掉前后各window//2个点轨迹会短一点但换来平滑。make_interp_spline做三次样条插值把点数量翻倍线条更细腻。k取min(3, len-1)防止点数太少时报错。参数说明window5是经验值无人机视频 30 FPS 下5 帧约 0.17 秒能滤掉高频抖动又不至于让轨迹滞后太多。如果目标运动很慢可以调到 7运动快就降到 3。4.2 轨迹颜色编码按时间渐变还是按 ID 固定可视化时轨迹颜色有两种方案按 ID 固定颜色每个目标一条固定颜色的线适合目标数量少的场景按时间渐变越新的轨迹点越亮适合展示运动方向。我一般用 ID 固定颜色 最新点加粗兼顾辨识度和方向感。import colorsys def get_color(track_id, total_ids20): 根据 track_id 生成固定颜色 hue (track_id * 0.618) % 1.0 # 黄金比例散列颜色区分度高 r, g, b colorsys.hsv_to_rgb(hue, 0.9, 0.9) return (int(b*255), int(g*255), int(r*255)) # BGR # 绘制时 for tid, points in trajectories.items(): color get_color(tid) for i in range(1, len(points)): thickness 3 if i len(points)-1 else 1 # 最新一段加粗 cv2.line(frame, points[i-1], points[i], color, thickness)逻辑说明hue (track_id * 0.618) % 1.0用黄金比例散列相邻 ID 的颜色差异大不会出现两个目标颜色相近分不清的情况。colorsys.hsv_to_rgb转 RGB 再转 BGR因为 OpenCV 用 BGR。最新一段轨迹加粗到 3 像素一眼能看出目标当前运动方向。参数说明total_ids不用传只是注释用。0.618是黄金比例换成其他无理数也行关键是避免相邻 ID 颜色接近。饱和度 0.9、亮度 0.9 保证颜色鲜艳但不刺眼。4.3 轨迹热力图统计目标经过频率如果要做区域分析比如无人机巡检时看哪个区域目标最密集可以把轨迹点叠加成热力图。用 OpenCV 的applyColorMap做伪彩色# 累积轨迹热力图 heatmap np.zeros((img_h, img_w), dtypenp.float32) for tid, points in trajectories.items(): for (cx, cy) in points: if 0 cx img_w and 0 cy img_h: heatmap[cy, cx] 1 # 高斯模糊让热力分布更自然 heatmap cv2.GaussianBlur(heatmap, (51, 51), 0) heatmap_norm cv2.normalize(heatmap, None, 0, 255, cv2.NORM_MINMAX) heatmap_color cv2.applyColorMap(heatmap_norm.astype(np.uint8), cv2.COLORMAP_JET) # 叠加到原图 overlay cv2.addWeighted(frame, 0.6, heatmap_color, 0.4, 0) cv2.imshow(Trajectory Heatmap, overlay)逻辑说明heatmap[cy, cx] 1在每个轨迹点位置累加计数GaussianBlur把离散点扩散成连续热区normalize把值域拉到 0-255applyColorMap映射成 JET 伪彩色。addWeighted把热力图和原图按 0.4:0.6 融合既能看到热区又不遮住画面细节。参数说明GaussianBlur的核大小 51 是经验值视频分辨率 1920×1080 下51 像素的扩散半径大约覆盖 2-3 个目标间距。分辨率小就降到 31大就加到 71。addWeighted的 alpha 控制热力图透明度0.4 是我常用的值再高就影响看原画面了。5. 避坑与排查无人机跟踪里最容易翻车的 5 个点5.1 检测框抖动导致 ID 频繁切换现象同一个目标跟踪 ID 每隔几帧就变一次轨迹断成好几段。原因YOLOv5 在无人机画面上的检测框中心点每帧抖动 5-15 像素DeepSORT 的卡尔曼滤波预测位置和观测位置偏差大马氏距离超过阈值匹配失败。解决调低_std_weight_position到 1/30让滤波器更相信预测同时把max_dist从 0.2 调到 0.3放宽外观匹配阈值。如果还不行在检测后加一个简单的滑动平均滤波把连续 3 帧的检测框中心点做平均再喂给 DeepSORT。5.2 小目标 ReID 特征提取失败现象远处的小目标像素面积小于 20×20跟踪 ID 乱跳或者干脆不显示轨迹。原因DeepSORT 的 ReID 网络输入是 128×64 的裁剪图小目标裁剪后放大特征模糊余弦距离区分度低。解决在deepsort.update()之前过滤掉面积小于阈值的检测框比如w*h 400的直接不参与跟踪只画检测框不画轨迹。或者换一个轻量级 ReID 模型输入尺寸改成 64×32对小目标更友好。5.3 轨迹线拖尾太长糊成一片现象画面里轨迹线太多太长目标密集时完全看不清。原因MAX_TRAIL_LEN设太大比如 200轨迹线覆盖半个画面。解决把MAX_TRAIL_LEN降到 30-60只保留最近 1-2 秒的轨迹。同时给轨迹线加透明度衰减越旧的点越透明用cv2.addWeighted逐段叠加实现。5.4 视频帧率不稳导致跟踪滞后现象跟踪框比目标实际位置慢半拍快速运动时尤其明显。原因YOLOv5 推理耗时波动帧率不稳DeepSORT 的dt参数固定为 1卡尔曼滤波的速度估计不准。解决在deepsort.update()之前记录实际帧间隔动态调整卡尔曼滤波的dt。或者把 YOLOv5 推理和 DeepSORT 更新放到不同线程用队列缓冲保证跟踪线程的帧率稳定。5.5 类别过滤后轨迹颜色冲突现象只跟踪人时两个不同 ID 的人轨迹颜色几乎一样分不清。原因get_color函数的黄金比例散列在 ID 数量少时区分度不够。解决把hue的散列步长从 0.618 改成 0.381另一个无理数或者在 ID 数量小于 10 时直接用预定义的 10 种高区分度颜色比如红、绿、蓝、黄、青、品红、橙、紫、棕、粉。6. 进阶技巧用轨迹预测补全遮挡段和导出可视化数据6.1 遮挡段轨迹补全卡尔曼预测外推无人机跟踪时目标被建筑物或者树木遮挡DeepSORT 的max_age内轨迹还在但检测框没了轨迹线会断。我一般用卡尔曼滤波的预测值补全这几帧# 在 deepsort.update() 返回空时用预测值补轨迹 if len(outputs) 0: for tid, tracker in deepsort.tracker.tracks.items(): if not tracker.is_confirmed(): continue # 卡尔曼预测下一帧位置 mean tracker.mean cx, cy int(mean[0]), int(mean[1]) if tid not in trajectories: trajectories[tid] [] trajectories[tid].append((cx, cy))逻辑说明tracker.mean是卡尔曼滤波的状态均值前两维就是中心点坐标。检测丢失时用预测值继续追加轨迹点等目标重新出现再切回观测值。这样轨迹线不会断但要注意预测值误差会累积max_age别设太大30 帧以内比较安全。参数说明tracker.is_confirmed()过滤掉未确认的轨迹避免误检产生的假轨迹被补全。mean[0]和mean[1]是x和y中心坐标mean[2]是宽高比mean[3]是高度别搞混。6.2 导出轨迹数据到 CSV 做离线分析跟踪跑完把轨迹数据导出成 CSV方便用 pandas 做统计分析或者喂给 ECharts 做可视化大屏import csv with open(trajectories.csv, w, newline) as f: writer csv.writer(f) writer.writerow([track_id, frame_idx, cx, cy, timestamp]) for tid, points in trajectories.items(): for i, (cx, cy) in enumerate(points): writer.writerow([tid, i, cx, cy, i/30.0]) # 假设 30 FPS逻辑说明每行记录track_id、帧序号、中心点坐标和时间戳。时间戳用帧序号除以帧率估算方便后续做速度、加速度分析。导出后可以用 pandas 算每个目标的平均速度、停留时间、运动方向分布。参数说明i/30.0里的 30 是视频帧率如果你的视频是 25 FPS 就改成 25。时间戳单位是秒保留浮点精度。6.3 验证跟踪效果MOTA 和 ID Switch 怎么算跟踪效果不能只看画面要量化。MOTAMultiple Object Tracking Accuracy和 ID Switch 是两个核心指标指标含义计算方式无人机场景目标值MOTA综合跟踪准确率1 - (FNFPIDS)/GT 0.6ID SwitchID 切换次数统计同一目标 ID 变化次数 总帧数的 5%MOTP跟踪位置精度平均距离误差 15 像素Frag轨迹碎片数轨迹中断次数 总轨迹数的 20%计算 MOTA 需要标注好的 GT 文件格式通常是frame_id, track_id, x, y, w, h。用py-motmetrics库可以算pip install motmetricsimport motmetrics as mm acc mm.MOTAccumulator(auto_idTrue) # 逐帧添加acc.update(gt_ids, pred_ids, distance_matrix) # distance_matrix 用 IoU 或者中心点欧氏距离逻辑说明acc.update()每帧调用一次传入 GT 的 ID 列表、预测的 ID 列表和距离矩阵。距离矩阵用 1-IoU 或者中心点欧氏距离小于阈值的算匹配。最后mm.metrics.create().compute(acc, metrics[mota, idsw, motp])输出指标。参数说明距离阈值一般设 0.5IoU或者 50 像素中心点距离。无人机场景目标小IoU 阈值可以降到 0.3中心点距离阈值降到 30 像素。6.4 我踩过的最大一个坑去年做无人机河道巡检跟踪水面船只。YOLOv5 检测没问题DeepSORT 跟踪也稳但轨迹画出来全是折线船明明在走直线轨迹却左右横跳。查了两天发现是视频帧时间戳不对——无人机录制的视频是可变帧率cap.read()返回的帧间隔不均匀卡尔曼滤波的dt固定为 1速度估计完全错了。后来改成用cap.get(cv2.CAP_PROP_POS_MSEC)读实际时间戳动态算dt轨迹立刻顺了。这个坑让我养成了一个习惯任何跟踪项目先检查视频帧率是否恒定。用ffprobe看一眼ffprobe -v error -select_streams v:0 -show_entries streamr_frame_rate,avg_frame_rate -of defaultnoprint_wrappers1 drone_video.mp4如果r_frame_rate和avg_frame_rate不一致就是可变帧率必须用时间戳而不是帧序号来算dt。这个检查花不了 10 秒能省两天调试。希望帮到你。本文还有配套的精品资源点击获取
返回列表