☰
YOLOv11+多目标跟踪:零售客流统计与热力图生成实战
2026/10/6 1:11:10 网站建设 项目流程

简介:这份PDF文档面向零售行业技术人员、计算机视觉初学者及希望落地智能客流分析的开发者,系统讲解如何用YOLOv11实现多目标跟踪并生成店铺热力图。内容从零售客流统计的重要性与现有方案局限切入,逐步展开YOLOv11骨干网络、颈部网络与检测头架构,基于检测的跟踪方法与目标关联算法,再到核密度估计、网格划分等热力图生成原理,并给出小型便利店、中型超市、大型购物中心三类实战案例与效果对比。资源包共1个PDF文件,大小约2.03MB,支持目录章节跳转与阅读器左侧大纲快速定位,33页内容完整、图表清晰。目前已有114人学习。读者可借此掌握从硬件选型、系统架构设计到算法优化、性能评估的完整链路,获得可直接参考的项目搭建思路与排错经验,适合作为零售智能化改造的技术指南。

1. 零售客流统计为什么总在“数人头”这一步翻车

做过门店数字化的人多半有过这种经历:摄像头装好了,模型也跑起来了,可一到晚高峰、逆光门口、顾客交叉走动的时候,计数就开始飘。明明只进来三十几个人,系统报出五十多;更离谱的是同一个人被反复计数,热力图上一片通红,实际那块区域根本没站几个人。零售业客流统计的核心难点从来不是“检测到人”,而是“在时间轴上把同一个人认成同一个人”,也就是多目标跟踪。YOLOv11 负责把每一帧里的人框出来,跟踪算法负责给每个人分配稳定 ID,最后把 ID 的轨迹点累积成热力图,才能回答店长真正关心的问题:哪个货架被停留最多、哪个通道被快速穿过、收银台前排队有多长。这套链路适合有 Python 基础、手上有几路门店摄像头、想自己搭一套可解释客流系统的工程师,也适合被商业客流盒子报价劝退、想先跑通最小闭环的团队。下面按“检测→跟踪→热力图→避坑→调优”的顺序,把我在实际项目里踩过的路讲清楚。

2. YOLOv11 检测层:从权重到门店画面里的稳定人框

2.1 为什么客流场景优先选 YOLOv11 而不是更重的检测器

门店客流统计对检测器的要求很具体:速度要能撑住多路视频实时或准实时,召回要能覆盖被货架遮挡半身的人,框的抖动要小,否则跟踪 ID 会频繁切换。YOLOv11 在 Ultralytics 体系里属于开箱即用、文档和社区踩坑记录都比较多的一档,权重文件小、推理速度快,在 1080p 门店画面里用 TensorRT 或 ONNX Runtime 加速后,单卡跑 4 到 8 路是常见做法。相比两阶段检测器,它少了候选框阶段,延迟更可控;相比更老的 YOLO 版本,它的 neck 结构对小目标和遮挡场景更友好一些。这里不展开网络结构图,只讲落地时真正影响计数的几个点:输入分辨率、置信度阈值、NMS IoU 阈值、以及是否只保留 person 类。

2.2 用 Ultralytics 跑通单帧检测的最小命令

先确认环境,Python 3.9 以上,装 ultralytics 和 opencv:

pip install ultralytics opencv-python

然后写一个最小检测脚本,只保留 person 类,输出框和置信度:

from ultralytics import YOLO import cv2 # 加载官方预训练权重,第一次运行会自动下载 model = YOLO("yolo11n.pt") # 门店画面建议用 960 或 1280,太小会漏掉远处的人 results = model.predict( source="store_gate.mp4", imgsz=960, conf=0.35, # 客流场景不要设太高,否则遮挡的人会丢 iou=0.5, # NMS 阈值,人多拥挤时可适当降到 0.45 classes=[0], # COCO 里 0 是 person stream=True, # 视频流式推理,省内存 verbose=False ) for r in results: for box in r.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = float(box.conf[0]) # 这里可以把框和 conf 传给跟踪器 print(f"person {conf:.2f} at {x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f}")

