商场做客流统计这件事,听起来简单,真做起来坑不少。早几年用红外对射或者WiFi探针的方案,精度和抗干扰能力都一言难尽。现在基于深度学习的目标检测技术成熟了,用YOLO系列去做视觉客流统计成了主流方案,精度高、部署灵活,还能顺带拿到行人轨迹这类额外数据。这篇文章就从一个实际落地项目的角度,把基于YOLOv5/v8/v10构建商场客流智能统计系统的全流程拆开讲清楚,包括数据集怎么搞、模型怎么选怎么练、追踪计数逻辑怎么做,以及最后怎么套一个能看的UI界面上去。内容偏实操,适合有Python基础、想拿视觉检测做完整项目的开发者参考。
1. 方案选型与整体架构设计
1.1 YOLOv5/v8/v10怎么选
YOLO系列现在版本多得让人眼花缭乱,刚接触的人很容易卡在第一步:到底用哪个版本?我给出的建议很直接——主力用YOLOv8,特殊场景看需求切换到v5或v10。
YOLOv5胜在生态成熟,网上资料最多,很多老项目都基于它,如果你要维护旧代码或者参考历史方案,v5是必须会的。但v5的CSPDarknet结构相对老了,同等精度下推理速度不如v8优化得好。
YOLOv8是目前工程落地的甜点版本。Ultralytics官方维护,API统一,训练、验证、导出一条龙,而且Anchor-Free的检测头设计让它在小目标(比如距离摄像头较远的行人)上表现更好。商场客流场景里,人通常是密集的、尺度和姿态变化都很大,v8的泛化能力实测下来比v5稳。
YOLOv10重在最极致的推理速度。它去掉了NMS后处理,端到端部署很干净,适合在Jetson这类边缘设备上跑极限帧率。但v10刚出来没多久,周边工具链没有v8成熟,我在实际项目里用它做过一次尝试,速度确实快,但训练调参的坑比v8多,新手不建议直接上。
我这套系统的选型逻辑:训练阶段用YOLOv8m,部署阶段按设备算力导出FP16或INT8量化模型。如果现场只有老旧GPU或者纯CPU,再考虑用YOLOv5s或者YOLOv10n做降级方案。
1.2 系统整体架构与模块划分
一个能实际运行的客流统计系统,最少包含四个模块:视频接入层、检测追踪层、计数逻辑层、可视化展示层。
视频接入层负责拉取摄像头RTSP流或者读取本地视频文件,解码成帧交给检测模块。这里要注意,商场摄像头一般是大华或海康的,RTSP流如果没配置好会产生花屏和卡顿,这种问题跟模型精度没关系,但排查起来很头疼,后面我会专门讲这块的坑。
检测追踪层就是YOLO系列模型做行人检测,再配合跟踪算法给同一个行人分配稳定的ID。客流计数不能只看单帧检测结果,不然一个人走过去会被重复计数,所以必须加跟踪。
计数逻辑层是整个系统的灵魂。常见的做法是虚拟线圈——在画面上画一条线,跟踪目标跨线时触发计数。这个方案逻辑简单、实时性高,而且好调阈值,商场出入口场景够用。更复杂的做法是计算轨迹方向,比如从摄像头画面左侧走到右侧记为“进”,反过来记为“出”,这在双通道门禁场景很实用。
可视化展示层有两种主流选择:基于PyQt/PySide的桌面应用或者基于Flask/FastAPI的Web界面。桌面端适合单机部署,直接接显示器看实时画面;Web端适合把数据暴露给商场运营团队,手机浏览器就能看。我项目里用的是Web方案,因为需求方要同时给财务、运营、安保三个部门开放查看权限。
2. 数据集构建与预处理
2.1 数据采集的工程细节
数据集质量直接决定模型精度天花板。很多人拿COCO的person类直接训,效果却不好,问题就出在场景分布差异上——COCO里的行人大多是户外自然场景,跟商场室内灯光、俯视角度、密集人群差别很大。
正确做法是去目标场景采集原始视频。我当时的做法是找了A同学的实战课模拟场地,另外还用了几段公开的商场监控视频片段。采集时要注意:
- 覆盖时间段:早中晚的光线不同,商场的灯光角度、自然光比例都会变。尽量采集早上10点、下午3点、晚上8点三个时段的视频,保证光照多样性。
- 覆盖摄像机角度:出入口的摄像头有高有低、有的正对着人走的方向、有的是侧向视角。角度不同,行人的形态差异非常大。至少要采集两种典型角度的数据。
- 覆盖人流量等级:要刻意拍一组平时的稀疏客流,再拍一组促销日或者节假日的密集客流。密集场景下人挤人,贴合边界严重,这是模型最容易翻车的地方。
如果现场采集不方便,我的后备方案是:用公开的MOT挑战赛数据集(比如MOT17)里截取行人帧,再通过风格迁移或者图像融合把户外行人“贴”到商场背景里。这种方法能快速扩充数据量,但要注意不要让贴图痕迹太重,否则模型会学到错误的纹理特征。
2.2 标注规范与工具选择
数据集格式推荐直接做YOLO格式的TXT标注,一行一个目标,内容是“类别的索引 x_center y_center width height”,坐标都是归一化到0~1的。标注工具我用的是LabelImg,虽然界面老旧了点,但胜在稳定、上手零成本。XAnyLabeling这种新工具也支持自动预标注,适合处理大规模数据,但我自己的经验是:全人工标注虽然慢,但质量可控,尤其是对边界模糊的目标,人眼判断更可靠。
标注时最容易翻车的点是类别的定义。有人会把person、customer、passenger这种类别全建一遍,结果网络被搞糊涂了。商场客流统计只需要一个类别:person(或者如果你还要顺带统计员工,可以加一个staff类,但要保证标注样本数量足够多)。类别越少,模型越好训,这是铁律。
关于是否要标注“被遮挡只露出一半的人”这个问题,我的建议是:一半以上身体可见的都要标。如果完全不标,模型看到遮挡目标就会漏检;如果标得太多,又会让模型对大面积遮挡目标产生误检。折中方案是,在训练数据里保留约20%的遮挡样本,标注时统一标准:露出头部和肩部的算正样本。
标注规模上,我建议至少准备5000张有效标注帧。如果从30分钟的视频里抽帧,大概能抽到3000~4000张,加上增强扩样,基本够用。不要追求数量上的虚高,每张图平均标注人数在5~20人的数据,比每张图只有1~2人的数据有价值得多,因为密集场景的拥挤度分布wisdom直接决定模型上限。
2.3 数据增强策略
YOLOv8自带的增强已经很强了,默认配置里有马赛克(Mosaic)、随机仿射变换、HSV色域变换等。对于商场场景,我要额外注意两点:
第一,不要开太大的水平翻转。商场摄像头如果是装在出入口正上方,画面里人的左右朝向是有实际意义的——左边是门内,右边是门外。盲目开水平翻转会让模型分不清左右方向,影响后面方向判定逻辑。如果计数的核心是跨线而不是方向,那翻转可以开;但你要做进出分流计数,我建议关掉或者只在部分数据上开。
第二,马赛克增强对密集人群有奇效。Mosaic把四张图拼在一起,相当于变相增加了每张图的密度,模型能提前适应“人多到爆炸”的情况。v8的训练默认会自动用这个策略,想调节比例可以查看ultralytics配置文件的mosaic参数。
还有一个我在预处理时踩过坑的细节:不要用太高的输入分辨率。YOLOv8默认训练分辨率是640x640,如果你觉得检测小目标不够准,尝试提升到960或1280确实有效,但训练时间会暴增,而且部署时输入尺寸必须跟着改。商场的1080p画面缩小到640x640,行人大约只有30~60像素高,这个尺寸对YOLOv8来说完全够识别了,没必要盲目上高分辨率。
3. 模型训练全流程与参数调优
3.1 环境配置与训练脚本
训练环境建议直接上显卡,最低6GB显存起步。数据标注是在某台老台式机上做的,CPU是十年前的水平,但训练和推理根本跑不动,后来换了云GPU环境,每天租用的成本比买卡划算得多。哪怕你手头有卡,我也建议至少先本地把环境配好,跑通一次训练流程再转云。
依赖安装没什么好说的,pip install ultralytics一条命令搞定。需要注意版本锁定,ultralytics升级很频繁,我用的是8.2.x版本,训练和导出都稳定。如果新版本出问题,直接回退到8.1.x大概率能救回来。
训练脚本非常简洁,用官方API就能启动:
yolo train data=config/data.yaml model=yolov8m.pt epochs=150 imgsz=640 batch=16 device=0关键全在config/data.yaml里,结构如下:
train: /path/to/dataset/images/train val: /path/to/dataset/images/val nc: 1 names: ['person']一定要检查路径写的是绝对路径还是相对路径,我吃过亏——在本地写的是相对路径,换到云服务器上训练直接报错找不到数据。
3.2 训练参数设置逻辑
训练时最核心的超参数是epochs和batch。epochs我建议至少120起步,配合早停(patience=20)机制,模型训练到后期精度提升会越来越慢,早停会自动截断,避免白烧显卡。batch则看显存,如果训练时报CUDA Out of Memory的错误,把batch调小,或者开启梯度累积(accumulate参数)。
初始学习率使用默认值就行。v8的默认初始lr是0.01,配合warmup和余弦退火调度,整体曲线比较平滑。如果你想更快收敛到低loss,可以调高到0.02,我有次实验确实缩短了10%的训练时长,但最终mAP差不多,没必要为了节省一点点时间增加风险。
图像大小imgsz前面说过,这里再强调一遍部署一致性:训练和推理的分辨率如果差太多,精度会有明显下降。最好是训练用640,部署也把输入resize到640。
训练过程的监控主要看两个指标:训练loss曲线和验证集mAP50。loss曲线稳步下降说明在正常收敛;mAP50在0.8以上,对于单类行人检测来说就算及格;达到0.87以上,在商场场景下基本不会出现大面积漏检了。我记得当时训练到第80个epoch左右收敛,最终mAP50为0.89,mAP50-95为0.71。这组数据供参考,如果低于这个水平,先怀疑数据集而不是模型结构。
3.3 预训练权重与模型微调
从零训练YOLOv8m大约要训到天荒地老,而且效果还不如迁移学习。强烈建议加载COCO预训练权重(yolov8m.pt),然后用自己的商场数据集微调。COCO里各种类别的通用特征已经学得很好了,你只需要让网络在“行人”这个类别上去适应你场景的风格。
微调时要注意冻结部分层。YOLOv8m的身体层(比如前10层)冻结不更新,只训练检测头和后面某些特征层,好处是训练速度快、所需数据量少,特别适合数据集只有几千张的情况。用Ultralytics的freeze参数就可以,freeze=10一般是指定冻结前10层。
我实际测试下来,冻结前10层确实让训练速度提升了30%左右,而且不会掉点。因为在预训练模型上,底层特征提取器学的已经是通用视觉特征了,你场景里的光照角度变化还没到颠覆它的时候。
4. 客流计数核心逻辑:检测 + 跟踪 + 跨线判定
4.1 跟踪器的选择:ByteTrack优先
检测是单帧的事,但统计是连续帧的事。需要一个跟踪算法把不同帧里的同一个行人关联起来,保持ID稳定。
主流的跟踪方案有DeepSORT和ByteTrack两种。DeepSORT依赖外观特征提取模型(一般是ReID网络),流程复杂、模型又多一套,推理速度慢,而且外观特征在商场这种帽子口罩、衣服相似度很高的场景里经常失效——你让两个穿黑衣服的人从摄像头前走过,DeepSORT非常容易把这个人的ID换到那个人身上。
ByteTrack则是基于运动信息的优雅方案。核心思想是“谁说检测置信度低就一定不是人”——它会把低置信度的检测框也保留下来,利用高置信度匹配后的剩余框和剩余轨迹再做一轮匹配。这样做的效果是:即使模型这一帧有个目标检测置信度只有0.3(一般会被阈值卡掉),ByteTrack也能通过位置交并比把它匹配上,保持ID稳定。
我在商场场景里实测对比过,DeepSORT的ID Switch次数是ByteTrack的3倍以上。对于客流统计这种系统,ID Switch多就意味着计数错误多,所以不用犹豫,直接ByteTrack。
ByteTrack的调用不需要自己写,Ultralytics官方已经内置了,tracker配置文件指向bytetrack.yaml就行,当然后端也有官方写的参考实现。关键参数是track_buffer(追踪缓冲帧数),这个控制目标丢失后保留轨迹多久。商场里大量的人被瞬时遮挡,如果buffer设得太短,人从柱子后面出来就换新ID了;设得太长,又会增加误匹配。我调参的时候发现**track_buffer=60**是个比较稳的值,对应约2~3秒的遮挡容忍,足够应对大部分情况。
4.2 虚拟线圈跨线计数原理
拿到稳定的跟踪ID后,计数就简单了。虚拟线圈的本质是:在画面里定义一条有方向的线段,记录每个目标中心点的运动轨迹,当中心点从线段一侧运动到另一侧时,判断方向并累加计数。
具体代码逻辑我用一个简单的示例来说明:
def is_crossing_line(prev_point, curr_point, line_start, line_end): # 线段向量 line_vec_x = line_end[0] - line_start[0] line_vec_y = line_end[1] - line_start[1] # 目标运动向量 mv_x = curr_point[0] - prev_point[0] mv_y = curr_point[1] - prev_point[1] # 计算叉积判断跨线方向 side_prev = (prev_point[0] - line_start[0]) * line_vec_y - (prev_point[1] - line_start[1]) * line_vec_x side_curr = (curr_point[0] - line_start[0]) * line_vec_y - (curr_point[1] - line_start[1]) * line_vec_x return side_prev * side_curr < 0当检测到目标与虚拟线圈有交点时,再结合前后帧中心点的位置变化,就能判断是出还是进。这套逻辑很成熟,但细节在工程化:
- 线圈不要画在画面最边缘,那里人刚进入镜头,只检测到半个身体,中心点不稳,容易发生横跳。我一般把线圈画在画面中间偏出入口的位置,保证人至少有1秒的有效轨迹被跟踪到。
- 线圈宽度建议跟一个成年人的肩宽差不多,画太宽会把并行走进来的两个人算成一次穿越,画太窄又容易漏。
- 用中心点判断时,连续帧之间轨迹要做平滑滤波,否则中心点抖动会导致误判“跨了两次线”,结果一个进出动作被重复计数。最简单的办法是当检测到跨线事件后,对该ID设置200帧的冷却时间,期间不再触发计数,这是所有防重复计数的底线。
4.3 进出方向判定与高峰时段数据清洗
方向判定是商场客户比较在意的功能,除了知道“有几个人来了”,更想知道“净增加人数”是多少。这里要把进、出两个方向分开累计。
我在代码里维护两个计数器entered_count和exited_count,当目标从一个方向上跨线时递增对应方向。有个细节要注意:如果人的轨迹是先往左移动再折返往右走,这一来一回不应该被计为“进了一次又出一次”。为了应对这种情况,需要对每个目标记录它的完整运动路径,计算它在线圈附近的整体方向向量,而不是只看局部几帧的位移。简单做法是把目标第一次出现在画面里的位置和最后离开的位置(或最终位置)连成一条向量,用这个向量去跟线圈方向向量做点积。
高峰期还有个数据清洗问题:商场开门前保洁人员来回走动、铺货人员经过出入口,这些都会被算成客流。我在实际项目里会给计数结果加一个滞留时间过滤——目标如果从出现到跨线中间停留超过5秒(比如站在那里聊天的安保),就认为是非纯经过类目标,不计入客流数据。这个过滤不能直接做在检测层,最好做在计数层,让统计逻辑更灵活。
5. UI界面与可视化系统实现
5.1 技术栈选定:FastAPI + Web前端
很多人以为做UI就是硬啃PyQt,其实对于商场方这种同时多部门查看的需求,Web界面的性价比要高太多了。我用FastAPI做后端接口,前端用Vue + ECharts做数据可视化,整体开发量比PyQt小,而且跨平台、随时远程访问。
FastAPI这里有两个先天优势:一是异步事件循环天然适合对接视频流的实时推送;二是自带Swagger文档,调试接口不需要额外装工具。
后端处理视频帧的流程是这样的:摄像头帧拿到后,送进YOLOv8模型推理(同步或异步都行),得到检测框后交给ByteTrack更新ID,然后在视频帧上画线和框,最后把帧编码成JPEG推给前端,前端用img标签每秒刷新10~15次实现实时画面。这个方案比WebRTC简单多了,而且实测延迟在200毫秒以内,对看实时画面来说完全可以接受。
5.2 数据面板与图表模块
数据面板才是客户每天盯着的页面。我用ECharts做了两个核心图表:
- 某时间段的进出量柱状图:统计每半小时进、出人数,方便商场运营判断早晚高峰。数据存在后端内存里或SQLite中,前端每隔30秒拉一次接口刷新,不搞实时推送,因为半小时级别粒度的数据没必要用WebSocket。
- 实时累计差额折线图:把
entered_count - exited_count随时间变化画出来,这就是商场当前滞留人数的近似值,在消防疏散和预案制定时很有参考价值。
另外还要有一个人流量热力图模块。我把视频画面划分成网格(比如6x8),统计每个格子内检测框中心点的出现频次,叠加在画面上。哪个区域最拥挤一目了然,客户看了都说这比光看数字强得多。实现上用后端维护一个二维数组,每隔1秒把所有活跃目标的坐标刷进对应格子,再映射成热力颜色返回前端。
5.3 系统配置与告警功能
UI不光是展示,还要能配置。我在界面上做了几个实用功能:画线——操作人员可以鼠标在视频流画面上直接拖拽线条,修改虚拟线圈位置,不用重启服务;阈值设定——当某块区域人员密度超过设定值,界面弹告警提示;回放检索——按时间段重放历史计数数据,这个目前是简单实现,真的要做大型回放系统牵扯到视频存储架构,那是另一个工程了。
前端画线的实现其实不复杂:捕捉鼠标在img标签上的像素坐标,换算成模型推理输入下的坐标(注意缩放比例),存到后端配置里,下一次推理就用新线圈。前端再在画布层覆盖画一条线显示位置。
6. 模型部署、性能优化与常见问题排查
6.1 导出与推理加速
模型训练完不能直接拿.pt文件用。部署环境千差万别,最好把模型导出成更适合运行的格式。
英伟达GPU部署用TensorRT封装的engine格式。推理速度对比,我实测在RTX 3060上,YOLOv8m的PyTorch原生推理大约25毫秒/帧,转TensorRT FP16后直接压缩到8毫秒/帧,快了3倍以上。这个优化是必须做的,不然同时接4路摄像头,单卡根本忙不过来。
导出命令:
yolo export model=best.pt format=engine device=0 half=True导出时指定half=True开启FP16。如果显存紧张,还可以尝试int8=True做INT8量化,但要做校准集,否则精度可能崩掉。我的建议是:FP16是性能与质量平衡点,INT8除非极限需要,否则不碰。
如果部署机是CPU,用ONNX Runtime跑INT8量化的onnx模型,速度也还过得去,但跟GPU不是一个量级。纯CPU跑YOLOv8m在1080p上大约150毫秒/帧,也就是说每秒不到7帧,勉强能动,但做实时统计会很吃力。CPU部署优先考虑YOLOv8n或YOLOv10n。
6.2 摄像头流处理的实战技巧
商场摄像头RTSP流是最容易出幺蛾子的环节,这里单独列几个排查重点:
- 断流重连:RTSP流可能因为网络抖动或者摄像头重启而断掉。代码里必须实现自动重连机制,不然摄像头掉线一次,整个系统就挂到第二天早上,特尴尬。我建议维护一个“没收到帧超时”的定时器,超过10秒没新帧就重新尝试拉流,失败则退避重试。
- 解码延迟问题:当发现画面延迟越来越大,很大概率是OpenCV的
VideoCapture缓冲队列累积了太多帧。解决办法是设置缓冲区大小,或者在读取帧时先清空缓冲再取最新帧。 - 多路流并发:不要在主进程里每路摄像头都开一个
VideoCapture循环。正确做法是每路流一个独立线程或进程,经队列送推理。如果环境允许,多进程隔离更好,某个模型崩溃不会影响其他摄像头。
6.3 计数精度的验证与误差评估
系统上线前必须有精度验收环节,否则客户说“你数得不对”时你没有任何反驳依据。
我采用的方法是:人工抽验法。取一段15分钟的监控视频,人工数出每分钟的进出人数,然后让系统跑同样视频,对比结果。以人工计数的结果作为基准,统计系统的绝对误差和相对误差。
我们跑出来的实测数据是:平峰时段(人少)准确率能达到95%以上;高峰时段(人潮密集)准确率会掉到88%左右,误差来源主要是遮挡严重时跟踪ID切换导致的重计或漏计。这个水平在商场客流场景基本够用。
如果实测误差过大,需要分模块定位。我的一套排查思路:先看检测框有没有漏检(单帧画面里框的数量跟人眼数量是否一致),再看跟踪的ID是否稳定(同一个人的框ID是不是一直在变),最后看线圈位置和方向判定逻辑。定位到具体模块再针对性优化,千万别一上来就重新训练模型。
6.4 常见错误与避坑速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 训练时CUDA OOM | batch太大/分辨率太高 | 调小batch,使用梯度累积 |
| 推理慢,GPU利用率低 | 未转TensorRT或输入尺寸过大 | 用engine格式,及时关闭torch.no_grad() |
| 有检测框但计数没反应 | 线圈坐标与推理尺寸不匹配 | 检查缩放比例,前后端坐标保持一致 |
| 一个人来回走动反复计数 | 轨迹折返导致多次跨线 | 引入冷却时间或轨迹方向向量判断 |
| 跟踪ID频繁切换 | 检测置信度阈值过高,漏检低置信度目标 | 检测置信度降到0.25,并增大track_buffer |
| 实画面白屏 | RTSP流失败/解码异常 | 检查网络连接与摄像头在线状态,打印RTSP返回码 |
| 前端热力图很稀疏 | 网格精度设太高,目标攒不进格子 | 调小网格分辨率,加大统计频率 |
6.5 边缘设备部署与云端协同架构
有些商场要求把计算放在本地边缘盒子,视频不必上传云端,避免隐私问题。这种场景下推荐Jetson Orin Nano或者带独立显卡的Mini PC。实测Jetson Orin Nano部署TensorRT FP16的YOLOv8s模型,处理一路1080p流能做到25帧/秒左右,功耗低、体积小,放在弱电箱里很合适。
云端和边缘的协同是这样做的:边缘端负责实时推理和计数,把聚合后的数据每10秒推一份到云端;云端负责长期存储和展示报表。这样网络断了也不影响现场,因为数据都留在本地,恢复后能续传。架构上有一个细节:消息传输用MQTT协议比裸HTTP轮询更稳妥,带宽占用小,且具备断线重连的天然机制,省掉自己写心跳的麻烦。
6.6 代码工程化与异常兜底
最后想强调一个很多人忽略的问题:所有外部依赖都要加异常兜底。YOLO推理很简单,但生产环境中摄像头断流、模型推理出错、GPU内存泄漏,这些情况都要预判。
维护一个简单的日志模块,记录模型推理时间、每路摄像头状态、计数异常告警,这样凌晨一点设备报错时,你翻日志能10分钟定位问题,而不是先把系统跑一遍看现象。我的习惯是所有关键路径都加try-except,外围再套一层看门狗(看门狗负责进程崩溃后自动拉起),这比排出完美的算法代码对项目存活的意义更大。
7. 收尾前最后几句话
这个系统做下来,我的体会是:YOLO模型只占整个项目工作量的小头,真正花时间的是数据集的取舍、追踪参数的调优和计数逻辑的防重维护。模型精度再高,追踪环节做得糙,计数对不上账一样白搭。
另外一个小建议:如果有条件,做项目前先跟商场运营聊半个小时。你会发现他们真正想要的并不是“实时人数”,而是“哪个时间段该加开收银台、哪个区域容易堵人、周末和节假日的客流趋势有没有异常”。这些目标会直接影响你要画哪些页面、输出哪些指标。方向对了,技术方案才站得住。
如果对追踪计数逻辑或者界面实现细节有兴趣,欢迎一起交流,这套系统的代码整理一下还是很有参考价值的。