AI客流分析技术链路:从人体检测到多目标跟踪与轨迹事件
有没有认真想过一个问题:商场门口那个摄像头,背后到底在算什么?它真的“认识”每一个顾客吗,还是只是在猜?我做了几年视频分析项目,几乎每次跟客户聊AI客流分析系统,都会被问一句“能不能认出这是谁”。这里得先把概念掰开:真正的客流分析很少去做人脸识别,它做的是人体检测、多目标跟踪,然后把一个个移动目标转换成轨迹事件,最后落到进店人数、停留时长、区域热力这些业务数字上。
这篇文章想给你一条完整的链路,从摄像头进来的每一帧画面,到“某人在入口徘徊了40秒然后离开”这种结构化事件。适合正在选型的技术负责人,也适合刚接触计算机视觉、想知道算法到底在做什么的开发同学。我不会只讲概念,会带上我在项目中实际调过的参数、踩过的坑,以及一个最小可落地的系统骨架。
1. AI客流分析到底在看什么
1.1 一条链路,三个角色
任何AI客流分析系统都可以看成三个角色在接力:摄像头是眼睛,持续返回原始视频流;后端分析程序是大脑,做检测、跟踪和事件推理;报表和告警是手,把结构化结果呈现给运营者。
完整的链路长这样:视频流 -> 解码抽帧 -> 人体检测 -> 多目标跟踪 -> 轨迹数据处理 -> 事件引擎 -> 统计报表/实时告警。
这里最容易被忽略的一点是:人体检测只是起点,不是终点。检测模型每帧只告诉你“这里有个人框”,但系统真正需要的是“这个人是什么时候来的、往哪个方向走、在哪个区域停留了多久”。所以检测框必须被跟踪模块串成轨迹,轨迹再被事件引擎翻译成业务语言。这个过程一环扣一环,前面任何一步出问题,后面的指标都会失真。
1.2 核心输入不是一张张照片,而是连续时间轴
初学的时候我犯过一个错:把摄像头画面当图片来处理,一帧一帧独立检测,然后直接拿检测框的数量当人数。结果客户在门口来回走了两圈,系统报出了3倍的人次。
原因很简单:单帧检测缺少时间维度的上下文。视频本质上是带时间戳的连续图像序列,“进入”“离开”“停留”这些事件不可能在某一帧里定义出来。你至少需要连续几帧甚至几秒的轨迹数据,才能判断一个目标是否真的跨过了入口线,或是在收银台前面停住了。
所以项目里需要重点处理三个时间概念:视频流的到达时间、每一帧的实际采集时间、事件触发后的业务时间。当视频出现卡顿或重传时,这三者可能不一致。我见过一个案子,因为服务器处理不过来,事件时间直接按处理完成时间记录,结果是早上10点的客流高峰被记到了10点07分,整个时段报表全部偏移。后来统一用摄像头帧的原始时间戳,才把问题解决。
1.3 从“方框”到“业务数字”
如果一个系统输出“检测到32个框”,这基本上没法直接给运营用。运营想知道的是“今天进店多少人、平均停留多久、哪个区域最拥挤”。
这两种表达之间隔着一层事件聚合:系统先把同一目标的框串成一条轨迹,给一个唯一ID,再由事件引擎根据轨迹和预设区域计算出结构化事件,比如“轨迹ID 1023 在12:23:15跨越入口线进入店内”。之后所有报表都围绕事件来做——进店客流统计是进入事件去重求和,热力是轨迹点在区域上的累积,平均停留时长则是进入与离开事件之间的时间差。
这里必须强调“去重”。如果不做跟踪,单纯靠检测框数量统计,同一人只要在门口站5秒,多帧检测就会把他算成好几个人。轨迹ID正是解决重复计数问题的关键。
2. 人体检测:一切的基础
2.1 主流检测模型怎么选:YOLO系、RT-DETR,还是更重的Faster R-CNN?
人体检测这个任务,实践里基本就是目标检测模型的应用。项目选型时需要考虑算力、帧率、精度和部署环境,我在不同项目里用过Faster R-CNN、YOLO系和RT-DETR,简单对比如下:
| 模型 | 速度 | 精度 | 部署友好度 | 适合场景 |
|---|---|---|---|---|
| Faster R-CNN | 慢 | 高 | 一般 | 服务端强算力、离线分析 |
| YOLOv5 / YOLOv8 | 快 | 高 | 高,支持ONNX/TensorRT | 绝大多数实时客流 |
| RT-DETR | 较快 | 高 | 中等,需配套框架 | 新项目选型可考虑 |
| 轻量模型(YOLOv8n等) | 极快 | 中 | 高 | 嵌入式盒子、低功耗设备 |
如果场景是普通门店、通道、档口,我通常直接用YOLOv8或者YOLOv5,导出成ONNX后用OpenVINO或TensorRT做推理。选这两类的原因很实际:资料多、生态成熟、调参经验丰富,真出问题能找到人问。Faster R-CNN精度高但推理慢,除非服务器很闲,否则不推荐做实时客流。
有一个细节容易搞混:人脸检测不等于人体检测。客流分析关心的是“有没有一个人经过”,不是“这个人是谁”。人脸框在远距离、低头、侧身时很容易丢失,而人体框相对稳定。如果项目客户特别在意隐私,人体检测本身就是一种更安全的方案,因为它不采集生物特征。
2.2 检测结果如何被“清洗”:置信度阈值、NMS和IoU
模型输出一般是一堆候选框,每个框带一个置信度分数。比如模型说“这个位置有85%概率是人”。问题是一张图里同一个位置可能重叠着几十个候选框,必须用规则清理。
首先是置信度阈值。设置得太高会漏人,设置得太低会把椅子、模特、海报上的人像误识别为真人。我在常规门店场景里一般先取0.5,密集场景降到0.3左右。但这不是拍脑袋定死的,必须配合实际数据验证。比如一个超市的促销海报区,模型经常把真人模特当成人,那就要把置信度阈值上调,或者加一个ROI屏蔽区。
接着是NMS(非极大值抑制):想象检测模型在一个人身上打了十几个框,靠NMS选出一个最可信的框,并把其余重叠框抑制掉。IoU(交并比)在这里用来衡量两个框的重叠程度。NMS的IoU阈值一般取0.45到0.6。调大IoU阈值,允许更多重叠框保留,容易产生重复检测;调小阈值,可能把挨得很近的两个人误合并成一个框。密集人群场景建议把IoU阈值调高一些,让紧挨着的人尽量分开。
我建议把以下三个参数作为每次项目启动的默认起点,然后烧录一小段有代表性的视频反复调:
- 置信度阈值:常规0.5,密集0.25-0.35
- NMS IoU阈值:0.45-0.6
- 输入分辨率:常规640,密集或远距离可考虑1280,但会成倍增加计算耗时
2.3 密集场景最头疼的遮挡问题,我常用的三招
客流系统最大的挑战永远是“人挤人”。超市收银区、地铁闸机、景区入口,一旦人群密度上来,检测框之间会互相遮挡,模型经常漏掉后面的人。
我实战里用效果最好的三个办法:
第一,提高输入分辨率。低分辨率下远处的人和密集处的人只有十几个像素,再强的模型也难识别。从640提高到960或1280,通常能明显改善小目标漏检。代价是推理耗时增加,需要配合TensorRT或更高算力的设备。
第二,调整摄像头安装角度。如果摄像头几乎正对着人头顶俯拍,模型只能看到一堆头顶,人体检测很容易失效。我尽量让摄像头与地平线夹角保持在30度到60度之间,这样能看到躯干和肩部,人体框更完整。
第三,对密集区域做针对性微调。用目标场景的截图收集几百到上千个样本,给检测模型做增量训练。很多人低估这一步,觉得预训练模型已经够用,但真实店里的灯光、货架、玻璃反光都会让通用模型打折扣。花一天时间标数据再微调,往往比调三个月阈值都管用。
3. 轨迹生成:从“框”到“线”的完整过程
3.1 多目标跟踪是个什么数学题
当检测模块每帧都输出若干“人框”之后,系统需要回答一个关键问题:当前这帧的哪个框,是上一帧那个人的延续?这就是多目标跟踪(MOT)。目的不是“识别他是谁”,而是“维持他唯一的ID”。
我用一个类比来理解:商场保安盯着一群穿得差不多的顾客走,他不能靠长相,只能靠“位置预测+动作连贯性”来判断谁是谁。算法也一样,主流流程分四步:
- 预测:用卡尔曼滤波对每条已有轨迹预测下一帧位置。
- 计算相似度:把新检测框和所有轨迹预测框放在一起,算IoU重叠度,或者提取外观特征算余弦相似度。
- 数据关联:用匈牙利算法做全局最优匹配,尽量让每一对轨迹和检测框的组合代价最小。
- 更新状态:匹配成功的轨迹更新状态;没有匹配上的检测框可能开新轨迹;长期没匹配上的轨迹标记丢失。
卡尔曼滤波在这里的作用是“平滑预测”:即使某一瞬间检测框短暂丢失,轨迹还能靠上一帧的速度和方向往前推一下,等到下一帧检测框重新出现时再接上。匈牙利算法解决的是“多人多框到底谁匹配谁”的排列组合问题。
3.2 DeepSORT、ByteTrack这些方案怎么选
跟踪算法的选择会直接影响ID稳定性和系统复杂度。我接触较多的三种方案:
| 方案 | 核心思路 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| IOU Tracker | 只看前后帧框的IoU,贪心匹配 | 极快、实现简单 | 遮挡或漏检时ID很容易丢 | 稀疏低密度场景 |
| DeepSORT | 卡尔曼滤波+匈牙利匹配+ReID外观特征 | ID稳定性强,抗遮挡 | 需要额外ReID模型,资源开销大 | 需要高稳定性的复杂场景 |
| ByteTrack | 把高、低分检测框一起关联 | 密集场景表现好,速度快 | 极端遮挡下仍会丢ID | 大多数客流场景首选 |
我做客流分析这几年,优先尝试的通常是ByteTrack。它有个很聪明的思路:传统方法只保留置信度高的检测框做关联,丢失的信息不要了;ByteTrack把低置信度框也利用起来,在物体边缘、被遮挡部分的场景里,很多低分框其实是有用信息。它不需要额外跑ReID模型,在边缘盒子上跑起来压力小很多。
如果现场遮挡严重、ID频繁跳变,再考虑换成DeepSORT,用ReID特征把“穿白衣服那个人”的视觉特征也纳入匹配。但要注意,ReID特征模型本身也要消耗算力,而且要确保不会把不同人但穿同款衣服的ID搞混。说到底,没有万能方案,一切取决于场景。
3.3 坐标校正:像素坐标到真实世界的映射
摄像头画面是透视投影,离镜头近的人大,离镜头远的人小。如果直接拿像素坐标算速度、区域、轨迹形状,会得到严重失真。
如果只是判断“人有没有越线”,在原始画面里画一条虚拟线也能工作,但越线的位置会因透视关系产生偏差。想要精确判断“人站在哪个货架前”,或者绘制顶视图热力,就需要坐标校正。
通常做法是选地面四个参考点,比如瓷砖的四个角,然后计算单应矩阵H,把像素坐标映射到俯视图坐标。OpenCV里就是一组函数:
import cv2 import numpy as np src_points = np.array([[x1, y1], [x2, y2], [x3, y3], [x4, y4]], dtype=np.float32) dst_points = np.array([[0, 0], [real_w, 0], [real_w, real_h], [0, real_h]], dtype=np.float32) H = cv2.getPerspectiveTransform(src_points, dst_points) # 对一个归一化后的点做透视变换 pixel_point = np.array([[x_foot, y_foot]], dtype=np.float32) real_point = cv2.perspectiveTransform(pixel_point[None, :, :], H)这里的x_foot, y_foot不是检测框中心,而是检测框底边中点。我的习惯是永远用底边中点代表一个人的落脚点。原因很简单:人体框整体会随人弯腰、伸胳膊而上下变化,框底边在大部分情况下最贴近地面位置,用它判断“是否进入某个地面区域”最稳。
4. 轨迹事件:把技术结果翻译成业务语言
4.1 事件引擎的三个核心要素:区域、时间窗口、状态机
检测和跟踪解决的是“这个人从哪到哪”,还回答不了“这算不算进店”。要回答业务问题,需要事件引擎。事件引擎最少要有三样东西:
- 区域:在画面中定义多边形区域,比如入口、收银台、陈列区。
- 时间窗口:判断事件成立需要持续多久,比如“进入店内”需要目标在店内保持2秒,避免只是路过。
- 状态机:每个轨迹ID维护一个状态,比如“区域外”“区域内”“即将离开”,只有状态发生规定变化才产生事件。
区域判断可以用一个基础几何算法:点在多边形内。OpenCV的cv2.pointPolygonTest就能做。但真正的难点是时间窗口和状态切换,因为真实世界里的人不会干净利落地一步跨过一条线,他可能在门口停一下、退半步、又进去。
4.2 典型事件是怎么判定的:进店、离店、逗留、折返
拿最常见的“进店”举例。假设入口区域是一个多边形ROI,系统判定逻辑如下:
- 轨迹ID第一次出现在ROI外,记录状态为
outside。 - 底边中点进入ROI,状态变为
entering,此时先不触发事件。 - 连续N帧(比如2秒)都保持在ROI内,状态变为
inside,同时触发enter事件。 - 如果进入不到2秒又跑出去,状态回到
outside,不触发进店事件,可以另记“经过”或“折返”。
离店事件刚好反过来。逗留事件则是在inside状态下,统计轨迹在ROI内持续的总时长,超过阈值后触发。折返事件则要判断轨迹方向:定期记录轨迹点序列,当方向向量从向里变成向外,再变成向里,说明这个人来回犹豫了。
我把常用事件规则整理成一张表:
| 事件 | 基本判定 | 备注 |
|---|---|---|
| 进入 | 状态outside -> inside,且稳定N秒 | N建议1.5-3秒,防止误触 |
| 离开 | 状态inside -> outside,且稳定N秒 | 常和进入事件配对计算停留时长 |
| 路过 | 穿过ROI但停留时间不足 | 不进入客流统计 |
| 逗留/徘徊 | ROI内停留超过阈值,或方向反复变化 | 需要另外配视觉告警给运营 |
| 折返 | 轨迹方向在边界内反向大于阈值 | 防止门口拉客、犹豫行为被漏统计 |
有一个我踩过很多次的坑:不要用单帧判断事件。一个人在门口低头看手机,身体微微晃动,检测框的底边可能在ROI边缘反复穿越,如果不做时间窗口,系统会崩溃式地产生大量重复事件。加一个“稳定N帧再触发”的机制,就能过滤掉绝大多数抖动。
4.3 从事件到报表,以及隐私红线
事件引擎输出的应该是一份干净的结构化数据,例如:
track_id, event_type, area_id, timestamp, x, y, duration 1023, enter, entrance_01, 2025-06-14 12:23:15, 123.4, 56.7, 0 1023, exit, entrance_01, 2025-06-14 12:31:40, 118.2, 61.0, 504这类事件数据进入统计层,可以聚合出:
- 分时段进店人数、离店人数
- 平均停留时长
- 区域热力(轨迹点在ROI内的累计密度)
- 实时在店人数(进入未离开的轨迹数量)
这里必须提醒一句隐私合规问题。客流分析的原则是“只算数,不认识人”。项目里我通常要求系统不保存原始视频中的可辨认人脸特写,不关联手机MAC、身份信息,只保留坐标、时间和轨迹ID。如果需要做安全分析,另走授权流程。数据保留周期越短越好,产品设计时就要把“聚合统计”和“个人可识别信息”严格分开。这不是技术问题,却是项目能顺利落地的底线。
5. 实操链路复盘:搭一套最小可落地系统
5.1 整体架构:六个模块各司其职
为了让你能把这套链路跑起来,我按最小可用原则拆成六个模块。
- 视频采集:通过RTSP拉流或本地视频文件,用FFmpeg或OpenCV读取帧。
- 解码抽帧:控制帧率,不是每一帧都要送进模型,常见做法是15FPS抽帧分析。
- 人体检测:加载预训练检测模型,输出人框和置信度。
- 多目标跟踪:ByteTrack或DeepSORT维护轨迹ID。
- 事件引擎:根据ROI和时间窗口产生事件。
- 存储与展示:事件数据写入MySQL或ClickHouse,前端仪表盘做报表。
这里我特别提一下抽帧策略。检测模型很耗资源,但跟踪模型大多不需要每帧都做检测。一个比较稳的组合是:每两帧做一次检测,跟踪模块在检测间隔期间用卡尔曼滤波预测来维持轨迹。这样既能保住跟踪连续性,又能降低CPU/GPU负载。当然,如果你的视频里人移动速度很快,两帧一检测会丢轨迹,那就得回到全帧检测。
5.2 核心代码骨架:检测+跟踪+事件判断
下面是个高度简化的骨架,重点展示链路如何串起来。真实环境还需要处理多线程、时间戳和异常等,但核心流程是清楚的。
import cv2 import numpy as np from detector import PersonDetector # 你的检测模型封装 from bytetrack import ByteTrack # 开源实现或自封装 detector = PersonDetector(weights="yolov8n.onnx", conf=0.4, iou=0.5) tracker = ByteTrack(track_buffer=30, min_confidence=0.3) # 例如:入口ROI,坐标在画面上的多边形 entrance_roi = np.array([[520, 320], [760, 320], [760, 540], [520, 540]], dtype=np.int32) for frame, timestamp in video_stream(): # 1. 人体检测,得到框 [x1, y1, x2, y2, score] detections = detector.infer(frame) # 2. 多目标跟踪,得到轨迹 [x1, y1, x2, y2, score, track_id] tracks = tracker.update(detections, frame.shape[:2]) for track in tracks: x1, y1, x2, y2, score, track_id = track # 3. 用底边中点作为“落脚点” foot_x = int((x1 + x2) / 2) foot_y = int(y2) # 4. 判断是否在入口ROI内 inside = cv2.pointPolygonTest(entrance_roi, (foot_x, foot_y), False) >= 0 # 5. 这里根据inside布尔值做状态机更新,记录状态变化 # 状态从outside变为inside且保持N帧,触发enter事件 # 状态从inside变为outside且保持N帧,触发exit事件 event_engine.update(track_id, inside, timestamp, (foot_x, foot_y))这段代码里最值得注意的地方是cv2.pointPolygonTest的第三个参数。传False时只做包含判断,传True时会计算点到多边形的最短距离,用来判断边界抖动比较有用。实际项目里,我建议用最短距离加一个缓冲区间,避免点刚好压在边界上反复横跳。
5.3 性能验证:用什么指标来评估
技术链路搭完,最容易被问的是“效果到底行不行”。我一般不看检测mAP,因为那和业务差太远。我更关心这几个指标:
- 客流计数准确率:AI统计的总进出人数与人工计数比对,公式是
1 - |AI计数 - 人工计数| / 人工计数。 - ID稳定性:同一目标在视频中是否始终是同一个ID,有没有中途分裂、跳变。
- 事件准确率:进店、离店、逗留这些事件能否在正确时间点触发。
- 端到端延迟:从事件发生到屏幕展示,业务上通常要求秒级以内。
做法是剪辑一段20到30分钟的现场视频,找一个人工计数,再让系统跑一遍,按错误类型分类统计。比如“门口徘徊被重复计数”“抱小孩算成两个人”“排队区的人被算成进店”这类典型错误,比一个笼统的准确率有用得多。我见过一个项目,整体准确率95%,但最关键的周末高峰时段准确率只有80%——分时段看指标,才有意义。
6. 实际问题排查:我在现场踩过的坑
6.1 计数总是偏大或偏小,先查哪里
计数偏大多半是重复计数,计数偏小多半是漏检或轨迹丢失。
排查第一步是看轨迹日志。如果同一个人的track_id分裂成了好几个段,说明跟踪在中间丢了;如果某个区域出现了大量短命轨迹,说明检测频繁丢失目标;如果是多个人被合并成一个框,说明模型把紧挨着的两个人当成一个目标了。
针对重复计数,方案是调大事件稳定时间,比如进店需要2秒以上才算;或者使用双绊线,要求目标连续跨过两道线才计一次数。针对漏检,先查看检测置信度和分辨率,再考虑微调模型。
6.2 ID频繁跳变,不一定全是跟踪算法的锅
ID跳变是跟踪系统最典型的问题,但根因往往藏在检测里。因为跟踪依赖检测,检测一旦在某几帧漏掉目标,轨迹就会断,重新出现时会被当成新人。
遇到ID跳变,我通常按这个顺序排查:
- 先看检测结果是否连续。把每帧检测框做成视频回放,看目标消失了几帧。
- 检查跟踪参数,特别是
track_buffer。ByteTrack里这个参数表示允许轨迹保持多少帧不匹配才删除,默认30,如果现场视频帧率是25FPS,意味着允许丢1秒多。密集场景建议加大到50或60。 - 如果检测稳定、参数也没问题,还是跳变,再换外观特征方案,比如DeepSORT。
- 最后检查摄像头安装角度和光照,玻璃反光、逆光造成的阴影会严重影响检测和特征提取。
6.3 边缘机器跑不动,压缩哪一头更划算
很多项目要求把系统部署到Jetson、RK3588这类边缘盒子上,算力非常有限。常见做法是压低分辨率、降低帧率、切轻量模型。但我的建议是优先保证检测质量:宁可抽帧降到10FPS,也不要把分辨率压到无法识别。因为跟踪ID的中断大多与检测帧率低有关,插值补帧帮不上太大忙。
另外可以考虑模型加速:YOLO转成TensorRT INT8后,推理耗时常常能降到原来的三分之一。内存和CPU占用也会明显下降。如果业务上10FPS够了,就别硬上30FPS,边缘盒子的稳定性比纸面帧率值更重要。
6.4 常用问题速查表
| 现象 | 可能原因 | 建议排查方向 |
|---|---|---|
| 总人数大于真实人数 | 重复计数、轨迹分裂 | 增加稳定时间;检查跟踪链路;调大track_buffer |
| 总人数小于真实人数 | 漏检、合并检测 | 提高输入分辨率;降低置信度阈值;调整摄像头角度 |
| ID频繁跳变 | 检测断续、遮挡、特征相似 | 先修检测;调跟踪参数;必要时换DeepSORT |
| 进店事件延迟或丢失 | 时间窗口设置不合理 | 延长稳定N帧;检查ROI是否覆盖正确 |
| 店外路人被算进店 | ROI边界外溢 | 缩小ROI;增加双线判定 |
| 边缘设备高负载 | 模型过大、帧率过高 | 换轻量模型;TensorRT加速;降低抽帧率 |
这张表我每次项目验收前都会过一遍。别等客户发现问题,主动拿一段典型视频逐项测,能省掉很多交付后的麻烦。
最后说点个人习惯。我在实际项目里最深的体会是:这类系统调试到最后,拼的不是算法多先进,而是数据质量和对场景的理解。同一套YOLO+ByteTrack,在鞋店和地铁站的表现可能天差地别,因为光照、视角、人流量完全不同。每次动模型或参数之前,先把自己代入现场,看一段回放,问一句“如果我是运营,我想看到什么”。
还有一个很实用的小技巧:调试时把所有轨迹用不同颜色画出来,并记录每条轨迹的ID和时间戳,做成视频回放。这比看任何指标都直观。一条轨迹颜色稳定地从入口连到收银台再出去,业务逻辑大概率没问题;如果一条轨迹颜色断断续续,那系统就还有得调。项目做多了你会发现,真正耗时间的不是写代码,而是拿着回放一帧一帧地跟现场对质。