☰
基于深度学习的密度图人流量检测:PyTorch实现与毕设指南
2026/9/30 4:50:03 网站建设 项目流程

简介:本资源为面向毕业设计、课程设计及论文写作的人流量检测方向参考文档,适合计算机视觉、深度学习初学者及高校师生使用。文档以MobileNet-SSD轻量级检测模型为核心,系统介绍从数据集构建、模型训练到行人检测与追踪的完整流程,并给出公园、文化广场及传染病疫情期间疏散人群等典型应用场景。内容同时涵盖网络结构细节、开发环境配置与实验验证结果,可作为项目开发、论文撰写或答辩准备的有力参照。资源包共1个文件,为docx格式,约224KB,便于直接阅读与引用。目前已有81人学习下载,适合希望系统掌握轻量级目标检测落地思路的读者进一步研读。

1. 从选题到落地:人流量检测方法在毕设论文里到底要做什么

“基于深度学习的人流量检测方法”这个选题,放在毕设或课设里,目标不是训练一个能画框的目标检测器,而是形成一条“输入视频帧、输出人数”的完整链路。很多人选完题直接去跑YOLO,跑完才发现密集场景下计数偏差大得没法写进论文;换个角度,把任务定义成密度回归,人流量检测会顺手很多。这篇笔记按一个三周能跑通主线、一周补实验的课程设计节奏来讲:先选技术路线,再做数据集,再用PyTorch训练和评估,最后给出一组避坑经验。适合想把深度学习毕设做成完整闭环、GPU预算又有限的读者。

2. 先定技术路线:检测、密度估计还是多任务

2.1 三种主流做法怎么选:计数任务不是检测任务的子集

目标检测给的是每个人头的边界框,把框的数量数出来就得到人数,这个逻辑在稀疏场景没问题,但人流密集时会出现两个连锁问题:一是NMS会把两个重叠行人的框压成一个,二是低置信度行人被置信度阈值直接滤掉。地铁车厢、校门口早高峰这类密集小目标场景,漏检率一点就会被放大到10%以上,论文里很难解释。

密度估计换了一条路:不判断“这里有几个人”,而是为每个头部点生成一个高斯核,让模型去回归整个密度分布,最后对密度图积分得到总人数。多任务方案则把两条路合并,在检测分支基础上增加密度图分支,两个输出互相约束,精度更高,但工程量和训练难度也更高。我一般会先问自己:毕设的核心创新点到底放在哪里。如果放在应用验证,检测方案足够;如果放在方法改进,密度图是性价比最高的起点。

另一个差异在标注成本。检测方案需要画完整边界框,密度图方案只需要点标注头部中心。点标注的速度大约比框标注快三分之一到一半,对时间紧张的毕设来说,这是实打实的优势。推理阶段,检测要做NMS后处理,密度图是纯卷积推理,更容易压到视频流的实时帧率。这些差异都可以写进论文的需求分析和方案对比章节,比空泛地写“深度学习技术具有优势”要有说服力。

2.2 密度图方案的理论闭环:高斯核如何把点标注变成回归目标

密度图的核心思路是:假设第i个行人头部中心坐标为x_i,人群分布可以用delta函数叠加为F(x)=Σδ(x-x_i)。这个离散点序列没法直接作为回归目标,于是把它与二维高斯核做卷积,得到密度图:

D(x)=ΣG_σi(x-x_i)

其中G_σi是标准差为σi的二维高斯核。式子里隐含着一个关键性质:对全图积分,密度图的累积和等于总人数。也就是说,训练阶段用MSE让预测密度图逼近真实密度图,评估阶段直接对预测密度图求和,两个环节用的是同一个数学信号,闭环非常干净。

人群稀疏时,固定一个σ就够了,通常取3到5个像素。密集场景下固定σ会出问题:远处人头在图像里只有十几个像素,近处人头占几十个像素,同一个σ必然顾此失彼。这时候要用MCNN提出的几何自适应高斯核:对每个头部点求它到最近k个头部点的平均距离d_i,取σ_i=β*d_i,β一般取0.3,k取3或4。这样远处密集人群的核自动变小,近处稀疏人群的核变大,与透视关系基本匹配。自适应核这部分内容,写进论文的数据预处理小节时,可以作为一条明确的“方法改进点”。

2.3 选型对照表与论文写作的切入角度

方案计数精度标注成本训练难度论文展开空间适用场景
目标检测稀疏场景高,密集场景下降快需要边界框标注中低,偏工程复现商场出入口、校园道路
密度回归密集场景稳定,稀疏场景也可用只要头部点标注中高高,可在核函数/网络结构上改进地铁站、景区、集会
检测+密度多任务综合最好,两个输出互相纠错框+点标注高高,可设计联合损失需要位置和数量同时输出

