尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

DeepSORT+OpenCV实现ROI行人测速统计方案详解

DeepSORT+OpenCV实现ROI行人测速统计方案详解 简介面向计算机视觉与智能监控方向的开发者和学习者这个资源包呈现了一个基于深度排序算法与开源计算机视觉库的指定区域行人测速统计完整工程。项目围绕视频输入、图像预处理、行人检测、多目标跟踪、运动速度估算与统计输出展开代码模块划分清晰同时附带多张运行效果图和使用说明文件便于复现实验或在此基础上做二次开发。压缩包共二十个文件包含十个Python脚本、九张效果图片与一份说明文档大小约八点七二兆字节。脚本涵盖模型训练、中央处理器与图形处理器两种推理方式、目标跟踪、速度测算以及数据格式转换等典型环节能够帮助读者快速理解完整工作链路。目前已有三十七人学习下载内容对研究行人检测跟踪算法或构建智能监控原型系统具有实用参考价值也可迁移到交通路口通行监测、公共场所人流量统计等场景。1. 为什么路边监控里的行人测速需要DeepSORT和OpenCV同时上场小区门岗、园区内部路、学校门口这类场景常常要做行人测速统计目的是判断异常横穿、逆行或者长时间逗留。一个摄像头如果能圈定一块ROI区域再给出穿过该区域的行人速度很多研判工作就可以自动化。要做到这一步只用OpenCV做帧差是不行的帧差只能告诉你“画面哪里在变”回答不了“这个正在运动的目标上一帧是谁”。DeepSORT的价值正好补在这一点上——它把同一个行人跨帧关联成一条稳定轨迹再配合OpenCV做多边形区域判断、视频读写与结果绘制才形成从“检测到人”到“测出速度”的完整闭环。这套DeepsortOpenCV的ROI行人测速统计方案适合给监控加行为分析的工程师也适合拿OpenCV图像处理做课设、毕设的人把ROI改一改还能顺势改成车辆测速或越界报警。2. 系统拆解DeepSORT管跟踪OpenCV管几何ROI管范围2.1 为什么测速必须先有稳定ID检测、跟踪、位移换算是三个独立步骤直接说结论行人测速不是“每帧检测到人然后算距离”这么简单中间缺的是“同一目标归属”这一步。逐帧检测出来的框是彼此独立的第12帧的甲和第13帧的乙没有任何数据结构告诉系统它们是同一个人。OpenCV里的帧间差分、背景建模能找出运动区域但它在多目标遮挡后无法保持身份光流能给出像素运动方向却没有“一个人”的概念。DeepSORT把这个问题解决到了工程可用的程度它对检测框做卡尔曼滤波预测下一帧位置抽取外观特征建立特征库最后用级联匹配和匈牙利算法决定哪个框属于哪条历史轨迹。放在测速题目里DeepSORT的性能指标不是跑得多快而是返回的track_id足够稳定能让人把连续帧连起来算位移。为什么不选KCF、MOSSE这类短时跟踪器它们在行人转身、被遮挡、走出画面再回来时非常容易把框锁到背景上ID连续性基本靠运气。DeepSORT用运动特征加外观特征联合判别抗遮挡和重识别能力明显更强这也是很多项目做“deepsort改进”的动机——本质上都是想让ID连续得更久。测速统计最怕ID跳变一个人走到画面中间ID从3跳到17系统会把两段轨迹拼成同一个人输出速度要么变成0要么飞到每小时80公里整个统计表当场报废。所以在一个测速项目里ID稳定性就是生命线。对你自己的项目来说检测器选什么、max_cosine_distance怎么调全部要围绕这条生命线来定。2.2 ROI用多边形而不是矩形OpenCV图像处理项目里的常规做法ROI全称是感兴趣区域在OpenCV里最直观的实现是矩形框但做行人测速我几乎不用矩形。原因很简单镜头带透视同样10米道路在画面近端可能占400像素在远端可能只有80像素。矩形ROI把远近目标当成同一个尺度速度计算一开始就是错的。常见做法是把ROI画成梯形或多边形贴合实际道路边界同时还能把近端、远端分开标定。import cv2 import numpy as np # 以一段斜向拍摄的小区道路为例 # 四个顶点顺序按左上 - 右上 - 右下 - 左下排列 roi_pts np.array([ [160, 420], # 左上远端道路左侧 [720, 200], # 右上远端道路右侧 [1180, 640], # 右下近端道路右侧 [460, 760] # 左下近端道路左侧 ], dtypenp.int32) # 生成掩膜只要一次不要在视频主循环里重复生成 mask np.zeros((720, 1280), dtypenp.uint8) cv2.fillConvexPoly(mask, roi_pts, 255) point (600, 250) inside cv2.pointPolygonTest(roi_pts, point, False) 0 print(point inside roi:, inside)逻辑说明先把ROI顶点按顺序存进数组fillConvexPoly生成一张掩膜图pointPolygonTest检测点与多边形的关系返回值大于等于0表示点在多边形内或边界上。这里特别强调False参数是“不计算点到边的距离”只要布尔结果速度最快。因为ROI在整个视频里是不变的掩膜在程序初始化时生成一次即可主循环里只需要对每个track做pointPolygonTest单次开销微秒级不会成为性能瓶颈。还要多说一句进入ROI判断的坐标点我不用检测框中心而是用脚点。检测框中心大致在行人胸部高度目标站立和蹲下时中心位置变化很大脚点取“框底边中点”更接近行人与地面的接触位置用它判断是否跨入道路边界才符合常理。后面专门讲测速时这个脚点还会再次出现。2.3 每一条轨迹到底要记录什么track数据字段设计工程上最容易犯的错是“先把画面跑起来再回头补数据结构”。跑demo可以蒙混过关做统计系统不行。每条被追踪的行人轨迹至少要按下面的字段组织字段含义对测速的作用track_idDeepSORT维护的全局ID跨帧关联判断是否同一个人frame_id当前帧序号计算时间差x1, y1, w, h检测框坐标与尺寸提取脚点、过滤过小目标foot_point脚点坐标取(x1w/2, y1h)进入ROI判断和测速的基准点confirmedDeepSORT轨迹是否已确认过滤未确认的短碎片轨迹last_seen最近一次出现的帧号超时清理缓冲区防止内存上涨这个表不是理论设计是踩出来的经验。比如confirmed这个字段DeepSORT刚起步的目标会经历未确认阶段这时候框的坐标还不稳定把它也送去算速度输出的经常是莫名其妙的抖动值。另一个经验是last_seen轨迹离开画面后如果一直留着缓冲视频跑十分钟内存也会涨到不可接受。正确做法是每处理完一帧把所有超过max_age还没更新的track清理掉。2.4 数据流向从检测器到测速统计的完整链路把上面几段串起来一条完整的链路是检测器对当前帧输出若干个人形框DeepSORT拿这些框和上一轮的历史轨迹做关联匹配输出带track_id的轨迹集合系统对每条轨迹取脚点做ROI判定脚点在区域内的进入测速缓冲速度估计器用缓冲里跨越时间窗的位移算出速度最后把结果画到视频上并写入CSV。这个链条的每一环都可以单独替换比如检测器从YOLOv5换成YOLOv8DeepSORT换成某个改进版本ROI从梯形改成六边形但数据流顺序不要动。很多人把“ROI过滤”放在DeepSORT之前想减少跟踪计算量这在单目标demo里没问题放到密集人群场景反而会翻车人走到ROI边界时前一帧过滤掉、下一帧保留ID匹配的连续性就被打断了。3. 用DeepSORT和OpenCV把ROI测速跑起来最小可执行方案3.1 接线方式检测器输出框DeepSORT输出轨迹这一节给一个人人可改的最小实现骨架。检测器部分不做展开项目里常见的做法是加载一个预训练的YOLO权重输出bboxes和scoresDeepSORT部分不同开源版本接口有差异代码里保留接口名你换成自己手头能跑的那一版即可。import cv2 import numpy as np # detector: 预训练目标检测器输出 bboxes 和 scores # deepsort: 已加载权重并构造好的追踪器实例 tracker build_deepsort(model_pathckpt/deepsort.pt) cap cv2.VideoCapture(demo.mp4) fps cap.get(cv2.CAP_PROP_FPS) or 25 while True: ret, frame cap.read() if not ret: break bboxes, scores detector.predict(frame) # 这里传 frame 进去是因为 DeepSORT 需要提取外观特征 tracks tracker.update(bboxes, scores, frame) for t in tracks: if not t.is_confirmed(): continue # 跳过未确认的碎片轨迹 tid t.track_id x1, y1, w, h t.to_tlwh() # 具体方法名以你所用版本为准 foot_x int(x1 w / 2) foot_y int(y1 h) # 到这一步foot_x, foot_y 就可以交给ROI测速模块逻辑说明tracker.update每帧调用一次内部会完成卡尔曼预测和级联匹配is_confirmed()用来判断这条轨迹是否已经稳定比过滤未确认轨迹更可靠。to_tlwh()把框转成“左上角x、左上角y、宽、高”的格式再取“宽的一半x”得到脚点横坐标。如果你用的DeepSORT版本返回的是xyxy格式这一步自行换算即可核心思想不变。参数说明里要留意两个点第一detector.predict返回的scores建议在进DeepSORT前就做一次阈值过滤常见值是0.5太低会把大量背景框送进关联器track_id闪烁太高又会漏掉远处的小目标。第二有些版本在update时要求传入original_wh这是为了把框坐标缩放到原图尺寸代码不要漏掉这个参数。3.2 速度怎么算用滑动窗口而不是紧贴的两帧很多人第一版会把速度写成“最近两帧位移除以时间差”结果就是画面里站着不动的人也在不停跳动因为检测框本身在抖动。更好的做法是用滑动窗口取最近20到30帧首尾的脚点位移除以总时间削弱单帧检测噪声。这里用一个独立的类来管理这段缓冲。from collections import deque class LinearSpeedEstimator: def __init__(self, maxlen25): # 最多保留25帧的脚点记录自动覆盖最旧的数据 self.buf deque(maxlenmaxlen) def push(self, frame_id, foot_x, foot_y): self.buf.append((frame_id, foot_x, foot_y)) def estimate(self, fps, px_per_meter): # 数据点太少说明轨迹刚建立或刚离开ROI if len(self.buf) 8: return None first self.buf[0] last self.buf[-1] dt (last[0] - first[0]) / fps if dt 0: return None dist_px ((last[1] - first[1]) ** 2 (last[2] - first[2]) ** 2) ** 0.5 dist_m dist_px / px_per_meter speed_ms dist_m / dt return speed_ms * 3.6 # 从 m/s 换算成 km/h逻辑说明窗口采用deque(maxlen25)新数据进来会自动丢弃最旧数据不需要手工维护数组长度。estimate取窗口首尾两点做线段距离除以帧间隔和每秒帧数得到真实时间差。速度单位最后乘以3.6是为了输出每小时公里数方便直接写进报表。参数说明maxlen选25不代表精确取决于你的视频帧率。25帧30fps下大约是0.83秒窗口足够平滑检测框抖动也不至于把人从画面近端走到远端的时间都算进去。8这个最小帧数阈值是经验值低于它宁可不出速度也不要输出一个波动很大的假值。px_per_meter是“每米对应多少像素”由相机标定得到第4章专门讲。3.3 进入ROI和离开ROI一个简单可靠的状态机测速统计还要回答“这个人算不算穿过ROI”。最朴素的办法是上一秒不在区域内下一秒在区域内记一次进入上一秒在区域内下一秒不在记一次离开。def point_in_roi(foot_x, foot_y, roi_pts): return cv2.pointPolygonTest(roi_pts, (int(foot_x), int(foot_y)), False) 0 roi_status {} def track_crossing(track_id, now_in): prev_in roi_status.get(track_id, False) roi_status[track_id] now_in if now_in and not prev_in: return enter if not now_in and prev_in: return exit return None逻辑说明roi_status记录每个ID上一次的进出状态每次用当前状态跟历史状态比较即可得出事件。这个状态机足够简单但它解决了统计系统最关键的问题不会因为一帧的噪声就重复计数。注意在“进入”或“离开”触发后应该把该ID的速度和帧号写入汇总记录用于后续画曲线或者导出报表。经常被忽略的是清理动作如果一个track已经离开ROI超过若干秒roi_status里还留着它的记录字典会越胀越大。正确做法是在前面last_seen清理逻辑里顺手把roi_status.pop(track_id, None)。3.4 输出端叠加ROI、画轨迹、写CSV出报表是统计系统最后的落脚点。视频叠加里至少要把ROI多边形、行人框、当前速度画出来数据端每生成一条有效速度记录就追加一行CSV字段顺序和2.3的数据表保持一致。import csv def write_record(csv_path, fields): with open(csv_path, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow(fields) # 每次 enter 事件发生追加一行 write_record(speed_log.csv, [ tid, frame_id, speed_kmh, int(time.time()), 0 ])# 视频叠加ROI边界用黄色行人的速度文本放在框上方 cv2.polylines(frame, [roi_pts], True, (0, 255, 255), 2) cv2.putText(frame, f{speed_kmh:.1f} km/h, (x1, max(20, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)逻辑说明write_record用追加模式打开文件每次写一行后立刻关闭这样即使程序中断也不会丢太多数据。缺点是有文件I/O开销所以不要在逐帧循环里调用只在跨入ROI的那一帧调用一次性价比最高。参数说明CSV里加一个时间戳字段很重要后续做时段统计比如“早高峰行人平均速度”和“午间行人数量的对比”全依赖这个字段。视频叠加的坐标要计算一下如果行人框贴到画面上边缘y1 - 8会变成负值文本画到画面外面去所以用max(20, y1 - 8)兜底。4. 参数标定、DeepSORT阈值、速度平滑三个值得反复调的位置4.1 单相机标定没有pixel_per_meter前面算的速度全是像素速度速度公式里最容易被忽略的变量是px_per_meter。这个东西必须由人工标定得到在场景里找一段已知真实长度的地面线段测量它在画面里的像素长度两者相除就是标定系数。import numpy as np # 在画面里选一段已知真实长度的地面线段 p1 (120, 600) p2 (500, 600) known_len_m 5.0 d_px np.linalg.norm( np.array(p2, dtypenp.float64) - np.array(p1, dtypenp.float64) ) px_per_meter d_px / known_len_m print(px_per_meter:, px_per_meter)逻辑说明这是一把“把像素距离翻译成实际米数”的尺子。人站在线段同一深度上行走时这个系数才能代表真实距离换算。参数说明里有一点必须讲清单目相机天然有近大远小的透视如果你把这段5米线段放在画面近端远端同样的5米地面线可能像素长度只有它的一半。应对措施是分段标定在近端、中段、远端各标一次得到一组(foot_y, px_per_meter)映射按脚点纵坐标插值使用。这个做法能明显改善斜向视角下的速度误差但不要追求完美。单目视觉在深度方向上的信息本身是缺失的标定能保证ROI区域内的平均误差落到可接受范围通常是10%以内。如果场景要求“必须非常精确”那就要上激光测距或者双目方案这不是OpenCV单目能解决的边界。4.2 DeepSORT三个必调参数max_cosine_distance、max_age、检测置信度很多人拿到就默认跑速度波动大时先怀疑算法其实DeepSORT里决定ID稳定性的就那么几个旋钮参数常见取值对测速的影响建议max_cosine_distance0.2 ~ 0.3外观特征匹配的容忍度行人着装相近时调低遮挡频繁时适当调高max_age15 ~ 30轨迹丢Target后保留的帧数人走出画面又回来设长一点怕碎片ID设短一点检测置信度下限0.3 ~ 0.6进DeepSORT前过滤低质量框场景远目标多调低近景大目标调高参数说明max_cosine_distance这个值决定外观特征库里两条轨迹的相似度要有多“近”才配到一起。值设得太低一个人转身、背包、被树枝挡住半秒就会被认为是新目标track_id断裂值设得过高两个穿同色衣服的人可能被拼成同一个ID速度从3.5直接跳到20。街拍行人场景0.25是常见的起步值。max_age是目标丢失后的容忍时间30fps下取30能让目标被遮挡一秒后还能找回原ID但设得太长旧轨迹会继续预测位置实际画面里可能出了一个新的人ID还是旧的测速结果一样是歪的。这三个参数不是独立能调好它们互相牵扯。经验顺序是先把检测置信度调到“画面里没有漫天乱框”的程度再调max_age让ID碎片变少最后动max_cosine_distance解决“同一个人总被换号”的问题。这个过程多少带点玄学最好一次只改一个值用五分钟回放视频观察ID连续性。4.3 速度平滑与异常限幅输出曲线不再“跳舞”滑动窗口已经过滤掉一部分高频抖动但近距离目标的脚点偶尔会突然跳两个像素换算成速度可能就是每小时3到5公里的波动。对统计报表来说这种毛刺会造成平均数偏差。更实用的滤波器是限幅器如果当前速度和上一输出值差别超过阈值就判定为异常直接沿用上一个值。def apply_plausible_limit(cur_speed, prev_speed, max_delta5.0): if prev_speed is None: return cur_speed if abs(cur_speed - prev_speed) max_delta: # 跳变量超过5km/h通常不是真实运动而是ID漂移或框抖动 return prev_speed return cur_speed逻辑说明这个函数不是数学滤波而是一个基于常识的物理约束。行人正常步速4到6km/h加速跑也就十几km/h一帧之内从4跳到15属于罕见情况直接接受它会污染统计样本。参数max_delta设成5行人加速、转身都能覆盖只有ID跳变或脚点检测异常才触发。参数说明要提醒限幅器只能治标如果速度经常触发它说明前面的ID漂移和标定没有做好要回去查max_cosine_distance和px_per_meter而不是把这个bug当成正常容忍。5. 避坑/常见问题/排查ROI测速落地的五个典型翻车现场5.1 ID漂移让速度瞬间“起飞”现象视频里行人明明在正常走报表里某条速度记录突然出现31km/h之后再降回4km/h。 原因DeepSORT的ID发生切换系统把两个不同行人的轨迹拼成了同一人。最典型是两名行人在ROI内交叉外观特征匹配错误ID的跳变让速度缓冲里混合了两个目标的位置。 解决先回放确认是否发生在遮挡或交叉瞬间。然后把max_cosine_distance从0.3下调到0.2并检查检测器是否在密集帧段漏检导致单目标长时间只剩预测框。代码上给速度估计增加一个“帧间隔上限”判断如果窗口内最近一次更新与最新帧间隔超过10帧直接清空速度缓冲不让旧预测点参与计算。5.2 静止行人被算成慢速移动现象人在ROI里站着不动屏幕上速度始终在0.5到3之间跳动。 原因检测框本身每帧有轻微抖动脚点也跟着抖滑窗位移不为零。 解决不要试图用更低阶的滤波把抖动磨平更直接的办法是设置一个像素级判定脚点位移小于2像素时速度直接归零。这个阈值在1080P画面、远景深度下已经够用不同分辨率要按比例调整。另外检查是否把速度估计的最小帧数设置太少比如少于5帧就开始输出这会导致抖动被放大。5.3 人在ROI边界反复横跳现象一个行人站在ROI边界上enter和exit事件交替触发统计人数比实际多出一倍。 原因脚点在边界附近抖动每帧在“内侧”和“外侧”之间变化状态机来回翻转。 解决在状态机里加入“连续确认”逻辑比如连续两帧都判定为进入才确认进入事件离开同理。实现上可以给每个track维护一个“候选状态”计数器当前状态与最新判定不一致时先积累达到2帧再翻转。同时把脚点做一次轻量均值取最近3帧脚点坐标平均后再送pointPolygonTest边界抖动会明显收敛。5.4 拉流中断OpenCV读RTSP流中途黑屏现象cv2.VideoCapture(rtsp://...)在程序运行几分钟后返回黑帧进程卡在read()上整条测速链路停摆。 原因OpenCV依赖的FFmpeg在弱网和高延迟下没有足够强的缓冲和重连策略底层socket一旦堵住read()会阻塞另外OpenCV版本过低时对H.265编码的兼容性也差。这个现象关键词就是“opencv python拉流中断”跑生产环境很常见。 解决不要把OpenCV当成万能拉流器。稳定做法是让FFmpeg或GStreamer子进程把RTSP转成本地UDP或文件流OpenCV只消费本地的video.mp4彻底绕开网络抖动。如果只想短期跑通可以给cap.read()加超时控制并用独立线程拉流。另一个版本层面的注意建议opencv-python保持4.5以上旧版本对部分摄像头协议支持很差。5.5 环境不一致OpenCV装完DeepSORT跑不起来现象复制项目到新电脑第一句import cv2就报错ModuleNotFoundError: No module named opencv或者跑了十分钟才报NumPy版本冲突。 原因有人装了opencv-python有人装了opencv-contrib-python两者对部分模块的实际版本不同DeepSORT又常常依赖较老的NumPy或PyTorch把环境搅在一起后谁也说不清哪个库把哪个库的依赖覆盖了。 解决创建一个干净的虚拟环境把需求锁定成文件安装。碰到No module named opencv直接先pip install opencv-python再用python -c import cv2; print(cv2.__version__)验证。如果项目本身就要求opencv4.2就在requirements里写死不要图省事用全局环境跑不同项目。虚拟环境多花五分钟能省下后面一整天的排错时间。6. 验证与扩展人工标定校对后再做多ROI和异常剔除6.1 精度确认不该靠拍脑袋系统能跑出速度之后第一步不是调得更快而是拿真实数据校对。我一般准备一段总长10米的平整道路让测试人员在ROI里分别进行正常步行和慢跑用秒表记录通过时间算出真实速度作为基准。然后把系统输出和真实速度做对照表。测试项操作方式合格参考静止误差人站在ROI中央5秒平均输出应小于0.3km/h匀速误差沿ROI边线正常步行3次平均误差在10%以内ID连续性人在ROI内走一个来回完整来回不应被拆成超过3段轨迹边界计数人在边界左右走动10次enter/exit总次数应接近实际次数误差不超过1这条验证流程比任何调参都重要。因为它把“我觉得准”和“数据真的准”区分开了。如果匀速误差超过10%先回头看px_per_meter的标定点是否选得不对如果ID连续性差再回头调max_cosine_distance。反向顺序调参基本就是在黑匣子里盲试。6.2 多ROI扩展车道级统计与时段留存单ROI跑通后扩展成多ROI的代价很小。把所有ROI放进一个数组每个track的脚点依次跟各个多边形做判断命中哪个区域就归到哪个统计桶。这样一条道路可以拆成车道A、车道B、人行横道区分别统计速度是否异常。代码上要额外记录每个track关联的ROI编号避免同一个目标同时命中多个重叠区域。有了时间戳字段还可以按小时切片画出“哪个时段横穿速度最快”的热力曲线这类的趋势数据在现场汇报里比单条速度记录更有说服力。6.3 一个建议把速度记录做成“要素”而不是“结果”我做这类项目到最后有一个习惯CSV里除了速度还会保存窗口内的位移方向、首尾帧号、脚点坐标。因为这些原始数据是后悔药万一后面发现某一批速度记录被ID漂移污染了还能回溯原始轨迹重新计算不用重新跑一遍视频。ROI行人测速这个需求不算难但真正做到能交付拼的从来不是模型有多新而是ID稳不稳、标定准不准、数据能不能回溯。希望这份踩坑清单能让你少走一段弯路顺利把方案落到自己的场景里。本文还有配套的精品资源点击获取
返回列表