简介:这份资源面向计算机视觉方向的本科毕业生与深度学习入门者,提供一套可直接运行的摔倒检测完整方案,解决从人体关键点提取到动作分类的工程落地问题。项目以YOLOv5完成人体检测,结合OpenPose提取骨骼关键点,再通过动作分类网络判断跌倒行为,适合作为毕业设计、课程设计或姿态识别练手项目。压缩包共193个文件,约40.29MB,包含39个Python脚本、21个pyc缓存、17个yaml与6个yml配置、2个pt权重及2个jit模型,另有75张jpg与11张jpeg样本图、若干txt与xml标注说明,覆盖训练、推理与配置全流程。资源已获导师认可并通过严格调试,可直接运行;若需检测其他姿势,可按说明收集图片、运行runOpenpose.py生成关键点图,再分类放入data/train与data/test,最后执行action_detect/train.py完成再训练。目前已有1243人学习下载,适合希望快速掌握姿态检测与跌倒识别完整链路的学习者参考。
1. 摔倒检测这件事:为什么 YOLOv5 加 OpenPose 是毕业设计里最稳的组合
做计算机毕业设计选题时,摔倒检测是个高频方向,但真正动手才发现坑不少。纯靠 YOLOv5 做目标检测,只能框出“人”这个类别,判断不了人是站着还是躺着;纯靠 OpenPose 拿骨架,又缺少人体框的定位,画面里人一多就串线。把两者串起来——YOLOv5 负责“找到人”,OpenPose 负责“看清姿态”,再用几何规则判断摔倒——这套方案在毕业设计里落地成本低、可解释性强、答辩时讲得清楚。这篇笔记就按这个思路,从环境配置、数据准备、模型推理到摔倒判定逻辑,把每一步的参数和踩坑点讲透,适合正在做人工智能专业毕业设计、想用 Python 快速跑通一个完整项目的同学。
2. 环境配置与两个模型的加载:conda 隔离、权重放置、推理入口
2.1 为什么必须用 conda 隔离环境
YOLOv5 和 OpenPose 对 PyTorch、NumPy、OpenCV 的版本要求经常打架。我见过太多人直接在 base 环境里 pip install,结果 YOLOv5 能跑,OpenPose 一 import 就报undefined symbol。常见做法是建一个独立环境,把两个模型的依赖锁在各自能接受的版本区间里。
# 创建独立环境,Python 版本选 3.8,兼容性最好 conda create -n fall_detect python=3.8 -y conda activate fall_detect # 先装 PyTorch,YOLOv5 和 OpenPose 都依赖它 # CUDA 11.3 对应 torch 1.10.1,这是经过验证的稳定组合 pip install torch==1.10.1+cu113 torchvision==0.11.2+cu113 -f https://download.pytorch.org/whl/cu113/torch_stable.html # 装 OpenCV 和常用科学计算库 pip install opencv-python==4.5.5.64 numpy==1.21.6 scipy==1.7.3 matplotlib==3.5.3参数说明:python=3.8不是随便选的,OpenPose 的 Python API 在 3.9 以上经常编译失败;torch==1.10.1对应 CUDA 11.3,如果你的显卡驱动只支持 CUDA 11.0,就降到torch==1.9.0+cu111。装完后用python -c "import torch; print(torch.cuda.is_available())"验证,返回 True 才算环境通了。
2.2 YOLOv5 权重放置与推理脚本
YOLOv5 源码从官方仓库拉下来后,权重文件yolov5s.pt放在weights/目录下。推理时不需要改源码,直接调detect.py或者自己写一个封装函数。
import torch import cv2 import numpy as np # 加载 YOLOv5 模型,这里用 yolov5s,速度快,适合毕业设计的实时性要求 model = torch.hub.load('ultralytics/yolov5', 'custom', path='weights/yolov5s.pt') model.conf = 0.5 # 置信度阈值,低于 0.5 的框直接丢弃 model.iou = 0.45 # NMS 的 IoU 阈值,防止同一个人被框多次 model.classes = [0] # 只检测 person 类,COCO 数据集里 person 的索引是 0 def detect_person(img): """输入 BGR 图像,返回每个人的边界框列表 [(x1,y1,x2,y2), ...]""" results = model(img) boxes = results.xyxy[0].cpu().numpy() # 格式 [x1, y1, x2, y2, conf, cls] person_boxes = [] for box in boxes: x1, y1, x2, y2 = map(int, box[:4]) person_boxes.append((x1, y1, x2, y2)) return person_boxes逻辑说明:model.conf=0.5是经验值,太低会框到背景里的杂物,太高会漏掉远处的人;model.classes=[0]把非人类目标全部过滤,减少后续 OpenPose 的计算量。返回的person_boxes直接喂给 OpenPose 做单人姿态估计,这样 OpenPose 就不用在整张图上跑,速度能提升 3 到 5 倍。
2.3 OpenPose 的两种接入方式与选择
OpenPose 在毕业设计里有两条路:一是编译官方 C++ 版本再调 Python API,二是直接用开源的轻量级 Python 实现。官方版本精度高但编译极其痛苦,我一般推荐用openpose-python或者mediapipe作为替代,后者在树莓派 5 上都能跑。
import mediapipe as mp mp_pose = mp.solutions.pose pose = mp_pose.Pose( static_image_mode=False, # 视频流模式,复用上一帧结果,更快 model_complexity=1, # 0/1/2,1 是精度和速度的平衡点 smooth_landmarks=True, # 平滑关键点,减少抖动 min_detection_confidence=0.5, min_tracking_confidence=0.5 ) def get_keypoints(img, box): """在 YOLOv5 框出的区域内做姿态估计,返回 33 个关键点坐标""" x1, y1, x2, y2 = box roi = img[y1:y2, x1:x2] # 裁剪出人体区域 roi_rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB) result = pose.process(roi_rgb) if result.pose_landmarks: h, w = roi.shape[:2] keypoints = [] for lm in result.pose_landmarks.landmark: # 把归一化坐标还原到原图坐标系 kx = int(lm.x * w) + x1 ky = int(lm.y * h) + y1 keypoints.append((kx, ky, lm.visibility)) return keypoints return None参数说明:model_complexity=1在 1080p 视频上单帧约 15ms,static_image_mode=False开启视频流模式后,MediaPipe 会利用上一帧的跟踪结果,实际耗时降到 8ms 左右。smooth_landmarks=True对摔倒检测很关键,因为摔倒瞬间关键点抖动剧烈,不平滑会误判。
3. 摔倒判定逻辑:从骨架坐标到“摔倒/未摔倒”的规则设计
3.1 用躯干角度做第一层判断
拿到 33 个关键点后,最直观的特征是躯干与地面的夹角。人站立时肩膀到髋部的连线接近垂直,摔倒时这条线接近水平。
import math def calculate_torso_angle(keypoints): """计算躯干与垂直方向的夹角,单位:度""" # MediaPipe 关键点索引:11=左肩,12=右肩,23=左髋,24=右髋 left_shoulder = keypoints[11] right_shoulder = keypoints[12] left_hip = keypoints[23] right_hip = keypoints[24] # 取肩膀和髋部的中心点 shoulder_center = ((left_shoulder[0] + right_shoulder[0]) / 2, (left_shoulder[1] + right_shoulder[1]) / 2) hip_center = ((left_hip[0] + right_hip[0]) / 2, (left_hip[1] + right_hip[1]) / 2) # 计算向量 dx = shoulder_center[0] - hip_center[0] dy = shoulder_center[1] - hip_center[1] # 与垂直方向的夹角,dy 取负是因为图像坐标系 y 轴向下 angle = abs(math.degrees(math.atan2(dx, -dy))) return angle逻辑说明:站立时angle接近 0 到 30 度,摔倒时躯干倾斜,angle会超过 60 度。但仅靠角度不够,因为弯腰捡东西也能到 60 度。所以需要第二层判断。
3.2 用人体框宽高比做第二层过滤
YOLOv5 给出的人体框,站立时高度远大于宽度,摔倒时宽度接近甚至超过高度。
def calculate_aspect_ratio(box): """计算人体框的宽高比""" x1, y1, x2, y2 = box w = x2 - x1 h = y2 - y1 if h == 0: return 0 return w / h def is_fall(box, keypoints, angle_thresh=60, ratio_thresh=0.8): """综合角度和宽高比判断是否摔倒""" angle = calculate_torso_angle(keypoints) ratio = calculate_aspect_ratio(box) # 两个条件同时满足才判定为摔倒,降低误报 if angle > angle_thresh and ratio > ratio_thresh: return True return False参数说明:angle_thresh=60是经验阈值,低于 60 度基本是正常活动;ratio_thresh=0.8表示框的宽度达到高度的 80% 以上。两个条件用and连接,宁可漏报也不误报,因为毕业设计答辩时误报比漏报更难解释。
3.3 加入时间窗口做第三层确认
单帧判断容易受遮挡和抖动影响,实际部署时要用一个滑动窗口,连续多帧都判定为摔倒才触发报警。
from collections import deque class FallDetector: def __init__(self, window_size=5, trigger_count=3): self.window = deque(maxlen=window_size) self.trigger_count = trigger_count def update(self, box, keypoints): fall = is_fall(box, keypoints) self.window.append(fall) # 窗口内超过 trigger_count 帧判定为摔倒,才最终确认 if sum(self.window) >= self.trigger_count: return True return False逻辑说明:window_size=5表示看最近 5 帧,trigger_count=3表示至少 3 帧判定为摔倒。这样即使某一帧关键点跳变,也不会立刻报警。在 30fps 的视频里,5 帧约 0.17 秒,响应速度足够快。
4. 数据准备与模型微调:用自定义数据集提升场景适配
4.1 毕业设计数据集从哪来
公开数据集里,UR Fall Detection Dataset 和 Le2i Fall Detection Dataset 是最常用的两个。UR Fall 包含 30 个摔倒视频和 40 个日常活动视频,Le2i 有 191 个视频。如果要做更贴近实际的场景,可以用手机在宿舍、走廊拍几十段视频,用 LabelImg 标注人体框,再用 YOLOv5 做微调。
# 用 LabelImg 标注后,生成 YOLO 格式的标签文件 # 目录结构: # dataset/ # images/ # train/ (存放训练图片) # val/ (存放验证图片) # labels/ # train/ (存放对应的 .txt 标签) # val/ # 创建 data.yaml 配置文件 cat > data.yaml << 'EOF' path: ./dataset train: images/train val: images/val nc: 1 names: ['person'] EOF参数说明:nc: 1表示只有一个类别 person,names列表里写类别名。标签文件每行格式是class_id x_center y_center width height,坐标都要归一化到 0 到 1 之间。
4.2 YOLOv5 微调的关键超参数
# 在 YOLOv5 目录下执行训练 python train.py \ --data data.yaml \ --weights yolov5s.pt \ --epochs 100 \ --batch-size 16 \ --img-size 640 \ --lr0 0.01 \ --lrf 0.01 \ --patience 20 \ --device 0参数说明:--epochs 100对毕业设计的数据量足够,太多会过拟合;--batch-size 16取决于显存,8G 显存跑 640 尺寸最多 16;--lr0 0.01是初始学习率,--lrf 0.01是最终学习率因子,YOLOv5 默认用余弦退火;--patience 20表示 20 轮验证集指标不提升就早停。训练完后在runs/train/exp/weights/best.pt拿到微调权重。
4.3 摔倒检测的评估指标怎么算
毕业设计答辩时老师一定会问准确率、召回率。摔倒检测不能只看整体准确率,因为正常样本远多于摔倒样本。
| 指标 | 计算公式 | 含义 | 毕业设计建议值 |
|---|---|---|---|
| 准确率 | (TP+TN)/(TP+TN+FP+FN) | 整体判对的比例 | >90% |
| 召回率 | TP/(TP+FN) | 摔倒样本被检出的比例 | >85% |
| 精确率 | TP/(TP+FP) | 报警中真正摔倒的比例 | >80% |
| F1 分数 | 2PR/(P+R) | 精确率和召回率的调和平均 | >82% |
TP 是正确检出的摔倒,FN 是漏掉的摔倒,FP 是把正常活动误报为摔倒。如果召回率低,就降低angle_thresh;如果精确率低,就提高ratio_thresh。
5. 避坑与排查:那些让我熬夜的翻车现场
5.1 现象:OpenPose 关键点全部挤在画面左上角
原因:YOLOv5 返回的框坐标是相对于原图的,但裁剪 ROI 后 OpenPose 输出的归一化坐标没有加回偏移量。解决:在get_keypoints里把lm.x * w + x1和lm.y * h + y1加上,确保坐标映射回原图。
5.2 现象:视频里人一多就串线,A 的骨架连到 B 身上
原因:OpenPose 在整张图上做多人姿态估计时,靠 PAF 做匹配,人挨得近就容易错。解决:先用 YOLOv5 框出每个人,再对每个框单独跑 OpenPose,从“多人”变成“多个单人”,串线问题直接消失。
5.3 现象:训练 loss 降到 0.01 但验证集准确率只有 60%
原因:数据集太小或者标注质量差,模型过拟合。解决:检查标签文件里有没有框超出图像边界,用albumentations做数据增强,把--epochs降到 50 并加--dropout 0.2。
5.4 现象:树莓派 5 上跑 YOLOv5 只有 2fps
原因:树莓派没有 CUDA,PyTorch 走 CPU 推理,yolov5s 在 CPU 上就是慢。解决:换成yolov5n权重,用 ONNX Runtime 或者 NCNN 做推理加速,输入尺寸从 640 降到 320,帧率能到 8 到 10fps。
5.5 现象:摔倒后 3 秒才报警,错过最佳响应时间
原因:滑动窗口window_size=5加上trigger_count=3,在低帧率下需要 1 秒以上。解决:把window_size降到 3,trigger_count设为 2,同时提高视频采集帧率到 25fps 以上。
6. 把检测结果推到前端:一个可演示的 Flask 页面与调参技巧
毕业设计最终要演示,光在命令行跑不够直观。我一般用 Flask 搭一个最小 Web 界面,把检测结果实时画在视频流上。
from flask import Flask, render_template, Response import cv2 app = Flask(__name__) camera = cv2.VideoCapture(0) # 0 是默认摄像头,也可以换成视频文件路径 detector = FallDetector(window_size=3, trigger_count=2) def gen_frames(): while True: success, frame = camera.read() if not success: break boxes = detect_person(frame) for box in boxes: kps = get_keypoints(frame, box) if kps: fall = detector.update(box, kps) color = (0, 0, 255) if fall else (0, 255, 0) x1, y1, x2, y2 = box cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) if fall: cv2.putText(frame, 'FALL!', (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2) ret, buffer = cv2.imencode('.jpg', frame) yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n') @app.route('/') def index(): return render_template('index.html') # 一个简单的 <img src="/video_feed"> 页面 @app.route('/video_feed') def video_feed(): return Response(gen_frames(), mimetype='multipart/x-mixed-replace; boundary=frame') if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)逻辑说明:gen_frames是一个生成器,每读一帧就做一次检测,把结果画上去再编码成 JPEG 推给前端。detector.update内部维护滑动窗口,只有连续多帧判定摔倒才画红框。debug=False在部署时一定要关,否则多线程会出问题。
调参时我习惯先固定angle_thresh=60,只调ratio_thresh,从 0.6 开始每次加 0.1,观察误报和漏报的平衡点。如果场景里椅子多,ratio_thresh要提到 0.9 以上,因为人坐在椅子上时框的宽高比也会接近 0.8。另外,min_detection_confidence在光线暗的走廊要降到 0.3,否则 OpenPose 直接丢帧。
这套方案我在三个毕业设计里用过,最深的教训是:不要一上来就追求端到端的深度学习分类模型,YOLOv5 加 OpenPose 加规则判断的组合,调试成本低、每一步都可解释,答辩时老师问“为什么阈值设 60 度”你能答上来,比黑匣子模型稳得多。希望帮到你。
本文还有配套的精品资源,点击获取