如果选密度图方案,论文的章节建议按“密度图生成方法 → 网络结构设计 → 实验对比 → 场景验证”来排。这个结构更接近学术论文的叙事方式,指导老师也更愿意接受。对比实验至少要挑1到2个公开方法,在同一个测试集上重跑,而不是直接抄原论文里的数字,因为数据划分不同,直接抄数字在答辩时经不起问。你还可以把“检测方案在密集帧的NMS丢框问题”作为对比实验的一个观察点,用几组数据说明为什么密度图方案更适合人流量检测。

3. 数据先行:行人数据集的采集、标注与密度图生成

3.1 公开数据集与自建数据怎么搭配

公开人群计数数据集里,ShanghaiTech Part_A/Part_B是最常用的组合:Part_A是密集人群,Part_B是稀疏街景,两者交叉验证能看出方法在不同密度下的表现。UCF-QNRF的规模更大、密度更高,适合用来写泛化性实验;Mall数据集是室内固定摄像头视角,适合做场景化验证。下载后先确认标注格式,因为有的是MAT文件,有的是JSON,有的是XML,不统一。

自建数据建议抽帧标注,不要每帧都标。每隔10帧抽一帧,每帧标出所有可见头部中心,500到1000个行人量级就够用了。自采数据的价值不在数量,而在“场景验证”:论文里写“对自采视频人工抽样核对10帧,平均误差在10%以内”,比单纯在公开数据集上报一个MAE更有落地感。标注时有三条清洗原则:人物头部小于10像素的不标,遮挡超过80%的不标,图像边界上只有半个头且没有完整轮廓的不标。这些原则要写进论文的数据处理部分,能增加可信度。

3.2 用Python把XML标注转成密度图:脚本与参数

如果你的标注工具输出的是Pascal VOC格式XML,每个object是person的bndbox,而密度估计需要的是头部中心点。这里有一个通用做法:取bbox上部大约15%处作为头部点位置,因为行人框中心往往落在躯干上,直接取中心会把身体部分算进头部,引入计数偏差。下面的脚本把XML解析成点坐标列表,再生成密度图。

import os import cv2 import numpy as np import xml.dom.minidom as minidom def parse_xml_heads(xml_path): """ 解析VOC标注XML,返回头部中心点坐标列表 [(x, y), ...] """ dom = minidom.parse(xml_path) objs = dom.getElementsByTagName('object') points = [] for obj in objs: bndbox = obj.getElementsByTagName('bndbox')[0] xmin = float(bndbox.getElementsByTagName('xmin')[0].firstChild.data) ymin = float(bndbox.getElementsByTagName('ymin')[0].firstChild.data) xmax = float(bndbox.getElementsByTagName('xmax')[0].firstChild.data) ymax = float(bndbox.getElementsByTagName('ymax')[0].firstChild.data) # 头部中心点取框顶部偏下一点,而不是框中心 cx = (xmin + xmax) / 2.0 cy = ymin + (ymax - ymin) * 0.15 points.append((cx, cy)) return points def make_density_map(shape, points, sigma=4.0): """ 将点标注展开为密度图,返回与图像形状一致的float32矩阵 """ density = np.zeros(shape[:2], dtype=np.float32) h, w = shape[:2] radius = int(sigma * 3) for x, y in points: if not (0 <= x < w and 0 <= y < h): continue x_lo = max(0, int(x) - radius) x_hi = min(w, int(x) + radius + 1) y_lo = max(0, int(y) - radius) y_hi = min(h, int(y) + radius + 1) xs = np.arange(x_lo, x_hi, dtype=np.float32) ys = np.arange(y_lo, y_hi, dtype=np.float32) gx = np.exp(-(xs - x) ** 2 / (2 * sigma ** 2)) gy = np.exp(-(ys - y) ** 2 / (2 * sigma ** 2)) density[y_lo:y_hi, x_lo:x_hi] += np.outer(gy, gx) return density

逻辑说明:脚本把二维高斯核拆成行向量和列向量的外积,比逐像素meshgrid更快,也更省内存。每个头部点的高斯值只叠加到一个局部窗口内,窗口半径取3倍sigma,因为高斯核在3倍标准差外贡献已经可以忽略。多个点距离较近时,密度值自然叠加,因此密度图积分仍然等于人数。

参数说明:sigma=4适合摄像头离人群中等距离的场景;俯视摄像头建议sigma=2左右;平视近距场景可以取5到6。radius取int(sigma*3),控制局部窗口大小。如果你的标注是纯头部点而不是行人框,直接把parse_xml_heads换成读取点坐标即可,后面流程完全一样。

