简介:基于Python的YOLOv7人员跌倒检测系统是一套可直接运行学习的完整项目,面向计算机视觉初学者、安防监控开发人员及养老机构/公共区域安全管理者。资源打包了源代码、教程和专用数据集,能帮助读者快速掌握YOLOv7在跌倒识别场景中的落地方案,项目整体仅14.9MB,共20个文件,以17张效果展示图片、1个Python脚本、1个README说明文档和1个txt说明为主,结构清晰便于按需查阅。目前已有199人学习下载。代码基于YOLOv7目标检测框架,结合OpenCV等Python生态实现实时监测,教程部分覆盖数据预处理、模型训练、评估优化与系统集成等关键环节;数据集提供正常与跌倒行为的标注样本,配合示例图片可直观对照检测效果。尤其适合需要快速构建跌倒警报原型、或想深入理解YOLO系列工程化应用的读者,是一份轻量但完整度较高的参考资源。
1. 跌倒检测选YOLOv7,核心是扛住实时监控的帧率波动
养老院走廊的摄像头通常要同时跑8路画面,值班室的大屏上如果出现一个老人从站立到倒地,留给系统反应的时间往往不到1.5秒。我拆过几个跌倒检测项目,最后都从姿态估计转向了目标检测路线,原因很简单:姿态估计需要先检测到人再回归关键点,在遮挡和低照度下关键点经常飞掉,而YOLOv7这类一阶段检测器能直接给出稳定的边界框,配合连续帧的框坐标变化就能做跌倒判定。YOLOv7在同等精度下比YOLOv5的推理速度快约50%,在RTX 3060上跑640分辨率可以达到60fps以上,这是选择它的硬指标。这个资源适合已经具备Python基础、想在自己采集的视频数据上复现完整跌倒检测流程的开发者,也适合需要把模型部署到实时监控系统中的工程人员。下文从数据整理开始,一步步走完训练、判定和部署全链路。
2. 数据集与标注格式:把跌倒样本整理成YOLOv7能吃的结构
2.1 原始数据目录规划
下载解压后你会看到以person-fall-detection-yolo-python命名的根目录,里面除了模型脚本和README,还有大量按序号命名的图片和几个视频片段。项目自带的listdir.py用来列出当前目录下所有文件,我建议先跑一遍它确认图片数量是否完整,避免因解压失败导致训练集缺失。完整的数据准备阶段,我会按以下结构重新组织目录:
fall_detection/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── fall.yaml └── split_data.py这一步的核心思路是让图片和同名txt标签文件分离,YOLO的训练脚本会通过路径拼接同时加载它们。原始数据中如果混有大量从视频抽帧得到的连续相似帧,一定要先做相似帧剔除,否则训练集和验证集会出现数据泄漏,模型评估指标虚高,落地后跌检测率立刻拉垮。
2.2 用OpenCV抽帧与清晰度筛选
原始视频素材往往是25fps的MP4文件,直接全部抽帧会导致同一动作被重复采样几百次。我一般会按每5帧抽取1帧,并用拉普拉斯方差作为清晰度指标,去掉低于阈值的模糊帧。下面这段代码就是把视频转成训练图片的标准处理流程:
import cv2 import os import numpy as np video_path = "fall_videos/fall_01.mp4" output_dir = "images/train" os.makedirs(output_dir, exist_ok=True) cap = cv2.VideoCapture(video_path) frame_idx = 0 save_idx = 0 while True: ret, frame = cap.read() if not ret: break if frame_idx % 5 == 0: gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var() if laplacian_var > 50: # 清晰度阈值 save_path = os.path.join(output_dir, f"fall01_{save_idx:05d}.jpg") cv2.imwrite(save_path, frame) save_idx += 1 frame_idx += 1 cap.release() print(f"抽取完成,共保留 {save_idx} 帧")代码里frame_idx % 5 == 0控制抽帧频率,25fps视频相当于每秒保留5帧,足够捕捉跌倒动作的连续变化。Laplacian算子的方差值越大代表图像越清晰,阈值50是我在室内监控场景下的经验值;如果素材本身光照极好或极差,需要先跑几帧统计方差分布再定阈值。抽取后的图片必须统一为JPG格式,并且文件名避免使用中文和空格,否则后续标注工具和YOLO脚本容易在路径解析上踩坑。
2.3 将标注转为YOLO txt格式
数据集里的原始标注可能是LabelImg生成的VOC XML或Text格式,YOLO需要的是一行一个目标的归一化坐标文件。转换代码的关键是把<bndbox>中的绝对像素坐标转换为相对于图片宽高的值。下面是VOC XML转YOLO txt的完整脚本:
import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, output_path, class_map): tree = ET.parse(xml_path) root = tree.getroot() img_width = int(root.find("size/width").text) img_height = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): class_name = obj.find("name").text if class_name not in class_map: continue class_id = class_map[class_name] bbox = obj.find("bndbox") x1 = int(bbox.find("xmin").text) y1 = int(bbox.find("ymin").text) x2 = int(bbox.find("xmax").text) y2 = int(bbox.find("ymax").text) center_x = ((x1 + x2) / 2) / img_width center_y = ((y1 + y2) / 2) / img_height box_width = (x2 - x1) / img_width box_height = (y2 - y1) / img_height lines.append(f"{class_id} {center_x:.6f} {center_y:.6f} {box_width:.6f} {box_height:.6f}") with open(output_path, "w") as f: f.write("\n".join(lines) + "\n") class_map = {"fall": 0, "normal": 1} voc_to_yolo("annotations/fall01.xml", "labels/train/fall01.txt", class_map)注意转换时平方坐标系的中心点计算:(x1+x2)/2必须是浮点数,Python 3中普通除法没问题,但如果你从旧代码粘贴来的脚本用了//,会导致中心点直接变成0,训练Loss根本不收敛。归一化后的坐标值应始终在0到1之间,但当目标越过图片边界时可能出现略大于1的值,YOLO训练会裁剪,不必手工清洗。转换完成后,用下面的脚本校验标签与图片是否一一对应:
import os img_dir = "images/train" label_dir = "labels/train" img_files = {os.path.splitext(f)[0] for f in os.listdir(img_dir)} label_files = {os.path.splitext(f)[0] for f in os.listdir(label_dir)} missing_labels = img_files - label_files missing_images = label_files - img_files print("缺标签的图片数:", len(missing_labels)) print("缺图片的标签数:", len(missing_images))这个校验步骤非常关键,因为数据集里经常混有未标注的负样本。如果一张图片没有标签文件,YOLO默认会忽略它;但如果标注了文件却找不到原图,训练会直接报错。
2.4 划分训练集与验证集
目标检测需要独立验证集来评估mAP,不能把同一段视频的连续帧同时放进训练集和验证集。我通常在抽帧阶段就按视频来源拆分:同一视频的所有帧只属于train或val。下面这段划分脚本按文件名的视频前缀进行分配:
import os import random from collections import defaultdict image_dir = "images/train" video_groups = defaultdict(list) for img_file in os.listdir(image_dir): prefix = img_file.split("_")[0] # 取视频前缀 fall01 video_groups[prefix].append(img_file) val_videos = random.sample(list(video_groups.keys()), max(1, len(video_groups) // 5)) val_images = set() for v in val_videos: val_images.update(video_groups[v]) os.makedirs("images/val", exist_ok=True) os.makedirs("labels/val", exist_ok=True) for img in val_images: os.rename(os.path.join("images/train", img), os.path.join("images/val", img)) label_file = img.replace(".jpg", ".txt") os.rename(os.path.join("labels/train", label_file), os.path.join("labels/val", label_file))| 数据集划分 | 图片数量 | 跌倒样本 | 正常样本 |
|---|---|---|---|
| 训练集 | 4800 | 2600 | 2200 |
| 验证集 | 1200 | 650 | 550 |
上表是一个比较合理的数据规模。如果你的原始数据只有一两千张,建议先用--augment参数打开在线增强,或复制跌倒样本做水平翻转和亮度扰动,而不是强行增加训练轮数。数据到位后,下面进入YOLOv7的训练阶段。
3. 模型训练与参数调优:YOLOv7在自定义数据集上的复现路径
3.1 建立独立的Python环境
我不建议在系统全局Python里直接装依赖,因为YOLOv7的requirements要求torch>=1.7.0,而你的其他项目可能已经在用PyTorch 2.x,两者冲突会非常痛苦。用conda创建独立环境是常见做法:
conda create -n fall_yolov7 python=3.8 -y conda activate fall_yolov7 cd person-fall-detection-yolo-python pip install -r requirements.txt资源自带requirements.txt中包括opencv-python、tqdm、matplotlib、torch和torchvision等。由于国内网络原因,如果pip下载缓慢,可以加-i https://pypi.tuna.tsinghua.edu.cn/simple镜像源。安装完成后,先跑一段单张图片的检测确认环境可用:
python detect.py --weights yolov7.pt --source data/images/fall_sample.jpg其中yolov7.pt是预训练权重,直接运行python detect.py会自动下载COCO预训练模型。如果这一步能正常输出检测框,说明torch与CUDA版本兼容。如果报No module named 'torch',多半是conda环境没激活,或者pip装到了另一个Python目录下。
3.2 配置数据文件与模型结构文件
在data/目录下新建fall.yaml,内容如下:
train: ./images/train val: ./images/val nc: 1 names: ['fall']nc: 1表示只检测一个类别,也就是跌倒的人。这里我只保留了跌倒类,实际项目中建议同时保留normal类,否则模型对正常行走的误检率会偏高。接着打开cfg/training/yolov7.yaml,把配置文件里的nc: 80改成nc: 1,并确认depth_multiple和width_multiple保持默认值。如果显存只有6GB,可以把yolov7.yaml替换为yolov7-tiny.yaml,但检测精度会下降约10%,在跌倒这类小目标上不明显,可以接受。
3.3 训练命令详解与断点续训
正常情况下,一张RTX 3060 12GB显存可以跑到batch-size 16。训练命令如下:
python train.py \ --workers 4 \ --device 0 \ --batch-size 16 \ --data data/fall.yaml \ --img 640 \ --cfg cfg/training/yolov7.yaml \ --weights 'yolov7.pt' \ --name fall_det \ --hyp data/hyp.scratch.p5.yaml \ --epochs 100参数含义:--workers 4指定数据加载线程数,Windows系统建议设为0,否则容易报DataLoader worker错误;--device 0指定GPU编号,CPU训练则写--device cpu但会慢得让人崩溃;--img 640是输入分辨率,跌倒检测中人体占画面比例较小,可以设为--img 960提升小目标召回率,但显存占用会增加约1.5倍;--name fall_det指定训练输出目录。首次训练时用yolov7.pt作为预训练权重,可以显著加快收敛并防止初期梯度爆炸。如果中断了训练,在相同命令后加--resume即可从最近checkpoint继续,YOLOv7会自动查找runs/train/fall_det/weights/last.pt。
3.4 训练Loss曲线判读与常见问题
训练中终端会每秒打印一次训练Loss、val Loss和mAP指标。值得关注的指标是val mAP@0.5,如果它在前20轮没有超过0.5,就属于训练异常。
| 现象 | 原因 | 处理方案 |
|---|---|---|
| Loss一开始就是NaN | 学习率过大或数据集有空白标签 | 调低hyp.scratch.p5.yaml中的lr0从0.01到0.001 |
| 验证集mAP一直为0 | 标签的class id越界 | 检查txt第一列数字不超过nc-1 |
| 训练集Loss下降但val Loss升高 | 过拟合 | 增加--cos-lr与数据增强,或提前停止epoch |
| 显存不足OOM | batch-size太大 | 降到8或4,同时把--img降到512 |
我遇过最隐蔽的一个坑是标签文件里的类别名写成了fall带空格,导致生成的txt第一列是"fall"字符串而不是数字,训练脚本直接抛异常。在转换脚本里加了class_id = int(class_name)这一层强制类型检查后问题才暴露。训练完成后,runs/train/fall_det/weights/best.pt就是我们要部署的模型。
4. 跌倒行为判定:从目标框到“人摔了”的算法逻辑
4.1 提取目标框坐标流
部署时,我们用YOLOv7对每一帧检测,获取人员边界框的中心点坐标和宽高。下面这段代码展示了如何加载训练好的best.pt并对视频逐帧做推理:
import cv2 import torch import numpy as np model = torch.hub.load('WongKinYiu/yolov7', 'custom', 'runs/train/fall_det/weights/best.pt', force_reload=True) model.cuda().eval() cap = cv2.VideoCapture("test_sequences/fall_test.mp4") prev_boxes = [] while True: ret, frame = cap.read() if not ret: break results = model(frame) detections = results.xyxy[0].cpu().numpy() boxes = [] for det in detections: x1, y1, x2, y2, conf, cls = det if int(cls) == 0 and conf > 0.5: boxes.append((x1, y1, x2, y2, conf)) current_frame_box = boxes # 将当前帧的框交给后端逻辑处理 prev_boxes = current_frame_box这里使用torch.hub.load加载本地权重,force_reload=True是为了防止torch.hub缓存了旧的模型文件。results.xyxy[0]是一个二维数组,每一行为[x1, y1, x2, y2, confidence, class],class对应训练时的类别索引。注意视频推理时要反转BGR到RGB,但YOLOv7的detect.py内部已处理这步,如果你直接调model(frame),传入OpenCV的原生BGR帧会得到偏色的检测结果。此时应写frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)再送入模型。
4.2 跌倒特征:宽高比、中心点速度与连续丢帧
拿到目标框后,跌倒判定不能只依赖单帧形状。一个站立的人框宽高比约为0.4,仰躺时这个值会反转为1.5以上,但侧躺时框会变得细长,宽高比降到0.2左右。所以单一ratio不可靠,必须综合以下三个特征:
- 宽高比突变:站立ratio接近0.4,跌倒瞬间ratio变化幅度超过0.8。
- 中心点Y坐标骤降:人在垂直方向上快速下落,连续3帧的y变化量累加超过图像高度的15%。
- 目标框短暂丢失:倒地后与地面颜色接近,检测置信度下降,导致连续几帧无检测框。
下面是实现跌倒特征提取的代码:
class FallDetector: def __init__(self, frame_height, ratio_thresh=0.8, speed_thresh=0.15): self.frame_height = frame_height self.ratio_thresh = ratio_thresh self.speed_thresh = speed_thresh self.prev_ratio = None self.prev_center_y = None self.fall_count = 0 self.missing_frames = 0 def update(self, box): if box is None: self.missing_frames += 1 if self.missing_frames > 5 and self.fall_count > 0: return "fall" return "uncertain" self.missing_frames = 0 x1, y1, x2, y2, conf = box w = x2 - x1 h = y2 - y1 ratio = h / w center_y = (y1 + y2) / 2 fall_flag = False if self.prev_ratio is not None: ratio_diff = abs(ratio - self.prev_ratio) y_speed = abs(center_y - self.prev_center_y) / self.frame_height if ratio_diff > self.ratio_thresh and y_speed > self.speed_thresh: self.fall_count += 1 else: self.fall_count = max(0, self.fall_count - 1) self.prev_ratio = ratio self.prev_center_y = center_y if self.fall_count >= 3: fall_flag = True return "fall" if fall_flag else "normal"fall_count >= 3表示连续3帧出现跌倒特征,这是为了防止人体正常转身时ratio短暂变化带来的误检。speed_thresh取0.15意味着中心点在单帧内移动超过画面高度15%才计数,坐下的速度一般是达不到这个值的。如果你摄像头画面高度只有480像素,15%就是72像素,实际场景中跌倒发生在0.4秒内,移动距离通常会超过150像素,这个阈值可以从上往下调。
4.3 状态机消除坐姿与弯腰误检
最容易被误判成跌倒的是坐椅子和弯腰捡东西。这两种情况下ratio同样会突然变大,但中心点Y轴移动速度明显不同。我推荐引入一个简单的时间状态机,而不是对每一帧单独分类:
STATES = {"STANDING": 0, "FALLING": 1, "DOWN": 2, "RECOVERED": 3} state = STATES["STANDING"] duration = 0 def state_transition(box): global state, duration if state == STATES["STANDING"]: if fall_feature_detected(box): state = STATES["FALLING"] duration = 0 elif state == STATES["FALLING"]: duration += 1 if duration > 5 and center_y_low(box): state = STATES["DOWN"] trigger_alarm() elif duration > 10 and not center_y_low(box): state = STATES["STANDING"] elif state == STATES["DOWN"]: if center_y_rises(box): state = STATES["RECOVERED"] elif state == STATES["RECOVERED"]: state = STATES["STANDING"] return state状态机的好处是把“跌倒”看作一个持续性事件:从站立短暂转换到FALLING,再确认到DOWN才触发告警。正常坐姿不会进入FALLING状态超过5帧,因为坐下时中心点下降速度是均匀的,达不到速度阈值。只有当模型连续5帧都在FALLING且中心点保持在低位,才确认跌倒。实际部署时,还需要结合场景摄像头高度来调整阈值,比如电梯内摄像头俯视角度大,框宽高比特征就会失真,这时可以把ratio阈值调高,更多依赖中心点下降速度。
5. 部署到实时监控:把检测模型接进视频流并输出告警
5.1 接入RTSP流并控制缓冲区延迟
实际监控项目中,摄像头输出多为RTSP流。OpenCV的VideoCapture在连接网络流时若不加处理,会因为缓冲区积累导致延迟越来越大,告警画面比实际事件慢5到10秒。我用的方案是缩短缓冲区并主动丢帧,让模型始终处理最新一帧:
cap = cv2.VideoCapture("rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101") cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) cap.set(cv2.CAP_PROP_FPS, 15) fps_target = 15 frame_time = 1.0 / fps_target while True: t0 = time.time() grabbed = cap.grab() if not grabbed: cap.reconnect() continue grabbed, frame = cap.retrieve() if not grabbed: continue frame = cv2.resize(frame, (640, 640)) results = model(frame) # 跌倒判定逻辑... # 丢弃积压帧 elapsed = time.time() - t0 if elapsed < frame_time: time.sleep(frame_time - elapsed) cap.grab() # 清空当前缓冲,保证下一帧不会排队cap.grab()只抓取帧但不解码,用来清空缓冲区非常有效。注意CAP_PROP_BUFFERSIZE在部分摄像头下不生效,所以我在循环末尾额外加了一次cap.grab(),粗暴但可靠。
5.2 告警日志与截图留存
监控系统触发告警时,必须留下可追溯的现场数据。我一般把告警事件写入CSV并保存三张连续截图,便于事后核查是否误报。下面是日志落盘代码:
import csv import datetime def save_alarm(frame, box, alarm_id): ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") cv2.imwrite(f"alarm_snapshots/{alarm_id}_{ts}.jpg", frame) with open("alarm_log.csv", "a", newline="") as f: writer = csv.writer(f) writer.writerow([ts, alarm_id, box[0], box[1], box[2], box[3]]) # 告警触发时 alarm_id = f"fall_{int(time.time())}" save_alarm(frame_with_box, box, alarm_id)box保存的是原始像素坐标,而不是归一化坐标,方便后续在图片上直接画框复现。如果项目需要推送到手机,可以在此基础上把alarm_id发到MQTT broker,值班室的订阅端收到消息后拉取截图。
5.3 验证检测延迟与阈值适用性
部署到真实环境前,先用一段人为模拟跌倒的视频做端到端验证。我用这样一个脚本统计从跌倒动作开始到告警触发的帧间隔:
start_time = None for frame in test_frames: result = detect(frame) if is_falling_start: start_time = time.time() if state == "DOWN": delay = time.time() - start_time print(f"告警延迟: {delay*1000:.1f} ms") break调整speed_thresh和fall_count时,分别用跑步、坐下、捡东西、正常走路四段视频做回归测试。通常speed_thresh从0.15降到0.1会提升跌倒召回率,但会把快速坐下识别成跌倒,增量还需要结合具体场景的摄像头安装角度来决定。另一个技巧是设置检测区域ROI,比如只在座椅前的地面区域启用跌倒判定,这样可以彻底排除过路人员的干扰。
本文还有配套的精品资源,点击获取