基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的跌倒检测系统详解(深度学习模型+UI界面代码+训练数据集)
摘要
跌倒,尤其是老年人或特殊工作场景下的跌倒,是引发严重人身伤害的主要原因之一。传统的监控方式依赖人工值守,效率低下且容易漏报。本文旨在详细介绍如何利用当前最先进的YOLO(You Only Look Once)目标检测算法(涵盖v5、v6、v7、v8版本),构建一个高效、实时的跌倒检测系统。内容将涵盖核心算法原理解析、公开数据集介绍与处理、模型训练与优化技巧、以及一个完整的PyQt5/PySide6可视化界面开发。我们将提供从零到一的完整代码,读者可依据本教程复现整个系统,或将其作为模板开发其他行为检测应用。
关键词: YOLOv8, 跌倒检测, 深度学习, 计算机视觉, PyQt, 实时监测, 安全监护
目录
2.3 YOLOv8:Ultralytics的新一代集大成者
1. 引言:问题背景与研究意义
随着全球人口老龄化趋势加剧,独居老人的安全监护成为一个严峻的社会问题。据统计,跌倒是65岁以上老年人因伤害死亡的首位原因。同时,在建筑工地、工厂等高风险作业区域,工人跌倒也可能导致严重事故。传统的视频监控需要安保人员持续盯着多个屏幕,不仅人力成本高,且易因疲劳导致疏忽。
基于计算机视觉的自动跌倒检测系统应运而生。这类系统通过分析视频流,自动识别“人”这一目标,并判断其姿态是否为“跌倒”状态,从而及时发出警报。在众多目标检测算法中,YOLO系列以其卓越的速度-精度平衡,成为实时跌倒检测的理想选择。
为什么选择YOLO?
-
单阶段检测:将目标检测视为一个回归问题,直接在输出层回归边界框和类别,速度极快。
-
实时性:在通用GPU上可达30 FPS甚至更高,满足实时监控需求。
-
高精度:从YOLOv1发展到YOLOv8,精度已可比肩甚至超越两阶段检测器(如Faster R-CNN)。
-
生态完善:拥有活跃的社区和详尽的文档,便于研究和部署。
本文将以YOLOv8为主要框架(兼顾与其他版本的对比),构建一个端到端的跌倒检测系统。
2. YOLO系列算法演进与核心原理精讲
2.1 YOLOv5:工程化的典范
YOLOv5并非官方YOLO系列,但其凭借极致的工程化优化和用户友好性,成为工业界最受欢迎的版本之一。
-
网络结构:Backbone使用CSPDarknet, Neck采用PANet+FPN, Head为解耦头(Decoupled Head)。
-
创新点:
-
自适应锚框计算:在训练前根据数据集自动计算最佳锚框尺寸。
-
自适应图片缩放:减少推理时的冗余计算。
-
Mosaic数据增强:将四张图片拼接,提升小目标检测和场景泛化能力。
-
-
优势:代码结构清晰,训练/部署流程极其简单,适合快速原型开发。
2.2 YOLOv6 & YOLOv7:面向工业场景的优化
-
YOLOv6(美团):重设计了Backbone(EfficientRep)和Neck(Rep-PAN),引入了更高效的RepConv重参数化卷积,在硬件上推理速度更快。
-
YOLOv7:提出了扩展高效层聚合网络(E-ELAN) 和基于级联的模型缩放。其“可训练免费袋”(Trainable Bag-of-Freebies)技术,如重参数化、标签分配策略等,在不增加推理成本的情况下大幅提升精度。
2.3 YOLOv8:Ultralytics的新一代集大成者
YOLOv8是Ultralytics公司发布的最新版本,提供了一个统一的框架,支持目标检测、实例分割、姿态估计等多种任务。
-
主要改进:
-
新的Backbone和Neck:C2f模块替代了C3模块,保留了更丰富的梯度流信息。
-
无锚(Anchor-Free)检测:直接预测目标中心点,避免了锚框先验的复杂性与超参数敏感性问题,简化了训练流程。
-
新的损失函数:分类任务使用二元交叉熵(BCE),回归任务使用Distribution Focal Loss(DFL) + CIoU Loss的组合,提升边界框回归精度。
-
更灵活的任务接口:通过
model = YOLO('yolov8n.pt')模式,无缝切换检测、分割、姿态等任务。
-
鉴于YOLOv8的先进性、易用性和活跃维护,本系统将主要采用YOLOv8进行开发,但提供的代码架构兼容v5/v7。
3. 跌倒检测数据集:构建与准备
高质量的数据集是模型性能的基石。
3.1 参考公开数据集
-
UR Fall Detection Dataset (URFD):经典数据集,包含30个跌倒和40个日常活动视频序列。提供RGB和深度图像,但规模较小。
-
Multiple Cameras Fall Dataset (MCFD):多视角跌倒数据集,包含8个场景下24个人的384个视频(192个跌倒,192个日常活动)。
-
Fall Detection Dataset (FDD):一个较大的数据集,包含191个视频(130个跌倒,61个日常活动)。
-
自行构建数据集:在确保隐私和伦理的前提下,可通过模拟拍摄(使用假人、演员)或从公开影视资源中收集跌倒和日常活动片段。
提示:由于公开数据集规模有限,且可能涉及隐私,建议使用合成数据或小规模自建数据集作为起点,并利用迁移学习(使用在COCO等大型数据集上预训练的权重)。
3.2 数据标注与YOLO格式转换
YOLO要求特定的标注格式:每个图像对应一个.txt文件,每行代表一个对象。<class_id> <x_center> <y_center> <width> <height>
坐标值为归一化后的值(0-1)。
我们可以使用强大的标注工具LabelImg或Roboflow进行标注,类别通常为:person_standing(站立/行走)、person_falling(跌倒)。为了简化,本教程将跌倒检测视为一个细粒度目标检测问题,即“人”是一个大类,而“站立”和“跌倒”是两种状态。更复杂的方案可以使用姿态估计关键点(如YOLOv8-Pose)来判断姿态。
示例数据集结构 (datasets/):
text
Fall_Detection/
├── images/
│ ├── train/
│ │ ├── video1_frame_001.jpg
│ │ └── ...
│ └── val/
│ └── ...
└── labels/
├── train/
│ ├── video1_frame_001.txt
│ └── ...
└── val/
└── ...
3.3 数据集配置文件
创建 dataset.yaml 文件:
yaml
# datasets/Fall_Detection.yaml path: ../datasets/Fall_Detection # 数据集根目录 train: images/train # 训练集路径(相对于path) val: images/val # 验证集路径 # 类别数 nc: 2 # 类别名称 names: ['person_standing', 'person_falling'] # 可选:下载地址/自动下载 #download: ...
4. 模型训练与性能优化
4.1 环境配置
bash
# 创建conda环境 (Python>=3.8) conda create -n yolo_fall_det python=3.9 -y conda activate yolo_fall_det # 安装PyTorch (根据CUDA版本选择) pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 安装Ultralytics YOLOv8 (核心库) pip install ultralytics # 可选:安装YOLOv5 (如果需要对比) # git clone https://github.com/ultralytics/yolov5 # cd yolov5 # pip install -r requirements.txt # 安装UI库 (我们选择PySide6, Qt for Python的官方版本) pip install PySide6 opencv-python pillow
4.2 训练脚本与参数解析
使用YOLOv8训练非常简单,其命令行接口(CLI)和Python API都非常直观。
脚本 train.py:
python
from ultralytics import YOLO
import argparse
import os
def train_model(config):
"""
训练YOLOv8模型
Args:
config: 包含训练参数的配置字典
"""
# 加载模型
# 你可以选择不同的预训练模型: yolov8n.pt, yolov8s.pt, yolov8m.pt, yolov8l.pt, yolov8x.pt
# 'n'=nano, 's'=small, 'm'=medium, 'l'=large, 'x'=extra large
model = YOLO(config['model'])
# 训练模型
results = model.train(
data=config['data'], # 数据集配置文件路径
epochs=config['epochs'], # 训练轮数
patience=config['patience'], # 早停耐心值
batch=config['batch'], # 批量大小
imgsz=config['imgsz'], # 输入图像尺寸
device=config['device'], # 设备,如 '0' 或 '0,1,2,3' 或 'cpu'
workers=config['workers'], # 数据加载线程数
project=config['project'], # 项目名称
name=config['name'], # 实验名称
exist_ok=config['exist_ok'], # 是否覆盖现有实验
pretrained=config['pretrained'], # 是否使用预训练权重
optimizer=config['optimizer'], # 优化器,如 'SGD', 'Adam', 'AdamW'
lr0=config['lr0'], # 初始学习率
lrf=config['lrf'], # 最终学习率因子 (lr0 * lrf)
momentum=config['momentum'], # 动量
weight_decay=config['weight_decay'], # 权重衰减
warmup_epochs=config['warmup_epochs'], # 热身轮数
box=config['box'], # 框损失权重
cls=config['cls'], # 分类损失权重
dfl=config['dfl'], # DFL损失权重 (YOLOv8)
hsv_h=config['hsv_h'], # 色调增强幅度 (0-1)
hsv_s=config['hsv_s'], # 饱和度增强幅度 (0-1)
hsv_v=config['hsv_v'], # 明度增强幅度 (0-1)
degrees=config['degrees'], # 旋转角度范围
translate=config['translate'], # 平移范围
scale=config['scale'], # 缩放范围
shear=config['shear'], # 剪切范围
perspective=config['perspective'], # 透视变换系数
flipud=config['flipud'], # 上下翻转概率
fliplr=config['fliplr'], # 左右翻转概率
mosaic=config['mosaic'], # Mosaic增强概率
mixup=config['mixup'], # Mixup增强概率
copy_paste=config['copy_paste'], # Copy-Paste增强概率
resume=config['resume'], # 是否从上次检查点恢复训练
verbose=config['verbose'], # 是否打印详细输出
seed=config['seed'], # 随机种子
deterministic=config['deterministic'], # 是否确定性训练
single_cls=config['single_cls'], # 将所有类别视为单一类别
amp=config['amp'], # 是否使用自动混合精度训练
fraction=config['fraction'], # 数据集使用比例 (用于调试)
profile=config['profile'], # 是否在训练期间进行性能分析
freeze=config['freeze'], # 冻结前n层或指定层列表
# ... 更多参数详见 https://docs.ultralytics.com/modes/train/#arguments
)
# 打印最佳模型指标
print(f"训练完成!最佳模型mAP50-95: {results.best_fitness}")
return model, results
def parse_opt():
parser = argparse.ArgumentParser()
parser.add_argument('--model', type=str, default='yolov8n.pt', help='初始模型权重路径')
parser.add_argument('--data', type=str, default='datasets/Fall_Detection.yaml', help='数据集配置文件路径')
parser.add_argument('--epochs', type=int, default=100)
parser.add_argument('--patience', type=int, default=50, help='早停耐心值')
parser.add_argument('--batch', type=int, default=16)
parser.add_argument('--imgsz', type=int, default=640)
parser.add_argument('--device', default='0', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--workers', type=int, default=8)
parser.add_argument('--project', default='runs/train', help='保存结果的项目目录')
parser.add_argument('--name', default='exp', help='实验名称')
parser.add_argument('--exist_ok', action='store_true', help='允许覆盖现有实验')
parser.add_argument('--pretrained', action='store_true', default=True, help='是否使用预训练权重')
parser.add_argument('--optimizer', type=str, default='SGD', choices=['SGD', 'Adam', 'AdamW'])
parser.add_argument('--lr0', type=float, default=0.01)
parser.add_argument('--lrf', type=float, default=0.01)
parser.add_argument('--momentum', type=float, default=0.937)
parser.add_argument('--weight_decay', type=float, default=0.0005)
parser.add_argument('--warmup_epochs', type=int, default=3)
parser.add_argument('--warmup_momentum', type=float, default=0.8)
parser.add_argument('--warmup_bias_lr', type=float, default=0.1)
parser.add_argument('--box', type=float, default=7.5, help='框损失权重')
parser.add_argument('--cls', type=float, default=0.5, help='分类损失权重')
parser.add_argument('--dfl', type=float, default=1.5, help='DFL损失权重')
parser.add_argument('--hsv_h', type=float, default=0.015)
parser.add_argument('--hsv_s', type=float, default=0.7)
parser.add_argument('--hsv_v', type=float, default=0.4)
parser.add_argument('--degrees', type=float, default=0.0)
parser.add_argument('--translate', type=float, default=0.1)
parser.add_argument('--scale', type=float, default=0.5)
parser.add_argument('--shear', type=float, default=0.0)
parser.add_argument('--perspective', type=float, default=0.0)
parser.add_argument('--flipud', type=float, default=0.0)
parser.add_argument('--fliplr', type=float, default=0.5)
parser.add_argument('--mosaic', type=float, default=1.0)
parser.add_argument('--mixup', type=float, default=0.0)
parser.add_argument('--copy_paste', type=float, default=0.0)
parser.add_argument('--resume', action='store_true', help='从最新检查点恢复训练')
parser.add_argument('--verbose', action='store_true')
parser.add_argument('--seed', type=int, default=42)
parser.add_argument('--deterministic', action='store_true')
parser.add_argument('--single_cls', action='store_true', help='将所有类别视为单一类别')
parser.add_argument('--amp', action='store_true', default=True, help='使用自动混合精度')
parser.add_argument('--fraction', type=float, default=1.0)
parser.add_argument('--profile', action='store_true')
parser.add_argument('--freeze', type=int, default=None, help='冻结前n层参数')
return parser.parse_args()
if __name__ == '__main__':
opt = parse_opt()
config = vars(opt) # 转换为字典
train_model(config)
运行训练:
bash
python train.py --data datasets/Fall_Detection.yaml --epochs 100 --imgsz 640 --batch 16 --device 0 --name fall_det_v8n
4.3 模型评估与验证
训练完成后,模型保存在 runs/train/fall_det_v8n/weights/ 目录下(best.pt 和 last.pt)。
评估脚本 val.py:
python
from ultralytics import YOLO
def evaluate_model(model_path, data_path, device='0'):
"""
评估模型性能
"""
# 加载训练好的最佳模型
model = YOLO(model_path)
# 在验证集上评估模型
metrics = model.val(
data=data_path,
imgsz=640,
batch=32,
device=device,
conf=0.001, # 评估时使用的置信度阈值
iou=0.6,
half=True, # 使用半精度评估以加速
plots=True, # 生成评估图表
save_json=False, # 是否保存JSON格式的结果
save_hybrid=False,
max_det=300,
)
# 打印关键指标
print(f"mAP50-95: {metrics.box.map:.4f}")
print(f"mAP50: {metrics.box.map50:.4f}")
print(f"mAP75: {metrics.box.map75:.4f}")
print(f"Precision: {metrics.box.precision.mean():.4f}")
print(f"Recall: {metrics.box.recall.mean():.4f}")
return metrics
if __name__ == '__main__':
# 指定训练好的最佳模型路径和数据集配置文件
model_path = 'runs/train/fall_det_v8n/weights/best.pt'
data_path = 'datasets/Fall_Detection.yaml'
evaluate_model(model_path, data_path, device='0')
5. 实时跌倒检测系统UI界面开发
我们使用PySide6开发一个功能完整的桌面应用程序。
5.1 主界面设计 (main_window.py)
python
import sys
import os
import cv2
import time
import threading
import queue
from pathlib import Path
from datetime import datetime
import numpy as np
from PySide6.QtCore import Qt, QTimer, Signal, QThread, Slot, QSize
from PySide6.QtGui import QImage, QPixmap, QFont, QIcon, QAction, QPalette, QColor
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QLabel,
QPushButton, QVBoxLayout, QHBoxLayout,
QFileDialog, QMessageBox, QComboBox,
QSpinBox, QDoubleSpinBox, QCheckBox,
QGroupBox, QFormLayout, QTextEdit,
QProgressBar, QStatusBar, QSplitter,
QTabWidget, QListWidget, QListWidgetItem)
from ultralytics import YOLO
from ultralytics.utils.plotting import Annotator, colors
class DetectionThread(QThread):
"""
检测线程,防止UI卡顿
"""
# 定义信号:发送检测结果(带标注的图像)和检测信息
result_signal = Signal(np.ndarray, list) # image, detections list
info_signal = Signal(str)
finished_signal = Signal()
error_signal = Signal(str)
def __init__(self):
super().__init__()
self.model = None
self.source = None # 视频源:0为摄像头,或文件路径
self.conf_thres = 0.5
self.iou_thres = 0.45
self.is_running = False
self.pause_flag = False
self.model_loaded = False
self.fall_count = 0
self.total_frames = 0
self.fps = 0
def load_model(self, model_path):
"""加载YOLO模型"""
try:
self.model = YOLO(model_path)
self.model_loaded = True
self.info_signal.emit(f"模型加载成功: {os.path.basename(model_path)}")
except Exception as e:
self.error_signal.emit(f"模型加载失败: {str(e)}")
def set_source(self, source):
"""设置视频源"""
self.source = source
def set_params(self, conf_thres, iou_thres):
"""设置检测参数"""
self.conf_thres = conf_thres
self.iou_thres = iou_thres
def run(self):
"""线程主循环"""
if not self.model_loaded or self.source is None:
self.error_signal.emit("模型或视频源未设置")
return
cap = cv2.VideoCapture(self.source)
if not cap.isOpened():
self.error_signal.emit(f"无法打开视频源: {self.source}")
return
self.is_running = True
self.fall_count = 0
self.total_frames = 0
fps_start_time = time.time()
fps_frame_count = 0
while self.is_running:
if self.pause_flag:
time.sleep(0.1)
continue
ret, frame = cap.read()
if not ret:
if self.source == 0: # 摄像头
time.sleep(0.1)
continue
else: # 视频文件结束
break
self.total_frames += 1
fps_frame_count += 1
# 执行检测
results = self.model(frame,
conf=self.conf_thres,
iou=self.iou_thres,
verbose=False)[0]
# 处理检测结果
detections = []
annotator = Annotator(frame, line_width=2)
if results.boxes is not None:
boxes = results.boxes.cpu().numpy()
for box in boxes:
# 获取坐标、置信度和类别
x1, y1, x2, y2 = box.xyxy[0].astype(int)
conf = box.conf[0]
cls_id = int(box.cls[0])
cls_name = self.model.names[cls_id]
# 统计跌倒次数
if cls_name == 'person_falling':
self.fall_count += 1
# 触发警报(这里用信号模拟)
if conf > 0.7: # 高置信度跌倒才报警
self.info_signal.emit(f"警告:检测到跌倒!置信度: {conf:.2f}")
# 添加到检测列表
detections.append({
'bbox': [x1, y1, x2, y2],
'confidence': conf,
'class': cls_name,
'class_id': cls_id
})
# 绘制边界框和标签
color = colors(cls_id, True)
label = f'{cls_name} {conf:.2f}'
annotator.box_label([x1, y1, x2, y2], label, color=color)
# 计算FPS
if time.time() - fps_start_time >= 1.0:
self.fps = fps_frame_count
fps_frame_count = 0
fps_start_time = time.time()
# 添加FPS和统计信息到图像
info_text = f"FPS: {self.fps} | 总帧数: {self.total_frames} | 跌倒次数: {self.fall_count}"
cv2.putText(frame, info_text, (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
# 发送结果
self.result_signal.emit(frame, detections)
# 控制处理速度
time.sleep(0.01) # 最小延迟
cap.release()
self.finished_signal.emit()
def stop(self):
"""停止检测"""
self.is_running = False
def pause(self):
"""暂停检测"""
self.pause_flag = True
def resume(self):
"""继续检测"""
self.pause_flag = False
class MainWindow(QMainWindow):
"""主窗口类"""
def __init__(self):
super().__init__()
self.model = None
self.detection_thread = None
self.current_video_path = None
self.is_camera_mode = False
self.init_ui()
self.init_menu()
self.init_status_bar()
def init_ui(self):
"""初始化用户界面"""
self.setWindowTitle("基于YOLOv8的跌倒检测系统 v1.0")
self.setGeometry(100, 100, 1400, 900)
# 设置应用图标和样式
self.setStyleSheet("""
QMainWindow {
background-color: #f0f0f0;
}
QGroupBox {
font-weight: bold;
border: 2px solid #cccccc;
border-radius: 5px;
margin-top: 10px;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
QPushButton {
padding: 8px 15px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #e0e0e0;
}
QPushButton#start_btn {
background-color: #4CAF50;
color: white;
}
QPushButton#stop_btn {
background-color: #f44336;
color: white;
}
QPushButton#pause_btn {
background-color: #ff9800;
color: white;
}
QTextEdit {
background-color: white;
border: 1px solid #cccccc;
border-radius: 3px;
font-family: Consolas, monospace;
}
""")
# 创建中心窗口部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout(central_widget)
# 左侧控制面板
control_panel = self.create_control_panel()
main_layout.addWidget(control_panel, 1)
# 右侧显示区域
display_panel = self.create_display_panel()
main_layout.addWidget(display_panel, 3)
def create_control_panel(self):
"""创建左侧控制面板"""
panel = QWidget()
layout = QVBoxLayout(panel)
# 模型加载组
model_group = QGroupBox("模型配置")
model_layout = QFormLayout()
self.model_combo = QComboBox()
self.model_combo.addItems(['yolov8n.pt', 'yolov8s.pt', 'yolov8m.pt', 'yolov8l.pt', 'yolov8x.pt'])
self.model_combo.setCurrentText('yolov8n.pt')
self.load_model_btn = QPushButton("加载模型")
self.load_model_btn.clicked.connect(self.load_model)
model_layout.addRow("模型选择:", self.model_combo)
model_layout.addRow(self.load_model_btn)
model_group.setLayout(model_layout)
# 检测参数组
params_group = QGroupBox("检测参数")
params_layout = QFormLayout()
self.conf_spin = QDoubleSpinBox()
self.conf_spin.setRange(0.01, 1.0)
self.conf_spin.setValue(0.5)
self.conf_spin.setSingleStep(0.05)
self.iou_spin = QDoubleSpinBox()
self.iou_spin.setRange(0.1, 1.0)
self.iou_spin.setValue(0.45)
self.iou_spin.setSingleStep(0.05)
params_layout.addRow("置信度阈值:", self.conf_spin)
params_layout.addRow("IoU阈值:", self.iou_spin)
params_group.setLayout(params_layout)
# 视频源组
source_group = QGroupBox("视频源")
source_layout = QVBoxLayout()
self.camera_radio = QCheckBox("摄像头")
self.camera_radio.toggled.connect(self.on_camera_toggled)
self.file_radio = QCheckBox("视频文件")
self.file_radio.setChecked(True)
self.select_file_btn = QPushButton("选择视频文件")
self.select_file_btn.clicked.connect(self.select_video_file)
source_layout.addWidget(self.camera_radio)
source_layout.addWidget(self.file_radio)
source_layout.addWidget(self.select_file_btn)
source_group.setLayout(source_layout)
# 控制按钮组
control_group = QGroupBox("控制")
control_layout = QVBoxLayout()
self.start_btn = QPushButton("开始检测")
self.start_btn.setObjectName("start_btn")
self.start_btn.clicked.connect(self.start_detection)
self.pause_btn = QPushButton("暂停")
self.pause_btn.setObjectName("pause_btn")
self.pause_btn.clicked.connect(self.pause_detection)
self.pause_btn.setEnabled(False)
self.stop_btn = QPushButton("停止")
self.stop_btn.setObjectName("stop_btn")
self.stop_btn.clicked.connect(self.stop_detection)
self.stop_btn.setEnabled(False)
self.screenshot_btn = QPushButton("截图")
self.screenshot_btn.clicked.connect(self.take_screenshot)
control_layout.addWidget(self.start_btn)
control_layout.addWidget(self.pause_btn)
control_layout.addWidget(self.stop_btn)
control_layout.addWidget(self.screenshot_btn)
control_group.setLayout(control_layout)
# 统计信息组
stats_group = QGroupBox("统计信息")
stats_layout = QFormLayout()
self.fps_label = QLabel("0")
self.total_frames_label = QLabel("0")
self.fall_count_label = QLabel("0")
stats_layout.addRow("FPS:", self.fps_label)
stats_layout.addRow("总帧数:", self.total_frames_label)
stats_layout.addRow("跌倒次数:", self.fall_count_label)
stats_group.setLayout(stats_layout)
# 添加到主布局
layout.addWidget(model_group)
layout.addWidget(params_group)
layout.addWidget(source_group)
layout.addWidget(control_group)
layout.addWidget(stats_group)
layout.addStretch()
return panel
def create_display_panel(self):
"""创建右侧显示面板"""
panel = QWidget()
layout = QVBoxLayout(panel)
# 创建标签页
self.tab_widget = QTabWidget()
# 视频显示标签页
video_tab = QWidget()
video_layout = QVBoxLayout(video_tab)
self.video_label = QLabel()
self.video_label.setAlignment(Qt.AlignCenter)
self.video_label.setMinimumSize(640, 480)
self.video_label.setText("视频显示区域")
self.video_label.setStyleSheet("border: 2px solid #cccccc; background-color: black; color: white;")
video_layout.addWidget(self.video_label)
# 检测结果标签页
result_tab = QWidget()
result_layout = QVBoxLayout(result_tab)
self.result_text = QTextEdit()
self.result_text.setReadOnly(True)
self.result_text.setMaximumHeight(200)
self.detection_list = QListWidget()
result_layout.addWidget(QLabel("检测日志:"))
result_layout.addWidget(self.result_text)
result_layout.addWidget(QLabel("检测结果列表:"))
result_layout.addWidget(self.detection_list)
# 添加标签页
self.tab_widget.addTab(video_tab, "视频显示")
self.tab_widget.addTab(result_tab, "检测结果")
layout.addWidget(self.tab_widget)
return panel
def init_menu(self):
"""初始化菜单栏"""
menubar = self.menuBar()
# 文件菜单
file_menu = menubar.addMenu("文件")
load_model_action = QAction("加载模型", self)
load_model_action.triggered.connect(self.load_model)
file_menu.addAction(load_model_action)
open_video_action = QAction("打开视频", self)
open_video_action.triggered.connect(self.select_video_file)
file_menu.addAction(open_video_action)
file_menu.addSeparator()
exit_action = QAction("退出", self)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 工具菜单
tools_menu = menubar.addMenu("工具")
settings_action = QAction("设置", self)
tools_menu.addAction(settings_action)
# 帮助菜单
help_menu = menubar.addMenu("帮助")
about_action = QAction("关于", self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def init_status_bar(self):
"""初始化状态栏"""
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("就绪")
def load_model(self):
"""加载模型"""
model_name = self.model_combo.currentText()
# 这里可以改为从文件对话框选择自定义模型
try:
if self.detection_thread is None:
self.detection_thread = DetectionThread()
self.detection_thread.result_signal.connect(self.update_display)
self.detection_thread.info_signal.connect(self.update_log)
self.detection_thread.finished_signal.connect(self.detection_finished)
self.detection_thread.error_signal.connect(self.show_error)
self.detection_thread.load_model(model_name)
self.update_log(f"加载模型: {model_name}")
self.status_bar.showMessage(f"模型已加载: {model_name}")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载模型失败: {str(e)}")
def select_video_file(self):
"""选择视频文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择视频文件",
str(Path.home()),
"视频文件 (*.mp4 *.avi *.mov *.mkv *.flv);;所有文件 (*.*)"
)
if file_path:
self.current_video_path = file_path
self.is_camera_mode = False
self.update_log(f"选择视频文件: {file_path}")
self.status_bar.showMessage(f"已选择文件: {os.path.basename(file_path)}")
def on_camera_toggled(self, checked):
"""摄像头模式切换"""
if checked:
self.is_camera_mode = True
self.current_video_path = 0 # 默认摄像头
self.update_log("切换到摄像头模式")
self.status_bar.showMessage("摄像头模式")
else:
self.is_camera_mode = False
def start_detection(self):
"""开始检测"""
if self.detection_thread is None or not self.detection_thread.model_loaded:
QMessageBox.warning(self, "警告", "请先加载模型")
return
if self.current_video_path is None and not self.is_camera_mode:
QMessageBox.warning(self, "警告", "请选择视频源")
return
source = 0 if self.is_camera_mode else self.current_video_path
self.detection_thread.set_source(source)
self.detection_thread.set_params(self.conf_spin.value(), self.iou_spin.value())
# 更新按钮状态
self.start_btn.setEnabled(False)
self.pause_btn.setEnabled(True)
self.stop_btn.setEnabled(True)
# 启动检测线程
self.detection_thread.start()
self.update_log("开始检测...")
self.status_bar.showMessage("检测中...")
def pause_detection(self):
"""暂停/继续检测"""
if self.detection_thread and self.detection_thread.isRunning():
if self.detection_thread.pause_flag:
self.detection_thread.resume()
self.pause_btn.setText("暂停")
self.update_log("继续检测")
else:
self.detection_thread.pause()
self.pause_btn.setText("继续")
self.update_log("暂停检测")
def stop_detection(self):
"""停止检测"""
if self.detection_thread and self.detection_thread.isRunning():
self.detection_thread.stop()
self.detection_thread.wait()
# 重置按钮状态
self.start_btn.setEnabled(True)
self.pause_btn.setEnabled(False)
self.stop_btn.setEnabled(False)
self.pause_btn.setText("暂停")
self.update_log("检测停止")
self.status_bar.showMessage("就绪")
def detection_finished(self):
"""检测完成"""
self.start_btn.setEnabled(True)
self.pause_btn.setEnabled(False)
self.stop_btn.setEnabled(False)
self.update_log("检测完成")
@Slot(np.ndarray, list)
def update_display(self, frame, detections):
"""更新视频显示"""
# 转换图像格式 (BGR -> RGB -> QImage)
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = frame_rgb.shape
bytes_per_line = ch * w
q_img = QImage(frame_rgb.data, w, h, bytes_per_line, QImage.Format_RGB888)
# 缩放图像以适应标签
pixmap = QPixmap.fromImage(q_img)
scaled_pixmap = pixmap.scaled(
self.video_label.size(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.video_label.setPixmap(scaled_pixmap)
# 更新统计信息
if self.detection_thread:
self.fps_label.setText(str(self.detection_thread.fps))
self.total_frames_label.setText(str(self.detection_thread.total_frames))
self.fall_count_label.setText(str(self.detection_thread.fall_count))
# 更新检测结果列表
self.detection_list.clear()
for det in detections:
item_text = f"[{det['class']}] 置信度: {det['confidence']:.2f}, 位置: {det['bbox']}"
item = QListWidgetItem(item_text)
# 根据类别设置不同的颜色
if det['class'] == 'person_falling':
item.setForeground(QColor(255, 0, 0)) # 红色
else:
item.setForeground(QColor(0, 0, 0)) # 黑色
self.detection_list.addItem(item)
@Slot(str)
def update_log(self, message):
"""更新日志"""
timestamp = datetime.now().strftime("%H:%M:%S")
log_message = f"[{timestamp}] {message}"
self.result_text.append(log_message)
# 滚动到底部
scrollbar = self.result_text.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
@Slot(str)
def show_error(self, error_msg):
"""显示错误信息"""
QMessageBox.critical(self, "错误", error_msg)
self.update_log(f"错误: {error_msg}")
def take_screenshot(self):
"""截图保存"""
if self.video_label.pixmap() is None:
QMessageBox.warning(self, "警告", "没有可保存的图像")
return
file_path, _ = QFileDialog.getSaveFileName(
self, "保存截图",
str(Path.home() / f"screenshot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"),
"PNG图像 (*.png);;JPEG图像 (*.jpg);;所有文件 (*.*)"
)
if file_path:
self.video_label.pixmap().save(file_path)
self.update_log(f"截图已保存: {file_path}")
def show_about(self):
"""显示关于对话框"""
about_text = """
<h2>基于YOLOv8的跌倒检测系统</h2>
<p>版本: 1.0</p>
<p>功能: 实时跌倒检测与报警</p>
<p>技术支持: Ultralytics YOLOv8</p>
<p>UI框架: PySide6</p>
<hr>
<p>© 2023 计算机视觉研究组</p>
"""
QMessageBox.about(self, "关于", about_text)
def closeEvent(self, event):
"""关闭事件"""
reply = QMessageBox.question(
self, '确认退出',
'确定要退出程序吗?',
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
if self.detection_thread and self.detection_thread.isRunning():
self.detection_thread.stop()
self.detection_thread.wait()
event.accept()
else:
event.ignore()
def main():
"""主函数"""
app = QApplication(sys.argv)
app.setStyle('Fusion')
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()更多推荐
所有评论(0)