ARTICLE DETAIL

资讯详情

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

Deepsort+OpenCV实现ROI区域行人测速统计系统详解

Deepsort+OpenCV实现ROI区域行人测速统计系统详解 简介一套基于DeepSORT与OpenCV的ROI区域行人测速统计系统源码包面向计算机视觉学习者、智能监控开发者及安防领域研究人员用于解决特定区域内行人检测、连续跟踪与速度统计问题。包内共20个文件包括10个Python脚本覆盖检测器CPU/GPU版本、目标跟踪、速度估算、模型训练及数据整理等模块9张PNG图片展示检测跟踪效果另附README.md说明文档便于了解项目结构与运行方式。整个压缩包仅8.72MB轻量易部署适合本地复现目前已有37人学习下载。借助这套源码可完整理解视频输入、预处理、目标检测、DeepSORT深度特征匹配、基于位置变化推算速度再到统计分析的技术链路代码模块清晰适合作为课程设计或工程实践的参考模板。典型应用场景包括交通路口行人过街速度监测、公共场所人流疏导与安全预警也能为城市步行环境评估提供数据参考。1. 这个系统解决什么问题从跟踪到测速统计的完整闭环平时做视频分析大家最容易高估检测和跟踪的难度低估“把像素距离换算成真实距离”这一步的复杂度。基于Deepsort和OpenCV的ROI区域行人测速统计系统做的就是一件很具体的事用Deepsort把视频里的每个行人稳定跟踪住用OpenCV在画面里划定ROI区域再把行人穿过ROI时留下的轨迹换算成真实世界里的速度最终输出某个时间段内的行人数、平均速度和进出方向。它对应的是安防摄像头、园区通道、商场出入口这类场景里最常被问的问题这里一小时过了多少人、走得快还是慢、往哪个方向去。适合刚接触目标跟踪想跑通完整链路的同学也适合要拿数据交差的工程实施人员。2. 搭建检测跟踪链路Deepsort和OpenCV的分工与最小主循环2.1 Deepsort不是检测器跟踪器与检测器的边界在哪必须先说清楚一件事Deepsort本身不识别行人它只负责把检测器给出的目标框“串”成轨迹。它的核心是卡尔曼滤波预测每个轨迹的下一帧位置再用级联匹配把新检测框和已有轨迹做关联最后一层用IOU匹配兜底处理遮挡后重现的目标。也就是说检测器负责每一帧的“谁在哪”Deepsort负责跨帧的“谁是谁”。这个分工决定了系统的错误来源也分为两类检测器漏检导致轨迹中断跟踪器ID switch导致速度跳变。落地时要同时防。我一般会把检测器的置信度阈值压到较低的值比如0.3左右宁可多输出几个假框也要先保证不丢检。假框通常很快会被Deepsort的ReID特征层丢弃而漏检会让一个已经算了一半速度的轨迹突然断掉统计就不完整。2.2 检测器选型先保证检测质量再谈跟踪常见做法是YOLOv5s或YOLOv8n。这两个模型的共同点是检测框稳定、小目标友好、CPU也能跑出10fps以上。实际用下来的体验是在1080p视频里YOLOv5s比YOLOv8n的框在行人底部更贴地这会影响后面测速时“底部中点”这个点的投影准确性。如果摄像头安装在3米以上的高度可以用低置信度阈值来缓解框偏高的问题。选检测器时不要贪模型大小。就行人测速来说检测框的稳定性比检测精度更关键。一张1080p画面里检测损失的几个点AP对测速影响不大但底部中点上下抖动10个像素投影到地平面后可能就是0.3米的位移误差它会直接成为速度噪声的一部分。所以选检测器的指标建议看“不同帧之间同一人的框位置标准差”而不是mAP。2.3 整条数据流的最小主循环代码与参数说明用deep_sort_realtime这个pip包它是社区里封装得比较顺手的Deepsort实现和OpenCV搭配很自然。下面是一个能跑通的最小主循环把检测、跟踪、ROI判断三个环节串起来。import cv2 import numpy as np from deep_sort_realtime.deepsort_tracker import DeepSort # 初始化Deepsort跟踪器 tracker DeepSort( max_age30, max_cos_dist0.2, nn_budgetNone, embeddermobilenet, halfTrue, ) cap cv2.VideoCapture(pedestrian.mp4) roi_polygon np.array([(200, 400), (800, 400), (950, 700), (50, 700)], np.int32) while cap.isOpened(): ret, frame cap.read() if not ret: break # detector是占位符,实际接YOLO输出,格式为[(x1,y1,x2,y2,conf), ...] detections detector(frame) tracks tracker.update_tracks(detections, frameframe) for track in tracks: if not track.is_confirmed(): continue track_id track.track_id x1, y1, x2, y2 track.to_ltrb() cx, cy (x1 x2) / 2, y2 # 底部中点,用于地面投影 if cv2.pointPolygonTest(roi_polygon, (cx, cy), False) 0: print(track_id, cx, cy)代码里值得注意的参数max_age30表示一个轨迹在连续30帧没有匹配到检测框之前不会消失遮挡容忍度就靠它撑max_cos_dist0.2是ReID特征向量夹角的余弦距离阈值大于这个值就认为不是同一个人。这两个参数是一对跷跷板——调大max_age防断档调大max_cos_dist防窜号但调过头会互相恶化。首次跑通时不要追求最优用这组开局值先把流程走完后面再针对自己的视频内容调。2.4 OpenCV在链路里的三个不可替代职责第一是ROI掩码。在每帧上做一次cv2.fillPoly把多边形区域生成一张mask再用cv2.bitwise_and过滤ROI外的检测这个做法比在Python层做坐标判断快得多。第二是透视变换。cv2.findHomography和cv2.warpPerspective是标定和投影的标准工具Deepsort本身完全不涉及几何变换。第三是视频读写与叠加绘制。cv2.VideoCapture读流、cv2.VideoWriter写结果视频、cv2.putText叠加速度值、cv2.polylines画轨迹这些Deepsort包里都没有。跟踪归跟踪图像和几何归OpenCV这就是标题里OpenCV存在的意义。3. ROI区域与地面标定测速精度真正由几何决定3.1 ROI的两种角色测速区域和虚拟线ROI在这个项目里承担两个角色。角色一是测速生效区域行人的轨迹点只有落在ROI多边形内才参与速度计算。角色二是虚拟线ROI的一条边可以充当计数线通过判断轨迹从哪一侧穿过这条线来区分进出方向。配置ROI最直接的方式是在视频里人工点选多边形顶点把坐标写进配置文件。我在实际项目里通常把ROI设置成平行四边形或梯形顺着通道走向拉长这样轨迹在ROI内部覆盖足够多帧。ROI太小轨迹里可用的点太少ROI太大把入口干扰区域也包进去了统计会混进无关目标。3.2 像素坐标换成米比例尺与单应矩阵两种标定方案把像素轨迹换算成真实距离最简单的方案是单一比例尺在画面里量一段已知长度的地面线段得到“每像素等于多少米”。这个方案在俯视摄像头下还能用一旦摄像头是斜视的——比如装在立杆上往下看——近处和远处的像素尺度完全不同单一比例尺会让测速在近处偏大、在远处偏小而且是系统性的。这个方案在斜视摄像头下基本是血泪经验能不用则不用。推荐方案是求单应矩阵H。做法是在ROI区域内的地面上取4个以上的参考点同时记录这些点的真实世界坐标。世界坐标不一定要经纬度只要点与点之间的相对距离是准确的就行因为速度本身就是相对量。H把像素平面映射到地平面之后轨迹上任何一个像素点都能投影成地面坐标两个地面坐标之间的距离除以时间差就是速度。两种方案对比对比项单一比例尺单应矩阵原理全画面用同一个像素/米比平面到平面射影变换适用摄像头俯视、高度差小斜视立杆、常规监控参考点要求已知一段地面长度4个以上像素/世界坐标点对误差表现近处偏大、远端偏小均匀分布取决于选点质量推荐度不推荐推荐3.3 用cv2.findHomography把轨迹投影到地平面标定和投影的完整步骤可以直接抄。注意pixel_pts里的四个点要选择地面上的稳定参考点不要选行人的头顶或画面边缘因为投影最终要把人的落脚点映射到地面。import cv2 import numpy as np # 1. 在画面里选点,顺序要和world_pts一一对应 pixel_pts np.array([ [156, 520], # 对应世界坐标[0, 0] [524, 420], # 对应世界坐标[2, 0] [880, 520], # 对应世界坐标[2, 2] [420, 640] # 对应世界坐标[0, 2] ], dtypenp.float32) world_pts np.array([ [0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0] ], dtypenp.float32) # 2. 求单应矩阵,RANSAC能容忍手工选点误差 H, _ cv2.findHomography(pixel_pts, world_pts, methodcv2.RANSAC) # 3. 任意像素点 - 地面坐标 def pixel_to_world(px, py): w H np.array([px, py, 1.0]) return w[0] / w[2], w[1] / w[2] # 4. 两个地面坐标 - 距离,单位米 def world_distance(x1, y1, x2, y2): return float(np.sqrt((x1 - x2) ** 2 (y1 - y2) ** 2)) print(pixel_to_world(300, 500))这段代码有几个关键点。世界坐标用相对坐标就行比如把ROI的某个角定为原点向右2米、向前2米只要真实长度准确。findHomography的RANSAC会自动排除手工选点的外点但前提是至少给5个点4个点只是理论上可解实际标定时我会取6到8个点。投影完成后有个快速验证方法随便找一帧把一个人的脚底点投影到地面坐标让他往前走几步看投影轨迹是否平滑。如果投影轨迹在远端点出现弯曲说明H不准需要增加参考点重新算。3.4 测速计时基准用时间戳别用帧号这个坑在Deepsort项目里几乎必踩。很多第一版实现会用“轨迹的第N帧和第M帧之差除以视频fps”来算时间。视频文件是固定帧率录制时误差不大但项目一换到实时摄像头每帧的处理耗时是波动的帧号差不再代表真实时间速度值就会跟着忽大忽小。正确做法是每处理一帧记录cap.get(cv2.CAP_PROP_POS_MSEC)或者直接用time.time()。后一种在实时流里更稳。测速公式就变成速度等于地面位移除以真实时间差。这样不管处理器怎么波动速度只依赖真实时间差Deepsort的卡尔曼预测不关心时间戳一致性但速度计算这层必须自己保证计时准。4. 实现行人测速统计参数配置、速度平滑与计数逻辑4.1 Deepsort六个关键参数和一组开局配置Deepsort能调的核心参数有六个max_age、max_cos_dist、min_confidence、nn_budget、nms_max_overlap、embedder。min_confidence是检测框置信度阈值低于它的检测不会进入跟踪器nn_budget是ReID特征库容量nms_max_overlap是检测框去重阈值embedder决定ReID网络。我建议的开局配置是max_age30、max_cos_dist0.2、min_confidence0.3、nn_budgetNone、nms_max_overlap0.7、embeddermobilenet。mobilenet在CPU上的ReID推理大约5到8毫秒不会把整个系统拖慢。调参顺序有讲究先固定embedder和nn_budget然后按“检测漏检率高先降min_confidence”“轨迹频繁断就升max_age”“不同行人串号就降max_cos_dist”的顺序调不要一上来同时动多个参数否则出了毛病根本定位不到是谁引起的调参直接变成玄学。4.2 为什么不能用瞬时帧差算速度三个典型翻车现场用相邻两帧的坐标差直接除以帧时间理论上就是瞬时速度实际跑起来速度曲线毛刺非常多甚至出现10m/s以上的荒谬值。翻车原因有三个。一是检测框噪声底部中点在相邻帧之间抖动几个像素投影到地面后会被放大成几十厘米的位移。二是卡尔曼滞后Deepsort的轨迹更新有预测和修正交替直接用预测位置和检测位置交替参与计算会引入周期性抖动。三是底部中点本身的问题人走在画面里脚底点在小目标上往往不稳定瞬时帧差把这种不稳定全部变成速度误差。所以速度计算必须做平滑。4.3 速度平滑与统计聚合一个可以直接改的Python模块下面这个类把速度计算和统计聚合封装在一起可以直接嵌入主循环。核心思想是用滑动窗口内的中位速度代替瞬时速度同时对轨迹做一次性计数避免重复统计。import time import cv2 import numpy as np from collections import defaultdict class SpeedStats: def __init__(self, H, roi, window10, max_speed6.0): self.H H self.roi roi self.window window self.max_speed max_speed self.buffer defaultdict(list) # track_id - [(ts, wx, wy)] self.counted set() # 已统计的track_id self.hourly defaultdict(lambda: {count: 0, speeds: []}) def _pixel_to_world(self, px, py): w self.H np.array([px, py, 1.0]) return w[0] / w[2], w[1] / w[2] def update(self, tracks): records [] now time.time() for tr in tracks: track_id tr.track_id if not tr.is_confirmed() or track_id in self.counted: continue x1, y1, x2, y2 tr.to_ltrb() wx, wy self._pixel_to_world((x1 x2) / 2, y2) # 底部中点投影 if cv2.pointPolygonTest(self.roi, (wx, wy), False) 0: continue self.buffer[track_id].append((now, wx, wy)) if len(self.buffer[track_id]) self.window: self.buffer[track_id].pop(0) if len(self.buffer[track_id]) 5: continue buf self.buffer[track_id] speeds [] for i in range(1, len(buf)): dt buf[i][0] - buf[i-1][0] if dt 1e-6: continue dist np.sqrt((buf[i][1] - buf[i-1][1]) ** 2 (buf[i][2] - buf[i-1][2]) ** 2) speeds.append(dist / dt) speed float(np.median(speeds)) if speed self.max_speed: continue # 异常值直接丢弃 hour_key time.strftime(%Y-%m-%d %H:00) self.hourly[hour_key][count] 1 self.hourly[hour_key][speeds].append(speed) self.counted.add(track_id) records.append((track_id, hour_key, speed)) return records这个模块里window10表示取最近10个轨迹点做速度估计max_speed6.0是速度上限过滤超过就丢弃counted集合保证一条轨迹只被统计一次。像素坐标到世界坐标用的是底部中点而不是检测框中心这一行的差别在最终测速精度上影响巨大。代码里没有做轨迹的ID切换清理实际使用时还要监听新出现的track_id立刻清空对应buffer否则会把新轨迹的坐标差算进旧轨迹的位移里。4.4 进出方向判定与CSV输出方向判断我有一个简单可靠的办法取ROI两侧的虚拟线用叉积判断轨迹起始点和终点分别在线哪一侧符号变化就说明轨迹穿过了虚拟线结合两侧的方向定义就能得出“进”还是“出”。叉积的正负只代表左右不依赖距离绝对值ROI画得不太规则也不影响判断。统计结果写CSV时我习惯按小时聚合每小时一行包含方向、人数、平均速度、中位速度。速度分布不要只用平均值行人场景里中位数比平均数稳健——一个人慢速徘徊就能把平均值拉低0.3m/s中位数基本不受影响。字段按需扩展成日期、小时、方向、人数、平均速度、中位速度即可这个格式直接交给后端或存档都够用。5. 避坑与调参行人测速统计的五个典型问题5.1 ID switch让速度从1.2跳成3.8现象行人A从画面右边走进ROI走到中间时被一个骑电动车的人挡了一下屏幕上的track_id从17变成了23速度输出从1.2m/s跳到了3.8m/s。原因遮挡让ReID特征匹配失败Deepsort在max_age耗尽后终结旧轨迹同时新轨迹在旧轨迹的预测位置附近诞生两个轨迹坐标发生跳变。速度模块没有感知到轨迹切换把两个轨迹的坐标差算进了同一段位移。解决在SpeedStats里发现某个track_id是上一帧没有的新ID时立即清空该ID的buffer不要让它继承任何历史窗口。同时把max_age从30调到45给遮挡留出更多恢复时间。这两个手段合起来ID switch对速度统计的影响能消掉大半。5.2 人在边界线上踱步被反复计数现象ROI的虚拟线附近有块减速带行人走到那里习惯性停顿只要他在虚拟线两侧来回挪一步计数器就加一次10分钟攒了20多次计数。原因计数逻辑只判断了轨迹首尾两点是否在线的两侧没有给轨迹加“只计一次”的约束也没有滞回机制瞬时抖动会被当成一次穿越。解决给每条轨迹加counted标志第一次满足穿越条件后锁死不再参与计数。同时在穿越判定里加连续帧数约束要求轨迹在进入侧连续出现N帧N取5左右才认为真的在通过而不是抖动。这样在边界徘徊的人最多计一次不再刷数据。5.3 远端速度系统性偏小标定参考点没铺满现象同一段路近端摄像头下方测出来是1.3m/s远端测出来只有0.7m/s两个位置差了近一倍怎么看都不合理。原因求单应矩阵时只用了ROI近处的几个参考点H在近处拟合得好向外推到远处就发散。斜视摄像头下画面像素在远端对应的地面距离远大于近端H推到远端后位移被压缩了。解决标定参考点必须铺满ROI整个区域尤其是远端角落。我一般取6到8个点沿通道两侧均匀分布让H的映射误差被均摊。改完参考点后出现了一个新现象远端速度反而比近端略高因为远端人的脚底点在图像上太小检测框底部中点偶尔偏高投影到地面后有一段无中生有的额外位移。这个只能靠速度上限过滤兜底。5.4 fps一抖速度就飞用帧号当时间的后果现象系统在高负载下速度值经常窜到7m/s、9m/s看起来像有人跑步但画面里全是散步的人。原因第一版用帧号差除以fps计算时间间隔fps取的是视频平均值。负载高时某一帧处理要花0.25秒帧号差却只计了1分母用了1/24秒时间被缩小到1/6速度直接放大6倍。解决全部改成时间戳。视频文件就用cap.get(cv2.CAP_PROP_POS_MSEC)实时流就用time.time()。排查时可以打印相邻帧间隔验证prev_ts time.time() while cap.isOpened(): ret, frame cap.read() now time.time() print(f帧间隔: {now - prev_ts:.3f}s) prev_ts now如果这个值波动超过50%说明fps不可靠速度计算必须使用时间戳。改完之后同样的负载下速度毛刺从7m/s降回1.3m/s附近。这个问题排查了整整一个下午最后发现不是算法问题是计时单位的问题。5.5 检测框底部中点漂移投影轨迹转向现象行人在画面里贴着通道边走头顶被树枝挡住半秒检测框底部中点从脚下漂到路牙外投影出的世界坐标轨迹瞬间拐了一个直角速度也明显偏大。原因YOLO在遮挡时检测框不稳定底部中点不再对应真实落脚点。所有后续地面投影和速度计算都基于这个点点漂了整套结果就跟着漂。解决在进入速度计算前对底部中点的世界坐标做一个5帧滑动中位数把单帧突变消掉。中位数对尖刺完全免疫不会把一次漂移平滑成一段假位移。再配合max_speed6.0的上限过滤即使中位数来不及处理异常速度也会被丢弃。这两层兜底是速度统计的后悔药务必都保留。6. 验证方法、参数速查表与标定结果复用技巧6.1 用已知距离走一遍误差控制在10%以内标定和速度算法都写完第一件事不是接真实摄像头而是做一次端到端验证。找一段10米平直地面两端各放一个标志物请一个人正常步速走过去用秒表记录时间算出参考速度。同时让系统跑这段视频输出测速结果。两者对不上就返回去检查H、检查底部中点、检查时间戳。我的经验是误差在10%以内算合格超过10%优先怀疑标定而不是跟踪因为Deepsort在行人场景的轨迹精度基本够用误差大头在几何换算。6.2 把H矩阵和参数固化启动即用、避免重新标定每次启动都重新选点标定很累而且手选点有偏差结果不稳定。我习惯把标定结果存成JSON文件运行主程序时直接加载摄像头位置动过才重新标定import json import numpy as np with open(calib.json, r) as f: cfg json.load(f) H np.array(cfg[homography], dtypenp.float32) roi np.array(cfg[roi_polygon], dtypenp.int32)JSON里放三样东西roi_polygon顶点、homography矩阵、world_pts参考点。这样每次启动都是同一套标定结果不会再因为手滑多点或少点导致结果漂移。6.3 一张参数速查表收拢所有配置配置项推荐值作用min_confidence0.3检测置信度下限max_age30-45遮挡后轨迹保留帧数max_cos_dist0.2ReID特征匹配阈值nms_max_overlap0.7检测框重叠去重阈值speed_window10速度计算窗口帧数max_speed6.0速度上限过滤m/scount_confirm_frames5计数确认连续帧数标定参考点数6-8单应矩阵计算点数这组参数适合户外行人场景。换成室内密集人群max_age要降count_confirm_frames要升因为密集场景ID更容易丢、边界抖动更容易发生。没有一套参数能通吃所有摄像头但按这张表起步配合第5章的避坑经验足够在半天内跑出一个稳定版本。我做这类项目有个固定习惯每次改完标定先录一段10秒匀速行走视频不急着跑完整流程先回放投影轨迹看是否贴地。投影曲线越平滑后面调参省的时间越多。希望帮到你。本文还有配套的精品资源点击获取
返回列表