拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于改进YOLOv5的人群密度检测:FasterNet主干与Soft-NMS优化实践

简介这份资源面向深度学习与计算机视觉方向的学习者提供一套基于改进YOLOv5的人群密度检测系统完整实现方案可用于商场、车站、体育场等人流密集场景的实时计数与安全监控研究。方案以FasterNet替换原YOLOv5主干网络提升推理速度并引入Soft-NMS缓解重叠目标漏检、采用最优运输分配OTA优化损失函数兼顾精度与实时性。压缩包共267个文件约19.77MB包含53个Python源码、49个YAML配置、28个JavaScript与26个map前端资源以及JPG、PNG图像样本、Markdown说明、Dockerfile部署脚本和模型权重文件覆盖数据采集、预处理、模型推理到结果解析的完整链路。目前已有205人学习下载。读者可据此复现改进模型、理解主干替换与损失函数优化思路并借助Docker配置快速搭建训练与部署环境适合作为课程设计、毕业设计或工程落地的参考。1. 从一次商场人流告警翻车说起这套改进 YOLOv5 人群密度检测系统到底能干什么去年帮朋友看一个商场中庭的客流统计项目他们原本用传统帧差法加人头检测结果一到周末下午就疯狂误报——玻璃幕墙反光被当成密集人群真正拥挤的区域反而因为遮挡漏检。后来换成基于 YOLOv5 的方案又遇到新问题密集场景下 NMS 把挨得近的人头框直接删掉计数偏低三成。这套「基于 YOLOv5 的人群密度检测系统设计与实现」资源恰好就是冲着这两个痛点去的主干换成 FasterNet 提升推理速度用 Soft-NMS 缓解密集遮挡漏检再用 OTA 最优运输分配优化正负样本匹配让损失函数在人群这种小目标密集场景下更合理。它适合做安防监控、智慧交通、商场客流分析的工程师也适合正在拿 YOLOv5 做课设或毕设、想找一个能跑通且带改进点的完整工程参考的人。资源包里能看到 Dockerfile、Dockerfile-arm64、Dockerfile-cpu 三套部署文件说明作者考虑过 x86 服务器和树莓派 5 这类边缘设备两种落地路径不是只丢一个 notebook 就完事。2. FasterNet 主干替换与 Soft-NMS、OTA 的工程落地逻辑2.1 为什么人群密度场景要动主干网络YOLOv5 原版主干是 CSPDarknet53在 COCO 这类通用数据集上表现均衡但人群密度检测有两个特殊约束一是画面里目标数量可能上百二是很多场景要求 30 FPS 以上的实时性。CSPDarknet53 的残差堆叠在边缘设备上算力吃紧常见做法是换成更轻的主干。FasterNet 的核心是 PConv部分卷积只对部分通道做空间卷积其余通道保留不动这样 FLOPs 降下来但特征提取能力不至于崩。我一般会先把主干替换后的模型在验证集上跑一遍 mAP 和 FPS 对比确认精度掉点不超过 2% 再继续调后面。替换主干不是改个类名就完事涉及通道数对齐、特征图尺寸匹配、预训练权重加载三个环节。下面这段是常见的主干替换骨架把 FasterNet 的四个 stage 输出接到 YOLOv5 的 Neck 上# models/backbone/fasternet.py import torch import torch.nn as nn class PConv(nn.Module): 部分卷积只对 1/4 通道做 3x3 卷积其余通道恒等映射 def __init__(self, dim): super().__init__() self.dim_conv dim // 4 # 参与卷积的通道数 self.dim_untouched dim - self.dim_conv self.conv nn.Conv2d(self.dim_conv, self.dim_conv, 3, padding1, biasFalse) self.bn nn.BatchNorm2d(self.dim_conv) def forward(self, x): x1, x2 torch.split(x, [self.dim_conv, self.dim_untouched], dim1) x1 self.bn(self.conv(x1)) return torch.cat((x1, x2), dim1) class FasterNetBlock(nn.Module): def __init__(self, dim): super().__init__() self.pconv PConv(dim) self.pwconv1 nn.Conv2d(dim, dim * 2, 1) # 逐点卷积升维 self.pwconv2 nn.Conv2d(dim * 2, dim, 1) # 降回原维度 self.act nn.GELU() def forward(self, x): residual x x self.pconv(x) x self.act(self.pwconv1(x)) x self.pwconv2(x) return x residual逻辑说明PConv 把通道切成两部分只对前 1/4 做卷积后 3/4 直接透传这样计算量约为普通卷积的 1/4。FasterNetBlock 里先 PConv 做局部特征再用两个 1x1 卷积做通道混合最后残差相加。参数上dim // 4这个比例可以调我试过 1/2 和 1/41/4 在树莓派 5 上帧率提升最明显但 mAP 会掉 1.5 个点左右需要根据你的硬件权衡。2.2 Soft-NMS 在密集人头框上的参数怎么设传统 NMS 是硬阈值IoU 超过阈值直接删框。人群密集时两个人头框 IoU 可能到 0.5 以上硬删就漏检。Soft-NMS 改成给重叠框降分而不是删除降分函数常见两种线性加权和高斯加权。高斯形式对密集场景更友好公式里有个 sigma 参数控制衰减速度。# utils/soft_nms.py import torch def soft_nms(boxes, scores, sigma0.5, score_threshold0.001): boxes: (N, 4) xyxy 格式 scores: (N,) sigma: 高斯衰减系数越大衰减越慢密集场景建议 0.5~0.7 N boxes.size(0) for i in range(N): max_idx torch.argmax(scores[i:]) i boxes[i], boxes[max_idx] boxes[max_idx].clone(), boxes[i].clone() scores[i], scores[max_idx] scores[max_idx].clone(), scores[i].clone() iou compute_iou(boxes[i].unsqueeze(0), boxes[i1:]) # 高斯衰减IoU 越大分数压得越狠但不归零 weight torch.exp(-(iou * iou) / sigma) scores[i1:] * weight # 低于阈值的框后续不再参与 keep scores[i1:] score_threshold boxes[i1:] boxes[i1:][keep] scores[i1:] scores[i1:][keep] N boxes.size(0) return boxes, scores逻辑说明每轮选当前最高分框计算它和剩余框的 IoU用高斯函数生成衰减权重乘到剩余框分数上。sigma 是关键参数设 0.3 衰减太快接近原版 NMS设 0.7 又太软会留下大量重复框。我在商场数据集上实测 0.5 比较稳计数误差从硬 NMS 的 -28% 降到 -6% 左右。注意 Soft-NMS 是串行操作比原版 NMS 慢如果帧率吃紧可以只在检测头输出后对 score 前 300 个框做不要全量跑。2.3 OTA 损失函数分配对训练收敛的影响OTAOptimal Transport Assignment解决的是「哪个预测框该负责哪个真实框」的问题。原版 YOLOv5 用跨网格搜索加宽高比匹配人群场景下小目标多一个真实框可能被多个预测框同时认领或者干脆没人认领。OTA 把它建模成最优运输问题把真实框看作供应方预测框看作需求方运输成本是分类和回归损失的加权用 Sinkhorn 迭代求最优分配矩阵。# utils/ota_assigner.py import torch def ota_assign(pred_scores, pred_boxes, gt_boxes, gt_labels, cost_cls1.0, cost_reg3.0, sinkhorn_iter50): pred_scores: (num_pred, num_classes) pred_boxes: (num_pred, 4) gt_boxes: (num_gt, 4) cost_reg 权重通常设 3.0回归比分类更重要 num_pred, num_gt pred_boxes.size(0), gt_boxes.size(0) # 分类成本负对数似然 cls_cost -torch.log(pred_scores[:, gt_labels] 1e-8) # 回归成本IoU 损失 iou compute_iou_matrix(pred_boxes, gt_boxes) reg_cost 1 - iou cost cost_cls * cls_cost cost_reg * reg_cost # Sinkhorn 迭代求最优运输方案 u torch.zeros(num_pred, 1) v torch.zeros(1, num_gt) for _ in range(sinkhorn_iter): u -cost u # 行归一化 v -cost v # 列归一化 assign_matrix torch.exp(-(cost u v)) return assign_matrix逻辑说明cost_reg 设 3.0 是常见做法因为回归精度对检测框位置影响更大。Sinkhorn 迭代次数 50 是精度和速度的折中设 20 也能收敛但分配矩阵会糙一些。OTA 训练初期收敛比原版慢因为分配矩阵在动态调整但后期 mAP 通常能高 1-2 个点。如果显存不够可以把 num_gt 限制在每张图最多 100 个超出部分随机采样。3. 从 Dockerfile 到推理脚本把系统跑起来的完整步骤3.1 三套 Dockerfile 的选型与构建命令资源包里给了 Dockerfile、Dockerfile-arm64、Dockerfile-cpu 三个文件这不是冗余是三种部署场景。x86 服务器带 GPU 用默认 Dockerfile树莓派 5 或 Jetson 用 arm64 版本纯 CPU 推理用 cpu 版本。构建前先确认你的 Docker 版本支持 buildx否则 arm64 镜像在 x86 上构建会报 exec format error。# x86 GPU 构建 docker build -f Dockerfile -t yolov5-crowd:gpu . # 树莓派 5 / arm64 构建在 arm 设备上直接跑 docker build -f Dockerfile-arm64 -t yolov5-crowd:arm64 . # 纯 CPU 构建 docker build -f Dockerfile-cpu -t yolov5-crowd:cpu . # 启动推理容器挂载模型和测试图片 docker run --gpus all -it --rm \ -v $(pwd)/weights:/app/weights \ -v $(pwd)/data:/app/data \ yolov5-crowd:gpu \ python detect.py --weights weights/best.pt --source data/test.jpg参数说明--gpus all只在 GPU 版本需要CPU 版本去掉。-v挂载权重目录和测试数据目录避免每次改文件都重建镜像。如果树莓派上跑把--gpus all换成--device /dev/video0接摄像头。常见坑是 arm64 镜像里 PyTorch 版本和系统架构不匹配构建时看 pip install 那层有没有报错有的话把 torch 换成官方 arm64 wheel 源。3.2 数据准备与 results.csv 里的训练曲线怎么读资源里有个 results.csv这是 YOLOv5 训练过程自动生成的日志每行一个 epoch列包括 train/box_loss、train/obj_loss、train/cls_loss、metrics/mAP_0.5、metrics/mAP_0.5:0.95 等。很多人训练完只看最后一行 mAP其实中间曲线更能说明问题。列名含义正常趋势train/box_loss边界框回归损失持续下降后期趋平train/obj_loss目标置信度损失下降但可能震荡train/cls_loss分类损失下降人群场景下降较慢metrics/mAP_0.5IoU0.5 时 mAP上升后期波动小metrics/mAP_0.5:0.95多 IoU 阈值平均上升通常比 mAP_0.5 低 15-20 个点如果 box_loss 下降但 mAP 不涨常见原因是过拟合看验证集 loss 是否开始上升。如果 obj_loss 一直震荡可能是学习率太大或 batch size 太小人群数据集建议 batch 至少 16。results.csv 可以直接用 pandas 读出来画图import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(results.csv) df.columns df.columns.str.strip() # 列名可能带空格 fig, ax1 plt.subplots() ax1.plot(df[epoch], df[train/box_loss], labelbox_loss) ax1.plot(df[epoch], df[train/obj_loss], labelobj_loss) ax1.set_xlabel(epoch) ax1.set_ylabel(loss) ax2 ax1.twinx() ax2.plot(df[epoch], df[metrics/mAP_0.5], r-, labelmAP0.5) ax2.set_ylabel(mAP) plt.legend() plt.savefig(train_curve.png)逻辑说明双 y 轴把 loss 和 mAP 画在一起方便看拐点。df.columns.str.strip()这行别省YOLOv5 某些版本的 csv 列名前后有空格不处理会 KeyError。如果 mAP 在第 50 epoch 后基本不动可以提前停不用跑满 300 epoch。3.3 推理脚本与计数逻辑的对接检测框出来之后要转成人群密度数据常见做法是直接数框数量但密集场景下框会重叠需要按面积加权或做密度图。下面这段是推理加计数的骨架import cv2 import torch from models.experimental import attempt_load from utils.general import non_max_suppression model attempt_load(weights/best.pt, map_locationcpu) model.eval() img cv2.imread(data/test.jpg) img cv2.resize(img, (640, 640)) tensor torch.from_numpy(img).permute(2, 0, 1).float().unsqueeze(0) / 255.0 with torch.no_grad(): pred model(tensor)[0] # conf_thres 人群场景建议 0.25iou_thres 配合 Soft-NMS 设 0.6 det non_max_suppression(pred, conf_thres0.25, iou_thres0.6)[0] count len(det) if det is not None else 0 # 按框面积加权小框可能是误检权重降低 if det is not None: areas (det[:, 2] - det[:, 0]) * (det[:, 3] - det[:, 1]) weights areas / areas.max() density_score weights.sum().item() else: density_score 0 print(f人数估计: {count}, 密度加权分: {density_score:.2f})逻辑说明conf_thres 设 0.25 是人群场景的常见起点设太高会漏掉被遮挡的人头。iou_thres 设 0.6 配合 Soft-NMS 使用如果用的是原版 NMS 建议降到 0.45。密度加权分用框面积归一化面积小的框权重低能一定程度抑制误检。实际部署时可以把 density_score 映射成三级告警小于 10 正常10-30 注意大于 30 拥挤。4. 避坑与排查训练和部署中最容易翻车的五个点4.1 现象替换主干后模型加载预训练权重报 size mismatch原因FasterNet 的通道数和 CSPDarknet53 不一致直接 load_state_dict 会对不上。解决只加载主干部分权重Neck 和 Head 随机初始化或者用 strictFalse 加载并打印缺失层。ckpt torch.load(yolov5s.pt, map_locationcpu) model_dict model.state_dict() # 只保留 shape 一致的层 filtered {k: v for k, v in ckpt[model].items() if k in model_dict and v.shape model_dict[k].shape} model_dict.update(filtered) model.load_state_dict(model_dict) print(f加载了 {len(filtered)}/{len(model_dict)} 层权重)4.2 现象Soft-NMS 后处理耗时超过推理本身原因Soft-NMS 是串行循环框越多越慢。解决限制参与 Soft-NMS 的框数量只对 score 前 300 个做其余直接按阈值过滤。另外可以把 sigma 调小到 0.4 减少迭代轮数。4.3 现象树莓派 5 上 Docker 构建到 torch 安装就卡死原因arm64 的 PyTorch wheel 体积大默认 pip 源慢且树莓派内存可能不够编译。解决用预编译的 arm64 wheelDockerfile 里加--no-cache-dir并把 pip 源换成国内镜像。如果内存只有 4G构建时加 swap。4.4 现象results.csv 里 mAP 一直是 0原因验证集路径配错或者 label 格式不是 YOLO 的 txt 格式。解决检查 data.yaml 里 val 路径确认每个图片有对应 txttxt 里每行是class x_center y_center w h且归一化到 0-1。用ls labels/val | head和cat labels/val/xxx.txt快速确认。4.5 现象OTA 训练 loss 出现 NaN原因Sinkhorn 迭代中 exp 溢出或者 cost 矩阵有 inf。解决在 exp 前做 clamp把 cost 限制在 [-50, 50]并检查 gt_boxes 是否有宽高为 0 的框。另外学习率 warmup 阶段设长一点前 3 个 epoch 用 0.001 起步。5. 进阶技巧用密度图替代计数框做区域告警框计数只能告诉你「有多少人」但商场中庭真正需要的是「哪个区域在变挤」。我后来在这套系统上加了一层把检测框中心点做高斯核密度估计生成密度热力图再按预设的 ROI 区域求和。这样即使两个人头框重叠被 Soft-NMS 合并成一个密度图上的峰值也不会丢。import numpy as np from scipy.ndimage import gaussian_filter def density_map(det, img_shape(640, 640), sigma15): 把检测框中心点转成密度热力图 h, w img_shape heat np.zeros((h, w), dtypenp.float32) if det is None or len(det) 0: return heat for box in det: cx int((box[0] box[2]) / 2) cy int((box[1] box[3]) / 2) if 0 cx w and 0 cy h: heat[cy, cx] 1 # sigma 控制热力扩散范围15 对应约 30 像素半径 return gaussian_filter(heat, sigmasigma) # ROI 区域求和假设中庭区域是 (200,200) 到 (440,440) heat density_map(det) roi_density heat[200:440, 200:440].sum() print(f中庭区域密度分: {roi_density:.1f})逻辑说明sigma 设 15 是经验值画面 640x640 时对应约 30 像素扩散半径太小热力图会碎太大区域间会糊在一起。ROI 坐标根据你的摄像头安装位置标定标一次就行。这个密度分比单纯计数更稳因为高斯核把邻近的人头能量叠加了不会因为 NMS 合并而突变。从那以后我每次部署人群检测系统都强制走一遍「框计数 密度图 ROI 求和」双通道验证两个数差超过 20% 就回去查 NMS 参数和置信度阈值。这套改进 YOLOv5 的资源把 FasterNet、Soft-NMS、OTA 三个改进点都落到了代码层面Dockerfile 也覆盖了服务器和边缘设备拿来当基线改比从零搭省至少两周。希望帮到你。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门