简介基于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_okTrue) 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, ffall01_{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)注意转换时平方坐标系的中心点计算(x1x2)/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_okTrue) os.makedirs(labels/val, exist_okTrue) 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))数据集划分图片数量跌倒样本正常样本训练集480026002200验证集1200650550上表是一个比较合理的数据规模。如果你的原始数据只有一两千张建议先用--augment参数打开在线增强或复制跌倒样本做水平翻转和亮度扰动而不是强行增加训练轮数。数据到位后下面进入YOLOv7的训练阶段。3. 模型训练与参数调优YOLOv7在自定义数据集上的复现路径3.1 建立独立的Python环境我不建议在系统全局Python里直接装依赖因为YOLOv7的requirements要求torch1.7.0而你的其他项目可能已经在用PyTorch 2.x两者冲突会非常痛苦。用conda创建独立环境是常见做法conda create -n fall_yolov7 python3.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 mAP0.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显存不足OOMbatch-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_reloadTrue) 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_reloadTrue是为了防止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_thresh0.8, speed_thresh0.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 normalfall_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:password192.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(falarm_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 ffall_{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比如只在座椅前的地面区域启用跌倒判定这样可以彻底排除过路人员的干扰。本文还有配套的精品资源点击获取
