
简介一套完整的YOLOv5目标检测、DeepSORT多目标跟踪与Flask Web部署实战项目面向计算机视觉学习者和开发者可用于快速掌握检测跟踪原理并搭建可交互的浏览器端应用。项目共含206个文件压缩包大小约为140.63MB内部以Python源码、YAML配置、模型权重及pyc编译文件为主并附带Shell运行脚本、测试视频与图片、Dockerfile和说明文档便于环境还原、结果验证与二次开发。目前已有168人学习下载。内容涉及NMS非极大值抑制、IoU重叠度计算、mAP精度评估等关键概念从Two stage与One stage算法对比到DeepSORT跟踪链路均有体现可帮助理解目标检测与跟踪的算法原理及工程落地流程。对于刚开始接触这套技术栈的读者可直接阅读源码与配置结合测试样本运行再逐步适配到自定义检测场景从而省去从零搭建环境与调试模型的时间。1. 用yolov5deepsortflask做web端实时检测跟踪项目先弄清楚这个.zip里发生了什么拿到一个名为“使用yolov5作为目标检测器deepsort作为跟踪器使用flask部署到web前端”的项目压缩包里面通常是一套经典的实时多目标检测跟踪演示yolov5负责在每一帧图像里找出目标并给出边界框deepsort在这些框的基础上为每个目标分配持续不变的IDflask作为后端把处理后的画面推送到浏览器web前端只负责显示画面和下发控制指令。这套组合在行人计数、车流统计、周界报警类的课程项目和毕设里非常常见实用价值在于把模型能力、跟踪逻辑和Web交互拆成了三层每一层都能单独替换。下面按“原理→参数→对接→验证”的顺序讲训练和标注部分只做必要说明重点放在三个组件怎么协作、参数怎么联动以及把detect代码改成flask可调用时最容易踩的几个坑。2. 从detect.py到deepsort拆解yolov5检测与deepsort跟踪的协作流程先理清“检测”和“跟踪”的边界。yolov5是单帧检测器它对当前这一帧图像独立推理输出每个目标的类别、置信度和边界框它不管该目标上一帧是否出现过。deepsort是跨帧关联器它接收每一帧的检测结果用外观特征和运动模型判断当前帧的框与已有轨迹是否属于同一个目标从而维持一套稳定的ID序列。一句话概括yolov5负责“看见”deepsort负责“记住”。跟踪器不会修正检测框的质量如果yolov5漏检一帧deepsort只能靠运动估计硬撑撑不过去ID就会断。这就是为什么调优时永远先调检测器再调跟踪器。2.1 yolo网络结构输出什么deepsort需要什么yolov5网络结构分为backbone、neck和head三部分。backbone用CSPDarknet提取图像特征neck用PANet做多尺度融合head在三个不同尺度的特征图上输出预测框。以COCO 80类的模型为例推理结果经过NMS后每帧拿到的是形状为(N, 6)的numpy数组6列分别是x1、y1、x2、y2、confidence、class_idN是该帧最终保留的目标数量。deepsort的update方法不关心class_id它的输入是形如[x1, y1, x2, y2, score]的检测列表输出跟踪结果列表每个结果包含跟踪框坐标和track_id。因此从yolov5到deepsort之间必须做一次格式变换把torch.Tensor转成numpy数组去掉class_id列只留坐标和置信度。另一个容易忽略的细节是颜色通道opencv读入的BGR图像在传给yolov5前要转成RGB否则检测置信度会明显下降跟踪特征也会跟着受影响。2.2 克隆yolov5官方仓库并用官方权重跑通检测提示下面命令面向Linux或macOSWindows下建议使用Anaconda Prompt路径分隔符也要相应调整。常见做法是先克隆ultralytics维护的yolov5仓库再单独准备一份deepsort实现。先抛开跟踪器用官方权重验证基础环境。git clone https://github.com/ultralytics/yolov5 cd yolov5 python -m venv venv source venv/bin/activate pip install -r requirements.txt python detect.py --weights yolov5s.pt --source data/images/zidane.jpg --conf-thres 0.25这里--conf-thres 0.25是置信度阈值低于0.25的检测结果被丢弃。跑通这一步验证的是yolov5环境配置本身是否完整torch版本、CUDA驱动、opencv依赖是否都就绪。如果机器没有GPUdetect.py会自动切到CPU推理但帧率会很低后续用flask推流时必须考虑这个瓶颈否则前端画面看起来会像幻灯片。2.3 把deepsort接到检测结果上最小接入代码在yolov5的detect.py里找到推理循环在每帧拿到预测结果pred后直接把检测框喂给tracker。下面是核心接入代码。from deep_sort.deep_sort.tracker import Tracker from deep_sort.deep_sort import nn_matching metric nn_matching.NearestNeighborDistanceMetric( cosine, max_cosine_distance0.2, nn_budget100 ) tracker Tracker(metric) def yolo_to_deepsort(pred): boxes pred[:, :4].cpu().numpy() scores pred[:, 4].cpu().numpy() return boxes, scores boxes, scores yolo_to_deepsort(pred) # features 由ReID模型对每个检测框裁剪区域提取得到 detections [Detection(box, score, feat) for box, score, feat in zip(boxes, scores, features)] tracker.predict() tracker.update(detections)这段代码的逻辑是先定义距离度量这里用余弦距离然后创建Tracker实例。每个Detection由检测框、置信度和外观特征组成外观特征来自一个独立的ReID模型对每个检测框裁剪出的区域提取128维向量。tracker.predict()用卡尔曼滤波预测每条轨迹在当前帧的位置tracker.update()用匈牙利算法把检测框和预测位置做最优匹配匹配成功的轨迹ID保持不变失配超过max_age的轨迹会被删除。注意features的提取在GPU上也会有额外开销实际部署时这部分耗时经常被误算成yolov5的推理时间。2.4 实际项目里检测跟踪代码通常放在哪里在打包好的zip里目录结构一般长这样。路径作用yolov5/官方仓库可能改过detect.py或新增了track.pydeep_sort/deepsort源码和ReID模型app.pyflask入口templates/index.htmlweb前端页面static/前端js与css文件detect.py是主力脚本里面有加载权重、循环读取视频、逐帧推理、结果可视化这四段逻辑。改造为flask项目时要拆掉两处视频读取改成opencv的VideoCapture直接管理detect循环改成“读一帧→推理→跟踪→返回jpg”的独立函数。改造后的代码不再靠命令行参数控制输入源而是由flask路由决定读取摄像头、本地视频文件还是接收前端上传的视频流。3. 参数怎么设yolov5推理超参数与deepsort跟踪参数对照调整很多人能跑通demo但一上web端就开始乱跳框、ID乱换大部分是参数没有跟着场景改。这里把yolov5推理参数和deepsort跟踪参数放在一起说因为它们互相牵制检测器吐出的框如果又碎又抖跟踪器再怎么调也稳不了。3.1 yolov5三个必调推理参数真正影响部署效果的是这三个imgsz、conf-thres、iou-thres。参数默认值含义与调整方向imgsz640输入分辨率。提升到1280能改善小目标召回但推理耗时会变成原来的4倍conf-thres0.25低于该置信度的框被丢弃。web展示时建议提到0.4以上iou-thres0.45NMS的IoU阈值。值越大重叠目标越容易被合并成一个框当检测结果送给deepsort时conf-thres过低会带来大量虚假检测框deepsort会为这些假目标创建轨迹导致ID频繁切换。所以web部署时建议把基准conf-thres提到0.4再把控制权暴露给前端滑杆让使用者在0.3到0.7之间调节。iou-thres在小目标密集场景要适当降低比如0.3否则两个贴得很近的人会被并成一个框跟丢是必然的。3.2 deepsort参数表与含义deepsort需要调的参数集中在距离度量定义和Tracker内部参数两处。参数常见取值作用max_cosine_distance0.2外观特征余弦距离超过该值的检测与轨迹不匹配nn_budget100每条轨迹保留的样本数越小越省内存但遮挡恢复能力下降max_age30轨迹失配后仍保留的帧数目标被完全遮挡时靠它续命n_init3检测框连续出现多少帧才确认生成正式轨迹过低会让闪现误检变成轨迹max_iou_distance0.7运动匹配时IoU低于该阈值视为不可能匹配这组参数里max_age对体验影响最大。摄像头画面里一个人被柱子挡住5秒如果max_age太小ID会立刻丢失人出来后又领一个新ID统计人数直接翻倍。max_age也不是越大越好过大会让已经离开画面的轨迹继续占用计算资源还可能抢走新目标的ID。人群密集且镜头固定的场合max_age取10到20就够了目标少、遮挡多的场景取30到60更合适。3.3 用tracker_config.json把参数暴露给前端把上面两组参数集中到一个配置文件里每次调参不用改代码前端通过接口更新配置服务端热加载这是web项目比纯命令行灵活的地方。{ detector: { imgsz: 640, conf_thres: 0.45, iou_thres: 0.45 }, tracker: { max_cosine_distance: 0.2, nn_budget: 100, max_age: 30, n_init: 3, max_iou_distance: 0.7 } }flask启动时加载这个文件提供一个/api/config的POST接口接收新的JSON把值更新到全局变量。要注意的是imgsz不能随意热更新它决定了模型输入张量的shape改变它需要重新做一次前向流程稳妥的做法是imgsz只在启动时读取conf和iou这些值运行时可以改。前端拿到这些配置项后渲染成滑杆和输入框提交时以JSON格式打给接口整套链路就通了。3.4 常见误区训练超参数和推理参数不能混调网上常说的yolov5超参数大多指训练阶段hyp.scratch.yaml里的学习率、动量、数据增强倍数这些参数在训练启动时已经固化在权重里。部署时改conf-thres、iou-thres是推理策略两者作用阶段完全不同。有人把训练时给anchor分配用的anchor_t想当然地搬到web界面里调结果模型推理完全不受影响因为anchor是训练阶段的事推理模型已经固定。定位参数调不动时先确认改的是推理代码里的detect参数还是训练配置方向错了只会白费时间。4. flask后端与web前端让浏览器看到跟踪画面的可落地方案模型和跟踪器准备好之后剩下的问题是如何把它变成浏览器里能打开的网页。这里的关键是flask后端不能只在请求来时才加载模型模型必须在服务启动时就驻留内存否则每个视频帧请求都加载一次权重系统直接卡死。下面从后端视频流接口开始逐层写到前端页面。4.1 写一个flask应用服务器把检测器放进进程里app.py的核心是把之前的检测跟踪逻辑收进一个类对外提供处理单帧的函数。import cv2 import torch from flask import Flask, Response, render_template, request, jsonify from deep_sort.deep_sort.tracker import Tracker from deep_sort.deep_sort import nn_matching class VideoProcessor: def __init__(self, weights, config): self.model torch.hub.load(ultralytics/yolov5, custom, pathweights) self.model.conf config[detector][conf_thres] self.model.iou config[detector][iou_thres] metric nn_matching.NearestNeighborDistanceMetric( cosine, config[tracker][max_cosine_distance], config[tracker][nn_budget] ) self.tracker Tracker(metric) self.config config def process_frame(self, frame): results self.model(frame) pred results.xyxy[0].cpu().numpy() # pred列: x1, y1, x2, y2, conf, class boxes pred[:, :4] scores pred[:, 4] # 这里省略ReID特征提取与tracker.predict/update # 返回带框标注的frame return frame app Flask(__name__) processor VideoProcessor(yolov5s.pt, tracker_config.json) app.route(/) def index(): return render_template(index.html) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)读取摄像头或者视频文件时把cap cv2.VideoCapture(0)提到全局不要每个路由都新建。VideoProcessor在flask启动前实例化模型只加载一次。这里模型用torch.hub加载路径指向本地权重文件避免每次请求拉权重。debug必须设为Falsedebug模式下werkzeug会启动reloader进程被fork成两份两份都会加载模型显存直接翻倍。4.2 视频流推送用multipart/x-mixed-replace把逐帧画面推到前端浏览器端最省事的做法是用img标签的src指向一个视频流路由后端持续吐出jpg帧浏览器自动持续刷新画面这就是multipart/x-mixed-replace协议。app.route(/video_feed) def video_feed(): def generate(): cap cv2.VideoCapture(0) while True: ok, frame cap.read() if not ok: break frame processor.process_frame(frame) ret, jpeg cv2.imencode(.jpg, frame) yield (b--frame\r\n bContent-Type: image/jpeg\r\n\r\n jpeg.tobytes() b\r\n) return Response(generate(), mimetypemultipart/x-mixed-replace; boundaryframe)这个路由无限循环读帧、推理、编码再把jpg字节流按frame边界分割传下去。前端img标签一直接收画面就是连续视频。这里的坑在于generate()是生成器请求断开时循环不会自动停止要加一个客户端断开检测。常见做法是用request.environ里的wsgi相关属性判断连接状态或者在生成器外包裹try/except客户端断开时主动释放摄像头资源否则本机摄像头会被持续占用进程也退不掉。4.3 前端页面用img标签拉流用fetch驱动yolov5参数调整前端的核心非常短一个img标签负责视频一个滑杆负责调参。在vscode里做web前端开发时想查看web界面代码构成的界面按F12打开Elements面板找到img src/video_feed这个节点就能确认视频流是否通调参逻辑则看Network面板里有没有每分钟的/api/config请求。!DOCTYPE html html langzh head meta charsetUTF-8 titleyolov5deepsort 实时跟踪/title /head body div img src/video_feed width960 height540 altlive /div div classcontrols label置信度阈值 input typerange idconf min0.1 max0.9 step0.05 value0.45/label span idconf_value0.45/span /div div button idsave_frame保存当前检测帧/button /div script const confSlider document.getElementById(conf); const saveBtn document.getElementById(save_frame); confSlider.addEventListener(input, async () { const conf parseFloat(confSlider.value); document.getElementById(conf_value).textContent conf; await fetch(/api/config, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({detector: {conf_thres: conf}}) }); }); saveBtn.addEventListener(click, async () { await fetch(/api/save_frame, {method: POST}); }); /script /body /html滑杆松开后把conf_thres通过fetch打给flask的/api/config接口后端只需更新processor.model.conf这一行就能让下一帧生效。注意不要在input事件里每毫秒都发请求滑杆拖动时请求会塞满网络队列节流到mouseup或者改用change事件都要好得多。保存检测帧的接口是给数据采集用的点击后后端从上一帧的RGB数据里取当前帧用cv2.imwrite存成jpg文件名带时间戳。这套做法对应了“通过界面操作yolov5完成数据集的自动标注”这样一个很典型的真实需求先用检测模型产出带框图片再导入labelImg人工精修比纯手工框标注快不少。4.4 视频流多路复用与全局锁页面开着两个标签页img标签会向/video_feed发起两个请求generate()被调用两次摄像头被打开两次。常见解决方案是在VideoProcessor里维护一个全局锁process_frame前后加threading.Lock让两个请求串行读帧。同时把cap挪到Processor内部用一个_ensure_camera方法保证摄像头只初始化一次两个生成器共用同一帧缓冲。这样虽然会在画面上多出几十毫秒延迟但避免了显存和摄像头资源双倍占用的问题是web部署阶段性价比最高的处理方式。5. 用ID Switch作为跟踪质量指标验证yolov5与deepsort的配合效果调完参数需要一个客观指标来确认跟踪器到底稳不稳。deepsort最有代表性的质量指标是ID Switch指同一个目标在跟踪过程中ID突然变化的次数。统计ID Switch不需要标注数据集用一段固定视频跑一遍逐帧比对相邻两帧的跟踪结果就能得到。def count_id_switches(tracks_by_frame): switches 0 prev_ids set() for tracks in tracks_by_frame: cur_ids set(tracks.values()) ended prev_ids - cur_ids for tid in ended: # 旧ID消失后又有新ID从相近位置出现计一次switch matches [t for t in cur_ids if tid not in tracks] switches len(ended) prev_ids cur_ids return switches这个简略版本统计的是轨迹中断次数严格来说ID Switch还要按框位置的重合度判断新旧ID是否指向同一物理目标但对日常调参已经够用。跑完一遍记录max_age分别取10、30、60时各自的ID Switch次数和平均推理帧间隔两者综合起来选平衡点。只看ID Switch会引导你把max_age调得很大遮挡虽然稳了但已离开画面的轨迹迟迟不释放新目标反而抢不到ID所以必须同时看耗时和ID Switch两条曲线。接着定位耗时瓶颈。在VideoProcessor里用time.perf_counter包住ReID特征提取和tracker.update两段逻辑。import time t0 time.perf_counter() features extract_features(frame, boxes) t1 time.perf_counter() tracker.update(detections) t2 time.perf_counter() print(freid: {(t1-t0)*1000:.1f}ms, track: {(t2-t1)*1000:.1f}ms)这样能清楚看到瓶颈是在特征提取的ReID模型上还是在匈牙利匹配上。多数项目ReID耗时是tracker.update的5到10倍这时可以考虑换成更小的ReID权重或者把每帧都提取特征改成每隔N帧提取一次中间帧只做IoU运动匹配。yolov5本身的检测耗时可以用同样的方法单独包住model(frame)那行如果检测耗时超过单帧时间预算的60%优先降低imgsz或换更小的yolov5n权重而不是去改deepsort的参数。最后提一个生产部署层面的实践flask自带的开发服务器只能支撑单进程如果换用gunicorn且配置了多worker每个worker都会加载一份yolov5权重和一个deepsort实例8G显存跑4个worker就会OOM。常见做法是维持单worker运行靠多线程和全局锁支撑多路视频把耗时操作放进独立线程池或者把检测跟踪服务独立成一个常驻进程通过消息队列接收帧数据flask只负责转发结果。两种方案都可行但第一种对没有分布式基础设施的小项目更友好改动量也最小。日常迭代时打开页面把conf-thres从0.3拖到0.7同时观察画面里的框数量和ID切换频率再对照ID Switch统计结果就能判断当前配置是否适合你的场景。本文还有配套的精品资源点击获取