
简介基于深度学习的人流量检测系统是一份面向计算机相关专业毕业设计与课程设计的高分项目资源覆盖模型训练、数据处理、后端服务与前端展示等环节。项目经导师指导并调试通过可直接运行适合正在完成毕设的学生或需要真实项目练手的学习者参考。压缩包共1235个文件以Python源码为核心配合HTML、CSS、JavaScript等Web前端界面文件以及PNG、GIF、JPG等图表素材并包含可执行程序、说明文档和配置文件整体约61.54MB。资源内项目说明详细针对检测流程、环境依赖与运行方式做了梳理便于快速搭建环境并理解代码结构从内容预览可见其中包含后端服务与配置管理模块可支撑完整系统演示。目前已有263人学习使用是一份兼具参考价值和实践意义的人流量检测项目。1. 基于深度学习的人流量检测系统毕设到底在做什么人流量检测放在毕业设计里通常不是让你从零训练一个大模型而是把目标检测或人群密度估计的方法迁移到“统计某个区域有多少人”这个具体任务上。输入是一张监控画面或一段视频输出是人数、密度热力图以及随时间变化的流量曲线。很多同学把题目理解成“用 YOLO 框人然后数框”这确实是一条路但遇到人群密集、互相遮挡的场景检测框会漏掉一大半。更常见、也更适合拿高分的方法是先做密度估计再对密度图积分得到人数这条路线既能出可视化效果又有足够深的算法细节可以写进论文。这篇笔记会按这个思路把方案选型、环境搭建、数据准备、核心代码和答辩材料一条线讲完。2. 人流量检测技术选型检测计数还是密度估计2.1 两种主流方案各自的适用边界人流量检测在技术上有两条路线区别在于“人”在算法里被定义成什么。基于目标检测的方案把每个人当作一个矩形框用 YOLO 或 Faster R-CNN 找出所有人然后数框。这个方案的优势是直观框画出来便于展示也有现成的预训练权重可用适合人流稀疏的场景比如走廊、出入口、办公室。缺点也很明显当人挤人、互相遮挡时检测框大量重叠NMS 会把叠在一起的人误判成一个人人数一旦超过每平米三四人漏检率就直线上升。另一条路线是密度估计。它不关心每个人的框而是学习从图像到密度图的映射密度图里每个位置的像素值表示该处人的密集程度把整张图的密度值积分就是人数估计。密度图可以直接变成热力图叠加到原图上视觉效果比一堆框更有“高级感”也更容易解释模型在关注什么区域。这条路线对密集场景的鲁棒性明显更好FLOPs 和参数量也可以用 CSRNet 这类轻量骨干控制在可接受范围内。毕业设计选密度估计论文里能写的东西也更多密度图怎么生成、不同高斯核的影响、网络的感受野设计这些都有话可说。2.2 前置对比YOLO 计数方案为什么在毕设里吃亏对比维度目标检测计数YOLO密度估计计数CSRNet遮挡场景表现漏检明显依赖 NMS 阈值通过密度分布推测被遮挡人输出形式矩形框 类别热力图 总人数可视化效果直观但信息量少热力层叠便于答辩展示论文可写深度调参和训练技巧为主密度图生成、网络结构、感受野均可深挖工程复杂度低几分钟能跑通中需要自己写密度图生成逻辑需要说明的是YOLO 方案并非不能做毕设如果你的场景固定比如校门口、人不多、视角单一YOLOv8 加 DeepSORT 也能做出一套完整的检测加流量统计系统。但从“高分项目”这个目标倒推密度估计路线的上限更高。评审老师看到的东西是你理解了人群计数的本质困难而不是调了一个现成权重。2.3 确定方案后系统的整体模块划分整套系统按功能拆分成五个模块这也是后面章节展开的顺序。数据模块负责把原始图像和标注点坐标处理成网络输入。模型模块用 CSRNet 结构骨干选用 VGG16将最后几层全连接换成空洞卷积兼顾分辨率和感受野。训练模块包含数据加载器、损失函数、优化器和学习率调度。评估模块计算 MAE 和 MSE这是人流量检测领域最通用的两个指标。展示模块把密度图映射成热力图叠加到原图上并输出一个随时间变化的人数曲线。关于为什么选 VGG16 而不是 ResNet50 这类更深的骨干核心原因是 CSRNet 的实验结论在人群计数任务上VGG16 提取的特征配合空洞卷积在分辨率保持和感受野扩大之间取得了更好的平衡。ResNet 的残差结构虽然让网络更深但在密度估计这类逐像素回归任务上深层下采样会损失空间细节后期需要更多反卷积来恢复训练难度反而更高。对于毕设来说VGG16 还有一个额外好处权重文件小、加载快、显存占用可控这对大多数学生手里的单卡环境更友好。3. 搭出可复现的深度学习训练环境PyTorch 与依赖配置3.1 GPU 环境判断与 CUDA 版本取舍动手之前先做一个判断你的机器有没有 NVIDIA 显卡够不够训练。密度估计网络虽然不重但输入图像一般会缩放到 768×1024 甚至更高来保留细节这个尺寸下的特征图显存开销不小。如果你手里的卡显存低于 6G建议直接走 CPU 训练加小尺寸输入的路线或者先用上海集美大学的 Part_B 子集练一轮这个子集的图像只有 768×1024 的一半左右吃显存明显更少。确定要用 GPU 之后用终端跑nvidia-smi看驱动支持的 CUDA 版本。这句话几乎是所有环境问题的根源PyTorch 的 CUDA 版本不能高于驱动支持的版本否则会报找不到 CUDA driver 的错误。一个常见的保守做法是装 CUDA 11.8 对应的 PyTorch这一版本对驱动的要求不算苛刻同时兼容绝大多数近几年的显卡。3.2 创建虚拟环境与安装核心依赖创建虚拟环境时不要直接装在系统 Python 里因为毕设项目会持续迭代依赖冲突会浪费大量时间。用 conda 或 venv 都可以关键是把环境隔离出来。conda create -n crowd python3.9 -y conda activate crowd pip install torch2.1.1 torchvision0.16.1 --index-url https://download.pytorch.org/whl/cu118 pip install numpy opencv-python matplotlib pandas tqdm scipy pillow第一行创建名为 crowd 的 Python 3.9 环境3.9 与大部分深度学习库的兼容性都很好尤其是 scipy 和 opencv 相关轮子。第二行通过 PyTorch 官方源安装 CUDA 11.8 版这里不指定版本直接装最新版也能用但后面如果跑 CSRNet 原作者代码某些接口在 2.2 以上版本会出现的函数签名变化导致老代码报错。固定版本可以减少这类兼容性问题。第三行安装的是数据处理和展示用的常规库。装完验证一下能训练import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else no gpu)能打印出显卡名字说明 CUDA 这一层通了。这里经常出现的情况是 torch 已经装成 CPU 版torch.cuda.is_available()返回 False排查方式是用pip list看 torch 版本后面有没有cu118或cu121的后缀没有后缀就是 CPU 版需要重新按指定 index-url 安装。3.3 数据读取组件的依赖细节除了 torch 之外最需要留意的两个库是 scipy 和 opencv-python。密度图生成时需要二维高斯滤波scipy 的ndimage.gaussian_filter是标准做法但它对输入数组的 dtype 是 float32 还是 float64 很敏感用错会导致内存溢出。opencv 负责图像的读取和缩放注意不要把cv2.imread读出来的 BGR 顺序当成 RGB 直接喂给网络这个顺序问题在可视化时会得到一张颜色诡异的热力图排查起来还挺花时间的。还有一个隐蔽的依赖项是 h5py很多公开人群计数数据集用 .mat 文件存标注而 .mat 文件在 Python 里的读取依赖 h5py。不同 MATLAB 版本生成的 .mat 文件格式不同v7.3 以上要用h5py.File读取旧版本用scipy.io.loadmat这个差异会在下一章数据准备里展开讲。提前装好这两者避免做数据时中断去装依赖。常见做法是先准备一个requirements.txt把上述依赖都锁住版本方便后面换机器复现。4. 数据准备与密度图生成人流量检测的“地基”4.1 公开数据集使用建议与文件格式判断人流量检测领域最常用的公开数据集是 ShanghaiTech分为 Part_A 和 Part_B 两个子集。Part_A 是密集场景平均每张图有 501 个人适合体现密度估计方案的优越性Part_B 是街景人流相对稀疏。数据集在官网公开但考虑到网络环境和下载速度你可以找老师要一份或者从学校的内部共享站拷贝这比在下载上耗时间划算。每个标注文件里存的是人头点的坐标格式是 N×2 的数组N 表示这张图里有多少个人头每行是该人头的 (x, y) 坐标。拿到之后先用代码确认 .mat 文件的版本。from scipy.io import loadmat import h5py def read_mat(path): # 尝试用 h5py 读取 v7.3 格式的 mat 文件 try: with h5py.File(path, r) as f: # 常规键名是 image_info / annotation / point ann f[image_info][annotation][()] points ann[point][0][0] return points except OSError: # 失败则退回 loadmat 读旧版本 data loadmat(path) points data[image_info][0, 0][annotation][0, 0][0, 0] return points pts read_mat(./data/part_A/GT_IMG_1.mat) print(pts.shape, pts[:3])逻辑说明代码先尝试用 h5py 打开文件因为 MATLAB v7.3 保存的 .mat 本质是 HDF5 格式只有 h5py 能读如果抛OSError说明是旧版格式退回 scipy 的loadmat。里面的image_info - annotation - point是 ShanghaiTech 的固定键路径换成其他数据集时需要改这里的键名。参数说明pts.shape是人数 × 2打印前三行能看到每个点的横纵坐标。从零开始做数据时强烈建议直接写一个独立的标注转密度图脚本不要边训练边在每次迭代里生成密度图。原因有两个一是每次训练都重新算高斯滤波是浪费二是生成结果可以先可视化检查避免错误标注污染训练数据。4.2 高斯密度图生成原理与核心代码密度图标签的生成规则是每个人头点(x_i, y_i)处放一个单位冲激然后和二维高斯核做卷积。由于透视关系人头尺寸在图像不同位置是不同的所以要用几何自适应核对每个点取 k 近邻用邻近人头间的平均距离作为该点高斯核的标准差。import numpy as np from scipy.ndimage import gaussian_filter def gaussian_density_map(points, h, w, k3, sigma_factor0.3): # 初始化零矩阵尺寸与输入图像一致 density np.zeros((h, w), dtypenp.float32) pts np.array(points, dtypenp.float32) if len(pts) 0: return density for i, pt in enumerate(pts): # 限制点不能落在图像边界外太远 x, y min(int(pt[0]), w - 1), min(int(pt[1]), h - 1) dists np.sqrt(((pts - pt) ** 2).sum(axis1)) sorted_dists np.sort(dists) # 取 k 个近邻的平均距离作为自适应尺度 sigma sorted_dists[1:k 1].mean() * sigma_factor sigma max(sigma, 1e-3) # 以当前点为中心画一个小区域 d int(3 * sigma) x_min, x_max max(0, x - d), min(w, x d 1) y_min, y_max max(0, y - d), min(h, y d 1) # 区域内的高斯基底 xx, yy np.meshgrid(np.arange(x_min, x_max), np.arange(y_min, y_max)) g np.exp(-((xx - x) ** 2 (yy - y) ** 2) / (2 * sigma ** 2)) density[y_min:y_max, x_min:x_max] g return density逻辑说明每个点的 sigma 由它到最近三个邻近人头的平均距离决定远近不同的人群用不同宽度的核这就是几何自适应高斯核。sigma_factor控制核的收缩程度经验上取 0.3 可以让密度图的总积分接近真实人数取值过大会让每个头的区域摊得过宽密度图在不同人头之间糊成一片。参数说明函数里d 3 * sigma的作用是把计算范围限制在标准差的三倍以内因为高斯分布在超过三倍标准差后数值趋近于零这样避免了对整张图所有像素做指数运算训练阶段数据量大时能明显加速。实际只做毕设时你也可以偷懒用固定 sigma即所有点同用一个标准差这在人群稀疏时效果也能看。但论文里如果写了“自适应密度图生成”而代码却是固定核答辩被追问时很难自圆其说所以这里还是给出完整实现代码量也不大。4.3 训练集验证集划分与数据加载器实现数据划分的重点是验证集要从不同场景的图片里抽而不是随机抽。ShanghaiTech 的两个子集本身就是按场景区分的Part_A 直接沿用官方划分即可。我一般会再额外抽 10% 的验证集用来做早停判断防止官方测试集被反复调参污染。from PIL import Image import torch from torch.utils.data import Dataset, DataLoader class CrowdDataset(Dataset): # dir_path 下面按 img 和 gt 两个子目录存放 def __init__(self, img_list, dir_path, size(768, 1024)): self.img_list img_list self.dir dir_path self.size size def __len__(self): return len(self.img_list) def __getitem__(self, idx): name self.img_list[idx] img Image.open(f{self.dir}/img/{name}.jpg).convert(RGB) img img.resize(self.size) gt np.load(f{self.dir}/gt/{name}.npy) # 提前算好的密度图 gt_img torch.from_numpy(gt).unsqueeze(0) # 加通道维 # 归一化到 0-1并转成 tensor img_tensor torch.from_numpy(np.array(img)).permute(2, 0, 1).float() / 255.0 return img_tensor, gt_img逻辑说明这里假设你已经在一开始把 .mat 标注批量转成了 .npy 密度图文件并在转换时用同样的resize尺寸生成密度图。permute(2,0,1)是把 HWC 转成 CHWunsqueeze(0)给密度图加上通道维让它和图像都是四维张量。一个容易踩的坑是两边的 resize 尺寸不一致图像缩放了密度图没缩放导致训练时损失值大但人数完全对不上检查方法是在 DataLoader 返回后打印一下二者的 shape。另一个坑是Image.resize默认用 BILINEAR如果密度图是 float32 的 numpy 数组直接用同一套 API 会丢失小数精度建议预先将密度图缩放到和图像一致并单独保存。关于 resize 参数 768×1024这是显存和精度的折中。分辨率越高密度图中的细节越清晰但显存占用按面积增长。6G 显存跑 768×1024 已经有点紧张8G 是及格线12G 以上可以放心跑。如果显存不够把尺寸降到 576×768MAE 会有一定程度的上升但整个项目依然能跑通。5. CSRNet 模型实现与训练闭环从加载权重到评估指标5.1 网络结构解析VGG16 骨干加空洞卷积的拼接CSRNet 把 VGG16 的最后一个池化层和全连接层去掉用空洞卷积替代后面几层的普通卷积然后接一个普通的卷积层输出单通道密度图。空洞卷积的作用是不增加参数量地扩大感受野因为人群计数需要对大范围上下文有感知才能区分前景人头部和背景纹理。import torch import torch.nn as nn from torchvision import models class CSRNet(nn.Module): def __init__(self, pretrainedTrue): super().__init__() # 前半部分是 VGG16 的前 23 层负责提取基础特征 vgg16 models.vgg16(weightsmodels.VGG16_Weights.IMAGENET1K_V1) if pretrained else models.vgg16() features list(vgg16.features)[:23] # 把第 9 层和第 14 层的普通卷积替换成空洞卷积 # 第 9 层是 maxpool 之后的第一层 conv3-256 features[9] nn.Conv2d(256, 256, 3, 1, 1, dilation2) features[14] nn.Conv2d(512, 512, 3, 1, 1, dilation2) # 第 16、19、22 层是 maxpool 之后的 conv3-512用 dilation4 features[16] nn.Conv2d(512, 512, 3, 1, 1, dilation4) features[19] nn.Conv2d(512, 512, 3, 1, 1, dilation4) features[22] nn.Conv2d(512, 512, 3, 1, 1, dilation4) self.frontend nn.Sequential(*features) # 后端是三个空洞卷积层加一个输出层 self.backend nn.Sequential( nn.Conv2d(512, 512, 3, 1, 1, dilation2), nn.ReLU(inplaceTrue), nn.Conv2d(512, 256, 3, 1, 1, dilation2), nn.ReLU(inplaceTrue), nn.Conv2d(256, 128, 3, 1, 1, dilation2), nn.ReLU(inplaceTrue), nn.Conv2d(128, 1, 1), ) def forward(self, x): return self.backend(self.frontend(x)) model CSRNet() # 打印输出尺寸确认得到单通道密度图 print(model(torch.randn(1, 3, 256, 256)).shape)逻辑说明features[:23]取 VGG16 的前 23 层保留了 4 个 maxpool输入的高被降为原来的 1/16。如果把第 5 个 maxpool 也保留分辨率会降到 1/32人数细节全丢所以要截断。替换空洞卷积时dilation2 对应原来第 2 个 maxpool 之后dilation4 对应第 4 个 maxpool 之后这样分辨率不降但感受野却能达到和完整 VGG16 相近的效果。参数说明torch.randn(1,3,256,256)的 3 是 RGB 三通道256 是测试分辨率输出维度的最后一维是 1表示密度图是一个单通道灰度图。很多学生在这步会问为什么不用 torchvision 里现成的分割模型答案是为了论文里能画出网络结构图CSRNet 是教材级的简单结构你可以手绘出每一层的尺寸变化答辩时能讲得清楚。这比用 Mask R-CNN 这种大模型更可控。5.2 训练循环核心代码与损失函数选择回归任务常用的损失函数是欧氏距离损失也就是 MSE 的变体。对于人群计数L2 损失对异常值更敏感但训练更稳定L1 损失在人群稀疏区域表现更好但对密集区域容易震荡。常见做法是直接用 L2并在验证集上同时报告 MAE因为 MAE 是领域内公认的指标。from torch.utils.data import DataLoader import torch.optim as optim def train_one_epoch(model, loader, optimizer, device): model.train() total_loss 0.0 for imgs, gts in loader: imgs, gts imgs.to(device), gts.to(device) preds model(imgs) # 欧氏距离损失统计所有像素差平方和 loss ((preds - gts) ** 2).sum() / preds.size(0) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loader) device torch.device(cuda if torch.cuda.is_available() else cpu) model CSRNet().to(device) optimizer optim.SGD(model.parameters(), lr1e-5, momentum0.95, weight_decay1e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.8)逻辑说明((preds - gts) ** 2).sum()计算的是整张密度图的 MSE除以 batch 大小是为了让损失不受 batch 内图片数量影响。SGD 的 momentum 设为 0.95、学习率固定在 1e-5这是 CSRNet 论文里的默认配置。Adam 很快就能让损失降到很低但后期密度图的质量不如 SGD 打磨得精细尤其是计数任务对空间分布的精确度有要求。参数说明StepLR每 10 个 epoch 将学习率乘以 0.8这个衰减节奏适合训练 50 到 100 epoch 的场景太快的衰减会让后期收敛不动。5.3 评估阶段MAE 和 MSE 的计算口径评估时看到的 MAE 通常是“平均绝对误差”即每张图片的预测人数和真实人数之差的绝对值再对测试集所有图片取平均。MSE 同理但差是平方后累加再开根号它对大误差的惩罚更重。def evaluate(model, loader, device): model.eval() mae_sum, mse_sum 0.0, 0.0 count 0 with torch.no_grad(): for imgs, gts in loader: imgs imgs.to(device) preds model(imgs) gt_counts gts.view(gts.size(0), -1).sum(dim1) pred_counts preds.view(preds.size(0), -1).sum(dim1) mae_sum (pred_counts - gt_counts).abs().sum().item() mse_sum ((pred_counts - gt_counts) ** 2).sum().item() count gt_counts.size(0) return mae_sum / count, (mse_sum / count) ** 0.5 mae, mse evaluate(model, val_loader, device) print(fMAE{mae:.2f}, MSE{mse:.2f})逻辑说明sum(dim1)把密度图所有像素加起来得到该图的预测人数。因为密度图是浮点数积分不一定是整数直接和真实人数做差是可行的。评估必须在no_grad()环境下进行否则会额外占用显存并拖慢速度。这里 count 是累计的测试图片数量最终 MAE 是所有图片误差的平均而不是批次的平均口径更严谨。如果 Part_A 测试集上 MAE 在 60 到 80 之间说明模型训练收敛正常如果能做到 55 以下已经是很不错的成绩。Part_B 的 MAE 通常能到 15 以内因为场景相对稀疏。如果 MAE 比随机猜测还差比如上百分不要急着调网络结构先检查密度图的数据范围是否正确最常见的问题是密度图没有归一化和网络输出的数值范围差了一个量级。6. 训练避坑指南显存、黑图和收敛三个高频翻车点6.1 显存溢出不是模型太大是输入图和 batch 太大现象训练刚开始或第一个 epoch 还没跑完程序抛CUDA out of memory。原因768×1024 的输入经过 VGG16 前 23 层后特征图是 48×64×512这部分占用的显存是固定的但如果你把 batch size 设置成默认的 8 或 16一算需求量就超了。很多 PyTorch 教程里的 batch 设置是为分类任务设计的人流量检测的输入分辨率高不能直接套。解决把 batch size 降到 1同时在 DataLoader 里设置pin_memoryTrue。如果 batch1 还不够将输入尺寸降到 640×480或者使用torch.cuda.amp混合精度训练显存占用能再省一半。另一个被忽略的点是验证阶段也占显存评估时临时把 batch size 降为 1。6.2 训练集损失已经很小保存的密度预测图却是全黑的现象训练损失从几百降到个位数但训练时保存的预测密度图打印出来是一片黑完全看不到人头位置的高亮区域。原因这个现象很隐蔽多数情况是密度图生成时用了 float64而模型输出的 float32 在可视化时数值范围被压缩到 0 到 1内部的小数差异在画图时无法显示。另一个可能是 matplotlib 的imshow默认用 viridis 色图数值范围如果远小于 0 到 255整个图会偏暗。解决保存可视化图片前做一次线性拉伸vis (pred - pred.min()) / (pred.max() - pred.min())。这只是一个可视化操作不要用在评估上因为评估时密度图必须保持原始积分值。如果发现密度图整体偏亮也就是pred.max()很大说明网络输出范围没约束住可以检查归一化是否正确。6.3 训练损失不降或震荡学习率设置与数据加载顺序现象训练 20 个 epoch损失在某个水平来回摆动MAE 始终很高。原因SGD 的学习率 1e-5 对这个任务很保守但在预训练权重加载不到位时这个值又不足以让网络尽快适应新的数据分布。另一种情况是 DataLoader 每次 shuffle 出来的数据分布差异巨大比如相邻两个 batch 的人头数从 17 跳到 500模型被大 batch 拉偏后面几个 batch 又拉不回来。解决首先要确认预训练权重确实加载成功打印model.frontend[0].weight的值看是否接近 ImageNet 预训练的分布。如果数据分布确实差异大把batch_size调到 4 或 8 平滑一下梯度的方差。学习率方面前 5 个 epoch 用 1e-4后面切回 1e-5这一步能有效防止训练初期收敛太慢。说到底模型训练中的许多“玄学”问题都出在数据分布和训练配置不匹配上而不是网络结构本身。6.4 验证集 MAE 低但实际可视化效果差评估与可视化的标准不一致现象测试集 MAE 得到 70看着很不错但把模型跑在新场景的视频上预测人数明显偏少或偏多。原因评估的图片和你实际使用的场景存在分布差异。ShanghaiTech 的训练数据大多是俯拍或中等视角如果你在毕设展示时用广角平视的摄像头画面模型看到的特征完全不同。另外评估用的是全图而实际应用时可能只需要统计画面中的某个区域比如门口如果直接把整张图送进去会统计到背景里的无关行人。解决在系统设计时加入 ROI 区域裁剪。先用鼠标框定统计区域对区域外像素做掩码把掩码后的图像送入模型再把密度图积分限制在掩码内。这个需求应该从第一天就写进设计文档里而不是在最后整合阶段才补否则前端和后端接口对不上数据处理流程要重写。7. 让“高分项目”名副其实答辩材料与验证闭环这个系统到最后能不能拿高分取决于你是“跑通了一个模型”还是“做完整了一个系统”。跑通模型只需要训练脚本和测试脚本而完成系统至少要包含五样东西可训练的源码、说明文档、演示视频或界面截图、性能对比表格、以及一份能讲清楚设计思路的答辩 PPT。先说你手里已有的源码。整理仓库时把训练脚本train.py、评估脚本evaluate.py、数据预处理脚本preprocess.py和模型定义model.py分开入口统一用main.py加命令行参数控制。不要在 notebook 里写训练逻辑评审老师看到 .py 文件才会认为你具备工程交付能力。项目说明文档的写法有一个固定的逻辑链问题定义 → 方案选型 → 数据说明 → 实验结果 → 系统展示。在“实验结果”章节里不要只贴一张损失下降曲线要把测试集上每张图的预测人数和真实人数画成散点图并且按人流量区间分段统计 MAE。例如 0 到 50 人的图片 MAE 是多少200 人以上的图片 MAE 是多少这能从数据上说明模型在密集和稀疏场景哪里更强哪里是瓶颈。性能对比表格至少要三行你的 CSRNet、一个 YOLO 检测计数 baseline、真实人数标注。每一行给出 MAE 和 MSE如果 CSRNet 的 MAE 明显低于 YOLO baseline就说明你的选型判断正确。如果你的模型 MAE 反而更高也有办法解释检测计数在稀疏场景更准密度估计在密集场景更准你可以按场景粒度分开对比呈现出自己的模型在密集场景胜出的事实。验证闭环的最后一环是做一次实时推理演示。用一段 10 秒左右的视频逐帧处理并显示密度热力图和累计人数曲线把 fps 也打出来。代码里用cv2.VideoCapture读取视频每帧 resize 后送入模型把密度图用cv2.applyColorMap映射成伪彩色图叠加到原图再用一个队列保存最近 30 帧的人数计算滑动平均后显示。这样演示的观众能看到人数在 20 到 80 之间浮动而不是一个静止的数字这比漂亮的热力图更能证明系统可用性。我在做自己的毕设时吃过一个亏当时只关注了 MAE 指标没做失败案例分析答辩时评审直接问“这个模型在什么场景下会失效”我一时答不上来。后来补了一组夜景测试和低分辨率测试发现模型对光照变化特别敏感夜间场景几乎全部失效。于是提前在文档里写了“本研究方法的局限性”一节主动承认并说明原因反而把劣势变成了加分项。这个习惯我现在还在用先把系统的边界找出来再谈优化。希望这篇从方案选型到答辩材料的完整路径能帮你在人流量检测毕设上少走弯路。按这条线把环境搭好、数据准备到位、代码跑通、文档写扎实这个项目的高分就有了基本的保证。本文还有配套的精品资源点击获取