ARTICLE DETAIL

资讯详情

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

YOLOv5+DeepSORT+Flask实时多目标跟踪Web系统

YOLOv5+DeepSORT+Flask实时多目标跟踪Web系统 简介本资源是一个基于YOLOv5目标检测与DeepSORT多目标跟踪的Web端部署实战项目面向计算机视觉初学者与算法工程化实践者解决目标检测模型落地为可交互Web服务的核心问题。压缩包共206个文件包含56个Python源码含模型训练、推理、Flask后端及前端交互逻辑、21个YOLOv5相关配置yaml文件、96个编译后的pyc文件、3个MP4演示视频、3个JPG测试图像及Dockerfile、HTML页面等整体约140.63MB结构完整覆盖数据预处理、模型加载、跟踪逻辑封装与前后端联调全流程。已有168人学习下载提供开箱即用的Flask部署方案、DeepSORT参数调优注释、YOLOv5与跟踪器协同集成要点以及实际场景下的多目标ID持续性可视化示例便于读者快速掌握从算法到服务的端到端工程实现路径。1. 为什么用 YOLOv5 DeepSORT Flask 做实时多目标跟踪 Web 系统反而比纯前端方案更稳、更准、更易调你有没有试过在浏览器里直接跑 YOLO 模型TensorFlow.js 或 ONNX.js 虽然能加载权重但一帧推理动辄 300–800ms尤其在中低端 PC 或笔记本上加上 JS 里做 Kalman 滤波和匈牙利匹配的开销ID 切换频繁、轨迹抖动、漏检率飙升——这不是模型不行是前端算力和调度机制天然不适合做「检测跟踪」耦合任务。而本项目把 YOLOv5轻量级 s/m 模型和 DeepSORT带外观特征提取的 ReID 模块全放在后端 Python 进程里跑只把原始视频流或上传文件送进来后端完成推理→跟踪→轨迹聚合→JSON 结果返回前端只负责渲染canvas和video标签用requestAnimationFrame控制帧率不碰模型、不碰矩阵运算。实测RTX 3060 上单路 720p 视频可稳定 22–25 FPS树莓派 54GB部署yolov5s.ptosnet_x0_25_msmt17.pt也能跑通 8–10 FPS延迟 350ms。它不是“炫技型”Demo而是面向安防巡检、仓储盘点、实验室行为分析等真实场景的最小可行闭环模型可换、跟踪可调、接口可测、部署可缩容。适合已有 YOLO 训练经验、想快速把算法落地成 Web 工具的工程师也适合需要交作业/参赛但不想被前端框架绑架的算法同学——Flask 不是最终生产框架但它是最短路径验证「算法→服务→可视化」链路是否成立的那块试金石。2. 从零构建 YOLOv5 DeepSORT 后端服务模型加载、推理流水线与跟踪状态管理2.1 安装依赖与环境隔离为什么不用 conda而坚持 pip requirements.txtYOLOv5 官方 repoultralytics/yolov5 v6.0–v7.0对 PyTorch 版本敏感DeepSORT 的torchreid子模块又要求torch1.9.0,2.0.0而 Flask 2.3 已弃用Werkzeug2.3。若用 conda 创建环境常因 channel 源混杂导致torch和torchvisionABI 不匹配典型报错undefined symbol: _ZNK3c104Type14is_subclass_ofERKS_。我坚持用python -m venv yolov5-deepsort-env新建干净虚拟环境再按顺序执行pip install --upgrade pip pip install torch1.13.1cu117 torchvision0.14.1cu117 --extra-index-url https://download.pytorch.org/whl/cu117 pip install -r requirements.txt其中requirements.txt内容严格锁定版本关键项Flask2.2.5 numpy1.23.5 opencv-python4.8.1.78 pyyaml6.0.1 requests2.31.0 tqdm4.66.1 ultralytics8.0.194 # 注意不是官方 yolov5是 ultralytics 官方维护的统一接口兼容 yolov5/v8/v10 torchreid1.5.0 # DeepSORT 所需的 ReID backboneosnet, resnet提示ultralytics8.0.194是当前2024Q2最稳定的版本它把yolov5s.pt加载逻辑封装为YOLO(yolov5s.pt)自动处理模型输入归一化、NMS 阈值、输出格式统一为Results对象省去手动写torch.no_grad()和non_max_suppression的麻烦。别用git clone官方 yolov5 仓库——它的detect.py是脚本式设计难以嵌入 Web 服务生命周期。2.2 构建可复用的 Detector Tracker 流水线避免每次请求都 reload 模型Web 服务最怕「每来一个 POST 请求就torch.load()一次模型」——yolov5s.pt14MB加载耗时 1.2–1.8sosnet_x0_25_msmt17.pt12MB再加 0.9s用户点一次「开始跟踪」要等 3 秒体验崩坏。正确做法是服务启动时一次性加载模型到内存后续所有请求复用同一实例。核心类YOLODeepSORTPipeline设计如下tracker/pipeline.pyfrom ultralytics import YOLO from torchreid import models import torch import numpy as np class YOLODeepSORTPipeline: def __init__(self, det_model_pathyolov5s.pt, reid_model_pathosnet_x0_25_msmt17.pt): # 1. 加载检测器GPU 加速 self.detector YOLO(det_model_path) self.detector.to(cuda if torch.cuda.is_available() else cpu) # 2. 加载 ReID 模型必须用 torchreid.models.build_model self.reid_model models.build_model( nameosnet_x0_25, num_classes1000, losssoftmax, pretrainedFalse ) state_dict torch.load(reid_model_path, map_locationcpu) self.reid_model.load_state_dict(state_dict) self.reid_model.to(cuda if torch.cuda.is_available() else cpu) self.reid_model.eval() # 3. 初始化 DeepSORT 跟踪器关键参数说明见 2.3 节 from tracker.deep_sort import DeepSort self.tracker DeepSort( model_pathreid_model_path, max_dist0.2, # 外观特征余弦距离阈值0.2 则认为非同一人 min_confidence0.3, # YOLO 检测框置信度下限低于此不送入跟踪器 nms_max_overlap0.5, # 检测框 NMS IOU 阈值抑制重叠框 max_iou_distance0.7, # 匈牙利匹配时的 IOU 阈值位置相似性 max_age30, # ID 最长消失帧数30 帧 ≈ 1 秒 30FPS n_init3, # 新 ID 需连续 3 帧确认才激活 nn_budget100 # 特征库最大缓存数防内存爆炸 ) def process_frame(self, frame: np.ndarray) - list: 输入BGR 格式 OpenCV 图像HWC 输出[{track_id: int, bbox: [x1,y1,x2,y2], label: str, conf: float}, ...] # Step 1: YOLO 检测自动归一化、NMS、返回 xyxy 格式 results self.detector(frame, verboseFalse) detections [] for r in results: boxes r.boxes.xyxy.cpu().numpy() # shape: (N, 4) confs r.boxes.conf.cpu().numpy() # shape: (N,) classes r.boxes.cls.cpu().numpy() # shape: (N,) for i in range(len(boxes)): if confs[i] self.tracker.min_confidence: x1, y1, x2, y2 boxes[i] cls_id int(classes[i]) label self.detector.names[cls_id] # 如 person, car detections.append([x1, y1, x2, y2, confs[i], cls_id]) # Step 2: DeepSORT 跟踪输入 detections输出 tracked_objects if len(detections) 0: return [] # 转为 DeepSORT 所需格式[x1,y1,x2,y2,conf,cls_id] detections np.array(detections) tracks self.tracker.update(detections, frame) # frame 用于 crop 提取 ReID 特征 # Step 3: 格式化输出适配前端 JSON 序列化 output [] for track in tracks: bbox track.to_tlbr() # [top, left, bottom, right] → 转为 [x1,y1,x2,y2] output.append({ track_id: int(track.track_id), bbox: [float(bbox[0]), float(bbox[1]), float(bbox[2]), float(bbox[3])], label: self.detector.names[int(track.class_id)] if hasattr(track, class_id) else unknown, conf: float(track.conf) if hasattr(track, conf) else 1.0 }) return output这段代码的关键在于self.detector.to(cuda)和self.reid_model.to(cuda)必须显式指定设备否则torchreid默认 CPU 推理速度慢 5 倍self.tracker.update(...)的第二个参数frame是必需的——DeepSORT 内部会根据bbox截取图像 patch送入reid_model提取 512 维特征向量没有原始 frame 就无法做外观匹配track.to_tlbr()返回的是[top,left,bottom,right]即y1,x1,y2,x2需手动转为前端习惯的[x1,y1,x2,y2]否则 canvas 绘制错位。2.3 DeepSORT 的 4 个必调参数为什么默认值在监控场景下必然翻车DeepSORT 不是开箱即用的黑盒。YOLOv5 检测框抖动、光照变化、遮挡频繁时原版参数会让 ID 切换率高达 40%。以下是我在 3 类典型场景室内走廊、停车场出入口、实验室操作台中反复验证的参数组合参数名默认值推荐值作用说明调参依据max_dist0.20.18–0.22ReID 特征余弦距离阈值0.18更严苛减少跨 ID 匹配0.22更宽松适应光照突变。实测0.2在阴天走廊误匹配率 12%调至0.19降至 4.3%max_iou_distance0.70.4–0.55匈牙利匹配时 IOU 阈值监控视频运动缓慢0.7导致新旧 ID 过早合并。停车场车辆并行时0.5可将 ID 切换率从 31% 降到 9%max_age3020–25ID 最长消失帧数30帧1s太长遮挡后易错误复活旧 ID。实验室手部操作遮挡频繁设22帧≈0.73s最佳n_init32–3新 ID 连续确认帧数2加速 ID 创建但增加误检风险3更稳。建议固定3靠min_confidence控制误检血泪经验不要迷信论文里的默认值。max_iou_distance0.7是针对 MOT16 数据集高帧率、小目标、少遮挡调优的而你的监控视频往往是 25FPS、大目标、强遮挡——必须降档使用。我曾因没调max_iou_distance导致同一辆车在路口转弯时被拆成 3 个 ID客户当场质疑算法可靠性。3. Flask Web 服务搭建路由设计、视频流处理与前后端数据协议3.1 单线程 vs 多线程为什么 Flask 默认开发服务器不能跑实时视频Flask 自带的app.run()是单线程阻塞式服务器同一时间只能处理一个请求。当你上传一个 60 秒 MP4 文件后端要花 40 秒推理跟踪此时第二个用户刷新页面请求会被挂起直到第一个完成——用户体验极差。必须启用多线程或异步模式。正确启动方式app.pyfrom flask import Flask, request, jsonify, render_template, Response from tracker.pipeline import YOLODeepSORTPipeline import threading import queue import time app Flask(__name__) # 全局单例 pipeline服务启动时加载 pipeline YOLODeepSORTPipeline( det_model_pathweights/yolov5s.pt, reid_model_pathweights/osnet_x0_25_msmt17.pt ) # 用线程安全队列暂存结果避免全局变量竞争 result_queue queue.Queue(maxsize100) app.route(/) def index(): return render_template(index.html) app.route(/upload, methods[POST]) def upload_video(): if video not in request.files: return jsonify({error: No video file}), 400 video_file request.files[video] if video_file.filename : return jsonify({error: Empty filename}), 400 # 保存临时文件生产环境应改用 Redis 或对象存储 temp_path ftemp/{int(time.time())}_{video_file.filename} video_file.save(temp_path) # 启动后台线程处理非阻塞 def process_and_enqueue(): cap cv2.VideoCapture(temp_path) frame_count 0 while cap.isOpened(): ret, frame cap.read() if not ret: break # 每 2 帧处理一次降负载保实时性 if frame_count % 2 0: try: results pipeline.process_frame(frame) result_queue.put({ frame_id: frame_count, tracks: results, timestamp: time.time() }) except Exception as e: print(fFrame {frame_count} error: {e}) frame_count 1 cap.release() # 标记结束 result_queue.put({end: True}) threading.Thread(targetprocess_and_enqueue, daemonTrue).start() return jsonify({status: processing, video_id: temp_path.split(/)[-1]}) app.route(/stream) def stream(): def generate(): while True: try: # 非阻塞获取超时 1s 防卡死 item result_queue.get(timeout1.0) if end in item: yield fdata: {json.dumps({end: True})}\n\n break yield fdata: {json.dumps(item)}\n\n result_queue.task_done() except queue.Empty: yield fdata: {json.dumps({ping: time.time()})}\n\n return Response(generate(), mimetypetext/event-stream)关键点解析threading.Thread(target..., daemonTrue)后台线程主进程退出时自动销毁避免僵尸进程result_queue作为线程间通信桥梁容量限制maxsize100防内存溢出/stream路由采用SSEServer-Sent Events协议而非 WebSocket——SSE 更轻量、兼容性更好IE11 支持且天然支持文本流适合推送 JSON 轨迹数据frame_count % 2 0是主动降帧策略YOLOv5s 在 1080p 下单帧推理约 45ms25FPS 视频若全帧处理需 1125ms/s超 GPU 负载跳帧后实际处理 12.5FPS延迟可控且 ID 连续性无损。3.2 前端 Canvas 渲染如何用 requestAnimationFrame 同步后端 SSE 流前端index.html中核心渲染逻辑如下static/js/app.jslet canvas, ctx, video; let currentFrameId -1; let tracksCache {}; // {frame_id: [...tracks]} function initCanvas() { canvas document.getElementById(detectionCanvas); ctx canvas.getContext(2d); video document.getElementById(videoInput); // 动态设置 canvas 尺寸匹配视频 video.addEventListener(loadedmetadata, () { canvas.width video.videoWidth; canvas.height video.videoHeight; }); } async function startStreaming() { const eventSource new EventSource(/stream); eventSource.onmessage (event) { const data JSON.parse(event.data); if (data.end) { console.log(Tracking finished); eventSource.close(); return; } if (data.frame_id ! undefined) { tracksCache[data.frame_id] data.tracks; // 触发下一帧渲染requestAnimationFrame 保证 60FPS 渲染节律 if (currentFrameId data.frame_id) { currentFrameId data.frame_id; renderFrame(); } } }; eventSource.onerror (err) { console.error(SSE error:, err); }; } function renderFrame() { if (!video.paused video.readyState video.HAVE_ENOUGH_DATA) { // 清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制视频帧注意drawImage 会拉伸需保持宽高比 ctx.drawImage(video, 0, 0, canvas.width, canvas.height); // 绘制轨迹框仅渲染当前帧 tracks const tracks tracksCache[currentFrameId] || []; tracks.forEach(track { const [x1, y1, x2, y2] track.bbox; ctx.strokeStyle hsl(${track.track_id * 43 % 360}, 100%, 50%); // 彩虹色 ID ctx.lineWidth 2; ctx.strokeRect(x1, y1, x2 - x1, y2 - y1); // 绘制 ID 标签 ctx.fillStyle white; ctx.font 14px sans-serif; ctx.fillText(ID:${track.track_id} ${track.label}, x1 5, y1 - 10); }); } requestAnimationFrame(renderFrame); // 持续循环 } // 页面加载完成后初始化 document.addEventListener(DOMContentLoaded, () { initCanvas(); startStreaming(); });玄学细节ctx.drawImage(video, 0, 0, canvas.width, canvas.height)会强制拉伸视频填充 canvas但 YOLO 输出的bbox坐标是基于原始分辨率的。解决方案是让 canvas 尺寸始终等于 video 的videoWidth/videoHeight通过loadedmetadata事件监听这样坐标无需缩放直接绘制即可。若强行用 CSS 缩放video标签bbox必须按比例换算极易出错。4. Docker 部署实战Dockerfile 编写、GPU 支持与体积优化4.1 最小可行 Dockerfile为什么 base 镜像选nvidia/cuda:11.7.1-devel-ubuntu20.04很多教程用python:3.9-slim但slim镜像不含nvidia-smi、libcuda.so等 GPU 运行时库即使安装nvidia-container-toolkit也无法调用 GPU。必须用 NVIDIA 官方 CUDA base 镜像# 使用 NVIDIA 官方 CUDA 开发镜像预装 cudnn、cuda-toolkit FROM nvidia/cuda:11.7.1-devel-ubuntu20.04 # 设置环境 ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone ENV DEBIAN_FRONTENDnoninteractive # 安装系统依赖OpenCV 需要 RUN apt-get update apt-get install -y \ libglib2.0-0 \ libsm6 \ libxext6 \ libxrender-dev \ libglib2.0-dev \ rm -rf /var/lib/apt/lists/* # 创建工作目录 WORKDIR /app COPY requirements.txt . # 分层安装先装 torch耗时最长再装其他 RUN pip install --no-cache-dir torch1.13.1cu117 torchvision0.14.1cu117 --extra-index-url https://download.pytorch.org/whl/cu117 RUN pip install --no-cache-dir -r requirements.txt # 复制代码 COPY . . # 暴露端口 EXPOSE 5000 # 启动命令--host0.0.0.0 绑定所有网卡--port5000 指定端口 CMD [gunicorn, --bind, 0.0.0.0:5000, --workers, 2, --threads, 4, --timeout, 120, app:app]关键点说明nvidia/cuda:11.7.1-devel-ubuntu20.04是 PyTorch 1.13.1 的官方推荐 baseABI 兼容性最好gunicorn替代flask run支持多 worker--workers 2、多线程--threads 4吞吐量提升 3 倍以上--timeout 120防止大视频处理超时被 killlibsm6、libxext6是 OpenCV GUI 模块依赖缺失会导致cv2.VideoCapture初始化失败报错libSM.so.6: cannot open shared object file。4.2 GPU 容器运行命令nvidia-docker 与 docker run 的区别在哪docker run默认无法访问 GPU必须显式添加--gpus all参数# 构建镜像 docker build -t yolov5-deepsort-web . # 运行容器关键--gpus all --shm-size2g docker run -d \ --gpus all \ --shm-size2g \ -p 5000:5000 \ -v $(pwd)/weights:/app/weights \ -v $(pwd)/temp:/app/temp \ --name yolov5-web \ yolov5-deepsort-web--gpus all挂载所有 GPU 设备/dev/nvidia0,/dev/nvidiactl等--shm-size2g必须设置YOLO 推理时 OpenCV 的cv2.dnn模块使用共享内存加速默认64MB不够会导致cv2.VideoCapture读帧卡死或报Failed to allocate memory-v $(pwd)/weights:/app/weights将本地weights/目录挂载进容器避免重新打包镜像。避坑不要用nvidia-docker命令已废弃docker run --gpus是 Docker 19.03 的标准语法。若报错docker: Error response from daemon: could not select device driver nvidia, 请检查nvidia-container-toolkit是否安装并配置/etc/docker/daemon.json。5. 避坑指南YOLOv5 DeepSORT Flask 部署的 5 个高频翻车点5.1 现象Flask 启动后报错ImportError: libcudnn.so.8: cannot open shared object file原因CUDA base 镜像中libcudnn.so.8路径未加入LD_LIBRARY_PATH或 PyTorch 安装的 cudnn 版本与镜像不匹配。解决在 Dockerfile 中显式导出路径并验证 cudnn 版本# 在 RUN pip install ... 后添加 RUN echo /usr/lib/x86_64-linux-gnu /etc/ld.so.conf.d/cuda.conf ldconfig RUN python -c import torch; print(torch.backends.cudnn.version()) # 应输出 8.5.05.2 现象前端 canvas 显示黑屏但控制台无报错原因video标签未设置autoplay和muted现代浏览器禁止自动播放有声音的视频。解决HTML 中video标签必须加autoplay muted loopvideo idvideoInput autoplay muted loop styledisplay:none;/video同时 JS 中用video.src URL.createObjectURL(file)加载文件后需手动调用video.play()。5.3 现象DeepSORT 跟踪 ID 频繁闪烁同一目标 ID 在 1/2/3 间跳变原因max_iou_distance过高如 0.7max_dist过低如 0.15导致匈牙利匹配优先选 IOU 高但外观差异大的框ReID 特征被忽略。解决按 2.3 节表格将max_iou_distance降至 0.45–0.5max_dist设为 0.2二者权重均衡。5.4 现象上传 MP4 后/stream返回 404或 SSE 连接立即关闭原因Flask 路由/stream未正确返回Response对象或generate()函数未yield字符串如忘了\n\n。解决严格按 3.1 节代码确保yield fdata: ...\n\n且mimetypetext/event-stream检查app.py是否from flask import Response。5.5 现象Docker 容器内cv2.VideoCapture无法打开本地 MP4 文件报Unable to load from file原因OpenCV 缺少 FFmpeg 后端支持Ubuntu base 镜像默认不装ffmpeg。解决在 Dockerfile 中apt-get install补充RUN apt-get install -y ffmpeg libavcodec-dev libavformat-dev libswscale-dev libv4l-dev rm -rf /var/lib/apt/lists/*并验证cv2.getBuildInformation()中FFMPEG: YES。6. 进阶技巧如何用 Redis 缓存轨迹、支持多路并发与前端性能压测6.1 用 Redis 替代内存队列解决高并发下的 result_queue 溢出问题当 10 个用户同时上传视频queue.Queue容量maxsize100会迅速填满新结果被丢弃。改用 Redis List 作为消息队列天然支持多消费者、持久化、长度限制import redis r redis.Redis(hostlocalhost, port6379, db0, decode_responsesTrue) app.route(/upload, methods[POST]) def upload_video(): # ... 保存文件逻辑 ... # 生成唯一 job_id job_id fjob_{int(time.time())}_{uuid.uuid4().hex[:6]} # 将 job_id 推入 Redis 队列 r.lpush(tracking_jobs, job_id) r.hset(fjob:{job_id}, mapping{ status: queued, video_path: temp_path, created_at: time.time() }) # 启动 Celery 任务或独立进程处理 job_id # 此处省略 Celery 配置可用 subprocess.Popen 调用独立脚本 subprocess.Popen([python, worker.py, job_id]) return jsonify({job_id: job_id}) app.route(/result/job_id) def get_result(job_id): # 从 Redis 获取该 job 的最新轨迹每秒轮询 tracks r.lrange(fjob:{job_id}:tracks, 0, -1) return jsonify({tracks: [json.loads(t) for t in tracks]})好处Redis List 支持LLEN查长度、LTRIM限长、BLPOP阻塞读比 Pythonqueue更健壮且可轻松扩展为多 worker 消费支撑百路并发。6.2 前端性能压测用 Chrome DevTools 模拟 3G 网络与低端设备真实用户可能用 4G 网络看 1080p 视频前端渲染压力巨大。在 Chrome DevTools → Network → Throttling 中选择Fast 3G再开启 Rendering → FPS Meter观察若 FPS 30说明requestAnimationFrame渲染跟不上需降低canvas分辨率如canvas.width video.videoWidth * 0.5若内存占用持续上升Memory tab说明tracksCache未清理旧帧需加定时清理// 每 5 秒清理超过 100 帧的缓存 setInterval(() { const keys Object.keys(tracksCache); if (keys.length 100) { const oldest Math.min(...keys.map(Number)); delete tracksCache[oldest]; } }, 5000);6.3 模型热切换不重启服务更换 YOLO 权重文件生产环境常需 A/B 测试不同模型如yolov5s.ptvsyolov5m.pt。修改pipeline.py支持运行时 reloadclass YOLODeepSORTPipeline: def __init__(self, ...): # ... 初始化代码 ... self._det_model_path det_model_path # 缓存路径 def reload_detector(self, new_path: str): 热加载新检测模型 self.detector YOLO(new_path) self.detector.to(self.device) self._det_model_path new_path print(fDetector reloaded from {new_path}) property def device(self): return next(self.detector.model.parameters()).device然后新增 Flask 路由app.route(/reload-detector, methods[POST]) def reload_detector(): data request.get_json() new_path data.get(path) if not new_path or not os.path.exists(new_path): return jsonify({error: Invalid path}), 400 pipeline.reload_detector(new_path) return jsonify({status: success, new_model: new_path})我的习惯上线前必做三件事——用cv2.VideoCapture本地跑通单帧推理用curl -X POST测试/upload接口返回用 Chrome Network Tab 看/stream是否持续收到data:块。这三步过了90% 的部署问题已排除。剩下 10% 是客户现场的网络波动和摄像头型号兼容性那得靠日志和cv2.CAP_PROP_FOURCC调试。希望帮到你。本文还有配套的精品资源点击获取
返回列表