逻辑说明:stream=True让视频逐帧处理,不会一次性把整段读进内存;classes=[0]过滤掉其他类别,减少后处理负担;conf=0.35是门店场景比较稳的起点,太高会漏掉被货架挡一半的人,太低会把模特和海报误检成人。参数上,imgsz直接决定小目标召回,门口远景多就上 1280,算力不够再降到 960。iou在人群密集时调低一点,能减少同一个人被多个框重复检出的情况。

2.3 用自己的门店数据微调时,标注和训练要注意什么

如果预训练权重在你的门店里误检明显,比如把反光地面的人影、模特假人检成顾客,就需要用自己的数据微调。标注只标 person,框要贴紧人体可见部分,被遮挡超过一半的可以不标,否则跟踪阶段会引入噪声。训练命令常见写法:

yolo detect train data=store_person.yaml model=yolo11n.pt epochs=80 imgsz=960 batch=16

store_person.yaml里写清 train、val 路径和names: {0: person}。epochs 不用太大,门店场景通常 60 到 100 轮就够,重点看验证集上的召回和误检。训练完导出 ONNX 或 TensorRT 再部署,推理速度会明显好于直接跑 pt。

3. 多目标跟踪层:让同一个人在整个进店过程里只算一次

3.1 ByteTrack 和 BoT-SORT 在客流场景的取舍

检测框有了,接下来是给每个人分配 ID。常见做法是 ByteTrack 或 BoT-SORT,Ultralytics 里直接支持model.track()。ByteTrack 的思路是把高分框和低分框分两轮关联,对遮挡后重新出现的人比较友好;BoT-SORT 加了 ReID 特征和相机运动补偿,ID 更稳,但计算量更大。门店摄像头通常固定安装,相机运动补偿用不上,如果算力紧张,ByteTrack 是性价比更高的选择;如果门店里顾客交叉走动特别频繁、ID 切换严重,再考虑 BoT-SORT 或额外接一个轻量 ReID 模型。

3.2 用 model.track 跑通带 ID 的客流计数

from ultralytics import YOLO import cv2 model = YOLO("yolo11n.pt") # persist=True 让跟踪器在视频帧之间保持状态 results = model.track( source="store_gate.mp4", imgsz=960, conf=0.35, iou=0.5, classes=[0], tracker="bytetrack.yaml", # 也可换成 botsort.yaml persist=True, stream=True, verbose=False ) seen_ids = set() for r in results: if r.boxes.id is None: continue for box, tid in zip(r.boxes, r.boxes.id): tid = int(tid) if tid not in seen_ids: seen_ids.add(tid) # 这里可以结合越线判断是进店还是出店 print(f"new track id {tid}")

逻辑说明:persist=True是关键,不加的话每帧跟踪器都会重置,ID 会乱跳。r.boxes.id就是跟踪器分配的 ID,用它去重就能得到进店人数。实际做进出统计时,还要在画面里画一条虚拟线,判断 ID 的轨迹是从外到内还是从内到外,只统计单向穿越,否则同一个人进出两次会被算两次。

3.3 越线计数和去重逻辑怎么写才不飘

越线计数常见做法是取框的中心点,记录上一帧中心点和当前帧中心点,判断是否跨越预设线段。伪代码思路:

def cross_line(prev_pt, curr_pt, line): # line 由两个端点定义,判断两帧中心点是否在 line 两侧 side_prev = (line[1][0]-line[0][0])*(prev_pt[1]-line[0][1]) - (line[1][1]-line[0][1])*(prev_pt[0]-line[0][0]) side_curr = (line[1][0]-line[0][0])*(curr_pt[1]-line[0][1]) - (line[1][1]-line[0][1])*(curr_pt[0]-line[0][0]) return side_prev * side_curr < 0