3.3 增广必须做同步变换:翻转、裁剪、缩放的三条规则

密度图不是分类标签,它是一张数值分布图,增广时最容易出错的是只改图像不改密度图。我见过有人只对图像做随机翻转,训练时loss还在下降,测试计数却明显偏差,因为模型一直在拟合一对不一致的目标。正确做法是图像和密度图永远做同一个几何变换,三条规则要同时遵守。

第一条,随机翻转时图像镜像,密度图也要镜像。第二条,随机裁剪时裁同一个区域。第三条,缩放时要特别注意积分修正。如果直接对密度图做resize,需要乘一个面积修正系数,否则积分会变。下面这段代码演示了缩放同步:

def sync_resize(img, density, size): h, w = img.shape[:2] new_w, new_h = size scale = min(new_w / w, new_h / h) img = cv2.resize(img, (int(w * scale), int(h * scale))) density = cv2.resize(density, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_LINEAR) # 密度图resize后像素数量变了,要乘修正系数保证积分等于人数 density = density / (scale ** 2) return img, density

缩放系数scale是新尺寸与旧尺寸的比值。如果图像缩小一半,scale等于0.5,密度图每个像素代表的人数需要增大到原来的4倍,代码里用除以scale的平方来修正。更省事的做法是:先把图像resize到统一尺寸,再基于原始点坐标和更新后的sigma直接生成密度图,这样完全绕开积分修正问题。我一般推荐后者,逻辑更清晰,也不会在训练阶段引入额外误差。

4. 用PyTorch训练一个人流密度回归模型:网络、参数与评估

4.1 一个显存友好的MCNN变体网络

MCNN是经典的人群计数模型,用三列不同卷积核尺寸并行提取特征。完整版包含多次池化和上采样,显存占用偏高。如果只有4G显存,可以在保留多列结构的前提下做简化:去掉全连接层、减小通道数、把特征融合改成1x1卷积,最后用双线性上采样恢复到原图尺寸。

import torch import torch.nn as nn import torch.nn.functional as F class SimpleMCNN(nn.Module): def __init__(self): super(SimpleMCNN, self).__init__() # 三列不同感受野,分别对应小头、肩部、小群体尺度 self.branch1 = nn.Sequential( nn.Conv2d(3, 20, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(20, 24, kernel_size=3, padding=1), nn.ReLU(inplace=True) ) self.branch2 = nn.Sequential( nn.Conv2d(3, 20, kernel_size=5, padding=2), nn.ReLU(inplace=True), nn.Conv2d(20, 24, kernel_size=5, padding=2), nn.ReLU(inplace=True) ) self.branch3 = nn.Sequential( nn.Conv2d(3, 20, kernel_size=7, padding=3), nn.ReLU(inplace=True), nn.Conv2d(20, 24, kernel_size=7, padding=3), nn.ReLU(inplace=True) ) self.merge = nn.Sequential( nn.Conv2d(72, 24, kernel_size=1), nn.ReLU(inplace=True), nn.Conv2d(24, 1, kernel_size=1) ) # 将特征图恢复到与输入密度图一致的尺寸 self.upsample = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False) def forward(self, x): b1 = self.branch1(x) b2 = self.branch2(x) b3 = self.branch3(x) feat = torch.cat([b1, b2, b3], dim=1) out = self.merge(feat) out = self.upsample(out) return out

逻辑说明:三个分支的卷积核大小分别取3、5、7,模拟小、中、大三种感受野。branch1适合捕捉单个人头轮廓,branch3能覆盖小群体的上下文信息。三个输出在通道维拼接后,用1x1卷积做融合,参数量比原始MCNN小很多。forward最后的上采样是为了让输出分辨率与输入一致,计算损失时不需要再做插值对齐。

参数说明:分支里的通道数直接决定显存占用量。把20和24改成16和20,大约能再省20%显存,代价是计数精度略微下降。如果输入是512x512,这个模型在4G显存的显卡上可以跑batch_size=1;输入降到384x384,能跑batch_size=4。

4.2 训练脚本与关键超参数:从环境配置到出曲线

环境配置是新手的第一个坎。建议用conda建一个Python 3.8环境,再按自己电脑的CUDA版本从PyTorch官网复制对应的安装命令。没有独立显卡也能跑,只是耗时长一些,把图像resize到512x512、batch_size设为1即可。数据加载器要同时输出图像和密度图,图像归一化到0到1之间,密度图保持float32。

