简介:这份资源面向计算机、人工智能、通信工程等专业的在校学生与教师,提供一套基于YOLOv8的智慧城市广场人群聚集密度预警系统完整方案,可用于毕业设计、课程设计或大作业,也适合作为目标检测入门进阶的实战案例。压缩包共8个文件,约15.91MB,包含3个Python源码文件、3个模型权重文件以及2个说明文本,分别对应可视化界面、模型训练与视频检测等核心模块,并附有部署说明,简单配置即可运行。项目已完整测试通过,可生成核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果与标签分布图,便于答辩展示与结果分析。目前已有41人学习下载。读者可据此快速掌握YOLOv8训练、推理与可视化全流程,并在此基础上修改扩展功能,是拿来即用的深度学习项目参考。
1. 广场人群密度预警:从 YOLOv8 检测框到分级告警的完整链路
节假日傍晚的市民广场,监控画面里人头攒动,值班人员盯着十几路视频画面,靠肉眼判断哪片区域快挤爆了——这种场景下漏判几乎是必然的。基于 YOLOv8 的智慧城市广场人群聚集密度预警系统,要解决的就是把这件事自动化:用目标检测模型逐帧找出画面中的人,把检测框换算成密度指标,再按阈值触发分级告警,最后在一个可视化界面上把热力图、人数曲线和告警状态实时呈现出来。整套东西包含源码、数据集、可视化界面和部署教程,简单部署即可运行,适合毕设或课程设计。读者如果是做智慧城市方向的学生或刚入行的工程师,想找一个能跑通、能改、能写进论文的完整方案,这篇笔记会把从环境配置到密度分级、从训练调参到界面联调的每一步拆开讲,包括我踩过的坑和参数边界。
2. 先搞清楚检测什么、怎么算密度:YOLOv8 在人群场景的选型与数据准备
2.1 为什么人群密度预警选 YOLOv8 而不是分类网络或密度图回归
人群密度预警的技术路线大致三条:一是分类网络判断「拥挤/不拥挤」,二是密度图回归直接输出人数分布,三是目标检测逐人框出再统计。分类网络太粗,只能给整张图的标签,没法定位哪片区域聚集;密度图回归在极端遮挡下精度会崩,而且标注成本高,毕设周期内很难做出稳定效果。YOLOv8 属于第三条路线,优势在于:检测框天然带位置信息,可以直接做区域密度统计;模型本身有 n/s/m/l/x 多个尺度,广场监控这种固定机位、人群尺度变化不大的场景,用 yolov8s 甚至 yolov8n 就能跑出可用精度;Ultralytics 的工程化做得好,训练、验证、导出、推理一条命令搞定,部署到边缘设备也有现成路径。
我一般会先明确一个边界:YOLOv8 检测的是「人」这个类别,不是「人群」这个抽象概念。密度是后处理算出来的,不是模型直接输出的。这个区分很重要,因为它决定了你后面调参时,模型侧只需要保证召回率和定位精度,密度分级逻辑是独立可调的。
2.2 数据集怎么来、怎么标、怎么转成 YOLO 格式
广场人群数据集通常两个来源:公开数据集(如 CrowdHuman、WiderPerson)加自采监控截图。公开数据集的好处是标注量大,坏处是场景和你的目标场景可能不一致——CrowdHuman 里很多是街拍视角,和广场俯拍机位差异大。我的做法是:公开数据集做预训练,自采数据做微调。自采数据不需要太多,每个典型时段(早中晚、工作日/周末)各采几百帧,覆盖不同光照和人群密度即可。
标注用 LabelImg 或 Roboflow,只标一个类:person。标注时有个血泪经验:广场监控里远处的人可能只有十几个像素,标的时候容易漏,但漏标会直接拉低召回率,导致密度算偏低。建议对远处小目标放大后再标,宁可多标几个模糊的,也别漏。
标注完是 VOC XML 或 COCO JSON 格式,需要转成 YOLO 的 txt 格式。转换脚本如下:
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_dir, img_dir, out_dir, classes=['person']): """ 将 VOC 格式标注转为 YOLO txt 格式 xml_dir: XML 标注文件夹 img_dir: 图片文件夹(用于读取宽高) out_dir: 输出 labels 文件夹 """ os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if not xml_file.endswith('.xml'): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() # 图片宽高从 XML 的 size 节点读取 size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) lines = [] for obj in root.iter('object'): cls_name = obj.find('name').text if cls_name not in classes: continue cls_id = classes.index(cls_name) bbox = obj.find('bndbox') x1 = float(bbox.find('xmin').text) y1 = float(bbox.find('ymin').text) x2 = float(bbox.find('xmax').text) y2 = float(bbox.find('ymax').text) # YOLO 格式:class_id cx cy w h,全部归一化到 0-1 cx = (x1 + x2) / 2.0 / w cy = (y1 + y2) / 2.0 / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_path = os.path.join(out_dir, xml_file.replace('.xml', '.txt')) with open(out_path, 'w') as f: f.write('\n'.join(lines)) voc_to_yolo('./annotations', './images', './labels')这段脚本的关键点:归一化用的是图片实际宽高,不是 XML 里写的尺寸——有些标注工具导出的 size 和实际图片不一致,会导致框偏移。转换完建议抽几张可视化验证,确认框位置对得上。数据集目录结构按 Ultralytics 的要求组织:images/train、images/val、labels/train、labels/val,然后写一个 data.yaml 指向这些路径,names 里只写 person。
2.3 环境配置与训练命令:从零跑通第一个 baseline
环境配置是新手最容易翻车的地方。YOLOv8 依赖 PyTorch,PyTorch 又和 CUDA 版本绑定。我的建议是:先确认显卡驱动支持的 CUDA 版本,再去 PyTorch 官网找对应命令,不要直接 pip install torch 了事。GTX 1660Ti 这类卡跑 yolov8n/s 完全够用,6GB 显存下 batch 设 8 或 16 都行。
# 创建虚拟环境 conda create -n crowd_yolo python=3.9 -y conda activate crowd_yolo # 安装 PyTorch(以 CUDA 11.8 为例,具体版本按自己驱动选) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装 ultralytics pip install ultralytics # 验证环境 yolo checks环境没问题后,训练命令一行搞定:
yolo detect train \ model=yolov8s.pt \ data=./data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=./runs \ name=crowd_baseline参数说明:imgsz=640 是默认输入尺寸,广场监控如果远处人特别小,可以提到 1280,但显存和速度会翻倍;lr0 初始学习率 0.01 是 Ultralytics 的默认值,微调时建议降到 0.001;patience=20 表示 20 轮没提升就早停,防止过拟合。训练完看 runs/crowd_baseline 下的 results.png,重点看 mAP50 和 recall——人群场景召回率比精度更重要,漏检一个人对密度统计的影响比多检一个大。
3. 密度分级与告警逻辑:从检测框到「这片区域快挤爆了」
3.1 密度指标怎么算:人数、面积、还是人均面积
检测框出来之后,直接数框的数量就是人数,但「人数多」不等于「密度高」——同样 50 个人,散在 1000 平米和挤在 100 平米,危险程度完全不同。所以密度指标要用「单位面积人数」或「人均面积」。工程上我一般用两个指标并行:区域人数(直观,给值班人员看)和人均面积(客观,做告警触发)。人均面积低于 0.5 平米/人时,基本就是踩踏风险区了,这个阈值可以参考大型活动安全管理的一般经验值,但具体项目要按场地和预案调。
实现上,把画面划分成网格(比如 4x4 或 6x6),每个网格统计框中心点落入的数量,再除以网格对应的实际面积。网格实际面积需要标定:在画面里选一个已知长度的参照物(比如地砖、护栏),算出像素到米的比例,然后换算。这一步很多毕设会忽略,导致密度值没有物理意义,只能看相对变化。
import numpy as np def compute_grid_density(boxes, img_w, img_h, grid=(4, 4), px_per_meter=20): """ boxes: Nx4 的检测框 [x1, y1, x2, y2] grid: (rows, cols) 网格划分 px_per_meter: 像素/米,用于换算实际面积 返回每个网格的人数与人均面积 """ rows, cols = grid cell_w = img_w / cols cell_h = img_h / rows # 网格实际面积(平方米) cell_area_m2 = (cell_w / px_per_meter) * (cell_h / px_per_meter) counts = np.zeros((rows, cols), dtype=int) for box in boxes: cx = (box[0] + box[2]) / 2 cy = (box[1] + box[3]) / 2 col = min(int(cx // cell_w), cols - 1) row = min(int(cy // cell_h), rows - 1) counts[row, col] += 1 # 人均面积,人数为 0 时设为无穷大 area_per_person = np.where(counts > 0, cell_area_m2 / np.maximum(counts, 1), np.inf) return counts, area_per_personpx_per_meter 这个参数是标定出来的,不是猜的。标定方法:在画面里量一段已知实际长度的物体占多少像素,除一下。这个值不准,密度绝对值就不准,但分级告警如果只用相对阈值,影响会小一些。
3.2 三级告警阈值怎么定:黄、橙、红的触发条件
告警分级我一般设三级:黄色(关注)、橙色(预警)、红色(紧急)。触发条件用「人均面积」加「持续时间」双条件,避免瞬时波动误报。比如:
| 级别 | 人均面积阈值 | 持续时间 | 动作 |
|---|---|---|---|
| 黄色 | < 1.0 平米/人 | 持续 10 秒 | 界面标黄,记录日志 |
| 橙色 | < 0.7 平米/人 | 持续 5 秒 | 界面标橙,弹提示 |
| 红色 | < 0.5 平米/人 | 持续 3 秒 | 界面标红,声音告警 |
持续时间这个条件很关键。人群流动时,某一帧密度突然升高很正常,但如果持续 3 秒以上还降不下来,才是真聚集。实现上用滑动窗口或计数器:连续 N 帧满足条件才触发,有一帧不满足就重置计数器。
class AlertManager: def __init__(self, fps=25): self.fps = fps self.levels = [ {'name': 'yellow', 'area_th': 1.0, 'duration': 10}, {'name': 'orange', 'area_th': 0.7, 'duration': 5}, {'name': 'red', 'area_th': 0.5, 'duration': 3}, ] self.counters = [0] * len(self.levels) self.current_level = None def update(self, min_area_per_person): """每帧调用,传入当前最小人均面积""" triggered = None for i, lv in enumerate(self.levels): if min_area_per_person < lv['area_th']: self.counters[i] += 1 # 持续帧数达到 duration * fps 才触发 if self.counters[i] >= lv['duration'] * self.fps: triggered = lv['name'] break else: self.counters[i] = 0 self.current_level = triggered return triggered注意 counters 的重置逻辑:高级别不满足时要重置,低级别不满足时也要重置,否则会出现「橙色已经解除但红色计数器还在涨」的玄学现象。这个坑我在第一次做的时候踩过,排查了半天才发现是计数器没清干净。
3.3 可视化界面怎么把检测、密度、告警串起来
可视化界面用 PyQt5 或 Gradio 都行,毕设推荐 PyQt5,显得完整。界面分三块:左边视频显示区,叠加检测框和网格热力图;右上人数和密度实时曲线;右下告警状态灯和日志。热力图用网格人数做颜色映射,人数越多越红,直观。
视频帧读取用 OpenCV,推理用 YOLOv8 的 predict 接口,注意要设 stream=True 做流式推理,否则每帧都重新加载模型会卡死。界面刷新用 QTimer 定时触发,不要在主线程里跑推理,否则界面会假死。我的做法是推理放子线程,通过信号槽把结果传回主线程刷新 UI。
from PyQt5.QtCore import QThread, pyqtSignal import cv2 from ultralytics import YOLO class InferThread(QThread): result_ready = pyqtSignal(object, object) # 帧, 检测框 def __init__(self, model_path, video_path): super().__init__() self.model = YOLO(model_path) self.cap = cv2.VideoCapture(video_path) self.running = True def run(self): while self.running: ret, frame = self.cap.read() if not ret: # 视频结束循环播放 self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) continue results = self.model.predict(frame, conf=0.3, verbose=False) boxes = results[0].boxes.xyxy.cpu().numpy() self.result_ready.emit(frame, boxes)conf=0.3 是置信度阈值,人群场景建议设低一点(0.25-0.35),宁可多检也别漏检,后面可以用 NMS 的 iou 参数控制重叠框。这个线程模型跑起来后,界面流畅度取决于推理速度,yolov8s 在 1660Ti 上大概 30-40 FPS,够用。
4. 避坑与排查:人群密度预警系统最常见的 5 个翻车点
4.1 漏检导致密度偏低,告警不触发
现象:画面里明明人很多,但系统显示人数偏少,红色告警迟迟不触发。原因通常是远处小目标漏检,或者 conf 阈值设太高把模糊的人过滤掉了。解决:先把 conf 降到 0.2 看召回是否改善,如果改善明显说明阈值问题;如果还是漏,检查训练数据里小目标标注是否充分,必要时提高 imgsz 到 1280 重训。另外 NMS 的 iou 阈值也会影响,人群密集时框重叠多,iou 设太高会误删,建议 0.5-0.6。
4.2 密度值跳变,告警频繁误报
现象:人均面积曲线像心电图一样上下跳,告警灯闪个不停。原因是逐帧独立计算密度,没有做平滑。解决:对密度值做滑动平均(窗口 5-10 帧),或者直接用 AlertManager 的持续时间条件过滤。另外检测框本身有抖动,可以加一个简单的跟踪(如 ByteTrack)稳定 ID 后再统计,但毕设如果不想引入跟踪,滑动平均就够了。
4.3 界面卡死或视频延迟越来越大
现象:跑几分钟后界面无响应,或者视频画面越来越滞后。原因是推理和 UI 在同一线程,或者视频读取没有丢帧机制。解决:推理必须放子线程;视频读取时如果处理速度跟不上,要主动丢帧——读一帧处理一帧,处理完再读下一帧,而不是缓存一堆帧。OpenCV 的 cap.read() 在缓冲区满时会返回旧帧,可以设 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) 减少缓冲。
4.4 换场景后精度暴跌
现象:在 A 广场调好的模型,换到 B 广场误检漏检严重。原因是模型过拟合了 A 场景的光照、角度、背景。解决:训练时做数据增强( mosaic、mixup、随机裁剪),自采数据尽量覆盖多个场景;如果只能单场景,至少做亮度、对比度、翻转增强。另外密度阈值也要按新场景重新标定 px_per_meter,否则密度绝对值没意义。
4.5 部署到边缘设备时速度不达标
现象:在 PC 上跑得好好的,部署到 RK3588 或 Jetson 上帧率掉到个位数。原因是模型没做量化或导出优化。解决:用 yolo export 导出 ONNX 或 RKNN 格式,做 INT8 量化;yolov8n 比 yolov8s 快一倍以上,边缘设备优先用 n。如果还不行,降低输入分辨率到 416 或 320,但要注意小目标召回会下降,需要重新评估。
5. 进阶技巧:用热力图和损失曲线把系统调到位
5.1 可视化热力图辅助调参
YOLOv8 推理时可以输出热力图,帮助判断模型关注区域是否合理。如果热力图集中在背景而非人身上,说明模型学偏了,需要检查标注或增加正样本。Ultralytics 没有直接的热力图接口,但可以用 Grad-CAM 或简单的检测框密度图替代。我一般用检测框密度图:把框的中心点做高斯核密度估计,叠加在原图上,颜色越红表示人越密。这个图既能给值班人员看,也能自己用来验证密度计算是否正确。
import cv2 import numpy as np def density_heatmap(boxes, img_shape, sigma=30): """用检测框中心点生成高斯核密度热力图""" h, w = img_shape[:2] heat = np.zeros((h, w), dtype=np.float32) for box in boxes: 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 # 高斯模糊做核密度估计 heat = cv2.GaussianBlur(heat, (0, 0), sigma) heat = heat / (heat.max() + 1e-6) heatmap = cv2.applyColorMap((heat * 255).astype(np.uint8), cv2.COLORMAP_JET) return cv2.addWeighted(heatmap, 0.5, np.zeros_like(heatmap), 0.5, 0)sigma 控制热力图的平滑程度,值越大越平滑,一般取画面宽度的 1/20 左右。这个热力图和网格密度是互补的:热力图看整体分布,网格密度看具体数值。
5.2 损失函数曲线怎么看、怎么调
训练完 results.png 里有 box_loss、cls_loss、dfl_loss 三条曲线。box_loss 下降慢说明定位不准,可能是标注框不紧或学习率太低;cls_loss 震荡说明分类困难,人群场景只有一个类,震荡通常是 batch 太小或数据分布不均;dfl_loss 是 YOLOv8 的分布焦点损失,一般跟着 box_loss 走。如果 val 的 mAP 曲线早早平了而 train 还在降,就是过拟合,加数据增强或早停。我习惯把 lr0 设小一点(0.001)跑微调,曲线会平滑很多,虽然收敛慢但最终精度更稳。
5.3 一个具体技巧:用「密度变化率」做提前预警
人均面积低于阈值才告警,其实已经有点晚了。更好的做法是看密度变化率:如果人均面积在快速下降(比如 5 秒内从 1.2 降到 0.8),即使还没到红色阈值,也应该提前预警。实现上记录最近 N 帧的人均面积,做线性拟合,斜率负得厉害就触发「趋势预警」。这个技巧在广场这种人群快速聚集的场景特别有用,能给值班人员多争取几十秒的响应时间。我现在的习惯是:任何密度预警系统,都要同时看绝对值和变化率,只看绝对值就是等出事。希望帮到你。
本文还有配套的精品资源,点击获取