摘要人群密度估计是智能安防、智慧城市和公共安全领域的关键技术。随着深度学习的发展目标检测算法在该任务中表现出色。本文将手把手带你构建一个基于YOLO系列YOLOv5、YOLOv8、YOLOv10的人群密度估计系统。我们将详细对比这三代算法的核心差异介绍如何标注和处理人群数据集并训练一个高精度的人数检测模型。最后我们将使用PyQt5开发一个图形用户界面实现图片、视频和实时摄像头的人数统计与拥挤度预警。全文包含完整的代码解析旨在帮助读者掌握从理论到落地的全流程。关键词YOLOv5YOLOv8YOLOv10人群计数密度估计深度学习PyQt5第一章 引言为什么需要人群密度估计在大型商场、地铁站、景区等公共场所人群过度聚集是发生踩踏事故的主要诱因。传统的人力监控效率低下且容易疲劳。基于计算机视觉的人群密度估计系统能够实时预警当区域内人数超过阈值时自动报警。流量管控统计人流量为疏散和限流提供数据支持。商业分析分析商场热点区域优化商业布局。在目标检测领域YOLOYou Only Look Once系列算法因其速度快、精度高成为工业部署的首选。本文将带领大家实现一个支持YOLOv5、YOLOv8、YOLOv10三款主流模型的检测系统。第二章 算法原理与YOLO系列对比2.1 YOLO核心思想YOLO将目标检测视为一个回归问题将输入图像划分为 S×S 的网格每个网格负责预测中心点落在该网格内的物体。它一次性预测边界框坐标、置信度和类别概率。2.2 YOLOv5 (2020)尽管没有官方论文但YOLOv5凭借其工程化易用性成为最流行的版本。网络结构由BackboneCSPDarknet、NeckPANet和Head组成。创新点Mosaic数据增强将四张图拼接成一张丰富了检测背景和小物体样本。自适应锚框计算在训练时自动计算最佳锚框。Focus结构将空间信息转移到通道维度减少计算量。2.3 YOLOv8 (2023)由Ultralytics公司开发是目前官方维护最活跃的版本集成了分类、检测、分割、姿态估计等多种任务。创新点Anchor-Free检测头摒弃了传统的Anchor-Based机制简化了后处理。C2f模块借鉴了ELAN的思想在CSP基础上增加了更多的梯度流分支使特征更丰富。解耦头分类和回归头分离提升了精度。2.4 YOLOv10 (2024)清华大学发布的YOLOv10主打“端到端”和“效率”。创新点NMS-Free训练引入一致性双重分配策略在训练时使用一对多分配提供监督推理时使用一对一分配直接输出结果彻底去除了NMS非极大值抑制延迟大幅降低。轻量化设计对模型各组件进行了全面的效率和精度权衡优化。2.5 人群密度估计的难点尺度变化大近处的人大远处的人小。严重遮挡人与人之间相互遮挡只能看到头部或局部。背景复杂光照变化、广告牌等人形图案干扰。第三章 数据集准备与处理3.1 数据集选择我们可以使用公开的人群数据集如VisDrone、ShanghaiTech或CrowdHuman。本文以CrowdHuman数据集为例该数据集标注密集非常适合训练拥挤场景。注意由于CrowdHuman原始标注是odgt格式我们需要将其转换为YOLO训练所需的txt格式。3.2 数据格式转换脚本假设我们下载了CrowdHuman数据集并希望将其转为YOLO格式。目录结构textdatasets/ ├── crowdhuman/ │ ├── images/ │ │ ├── train/ │ │ └── val/ │ └── labels/ │ ├── train/ │ └── val/转换代码convert_crowdhuman_to_yolo.pypythonimport json import os from tqdm import tqdm import cv2 # 类别映射CrowdHuman只有person一类 CLASS_MAPPING {person: 0} def convert_odgt_to_yolo(odgt_file, img_dir, label_dir): 将 CrowdHuman 的 odgt 文件转换为 YOLO 格式的 txt 文件 os.makedirs(label_dir, exist_okTrue) with open(odgt_file, r) as f: lines f.readlines() for line in tqdm(lines, descConverting): data json.loads(line.strip()) img_id data[ID] img_path os.path.join(img_dir, f{img_id}.jpg) # 读取图片获取宽高用于归一化 if not os.path.exists(img_path): # 尝试其他后缀 img_path os.path.join(img_dir, f{img_id}.png) if not os.path.exists(img_path): continue img cv2.imread(img_path) if img is None: continue img_h, img_w img.shape[:2] # 创建对应的label文件 label_file os.path.join(label_dir, f{img_id}.txt) with open(label_file, w) as lf: for obj in data[gtboxes]: if head in obj and obj[head] ! []: # 使用人头框避免身体遮挡带来的框重叠问题 box obj[head] cls_name person # 人头也视为person类 elif fbox in obj: box obj[fbox] cls_name person else: continue # box格式: [x, y, w, h] (左上角坐标宽高) x, y, w, h box # 转换为YOLO格式: [x_center, y_center, w, h] (均归一化) x_center (x w / 2) / img_w y_center (y h / 2) / img_h w_norm w / img_w h_norm h / img_h # 写入文件 lf.write(f0 {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n) print(fConversion completed. Labels saved to {label_dir}) if __name__ __main__: # 请根据实际路径修改 convert_odgt_to_yolo( odgt_filedatasets/crowdhuman/annotation_train.odgt, img_dirdatasets/crowdhuman/images/train, label_dirdatasets/crowdhuman/labels/train ) convert_odgt_to_yolo( odgt_filedatasets/crowdhuman/annotation_val.odgt, img_dirdatasets/crowdhuman/images/val, label_dirdatasets/crowdhuman/labels/val )3.3 数据集配置文件创建一个crowdhuman.yaml文件用于训练yaml# datasets/crowdhuman.yaml path: ./datasets/crowdhuman # 数据集根目录 train: images/train # 训练图片相对路径 val: images/val # 验证图片相对路径 # 类别数及名称 nc: 1 names: [person]第四章 模型训练与对比YOLOv5/v8/v10我们将分别训练三个模型并使用相同的测试集进行对比。确保你已经安装了相应的环境。4.1 环境准备bash# 通用依赖 pip install torch torchvision opencv-python tqdm # YOLOv5 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt # YOLOv8 YOLOv10 (通过Ultralytics统一包) pip install ultralytics4.2 训练YOLOv5修改yolov5/data/crowdhuman.yaml或者软链接我们的配置文件然后开始训练。训练脚本train_yolov5.pypythonimport os # 进入yolov5目录 os.chdir(./yolov5) # 训练命令 # --img: 输入图片大小 # --batch: 批次大小根据显存调整 # --epochs: 训练轮数 # --data: 数据集配置文件 # --weights: 预训练权重 !python train.py --img 640 --batch 32 --epochs 100 \ --data ../datasets/crowdhuman.yaml \ --weights yolov5s.pt \ --name crowdhuman_v5s关键参数说明yolov5s.pt轻量级版本适合快速实验。如果追求精度可以使用yolov5l.pt或yolov5x.pt。训练日志保存在runs/train/crowdhuman_v5s目录下。4.3 训练YOLOv8YOLOv8的命令行接口更加统一。训练脚本train_yolov8.pypythonfrom ultralytics import YOLO # 加载预训练模型 model YOLO(yolov8s.pt) # 也可使用 yolov8l.pt, yolov8x.pt # 训练模型 results model.train( data../datasets/crowdhuman.yaml, # 数据集配置 epochs100, imgsz640, batch32, namecrowdhuman_v8s, device0, # GPU设备 workers8, pretrainedTrue, optimizerAdamW, lr00.001 # 初始学习率 )4.4 训练YOLOv10YOLOv10的训练方式与YOLOv8类似因为也封装在Ultralytics中。训练脚本train_yolov10.pypythonfrom ultralytics import YOLO # 加载YOLOv10模型需要先下载v10的权重文件 # 可以从官方GitHub下载https://github.com/THU-MIG/yolov10 model YOLO(yolov10s.pt) # 或 yolov10n.pt, yolov10m.pt, yolov10b.pt, yolov10l.pt, yolov10x.pt # 开始训练 results model.train( data../datasets/crowdhuman.yaml, epochs100, imgsz640, batch32, namecrowdhuman_v10s, device0, workers8, optimizerAdamW, lr00.001 )4.5 模型性能对比训练完成后我们可以通过验证集来对比三个模型的性能mAP0.5, mAP0.5:0.95, 参数量, 推理速度。模型输入大小mAP0.5mAP0.5:0.95参数量(M)GPU推理速度(ms)有无NMSYOLOv5s6400.8420.5637.26.4有YOLOv8s6400.8550.57911.27.8有YOLOv10s6400.8620.5888.05.5无注以上数据仅为演示模拟实际数值因数据集和硬件而异。分析YOLOv5s最经典部署生态最好速度快精度略低。YOLOv8s精度最高但参数量略大速度稍慢。YOLOv10s速度和精度的完美平衡无NMS的特性使其在拥挤场景下处理大量检测框时更加流畅。第五章 系统UI界面设计PyQt5为了让模型落地应用我们设计一个包含以下功能的UI模型加载支持选择YOLOv5/v8/v10的权重文件。输入源图片、视频、摄像头。实时显示显示检测结果和实时人数。密度预警设定人数阈值触发红色边框警告。统计图表实时绘制人数变化曲线。5.1 UI框架搭建主界面代码main_window.pypythonimport sys import cv2 import torch import numpy as np from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * from ultralytics import YOLO import warnings warnings.filterwarnings(ignore) class DensityEstimationThread(QThread): 视频处理线程避免阻塞UI change_pixmap_signal pyqtSignal(np.ndarray) # 发送原始图像用于显示 update_count_signal pyqtSignal(int) # 更新人数计数 update_curve_signal pyqtSignal(list) # 更新曲线数据 def __init__(self): super().__init__() self.model None self.device cuda if torch.cuda.is_available() else cpu self.running False self.video_source 0 # 0表示摄像头也可以是视频文件路径 self.cap None self.threshold 50 # 预警阈值 self.count_history [] # 用于绘制曲线 def set_model(self, model_path, model_typev8): 加载模型model_type用于兼容v5/v8/v10但Ultralytics统一了接口 try: self.model YOLO(model_path) print(fModel loaded from {model_path} on {self.device}) except Exception as e: print(fError loading model: {e}) def set_video_source(self, source): self.video_source source def set_threshold(self, thresh): self.threshold thresh def run(self): self.running True if self.video_source 0: self.cap cv2.VideoCapture(0) # 摄像头 else: self.cap cv2.VideoCapture(self.video_source) if not self.cap.isOpened(): print(无法打开视频源) return fps int(self.cap.get(cv2.CAP_PROP_FPS)) if fps 0: fps 25 self.count_history [] while self.running: ret, frame self.cap.read() if not ret: break # 进行推理 if self.model is not None: results self.model(frame, conf0.5, deviceself.device, verboseFalse)[0] # 绘制检测框 annotated_frame results.plot() # 统计人数 person_count len(results.boxes) # 根据人数改变边框颜色预警功能 if person_count self.threshold: cv2.putText(annotated_frame, fWARNING: OVERCROWDED! Count: {person_count}, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 3) # 添加红色边框 cv2.rectangle(annotated_frame, (0, 0), (annotated_frame.shape[1]-1, annotated_frame.shape[0]-1), (0, 0, 255), 10) else: cv2.putText(annotated_frame, fCount: {person_count}, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 3) # 更新数据 self.count_history.append(person_count) if len(self.count_history) 100: # 保留最近100帧的数据 self.count_history.pop(0) self.update_count_signal.emit(person_count) self.update_curve_signal.emit(self.count_history) else: annotated_frame frame # 发送信号 self.change_pixmap_signal.emit(annotated_frame) # 控制帧率 self.msleep(30) # 大约33ms一帧约30fps self.cap.release() self.running False def stop(self): self.running False self.msleep(50) if self.cap is not None: self.cap.release() class MainWindow(QMainWindow): def __init__(self): super().__init__() self.initUI() self.thread DensityEstimationThread() self.initSignals() def initUI(self): self.setWindowTitle(人群密度估计系统 - YOLOv5/v8/v10) self.setGeometry(100, 100, 1400, 800) # 中央控件 central_widget QWidget() self.setCentralWidget(central_widget) main_layout QHBoxLayout(central_widget) # 左侧控制面板 control_panel QWidget() control_panel.setMaximumWidth(300) control_layout QVBoxLayout(control_panel) # 模型选择组 model_group QGroupBox(模型配置) model_layout QVBoxLayout() self.model_combo QComboBox() self.model_combo.addItems([YOLOv5, YOLOv8, YOLOv10]) model_layout.addWidget(QLabel(选择模型版本:)) model_layout.addWidget(self.model_combo) self.model_path_edit QLineEdit() self.model_path_edit.setPlaceholderText(模型权重文件路径 (.pt)) model_layout.addWidget(QLabel(权重文件:)) model_layout.addWidget(self.model_path_edit) self.browse_btn QPushButton(浏览...) self.browse_btn.clicked.connect(self.browse_model) model_layout.addWidget(self.browse_btn) self.load_model_btn QPushButton(加载模型) self.load_model_btn.clicked.connect(self.load_model) model_layout.addWidget(self.load_model_btn) model_group.setLayout(model_layout) control_layout.addWidget(model_group) # 输入源组 input_group QGroupBox(输入源) input_layout QVBoxLayout() self.source_combo QComboBox() self.source_combo.addItems([摄像头, 视频文件, 图片文件]) self.source_combo.currentIndexChanged.connect(self.source_changed) input_layout.addWidget(QLabel(选择输入:)) input_layout.addWidget(self.source_combo) self.source_path_edit QLineEdit() self.source_path_edit.setPlaceholderText(文件路径或摄像头ID(0)) input_layout.addWidget(self.source_path_edit) self.browse_source_btn QPushButton(浏览...) self.browse_source_btn.clicked.connect(self.browse_source) input_layout.addWidget(self.browse_source_btn) input_group.setLayout(input_layout) control_layout.addWidget(input_group) # 预警设置 warn_group QGroupBox(预警设置) warn_layout QVBoxLayout() self.threshold_spin QSpinBox() self.threshold_spin.setRange(1, 500) self.threshold_spin.setValue(50) warn_layout.addWidget(QLabel(人数预警阈值:)) warn_layout.addWidget(self.threshold_spin) warn_group.setLayout(warn_layout) control_layout.addWidget(warn_group) # 控制按钮 self.start_btn QPushButton(开始检测) self.start_btn.clicked.connect(self.start_detection) self.stop_btn QPushButton(停止检测) self.stop_btn.clicked.connect(self.stop_detection) self.stop_btn.setEnabled(False) control_layout.addWidget(self.start_btn) control_layout.addWidget(self.stop_btn) # 实时信息显示 info_group QGroupBox(实时信息) info_layout QVBoxLayout() self.count_label QLabel(当前人数: 0) self.count_label.setStyleSheet(font-size: 20px; font-weight: bold; color: blue;) info_layout.addWidget(self.count_label) self.status_label QLabel(状态: 未检测) info_layout.addWidget(self.status_label) info_group.setLayout(info_layout) control_layout.addWidget(info_group) control_layout.addStretch() # 右侧显示区 display_panel QWidget() display_layout QVBoxLayout(display_panel) # 图像显示 self.image_label QLabel() self.image_label.setFixedSize(800, 600) self.image_label.setStyleSheet(border: 2px solid black;) self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setText(等待视频启动...) display_layout.addWidget(self.image_label) # 人数曲线简单示意可用matplotlib嵌入这里简化用QLabel代替 self.curve_label QLabel() self.curve_label.setFixedHeight(100) self.curve_label.setStyleSheet(border: 1px solid gray; background-color: white;) display_layout.addWidget(QLabel(人数变化趋势:)) display_layout.addWidget(self.curve_label) # 添加左右布局 main_layout.addWidget(control_panel) main_layout.addWidget(display_panel) def initSignals(self): self.thread.change_pixmap_signal.connect(self.update_image) self.thread.update_count_signal.connect(self.update_count) self.thread.update_curve_signal.connect(self.update_curve) def browse_model(self): fname, _ QFileDialog.getOpenFileName(self, 选择模型文件, , PyTorch Model (*.pt)) if fname: self.model_path_edit.setText(fname) def browse_source(self): if self.source_combo.currentIndex() 1: # 视频 fname, _ QFileDialog.getOpenFileName(self, 选择视频文件, , Video Files (*.mp4 *.avi *.mov)) if fname: self.source_path_edit.setText(fname) elif self.source_combo.currentIndex() 2: # 图片 fname, _ QFileDialog.getOpenFileName(self, 选择图片文件, , Image Files (*.jpg *.png)) if fname: self.source_path_edit.setText(fname) def source_changed(self, index): if index 0: # 摄像头 self.source_path_edit.setText(0) self.source_path_edit.setEnabled(False) self.browse_source_btn.setEnabled(False) else: self.source_path_edit.setEnabled(True) self.browse_source_btn.setEnabled(True) self.source_path_edit.clear() def load_model(self): model_path self.model_path_edit.text() if not model_path: QMessageBox.warning(self, 警告, 请先选择模型文件) return # 根据选择的版本可能需要对模型做一些预处理但Ultralytics统一了 model_type self.model_combo.currentText().lower().replace(yolo, ) self.thread.set_model(model_path, model_type) self.status_label.setText(状态: 模型已加载) def start_detection(self): # 设置视频源 if self.source_combo.currentIndex() 0: self.thread.set_video_source(0) else: source self.source_path_edit.text() if not source: QMessageBox.warning(self, 警告, 请输入有效的视频源) return # 如果是数字字符串转为int if source.isdigit(): source int(source) self.thread.set_video_source(source) # 设置阈值 self.thread.set_threshold(self.threshold_spin.value()) # 启动线程 self.thread.start() self.start_btn.setEnabled(False) self.stop_btn.setEnabled(True) self.status_label.setText(状态: 检测中...) def stop_detection(self): self.thread.stop() self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) self.status_label.setText(状态: 已停止) self.image_label.clear() self.image_label.setText(等待视频启动...) self.count_label.setText(当前人数: 0) self.curve_label.clear() def update_image(self, cv_img): 将OpenCV图像转换为QPixmap并显示 rgb_image cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape bytes_per_line ch * w qt_image QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) self.image_label.setPixmap(QPixmap.fromImage(qt_image).scaled( self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) def update_count(self, count): self.count_label.setText(f当前人数: {count}) if count self.threshold_spin.value(): self.count_label.setStyleSheet(font-size: 20px; font-weight: bold; color: red;) else: self.count_label.setStyleSheet(font-size: 20px; font-weight: bold; color: blue;) def update_curve(self, history): 简单绘制人数曲线用QPainter绘制在curve_label上 if not history: return # 创建一个画布 pixmap QPixmap(self.curve_label.size()) pixmap.fill(Qt.white) painter QPainter(pixmap) painter.setRenderHint(QPainter.Antialiasing) # 绘制坐标轴 width pixmap.width() height pixmap.height() margin 20 painter.drawLine(margin, height - margin, width - margin, height - margin) # x轴 painter.drawLine(margin, margin, margin, height - margin) # y轴 # 绘制折线 if len(history) 1: max_count max(history) if max(history) 0 else 1 step (width - 2 * margin) / (len(history) - 1) points [] for i, count in enumerate(history): x margin i * step # y坐标反转因为屏幕y轴向下 y (height - margin) - (count / max_count) * (height - 2 * margin) points.append(QPointF(x, y)) # 画线 pen QPen(QColor(0, 0, 255), 2) painter.setPen(pen) for i in range(1, len(points)): painter.drawLine(points[i-1], points[i]) # 画点 pen.setColor(QColor(255, 0, 0)) painter.setPen(pen) for point in points: painter.drawEllipse(point, 3, 3) painter.end() self.curve_label.setPixmap(pixmap) if __name__ __main__: app QApplication(sys.argv) window MainWindow() window.show() sys.exit(app.exec_())