首页 / 资讯中心 / 文章详情

人流热度图估计实战:从人体检测到密度热力场的完整链路

人流热度图估计实战:从人体检测到密度热力场的完整链路 ★ FEATURED ARTICLE
前阵子接了一个偏工程向的活需求说简单也简单给一批监控画面做“人流热度图估计”用来判断哪个区域更容易聚集人群。我一开始以为这就是个可视化Demo拿人体检测框随便叠一层半透明色块上去就行真正动手才发现完全不是这么回事。平台任务列表里挂着的“人脸人体_人流热度图估计”这个标题我拆开研究了好几天最后落地做出来的效果跟公司原本想要的东西也还差着一段距离。这篇就把我从原理拆解到落地调参、再到踩坑排查的完整过程写下来给同样要碰这个需求的开发者一点参考。先说清楚这个任务到底是干嘛的。它不是一个单纯的“人体检测”也不是“人脸识别”而是基于视频帧里的人体位置估算整个画面区域的人群密度分布然后输出一张热度图。热度图常见的形式是一张背景图上叠加由蓝到红的半透明色带红的地方说明人多、停留久蓝的地方说明人少、流动快。这种结果最常见的用途是商场动线分析、景区人流预警、展台吸引力评估甚至还可以用于公共区域的拥挤度预测。所以它的读者不只是工程师还有运营、物业、安全管理人员。也就是说热度图估计这个任务的关键不在“图”而在“估计”这两个字上——你得让最终的可视化结果经得起业务侧的追问否则就是一张好看的废图。下面按我实际操作的顺序把这套流程从头到尾拆开讲。1. 热度图不是画出来的这任务到底在解决什么1.1 从“数人头”到“密度场”需求侧发生了什么变化很多传统客流方案都停留在“数人头”阶段视频里检测到多少人输出一个总数或者再细分几个出入口。这种方案在入口闸机、单向通道这些场景下还挺好用的但一旦进入开放区域比如商场中庭、展厅大堂、地铁换乘大厅问题就来了——人不会老老实实排队穿过闸机他们的分布是散乱的而且随时在移动。单纯的总数对运营没有任何意义你只知道“大厅里现在有300人”但不知道这300人聚集在哪、哪个展台前的停留时间最长、哪条通道明显堵住了。热度图估计解决的正是这个“分布问题”。它不再把每帧检测结果作为一个独立的人数样本而是把所有人体位置投影到统一的物理空间上通过密度计算形成一个连续的“场”。在这个场里你不仅能看出“哪里人多”还能看出“哪里曾经人多”——因为热度是可以累积的某个点位只要在一段时间内反复出现行人它的温度就会持续升高。所以从产品角度看热度图是从“离散计数”到“连续密度分布”的一次技术升级。1.2 一个热度图能支撑哪些实际业务决策我做过一轮使用者访谈发现不同角色对热度图的期待完全不同。运营人员最关心的是“热区联动”比如哪个展台的颜色深度在半小时内明显加深说明它吸引了客流该安排补位或加强引导。物业安全人员关心的是“临界密度”当热力图中某个区域的红色范围开始连成片就要考虑限流或改变动线。商场招商则关注“时段分布”同一块区域在工作日中午和周末晚上的热度形态差异很大这些数据最终会影响租金定价和业态排布。这里要特别提醒一句热度图本身只是中间产物它必须配合业务口径才有价值。我在调研中发现很多项目卡住的不是技术而是业务方说不清楚“热度”到底指“瞬时人数”还是“累计人次”或者“某个位置热不热”是按面积密度算还是按停留时长算。如果你在动手前不把这些口径问题跟需求方对齐后面百分之百要返工。2. 从人体框到热力场我把这条链路拆成了四段2.1 人体检测与位置锚点为什么偏偏用“足点”做热度图的第一步仍然要依赖人体检测模型。大多数云平台的人脸人体能力都会返回人体外接矩形框也就是一个 (x1, y1, x2, y2) 的坐标外加置信度分数。到这里为止绝大多数教程就会直接拿矩形框的中心点作为人的位置然后开始生成热力。我之前也这么干后来发现误差很严重。问题出在“中心点”和“人的实际站立位置”并不一致。人体框是一个瘦高的矩形在站立姿态下框的中心点大约在人的胸口附近。如果在俯视相机里这还能勉强用但监控相机的安装高度通常离地3到5米并不是正俯视人的头部和脚部在像素空间的投影位置会差一大截。直接用中心点热度点的位置会整体向画面内侧偏移聚集区域也会看起来比实际更靠近相机视角的中心方向。正确的做法是取“足点”作为位置锚点。更通用的称呼是脚底中间点也就是人体检测框底部边的中点。因为人站在地面上足点的像素位置最接近他在物理平面上的实际落脚点后续做透视映射时误差最小。如果平台返回了人体关键点那就更好了直接取左脚踝和右脚踝坐标的平均值会比用检测框底部更稳尤其在多人互相遮挡、框的下沿被人群截断时关键点的鲁棒性更好。2.2 坐标归一化和俯视映射热度图该画在哪一层拿到人足点坐标之后不能直接在原始视频帧上撒点。原因很简单原始画面是透视图近处的人又大又稀疏远处的人又小又密集直接在像素空间做密度估计画出来的热度图本身就是失真的。你需要先把像素坐标映射到一个标准的物理平面上。业界常规做法是用单应性变换也就是通过四个参考点计算一个3x3的单应矩阵把图像平面映射到地面俯视平面。操作上也不复杂取画面中一块比较规则的矩形地面区域比如地砖的四角、通道的四个边界点记录它们在画面中的像素坐标同时给它们设一个假设的物理坐标比如0~10米的相对坐标系。然后用 OpenCV 的cv2.findHomography算出变换矩阵把所有足点都投影到这个俯视坐标系里。这一步做完热度图天然就是“从上往下看”的业务方看着也直观。需要注意的是单应矩阵只对地面高度上的点有效。如果是高处物体、悬挂装饰和人的头部投影位置会偏离。所以我们前面才坚持用足点来映射——足点恰好就在地面高度附近。这个细节在文档里通常不会写但实际效果差异巨大。2.3 核密度估计把离散的框变成连续的“温度”坐标映射完成之后你手上是一堆稀疏的二维散点也就是每个行人的落脚位置。下一步是把这些离散点变成连续的密度场这一步在统计学里叫核密度估计。通俗地说每个行人不是一个“像素级”的点而是会向周围“扩散温度”的热源两个行人靠得越近温度叠加越高人群中心的颜色就会越红。实现上核心参数是核带宽也就是单个行人影响半径的物理尺寸。按我的经验带宽设得太小热度图会变成一个一个独立的红点看不到“区域”的概念带宽设得太大细小的通道会被糊成一大片红又失去了位置精度。我做过一组测试在 1080P 画面、相机离地 4 米、覆盖范围约 15 米×20 米的典型场景下带宽设置在 0.8 米到 1.2 米之间出来的热度图视觉上最自然业务方也最认可。这个值不用迷信某个固定数字和实际覆盖面积挂钩去调就行。2.4 时序聚合单帧热度图没意义多帧才有如果只是对一张静态图做人流热度图那充其量是“人群密度图”不是真正的“热度图”。热度的核心属性是积累效应它得回答“过去一段时间里哪些地方人流频繁”。所以实现时一定要把连续帧的结果按时间维度聚合进去。常见的做法是滑动窗口聚合维护一个长时间尺度的累积密度图每处理一帧就把当前帧密度图做一个衰减权重叠加进去。衰减系数根据你要观察的窗口长度来定比如你想看10分钟内的人流热区可以把每帧贡献按指数衰减叠加越早的画面权重越低。这么做还有一个好处画面里瞬时经过的散客不会留下明显痕迹而固定位置排队、驻足的人会让热度稳定上升最终呈现出来的热区才是真正值得关注的区域。3. 跑通最小实现调用某开放平台人脸人体接口的完整记录3.1 依赖与鉴权很多人卡在签名这一步我这次的实现前半部分用某开放平台的人脸人体服务来拿人体检测和关键点结果后半部分自己写热度渲染。为什么不全自己训练模型原因很现实监控场景的数据获取和标注成本太高成熟平台的人体检测模型在遮挡、不同角度上的泛化能力比我自己从零训的小模型要稳得多而且按量计费先跑通验证业务价值更划算。依赖部分其实不多主要就是 OpenCV、NumPy、一个发送 HTTP 请求的热门库再加官方 SDK。我这里建议早期直接跑 REST API不要急着包装成微服务。等流程稳定了再封装成模块也不迟。鉴权是第一个容易踩坑的地方。不同平台的签名机制不一样有的用 AccessKey 直接签名有的要求对请求参数做 Base64 编码和排序后再计算 HMAC 签名。我一开始按照普通 HTTP 请求的方式直接传参结果一直返回“签名不匹配”后来才发现把请求体里所有参数按字典序拼接、加盐、再做十六进制摘要。这里我的经验是先跑通一个最简单的离线图片请求确认鉴权逻辑没问题再上视频流。否则你会分不清是网络问题、参数问题还是签名问题。3.2 请求构造和返回解析容易被忽略的字段以一张图片请求为例请求体里通常要传图像数据Base64 编码和具体的检测参数。最容易被忽略的有两个第一个是“检测类型”参数。人脸人体服务往往包含人脸检索、人脸检测、人体检测、人体关键点等多项能力同一个接口下用不同的特征类型字段区分。如果只填了图片没指定能力类型平台可能只返回默认的人脸检测结果不会返回人体框和关键点你的后续流程就全断了。第二个是置信度阈值。不显式设阈值时服务端会返回全部目标的框里面混杂着非常多低分框比如把假人模特、海报上的人体都当成真人把远处模糊的行人也识别进来。我建议显式地设置一个阈值比如 0.5 到 0.6。这个值既不能太高太高会把遮挡严重的人体漏掉也不能太低太低会产生大量虚检直接污染后面所有密度计算。返回结构方面各家字段命名不完全一样但大致都会包括目标框坐标、置信度分、关键点坐标。我这里用自己顺手取名的格式示意一下{ code: 0, data: { image_id: frame_000123, persons: [ { box: [124, 356, 182, 512], score: 0.92, keypoints: { left_ankle: [132, 498], right_ankle: [141, 495] } }, { box: [631, 210, 701, 402], score: 0.88, keypoints: { left_ankle: [646, 389], right_ankle: [653, 386] } } ] } }注意 box 坐标系是原始图像的像素坐标原点在左上角。这个坐标系后面要跟你的画布坐标、俯视投影坐标做严格区分否则就会出现我后面要讲的“热力点飘到画面外”的坑。3.3 把返回数据变成图上能看的颜色渲染代码的关键片段拿到过滤后的足点之后渲染热力图的代码并不复杂。我在本地先用高斯核在累计密度图上做叠加。核心片段大致长这样import cv2 import numpy as np def render_heatmap(bg, points, sigma30, alpha0.6): h, w bg.shape[:2] density np.zeros((h, w), dtypenp.float32) for (x, y) in points: # 这里把每个足点当作一个高斯热源叠加 yy, xx np.mgrid[0:sigma*21, 0:sigma*21] g cv2.getGaussianKernel(sigma, -1) * cv2.getGaussianKernel(sigma, -1).T x0, y0 int(x - sigma), int(y - sigma) if 0 x0 w and 0 y0 h: x1, y1 min(x0 sigma*2, w), min(y0 sigma*2, h) sx0, sy0 x0 - x0, y0 - y0 region density[y0:y1, x0:x1] region g[:y1-y0, :x1-x0] / max(1, len(points) * 0.5) # 归一化到 0-255映射颜色 norm cv2.normalize(density, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) heat_color cv2.applyColorMap(norm, cv2.COLORMAP_JET) return cv2.addWeighted(heat_color, alpha, bg, 1 - alpha, 0)这段代码的核心思想很简单对每一个行人足点累加一个高斯核所有人叠加完之后归一化再用伪彩色映射转成红蓝色带。在实际项目中我还会把 density 这个累加图做成持久化变量配合衰减因子实现前面说的滑动窗口聚合。从代码角度讲整个渲染链路只需要几十行真正的复杂度全在“足点提取得准不准”和“坐标映射得对不对”上。4. 从能出图到决策可信参数调校与业务口径4.1 置信度阈值、核带宽、颜色映射的调节经验这组参数是热度图质感的关键。我单独跑了一轮参数敏感性测试针对同一段 10 分钟视频分别调整置信度阈值和核带宽观察热度图中心位置和扩散形状的变化。结论是阈值的影响在人数稀疏时段更明显带宽的影响在人群聚集时段更明显。具体来说阈值从 0.3 调到 0.7稀疏时段的总人数下降了大约 35%很多远景小目标被过滤掉了但这不一定是坏事——远景小目标在透视变换后的位置误差往往很大保留它们反而会让热区边缘变得很碎。带宽从 0.4 米调到 2.0 米热区中心的相对位置没有太大变化但红色区域的外沿会明显“糊开”。所以我的建议是先在目标场景里想办法弄清真实地面尺寸把带宽调到一个肉眼看着合理、业务方也能理解的范围然后再回头细调阈值。颜色映射方面COLORMAP_JET 是最常见的但要注意一个问题单个穿红色衣服的人在红蓝色带下会被误认为是“高温点”。我最后换成了带紫色底的彩虹映射让冷暖对比更明显。这个纯粹是视觉工程问题但处理不好汇报时会被业务方反复追问“为什么那个穿红衣服的路人一直顶着高温”。4.2 时间段聚合与热区划分的合理粒度热度图按多长时间窗口来聚合直接决定了它呈现出来的“语义”。10秒的聚合结果近似瞬时密度人眼看上去会非常闪因为画面里的人在不断移动10分钟的聚合结果则更像一个平滑的“交通热力图”可以清晰看出通道走向。对绝大部分业务需求来说我建议至少提供三个预设近5分钟、近30分钟、当天累计。分别对应“现场应急”“运营调度”“宏观趋势”三个决策场景。热区划分也别忘了做。不要只扔一张连续色带图就让业务方自己解读最好在热度图上叠加一个“热区边界”标注比如把超过一定密度阈值的连通区域用多边形圈出来。我是用常规的cv2.threshold findContours实现的效果还算直观。有了热区边界业务方才能够对着图说出“A区人群聚集持续了12分钟需要干预”而不是“图看起来很红”。4.3 业务口径里最容易踩的“时间”坑这是我踩得比较疼的一个坑。热度图叠加时间窗口后不同位置的“热度”会受人的停留时长影响一个人在某个位置站 5 分钟产生的热区强度可能比快速路过 100 个人的区域还高。这在很多安全场景里其实是对的——驻足意味着风险积累但在客流运营场景里商家可能更想知道“有多少人次经过”而不关心“某个人站了多久”。所以我在输出结果时会额外输出一套“人次计数热图”也就是在累计密度图里不把人按高斯扩散而是每次经过只加一个等权重的小峰值避免停留时长被重复放大。这相当于同一个底层检测结果渲染出两种语义完全不同的图一种是“密度-停留”热图适合安全预警一种是“人次-流动”热图适合运营分析。二者背后只有一行权重的差别但业务价值和解释方式完全不同。5. 踩坑排查实录四个真实问题四种完全不同的根因5.1 热力点飘到画面边界外侧坐标系换算的漏洞第一次跑通整条链路后我兴致勃勃地把热力图叠回到视频画面上结果发现最左侧通道明明站着两个人热力点却飘到画面外边去了。排查了很久最后发现原因特别低级我把 API 返回的 box 坐标当作归一化坐标直接乘上了画布的宽高而实际上那个坐标本身就是像素坐标原点在左上角单位是像素。等于在原始像素坐标的基础上又乘了一次画布的缩放比例坐标自然就飞出去了。这个问题的根因是坐标系没有各自封装。我的建议是在代码里从一开始就定义三个独立坐标系原始图坐标、模型返回坐标、俯视投影坐标然后只在明确的边界处做换算不要在流程中间随手混用或复用同一个变量名。这种问题一旦画面上不叠底图光看数字根本发现不了所以调试热力图时一定要保留“底图热力点”的叠加模式。5.2 人群密集区域热度值不升反降阈值连锁反应另一个让我困惑了很久的问题越拥挤的地方热度图反而裂成了稀疏的碎点。后来把没有做过滤波的检测框全部可视化出来才发现人群密度超过一定水平后人体检测的置信度整体下降大量框的分数集中在 0.4~0.5 之间。我之前统一设了一个较高的过滤阈值结果密集区的大部分行人被过滤掉了稀疏角落里反倒留着几个高分的清晰目标热度分布完全反过来了。处理办法是把固定阈值改成分级策略。如果检测结果里存在一个高分框说明画面整体清晰度还行此时可以把对“密集区相邻位置”的过滤阈值调低一些。更稳妥的做法是为人群密集区单独设置一个较低的阈值再结合非极大值抑制的最小距离限制避免同一个人的多个检测框被重复累加。这个问题最好的解法不是临时调参而是在部署前就要意识到“同一个阈值不可能适配稀疏和密集两种场景”。5.3 近大远小导致画面近端永远“高温”透视校正的必要性做俯视热度图时也会遇到一个问题画面近端的人群框又大又多远处的人群又小又少但面积上近端的热度贡献天然更高。我一度以为这不是问题因为近端的物理区域本来就更靠近相机占的画面面积更大。但当我把它映射到三视图上发现近端热区明显大于实际人群覆盖范围。原因就出在把高斯核密度放在像素空间做而不是在俯视物理空间做。像素空间里近处一个人顶着一个 40 像素半径的高斯核远处同一个人只顶着 12 像素半径二者映射到物理平面后近处核的物理尺寸远大于远处。正确做法是把高斯核做成“物理带宽”也就是先在像素空间算出位置再投影到物理坐标最后在物理坐标网格上做核密度估计。这样不管离相机近还是远每个行人的热力核的物理半径都是一致的。5.4 更换相机角度后历史热评图全部失效鲁棒性边界第三个问题准确说是一类问题的缩影。项目在测试阶段换了一台安装角度略有不同的相机结果同一区域的俯视热图热区中心偏移了将近一米历史数据和新数据完全对不上。这个坑在于单应变换的参考点是在旧相机画面中标定的相机角度一变像素坐标和物理坐标的对应关系就变了所有基于旧单应矩阵的历史热度图严格说都失真了。这类问题没有完美的工程解法只能管理预期和流程我把标定参数文件单独管理每次相机调整后重新记录一次单应矩阵并把旧标定参数标记为过期。同时为了避免未来因算法或版本变更导致口径不一致我还会在根目录保存一份“元数据说明”记录识别模型、标定矩阵、检测阈值、带宽参数、渲染版本。以后无论是自己回头看还是同事接手都能快速定位“这个热度图是用哪套口径算出来的”。6. 效果验证与下一步扩展6.1 拿什么验证“热度图准不准”热度图不像人脸识别那样有明确的准确率指标它验证的是“人群分布估计是否接近真实”。我用来评估的方法有两种。第一种是区域计数对比在画面里画出几个规则区域每隔五分钟人工数一次区域人数再和热度图积分密度做相关性对比相关系数能到 0.8 以上我就认为基本可用。第二种是热区中心漂移法让一个人在不同的已知点位来回走动看热度中心与真实位置的偏差有多大。这个偏差主要受相机角度、映射矩阵精度和带宽影响一般控制在 0.5 米以内可接受。这里必须提醒如果发现自己无法组织人工评估至少要跑一部分“再现性检验”即同一段视频输入两次结果完全相同。很多人忽略这个基础检查最后数据处理上出了问题都不知道。6.2 从热度图升级到轨迹热区与停留分析做完基础热度图后项目其实还有两个明显的延伸方向价值都比单纯的热度图大。第一个是轨迹热区基于连续帧的检测结果做目标匹配生成每个人的移动轨迹然后把轨迹线按方向分组就能画出不同流向的人流轨迹图对动线优化特别有用。第二个是停留分析当同一个目标在连续几十帧里位置基本不变就视为一次驻足把驻足点单独聚类可以得到“兴趣点”分布。这两个方向本质上用的还是人体检测的结果只是在上层把位置信息组织得更结构化。架构上我会把“底层检测”和“上层统计分析”拆成两个独立模块底层只负责输出每帧的行人位置上层负责累计密度、轨迹匹配、区域人数统计这些高维计算。这样以后哪怕换了检测模型上层业务逻辑完全不用动。另外一个很现实的建议是所有中间结果都落盘保存成结构化文件比如轻量序列化格式不要只保留最终渲染图。否则哪天业务方拿着一个月前的某张截图来问“这里当时的密度是多少”你会发现根本无法回答。最后说点这轮做完之后切身的体会。人流热度图估计并没有多大算法门槛真正难的是把各种隐含误差控制住坐标系的错位、阈值在不同密度下的漂移、透视带来的尺度失真、时间窗口与业务语义的匹配。这每一项单看都是小事叠在一起就决定了最后那张图能不能真的被拿来决策。抛开某一家平台的任务命名不谈这个方向的基本功——人体检测、位置锚点、单应变换、核密度叠加、时间衰减——本身是通用的你把这套链路理解透了换框架、换云平台、换数据结构都只是在同一个骨架上做参数替换。我踩过的那些坑大概率你也会踩一遍趁着还来得及建议你在动手前先把“物理平面”这个概念刻在脑子里它能帮你躲开一大半麻烦。
阅读完成 · 觉得有帮助?
咨询建站