参数上,线段位置要放在门口正中,不要贴着画面边缘,否则顾客在门口徘徊时容易反复触发。去重方面,给每个 ID 记录是否已经计过数,计过就不再计;同时设置一个超时时间,比如 ID 消失超过 3 秒后从活跃列表移除,避免内存一直涨。

4. 热力图生成:把轨迹点变成店长看得懂的停留分布

4.1 热力图到底该累积什么数据

热力图不是把检测框直接画上去,而是把每个人在画面里的位置按时间累积。常见做法是取框的底部中心点,也就是脚的位置,作为人在平面上的投影点。每帧对每个活跃 ID 取一个点,累积到一张和画面同尺寸的浮点矩阵里,最后做高斯模糊和颜色映射。这样得到的热力图能反映顾客在哪些区域停留久、哪些区域只是路过。如果直接用框中心,人在弯腰或伸手时点会偏移,脚点更稳。

4.2 用 OpenCV 累积轨迹点并生成热力图

import cv2 import numpy as np # 假设画面 1920x1080,先建一张累积矩阵 heat = np.zeros((1080, 1920), dtype=np.float32) def add_point(heat, x, y, radius=25): # 在脚点位置加一个高斯核,比单像素更平滑 cv2.circle(heat, (int(x), int(y)), radius, 1, -1) # 在跟踪循环里,对每个活跃 ID 的脚点调用 add_point # 处理完一段视频后归一化并上色 heat_blur = cv2.GaussianBlur(heat, (0, 0), sigmaX=35) heat_norm = cv2.normalize(heat_blur, None, 0, 255, cv2.NORM_MINMAX) heat_img = cv2.applyColorMap(heat_norm.astype(np.uint8), cv2.COLORMAP_JET) # 叠加到原图上看效果 overlay = cv2.addWeighted(original_frame, 0.6, heat_img, 0.4, 0) cv2.imwrite("heatmap.jpg", overlay)

逻辑说明:radius控制热力点的扩散范围,太小会变成散点,太大整个画面糊成一片,门店场景 20 到 30 像素比较合适。sigmaX=35是高斯模糊参数,让热力过渡自然。归一化用NORM_MINMAX把累积值映射到 0 到 255,再套 JET 色图。如果只想看某个时间段的热力,比如下午 2 点到 4 点,就把这段时间的轨迹点单独累积,不要全天混在一起。

4.3 把像素热力图映射到门店平面图

店长真正想要的是“货架 A 前停留最多”,而不是一张摄像头视角的热力图。常见做法是做透视变换,把画面里的地面区域映射到门店平面图坐标系。先在画面里选四个地面参考点,再在平面图上选对应四点,用cv2.getPerspectiveTransform求变换矩阵,把脚点变换到平面图坐标再累积。这样生成的热力图可以直接叠在门店布局图上,哪个货架热一目了然。注意参考点要选在同一水平面上,否则映射会歪。

5. 客流统计落地避坑:从 ID 跳变到热力图糊成一片

5.1 同一个人 ID 频繁切换

现象:顾客在货架间走动时,跟踪 ID 从 12 变成 45 又变成 78,导致进店人数被重复统计。原因通常是遮挡后检测框丢失,或者相邻两人框重叠严重,跟踪器关联失败。解决:先把conf从 0.35 降到 0.25 试试,让遮挡时框不要丢;再把iou从 0.5 降到 0.45,减少重叠框;如果还不行,换 BoT-SORT 或加一个轻量 ReID 做外观匹配。门店摄像头角度尽量俯视,减少人与人之间的遮挡。

5.2 热力图在门口区域异常亮

现象:热力图上门槛位置红得发紫,但实际顾客只是路过。原因是门口区域顾客停留时间短但人数多,累积值自然高;另外如果越线逻辑没做好,同一个人来回走会被反复加点。解决:对每个 ID 在单位时间内的加点做上限,比如每秒最多加 2 个点;或者对门口区域做时间衰减,只保留停留超过一定时长的轨迹点。热力图要反映停留,不是反映经过。

