简介:这份PDF文档面向零售行业数据分析人员、计算机视觉初学者及目标检测工程实践者,系统讲解如何用YOLOv11完成客流量统计中的轨迹跟踪与热力图生成。文档共43页,支持目录章节跳转与阅读器左侧大纲快速定位,内容完整、图表清晰,压缩包内仅含1个PDF文件,大小约2.24MB,便于随身查阅。已有94人学习。内容从零售业客流量统计的现状与挑战切入,依次展开YOLOv11网络结构、训练与推理流程、轨迹跟踪算法分类与实现、核密度估计与网格计数法生成热力图,并给出系统整合、性能优化及实验结果分析。读者可据此掌握从数据标注、模型训练到多目标跟踪评估指标(MOTA、MOTP、ID Switches)的完整链路,理解遮挡、光照变化与实时性问题的应对思路,并参考大型超市、时尚专卖店、便利店与购物中心等应用案例,将检测结果转化为店铺布局优化与营销决策依据。
1. 零售客流统计为什么绕不开 YOLOv11 加轨迹跟踪这条链路
一家 300 平米的社区超市,门口装了两个摄像头,老板想知道「今天进店多少人、哪个货架停留最久、收银台排队有没有堵住通道」。传统做法是人工数人头,或者用红外对射计数器,前者费人,后者只能给出一个进出总数,分不清谁在货架前站了 3 分钟、谁只是路过。零售业客流量统计真正要的是「人—时间—空间」三者的绑定关系,而 YOLOv11 轨迹跟踪加热力图生成,恰好能把这条链路串起来:检测负责找到人,跟踪负责给每个人一个稳定 ID,热力图负责把 ID 的坐标累积成空间分布。
这条链路的价值不在于单帧检测有多准,而在于跨帧的一致性。YOLOv11 相比前代在骨干网络和检测头上做了调整,小目标召回和推理速度更适合门店这种 1080P、人流密集、遮挡频繁的场景。但只做检测,同一帧里两个人挨着就会来回跳 ID,热力图会糊成一片。所以标题里的「轨迹跟踪」不是可选项,而是热力图能不能用的前提。适合读这篇的人:做过目标检测但没落地过客流统计的算法工程师、需要给门店做数字化改造的 IT 负责人,以及想把 YOLOv11 用到实际业务里的开发者。
2. YOLOv11 检测与轨迹跟踪的工程化拆解
2.1 YOLOv11 网络结构与零售场景的适配点
YOLOv11 的网络结构延续了「骨干—颈部—检测头」三段式,但在骨干里用了更轻量的跨阶段连接,颈部做多尺度特征融合,检测头改成解耦式,分类和回归分开算。对零售场景来说,真正影响落地的是三点:一是小目标分支对远处货架前的人更敏感,二是解耦头让遮挡下的分类置信度更稳,三是整体参数量下降后,单张 1080P 图在消费级显卡上能跑到实时。
我一般会先确认输入分辨率。门店摄像头通常是 1920×1080,如果直接缩到 640 训练,远处的人会变成十几个像素,小目标优化就无从谈起。常见做法是训练用 960 或 1280,推理时保持同样尺度,或者用切片推理把大图切块再合并。下面这段是加载模型并做一次推理的最小代码,重点看输入尺寸和置信度阈值这两个参数。
from ultralytics import YOLO # 加载 YOLOv11 检测模型,这里用官方预训练权重起步 model = YOLO("yolo11m.pt") # 对单张门店图片做推理 results = model.predict( source="store_01.jpg", imgsz=1280, # 输入分辨率,门店远景建议不低于 960 conf=0.35, # 置信度阈值,人流密集时可降到 0.25 减少漏检 iou=0.5, # NMS 的 IoU 阈值,遮挡多时适当调高 classes=[0], # 只保留 person 类,COCO 里 person 的 id 是 0 save=True, # 保存推理结果图,方便肉眼核对 project="runs/detect", name="store_01" )逻辑说明:imgsz决定特征图尺度,直接关系到小目标能不能被检测到;conf控制召回和误检的平衡,零售场景宁可多检一点,因为后续跟踪会过滤掉瞬时误检;classes=[0]把非人目标全部丢掉,减少跟踪器的无效计算。参数改动的判断依据是:如果热力图边缘出现大量孤立点,说明conf太低;如果货架前的人经常丢,说明imgsz不够或iou太低。
2.2 轨迹跟踪:从 ByteTrack 到 BoT-SORT 的选型
检测只给框,跟踪才给 ID。YOLOv11 官方生态里集成了 ByteTrack 和 BoT-SORT 两种跟踪器,调用方式很简单,但选型有讲究。ByteTrack 靠两次关联,先匹配高置信度框,再用低置信度框补漏,速度快,适合人流中等、遮挡不极端的门店。BoT-SORT 在 ByteTrack 基础上加了相机运动补偿和 ReID 特征,适合摄像头有轻微晃动、或者人走出画面再回来需要重新识别的场景。
| 跟踪器 | 关联策略 | 是否用 ReID | 适用场景 | 额外开销 |
|---|---|---|---|---|
| ByteTrack | 高低分两次匹配 | 否 | 固定机位、人流适中 | 低 |
| BoT-SORT | 匹配 + 运动补偿 | 可选 | 机位微晃、回头客识别 | 中 |
| BoT-SORT-ReID | 匹配 + 外观特征 | 是 | 遮挡严重、跨镜追踪 | 高 |
调用跟踪的代码和检测几乎一样,只是把model.track换上去,并指定跟踪配置。
from ultralytics import YOLO model = YOLO("yolo11m.pt") # 对视频流做跟踪,persist=True 表示帧间保持跟踪状态 results = model.track( source="store_entrance.mp4", imgsz=1280, conf=0.3, iou=0.5, classes=[0], tracker="bytetrack.yaml", # 可换成 botsort.yaml persist=True, # 视频流必须开启,否则每帧重新初始化 stream=True, # 逐帧返回,避免一次性占满内存 save=True ) for r in results: if r.boxes.id is not None: # boxes.id 就是每个行人的跟踪 ID ids = r.boxes.id.cpu().numpy() xyxy = r.boxes.xyxy.cpu().numpy() print(ids, xyxy)逻辑说明:persist=True是视频跟踪的关键,不开的话每帧都被当成新序列,ID 会疯狂跳变;stream=True让结果逐帧产出,长视频不会爆内存;r.boxes.id是跟踪器分配的 ID,热力图就是靠这个 ID 把同一人的坐标串起来。参数上,tracker指向 YAML 配置文件,里面能改track_high_thresh、track_buffer这些值,track_buffer决定人消失多少帧后 ID 才注销,门店场景建议设到 30 帧以上,避免人蹲下拿货就被判离场。
2.3 环境配置与训练自己的模型
YOLOv11 环境配置的坑主要集中在 CUDA 版本和 PyTorch 的匹配上。常见做法是先用nvidia-smi看驱动支持的 CUDA 上限,再去 PyTorch 官网找对应版本的安装命令,最后装 ultralytics。不要直接pip install ultralytics就完事,它会把 CPU 版 PyTorch 一起拉下来,训练时才发现用不了 GPU。
# 查看显卡驱动和 CUDA 版本 nvidia-smi # 按官方矩阵装对应 CUDA 版本的 PyTorch,例如 CUDA 12.1 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 再装 ultralytics pip install ultralytics # 验证 GPU 是否可用 python -c "import torch; print(torch.cuda.is_available())"训练自己的模型时,数据标注用 YOLO 格式,每张图一个 txt,每行是类别 x_center y_center width height,坐标归一化到 0 到 1。门店场景建议至少标 3000 张,覆盖早中晚不同光照、节假日不同人流密度。训练命令里epochs和batch要按显存调,imgsz和推理保持一致。
yolo detect train \ data=store_person.yaml \ model=yolo11m.pt \ epochs=100 \ imgsz=1280 \ batch=8 \ device=0 \ patience=20逻辑说明:patience=20表示 20 轮没提升就早停,省时间;batch=8是 1280 分辨率下 8G 显存的保守值,显存够可以往上加;data指向的 YAML 里写清训练集、验证集路径和类别名。训练完看runs/detect/train下的混淆矩阵,如果 person 的漏检率高,优先补远景和遮挡样本,而不是盲目加轮数。
3. 热力图生成:从轨迹坐标到空间分布
3.1 坐标累积与高斯核的数学处理
热力图的本质是把每个人的脚点坐标投到一张和画面同尺寸的累加图上,再对每个点做高斯扩散,最后归一化成颜色。脚点一般取检测框底边中点,因为人头会动,脚的位置更接近人实际站的地方。高斯核的大小决定热力图的「糊」的程度,核太小会看到一个个孤立亮点,核太大整个画面变成一团。
import numpy as np import cv2 def build_heatmap(frame_shape, points, sigma=25): """ frame_shape: (h, w) points: [(x, y), ...] 脚点坐标列表 sigma: 高斯核标准差,控制扩散范围 """ h, w = frame_shape[:2] heat = np.zeros((h, w), dtype=np.float32) for x, y in points: # 在脚点位置累加 1 if 0 <= int(y) < h and 0 <= int(x) < w: heat[int(y), int(x)] += 1 # 高斯模糊做扩散,ksize 取 sigma 的 6 倍左右保证覆盖 ksize = int(sigma * 6) | 1 heat = cv2.GaussianBlur(heat, (ksize, ksize), sigma) # 归一化到 0-255 并转成伪彩色 heat = heat / (heat.max() + 1e-6) heat = (heat * 255).astype(np.uint8) heat_color = cv2.applyColorMap(heat, cv2.COLORMAP_JET) return heat_color逻辑说明:先累加再模糊,顺序不能反,否则每个点的高斯核会互相干扰;sigma是核心参数,门店通道宽度大概 1.5 米,对应画面里约 100 像素,sigma取 20 到 30 比较合适;COLORMAP_JET是常见配色,蓝低红高,如果要叠加在原图上,用cv2.addWeighted按 0.4 的透明度混合。
3.2 用跟踪 ID 过滤,避免热力图被误检污染
直接拿检测框做热力图,最大的问题是误检和瞬时抖动。一个人被检测到 10 帧,其中 2 帧误检在货架边缘,热力图就会在货架上多出一块红斑。用跟踪 ID 过滤的思路是:只有同一个 ID 连续出现超过 N 帧,才把它的坐标计入热力图。这样瞬时误检因为 ID 存活时间短,自然被排除。
from collections import defaultdict # 记录每个 ID 的轨迹和出现帧数 tracks = defaultdict(list) min_frames = 15 # 至少连续出现 15 帧才计入热力图 for r in results: if r.boxes.id is None: continue ids = r.boxes.id.cpu().numpy().astype(int) xyxy = r.boxes.xyxy.cpu().numpy() for tid, box in zip(ids, xyxy): # 取底边中点作为脚点 cx = (box[0] + box[2]) / 2 cy = box[3] tracks[tid].append((cx, cy)) # 过滤掉帧数不足的轨迹 valid_points = [] for tid, pts in tracks.items(): if len(pts) >= min_frames: valid_points.extend(pts) heat_color = build_heatmap(frame_shape, valid_points, sigma=25)逻辑说明:min_frames是过滤强度的旋钮,设太小过滤不掉误检,设太大短时间停留的人会被漏掉,门店场景 15 到 25 帧比较稳;tracks用字典按 ID 聚合,最后统一出图,适合离线分析;如果是实时看板,就改成滑动窗口,只保留最近 5 分钟的轨迹。
3.3 相关热力图阈值与信号热力图的调参思路
热力图出来之后,怎么判断「哪个区域算热」需要一个阈值。常见做法是取热力值的 80 分位数作为高亮阈值,超过阈值的区域标红,低于阈值的保持冷色。这个阈值不是固定的,早高峰和晚高峰的人流基数不同,固定阈值会导致早高峰全红、晚高峰全蓝。更稳的做法是按当天总客流做归一化,或者用滑动窗口的动态分位数。
信号热力图的思路类似,只是把「人」换成「停留时长」。每个人在某个区域的停留时间累加,得到的是时间维度的热力图,能看出哪个货架真正留住了人,而不是只是路过。相关热力图阈值在这里的作用是区分「经过」和「驻足」,一般把停留超过 3 秒的点才计入信号热力图,阈值就设在 3 秒对应的帧数上。
4. 落地排错与进阶技巧
4.1 ID 跳变和热力图拖尾的排查顺序
ID 跳变是轨迹跟踪最常见的故障,表现是热力图上同一个人被画成两条断开的轨迹。排查顺序是:先看检测框是否稳定,如果同一帧里人的框忽大忽小,跟踪器匹配就会失败,这时候要回去调conf和iou;再看track_buffer是否太小,人短暂被遮挡后 ID 注销,重新出现就是新 ID,把track_buffer加到 30 以上;最后看画面里有没有相似外观的人并排走,ByteTrack 不带 ReID,这种情况会互换 ID,换成 BoT-SORT-ReID 能缓解。
热力图拖尾是另一个典型问题,表现为热区边缘有一条细长的尾巴。原因是脚点坐标在帧间抖动,尤其是人走动时框底边会上下跳。解决办法是对脚点做滑动平均,或者用卡尔曼滤波平滑轨迹。我一般会在轨迹层面加一个简单的均值滤波,窗口取 5 帧,拖尾基本就消失了。
4.2 小目标优化与推理结果保存的实用配置
远景小目标是零售场景的老大难,YOLOv11 的小目标优化可以从三个方向入手:提高输入分辨率、增加小目标检测头、用切片推理。提高分辨率最直接,但显存和速度会涨;增加检测头需要改网络结构,适合有训练资源的团队;切片推理是把大图切成带重叠的小块分别检测再合并,对远处货架效果好,代价是推理时间翻倍。
推理结果保存方面,除了save=True存图,更实用的是把每帧的 ID 和坐标存成 CSV 或 JSON,方便后续做停留时长统计和报表。下面这段把跟踪结果逐帧写进 CSV。
import csv with open("tracks.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["frame", "track_id", "x1", "y1", "x2", "y2"]) for frame_idx, r in enumerate(results): if r.boxes.id is None: continue ids = r.boxes.id.cpu().numpy().astype(int) xyxy = r.boxes.xyxy.cpu().numpy() for tid, box in zip(ids, xyxy): writer.writerow([frame_idx, tid, *box])逻辑说明:frame是帧序号,配合视频帧率能换算出时间;track_id是跟踪 ID,同一个 ID 的多行就是一条轨迹;坐标存原始像素值,后续要换算成实际距离再除以每米像素数。这份 CSV 是热力图和停留时长报表的共同数据源,比只存图片有用得多。
4.3 从热力图到经营指标的最后一步
热力图本身是给人看的,真正要进报表的是指标。常见的三个指标是:进店人数(去重后的 ID 总数)、区域停留时长(按 ID 在区域内的帧数累加除以帧率)、通道拥堵指数(单位时间内某区域的人数峰值)。把热力图和这三个指标结合,才能回答「哪个货架该补货、哪个通道该拓宽」。
一个具体技巧是给热力图叠加区域掩膜。门店的货架、收银台、通道在画面里是固定位置,提前画好多边形掩膜,统计时只算掩膜内的点,就能把「路过门口」和「在货架前停留」分开。掩膜用cv2.fillPoly生成,和热力图做按位与,再统计非零像素的分布。这样出来的数据才能直接对接 BI 系统,而不是一张只能看的彩色图。
本文还有配套的精品资源,点击获取