前言
利用YOLO V3 Tiny、Deepsort、ST-GCN方法来进行行人跟踪和摔倒检测。其中,YOLO V3 Tiny 用于行人检测,DeepSort 用于跟踪,ST-GCN 用于行为检测。
项目介绍
对于摔倒检测项目,有通过YOLO检测框直接判别的,有通过openpose进行识别的,但
对于摄像头方向的要求很高,侧面的角度检测效果比较理想,但位于高处的摄像头或者背面的角度检测效果较差。本项目使用ST-GCN 对于不同角度下的摔倒检测有较好的效果。
使用Tiny YOLO oneclass检测帧中的每个人,并使用AlphaPose获取骨架姿势,然后使用ST-GCN模型预测每个人跟踪的每30帧中的动作。
这个项目训练了一个新的TinyYOLO单类模型,只检测人的对象并减小模型的大小。使用旋转增强的COCO人关键点数据集进行训练,以便在不同角度姿势下进行更稳健的人检测。
对于动作识别,使用Le2i Fall detection Dataset(Coffee room,Home)中的数据通过AlphaPose提取骨骼姿势,并手动标记每个动作帧,用于训练ST-GCN模型。
现在支持7个动作:站立、行走、坐下、躺下、站起来、坐下、跌倒。
ST-GCN 介绍
给出一个动作视频的骨架序列信息,首先构造出表示该骨架序列信息的图结构,ST-GCN 的输入就是图节点上的关节坐标向量;然后是一系列时空图卷积操作,来提取高层的特征;最后用 SofMax 分类器得到对应的动作分类。整个过程实现了端到端的训练。
GCN 帮助我们学习了到空间中相邻关节的局部特征。在此基础上,我们需要学习时间中关节变化的局部特征。如何为 Graph 叠加时序特征,是图卷积网络面临的问题之一。这方面的研究主要有两个思路:时间卷积 (TCN) 和序列模型 (LSTM)。
ST-GCN 使用的是 TCN,由于形状固定,可以使用传统的卷积层完成时间卷积操作。为了便于理解,可以类比图像的卷积操作。ST-GCN的 feature map 最后三个维度的形状为 (C, V, T) ,与图像 feature map 的形状 (C, W, H) 相对应。
图像的通道数 C 对应关节的特征数 C。
图像的宽 W 对应关键帧数 V。
图像的高 H 对应关节数 T。
在图像卷积中,卷积核的大小为「w」×「1」,则每次完成 w 行像素、1 列像素的卷积。「stride」为 s,则每次移动 s 像素、完成 1 行后进行下 1 行像素的卷积。
在时间卷积中,卷积核的大小为「temporal_kernel_size」×「1」,则每次完成 1 个节点、temporal_kernel_size 个关键帧的卷积。「stride」为 1,则每次移动 1 帧、完成 1 个节点后进行下 1 个节点的卷积。以下是训练:
输入的数据首先进行 batch normalization,然后经过 9 个ST-GCN单元,接着是一个 global pooling,得到每个序列的 256 维特征向量,最后用 SoftMax 函数进行分类,得到最后的标签。
每一个ST-GCN采用Resnet的结构,前三层的输出有64个通道,中间三层有128个通道,最后三层有256个通道,在每次经过ST-CGN结构后,以0.5的概率随机将特征dropout,第4和第7个时域卷积层的strides设置为2。用SGD训练,学习率为0.01,每10个epochs学习率下降0.1。
ST-GCN论文链接:https://arxiv.org/abs/1801.07455
代码主体
import os
import cv2
import time
import torch
import argparse
import numpy as np
from Detection.Utils import ResizePadding
from CameraLoader import CamLoader, CamLoader_Q
from DetectorLoader import TinyYOLOv3_onecls
from PoseEstimateLoader import SPPE_FastPose
from fn import draw_single
from Track.Tracker import Detection, Tracker
from ActionsEstLoader import TSSTG
# source = '../Data/test_video/test7.mp4'
# source = '../Data/falldata/Home/Videos/video (2).avi' # hard detect ./output/test5.mp4
source = './output/test4.mp4'
# source = 2
def preproc(image):
"""preprocess function for CameraLoader.
"""
image = resize_fn(image)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
return image
def kpt2bbox(kpt, ex=20):
"""Get bbox that hold on all of the keypoints (x,y)获取保持所有关键点(x,y)的bbox
kpt: array of shape `(N, 2)`,
ex: (int) expand bounding box,
"""
return np.array((kpt[:, 0].min() - ex, kpt[:, 1].min() - ex,
kpt[:, 0].max() + ex, kpt[:, 1].max() + ex))
if __name__ == '__main__':
par = argparse.ArgumentParser(description='Human Fall Detection Demo.')
par.add_argument('-C', '--camera', default=source, # required=True, # default=2,
help='Source of camera or video file path.')
par.add_argument('--detection_input_size', type=int, default=384,
help='Size of input in detection model in square must be divisible by 32 (int).')
par.add_argument('--pose_input_size', type=str, default='224x160',
help='Size of input in pose model must be divisible by 32 (h, w)')
par.add_argument('--pose_backbone', type=str, default='resnet50', help='Backbone model for SPPE FastPose model.')
par.add_argument('--show_detected', default=False, action='store_true', help='Show all bounding box from detection.')
par.add_argument('--show_skeleton', default=True, action='store_true', help='Show skeleton pose.')
par.add_argument('--save_out', type=str, default='./output/output7.mp4', help='Save display to video file.')
par.add_argument('--device', type=str, default='cuda', help='Device to run model on cpu or cuda.')
args = par.parse_args()
device = args.device
# DETECTION MODEL.检测模型
inp_dets = args.detection_input_size
detect_model = TinyYOLOv3_onecls(inp_dets, device=device)
# POSE MODEL.姿势模型
inp_pose = args.pose_input_size.split('x')
inp_pose = (int(inp_pose[0]), int(inp_pose[1]))
pose_model = SPPE_FastPose(args.pose_backbone, inp_pose[0], inp_pose[1], device=device)
# Tracker.跟踪器
max_age = 30
tracker = Tracker(max_age=max_age, n_init=3)
# Actions Estimate.
action_model = TSSTG()
resize_fn = ResizePadding(inp_dets, inp_dets)
cam_source = args.camera
if type(cam_source) is str and os.path.isfile(cam_source):
# Use loader thread with Q for video file.
cam = CamLoader_Q(cam_source, queue_size=1000, preprocess=preproc).start()
else:
# Use normal thread loader for webcam.
cam = CamLoader(int(cam_source) if cam_source.isdigit() else cam_source,
preprocess=preproc).start()
# frame_size = cam.frame_size
# scf = torch.min(inp_size / torch.FloatTensor([frame_size]), 1)[0]
outvid = False
if args.save_out != '':
outvid = True
codec = cv2.VideoWriter_fourcc(*'mp4v')
print((inp_dets * 2, inp_dets * 2))
writer = cv2.VideoWriter(args.save_out, codec, 25, (inp_dets * 2, inp_dets * 2))
fps_time = 0
f = 0
while cam.grabbed():
f += 1
frame = cam.getitem()
image = frame.copy()
# Detect humans bbox in the frame with detector model.使用检测器模型检测帧中的人类bbox
detected = detect_model.detect(frame, need_resize=False, expand_bb=10)
# Predict each tracks bbox of current frame from previous frames information with Kalman filter.从先前帧信息预测当前帧的每个轨迹bbox
tracker.predict()
# Merge two source of predicted bbox together.将两个预测bbox源合并在一起
for track in tracker.tracks:
det = torch.tensor([track.to_tlbr().tolist() + [0.5, 1.0, 0.0]], dtype=torch.float32)
detected = torch.cat([detected, det], dim=0) if detected is not None else det
detections = [] # List of Detections object for tracking.用于跟踪的检测对象列表
if detected is not None:
# detected = non_max_suppression(detected[None, :], 0.45, 0.2)[0]
# Predict skeleton pose of each bboxs.预测每个BBOX的骨骼姿势。
poses = pose_model.predict(frame, detected[:, 0:4], detected[:, 4])
# Create Detections object.创建对象的检测。
detections = [Detection(kpt2bbox(ps['keypoints'].numpy()),
np.concatenate((ps['keypoints'].numpy(),
ps['kp_score'].numpy()), axis=1),
ps['kp_score'].mean().numpy()) for ps in poses]
# VISUALIZE.可视化
if args.show_detected:
for bb in detected[:, 0:5]:
frame = cv2.rectangle(frame, (bb[0], bb[1]), (bb[2], bb[3]), (0, 0, 255), 1)
# Update tracks by matching each track information of current and previous frame or通过匹配当前帧和前一帧的每个轨迹信息来更新轨迹
# create a new track if no matched.如果不匹配,则创建新轨迹。
tracker.update(detections)
# Predict Actions of each track.预测每个轨道的动作
for i, track in enumerate(tracker.tracks):
if not track.is_confirmed():
continue
track_id = track.track_id
bbox = track.to_tlbr().astype(int)
center = track.get_center().astype(int)
action = 'pending..'
clr = (0, 255, 0)
# Use 30 frames time-steps to prediction.使用30帧时间步长进行预测
if len(track.keypoints_list) == 30:
pts = np.array(track.keypoints_list, dtype=np.float32)
out = action_model.predict(pts, frame.shape[:2])
action_name = action_model.class_names[out[0].argmax()]
action = '{}: {:.2f}%'.format(action_name, out[0].max() * 100)
if action_name == 'Fall Down':
clr = (255, 0, 0)
elif action_name == 'Lying Down':
clr = (255, 200, 0)
# VISUALIZE.可视化
if track.time_since_update == 0:
if args.show_skeleton:
frame = draw_single(frame, track.keypoints_list[-1])
frame = cv2.rectangle(frame, (bbox[0], bbox[1]), (bbox[2], bbox[3]), (0, 255, 0), 1)
frame = cv2.putText(frame, str(track_id), (center[0], center[1]), cv2.FONT_HERSHEY_COMPLEX, 0.4, (255, 0, 0), 2)
frame = cv2.putText(frame, action, (bbox[0] + 5, bbox[1] + 15), cv2.FONT_HERSHEY_COMPLEX, 0.4, clr, 1)
# Show Frame.
frame = cv2.resize(frame, (0, 0), fx=2., fy=2.)
frame = cv2.putText(frame, '%d, FPS: %f' % (f, 1.0 / (time.time() - fps_time)), (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
frame = frame[:, :, ::-1]
fps_time = time.time()
if outvid:
writer.write(frame)
cv2.imshow('frame', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# Clear resource.
cam.stop()
if outvid:
writer.release()
cv2.destroyAllWindows()
检测效果
参考
AlphaPose : https://github.com/Amanbhandula/AlphaPose
ST-GCN: https://github.com/yysijie/st-gcn
总结
相较于其他摔倒检测算法,此算法的检测效果更好且能检测七种状态。但检测画面中人数较多时帧率会降低,对此改为使用YOLO+mediapipe进行检测,帧率无明显提升。对于帧率降低问题也可通过提升硬件配置进行改善。