
简介这份资源面向深度学习与计算机视觉方向的学习者和开发者提供一套基于YOLO实现人群计数的完整工程方案可用于车站、商场、体育场等密集场景的实时人数统计与监控分析。压缩包共35个文件约50KB以18个Python脚本为核心覆盖模型训练、推理、指标评估与数据转换等环节另含9个Markdown文档、3个YAML配置、1个Jupyter Notebook及若干辅助脚本分别承担训练指南、参数配置与实验记录等职责。目前已有79人学习下载。资源围绕ShanghaiTech等数据集展开包含训练计划、损失函数集成、课程排序、伪标签初始化与在线真值更新等模块并配套本地与云端训练的操作说明及环境配置脚本便于读者理解从数据准备、模型微调到结果可视化的完整流程适合希望快速复现人群计数项目并掌握工程落地细节的中高级开发者参考。1. 基于YOLO的人群计数从检测框到人数统计的那条分界线人群计数这个方向很多人第一反应是密度图回归CSRNet、CANNet那一套。但真到工程落地尤其是要同时知道有多少人和人在哪的时候基于YOLO的检测式计数反而是最省心的路线。原因很直接YOLO给你的是一个个带置信度的边界框框的数量就是人数框的位置就是人的位置不需要额外做积分求和也不需要为密度图单独准备点标注数据集。这个标题基于YOLO的人群计数实现.zip背后要解决的核心问题其实不是怎么训练一个YOLO而是怎么把通用目标检测器的输出稳定地变成一个人数。这两件事之间隔着一条分界线检测器只负责找出它认为像人的东西而计数逻辑要负责决定哪些框算数、哪些框是重复的、哪些框是误检。这条分界线处理不好模型mAP再高人数也会忽多忽少。适合读这篇的人有三类一是手里已经有YOLO权重、想快速搭一个人数统计demo的开发者二是做安防、客流、园区管理这类场景需要离线或边缘端跑人数的工程师三是想拿人群计数当毕设或课程项目但不想一上来就啃密度图回归的学生。下面按先跑通、再调优、最后避坑的顺序讲代码基于Ultralytics的YOLO接口这是目前最省事的做法。2. 用YOLO做人群计数的技术选型与最小可跑通方案2.1 为什么检测式计数在中等密度场景更实用密度图回归的优势在于极密集场景——比如一张图里几百上千人人头只有几个像素检测框根本框不准。但绝大多数实际项目的人群密度没那么夸张地铁口、商场入口、园区闸机画面里几十到一两百人人的像素高度普遍在30以上这种场景YOLO完全能框住。检测式计数有三个密度图给不了的好处。第一是可解释你能把每个框画出来给人看出了争议能回溯第二是可复用同一个模型既能计数又能做轨迹跟踪加个ByteTrack就能出人流方向第三是标注成本低YOLO格式的框标注比密度图的点标注直观得多标注员不容易标错。代价也要说清楚密度超过某个阈值后检测式计数会系统性偏低因为遮挡和粘连导致漏检。我一般把YOLO计数用在画面人数上限300以内的场景超过这个量级就老老实实上密度图或者检测密度图融合。2.2 环境准备与依赖安装先建一个干净的虚拟环境避免和系统里的其他包打架。Python版本建议3.9到3.11太新的版本有些CUDA轮子还没跟上。# 创建虚拟环境 python -m venv yolo_count_env # Linux/macOS 激活 source yolo_count_env/bin/activate # Windows 激活 # yolo_count_env\Scripts\activate # 安装核心依赖ultralytics会自动带上torch pip install ultralytics opencv-python numpy # 如果需要GPU推理确认CUDA可用 python -c import torch; print(torch.cuda.is_available())这里有个参数要留意ultralytics默认会装最新版torch如果你的显卡驱动比较老可能会装到不兼容的CUDA版本。稳妥做法是先按显卡驱动确定CUDA版本再去PyTorch官网拿对应安装命令最后装ultralytics时加--no-deps避免它覆盖torch。2.3 最小可跑通的人数统计脚本下面这段代码是整个方案的核心骨架输入一张图或一段视频输出人数和标注图。先跑通它再谈优化。import cv2 from ultralytics import YOLO # 加载预训练模型yolov8n最轻适合先验证流程 # 换成自己训练的权重时把路径替换掉即可 model YOLO(yolov8n.pt) # COCO数据集里 person 类的索引是 0 PERSON_CLASS_ID 0 def count_persons(image_path, conf_thres0.35, iou_thres0.5): 对单张图做人群计数 conf_thres: 置信度阈值低于它的框直接丢弃 iou_thres: NMS的IoU阈值控制重叠框合并力度 # 只保留person类减少后处理干扰 results model.predict( sourceimage_path, classes[PERSON_CLASS_ID], confconf_thres, iouiou_thres, verboseFalse ) result results[0] boxes result.boxes person_count len(boxes) # 框的数量就是人数 # 把框画回原图方便肉眼核对 annotated result.plot() cv2.imwrite(counted_output.jpg, annotated) return person_count if __name__ __main__: n count_persons(test_crowd.jpg) print(f检测到人数: {n})逻辑说明classes[0]这一步很关键它让模型只输出person类避免把背包、行李箱误算成人。conf和iou是两个必须调的参数后面单独讲。result.plot()是ultralytics自带的可视化直接给你画好框和标签省得自己写绘制逻辑。参数说明conf_thres0.35是人群场景的经验起点比默认的0.25高一点因为人群里遮挡多低置信度框里混着大量误检。iou_thres0.5是NMS标准值人群密集时这个值要往上调原因在避坑章节展开。2.4 从单图到视频流的改造单图跑通后改成视频流只需要把predict的source换成视频路径或摄像头索引然后逐帧处理。但逐帧独立计数会有一个明显问题人数在帧间跳变。解决办法是加一个简单的滑动窗口平滑。from collections import deque class CrowdCounter: def __init__(self, model, window_size10, conf0.35, iou0.5): self.model model self.window deque(maxlenwindow_size) # 滑动窗口存历史计数 self.conf conf self.iou iou def update(self, frame): results self.model.predict( sourceframe, classes[0], confself.conf, iouself.iou, verboseFalse ) current len(results[0].boxes) self.window.append(current) # 用窗口中位数比均值更抗单帧误检 smoothed int(sorted(self.window)[len(self.window) // 2]) return smoothed, results[0].plot()用中位数而不是均值是因为单帧偶尔会冒出一两个误检框均值会被拉高中位数对异常值不敏感。窗口大小10是个折中太小平滑不够太大响应迟钝人流快速变化时会滞后。3. 让计数更准置信度、NMS与输入分辨率的调参实战3.1 置信度阈值不是越低越好很多人觉得conf调低能减少漏检人数会更准。在人群场景里这个直觉是错的。人群遮挡严重低置信度区域里混着大量半个人的框——比如只露出肩膀、只露出头的模型会给0.1到0.3的置信度。这些框如果全留下人数会虚高。我的做法是分场景定阈值画面清晰、人离镜头近conf用0.4到0.5画面远、人小且密conf降到0.25到0.3但这时候必须配合更严格的NMS。判断标准很简单把框画出来看如果框里明显不是完整的人就把conf往上提。# 对比不同conf下的人数找到拐点 for conf in [0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5]: results model.predict(test_crowd.jpg, classes[0], confconf, iou0.5, verboseFalse) print(fconf{conf}: {len(results[0].boxes)}人)跑一遍这个循环你会看到人数随conf上升先快速下降然后趋于平缓。那个拐点附近就是比较合适的阈值再往上提就开始漏掉真实的人了。3.2 NMS的IoU阈值在密集人群里要往上调NMS非极大值抑制负责合并重叠框。默认iou0.5意味着两个框重叠超过50%就只留一个。问题在于人群里两个人挨得很近时他们的框重叠可能就超过50%NMS会把其中一个真实的人当成重复框删掉导致人数偏低。密集场景我一般把iou调到0.6到0.7。代价是可能留下少量重复框但相比漏掉真人重复框更容易通过后续逻辑过滤。如果用的是YOLOv8以上版本可以考虑换成Soft-NMS或者用agnostic_nms参数不过ultralytics默认接口对Soft-NMS支持有限需要自己改后处理。# 密集人群对比不同iou for iou in [0.4, 0.5, 0.6, 0.7]: results model.predict(dense_crowd.jpg, classes[0], conf0.3, iouiou, verboseFalse) print(fiou{iou}: {len(results[0].boxes)}人)3.3 输入分辨率直接决定小人能不能被检出YOLO默认推理尺寸是640。如果原图是4K缩到640后远处的人可能只剩几个像素直接漏检。把imgsz提到1280甚至1536小目标的召回会明显改善代价是显存和耗时上升。这里有个平衡点显存够就上1280边缘设备跑不动就维持640但把摄像头换成光学变焦、让远处的人占更多像素。我见过有人硬把imgsz拉到2048结果帧率掉到2fps实时性没了计数再准也没意义。# 分辨率对比注意显存占用 for size in [640, 960, 1280]: results model.predict(test_crowd.jpg, classes[0], conf0.35, iou0.6, imgszsize, verboseFalse) print(fimgsz{size}: {len(results[0].boxes)}人)3.4 用自定义权重替换COCO预训练模型COCO预训练的YOLO对人的定义偏向站立、完整的行人。如果你的场景是俯拍、或者人穿着特殊工服、或者大量坐着的人COCO权重的召回会打折。这时候需要用自己的数据微调。数据准备按YOLO格式每张图一个txt每行类别 x_center y_center width height坐标归一化到0到1。人群场景只需要一个person类所以类别索引全是0。标注时有个血泪经验密集区域一定要放大标宁可多标几个被遮挡的也别漏标否则模型学到的就是遮挡的人不算人。# 微调命令data.yaml里配置好train/val路径和类别 from ultralytics import YOLO model YOLO(yolov8n.pt) # 从预训练权重起步比从头训快 model.train( datacrowd_data.yaml, epochs100, imgsz960, batch16, conf0.001, # 训练时的conf和推理时不是一回事训练要低 iou0.6, patience20 # 20轮没提升就早停省时间 )训练时的conf0.001是给验证阶段算mAP用的不是推理阈值别搞混。patience20是早停人群数据集通常不大容易过拟合早停能救回来。4. 避坑与排查人群计数里那些让数字失真的细节4.1 人数忽高忽低帧间跳变超过20%现象视频里明明人流平稳计数却在80和120之间反复横跳。原因逐帧独立推理每帧的误检和漏检不一样没有时序约束。加上如果摄像头有自动曝光画面亮度变化会让模型输出不稳定。解决加滑动窗口平滑用中位数而非均值。窗口大小按帧率定25fps的话窗口10约等于0.4秒够用。如果还跳检查是不是摄像头自动增益在作怪把曝光和白平衡锁死。4.2 密集区域人数系统性偏低现象画面边缘稀疏区数得准中间密集区明显少人。原因两个因素叠加。一是NMS把挨得近的真实框合并了二是密集区遮挡严重模型置信度普遍偏低被conf阈值滤掉了。解决iou提到0.6到0.7conf降到0.25到0.3同时imgsz提到1280。三个一起调单调一个效果有限。调完把框画出来重点看密集区还有没有漏。4.3 把广告牌、海报里的人像算进去现象画面里有个带人像的广告牌计数凭空多出几个。原因COCO预训练模型没见过你的具体场景广告牌上的人像在特征上和真人接近。解决短期用ROI感兴趣区域遮罩把广告牌区域排除在计数外。长期还是得用自己场景的数据微调让模型学会区分。ROI遮罩实现很简单检测框中心点落在遮罩区内的直接不计。def filter_by_roi(boxes, roi_polygon): roi_polygon是numpy数组形式的四边形顶点 valid [] for box in boxes: x1, y1, x2, y2 box.xyxy[0].tolist() cx, cy (x1 x2) / 2, (y1 y2) / 2 # 用cv2.pointPolygonTest判断中心点是否在ROI内 if cv2.pointPolygonTest(roi_polygon, (cx, cy), False) 0: valid.append(box) return valid4.4 边缘设备上帧率掉到个位数现象开发机上跑得好好的部署到边缘盒子后卡成幻灯片。原因默认imgsz640在边缘设备上如果没走GPU加速纯CPU推理YOLOv8n也就几帧。加上如果开了result.plot()每帧画图绘制本身也吃时间。解决导出成ONNX或TensorRT再推理速度能翻几倍。另外把可视化关掉只在需要预览时开。导出命令一行搞定# 导出ONNX边缘设备通用性最好 yolo export modelyolov8n.pt formatonnx imgsz640 # 如果有TensorRT环境导出engine更快 yolo export modelyolov8n.pt formatengine imgsz640 halfTruehalfTrue是FP16量化速度提升明显精度损失在人群计数场景基本可忽略。4.5 换了个摄像头人数就不准了现象同一个模型A摄像头准B摄像头偏得离谱。原因安装高度、俯仰角、焦距不同人的成像尺寸和姿态分布变了。模型是在特定视角分布上学的换视角就是分布偏移。解决要么每个摄像头单独采数据微调要么在预处理阶段做透视校正把画面统一到近似视角。工程上更现实的做法是每个点位单独标一小批数据几百张做微调比追求一个模型通吃所有点位靠谱得多。5. 进阶把计数变成可用数据流的几个技巧跑通计数只是第一步真正让这套东西有价值的是把它变成稳定的数据流。我一般会在计数之上加两层一层是区域计数一层是趋势输出。区域计数就是把画面切成几个多边形区域分别统计每个区域的人数。实现上复用4.3的ROI逻辑只是把过滤改成归类。这样你能知道入口多少人、出口多少人而不是只有一个总数。代码结构上把ROI列表传进去每个框判断中心点落在哪个区域分别累加。def count_by_zones(boxes, zones): zones是 {区域名: 多边形顶点} 的字典 zone_counts {name: 0 for name in zones} for box in boxes: x1, y1, x2, y2 box.xyxy[0].tolist() cx, cy (x1 x2) / 2, (y1 y2) / 2 for name, polygon in zones.items(): if cv2.pointPolygonTest(polygon, (cx, cy), False) 0: zone_counts[name] 1 break # 一个框只归一个区域避免重复计 return zone_counts趋势输出则是把平滑后的人数按时间戳写进一个队列或数据库前端画折线。这里有个容易忽略的点写库频率别跟着帧率走25fps写25条没意义按秒聚合每秒写一条平均值就够。数据库用SQLite起步完全够用别一上来就上时序数据库过度设计。验证这套系统准不准别只看总数。我的习惯是抽一段有代表性的视频人工逐帧数出真实人数然后算MAE平均绝对误差和MAPE平均绝对百分比误差。MAE看绝对偏差MAPE看相对偏差。人群计数场景MAPE能压到10%以内就算可用压到5%以内算优秀。如果某个时间段误差特别大回去看那段视频八成是遮挡或者光照突变。最后说个我踩过的坑别在开发阶段就用真实场景的全量数据调参容易过拟合到那一段视频。留出至少20%的不同时段、不同天气的数据做验证否则上线后遇到没见过的光照条件数字会难看到你想重写。这套方案值不值得做取决于你的场景密度——300人以内、要位置信息、要可解释YOLO计数就是性价比最高的选择。希望帮到你。本文还有配套的精品资源点击获取