资讯动态

YOLOv5人群计数实战:从检测框到人数统计的完整优化指南

发布时间:2026/9/1 23:56:34 来源:尧图企业网站定制
简介这套基于YOLOv5的人群计数Python应用面向需要快速落地目标检测与人数统计的开发者、研究人员及安防领域从业者。项目利用YOLOv5高精度、高速度的检测能力支持从摄像头或视频文件中实时读取画面并完成人群计数适用于出入口客流统计、公共场所密度监测等场景并可运行于Windows、Linux及macOS。压缩包共207个文件大小30.58MB以Python源码、YAML配置及Dockerfile为主同时包含预训练模型权重.pt、示例图片等便于直接复现环境并运行推理整体目录结构清晰贴近原生YOLOv5工程组织方式。已有1363人学习下载说明其实用性受到认可。通过该项目可掌握YOLOv5的模型调用、推理流程与OpenCV图像处理技巧也能基于清晰的代码结构快速二次开发接入自己的业务系统。 说实话接到这个活儿的时候我脑子里第一反应不是“又要写YOLOv5教程”而是上个月帮朋友调试商场客流统计系统的一段经历。当时他拿着一张截图问我“为什么这个检测框明明框住了两个人可是计数却只有一个人”我一看两个行人挨得太近检测框几乎重叠NMS直接把低置信度那个框给吞了。这种事在做人群计数的时候太常见了。所以这篇博文我不打算只教你怎么跑通YOLOv5训练而是想把“从检测框到人数统计”这条链路上那些真正要命的逻辑讲清楚顺便把我在树莓派5和NXPi.MX8MP上部署时踩过的坑也一并交代了希望能帮上正在做类似项目的朋友。1. 人群计数这件事为什么不能直接拿检测模型硬数先聊一个很多人容易忽略的底层问题目标检测输出的是“框”而计数项目要的是“数”。这个转换听起来简单实际上到处是坑。检测模型天生是处理“单个目标”的它在训练时学的是“把每个人的包围盒回归准”。可是人群场景的特点恰恰是目标之间高度重叠、互相遮挡而且视角越远同一个人的像素面积就越小。拿常规的行人检测模型直接硬数结果往往是密集区域漏检严重、稀疏区域又因为置信度乱跳导致数字忽高忽低。我见过不少同学在前期调研时拿YOLOv5官方预训练权重直接跑视频流发现效果还行就以为“计数检测完成后再len(detections)一下”。等到了真实场景就傻眼了超市收银台前排队的人、地铁闸机口早晚高峰、景区入口的蛇形通道这些场景下人头挨着人头模型输出的框重叠率经常超过0.7NMS一压制一排人只剩两三个框。另外还有两个容易被忽略的问题一是画面边界。视频监控里人走到画面边缘时身体被截断只有半个身子的框置信度往往不到0.3很容易被过滤掉造成计数偏少。二是透视畸变。同一帧画面里近处的人占几百像素远处的人可能只有十几个像素anchor的尺度要是没覆盖好远处的目标基本就废了。所以我的结论是人群计数项目里YOLOv5只是“底层的检测引擎”真正决定精度的是你围绕它做的数据策略、后处理逻辑和区域判定设计。检测模型负责告诉你“哪里有个人”但“这算不算一个有效计数”得由你来定义。2. 数据准备人群场景的数据集选型与标注细节如果你只是跑通流程用公开数据集就够了。但如果你要部署到某个具体场景比如商场门口、学校操场、工地出入口那大概率得自己标一批数据。我先说公开数据集再说自采数据的标注策略。2.1 主流公开数据集怎么选做人群计数公开数据集我给三个选择VisDrone无人机视角目标小、密度高训练出来的模型对“小目标”非常敏感适合做远距离人群场景。但它的视角和地平线监控差别很大用在固定摄像头场景时需要微调。CrowdHuman专门为行人检测设计图片来自街景和监控遮挡标注非常细致很多框都标了“可见部分”和“全身部分”。这是我最推荐做人群计数微调的底子。Mall Dataset老牌商场固定视角数据集画面简单、密度适中适合快速验证流程但分辨率偏低。我用得最多的是CrowdHuman加自采数据混合。CrowdHuman里的人物姿态和遮挡分布足够丰富能让模型先学到行人/头部的通用特征后面再来20到30张自己场景的图片做微调效果会好很多。2.2 自采数据标“人”还是标“头”很多人第一次标人群数据时习惯性地去标全身框。但我建议在做固定视角的人群计数时优先标“人头框”原因有三个第一人群场景中人体下半身遮挡概率极高标全身框会产生大量质量参差不齐的标注严重干扰训练。第二人头在密集场景下依然是“相对独立”的目标即使两个人身体完全贴在一起头部还是有可辨识的轮廓。第三从计数逻辑上看一个头就等价于一个人这种一对一的映射在后期处理上最干净。标注工具我用的LabelImgYOLO格式目录结构大概是dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/关于标注规范有三条建议遮挡超过50%的目标可以不标让模型自己学会忽略严重遮挡的目标减少误检。处于画面边缘、中心点完全出画的目标不标但中心点还在画面内的目标必须标。人头框不要刻意收紧到头顶发丝留出10%的余量让回归头更好学。提示如果你用的是CrowdHuman它的标注格式不是YOLO的txt需要转一下。CrowdHuman给的坐标是“左右上”的整数像素坐标网上有现成脚本可以转成归一化的xywh但要注意它标注的“head”属性可以直接拿来当人头框用。2.3 单类还是多类我的习惯如果你只做“人数统计”我建议只用一个类别person或者直接叫head。加一个背景类不会提升精度反而会增加分类头的负担。有人非要区分“站立的人”和“坐着的人”这在客流统计里意义不大计数只需要总数。3. 训练阶段的三个关键决定模型尺寸、输入分辨率与超参数YOLOv5系列的训练跑通不难难的是针对人群场景做技术选型。这里我重点说三个会影响最终计数效果的决定。3.1 模型尺寸怎么选我见过很多人在第一步就选错。记得当时我在NXPi.MX8MP上做过压测YOLOv5s的FP32推理时间大约是150到180毫秒每帧改用TensorRT INT8后能压到60毫秒左右。如果直接上YOLOv5m即使INT8也要120毫秒以上基本告别实时。所以我的选型逻辑是这样的部署设备推荐模型备注桌面级显卡GTX 1660以上YOLOv5m或YOLOv5l精度优先有TensorRT加持边缘盒子树莓派5、NXPi.MX8MPYOLOv5s或YOLOv5n必须配TensorRT或NCNN优化手机/低功耗设备YOLOv5n精度要求不高时可选人群计数的目标尺度变化很大YOLOv5n虽然快但对小目标的召回率明显下降在密度高的画面里会漏人。所以在算力允许的情况下我宁愿把YOLOv5s的分辨率拉高也不愿意为速度去选更浅的网络。3.2 输入分辨率6 40只是及格线YOLOv5默认的640x640在通用目标检测上表现不错但人群计数的关键痛点是“小目标”。远处的人群头部可能就十几个像素在640分辨率下特征非常弱。这个阶段你需要做一个取舍如果你有GPU训练资源把输入分辨率提高到960或1280小目标召回率会肉眼可见地提升。如果你的部署端要求实时千万不能直接把1280作为推理分辨率否则帧率会很低。比较合理的做法是训练时用稍大分辨率比如960推理时用640虽然会有轻微mAP损失但换来的是帧率稳定。我实测过一组数据用同样的YOLOv5s训练集640分辨率训练的模型在密度较高画面上计数误差在10%到15%换到960训练后误差降到5%到8%。所以在架构一致的前提下分辨率比换更大的模型更值得优先尝试。3.3 训练命令、超参数与个人习惯训练命令各家都差不多关键在超参数。我通用的训练脚本长这样python train.py --img 960 --batch 16 --epochs 120 \ --data crowd.yaml --weights yolov5s.pt \ --hyp hyp.scratch-low.yaml --device 0这里有两个隐藏坑hyp.scratch-low.yaml里默认的mosaic是1.0人群场景建议保留甚至可以把mosaic从1.0调到1.0到1.5之间的随机概率能显著增强模型对局部遮挡的鲁棒性。学习率建议用cosine衰减而不是默认的linear在人群这种类别少但尺度复杂的任务里余弦退火收敛更稳定不容易出现后期loss震荡。我自己常用的超参数调整lr0默认0.01可以如果Batch Size超过32建议降到0.005否则前期震荡严重。weight_decay0.0005基本不动。warmup_epochs从默认的3.0调到5.0让模型在前期先适应人群图片的分布。训练到稳定阶段后观察val/objectness和val/classification两个指标如果objectness持续偏高但精度还行说明模型对背景的判别力不够往往是背景太杂需要补充负样本。人群场景最怕“宁可多检也不少检”但泛滥的误检会把计数结果彻底带偏后处理怎么做都救不回来。4. 从检测框到计数结果中心点判定与遮挡优化模型训练完成接下来就是最容易被看轻、却最影响项目成败的后处理环节。很多人拿到检测输出直接数框这种做法的误差在密集场景里不可接受。我一般会把计数逻辑拆成三个层次。4.1 置信度阈值和NMS阈值怎么调检测模型输出的原始框有置信度选太高会把远处半遮挡的人丢掉选太低会引入大量误检。我习惯把置信度阈值设在0.25左右NMS的IoU阈值设在0.35到0.4之间。为什么要单独调低NMS的IoU阈值因为YOLOv5默认的NMS IoU阈值是0.45对于“两个挨得很近的人”如果框重叠率超过0.45模型会把低置信度那个框抑制掉导致漏检。把阈值降到0.35可以保留更多重叠的行人框。但也不能太低否则同一个人的重复检测框会大量残留计数虚高。这里没有一个万能参数需要在自己数据集上拿置信度阈值和NMS阈值做一次小网格搜索。我一般画一条“阈值-计数偏差”曲线看哪个组合的误差最小通常耗时不到半小时但对最终准确率的提升非常明显。4.2 核心计数逻辑中心点与有效区域这一步是项目成败的分水岭。具体做法是取每个检测框的中心点用点在多边形内的算法判断这个中心点是否落在你预先划定的计数区域内。只有落在区域内的目标才会计数。为什么用中心点而不是直接用框和ROI求交集因为站在ROI边缘的人身体大部分可能在区域外但头部已经进入区域这时候他应该被计数。同理如果一个人只有一只脚踩进区域但中心点还在外面通常不算进入。这种判定方式更符合实际业务语义。我在项目里用了一个简单的实现def is_inside_polygon(point, polygon): x, y point n len(polygon) inside False p1x, p1y polygon[0] for i in range(1, n 1): p2x, p2y polygon[i % n] if (y min(p1y, p2y)) and (y max(p1y, p2y)) and (x max(p1x, p2x)): if p1y ! p2y: xinters (y - p1y) * (p2x - p1x) / (p2y - p1y) p1x if p1x p2x or x xinters: inside not inside p1x, p1y p2x, p2y return inside # 对每个检测框取中心点 center_x (x1 x2) / 2 center_y (y1 y2) / 2 if is_inside_polygon((center_x, center_y), roi_polygon): total_count 1这个逻辑简洁、直观而且不依赖任何第三方几何库。多边形ROI我一般用LabelMe之类的工具先勾好导出坐标点保存成json推理时直接读取。4.3 遮挡严重时的“重复计数”问题密集人群里两个人紧挨着检测模型可能输出两个框也可能输出一个大框把两个人都包住。前者还好后者就会导致计数偏少。想要缓解有两个思路如果检测框的宽高比明显异常比如特别扁怀疑是合并框可以用该框的宽高比和置信度做惩罚必要时把它拆成多个预测目标但这个方法不稳定要谨慎。更实用的是引入多尺度预测融合对同一帧分别做一次原尺度检测和一次下采样检测把两个结果合并在合并时用中心点距离聚类重叠的框去重不重叠的保留。这个方法能明显提升对遮挡目标的召回率代价是推理耗时翻倍。如果你的视频流是连续帧而非单张图片强烈建议在时间维度上做跟踪去重。我用ByteTrack比较多它基于检测框的IoU和运动信息做关联能帮你统计“实时在场人数”而不是“每秒检测到的人数”。这在动态场景中是质变——镜头前的人稍微一动检测框轻微抖动如果没跟踪同一瞬间会被重复计数多次。5. 部署与帧率瓶颈在边缘设备上压榨YOLOv5的步骤把模型跑通只是第一步落地部署才是真正磨人的地方。我这里重点讲两种边缘设备的部署经验树莓派5和NXPi.MX8MP。两者都是ARM架构都适合跑轻量模型但它们的优化路径不太一样。5.1 树莓派5上的部署树莓派5的CPU性能比前几代强很多但跑YOLOv5s的FP32依然很吃力。我实测裸跑大概1到2 FPS完全没法用。路线是走NCNNNCNN对ARM平台做了比较深入的NEON优化转换也简单git clone https://github.com/Tencent/ncnn.git cd ncnn mkdir build cd build cmake -DCMAKE_TOOLCHAIN_FILE../../toolchains/raspberry-pi5.toolchain.cmake .. make -j4再把PyTorch模型转成ONNX最后转成NCNN的.param和.bin。转换过程中最常遇到的坑是上采样层和slice层在NCNN里的兼容性。YOLOv5的检测头里有Focus结构新版本已经换成普通卷积了老版本转NCNN时会出现维度不匹配。我的建议是直接用YOLOv5v7.0以上的版本Focus已经重写少很多麻烦。转完之后我实测树莓派5上YOLOv5s NCNN大概能跑到8到12 FPS虽然不算丝滑但对于一般客流统计场景够用了。如果你希望更流畅还能在NCNN里开启fp16存储大约再提升20%。5.2 NXPi.MX8MP的痛点与后处理优化NXPi.MX8MP这颗NPU的理论算力有2.3 TOPS看起来不错但坑在于它严格依赖ONNX算子支持。YOLOv5的原始模型里有不少自定义算子比如nn.SiLU、Focus以及检测头的grid生成逻辑直接导ONNX到NPU上会报一堆“unsupported op”。我当时的处理方案是把SiLU换成ReLU6牺牲一点精度但兼容性极大提升。去掉检测头里的grid动态生成代码把输出层改成纯卷积输出再在CPU端做解码。解码、NMS、中心点判定这些后处理全部放CPU端跑NPU只负责卷积特征提取。这样整体流程下来MX8MP上的推理时间能稳定在70到90毫秒一帧大约是11到14 FPS虽然算不上特别高但在客流统计项目里只要帧率不低于10配合几秒钟的平滑窗口业务指标完全够用。5.3 用TensorRT做桌面级加速如果你手头有NVIDIA显卡建议直接上TensorRT。YOLOv5官方仓库自带export.py转起来非常顺python export.py --weights best.pt --include engine --device 0 --half这一步会生成一个FP16精度的TensorRT引擎。我在RTX 3060上测试YOLOv5s的FP16推理时间大概是5毫秒左右FP32大约8毫秒。如果还想更快可以尝试INT8量化但需要准备校准数据集。用一帧图片直接转INT8精度可能掉得没法看必须用几百张有代表性的画面做校准。TensorRT有个小细节引擎文件跟GPU型号和驱动版本绑定换机器必须重新转换。很多人忽略了结果部署到客户机器上直接报“could not load library”白白浪费时间。6. 实测效果与一些不得不说的踩坑记录最后分享一组我们在真实场景下的实测数据以及过程中踩过的几个让人印象深刻的坑。项目背景是一个商场中庭的出入口客流统计摄像头安装在3米高的立柱上斜向下45度俯拍统计进出门的人数。设备是树莓派5模型是YOLOv5s960分辨率训练、640分辨率推理ROI是一个U型排队区域。6.1 实测数据场景人工计数系统计数误差率备注平峰期每小时约80人80822.5%轻微误检来自镜面反射晚高峰每小时约230人2302194.8%主要漏检发生在密集排队区强逆光时段45418.9%远处人头被光晕吞掉雨天带伞人群607220%雨伞面积大误检严重雨天场景那20%的误差特别有意思人群打着伞模型把一部分伞面当成了人头而且伞面的检测框置信度还不低。后来我们在后处理里对检测框的宽高比做了过滤——人头的宽高比通常接近1雨伞的框宽高比普遍大于1.2一过滤干净多了。这个方法虽然通用性不算强但针对具体场景很管用。6.2 几个印象深刻的坑第一个坑是光照变化导致的检测波动。白天和晚上用同一组阈值误差能差10%以上。我们的解决办法是写了一个简单的“光线模式切换”根据画面整体亮度自动切到白天权重或夜间权重置信度阈值也跟随调整效果立竿见影。第二个坑是镜头畸变对ROI判定的影响。画面边缘的人形会被拉歪中心点位置偏移不少。如果你用的是广角镜头建议在标ROI时留10%的缓冲带或者先用OpenCV做一次去畸变再跑检测。第三个坑是模型在训练环境里mAP很好看但部署后偶尔会把广告海报上的人像当成真人。解决方案是在后处理里加一个“静止过滤”如果检测框连续N帧在画面中的位置几乎不变且分类置信度波动不大就判定为“非活动目标”并从计数中剔除。人群计数关心的是人不是海报。7. 一些关于后续优化的想法项目做到这版收获还是很大的。整套流程从数据准备、训练、推理计数到边缘设备部署链路已经非常完整。如果后续再迭代我可能会优先尝试的方向有三个一是引入一个轻量的目标跟踪器比如ByteTrack或StrongSORT把单帧计数升级成轨迹级计数这样在遮挡严重时可以结合历史轨迹做插值减少漏检二是尝试用YOLOv8或者YOLOv11的检测头替换YOLOv5理论上它们在遮挡和小目标上有一些结构上的改进但要看部署端的算子兼容性是否支持三是针对固定机位的场景训练一个轻量背景分割模型做前景过滤把误检的海报、玻璃倒影这些静态干扰直接剔掉从源头减少后处理压力。回头来看人群计数的难点其实从来不在于“跑通模型”而在于你是否理解了检测结果和实际业务语义之间的差距。希望这篇分享能帮你在做类似项目的时候少走几个弯路。如果你在部署时遇到什么奇怪的算子兼容问题欢迎来交流——这种坑一个人踩太孤单了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价