训练主循环里,优化器用Adam,学习率从1e-4起步,损失用MSELoss。这是密度回归里最稳的一组初始配置。如果loss在前几个epoch不下降,优先检查密度图是不是有大量全零区域,背景像素会把loss主导住,这时要给密度图加一个小常数再参与计算,或者检查预处理阶段是否把密度图读丢了。

model = SimpleMCNN().cuda() criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-4) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.5, patience=3) for epoch in range(40): model.train() total_loss = 0.0 for img, density in train_loader: img, density = img.cuda(), density.cuda() pred = model(img) loss = criterion(pred, density) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() mae, mse = evaluate(model, val_loader) scheduler.step(mae) print(f"epoch {epoch:02d}, loss {total_loss/len(train_loader):.6f}, " f"MAE {mae:.2f}, MSE {mse:.2f}")

参数说明:batch_size建议为1,因为密度图是稠密预测,显存随图面积增长。想要大batch效果,可以用梯度累积,每8步做一次optimizer.step,等价于batch_size=8的效果。ReduceLROnPlateau会在验证MAE连续3轮不降时把学习率乘以0.5,这是密度回归任务里比较实用的调节方式。

如果想在损失函数上做文章,可以加一个计数一致性损失:把预测密度图积分得到的预测人数与真实人数做L1损失。代码上就是:

count_loss = torch.abs(pred.flatten(1).sum(dim=1) - density.flatten(1).sum(dim=1)).mean() loss = criterion(pred, density) + 0.01 * count_loss

0.01这个权重要调小,因为密度图的逐像素MSE数值通常比总人数误差大一到两个数量级。这个自定义损失可以独立成为论文里的一节,答辩时能讲出设计动机。

4.3 MAE与MSE:公式、代码与论文描述

MAE和MSE是人群计数论文里最核心的两个指标。MAE反映平均计数偏差,MSE反映计数误差的波动程度。先算每个样本预测人数与真实人数之差,MAE是误差绝对值的平均,MSE是误差平方均值再开根号。

def evaluate(model, val_loader): model.eval() pred_counts, gt_counts = [], [] with torch.no_grad(): for img, density in val_loader: img = img.cuda() pred = model(img) # flatten(1)把每个样本展平成 H*W,沿dim=1求和得到单帧人数 pred_count = pred.flatten(1).sum(dim=1).cpu().numpy() gt_count = density.flatten(1).sum(dim=1).numpy() pred_counts.extend(pred_count) gt_counts.extend(gt_count) pred_counts = np.array(pred_counts) gt_counts = np.array(gt_counts) mae = np.mean(np.abs(pred_counts - gt_counts)) mse = np.sqrt(np.mean((pred_counts - gt_counts) ** 2)) return mae, mse

逻辑说明:密度图的积分等于人数,所以对预测输出做flatten(1)后沿通道和空间维度求和,直接得到每帧的预测人数。评估阶段必须用model.eval()并包在torch.no_grad()里,否则计算图会占满显存,跑几个batch就OOM。

论文结果表建议按“数据集、方法、MAE、MSE”四列展示。要特别注意MAE和MSE的差异:如果MAE小但MSE偏大,说明大多数帧误差小,但有少数密集帧计数明显偏离,这时候可以挑几帧误差最大的图片在论文里做案例分析,解释原因。这个分析过程本身就是一个加分项。

5. 人流量检测避坑指南:5个翻车现场与排查思路

以下是做这个题目最容易踩的五个坑,按我遇到频率从高到低排。每一条都按“现象、原因、解决”展开,排查时可以对照。

5.1 密度图高斯核选错:模型学到的是模糊背景而不是人头

现象:训练了几十轮,预测密度图看起来是一大片模糊色块,预测人数只有真实值的三成左右。原因:所有头部点共用同一个固定σ,而摄像头是平视角度,近处人头占几十像素,远处人头只占几个像素,固定核不可能同时表示两种尺度。解决:改成几何自适应高斯核,先为每个头部点计算k近邻平均距离,再按σ_i=0.3*d_i生成密度图。如果不想一开始就做自适应,至少把图像按透视关系分成上下两区,分别用两个σ值。这条修改在论文里可以写成“基于局部几何自适应的密度图生成”,有理论依据也有实验对比。

5.2 增广不同步:训练loss在降,验证指标却在漂

现象:训练损失一路下降,验证MAE却居高不下,而且预测人数表现出系统性偏高或偏低。原因:代码里只对图像做了翻转或裁剪,密度图还是变换前的旧目标,模型被迫在同一个输入上拟合两个不一致的输出。解决:图像和密度图永远走同一个变换入口。我习惯用一个固定随机种子,在同一个函数里对两者依次做翻转和裁剪,确保操作一致。具体做法是:先用random.seed生成种子,再对图像和密度图分别调用cv2.flip和cv2.resize。

