简介该资源包以Python结合OpenCV实现人流量统计与上下行方向计数面向计算机视觉初学者及零售、交通等场景的开发者解决实时人数统计与行人方向识别问题。内含20个文件涵盖5个Python源码、4段演示视频、2个AVI输出以及MobileNetSSD和YOLO相关模型配置caffemodel、prototxt、cfg、names并配有README说明模型与脚本相互配套便于直接运行和二次改造。压缩包约138MB已有511人学习使用。资源不仅提供people_counter.py主程序和centroidtracker.py跟踪模块还包含多段进入/离开视频与演示动图可帮助理解背景减除、轮廓检测、卡尔曼滤波等关键流程并依照上下行边界设定实现双向计数。对于想快速落地人流统计方案或学习目标跟踪原理的开发者这是一份结构完整的参考实现。1. Python人流量计数一个压缩包里的完整上下行统计方案做零售门店和中小型站点的客流统计时最头疼的不是检测不到人而是算不清方向——进店和出店的人数一旦混在一起后续的转化率分析就是一笔糊涂账。这份基于 Python OpenCV 的人流量计数资源把检测、跟踪、上下行计数串成了一条完整链路压缩包里既有 MobileNet SSD 和 YOLO 两套检测模型也有写好的people_counter.py主程序和质心跟踪模块视频输入直接跑命令就能出结果。它适合三类人刚接触计算机视觉、想快速跑通一个完整项目的新手门店或场馆需要做进出入统计、但不想从零搭系统的从业者以及想研究检测跟踪落地细节、拿现成代码改业务逻辑的开发者。接下来我从项目文件拆起把检测器选型、上下行判定逻辑、参数调优和踩坑记录一次讲透。2. 项目文件拆解检测器选型与两个入口脚本的分工把压缩包解压后第一件事不是急着跑命令而是把people-counting-opencv-master目录里的文件角色认清。这个项目的结构不算复杂但检测器部分容易让人犯迷糊——为什么既有mobilenet_ssd又有yolo-coco两者到底是什么关系。2.1 检测器选型MobileNet SSD 与 YOLO 各管什么场景mobilenet_ssd目录下的MobileNetSSD_deploy.prototxt是网络结构描述文件MobileNetSSD_deploy.caffemodel是预训练权重这是一套基于 Caffe 的 SSD 目标检测模型。MobileNet 作为骨干网络的特点是计算量小CPU 上也能跑到可用帧率。yolo-coco目录里的yolov3.cfg是 YOLOv3 的网络配置coco.names是 COCO 数据集的 80 个类别名称YOLOv3 的精度比 MobileNet SSD 高但对算力的要求也上了一个台阶。项目默认走的是 MobileNet SSD 路线people_counter.py里通过--prototxt和--model两个参数加载模型文件。检测器的作用是给每一帧画面里的人画出边界框但只做检测还不够——单帧的框 ID 在下一帧会变没法判断同一个人是否已经跨过计数线。所以项目里pyimagesearch目录下的centroidtracker.py和trackableobject.py才是上下行计数的关键。文件角色可以用一张表看清文件/目录作用是否直接运行people_counter.py完整版入口检测 跟踪 上下行计数是people_counter_initial.py初始版入口只有单方向计数逻辑是pyimagesearch/centroidtracker.py质心跟踪器跨帧关联同一目标否pyimagesearch/trackableobject.py可跟踪对象记录质心轨迹与方向否mobilenet_ssd/SSD 检测模型默认否yolo-coco/YOLOv3 检测模型备选否example_01.mp4等视频测试输入否output/输出的带计数结果的视频否2.2 两个入口脚本的差异别拿初始版当完整版用people_counter_initial.py这个文件很容易让人误以为是功能精简的性能优化版其实它是功能残缺的初始版本只实现了最基础的跨线计数没有把上行和下行分开统计。而people_counter.py里通过每个TrackableObject实例维护的direction字段把人员流向拆成了up和down两个独立计数器。我一般会建议直接用完整版但两个脚本保留的最大价值是提供了对照——你在改代码时如果发现计数逻辑异常可以先跑初始版确认检测跟踪链路是否正常再回到完整版排查方向判定部分。这种分层排查方式在调模型参数时特别实用能快速定位问题出在检测环节还是计数环节。3. 上下行计数的核心逻辑质心跟踪与方向判定人流量计数的难点不在「检测到人」而在「怎么知道这是同一个人」以及「这个人往哪个方向走」。这两个问题分别由质心跟踪器和方向判定逻辑回答。先看跟踪是怎么做的再理解上下行判定就水到渠成了。3.1 质心跟踪用欧氏距离把跨帧目标串起来centroidtracker.py做的事可以概括为三步提取当前帧所有检测框的中心点坐标计算这些质心与上一帧已知对象质心之间的欧氏距离用贪心匹配把距离最近的点对关联起来让每个对象在连续帧里拥有稳定的 ID。这段伪代码还原了它的核心思路# 核心思路帧间质心最小距离匹配 # 上一帧有已知对象当前帧有检测到的质心集合 # 遍历每个已存在对象找距离最近的当前帧质心进行绑定 for object_id, centroid in self.objects.items(): # 计算该对象与当前帧所有候选质心的欧氏距离 distances [ ((c[0] - centroid[0]) ** 2 (c[1] - centroid[1]) ** 2) ** 0.5 for c in input_centroids ] # 取距离最小的质心索引 min_idx distances.index(min(distances))代码逻辑说明这里的self.objects是上一帧确认过的目标字典input_centroids是当前帧检测器输出的所有人物边界框中心点。每一帧都对每个旧目标做一次全量距离计算然后把最小值对应的新质心分配给这个旧目标。由于距离计算量级是 O(n*m)当画面里同时出现几十个人时会有明显开销这也是后面--skip-frames参数存在的意义——隔 N 帧检测一次中间帧只做跟踪。参数说明如果视频帧里人特别密集这种贪心匹配可能出现 ID 交换问题——两个人交叉走过时跟踪器可能把 A 的 ID 换到 B 身上。这是质心跟踪的已知局限简单场景够用复杂拥挤场景需要换成 Deep SORT 这类带外观特征的跟踪器。3.2 方向判定计数线穿越法是怎么工作的trackableobject.py里的每个对象实例保存了它的质心历史坐标和方向标记。方向判定的核心是预先在画面中设定一条水平计数线代码里默认取画面高度的一半H // 2然后比较同一目标在前后两帧的质心 y 坐标# 方向判定比较前后帧质心与计数线的相对位置 # 假设计数线在画面高度一半位置 H // 2 # prev_centroid 是该目标上一帧的质心坐标 # centroid 是该目标当前帧的质心坐标 # H 为画面高度 if prev_centroid[1] H // 2 and centroid[1] H // 2: # 上一帧在线上方当前帧在下方 - 下行 total_down 1 obj.direction down elif prev_centroid[1] H // 2 and centroid[1] H // 2: # 上一帧在下方当前帧在上方 - 上行 total_up 1 obj.direction up代码逻辑说明判定条件不依赖检测框的宽度变化只看质心点的 y 坐标穿越方向。上一帧质心在计数线上方、当前帧跑到下方就判定为下行反之是上行。注意这里专门比较了上一帧和当前帧两个位置而不是只看当前帧在线的哪一侧这样能避免目标从画面边缘直接出现时被误判方向。参数说明H // 2就是计数线的默认位置这个值直接改 Y 坐标即可调整计数区域。实际场景里计数线不一定放在正中间——入口在画面上方就往下调入口在下方就往上调保证行人有一段完整轨迹可供判断。另外TrackableObject里还有个counted字段确保每个目标只被计数一次防止同一个人在线附近来回走动时被重复统计。3.3 从检测框到计数结果的数据流一条完整的计数链路是这样的视频帧输入 → SSD/YOLO 检测出多个person类别边界框 → 非极大值抑制去掉重叠框 → 提取每个框的质心 → 交给centroidtracker.py做跨帧关联 → 取出每个目标的新旧质心 → 用 3.2 的逻辑判断穿越方向 → 更新total_up/total_down计数 → 把计数结果和轨迹画到帧上 → 写入输出视频。这个流程在people_counter.py的while循环里逐帧执行。看到这里你应该明白检测器决定「人在哪」跟踪器决定「人是谁」方向判定决定「人往哪走」三者缺一不可。如果换一个单阶段检测模型只需要改模型加载部分跟踪和计数逻辑完全不用动。4. 跑通与调参命令行参数、模型切换与置信度设置代码逻辑看完了现在进入实操部分。这个项目的命令行参数设计得比较规整跑通 demo 只需要一条命令但要把参数含义和调整策略说清楚不然换自己的视频时容易翻车。4.1 一条命令跑通 example 视频假设你在项目根目录下先跑内置的example_01.mp4测试视频python people_counter.py \ --prototxt mobilenet_ssd/MobileNetSSD_deploy.prototxt \ --model mobilenet_ssd/MobileNetSSD_deploy.caffemodel \ --input example_01.mp4 \ --output output/output_01.avi \ --confidence 0.4 \ --skip-frames 30参数说明--prototxt和--model指定 SSD 的网络结构和权重--input指定输入视频路径--output指定结果视频写出路径--confidence 0.4是检测置信度阈值低于 0.4 的检测框会被丢弃--skip-frames 30表示每 30 帧做一次目标检测中间 29 帧只做质心跟踪这是性能优化的关键参数。跑完后打开output/output_01.avi应该能看到画面里每个人身边有绿色边界框顶部实时显示Up和Down的计数数字。如果输出视频是空的或者编码不兼容先看第 5 章的避坑记录多半是编码器四字符代码的问题。4.2 切换 YOLO 检测器与置信度策略SSD 跑通了再试试 YOLO 的效果。把--prototxt和--model参数替换成--yolo模式或者直接修改脚本中的模型加载分支。项目里 YOLO 的加载逻辑通常长这样python people_counter.py \ --yolo yolo-coco \ --input example_02.mp4 \ --output output/output_02.avi \ --confidence 0.5 \ --skip-frames 30--yolo参数指向yolo-coco目录脚本内部会读取yolov3.cfg和coco.names并加载权重。注意 YOLO 的置信度我建议设得比 SSD 高一些因为 YOLOv3 对小目标的召回率不如 SSD 平滑低置信度会带来大量误检框反而干扰质心跟踪。从实际效果看SSD 在 CPU 上能跑到 10~15 FPS取决于视频分辨率和机器性能YOLO 在 CPU 上基本只有 2~5 FPS精度提升有限但耗时成倍增长。我的建议是没有 GPU 就用 SSD把--confidence调到 0.3~0.4再用--skip-frames拉高帧率。4.3 实时摄像头与本地视频输入的参数差异把--input从视频路径换成0就可以读取摄像头实时流python people_counter.py \ --prototxt mobilenet_ssd/MobileNetSSD_deploy.prototxt \ --model mobilenet_ssd/MobileNetSSD_deploy.caffemodel \ --input 0 \ --output output/camera_output.avi \ --confidence 0.5 \ --skip-frames 15摄像头场景下--skip-frames要调小因为摄像头帧率不稳定隔 30 帧检测一次容易把快速走过的行人漏掉。我一般设 10~15。本地视频如果本身帧率就低--skip-frames也要相应减小否则目标在两帧之间的位移过大质心跟踪会匹配失败。另外一个容易忽略的参数是输入尺寸。people_counter.py内部会用cv2.resize把帧统一到 400 像素宽某些版本是 500如果你想保留原视频分辨率做输出需要在写VideoWriter时把帧尺寸对齐否则输出的视频是黑屏或花屏这点在避坑章节详细说。5. 常见问题与避坑输出空白、计数错乱与性能翻车跑通 demo 只是起点换真实场景的视频时问题就来了。我把实际使用中遇到的高频问题整理成踩坑记录每一条都按「现象 → 原因 → 解决」的顺序写。5.1 输出视频文件打不开或全黑现象程序跑完不报错但生成的output_01.avi用播放器打开是黑屏或者提示编码格式不支持。原因OpenCV 的VideoWriter创建时指定的编码器与实际写入的帧格式不匹配。项目里默认用cv2.VideoWriter_fourcc(*MJPG)某些环境不支持 MJPG 编码或者--output路径的目录不存在导致写入失败但程序没有抛出异常。解决先确认输出目录存在再手动指定编码器。我一般会改用 XVID 编码兼容性更好# 在 people_counter.py 中修改 VideoWriter 初始化 # 将 fourcc 从 MJPG 替换为 XVID同时确保输出目录存在 fourcc cv2.VideoWriter_fourcc(*XVID) writer cv2.VideoWriter(args[output], fourcc, fps, (W, H), True)代码逻辑说明这里的W和H是输入帧的实际宽高必须和后续writer.write(frame)写入的帧尺寸完全一致。如果你在检测前把帧缩小了写完缩小后的帧视频就会因为尺寸变化产生花屏。解决思路是「检测用小图画出结果后放大回原尺寸再写入」。5.2 计数线附近目标来回走导致重复计数现象同一个人在计数线位置徘徊Up和Down的数字交替增长明明只有一个人却统计出了四五次进出。原因方向判定只看前后两帧质心的相对位置目标在线附近来回抖动时质心可能在计数线上下反复穿越。trackableobject.py里的counted字段本来是为了防止重复计数但实现上有些版本只有在对象direction从未被赋值时才允许计数一旦方向被赋过值就不再更新所以问题出在方向被子节点反复翻转。解决连续多帧确认方向再计数而不是单帧穿越就立刻判定。常见做法是给每个TrackableObject增加一个cross_count计数方向上连续两帧穿越同一侧才真正计数# 在方向判定处增加连续帧确认逻辑 # 只有连续两帧都满足穿越条件才确认方向 if prev_y LINE_Y and cur_y LINE_Y: obj.cross_count 1 if obj.cross_count 2 and not obj.counted: obj.counted True total_down 1 elif prev_y LINE_Y and cur_y LINE_Y: obj.cross_count 1 if obj.cross_count 2 and not obj.counted: obj.counted True total_up 1 else: obj.cross_count 0 # 方向不稳定时重置累加代码逻辑说明LINE_Y是自定义计数线 y 坐标prev_y和cur_y分别是前后两帧的质心 y。只有当目标连续两帧都往同一方向穿越计数线时才确认计数并锁定counted否则重置累加。这样能显著减少抖动带来的误计。5.3 SSD 检测不到画面上方远处的小目标现象视频上半部分的人经常被漏检计数结果明显比人工数少 20% 以上。原因SSD 的输入分辨率固定远处的人占据的像素面积小检测框置信度低被--confidence 0.4的阈值过滤掉了。解决先降置信度到 0.2~0.3 观察效果如果仍然漏检考虑缩小计数区域——把计数线移到检测效果好的中近景区域或者直接对画面下半部分做检测。另一种做法是提高检测输入尺寸把people_counter.py里 resize 的目标宽度从 400 改成 600 甚至 800代价是检测耗时明显上升。5.4 CPU 跑 YOLO 帧率只有个位数视频卡成幻灯片现象用 YOLO 检测器跑 1080p 视频程序输出帧率只有 3~5 FPS输出视频像慢放。原因YOLOv3 对 CPU 的算力要求远超 MobileNet SSD这是模型本身的特性不是代码问题。解决先用--skip-frames 10观察如果帧率不足把--confidence提到 0.6 减少候选框数量再把输入尺度调小。终极方案是换回 SSD或者把 YOLO 换成轻量版如 Tiny-YOLO。不要指望纯调参让 YOLO 在 CPU 上跑实时这是模型训练侧决定的不是后处理能挽救的。5.5 模型文件路径报错或者读取不到现象运行时报错提示找不到MobileNetSSD_deploy.caffemodel或者coco.names读取失败。原因相对路径和实际工作目录不一致。很多人直接在 IDE 里运行脚本工作目录是项目根目录之外的地方导致相对路径失效。解决用绝对路径或者在脚本开头基于脚本所在目录定位文件路径# 用脚本目录拼接绝对路径避免工作目录不一致导致加载失败 # 假设 people_counter.py 在项目根目录 import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) prototxt os.path.join(BASE_DIR, mobilenet_ssd, MobileNetSSD_deploy.prototxt) model os.path.join(BASE_DIR, mobilenet_ssd, MobileNetSSD_deploy.caffemodel)代码逻辑说明os.path.abspath(__file__)拿到当前脚本的绝对路径再用os.path.join拼接模型文件路径。这样不管终端在哪个目录下执行命令都能正确加载模型。6. 进阶验证用 demo 输出校准计数线与统计精度项目里自带demo_output_01.gif和example_01.mp4这两份资料不只是给你看效果的还是校准计数精度的基准。先把example_01.mp4跑一遍把输出视频和demo_output_01.gif逐帧对照——重点看人员穿越计数线的计数时机是否一致如果 gif 里某个方向先 1你的输出里却延后了 N 帧才 1说明方向判定逻辑里的cross_count累加和 demo 版本有偏差。自定义计数线是改造成本最低、收益最高的一个操作。假设你的监控画面里入口在底部、出口在顶部把计数线从画面中间改到偏下位置就能让行人一进入画面就完成方向判定入口在顶部则往上移。具体操作是找到people_counter.py里H // 2的赋值替换成固定值同时画一条线方便肉眼校验# 自定义计数线位置示例设置在第 300 行像素处 # 并绘制红色横线用于可视化确认 LINE_Y 300 cv2.line(frame, (0, LINE_Y), (frame.shape[1], LINE_Y), (0, 0, 255), 2)我自己的习惯是先用三个不同场景的视频分别校准计数线位置和--skip-frames值记录每组参数的计数误差率。有一次在门店现场测试时发现SSD 置信度调到 0.5 后傍晚逆光场景漏检严重降到 0.3 后噪声变多但总误差反而更小——这套项目里的参数没有通用最优解只有针对具体场景的局部最优。验证完计数精度还可以把计数结果定时写到 CSV 文件里做后续分析这也是这个项目最有实战价值的扩展方向。核心是每一帧或者每 N 帧把当前累计值追加到文件末尾注意用追加模式而不是覆盖模式防止程序中断丢数据# 每隔 30 帧把计数结果追加到 CSV # 后续可以用 pandas 或 Excel 直接做客流趋势分析 import csv frame_count 0 with open(count_log.csv, a, newline) as f: writer csv.writer(f) if frame_count % 30 0: writer.writerow([frame_count, total_up, total_down]) frame_count 1代码逻辑说明newline防止 Windows 下 CSV 写入出现空行frame_count % 30控制采样频率避免每秒写入几十条冗余数据。这样积累一天的数据就能画出分时段的进出店客流曲线比单纯看视频数字直观得多。从那以后我每次拿到新的计数项目都会强制走一遍「先跑 demo 对照 gif、再改计数线位置、最后记录参数与误差率」的流程不到半小时就能判断这份代码在目标场景下值不值得往下投入。希望这份拆解能帮你少走我踩过的弯路把上下行统计真正用起来。本文还有配套的精品资源点击获取