
做客流统计的项目这几年我前前后后碰过不少从最早用单目摄像头数人头到后来切换到3D视觉方案最大的感受是如果方案选型一开始就走偏后面不管算法调得多细业务方照样不满意。所谓的3D视觉AI客流系统说透了就是让摄像头不仅能“看见”人还能知道人在空间里的精确位置、移动方向并且把这些原始信息翻译成业务能直接用的“事件”谁进了门、谁在展台前停了多久、哪个区域今天人流量最大、排队队伍会不会过长。这篇文章不准备讲那些PPT里的花架子而是把从Sensor Pipeline到数据事件引擎的完整链路拆开包括传感器选型、深度数据处理、目标检测与跟踪、区域判定、事件去重、工程落地和性能调优。适合正在做线下门店数字化、商场客流分析、展览展台效果评估或者准备搭建自己视觉分析平台的朋友参考。你不需要懂很深的三维重建但需要有基本的计算机视觉和数据处理概念剩下的部分我会把关键细节和踩过的坑都摆出来。1. 项目概述3D视觉客流系统到底解决什么问题1.1 为什么是3D而不是2D很多人第一反应是普通摄像头加个YOLO不也能数人吗确实能但2D方案有几个绕不开的痛点。一是光照变化白天逆光、晚上灯光昏暗都会让检测精度明显波动二是遮挡问题两个人并排走模型经常把它们当作一个目标三是尺度问题摄像头装得高一点人变得很小装得低一点又容易被前景物体挡住。3D视觉的核心优势在于它多了一个深度维度。你可以知道目标距离摄像头多远可以估算出头肩高度可以在空间里做更稳定的轨迹关联。尤其是顶装场景人的头部特征非常明显深度图上人头就是一个凸起的小山包这个特征比RGB图像里的肤色、衣服颜色都要稳定得多。实测下来在正常门店环境里纯深度方案的人形检测精度能做到95%以上而单纯用2D视觉在同样环境下能到90%就算不错了。更重要的是业务方要的往往不是“有人”而是“人在什么位置、朝哪个方向走、停留了多久”。这些描述都是空间语义2D图像坐标很难直接回答而3D空间坐标天然匹配这些需求。1.2 整体架构与数据流向这个系统一共分五层我用最简单的话梳理一遍传感器层负责输出深度图、点云或者IR图。常见设备是ToF相机、结构光相机、双目相机。感知层在边缘端或服务器上完成深度预处理、人员检测、目标跟踪输出带有track_id的3D轨迹。数据层把轨迹整理成标准结构化消息比如“这个人在这一帧处于哪个位置、速度是多少、置信度多高”。事件引擎层接收轨迹流做区域判定、跨线判定、停留分析输出业务事件。应用层把事件写入数据库或者推送消息队列供BI看板、门店管理系统、实时大屏调用。整个链路里最容易被人忽视的是Sensor Pipeline和事件引擎的衔接。很多人把检测模型调好了、轨迹也出了却不知道下一步该干什么最后只能把原始轨迹直接丢给业务方让业务方自己去算“进了没进”。结果业务方拿到一堆x、y坐标根本不知道怎么用于是项目就卡在中间。这件事后面会展开讲先记住检测和跟踪只是手段数据事件引擎才是业务价值的出口。关于部署形态中型门店场景我推荐“边缘盒子中心事件引擎”的方式。摄像头只做采集和轻量推理轨迹数据通过局域网传到中心服务这样多门店集中管理方便也方便后续加模型、加规则。如果摄像头本身算力足够也可以直接在设备端跑完整Sensor Pipeline但要做好模型升级和远程运维的预案。2. Sensor Pipeline从传感器到干净的轨迹数据2.1 传感器选型ToF、结构光还是双目这个选择直接决定项目后面的坑多不多。三种主流深度相机方案各有各的性格我列个对比表说明方案测距范围抗环境光点云质量成本适合场景ToF0.3m~5m中强光下有噪点中等边缘抖动中高室内门店、中短距离顶装结构光0.2m~2m弱室外基本不可用较好近距离精度高低到中近距离人脸、小范围桌面双目0.5m~10m可调较强依赖纹理白墙、暗处较差低户外、大场景、成本敏感我的个人建议是室内客流场景首选ToF尤其是摄像头斜向下安装、覆盖范围3到5米的典型门店环境。ToF的深度数据虽然边缘有飞点但胜在全天候稳定不受光照变化影响夜间也能正常工作。结构光更适合近距离的精细测量双目适合预算有限、场景纹理丰富的户外环境但在纯色地板、玻璃门边上容易翻车。另外要注意选型的两个隐藏指标。一是帧率客流系统其实用不到30帧5到10帧足够帧率太高反而增加功耗和存储压力。二是深度图分辨率常见的320x240就能用再高分辨率对算力要求成倍增长收益却很有限。2.2 深度数据预处理与ROI裁剪原始深度图直接送进去做检测效果会很差。第一个原因是深度传感器在物体边缘、反光表面、暗色物体上经常产生空洞和飞点这些噪声会造成大量误检。第二个原因是场景里有很多无关区域比如天花板、货架、玻璃墙这些区域如果不裁掉检测器就得多处理很多无效数据。我常用的预处理流程是空洞填补对深度图中的0值空洞做邻域填充通常用快速双边滤波或者中值滤波。注意别把所有空洞都填掉否则会把前景和背景的边界糊平。时域滤波对连续帧的深度值做指数移动平均可以明显抑制传感器抖动。这个在有人快速移动时会有轻微拖影但客流场景完全能接受。ROI裁剪根据相机安装高度和角度把深度范围裁剪到地面以上0.1米到2.5米之间。这样天花板、地面反光点直接被过滤掉后面的检测压力小很多。点云降采样如果算法要使用点云建议先做体素滤波把百万级点云降到几万级速度能快一个数量级。这一段属于没什么技术含量但收益极大的环节。我见过不少团队跳过预处理直接拿原始深度图跑深度学习模型结果误检率高得离谱最后绕了一大圈回来补预处理。2.3 人员检测与追踪从点云到track在顶装场景下人形检测最稳定的方法反而不是复杂深度学习模型而是基于高度图的传统方法。思路很简单把深度图转换成相对地面的高度图然后提取高于某个阈值的连通区域每个连通区域就是一个候选目标。人的头顶在深度图上自然形成一个局部极大值脚底和地面之间的高度差正好是目标高度所以这个方法非常直接、速度极快单帧处理时间可以控制在几毫秒。当然这个方案也有局限。一是两个人贴得很近时连通域会合并需要在聚类时做分裂处理我常用DBSCAN聚类代替简单的连通域标记因为DBSCAN能自动处理密度不均的情况。二是人蹲下或者小孩走过时高度阈值不好设我通常用动态阈值先统计场景中位数高度再根据目标区域的最大高度确定该目标是否为有效人形。检测只是第一步客流系统真正需要的是连续轨迹所以目标跟踪必不可少。我推荐的基本组合是卡尔曼滤波加匈牙利匹配。卡尔曼滤波负责预测目标下一帧的位置匈牙利匹配负责把当前检测和已有的轨迹一一配对。代价矩阵用3D空间距离而不是图像平面距离这个细节很关键因为3D空间距离更稳定不会因为透视缩放产生歧义。跟踪还要处理几个日常问题轨迹丢失目标暂时被遮挡时保留轨迹超过1到2秒还没匹配上再删除。时间太短容易丢ID太长又会把两个不同的人错接成同一个人。新目标出现检测结果连续3帧以上都在同一位置附近才新建轨迹避免单帧误检产生大量假轨迹。静止目标在展台前站着不动的人轨迹中心点不动但目标仍然是活跃的不能用速度判断是否离开。这里再强调一次追踪输出的每条轨迹必须带全局唯一的track_id并且时间戳对齐。没有track_id后面的区域状态机根本写不下去事件也会乱成一锅粥。2.4 坐标标定与数据输出结构传感器输出的原始坐标是像素坐标系要把轨迹变成业务能用的空间信息必须做坐标标定。具体来说就是把图像坐标映射到地面世界坐标。这个映射通常用一个单应矩阵就能完成前提是地面近似为一个平面。对于室内门店和商场来说这个假设完全成立。标定不需要搞得多复杂一个简单可用的办法是用地面四点标定在地面上放四个标定物记录下它们在图像中的像素坐标和实际空间坐标然后用solvePnP或直接线性变换求出单应矩阵。要注意的是地面必须是同一个平面如果相机能扫到两个不同高度的区域需要分别标定后再做区域判断。标定结束后Sensor Pipeline输出的数据应该是标准化的轨迹消息结构类似这样{ track_id: cam_03_000123, camera_id: cam_03, timestamp_ms: 1735000000000, position_ground: { x: 3.42, y: 5.18 }, head_height_m: 1.72, velocity: { vx: 0.24, vy: -0.31 }, confidence: 0.92 }这个结构的设计原则是尽可能让每个字段都能被下游直接消费不要要让人再去做坐标换算或者单位换算。head_height_m是头到地面的高度velocity是地面平面上的速度分量position_ground已经是世界坐标单位统一用米。这样做的好处是事件引擎可以不用关心相机本身直接基于统一坐标工作以后加新相机、换新点位只要标定好、轨迹格式不变事件引擎完全不用动。3. 数据事件引擎把轨迹变成业务事件3.1 事件引擎的职责与核心抽象很多做视觉的人容易忽略事件的语义层觉得自己把轨迹画出来就完工了。但业务方不关心x、y坐标他们关心的是“今天的进店人数是多少”“顾客平均在哪个区域停留时间最长”。这些信息不是靠人肉盯轨迹看出来的而是要靠事件引擎自动计算和输出的。事件引擎本质上是一个状态机它消费来自Sensor Pipeline的轨迹流维护每个目标相对各个区域的状态然后在状态发生切换时产生事件。核心抽象只有四个Zone一个多边形区域可以是入口、货架前、收银台周边、展台区域。Rule一个规则描述在什么条件下产生什么事件。State一个目标相对一个区域的当前状态比如inside或outside。Event一条标准结构化输出记录某人某时在某区域做了什么。用这套抽象可以覆盖绝大部分客流需求。进店、出店可以建模为跨越入口线的enter/exit事件展台停留可以建模为进入展台Zone后的dwell事件排队检测可以建模为排队长度的周期性快照事件。3.2 区域判定与状态机实现区域判定最核心的算法是“点在多边形内”的判断。常用的射线法实现简单也够用性能不是问题因为Zone的多边形顶点一般就十几个一条轨迹点也就每秒几个。要注意的倒是边界情况点正好落在多边形边上时会有二义性实际项目中我把这种情况视为inside因为从业务角度看人站在边界线上通常应该认为已经进入区域。有了点面判定就可以实现区域状态机了。每个track进入系统后事件引擎为每个Zone维护它的状态。伪代码是这样的def on_track(track): for zone in zones: inside_now zone.contains(track.position_ground) prev_state track_states[(track.id, zone.id)] if prev_state OUTSIDE and inside_now: track_states[(track.id, zone.id)] INSIDE emit(Event( typeenter, track_idtrack.id, zone_idzone.id, timestamptrack.timestamp_ms, positiontrack.position_ground )) elif prev_state INSIDE and not inside_now: track_states[(track.id, zone.id)] OUTSIDE emit(Event( typeexit, track_idtrack.id, zone_idzone.id, timestamptrack.timestamp_ms, positiontrack.position_ground ))到这里一切看起来很简单但工程实现里还有个容易翻车的点边缘抖动。当人站在区域边界附近时轨迹点在边界两侧抖来抖去会产生大量enter和exit假事件。解决办法是加入粘滞状态只有连续N帧都判定为inside才真正切换状态同理只有连续N帧都不在区域内才输出exit。N一般取2到3帧对5fps的系统来说就是0.4到0.6秒这个延迟完全不影响业务。停留事件需要额外处理。不是进入Zone后停多久就算停留而是要在事件引擎里维护每个track在zone内的进入时间和最近活跃时间。如果目标一直在zone内移动就持续更新活跃时间只有当位置变化很小时才累计停留时间。否则顾客从Zone穿过也会被计算成停留这显然不符合业务直觉。3.3 事件时序、去重与幂等实时系统最难处理的就是消息乱序和重复。Sensor Pipeline在极端情况下的确会发生消息重发或者处理时间错乱事件引擎必须对此免疫。第一个原则是用event-time而不是processing-time。轨迹里的timestamp_ms是传感器采集时间事件引擎要做的事情是尽可能按这个时间顺序处理。简单方案是给每条track_id做分区因为同一个track_id的事件一定是有序的重排的代价不大。跨track的事件顺序一般不影响区域判定只要最终输出的结果语义正确即可。第二个原则是幂等。消息队列通常会保证at least once也就是至少投递一次所以消费者可能会收到重复消息。事件引擎在emit事件前要生成一个全局唯一的事件ID通常由track_id、zone_id、event_type和timestamp_ms联合哈希得到。然后在Redis里用SETNX写入这个ID设置30秒过期。只有第一次写入成功才真正emit重复消息直接丢弃。这套逻辑简单、可靠实测下来误杀率极低。第三个原则是延迟缓冲。轨迹流通常需要缓冲1到2秒再参与事件判定这样可以把绝大多数乱序的轨迹帧排好。代价是事件输出会有1到2秒的延迟但客流统计场景完全能接受。想要低延迟又要完全有序就得引入水位线机制复杂度会高很多普通项目不值得。3.4 引擎的工程落地选型事件引擎用什么技术栈取决于规模。我按自己实际接触过的两类场景说中小规模单门店或者几十路摄像头轨迹数据量在每秒几百条以内。这种情况下根本不需要上Flink一个普通的Go或Java服务消费Kafka、维护Redis状态就绰绰有余。开发快部署简单出问题也好排查。大规模多门店几百路摄像头、每秒上万条轨迹并且需要做跨门店统计、复杂窗口分析这时用Flink这类流处理框架更合适规则可以用SQL或CEP表达扩展性和容错性都更好。我的建议是不要在一开始就迷信大数据框架。很多项目的数据量其实一个轻量服务就能扛住上Flink反而让运维成本陡增得不偿失。先把业务跑通用最简单可靠的方式把事件算出来等数据量确实上来之后再演进。事件引擎的事件输出同样应该是标准JSON{ event_id: 3c8c8f60a0c1f2e0d2a7f1b4, event_type: dwell_start, zone_id: display_area_01, track_id: cam_03_000123, timestamp_ms: 1735000100000, duration_ms: 15000, position: { x: 2.1, y: 4.6 } }这个JSON会被推送到Kafka或Redis Stream业务系统直接消费这个队列就能实时更新看板。事件引擎和业务系统的边界在这里就很清楚了引擎只负责判断和产出事件不关心业务侧怎么用两者通过消息队列解耦。4. 实操部署与性能调优实录4.1 边缘部署与通信链路实际落地时我推荐“摄像头边缘盒子中心事件引擎”的部署结构。摄像头挂在2.8米到3.2米高的位置向下倾斜10到20度覆盖收银台、入口或者重点展区。边缘盒子负责跑Sensor Pipeline通常一个盒子带2到4路摄像头算力用8路左右的入门级GPU卡或者带NPU的边缘设备就够了。边缘盒子需要把轨迹消息实时传到中心事件引擎。传输方式我推荐gRPC或者MQTT协议本身不是重点关键是网络要稳。门店的Wi-Fi环境经常掉链子所有边缘盒子和中心服务之间最好走有线连接或者至少做断线重连和本地缓冲。边缘盒子本地至少要能缓存半小时以上的轨迹数据否则网络抖动一次数据就丢了业务方第二天来对账的时候哭都来不及。中心事件引擎从消息队列消费轨迹做区域判定产出的业务事件再写入另一个消息队列。这个队列的下游可以是BI数据库、实时大屏、或者告警系统。整个过程链路清晰每个环节都能独立扩缩容。我自己踩过最大的坑是一开始把感知和事件引擎放在同一个进程里后来加新规则时总要把感知服务也重新发布一遍风险极大。拆开之后世界清静了。4.2 关键参数调优细节参数调优这块直接说结论和理由检测帧率推荐5fps。这个频率对步行速度的人完全够用轨迹也连续不会影响事件判定。帧率再高只是增加算力开销识别精度不会因此提高。深度置信度阈值一般设在0.5到0.7之间。太低会引入大量飞点太高会让边缘部位被裁掉影响人形检测。这个值需要结合具体传感器标定后微调。跟踪丢失超时1到2秒。超过这个时间轨迹就删除并归档太长会导致ID错接太短会让短暂遮挡的目标丢失track_id。区域抖动粘滞帧数2到3帧。这是事件风暴的主要解药。停留事件最小触发时间根据业务场景设置比如展台停留至少10秒才算有效停留。太小会淹没真实停留太大又会漏掉短停留。还有一个容易被忽视的细节区域边界一定要和业务方一起对齐。比如“进店”这个事件边界是画在门口内侧还是门口外侧直接决定早晚高峰时计数的差异。我见过最夸张的一次同一个门算法说一天进来500人店长手数出来450人最后查下来是边界画在了感应门之外的区域把门口等人的也算了进去。4.3 性能优化三板斧当路过摄像头变多、轨迹量上来之后性能会逐渐成为瓶颈。我常用的优化手段有三板斧第一板斧是降低无谓计算。点云处理时先做ROI裁剪再体素滤波把有效点数量控制在几万级别。区域判定时先用Zone的包围盒做粗筛轨迹点明显在包围盒外就跳过精确的多边形判定。华北和华东几百个Zone时这一步能省掉90%的点面计算。第二板斧是批量处理。事件引擎不要一条轨迹一条轨迹地处理而是攒一批消息比如每100毫秒拉一次或者每500条处理一次然后批量校验事件ID、批量写Redis。这样吞吐能提升好几倍。Kafka消费端开启批量拉取Redis用pipeline效果立竿见影。第三板斧是巧用本地缓存。状态机没必要每次都查Redistrack_id和Zone的映射关系在本地内存里维护一份就够了。只有轨迹刚进入或者刚离开Zone时才需要同步状态到中心节点。这样中心事件引擎的Redis压力大幅降低整个链路也更不容易出现热key问题。5. 常见问题与排查技巧实录问题现象根因分析处理方式深度图出现黑洞或闪烁噪点传感器在强反光、深色物体上深度丢失开启时域滤波、空洞填补或调整传感器增益光线一暗计数就崩方案过度依赖RGB图像换用纯深度检测方案或在暗光场景补充红外补光两个人并排走被识别成一个人深度连通域合并改用DBSCAN聚类按头部高度密度分裂目标track_id频繁跳变跟踪匹配阈值过紧或轨迹超时太短放宽匈牙利匹配距离阈值把丢失超时调到1.5秒以上区域边界处事件反复抖动轨迹点在边界上来回穿越加上连续N帧粘滞判定输出前延迟缓冲事件重复计数消息队列重复投递消费者未做幂等用event_id做Redis SETNX去重设置30秒过期事件时序错乱不同相机产生的轨迹时间戳差太多做event_time对齐按track_id分区消费或者在事件引擎加1秒缓冲多个相机重叠区域重复计数同一人同时被多路摄像头发现在跨相机融合层做轨迹去重用空间距离和时间窗口判断是否同一个人算法统计和人工对不上账区域边界画法或事件定义与业务口径不一致上线前用实际录像回放与业务方逐个对齐事件语义这里挑两个现场最常见的问题再具体展开一下。第一个是多人密集场景下的漏检。节假日商场、活动现场经常出现十几个人同时挤在一个区域的情况深度方案虽然比2D稳但遮挡依然存在。我的处理技巧是用高度图而不是原始深度图来检测并把头部区域看作一个局部极大值。这样即使身体被遮挡、头部没被遮住也能抓到一个清晰信号。如果目标还被压缩得非常厉害那就需要配合RGB目标检测进行头肩验证两路信号互相补足。第二个是事件丢失问题。有一次在某个门店试点时业务方反馈整点的客流量总是少于实际情况。排查后发现是边缘盒子和中心服务之间用了HTTP短连接网络切换时连接断开积压消息没有重发机制全部丢了。后来我统一把传输协议改成带重试和确认机制的MQTT并在边缘盒子加了本地落盘缓存这个问题再也没有出现过。所以通信链路的可靠性设计真的不能省。调试和排查也要有工具支撑。我建议团队至少维护两个工具一个回放工具把历史轨迹数据和事件日志叠加到视频上回放出问题时一眼就能看到算法当时做了什么判断另一个是事件统计报表按小时聚合事件数出现异常波动时能快速定位是区域设置问题、传感器故障还是算法改动引入的回归。这个项目做到后面会发现最花时间的往往不是模型本身而是把每个环节的边界理顺、把每个细节打磨扎实。从Sensor Pipeline里的一次ROI裁剪到事件引擎里的一次状态切换每一步都决定了系统在真实环境里能不能稳定服务。我自己反复验证下来坚持从简单方案起步、优先保证数据质量和链路可控、再逐步增加复杂逻辑是这类项目最不容易翻车的路径。最近再做新门店接入时我仍然会先用这套地基打底等业务验证了价值再往上加更精细的行为分析。