5.3 图像resize后sigma没跟着缩放

现象:训练集MAE还不错,一到测试集就明显变差,预测密度图边缘发糊。原因:为了加速训练,把图像缩到512x512,但密度图用的还是原图尺寸下的σ=4,核在缩小后的图上覆盖范围偏大,人头周围的密度被过度扩散。解决:先确定输入尺寸,再按缩放比例更新σ。缩放系数scale等于新尺寸除以原尺寸,σ_new等于σ_old乘以scale。如果按“先resize后生成密度图”的流程做,这个坑可以完全绕开。注意:多个尺寸变换时要保证每个阶段σ都跟着变化,否则越到后面偏差越大。

5.4 视角漂移:训练集和测试集根本不是同一个场景

现象:在ShanghaiTech Part_A上训练的模型,直接到Part_B上测试,MAE涨了一倍。原因:Part_A是密集人群,Part_B是稀疏街景,摄像头俯仰角和行人像素范围都不一样,深度学习模型对视角非常敏感,这是领域偏移问题,不是模型bug。解决:要么在训练集里混合多视角数据做成多源训练,要么做目标场景微调。微调时用目标场景的几十帧标注数据,把整个模型以1e-5的学习率再训20个epoch,效果通常很明显。论文里可以把“跨场景泛化测试+微调恢复”单独写成实验小节,比只报一个数据集的结果更有说服力。

5.5 论文创新点写“我用了YOLO”会在答辩时被问住

现象:答辩老师问“你选YOLO是什么依据?和密度图方法比优势在哪”,回答不上来。原因:把工程选型当成了方法创新,而毕设论文要求的是方法层面的比较和取舍。解决:即使主线是密度回归,也要在论文里放一个对照实验:用YOLOv5做人数统计,与密度图方法在密集帧和稀疏帧上分别对比MAE。分析时指出YOLO的NMS在密集帧会压制相邻框,密度图方法没有NMS后处理,因此计数更稳。这样一来,“面向密集场景的密度回归方案选型与优化”就成了你的创新点,而不是“我调用了某个模型”。

6. 给毕设加分:消融实验与真实场景验证的收尾技巧

6.1 一次消融实验的设计模板

消融实验的目的是证明网络结构和数据预处理里的每个设计都有作用。最省事的做法是拿训练好的模型作为基线,依次做三个改动:把三个卷积分支去掉一个、把1x1融合层替换成直接相加、把自适应σ换回固定σ。每次只改一个变量,重新训练并记录MAE和MSE,做成一张两列指标的表。

配置MAEMSE
三分支+1x1融合+自适应σ12.418.9
去掉三分支中的branch314.121.3
1x1融合改为直接相加15.623.5
自适应σ改回固定σ=416.825.1

表格里的数字只是示例,要替换成你自己实验的结果。消融实验不用做太多组,三到四组足够说明问题,答辩时也不容易把自己绕晕。每组实验在论文里配一小段解释,说明改动后精度下降是因为丢失了哪种尺度信息。

6.2 用一段监控视频做端到端验证

最后一步建议用一段自采视频跑端到端验证,把每帧人数输出成一条曲线,和人工抽查的几帧做对比。这段代码很短,但它能串起整个系统:

import cv2 import torch import numpy as np cap = cv2.VideoCapture("demo.mp4") writer = open("flow_curve.csv", "w") with torch.no_grad(): while True: ret, frame = cap.read() if not ret: break img = cv2.resize(frame, (512, 512)) / 255.0 img = torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0).float().cuda() density = model(img).squeeze().cpu().numpy() count = float(density.sum()) frame_id = int(cap.get(cv2.CAP_PROP_POS_FRAMES)) writer.write(f"{frame_id},{count:.2f}\n") writer.close() cap.release()

这段代码直接把模型输出变成时间序列,你可以在论文里画一张“帧号-人数”曲线,再人工核对其中三帧,误差控制在10%以内就能写进实验结论。可视化部分可以把密度图用热力图叠加到原图上,选一个密集帧放对比图,视觉冲击力比你贴十行表格都强。

我自己做这个题目时,第一次跑通模型后就把所有注意力放在“把loss降得更低”上,结果答辩前才发现连MAE和MSE的分工都讲不清楚。后来养成一个习惯:任何模型实验,先记录验证指标的变化过程,再去看loss曲线,而不是只留一张训练截图。这个习惯帮我少走了很多弯路,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询