基于YOLOv9的人头检测与人数统计系统实战解析 简介在计算机视觉领域目标检测是智慧安防、人流统计等应用的核心技术之一。从传统的HOG特征到深度学习时代的YOLO系列检测算法不断演进尤其面对密集场景下的小目标检测如何平衡精度与速度成为工程落地的关键。YOLOv9通过可编程梯度信息PGI与高效网络架构GELAN有效缓解了深层网络中小目标特征丢失的问题为拥挤环境下的人头检测提供了更稳定的解决方案。本文从数据集标注、模型微调、训练参数调优到推理部署系统梳理了一套基于YOLOv9构建人头计数系统的完整流程涵盖环境配置、指标解读、计数逻辑实现以及常见问题排查。无论你是入门深度学习的开发者还是需要落地人群密度预警、考场人数校验等应用的工程师都能从中获得从理论到实践的参考路径。1. 项目整体设计与技术选型解析1.1 为什么最终选定YOLOv9来做人头检测先聊聊这套系统的选型逻辑。我做人群密度统计这个方向也有一段时间了早期用过背景差分、HOGSVM那套传统方法后来又试过Faster R-CNN、SSD这些两阶段或单阶段检测器再后面就是YOLO系列一路跟过来。到了YOLOv9这一代它在目标检测上的精度和速度平衡做得相当不错尤其是在密集小目标场景下比之前的v8、v7有明显提升。人头检测这个任务和常规的行人检测、车辆检测还不一样。人头的目标尺寸通常比较小在监控画面里经常只有几十个像素甚至更小而且人群密集的时候互相遮挡非常严重头部轮廓不完整。这个时候模型的浅层特征提取能力、多尺度融合能力就成了决定性因素。YOLOv9引入了PGIProgrammable Gradient Information可编程梯度信息和GELAN一种高效跨阶段网络架构这两个核心设计目标就是解决深度网络在浅层特征上信息丢失的问题。用人话解释就是以前网络在层层提取特征的时候小目标的细节信息很容易在中间过程被“稀释”掉而PGI机制能把这些信息更完整地传递到最终预测层所以YOLOv9在密集人头这种小目标场景里表现特别稳。另外还有一个很现实的原因社区生态。YOLOv9的官方仓库和网上开源资料非常丰富训练脚本、导出脚本、评估脚本都是完整开箱即用的遇到问题搜一下基本都能找到答案。对于不是专门做算法研究、只是想快速落地一个“人头计数系统”的朋友来说这套东西比从零写一个检测网络要省太多事。这里面还有一个关键点就是YOLOv9的官方权重文件和训练好的模型可以直接加载到推理代码里不用自己重新训练也能跑出不错的检测效果。当然如果要对特定场景比如商场出入口、校园走廊做优化还是需要用自己采集的数据做微调这部分后面我会详细展开。这套系统适合谁参考我的判断是有Python基础但对深度学习不太熟的人想做人流统计、考场人数校验、会议室参会人数统计、公共场所人群密度预警等场景的人以及正在学YOLO系列想找个完整练手项目的同学。项目里我整理了详细运行教程和训练好的模型权重哪怕你之前没跑过深度学习项目照着教程一步步来两三个小时也能把检测窗口调出来。1.2 人头检测与普通目标检测的差异点在哪里做这套系统的第一步其实是搞清楚“人头”这个目标和其他目标的区别这决定了数据怎么标、模型怎么调、后处理怎么写。先说标注层面的差异。检测一个人和检测一辆车最大的区别在于人和人的外观差异很大但“头”有一个相对稳定的共性结构顶部是圆形轮廓头发颜色或者头顶的肤色在大多数场景下与背景形成对比。所以标注人头的时候边界框不需要框太大幅度紧贴头顶和脸颊两侧就行通常用正方形框比长方形框效果更好因为人头本身在图像中接近圆形。我在标注第一批数据的时候走过弯路框子画得太大把肩膀、衣领、背景都包进去了模型训练出来后检测框发散得厉害后来改成紧贴头部的标注方式精度一下子涨了好几个点。再说图像特征层面的差异。人头在图像中的纹理信息相对简单没有车灯、车牌、文字这种强特征靠的是边缘轮廓和肤色/发色对比度。这意味着模型对小尺寸目标的响应能力要求很高也意味着数据增强策略要有所倾斜随机裁剪、马赛克增强Mosaic这些手段能显著提升小目标检测能力但对人的框的扰动不能太大毕竟人头在画面里占比本来就小。在YOLOv9训练时我使用了Mosaic、MixUp、随机仿射变换等组合增强实测下来mAP0.5从0.86涨到0.91效果很直接。然后是后处理层面的差异。监控场景下人头检测通常伴随遮挡问题两个头挨得很近时模型可能只输出一个框或者输出两个完全重叠的框。这时候NMS非极大值抑制的阈值设置就很关键。YOLOv9默认的NMS阈值是0.45我在密集人群场景下调到了0.35减少了重叠框漏检的情况代价是偶尔会把同一个头重复检测两次所以后面又加了一个基于IoU的合并逻辑让重叠度超过0.8的框自动合并成一个。这些细节看着小但对最终计数准确率的影响非常大。1.3 项目包里到底包含哪些东西如何使用你拿到的这个压缩包我按功能把它分成了几个部分避免一盘散沙找不到东西。根目录下首先是runs/train/exp系列文件夹这里面保存的是训练过程中自动生成的日志、权重文件和评估图表。weights/best.pt就是训练过程中验证集表现最好的那一轮模型日常推理直接用这个就行weights/last.pt是最后一轮的模型一般不用。results.png是训练过程的损失曲线和指标曲线汇总图confusion_matrix.png是混淆矩阵PR_curve.png是精确率-召回率曲线。这些图表是判断模型好坏的第一手素材后面我会专门讲怎么看。源码方面核心是detect.py、train.py和val.py三个脚本如果你只是用来做检测detect.py足够了如果要把检测功能集成到自己的业务流程里那就把detect.py里的推理逻辑抽出来封装成函数调用。项目里还有一个requirements.txt列出了所有Python依赖包及其版本号照着装就行。很多人装环境这一步就卡住了后面我会给出具体的安装命令和避坑方法。再有一个需要注意的点项目包里那份“详细运行教程”是Markdown格式的说明文档建议先花十分钟从头到尾读一遍再动手。我遇到过不少用户上来就双击运行报错后再回头翻文档结果发现文档第一步就写了要用命令行运行根本没必要双击。先把文档捋一遍后面至少能少踩一半的坑。2. 环境准备与核心依赖搭建2.1 Python虚拟环境创建与依赖安装这套系统基于PyTorch框架Python版本建议3.8到3.11之间我用的是3.9.18搭配PyTorch 2.0.1。如果你用的是Python 3.12或者更高版本可能会遇到某些依赖包没有预编译版本的情况到时候需要自己编译非常折腾。所以如果是新环境直接装Python 3.9或者3.10最省事。我是这样创建虚拟环境的# 创建虚拟环境-n后面是环境名可以自定义 conda create -n yolo9 python3.9 # 激活环境 conda activate yolo9 # 进入项目目录 cd YOLOv9-head-detection # 安装依赖 pip install -r requirements.txtrequirements.txt里我锁定了这些关键版本torch 2.0.1、torchvision 0.15.2、opencv-python 4.8.1.78、numpy 1.24.4、matplotlib 3.7.2、tqdm 4.65.0、PyYAML 6.0。有几个容易踩坑的地方第一不要直接用pip install torch装CPU版本的PyTorch那样虽然能跑但推理速度会慢至少20倍。务必先到PyTorch官网复制和自己CUDA版本匹配的安装命令比如CUDA 11.8对应的命令是pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118。在Linux服务器上装的话用nvidia-smi看一下驱动支持的CUDA版本再选择对应的PyTorch版本这里不用追新稳定就行。第二OpenCV的版本影响不大但要注意它依赖的libgl库在Ubuntu上如果报libGL.so.1的错误执行apt-get install -y libgl1就能解决。Windows上一般没这个问题。第三如果机器显存不足低于6G建议在train.py里调低--batch-size不然很容易OOM。显存不够又不方便换机器的话可以暂时把图片尺寸从640降到512对精度的影响可能不到1个百分点但显存占用能少一半。2.2 项目目录结构与代码逻辑梳理YOLOv9官方仓库的目录结构其实挺规整的我基于它做了一些精简把跟人头检测不相关的文件挪到了not_used文件夹里让新手看起来没那么头大。核心结构如下YOLOv9-head-detection/ ├── ckpt/ # 训练好的模型权重 │ └── best.pt # 验证集最优权重 ├── data/ │ ├── head_custom.yaml # 数据集配置文件 │ └── images/ # 测试图片 ├── models/ # 模型结构定义 │ ├── yolov9-c.yaml │ ├── yolov9-e.yaml │ └── ... ├── runs/ │ └── train/ │ └── exp/ │ ├── weights/ │ │ ├── best.pt │ │ └── last.pt │ ├── results.png │ ├── confusion_matrix.png │ ├── PR_curve.png │ └── ... ├── train.py # 训练脚本 ├── detect.py # 推理脚本 ├── val.py # 评估脚本 └── requirements.txt实际使用中你只需要关心detect.py怎么改、data/head_custom.yaml怎么改。detect.py里有个parse_opt函数里面定义了一系列命令行参数常用的有--weights指定用哪个权重文件默认是ckpt/best.pt--source指定输入来源可以是图片路径、视频路径、摄像头编号比如0表示本地摄像头或文件夹路径--conf-thres置信度阈值默认0.25调高减少误检调低提高召回率--iou-thresNMS的IoU阈值默认0.45密集场景建议调低到0.35--imgsz推理时的图片尺寸默认640越大越慢但可能提升小目标召回--save-txt把检测结果以txt格式保存内含类别、坐标和置信度做计数统计时很有用--project和--name指定结果输出目录如果你想把检测能力集成到自己的项目里不用改detect.py直接把里面的run函数拆出来传入一张图片的numpy数组拿到的det结果就是所有检测框的坐标信息。我在实际工作中就是这么干的前端接一个视频流每一帧调用一次检测函数然后把坐标信息传给计数逻辑。2.3 训练好的模型验证与快速启动拿到压缩包后的第一件事不要急着训练先用我给的best.pt跑一遍推理确认环境和代码都没问题。我放了一张测试图片在data/images/test01.jpg你可以用下面的命令验证python detect.py --weights ckpt/best.pt --source data/images/test01.jpg --conf-thres 0.3 --save-txt这条命令会读取test01.jpg用best.pt进行推理然后把画好检测框的图片保存到runs/detect/exp目录下同时生成对应的txt文件。如果一切正常你会看到类似下面的输出image 1/1 data/images/test01.jpg: 384x640 12 persons, 1 head, 34.1ms Speed: 2.7ms pre-process, 34.1ms inference, 1.3ms NMS per image at shape (1, 3, 640, 640)注意看输出中的类别名称我训练的头检测模型在导出ONNX时会把类别命名为“head”。如果你看到的是person而不是head说明权重文件加载错了可能加载了官方预训练的COCO模型。这里要提醒一下YOLOv9官方仓库里自带的yolov9-c.pt是80类COCO数据集训练的模型里面包含person类但不包含独立的head类所以做纯人头检测必须用微调过的模型。3. 模型训练与评估指标详解3.1 训练数据集构建与标注细节训练一个好的人头检测模型数据质量比数据数量更重要。我这套系统用到了两个公开数据集的组合SCUT-HEAD华南理工大学公开的人头检测数据集和Brainwash数据集两个加起来将近两万张图片。但如果你要部署到自己的实际场景强烈建议至少采集几百张本场景的图片补进去做微调因为公开数据集的拍摄角度、摄像头高度和你的现场可能差距很大。比如教室里的摄像头俯拍和商场里的斜上方摄像头拍出来的人头形态就不一样。标注方面我用的工具是LabelImg格式导出为YOLO格式的txt文件每一行对应一个目标框格式为class_id x_center y_center width height这里的x_center、y_center、width、height都是相对于图片宽高的归一化坐标。比如0 0.532 0.418 0.186 0.186就表示目标框中心点在图片横向53.2%、纵向41.8%的位置框宽高都是图片尺寸的18.6%。注意是中心点坐标加宽高不是左上角右下角坐标这个格式和COCO数据集的坐标格式不一样写代码做数据转换时最容易搞混。数据集的目录结构我整理成了这样YOLOv9训练时可以直接读取datasets/ ├── images/ │ ├── train/ # 训练集图片 │ ├── val/ # 验证集图片 │ └── test/ # 测试集图片 └── labels/ ├── train/ # 训练集标注txt ├── val/ # 验证集标注txt └── test/ # 测试集标注txt图片和标注文件的文件名必须一一对应比如img0001.jpg对应img0001.txt一个都不能少。我自己曾因为图片格式不统一有的jpg有的png导致标注文件匹配不上最后写了个脚本批量统一成jpg了事。标注的时候有几个经验可以分享不要过度标注极端模糊的人头模型学到的是“清晰人头特征”硬塞模糊样本反而干扰特征表达。但如果你的应用场景本来就是远距离小目标那模糊样本又是必须的这里要结合场景权衡。遮挡严重的头如果遮挡面积超过50%建议不标遮挡少于50%的正常标模型对轻微遮挡是有一定鲁棒性的。边界框尽量紧贴目标的可见部分不要去脑补被遮挡部分的大小。标注完用脚本做一次数据校验检查有没有坐标越界、宽高为0的脏数据。这个检查很简单读txt文件判断每个数值是否在[0,1]区间内即可。3.2 train.py关键参数解析与训练命令YOLOv9的train.py参数很多但真正需要关心的就下面几个参数默认值说明我的推荐值--data无数据集配置文件路径data/head_custom.yaml--cfg无模型结构配置文件models/yolov9-c.yaml--weights无预训练权重建议用yolov9-c.ptyolov9-c.pt--epochs100训练轮数150-300--batch-size16批大小受显存限制尽量大--imgsz640输入图片尺寸640--device0使用哪块GPU0多卡用0,1,2,3--patience100早停轮数50--cacheFalse是否缓存图片到内存显存够就True数据集配置文件head_custom.yaml长这样# 数据集路径可以是绝对路径或相对路径 path: ./datasets train: images/train val: images/val test: images/test # 类别数 nc: 1 # 类别名称 names: [ head ]训练命令示例python train.py \ --data data/head_custom.yaml \ --cfg models/yolov9-c.yaml \ --weights yolov9-c.pt \ --epochs 200 \ --batch-size 16 \ --imgsz 640 \ --device 0这里有个很关键的选择--cfg用yolov9-c.yaml还是yolov9-e.yaml我实测下来c模型精度稍低但速度更快e模型精度更高但显存占用和推理延迟都会增加。对于人头检测这种基本在固定监控场景下运行的任务我用c模型就够了帧率能稳定在30FPS以上如果你对精度有更高要求且GPU不太拉胯可以换e模型试试。关于是否用预训练权重建议用官方在COCO上预训练好的yolov9-c.pt做初始化而不是从零开始训练。原因很简单人头检测虽然是单类别任务但COCO预训练模型已经学到了丰富的通用视觉特征边缘、纹理、形状等迁移过来只需要改最后的输出层训练收敛速度能达到从零训练的三到五倍最终精度也更高。这个坑我踩过最早试过从零训练150轮才勉强到0.82 mAP而加载预训练权重后同样轮数能到0.9以上。训练过程中YOLOv9默认会把每个epoch的验证集指标和模型权重保存到runs/train/exp目录。我一般会观察前30轮的loss曲线如果box_loss和cls_loss在下降且波动不大说明训练状态正常如果loss从一开始就异常偏高或者震荡剧烈我会立刻停止训练去检查数据标注是否有问题而不是继续烧时间。3.3 评估指标与曲线图深入解读训练结束后runs/train/exp目录下会生成一堆图表和日志。很多新手看到这些图一脸懵不知道看什么这里把重点讲清楚。首先是results.png它把训练过程中的loss曲线和指标曲线整合在一张图里。看这张图有四个核心关注点train/box_loss训练框回归损失正常应该在稳定下降并逐渐趋于平缓如果下降后又反弹说明学习率调太大模型在震荡。val/box_loss验证集框回归损失如果训练损失下降但验证损失不降反升说明过拟合了需要增加数据增强或者减小模型复杂度。metrics/precision和metrics/recall精确率和召回率两者在训练后期可能出现“跷跷板”现象一个高一个低这是正常的关键是看它们整体是否在提升。metrics/mAP_0.5和metrics/mAP_0.5:0.95mAP0.5是IoU阈值0.5时的平均精度mAP0.5:0.95是IoU从0.5到0.95步长0.05的十个阈值下的平均精度。前者更宽松反映大体定位能力后者更严格对框的精确位置要求更高。人头检测这类任务我主要看mAP0.5因为计数场景不需要框完美贴合目标只要框的位置大体正确计数逻辑就能正常工作。然后是PR_curve.png精确率-召回率曲线。曲线下的面积就是AP值曲线越接近右上角越好。这条曲线还告诉我们一个关键信息置信度阈值往哪个方向调更合理。如果你在曲线拐点之前就把置信度阈值设得很高那召回率会很低意味着很多人头漏检反过来阈值太低误检会很多。实际操作中我会在曲线上找precision和recall相对均衡的点把阈值设在那里。比如我训练出的模型在conf0.3左右precision和recall都在0.9附近那在线推理时就把--conf-thres设为0.3。还有一个容易被忽略的是confusion_matrix.png。正常情况下上面的head类别对角线取值应该很高比如0.9以上。如果background那行取值偏高说明模型把大量背景区域误判成了人头这时候需要增加负样本纯背景图片参与训练或者调高置信度阈值。如果在head行里出现了一部分值落在其他类别列上恭喜你数据里有标注错误回去检查数据吧。我训练完成后最终跑出来的指标是mAP0.5为0.918mAP0.5:0.95为0.702在NVIDIA GeForce RTX 3060上推理速度约18毫秒/帧约55FPS。这个指标用来做人流统计是够用的。3.4 影响训练效果的常见隐藏因素很多人模型训练效果差到怀疑人生其实并不是算法问题而是一些隐藏因素没处理好。根据我的经验最常见的有四个第一是类别不平衡问题。单类别检测任务很少遇到类别间不平衡但人头和背景的比例其实非常不平衡很多图片里人头只占图像面积的不到5%。这时候数据增强策略很重要YOLOv9的Mosaic增强会随机拼接四张图让模型看到更多不同尺度、不同位置的小目标对小目标检测有显著提升。我用Mosaic之后mAP直接涨了3个点。第二是学习率的设置。YOLOv9默认使用SGD优化器起始学习率0.01配合余弦退火调度。但如果你是二次微调在别人的head模型基础上再训练起始学习率建议降到0.001到0.002不然很容易把已有的好权重冲掉。第三是batch size对BN层的影响。Batch Normalization在batch size较大时统计均值方差更稳定如果batch size只有2或4BN的统计量会很不稳定影响训练效果。我训练时至少保证batch size大于8实在不行可以降低图片分辨率来换取更大的batch size。第四是验证集和训练集的分布一致性。如果你的训练集全是白天的监控画面验证集却包含了大量夜间画面那验证集指标一定难看。这是数据切分的问题不是训练的问题。我在切数据时按时间序列切前几天的图片进训练集后一天的图片进验证集这样验证集更接近实际部署的分布。4. 推理检测与计数逻辑实现4.1 单张图片的人头检测代码实现先上代码再解释细节。这是我从detect.py里抽出来的一个核心推理函数用来对单张图片做检测并返回检测框列表import cv2 import torch import numpy as np from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords def detect_heads(image_path, weightsckpt/best.pt, conf_thres0.3, iou_thres0.45): # 加载模型设备优先选择CUDA device torch.device(cuda if torch.cuda.is_available() else cpu) model attempt_load(weights, map_locationdevice) model.eval() # 读取图片并预处理 img0 cv2.imread(image_path) img cv2.resize(img0, (640, 640)) img img[:, :, ::-1].transpose(2, 0, 1) # BGR转RGBHWC转CHW img np.ascontiguousarray(img) img_tensor torch.from_numpy(img).to(device).float() / 255.0 img_tensor img_tensor.unsqueeze(0) # 加batch维度 # 推理 NMS with torch.no_grad(): pred model(img_tensor)[0] det non_max_suppression(pred, conf_thres, iou_thres)[0] # 将坐标从640x640映射回原图尺寸 boxes [] if det is not None and len(det): det[:, :4] scale_coords(img_tensor.shape[2:], det[:, :4], img0.shape).round() for *xyxy, conf, cls in det: x1, y1, x2, y2 [int(p.item()) for p in xyxy] boxes.append({bbox: [x1, y1, x2, y2], confidence: conf.item(), class: int(cls.item())}) return boxes, img0这段代码的流程很清晰加载模型、图片预处理、推理、NMS后处理、坐标还原。有几个值得注意的细节第一图片预处理时BGR转RGB和HWC转CHW的顺序Opencv读入的是BGR格式且维度是HWC但PyTorch模型期望的是RGB格式和CHW维度这两步颠倒会导致检测结果异常。我见过有人在这里出错检测框的位置完全错乱找了半天原因。第二scale_coords这一步必不可少。模型训练时的输入尺寸是640x640但原图大概率不是这个尺寸推理出来的是缩放后图上的坐标必须映射回原图尺寸才能正确画框。第三model.eval()一定要调用否则Dropout和BatchNorm在推理模式下还会按训练模式计算输出结果会不确定。这也是新手容易漏掉的一个小点。4.2 视频流实时人头计数实现单张图片检测只是基础实际应用中更多是视频流或摄像头的实时计数。我写了一个视频检测计数的脚本逻辑是先对每一帧做检测然后用一个简单的“中心点计数”策略来统计画面中的人头数量def count_heads_in_video(video_path, weightsckpt/best.pt): cap cv2.VideoCapture(video_path) model load_model(weights) while True: ret, frame cap.read() if not ret: break boxes, _ detect_heads_frame(frame, model) count len(boxes) # 画框和计数文字 for box in boxes: x1, y1, x2, y2 box[bbox] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, fHead Count: {count}, (20, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow(Head Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()但这里我先提醒一个非常重要的问题如果摄像头画面是固定的比如装在天花板上的监控直接用每帧检测到的目标数作为“总人数”是合理的因为每个人在同一时刻只会被检测到一次。但如果你的摄像头有视角转动或者视频里有人在画面中来回走同一时刻的检测数量会高估实际人数因为同一个人可能被重复计算了。所以如果你的场景是“统计画面中当前有多少人”直接计数即可但如果你的目标是“统计一个时间段内累计进入过多少人”那就要做跨帧跟踪了。最轻量级的方案是加上DeepSORT之类的跟踪算法给每个检测框分配一个ID然后统计不同ID的数量。这个方案在YOLOv9社区里也有很多现成配套我自己的系统里接的就是ByteTrack因为它在密集场景下比DeepSORT更稳。不过ByteTrack的细节比较多这里不展开后面有机会单独写一篇。还有一个要注意的点检测框的置信度阈值对计数结果影响极大。阈值设得太低椅子靠背、地上的圆形标志都可能被当成头计数虚高阈值设得太高被遮挡的人头可能漏检计数偏低。我建议你在部署现场用一段标定视频连续跑一段时间调整阈值到“漏检和误检的总量最小”的状态而不是盲目相信默认的0.25。4.3 区域计数与跨线计数的灵活扩展很多场景的要求并不是“全屏计数”而是“某个区域不能超过多少人”或者“某个门口过去了多少人”。这个其实就是在检测结果之上加一层业务逻辑不用改模型。区域计数的做法是先在画面中画一个多边形区域把区域顶点坐标存下来然后对每个检测框取中心点用射线法判断中心点是否在多边形内在区域内就计入该区域人数。射线法的代码实现网上很多直接用就可以关键是处理好框中心点落在区域边界上的边缘情况。跨线计数的经典做法是“双向线计数”在画面中画一条线段给每个检测目标的中心点分配一个ID并记录上一帧的位置如果检测目标中心点在当前帧和上一帧分别位于线段两侧就认定为目标穿越了一次线段。穿越方向通过比较目标中心点相对线段的位置关系判断。这个逻辑在超市、地铁站出入口的人流统计中非常常用。我实际部署过一个校园图书馆出入口的项目用的是两条平行线加方向判断进入和离开各计一次最终净流量等于进入数减去离开数。当时踩了一个坑当两个人并排快速通过时检测框的中心点可能会在相邻几帧里跳到另一个人身上导致跨线误判。后来我加了一个简单的约束目标ID变化后短时间内不重复计数误判率才降下来。这类后处理逻辑虽然写起来不复杂但对实际业务的准确性影响非常大做系统时一定要考虑进去。5. 常见问题与排查技巧实录5.1 环境安装报错排查表做这套系统的过程中我自己踩过的坑和网友遇到的高频问题整理成一张速查表照着排查能省不少时间报错信息原因解决办法ModuleNotFoundError: No module named torchPyTorch未安装或虚拟环境未激活执行conda activate yolo9并重新安装依赖AttributeError: NoneType object has no attribute shape图片路径错误cv2读取失败检查图片路径使用绝对路径确认文件名是jpg不是JPGCUDA out of memory显存不足调低--batch-size到4或2或调低--imgsz到512No such file or directory: yolov9-c.pt预训练权重没下载或者路径不对到官方仓库下载权重放到项目根目录检查文件名是否完全一致AssertionError: CUDA unavailablePyTorch是CPU版本重新安装CUDA版PyTorch如果机器没有NVIDIA GPU那就只能CPU跑速度慢但不影响运行cv2.error: OpenCV(4.8.1) ...OpenCV依赖库缺失Ubuntu执行apt-get install -y libgl1 libglib2.0-0RuntimeError: The size of tensor a (80) must match ...类别数不匹配模型和数据集配置不一致检查head_custom.yaml里的nc和names是否与训练时一致80说明还是COCO模型5.2 检测精度不理想时的优化顺序如果你训练完的模型在测试集上效果还行但一放到真实场景就各种漏检误检先别急着加数据或者换模型按照下面这个顺序排查第一步确认置信度阈值是否合适。用detect.py跑一段真实场景视频把--conf-thres分别设为0.15、0.25、0.35、0.5肉眼对比哪组的误检漏检总体最少。这一步花十五分钟就能完成但经常能解决90%的“精度差”问题因为很多时候不是模型不行而是阈值不对。第二步检查数据分布。真实场景的人头尺寸和训练集差距大不大如果训练集里人头大多占图片尺寸的10%以上但真实场景里人头只占3%那模型肯定识别不好需要补充该尺度的小目标样本重新微调。这是导致精度差的根本原因之一只能通过数据补强解决。第三步检查检测框偏移。如果检测框大小合适但位置整体偏上或偏下可能是标注不一致的问题。检查训练集标注里有没有大量把头顶以上空余部分框进去的情况有的话说明标注口径不统一需要在数据层面修正。第四步考虑更复杂的模型结构。如果以上都没问题但精度还是不够把模型从yolov9-c换成yolov9-e同时把--imgsz从640提到768。代价是推理速度下降但密集小目标场景下往往有2到3个点的mAP提升收益还是很明显的。5.3 推理速度优化与部署建议推理速度受三个因素影响最大模型结构、输入尺寸、推理框架。如果你跑完发现速度不达标从这三个角度依次优化。模型结构层面yolov9-c已经是相对轻量的选择如果还不够快可以尝试把train.py里的--cfg换成更小的yolov9-t.yamlTiny版本速度提升明显精度会掉一些。如果你的场景是密集人群计数不建议换Tiny小目标召回率会下降明显。输入尺寸层面从640降到480速度提升大约1.5倍但mAP可能会掉2到3个点。反过来如果速度足够、精度不够把输入尺寸提到768或896对小目标检测有明显帮助。这个参数要结合你的GPU和业务需求来调试。最值得投入的优化是推理框架替换。PyTorch的Eager模式推理效率其实不高换成ONNX Runtime或者TensorRT能收获很大提升。我做过一个对比试验同一张图片同一份权重PyTorch推理耗时约18毫秒ONNX Runtime约12毫秒TensorRT FP16约6毫秒。TensorRT的FP16推理是传统部署落地最常用的方案之一能把推理延迟压缩到原来的三分之一。具体做法是先把best.pt导出为ONNX格式python export.py --weights ckpt/best.pt --include onnx然后可以用TensorRT的trtexec工具把ONNX转为TensorRT引擎文件。这里要注意TensorRT的优化是跟具体GPU型号绑定的在一台机器上生成的引擎文件拿到另一台机器上大概率不能直接用需要在部署机器上重新生成。5.4 模型权重跨平台部署时的坑最后说一个经常被忽视的问题模型权重文件格式在不同框架之间转换时类名和输出张量结构容易搞混。用ONNX导出后模型输出的张量形状是[batch, num_anchors, num_classes 5]其中5表示x、y、w、h和objectness分数后面才是每个类别的置信度。如果你的部署代码在解析输出时把维度假设错了轻则检测结果错乱重则程序直接崩溃。另一个常见坑是图片归一化。PyTorch训练时图片像素要除以255归一化到[0,1]但很多部署框架默认输入是[0,255]的整型数据。如果部署代码没做归一化检测置信度会全部变成极低值看起来像是模型坏了实际上是数据预处理不一致。这两个问题我在帮朋友排查部署问题时遇到过好几次写在这里提醒大家。6. 写在最后的一些个人体会这套人头识别计数系统从数据整理到模型训练再到实际部署我前后改了三版才稳定下来。第一版用了最原始的YOLOv5精度其实也够但密集场景下漏检偏多第二版换了YOLOv8速度提升明显但小目标检测还是差口气直到YOLOv9发布后把PGI机制加进来密集人头这种小目标场景才真正稳住了。如果你现在正要开始做类似的检测计数项目我的建议是不必追求最新最好的模型但一定要搞清楚你的部署场景最缺什么。如果是密集小目标YOLOv9值得一试如果是纯追求速度更轻量的模型可能更适合你。最后再分享一个小技巧训练完成后不要急着把模型拿去部署先在验证集上跑一遍val.py拿到精确率、召回率、F1分数再在自己的测试视频上跑一遍记录实际漏检和误检的画面片段然后针对性地补数据、调阈值。这套流程走下来你的系统不会差到哪里去。希望这篇经验对你有用有问题欢迎在评论区交流。本文还有配套的精品资源点击获取