简介:这份资源面向Python计算机视觉初学者与需要落地客流统计的开发者,围绕OpenCV实现人流量计数与上下行方向统计,可应用于零售门店、交通站点、公共场所等场景的人员流动分析。包内共20个文件,以py脚本、mp4与avi演示视频、pyc缓存、caffemodel与cfg/prototxt模型配置、names标签及md说明为主,压缩包约138.21MB,涵盖主程序、跟踪器与工具模块、MobileNetSSD与YOLO两类检测模型及示例视频,目录结构清晰便于按模块查阅。已有511人学习下载。读者可据此掌握背景减除、目标检测、连通组件分析、卡尔曼滤波或质心跟踪、越界方向判定与独立计数器管理等完整链路,并参考现成脚本与模型快速搭建可运行的人流统计原型,理解参数调优与复杂场景下改用深度学习检测的排错思路。
1. 从一段监控视频说起:python人流量计数到底在数什么
很多人第一次做人流量统计,脑子里想的是“找个人、画个框、加一”,真跑起来才发现完全不是这回事。摄像头架在门口,画面里有人进有人出,有人走到一半折返,有人并排走被检成一个,有人拎着大箱子被当成两个人。python人流量计数人数统计上下行计数统计这个方向,核心要解决的不是“检测到人”,而是“判断这个人往哪个方向走、有没有完整穿过一条虚拟线、同一帧里出现多个目标时怎么不重复计数”。它适合门店客流分析、通道通行统计、园区出入口管理等场景,输入通常是一路 RTSP 流或本地视频文件,输出是带时间戳的上下行人数。新手能靠 OpenCV 加检测模型跑通最小闭环,熟手则要在跟踪 ID 稳定性、越线判定逻辑、遮挡处理上反复打磨。这一篇就按“能复现”的标准,把整条链路拆开讲。
2. 检测加跟踪加越线判定:三段式链路怎么搭
2.1 为什么不能只靠检测框做上下行计数
只做目标检测,每帧输出一堆人框,帧与帧之间没有身份关联,你根本不知道这一帧的张三和下一帧的张三是不是同一个人。计数就变成了“每帧数框”,一个人站三秒能被数九十次。所以上下行计数的前提是跟踪,给每个目标分配一个稳定 ID,再观察这个 ID 的运动轨迹是否穿过了预设的计数线。常见做法是检测器出框,跟踪器做 ID 匹配,越线模块只认 ID 的轨迹点。检测器可以选 YOLO 系列,跟踪器可以选 ByteTrack 或 DeepSORT,越线逻辑自己写。选型上,ByteTrack 对遮挡和低分框的容忍度更好,速度快,适合实时流;DeepSORT 依赖外观特征,ID 切换少但吃算力。我一般先用 ByteTrack 跑通,ID 跳变严重再考虑换。
2.2 最小可运行环境与依赖安装
先确认 Python 版本在 3.8 以上,显卡驱动和 CUDA 按检测器要求配好。下面这套依赖是我在多个项目里验证过的组合,不绑定具体小版本,装最新稳定版即可。
# 创建独立环境,避免和系统包冲突 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 核心依赖:推理、视觉、跟踪、数值计算 pip install ultralytics opencv-python numpy scipy pip install lap # ByteTrack 的匈牙利匹配依赖逻辑说明:ultralytics 提供 YOLO 检测和内置的 ByteTrack 接口,opencv-python 负责读流、画线、写视频,scipy 用来做轨迹平滑。lap 是线性分配库,ByteTrack 匹配阶段会调用。参数上,如果机器没有独显,装 CPU 版 torch 也能跑,但帧率会掉到个位数,实时流场景建议至少有一块入门级显卡。
2.3 用 YOLO 加 ByteTrack 跑出带 ID 的轨迹
这一步的目标是拿到每一帧里每个人的 ID 和中心点坐标。不要一上来就写越线,先把轨迹打出来看。
import cv2 from ultralytics import YOLO # 加载检测模型,n 版最轻,适合实时 model = YOLO("yolov8n.pt") cap = cv2.VideoCapture("input.mp4") # 换成你的 RTSP 地址也可以 track_history = {} # 存每个 ID 的历史中心点 while cap.isOpened(): ret, frame = cap.read() if not ret: break # persist=True 让跟踪器在帧间保持状态 results = model.track(frame, persist=True, classes=[0], verbose=False) if results[0].boxes.id is not None: boxes = results[0].boxes.xywh.cpu().numpy() ids = results[0].boxes.id.int().cpu().numpy() for box, tid in zip(boxes, ids): cx, cy = int(box[0]), int(box[1]) # 每个 ID 保留最近 30 个点,够判断方向 track_history.setdefault(tid, []).append((cx, cy)) if len(track_history[tid]) > 30: track_history[tid].pop(0) cv2.imshow("frame", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()逻辑说明:model.track 内部已经串好了检测和 ByteTrack,classes=[0] 只保留人这一类,避免车、动物干扰。persist=True 是关键,不加的话每帧跟踪器重置,ID 全乱。track_history 用字典按 ID 存点,限制长度防止内存涨。参数上,xywh 是中心点加宽高,取前两位就是中心坐标。如果画面里人很小,把模型换成 yolov8s 或 yolov8m,召回会好一些,代价是帧率下降。
2.4 越线判定的三种写法与选择
拿到轨迹后,判断上下行的常见写法有三种。第一种是线段相交法:定义一条计数线,判断连续两个轨迹点组成的线段是否与计数线相交,相交时根据方向累加。第二种是区域法:在线上方和下方各划一个区域,ID 从上方区域进入下方区域记下行,反之记上行。第三种是投影法:把轨迹点投影到计数线的法向量上,看投影值的符号变化。线段相交法最直接,适合直线通道;区域法抗抖动好,但区域边界要调;投影法对斜线场景更稳。我一般用线段相交法起步,抖动大再叠加区域确认。
def cross_line(p1, p2, line_start, line_end): """判断线段 p1-p2 是否与计数线相交,并返回方向""" def ccw(a, b, c): return (c[1] - a[1]) * (b[0] - a[0]) > (b[1] - a[1]) * (c[0] - a[0]) a, b = line_start, line_end # 标准线段相交判定 if ccw(p1, a, b) != ccw(p2, a, b) and ccw(p1, p2, a) != ccw(p1, p2, b): # 用 y 方向变化判断上下行,画面坐标系 y 向下 return "down" if p2[1] > p1[1] else "up" return None逻辑说明:ccw 判断三个点的时针方向,两个条件同时成立说明两条线段真正交叉。方向用 y 坐标变化判断,因为图像坐标系原点在左上角,y 增大是向下。参数上,line_start 和 line_end 是你画的计数线两端,建议横跨整个通道,不要留缺口。注意这个函数只判断相交,实际使用时要配合“该 ID 之前没计过数”的状态位,否则一个人在线上来回走会反复触发。
3. 把计数结果做稳:状态机、去重与参数调优
3.1 用状态位防止同一个 ID 重复计数
越线判定最容易被忽略的是重复触发。同一个人走过线,连续几帧的轨迹点可能多次满足相交条件,计数直接翻倍。解决办法是给每个 ID 维护一个状态:未越线、已越线。一旦判定越线并累加,立刻把该 ID 标记为已计数,后续帧不再处理。如果人折返再次穿线,是否算第二次,取决于业务。门店客流通常折返不算新客,通道统计则可能要求双向都记。我一般加一个冷却时间,同一个 ID 在 2 秒内不重复计数。
counted_ids = {} # 记录 ID 上次计数的时间戳 import time def should_count(tid, direction, cooldown=2.0): now = time.time() last = counted_ids.get(tid) if last is None or now - last > cooldown: counted_ids[tid] = now return True return False逻辑说明:counted_ids 存 ID 和上次计数时间,冷却期内直接返回 False。参数 cooldown 按场景调,人流快可以设 1 秒,慢可以设 3 秒。注意这个字典要定期清理,否则长时间运行会积累大量过期 ID,内存缓慢上涨。可以每十分钟扫一遍,删掉超过冷却时间三倍的条目。
3.2 计数线位置与角度的四个必调参数
计数线不是随便画的。位置太靠画面边缘,人还没完整出现就被计数;角度太斜,并排走的人可能被线切到不同侧。下面这张表是我调参时固定会看的四个量。
| 参数 | 含义 | 建议值 | 调不好会怎样 |
|---|---|---|---|
| 线位置 | 计数线在画面中的 y 坐标 | 画面高度 40% 到 60% | 太靠上漏计,太靠下误计 |
| 线角度 | 计数线与水平方向夹角 | 0 到 15 度 | 角度大时并排人方向判断乱 |
| 轨迹长度 | 判断方向用的历史点数 | 15 到 30 点 | 太短方向抖动,太长反应迟钝 |
| 冷却时间 | 同 ID 重复计数间隔 | 1 到 3 秒 | 太短重复计,太长漏计折返 |
线位置的选择逻辑是:人从画面边缘走到线,需要有一段完整轨迹才能判断方向,所以线不能贴边。角度尽量与人的运动方向垂直,这样相交判定最干净。轨迹长度和帧率有关,25 帧的视频里 20 个点大约 0.8 秒,足够看出方向。冷却时间要大于人走过线所需时间,小于正常折返间隔。
3.3 轨迹平滑:去掉检测框抖动带来的假越线
检测框中心点会抖,尤其是人停下或遮挡时,中心点可能来回跳,导致轨迹在计数线附近反复穿越。常见做法是对中心点做滑动平均或卡尔曼滤波。滑动平均简单,几行代码;卡尔曼更稳,但要调过程噪声和观测噪声。我一般先用滑动平均,窗口取 5,效果不够再上卡尔曼。
from collections import deque class SmoothTrack: def __init__(self, window=5): self.points = deque(maxlen=window) def update(self, point): self.points.append(point) # 对 x 和 y 分别取平均 xs = [p[0] for p in self.points] ys = [p[1] for p in self.points] return sum(xs) / len(xs), sum(ys) / len(ys)逻辑说明:deque 固定长度,自动丢弃旧点。每次 update 返回平滑后的坐标,用它替代原始中心点做越线判断。参数 window 越大越平滑,但延迟越高,5 到 7 是常用范围。注意平滑只用于越线判定,画框显示还是用原始坐标,否则框会看起来滞后。
4. 避坑与排查:上下行计数翻车的五个现场
4.1 现象:人数只增不减,上行下行都是正数
原因:方向判断只用了 y 坐标变化,但画面里人可能横向走,y 几乎不变,或者计数线是竖线,y 变化不能代表方向。解决:方向判断要和计数线方向绑定。横线用 y 变化,竖线用 x 变化,斜线用投影。把 cross_line 里的方向逻辑改成基于计数线法向量的投影符号,通用性最好。
4.2 现象:同一个人被分配了多个 ID,计数偏多
原因:跟踪器 ID 切换,常见于遮挡、快速移动、低帧率。解决:先确认检测框质量,把置信度阈值从默认 0.25 提到 0.4 到 0.5,减少低分框干扰。再检查跟踪器参数,ByteTrack 的 track_buffer 可以适当加大,给遮挡后重匹配留时间。如果还不行,换 DeepSORT 并调外观特征权重。另外,画面分辨率太低也会导致 ID 跳,尽量保证人的高度在 80 像素以上。
4.3 现象:并排走的两个人被计成一个
原因:检测框重叠,NMS 把其中一个框抑制掉了。解决:调低 NMS 的 IoU 阈值,让重叠框保留更多。YOLO 里可以设 iou=0.5 甚至 0.4。同时确认模型输入尺寸,尺寸太小两个人挤在一起特征分不开,把 imgsz 从 640 提到 960 或 1280,代价是速度下降。如果并排非常紧,考虑用姿态估计或分割,但那是另一个量级的成本。
4.4 现象:计数线附近有人停留,数字乱跳
原因:人在线上来回走,轨迹反复穿越。解决:加停留判定,如果 ID 在计数线附近一定范围内停留超过阈值时间,暂时冻结该 ID 的越线判断,等人离开再恢复。或者用区域法替代线段法,要求 ID 必须从一侧区域完整进入另一侧区域才计数,单纯在线上晃不算。
4.5 现象:跑几分钟后帧率骤降,内存上涨
原因:track_history 和 counted_ids 只增不减,轨迹点越存越多。解决:track_history 每个 ID 限制长度,已经做了;counted_ids 定期清理,超过冷却时间三倍的条目删掉。另外,如果用的是 imshow 调试,记得在循环里处理窗口事件,否则 GUI 队列会堆积。长时间运行建议关掉 imshow,把结果写视频或推流。
5. 从能跑到好用:验证计数准确率的一个笨办法
计数准不准,不能靠眼睛看画面估。我一般用一段已知人数的视频做回归测试:找一段通道视频,人工数出上行和下行各多少人,作为真值。然后跑脚本,把输出和真值对比,算误差。误差超过 10% 就回去查是漏检还是重复计。漏检看检测召回,重复计看 ID 切换和冷却逻辑。这个笨办法比任何感觉都可靠。
验证时还有一个技巧:把计数线画在视频上,同时把每个 ID 的轨迹用不同颜色画出来,越线瞬间用红点标记。导出这段可视化视频,逐帧看红点是不是落在人真正过线的位置。如果红点提前或滞后,说明轨迹平滑窗口或冷却时间需要调。如果红点出现在人还没走到线的地方,说明计数线位置太靠上。
# 验证脚本核心:把越线事件写进日志,方便和人工计数对账 import csv with open("count_log.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["timestamp", "track_id", "direction"]) # 在越线判定成功处调用 # writer.writerow([time.time(), tid, direction])逻辑说明:日志记录每次计数的时刻、ID 和方向,跑完后按方向分组计数,和人工真值对比。参数上,时间戳用秒级就够,ID 用来排查重复。这个日志在调试阶段比 print 好用,因为可以排序、去重、统计。
我自己的习惯是,任何计数项目上线前,先用三段不同光照、不同人流密度的视频各跑一遍,误差都在 5% 以内才敢放到实时流上。实时流还要额外看延迟,从人过线到计数更新,超过 1 秒在门店场景就有点迟钝了。最后提醒一句,计数线不要画在门框正下方,那里人进出遮挡最严重,往通道里退半米,轨迹完整了,数字才稳。希望帮到你。
本文还有配套的精品资源,点击获取