简介基于YOLOv8的人头计数检测系统源码包适合具备一定Python与深度学习基础的开发者用于快速搭建人头检测与计数应用资源集成PyTorch、Ultralytics框架及ONNX模型附带精美GUI界面可直接运行或二次开发。模型针对head类别训练训练集20790张图片测试集1980张mAP达96.1%精度97.4%召回率92.2%检测效果稳定。包内共30个文件涵盖Python脚本、模型权重、XML标注、图片与评估图表等压缩包约10.16MB其中py文件实现检测逻辑与界面交互onnx模型便于跨平台部署png/jpg用于效果展示与测试txt含类别与说明文档整体结构清晰。已有611人学习浏览适合需要人头计数、客流统计或目标检测入门参考的开发者快速上手配套的评估指标曲线与模型说明可帮助验证训练效果为后续调优提供参考。1. 人头计数这事为什么非得用 YOLOv8 加 ONNX 走一遍做客流统计、车间在岗人数核查或者校园考场人数清点的时候我见过太多团队直接拿 YOLOv5 的权重硬怼摄像头流结果在密集小目标场景里漏检率飙到 30% 以上。人头检测和通用物体检测最大的区别在于目标尺寸小、相互遮挡严重、俯视角度下特征退化这三条叠加起来很多模型在公开数据集上刷的漂亮 mAP 根本扛不住真实场景。基于 YOLOv8 的人头计数检测系统提供的是一套从训练到部署的完整闭环核心价值在于把检测头和计数逻辑解耦再用 ONNX 把模型固定下来让 Python 推理和 GUI 展示各干各的活。这套方案适合两类人一类是要在本地摄像头或者离线视频里快速验证人头计数可行性的人另一类是准备把算法移植到盒子或者嵌入式设备、想先摸清精度和速度底线的工程师。它不解决摄像机标定和多相机跨镜追踪它解决的是单路视频里“人到底有几个、框到底准不准”这个最原始的问题。下面我会从模型选型、数据集组织、训练参数、ONNX 导出与推理、GUI 封装、踩坑记录这几个层面把整条链路拆开讲。2. 为什么 YOLOv8 适合人头计数模型结构、损失函数与场景对齐2.1 人头检测为什么难小目标、遮挡和俯视角度先看一个反直觉的结论人头检测的难度不在于“认出这是头”而在于“在遮挡条件下稳住框的回归”。常规的行人检测数据集里人体框是全身框头部区域只占整张图的 5% 到 10%但人头计数场景里摄像头通常架在 3 到 6 米高度俯视角度下人体信息被压缩只剩下头顶和肩部轮廓。这时候全身检测器会大量漏检不是因为它笨而是因为它学到的特征分布和俯视场景根本不匹配。YOLOv8 针对这类问题有几个结构上的优势。它的 C2f 模块在 Backbone 中把梯度流拆成多条支路再融合比 YOLOv5 的 C3 模块提取小目标纹理特征的能力更强。Head 部分换成了 Anchor-Free 的 Decoupled Head分类和回归分支不再共享同一个卷积输出这让模型在“框位置微调”和“类别置信度”两个任务上各调各的对遮挡严重的场景更友好。再加上它默认的 TaskAlignedAssigner 正样本分配策略会把预测框和真实框的对齐质量纳入匹配分数而不是单纯看 IoU这直接影响了密集场景下正样本的选取质量。2.2 训练数据怎么组织SCUT-HEAD 还是自建标注人头计数最容易翻车的点不是模型是数据。SCUT-HEAD 数据集有两千多张校园场景图像和三万多个标注框适合做预训练但真实项目里摄像头角度千奇百怪我一般建议先花一两天自建标注。用 LabelImg 或者 X-AnyLabeling 标注的时候只需要框头部范围从头顶到下巴不包含脖子和肩膀。如果你的摄像头是斜上方 45 度角宁可框得稍微紧一点也不要框到肩膀否则后处理 NMS 阶段会频繁误抑制相邻人头。目录组织按 YOLO 格式来这是最省事的做法dataset/ ├── images/ │ ├── train/ # 约 1200 张 │ └── val/ # 约 300 张 ├── labels/ │ ├── train/ # 每张图对应同名 .txt │ └── val/ └── data.yaml # 类别描述文件data.yaml 内容如下注意括号里是我实际项目里会微调的项目path: /home/user/dataset # 改成你自己的绝对路径 train: images/train val: images/val names: 0: head这里最容易踩的坑是路径用了相对路径或者 Windows 风格的反斜杠。Ultralytics 在读取 data.yaml 时对路径解析很敏感一旦找不到图片就会在训练开始时报AssertionError: train dataset not found。我习惯在 Linux 下统一用绝对路径Windows 下也用正斜杠省得后面排查。2.3 从头训练还是 fins-tune两个选择的关键区别如果你只有几千张自建数据直接modelyolov8n.pt或者yolov8s.pt从 COCO 预训练权重开始迁移学习是合理的。人头虽然不是 COCO 的独立类别但 COCO 里 person 类别的浅层特征纹理、边缘对人头检测有很强的迁移价值。如果你的场景非常特殊比如是红外热成像的人头那预训练权重的意义就小很多这时候可以考虑不加载权重从头训练只是训练轮数要拉到 300 epoch 以上。关键参数设置是这样的yolo detect train datadata.yaml modelyolov8s.yaml epochs150 imgsz640 batch16 lr00.01参数说明epochs150对小数据集够用再多容易过拟合imgsz640保持默认分辨率的平衡batch16看显存如果你是 8G 显存建议降到 8lr00.01是初始学习率配合 Ultralytics 自带的余弦退火会在后期把学习率压得很低对收敛稳定很重要。训练完后看runs/detect/train/weights/best.pt这是验证集上表现最好的权重导出和后续推理都用它。3. 把 PyTorch 权重转成 ONNX固定动态轴、验证输出、踩三个常用坑3.1 导出命令和动态轴为什么不能乱开转换本身一行命令就能搞定但真正决定部署命运的是三个细节opset 版本、动态轴配置、输出张量的形状。yolo export modelbest.pt formatonnx opset12 dynamicTrue simplifyTrue这份命令会产出best.onnx。dynamicTrue的意思是允许 batch size 动态变化这在检测任务里通常是推荐的因为你不确定一次喂一张图还是多张图。simplifyTrue会调用 onnx-simplifier 做图优化去掉一些冗余的 reshape 和 transposed 节点推理速度能提升 5% 到 10%。导出完成后务必用下面这段脚本检查模型的输出结构import onnx model onnx.load(best.onnx) onnx.checker.check_model(model) # 打印输出张量的名称和形状 for output in model.graph.output: print(output.name, [d.dim_value for d in output.type.tensor_type.shape.dim])逻辑说明onnx.checker.check_model是官方合规性检查能发现不合法节点的存在。更关键的是第二段输出张量的维度信息。YOLOv8 的 ONNX 输出形状为[batch, 84, 8400]其中 84 表示 4 个框坐标加 80 个 COCO 类别概率。如果你用的是自己训练的人头单类别模型输出会变成[batch, 5, 8400]5 是 4 加 1。如果在后处理代码里想当然地按 COCO 的 84 去切结果必然是灾难性的。所以导出后第一件事就是打印这个维度。3.2 ONNX 输出是反直觉的 [C, N] 布局后处理必须转置这是所有 ONNX 推理中最容易翻车的一步。YOLOv8 的 PyTorch 模型输出是[batch, boxes, channels]的布局但 ONNX 导出后变成[batch, channels, boxes]。具体来说8400表示特征图所有格子总数84或5表示每个格子的通道维度。如果不转置就直接做 NMS你会在数组切片时得到大量重复的边界框。正确做法是在 NMS 之前先把张量转成[batch, boxes, channels]的形状然后对每一行的 0 到 3 索引取中心坐标加宽高最后转成左上角右下角形式。ONNX 用的是[cx, cy, w, h]记得转化不然框全画歪。3.3 INT8 量化为什么人头的场景收益大但风险也大热词里反复出现的.onnx 量化 int8是部署环节的高频需求。人头检测非常适合 INT8 量化因为头部纹理相对简单量化对精度损伤通常小于通用物体检测。但前提是你有一段有代表性的校准数据集一般是几百张验证集图片。用 onnxruntime 的QuantizationTool做静态量化的时候校准数据集不能只有人头密集图也要包含一些空场景或少量人头的图否则量化后模型会在稀疏场景下输出诡异的高置信度误报。量化的坑主要在两个地方一是某些算子比如 Sigmoid在 INT8 下精度损失会累加二是分支结构多的网络量化后更容易出现精度跳水。我的经验是导出 ONNX 后先跑 FP32 推理记录 baseline mAP然后量化后再跑一遍对比如果 mAP 下降超过 3% 就放弃量化。不要在没跑 baseline 的情况下直接把量化模型接进 GUI否则你很难判断精度下降是量化造成的还是后处理代码写错了。4. 用 ONNX Runtime 做推理从加载模型到画框计数的最小工程代码4.1 推理代码的整体结构ONNX Runtime 是整个系统里 Python 和 ONNX 模型之间的桥梁它只负责把输入张量喂给模型、拿到输出张量所有的预处理、后处理和计数逻辑都要你自己写。这是和直接使用 Ultralytics 库最大的区别一旦转成 ONNX你就失去了 PyTorch 的自动预处理不能再指望库函数替你缩放图片、做 letterbox、解析输出。下面是我常用的一段最小推理代码直接可以用import cv2 import numpy as np import onnxruntime as ort # 创建推理会话并启用 CPU 优化 sess ort.InferenceSession( best.onnx, providers[CPUExecutionProvider] ) # 预处理步骤 def preprocess(image, input_size(640, 640)): h, w image.shape[:2] r min(input_size[0] / h, input_size[1] / w) new_w, new_h int(w * r), int(h * r) resized cv2.resize(image, (new_w, new_h)) canvas np.full((input_size[0], input_size[1], 3), 114, dtypenp.uint8) canvas[:new_h, :new_w] resized # HWC 转 CHW归一化到 [0,1] blob canvas.transpose(2, 0, 1).astype(np.float32) / 255.0 return np.expand_dims(blob, axis0), new_w, new_h, w, h # 后处理按人头单类别处理 def postprocess(output, conf_thres0.5): # output shape: [1, 5, 8400]先转成 [1, 8400, 5] preds output.transpose(0, 2, 1) boxes, scores [], [] for i in range(preds.shape[1]): cx, cy, bw, bh preds[0, i, :4] score preds[0, i, 4] if score conf_thres: x1, y1 cx - bw / 2, cy - bh / 2 x2, y2 cx bw / 2, cy bh / 2 boxes.append([x1, y1, x2, y2]) scores.append(score) # 用 cv2.dnn.NMSBoxes 做 NMS避免手写循环 indices cv2.dnn.NMSBoxes(boxes, scores, conf_thres, iou_thres0.45) final [boxes[i] for i in indices.flatten()] if len(indices) 0 else [] return final逻辑说明preprocess函数先按比例缩放图片再用灰色填充到 640 乘 640 的画布这是 letterbox 操作。对 ONNX 推理而言这是最容易出问题的地方——如果你做的是直接 resize 而不是 letterbox图像里物体形状会被拉伸模型精度会明显变差。postprocess函数里先做转置再过滤低置信度框最后用 OpenCV 自带的 NMS。注意我这里没有做坐标缩放回原图的操作实际项目中在postprocess返回值前需要按r反向换算否则画在原图上的框缩小一圈。4.2 计数逻辑判断进与出而不是只数瞬时人数单纯统计画面里有多少个框是不够的真实的计数系统需要统计“到今天一共进去了多少人”。这时候需要一个虚拟线或区域判定逻辑。我通常的做法是追踪每个检测框的中心点用轻量级的 IoU 匹配做跨帧关联def count_crossings(centers, prev_centers, line_y): count 0 for c in centers: for prev in prev_centers: if abs(c[0] - prev[0]) 50 and abs(c[1] - prev[1]) 50: # 跨过虚拟线: y 从线以上到线以下 if prev[1] line_y and c[1] line_y: count 1 break return count参数说明line_y是虚拟线的纵坐标位置阈值 50 是像素距离要按摄像头画幅调整画幅是 1920 宽就当放大一点1280 宽就缩小。用这类简化追踪逻辑能处理大部分单摄像头场景但如果人流量大且密集建议直接升级成 ByteTrack 或 DeepSORT否则计数会明显偏低。4.3 在 Ubuntu 20.04 CPU 环境跑通的完整依赖很多人问 Ubuntu 20.04 上 CPU 版本怎么搭建环境。这条命令组合是我经过多次踩坑后确定的python3 -m venv venv source venv/bin/activate pip install onnxruntime1.15.1 opencv-python4.8.0.74 numpy1.23.5参数说明onnxruntime1.15.1配 Python 3.8 最稳定如果你用 Python 3.10 或更高版本可以升到 1.16 以上numpy1.23.5这个版本要和 onnxruntime 兼容装太新的 NumPy 会在加载模型时出现 ABI 错误。opencv-python版本尽量选 4.8 系列4.10 之后有些 NMS 函数的返回类型有变化。CPU 环境跑 YOLOv8s 尺寸的 ONNX640 分辨率单帧推理大约 200 到 400 毫秒如果达不到这个水平先怀疑是不是未开启ort的线程优化参数。5. 人工头计数的精度与训练评估mAP、损失曲线、置信度阈值三条线怎么读5.1 mAP 曲线不只是看数值要看 PR 曲线的形状所谓评估指标曲线Ultralytics 在训练完会在runs/detect/train目录下自动生成results.png和confusion_matrix.png。这个文件会把 mAP50、mAP50-95、精确率、召回率、训练/验证损失曲线都画在一张图里。大多数新手只看 mAP 数值但我想强调的是三条线要对着看。第一条是val/box_loss验证集框回归损失。如果训练损失还在下降但验证损失开始上扬说明过拟合了对策是提前停止而不是继续撞上去。第二条是metrics/precision和metrics/recall的交叉点。人头计数场景普遍偏向保召回率宁肯多框一个假头也不漏掉一个真头所以置信度阈值要往低了调比如 0.3 而不是默认的 0.5。第三条是 PR 曲线如果曲线在召回率 0.8 附近出现明显的膝盖型拐点说明模型对低质量目标已经有些乏力再继续压召回率会引入大量虚警。5.2 置信度阈值对计数偏差的影响有多大我们来做一组很现实的估算假设验证集有 1000 个真实人头你选置信度阈值 0.5 时检出 900 个精确率 95%那计数偏差是漏检 100 个虚报 47 个最终统计值为 947偏差率约 5%。但如果阈值抬到 0.7漏检数可能飙升到 150最后 counting 结果反而偏小。阈值这东西不是越高越好先跑一段验证集画出 F1 曲线和置信度之间的关系找到 F1 最大的阈值再固定到配置里。5.3 自建数据集的评估方法划分验证集和计算误检率相比公测数据集自建数据集评估时我更关注一个指标位叫“单场景误检率”定义是误检率 检测到的假头框数量 / 真实人头数量这个指标比 mAP 更贴近业务。比如一个 50 人教室的监控画面如果模型输出 52 个框其中 3 个是椅子背或反光点误检率就是 6%。实用角度来说误检率大于 5% 就需要检查训练数据里有没有混入大量“类头背景”比如圆形灯罩、球类装饰。把这些负样本单独建一个背景类别或者加入负样本图片重训比调整 NMS 参数有效得多。6. 人工头计数系统的 GUI 界面用 PySide6 在 200 行内做出一版可交付的完整工具6.1 GUI 分两栏左侧实时画面右侧计数面板与启动控制热词里多次出现的 Python GUI 需求在这个项目里通常指的是一个简易桌面端应用。最常见的布局是左侧显示摄像画面或视频帧右侧显示当前人数、累计人数、置信度阈值滑动条和模型加载按钮。下面我写一个基于 PySide6 的最小骨架并解释每一段的作用。import sys import cv2 from PySide6.QtWidgets import QMainWindow, QLabel, QPushButton, QSlider, QVBoxLayout, QHBoxLayout, QWidget from PySide6.QtCore import QTimer from PySide6.QtGui import QImage, QPixmap class HeadCounterGUI(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(Head Counter - YOLOv8 ONNX) self.video_label QLabel(视频画面) self.count_label QLabel(当前人数: 0) self.total_label QLabel(累计人数: 0) self.btn_start QPushButton(开始检测) self.slider_conf QSlider() self.slider_conf.setRange(10, 90) self.slider_conf.setValue(35) # 对应 0.35 置信度 layout QHBoxLayout() left_layout QVBoxLayout() right_layout QVBoxLayout() left_layout.addWidget(self.video_label) right_layout.addWidget(self.btn_start) right_layout.addWidget(self.count_label) right_layout.addWidget(self.total_label) right_layout.addWidget(self.slider_conf) layout.addLayout(left_layout, 4) layout.addLayout(right_layout, 1) widget QWidget() widget.setLayout(layout) self.setCentralWidget(widget) self.timer QTimer() self.timer.timeout.connect(self.update_frame) self.btn_start.clicked.connect(self.toggle_detection) def update_frame(self): # 这里用 placeholder实际项目中在此处读取摄像头帧、调用推理函数、绘制框并更新计数 frame self.cap.read()[1] result_frame, current_count run_inference_on_frame(frame) self.video_label.setPixmap(QPixmap.fromImage(convert_cv_to_qimage(result_frame))) self.count_label.setText(f当前人数: {current_count})参数说明QTimer是刷新机制默认间隔要设在 30 毫秒左右约 33 FPSCPU 推理速度如果达不到这个频率会自降 FPS。slider_conf的范围设 10 到 90 对应 0.1 到 0.9 的置信度阈值实际使用中从 0.3 到 0.5 这一段调节对结果影响最明显。run_inference_on_frame是需要你自己接入第 4 节推理代码的接口函数返回绘制好的帧和当前人数。6.2 Windows 环境常见 GUI 缺失 DLL 问题和语言选项错乱在 Windows 上跑 PySide6 时有一个很典型的坑程序启动后报错缺少MSVCP140.dll或者VCRUNTIME140.dll这不是代码问题而是缺少 Microsoft Visual C Redistributable。解决方案是安装 VC_redist.x64.exe而不是去折腾 Python 安装包。此外 PySide6 在中文 Windows 环境中有时会出现 Qt 平台插件加载失败报platform plugin windows could not be loaded这时候检查环境变量QT_QPA_PLATFORM_PLUGIN_PATH是否指向了PySide6/Qt/plugins/platforms。这两个属于高频场景提前在 README 里写明能省掉用户大量排查时间。6.3 GUI 里模型加载失败和视频源打开失败的错误处理真实交付的时候这类代码需要保证不闪退。模型加载失败最常见的原因是best.onnx路径包含中文或者空格onnxruntime 对包含中文路径的模型文件支持不完整。视频源打不开时cap.isOpened()返回 False后续读帧时read()返回的是空元组如果在update_frame里直接索引下标就会报错。正确写法是在每次read()后检查返回值。建议在 GUI 里把错误信息弹出一个QMessageBox而不是直接打印到控制台因为使用 GUI 的用户大概率不会去看后台输出。7. 把误检率打下来的三个落地技巧负样本、置信度校准、视频帧抽帧策略7.1 负样本图片的权重让模型学会“这不是头”很多做检测项目的人忽略了一个事模型漏检往往不是因为它认不出头而是它把太多“像头的东西”当成了头。在人头计数场景里典型的假阳性是深色圆形座椅、球类、圆形灯罩、肩部以上反光物。这类目标在特征上和俯视的人头非常接近。负样本策略很简单——单独建一个background目录里面放 200 到 500 张不包含人头的现场背景图做成一张张没有标注文件的训练图。Ultralytics 支持train: images/train和单独一个background目录混合训练但一般做法是直接把负样本也放进images/train里、同时不生成对应的 label 文件模型就会自动把他们当作背景类学习。7.2 置信度校准为什么只对检测框有效对计数结果不一定有效置信度校准本质是把输出概率映射到真实概率分布但人头计数系统追求的是总数准确不是每个框的概率准确。有时候你把阈值调低导致召回率提升一个画面里多人重叠导致同一个头被检测了两次计数反而偏高。所以参数调整的最终评估标准应该落到业务指标上——单日计数偏差率、高峰时段漏检率。每次调参后在固定的 1 小时监控视频段上回放观察计数曲线和人工统计数量的偏差比盯着单个框的置信度数值有用得多。7.3 抽帧检测是最后的保底手段卡顿、掉帧和延时处理CPU 推理速度跟不上视频流帧率的时候不要试图强制同步。我的处理方法是设置一个采样间隔比如每 3 帧检测一次检测结果渲染到上一帧画面上。这样界面看起来流畅计数实时性损失也不大。真遇到高帧率且密集场景先降低推理分辨率到 480 或 416这比换更大的模型更实用。别忘了在 GUI 里的刷新逻辑中把检测和渲染拆成两个线程否则推理阻塞会导致界面卡死用户第一反应就是程序崩溃。这套基于 YOLOv8 的人头计数系统从数据准备到 GUI 交付全流程跑下来最怕的不是技术实现而是中途放弃。我见过太多人卡在 ONNX 输出维度不匹配这一步然后开始怀疑模型导出出了问题实际上只是忘了转置。希望这篇笔记能帮你把从训练到部署的路径理顺少走弯路。本文还有配套的精品资源点击获取