news 2026/10/23 9:10:09

基于深度学习的人流量检测:CSRNet密度估计与毕设实战全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于深度学习的人流量检测:CSRNet密度估计与毕设实战全解析

简介:基于深度学习的人流量检测系统是一份面向计算机相关专业毕业设计与课程设计的高分项目资源,覆盖模型训练、数据处理、后端服务与前端展示等环节。项目经导师指导并调试通过,可直接运行,适合正在完成毕设的学生或需要真实项目练手的学习者参考。压缩包共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 python=3.9 -y conda activate crowd pip install torch==2.1.1 torchvision==0.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, k=3, sigma_factor=0.3): # 初始化零矩阵,尺寸与输入图像一致 density = np.zeros((h, w), dtype=np.float32) pts = np.array(points, dtype=np.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(axis=1)) 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 转成 CHW,unsqueeze(0)给密度图加上通道维,让它和图像都是四维张量。一个容易踩的坑是两边的 resize 尺寸不一致,图像缩放了密度图没缩放,导致训练时损失值大但人数完全对不上,检查方法是在 DataLoader 返回后打印一下二者的 shape。另一个坑是Image.resize默认用 BILINEAR,如果密度图是 float32 的 numpy 数组,直接用同一套 API 会丢失小数精度,建议预先将密度图缩放到和图像一致并单独保存。

关于 resize 参数 768×1024:这是显存和精度的折中。分辨率越高,密度图中的细节越清晰,但显存占用按面积增长。6G 显存跑 768×1024 已经有点紧张,8G 是及格线,12G 以上可以放心跑。如果显存不够,把尺寸降到 576×768,MAE 会有一定程度的上升,但整个项目依然能跑通。

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, pretrained=True): super().__init__() # 前半部分是 VGG16 的前 23 层,负责提取基础特征 vgg16 = models.vgg16(weights=models.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, dilation=2) features[14] = nn.Conv2d(512, 512, 3, 1, 1, dilation=2) # 第 16、19、22 层是 maxpool 之后的 conv3-512,用 dilation=4 features[16] = nn.Conv2d(512, 512, 3, 1, 1, dilation=4) features[19] = nn.Conv2d(512, 512, 3, 1, 1, dilation=4) features[22] = nn.Conv2d(512, 512, 3, 1, 1, dilation=4) self.frontend = nn.Sequential(*features) # 后端是三个空洞卷积层加一个输出层 self.backend = nn.Sequential( nn.Conv2d(512, 512, 3, 1, 1, dilation=2), nn.ReLU(inplace=True), nn.Conv2d(512, 256, 3, 1, 1, dilation=2), nn.ReLU(inplace=True), nn.Conv2d(256, 128, 3, 1, 1, dilation=2), nn.ReLU(inplace=True), 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,人数细节全丢,所以要截断。替换空洞卷积时,dilation=2 对应原来第 2 个 maxpool 之后,dilation=4 对应第 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(), lr=1e-5, momentum=0.95, weight_decay=1e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.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(dim=1) pred_counts = preds.view(preds.size(0), -1).sum(dim=1) 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(f"MAE={mae:.2f}, MSE={mse:.2f}")

逻辑说明:sum(dim=1)把密度图所有像素加起来,得到该图的预测人数。因为密度图是浮点数,积分不一定是整数,直接和真实人数做差是可行的。评估必须在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_memory=True。如果 batch=1 还不够,将输入尺寸降到 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 指标,没做失败案例分析,答辩时评审直接问“这个模型在什么场景下会失效”,我一时答不上来。后来补了一组夜景测试和低分辨率测试,发现模型对光照变化特别敏感,夜间场景几乎全部失效。于是提前在文档里写了“本研究方法的局限性”一节,主动承认并说明原因,反而把劣势变成了加分项。这个习惯我现在还在用:先把系统的边界找出来,再谈优化。

希望这篇从方案选型到答辩材料的完整路径,能帮你在人流量检测毕设上少走弯路。按这条线把环境搭好、数据准备到位、代码跑通、文档写扎实,这个项目的高分就有了基本的保证。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 5:59:10

嵌入式串口DMA与HAL库实战:从printf重定向到稳定收发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 5:28:30

260条ChatGPT中文提示语实战指南:从模板拆解到工作流组合

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/22 14:36:11

DeepSeek流式响应与长文本分块:实时数据处理实战指南

简介:这份PDF文档聚焦实时数据处理场景,系统讲解DeepSeek流式响应与长文本分块处理方案,面向需要处理高并发、持续性数据流的开发人员与算法工程师。内容从实时数据处理概述切入,剖析流式响应的技术原理与增量处理机制&#xff0c…

作者头像 李华
网站建设 2026/10/23 7:09:24

Autelan交换机命令行配置手册:从开箱到可复现的配置基线

简介:《Autelan交换机产品命令行配置手册范本》面向网络工程师、IT运维人员及备考网络技术方向的读者,用于解决Autelan品牌交换机命令行操作与系统配置的实际问题。手册由傲天动联技术编写,版本V1.23,内容从命令行格式约定、五种主…

作者头像 李华
网站建设 2026/10/22 15:03:31

Multisim 14.3安装实战:从环境清理到数据库报错排查与静默部署

简介:Multisim 14.3安装包适合电子、电气相关专业学生及电路仿真入门者,可解决缺少可靠安装介质、难以部署完整仿真环境的问题,便于开展原理图绘制、电路仿真与调试练习。整个rar压缩包共270个文件,大小约872.26MB,以e…

作者头像 李华
网站建设 2026/10/23 12:04:28

DeepSeek跨框架迁移:PyTorch权重转TensorFlow实战指南

简介:针对DeepSeek模型在PyTorch与TensorFlow框架间迁移训练的完整技术文档,共197页、48个大章节,面向算法工程师与深度学习开发者,系统解决跨框架适配、权重转换、灵活训练方案落地等核心问题。文档支持目录章节跳转、书签大纲与…

作者头像 李华