简介:这份PDF文档面向零售行业技术人员、计算机视觉初学者及希望落地智能客流分析的开发者,系统讲解如何用YOLOv11实现多目标跟踪并生成店内热力图。内容从零售客流统计的重要性与现有方案局限切入,逐步展开YOLOv11骨干网络、颈部网络与检测头架构解析,基于检测的跟踪方法与目标关联算法,以及客流统计指标计算和实际应用案例。文档还覆盖核密度估计、插值方法、颜色映射等热力图数学原理,并给出基于密度估计与网格划分两种生成路径,最后通过小型便利店、中型超市、大型购物中心三类实战项目串联系统搭建、性能优化与效果评估。资源包为1个PDF文件,大小约2.03MB,共33页,支持目录章节跳转、阅读器左侧大纲显示与章节快速定位,图表目录显示正常。已有114人学习,适合需要完整掌握从检测跟踪到热力图可视化全流程的读者参考。
1. 从一段翻车的门店视频说起:这套 YOLOv11 客流方案到底能干什么
去年帮一个连锁便利店做数字化改造,老板指着监控室说:“我有 16 路摄像头,每天进多少人、哪个货架没人看,全靠店长拿本子记。”我调了一周的视频回放,发现最麻烦的不是检测不到人,而是同一个人被反复计数、遮挡后 ID 跳变、热力图糊成一团。后来拿到这份《零售业客流统计实战:YOLOv11多目标跟踪与热力图生成技术详解》,33 页的体量把检测、跟踪、计数、热力图这条链路完整串了起来,才意识到问题不在算法本身,而在工程细节。
这份文档面向的是真正要把系统跑起来的开发者,不是讲 YOLO 发展史的科普。它从 YOLOv11 的骨干网络、颈部网络、检测头拆到多目标跟踪的匈牙利匹配与卡尔曼滤波,再落到客流统计指标计算和核密度估计热力图生成,最后给了一套完整的系统搭建流程。适合两类人:一类是手里有门店视频、想做出可交付客流看板的工程师;另一类是做计算机视觉项目、需要把检测跟踪串成业务闭环的开发者。如果你只想要一个能跑通的 demo,文档里的代码片段够用;如果你要上线,第四、六、七章的架构和优化部分才是重点。
2. YOLOv11 检测与多目标跟踪:从单帧框到连续轨迹
2.1 为什么客流统计必须用「检测+跟踪」而不是纯检测
纯目标检测每帧独立输出边界框,帧与帧之间没有身份关联。放在客流统计场景里,这意味着同一个人在画面里停留 10 秒、被检测到 300 次,系统会认为来了 300 个人。更麻烦的是,你无法判断这个人是进店还是出店,因为检测结果里没有运动方向信息。
多目标跟踪(MOT)解决的就是这个问题。它的核心思路是:先检测,再关联。每一帧用 YOLOv11 把画面里的人框出来,然后通过关联算法把当前帧的框和上一帧的轨迹匹配上,给每个目标分配一个稳定的 ID。有了 ID,才能算停留时长、运动轨迹、进出方向。文档第三章把这条链路讲得很清楚,基于检测的跟踪方法(Tracking-by-Detection)是目前工程上最成熟的方案,YOLOv11 负责检测精度和速度,跟踪算法负责时序一致性。
这里有个选型上的取舍:YOLOv11 相比前代在骨干网络里引入了通道注意力机制,对小目标和遮挡场景的检测更稳。零售门店里货架遮挡、灯光变化是常态,检测漏一帧,跟踪就可能断轨。所以检测端的稳定性直接决定跟踪质量,这也是为什么文档花了大篇幅拆解 YOLOv11 的 Backbone、Neck 和 Head。
2.2 YOLOv11 检测模块的代码落地与参数说明
文档里给了一个简化的通道注意力模块实现,这是 YOLOv11 骨干网络的关键组件之一。我把它整理成可以直接跑的版本,并补上参数注释:
import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, in_planes, ratio=16): super(ChannelAttention, self).__init__() # 全局平均池化和全局最大池化,分别提取两种统计特征 self.avg_pool = nn.AdaptiveAvgPool2d(1) self.max_pool = nn.AdaptiveMaxPool2d(1) # 两层 1x1 卷积构成瓶颈结构,ratio 控制压缩比例 self.fc1 = nn.Conv2d(in_planes, in_planes // ratio, 1, bias=False) self.relu1 = nn.ReLU() self.fc2 = nn.Conv2d(in_planes // ratio, in_planes, 1, bias=False) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = self.fc2(self.relu1(self.fc1(self.avg_pool(x)))) max_out = self.fc2(self.relu1(self.fc1(self.max_pool(x)))) out = avg_out + max_out return self.sigmoid(out) # 输出通道权重,范围 0~1这段代码的逻辑是:对输入特征图分别做平均池化和最大池化,得到两个 1x1 的通道描述向量,经过共享的两层全连接(用 1x1 卷积实现)后相加,再通过 Sigmoid 归一化成通道权重。in_planes是输入通道数,ratio默认 16,表示压缩比,通道数越大这个值可以适当调大以控制参数量。实际使用时,这个权重会乘回原始特征图,让网络自动关注信息量大的通道。
检测部分调用预训练模型的代码文档里也给了,我补上推理后的处理逻辑:
import torch from yolov11.models import YOLOv11 # 加载预训练权重,pretrained=True 会自动下载官方权重 model = YOLOv11(pretrained=True) model.eval() # 推理模式,关闭 dropout 和 BN 更新 # 输入尺寸 640x640 是 YOLOv11 的默认训练分辨率 image = torch.randn(1, 3, 640, 640) with torch.no_grad(): # 推理不需要梯度,省显存 output = model(image) # output 通常包含边界框坐标、置信度、类别概率 # 实际项目中需要接 NMS 后处理过滤重叠框参数上要注意:输入分辨率 640 是精度和速度的平衡点,门店场景如果摄像头架得高、人占像素少,可以提到 1280,但帧率会掉。pretrained=True加载的是 COCO 预训练权重,person 类本身就在 COCO 里,所以不用重新训练就能检测人。如果门店有特殊需求(比如区分员工和顾客),才需要在自己的数据上微调。
2.3 卡尔曼滤波与匈牙利匹配:让 ID 不跳变
检测出框之后,跟踪的核心是两件事:预测和匹配。卡尔曼滤波负责预测目标在下一帧的位置,匈牙利算法负责把预测位置和实际检测框对应起来。文档里给了卡尔曼滤波的 OpenCV 实现:
import cv2 import numpy as np # 创建卡尔曼滤波器:4 个状态量(x, y, vx, vy),2 个观测量(x, y) kalman = cv2.KalmanFilter(4, 2) # 观测矩阵:只观测位置,不观测速度 kalman.measurementMatrix = np.array([[1, 0, 0, 0], [0, 1, 0, 0]], np.float32) # 状态转移矩阵:匀速运动模型,x' = x + vx, y' = y + vy kalman.transitionMatrix = np.array([[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 1, 0], [0, 0, 0, 1]], np.float32) # 过程噪声协方差:值越大越相信观测,越小越相信预测 kalman.processNoiseCov = np.array([[1e-2, 0, 0, 0], [0, 1e-2, 0, 0], [0, 0, 5e-3, 0], [0, 0, 0, 5e-3]], np.float32) # 观测噪声协方差:值越大越不相信检测框 kalman.measurementNoiseCov = np.array([[1e-1, 0], [0, 1e-1]], np.float32) # 模拟一次观测更新和预测 measurement = np.array([[100], [200]], np.float32) kalman.correct(measurement) prediction = kalman.predict() print("预测位置:", prediction)这里的关键参数是processNoiseCov和measurementNoiseCov。前者表示运动模型的不确定性,门店里顾客走走停停,这个值可以适当调大,让滤波器更相信检测结果;后者表示检测框的噪声,如果 YOLOv11 在遮挡时框抖动大,就调大这个值。文档里给的是一组经验值,实际部署时建议先用一段视频跑一遍,看 ID 切换次数再微调。
匈牙利匹配负责把预测框和检测框配对。相似度通常用 IoU(交并比)加外观特征的余弦距离。文档提到可以结合多种相似度指标综合判断,我的经验是:IoU 负责空间匹配,外观特征负责遮挡后的重识别。如果只靠 IoU,两个人交叉走过之后 ID 必换;加上外观特征,才能把断掉的轨迹接回来。
3. 客流统计指标计算:从轨迹到业务数字
3.1 虚拟检测线怎么画才不数错人
客流统计最基础的指标是进店人数和出店人数。文档给的方案是在出入口设置虚拟检测线,当目标轨迹穿过检测线时判断方向并计数。听起来简单,但实际部署时检测线的位置和方向判断逻辑直接决定准确率。
我一般会把检测线画在门框内侧 20 到 30 厘米处,而不是正对门口。原因是门口区域人员密集、遮挡严重,检测框容易合并或丢失。往店内挪一段距离,画面里人的间距拉开,跟踪更稳。方向判断用轨迹的 y 坐标变化:如果目标中心点从检测线一侧移动到另一侧,且位移超过一个阈值(比如 15 像素),才判定为有效穿越。这个阈值是为了过滤掉在检测线附近徘徊导致的反复计数。
文档里还提到店内停留人数的统计,这个直接用当前帧的活跃轨迹数量就行。但要注意轨迹的生命周期管理:新出现的轨迹要等连续几帧确认后才计入,消失的轨迹要等若干帧未匹配后才移除。这两个参数(确认帧数、丢失帧数)在 DeepSORT 类算法里通常叫max_age和n_init,门店场景建议n_init=3、max_age=30,能有效减少 ID 碎片。
3.2 停留时长和区域客流密度怎么算
高级指标里,顾客停留时长是分析购物行为的关键。计算方式是记录每个轨迹 ID 从首次出现到最终消失的帧数,乘以帧间隔时间。但这里有个坑:如果顾客走出画面再回来,会被当成两个 ID,停留时长就断了。解决办法是在跟踪器里维护一个已消失轨迹的缓存,新轨迹出现时先和缓存做外观匹配,匹配上就恢复原 ID。
区域客流密度需要把画面划分成多个区域,统计每个区域内轨迹点的数量。文档提到可以用网格划分或基于店铺地图的多边形区域。我倾向于用多边形,因为货架和通道的形状不规则,网格会把一个货架切成两半。实现上用 OpenCV 的pointPolygonTest判断轨迹中心点是否落在区域内:
import cv2 import numpy as np # 定义货架区域多边形(按实际店铺地图标注) shelf_polygon = np.array([[100, 200], [400, 200], [400, 500], [100, 500]], np.int32) def is_in_shelf(point): # point 是 (x, y) 元组 result = cv2.pointPolygonTest(shelf_polygon, point, False) return result >= 0 # 返回正数表示在内部 # 对每条轨迹的每个点做判断,累计区域内的轨迹数参数上,多边形的顶点坐标需要根据摄像头画面做透视变换后标注。如果摄像头是斜装的,直接按画面坐标画多边形会有偏差,建议先用标定板做透视校正,把画面映射到店铺平面图再标注区域。
3.3 实时更新与多线程处理
客流统计系统要求实时性,文档提到可以用多线程或异步处理。我的做法是把检测跟踪和统计计算拆成两个线程:检测线程负责跑 YOLOv11 和跟踪器,把每帧的轨迹结果推到一个队列;统计线程从队列取数据,更新计数和区域密度。这样即使统计逻辑复杂,也不会阻塞检测。
队列长度要设上限,比如 100 帧,防止统计跟不上时内存暴涨。如果队列满了,丢弃最旧的帧而不是最新的,保证统计的是当前状态。这个细节文档没展开,但实际部署时是必须处理的。
4. 热力图生成:从散点到可读的密度图
4.1 核密度估计的带宽怎么选
热力图的核心是把离散的顾客位置点转换成连续的密度分布。文档给的方案是核密度估计(KDE),公式里最关键的是带宽 h。带宽太小,热力图会变成一个个孤立的亮点;带宽太大,整个图糊成一片,看不出区域差异。
我的经验是:带宽取画面宽度的 1/20 到 1/10 之间。比如 1920 宽的画面,带宽设 100 到 200 像素。如果店铺面积大、顾客分散,取大值;如果只想看货架级别的密度,取小值。文档里用scipy.stats.gaussian_kde实现:
import numpy as np from scipy.stats import gaussian_kde # 假设已经收集到一小时内所有顾客的坐标点 x = np.array([...]) # x 坐标列表 y = np.array([...]) # y 坐标列表 data = np.vstack([x, y]) # 创建 KDE 模型,bw_method 控制带宽 # 传数值表示固定带宽,传 'scott' 或 'silverman' 用自适应规则 kde = gaussian_kde(data, bw_method=0.15) # 生成网格点用于计算密度 x_grid, y_grid = np.mgrid[0:1920:200j, 0:1080:200j] grid_coords = np.vstack([x_grid.ravel(), y_grid.ravel()]) # 计算每个网格点的密度值 density = kde(grid_coords).reshape(x_grid.shape)bw_method参数如果传数值,表示带宽相对于数据标准差的倍数。传 0.15 意味着带宽是标准差的 15%。实际调参时,先用默认的scott跑一遍,看热力图是否过平滑,再手动调小。
4.2 颜色映射与叠加到店铺平面图
密度算出来之后,需要映射成颜色。文档提到用matplotlib的cm模块,常见方案是jet或hot。但零售场景我建议用jet的变体,因为红色到蓝色的对比度高,店长一眼能看出热点区域。
import matplotlib.pyplot as plt import numpy as np # density 是上一步算出的密度矩阵 cmap = plt.get_cmap('jet') norm = plt.Normalize(vmin=np.min(density), vmax=np.max(density)) colors = cmap(norm(density)) # 绘制热力图 plt.imshow(colors, interpolation='bilinear', alpha=0.6) plt.colorbar() plt.show()alpha=0.6是透明度,方便叠加到店铺平面图上。interpolation='bilinear'让颜色过渡更平滑,避免网格感。如果要输出到 Web 端,可以把密度矩阵转成 JSON,前端用 heatmap.js 渲染,文档第五章提到的 D3.js 和 Chart.js 也是可选方案。
4.3 动态热力图的更新策略
实时热力图不需要每帧重算 KDE,那样计算量太大。我的做法是维护一个滑动窗口,比如最近 5 分钟的轨迹点,每隔 10 秒重算一次密度。窗口内的点按时间衰减加权,越新的点权重越高,这样热力图能反映当前客流分布而不是全天平均。
衰减系数用指数衰减,半衰期设 2 分钟。实现上给每个点乘一个权重exp(-lambda * (t_now - t_point)),lambda由半衰期决定。这个细节文档没提,但做实时看板时很关键,否则热力图更新滞后,店长看到的是一小时前的分布。
5. 避坑与排查:那些让项目返工的细节
5.1 摄像头俯角太大导致检测框重叠
现象:画面里人稍微靠近就检测成一个框,跟踪 ID 频繁合并。 原因:摄像头安装高度不够或俯角太大,人在画面里投影重叠。 解决:摄像头建议装在 3 到 4 米高度,俯角控制在 30 到 45 度。如果已经装好,可以在检测后加一个基于框宽高比的过滤,把异常宽的框拆开或丢弃。
5.2 光照突变导致跟踪断轨
现象:门店开灯或关门时,画面亮度突变,跟踪 ID 大面积切换。 原因:YOLOv11 对亮度变化敏感,检测框位置抖动,匈牙利匹配失败。 解决:在检测前加直方图均衡化或自适应亮度归一化。另外把卡尔曼滤波的measurementNoiseCov调大,让滤波器在检测抖动时更依赖预测。
5.3 热力图带宽设太小导致满屏噪点
现象:热力图看起来像星空,每个顾客位置一个亮点,看不出区域趋势。 原因:KDE 带宽设得太小,或者轨迹点没有做去重。 解决:带宽调到画面宽度的 1/10 以上。另外同一个顾客在同一个位置停留多帧,轨迹点会重复累积,需要按时间或距离做抽稀,比如每 30 帧或每移动 50 像素才记录一个点。
5.4 虚拟检测线计数偏多
现象:进店人数统计比实际多 20% 以上。 原因:检测线附近人员徘徊,轨迹反复穿越;或者跟踪 ID 切换导致同一人计两次。 解决:加穿越阈值(位移超过 15 像素才计数),加轨迹确认帧数(新 ID 连续 3 帧才计入),加已计数 ID 缓存(同一 ID 在短时间内不重复计数)。
5.5 服务器 GPU 显存不足
现象:跑几路视频后程序崩溃,报 CUDA out of memory。 原因:YOLOv11 模型加跟踪器的特征提取网络占用显存,多路并发时累积。 解决:限制每路视频的输入分辨率,或者用批处理方式把多路视频拼成一个 batch 推理。如果还不行,把外观特征提取网络换成轻量版,或者降低跟踪器的max_age减少缓存轨迹数。
6. 进阶技巧:把热力图和客流指标串成经营看板
系统跑通之后,真正有价值的是把热力图和客流指标关联起来。我一般会做三件事:第一,把热力图按小时切片,对比不同时段的区域热度变化,找出哪些货架在特定时段被冷落;第二,把停留时长和区域密度做交叉分析,密度高但停留短说明是通道,密度高且停留长才是真正的兴趣区;第三,把进店人数和成交数据做关联,算出门店转化率。
验证方法上,我会用人工计数做基准。选一个客流适中的时段,安排一个人在门口手动计数,同时跑系统统计,对比误差。如果误差在 5% 以内,说明检测跟踪链路没问题;如果超过 10%,先查检测线位置和穿越阈值,再查跟踪 ID 切换次数。ID 切换次数可以在跟踪器里加一个计数器,每发生一次 ID 切换就累加,正常情况下一小时内切换次数应该低于进店人数的 10%。
还有一个技巧是热力图的颜色映射不要用固定范围,而是用当前时段的最大密度做归一化。这样不同时段的热力图对比时,颜色深浅反映的是相对热度而不是绝对密度,店长更容易看出变化趋势。
从那以后我每次部署客流系统,都会先在目标门店录一段 30 分钟的测试视频,离线跑一遍完整链路,把 ID 切换次数、计数误差、热力图带宽这三个参数调到位再上线。上线后第一周每天对比人工计数,确认稳定后才交给运营。这套流程走下来,返工率能降一大半。希望帮到你。
本文还有配套的精品资源,点击获取