5.3 夜间或逆光时检测召回骤降

现象:白天计数正常,晚上或门口逆光时人数明显偏少。原因是画面对比度低,模型漏检。解决:在预处理阶段做直方图均衡或 Gamma 校正,把暗部提亮;如果门店灯光固定,可以用夜间数据微调模型;另外把imgsz提到 1280,小目标召回会好一些。不要指望一个模型全天候通吃,分时段用不同阈值是常见做法。

5.4 多路视频跑在同一张卡上帧率掉到个位数

现象:单路跑得好好的,加到 4 路后每路只有 5 帧,跟踪 ID 开始乱跳。原因是检测和跟踪都在抢 GPU。解决:先把模型导出 TensorRT 或 ONNX,推理速度通常能翻倍;再用批处理把多路帧拼成一个 batch 送进模型;如果还不够,把检测频率降到每 2 帧或每 3 帧一次,中间帧用跟踪器预测位置。客流统计不需要每帧都检测,5 到 10 帧的更新率足够。

5.5 热力图累积矩阵内存一直涨

现象:跑几个小时后程序变慢甚至崩掉。原因是每个 ID 的轨迹点都存着没释放。解决:给每个 ID 设一个活跃超时,比如连续 30 帧没检测到就移除;热力图矩阵用固定尺寸,不要按 ID 数量动态扩;如果要做长时间统计,定期把热力图归一化后存下来再清零,不要一直累加原始值。

6. 把客流数据用起来:从热力图到停留时长和转化分析

跑通检测、跟踪、热力图之后,真正有价值的是从轨迹里再挖一层。我一般会在每个 ID 的轨迹上算三个指标:停留时长、移动距离、区域停留次数。停留时长的算法很简单,对每个 ID 记录进入某个预设区域的时间和离开时间,差值就是停留时长。区域可以用多边形定义,比如货架前、收银台前、促销堆头前。移动距离用相邻帧脚点的欧氏距离累加,能区分“快速穿过”和“慢慢逛”。区域停留次数则统计 ID 进入和离开同一区域的次数,次数多说明顾客反复看。

这些指标和热力图叠在一起,就能回答店长更具体的问题:热力图上最红的货架,平均停留时长是多少;收银台前热力不低,但停留时长很短,说明排队动线有问题;某个通道热力很淡,但移动距离长,说明顾客只是路过没停留。我习惯把每个区域的停留时长做成表格,按天对比,看调整陈列后有没有变化。

区域平均停留时长停留人次移动距离均值
货架 A18 秒426.2 米
货架 B7 秒359.8 米
收银台45 秒283.1 米
促销堆头12 秒195.4 米

验证方法上,我会用人工计数做一次对照:挑一段 10 分钟的视频,人工数进店人数,和系统输出比,误差在 5% 以内才算可用。如果误差大,先查越线位置是不是太靠边,再查 ID 切换次数。热力图的验证更主观,但可以拿平面图对照,看红色区域是不是和实际顾客常去的货架一致。

一个具体技巧:把热力图按小时切片,而不是只出一张全天图。早高峰、午间、晚高峰的顾客动线往往完全不同,全天图会把它们平均掉,看不出问题。按小时出图后,经常能发现某个货架只在特定时段热,调整陈列或排班就有依据。

我自己踩得最狠的一次坑,是热力图跑出来门口一片红,以为是顾客多,结果发现是跟踪器把门口经过的路人也算进去了,而越线逻辑只判断了方向没判断停留。后来加了停留时长过滤,只保留在店内停留超过 5 秒的 ID 才加点,热力图才真正反映店内行为。做客流统计,检测和跟踪只是手段,最后一定要回到“这个数据能不能支撑一个门店决策”上。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询