乐于分享
好东西不私藏

基于深度学习实现跌倒检测(附源码)

基于深度学习实现跌倒检测(附源码)

点击下方卡片,关注“机器视觉与AI深度学习

视觉/图像重干货,第一时间送达!

    传统的跌倒检测方法(例如分析突然的姿势变化、计算角度或测量加速度)在发生意外动作时通常会失效,从而导致许多误报。在本文中,我们探索了一种不同的方法:我们将YOLO Pose与GRU 网络相结合以提取身体关键点,从而分析随时间变化的动作,从而实现更智能、更可靠的跌倒检测系统。

为什么是 GRU

    门控循环单元 ( GRU ) 网络是一种循环神经网络,旨在高效处理顺序数据。与 LSTM 不同,GRU 的架构更简单,消除了存储单元,只使用两个门(更新和重置)。这使它们更快、更轻量,非常适合我们的情况,因为我们需要实时处理动作而不影响性能。此外,对于跌倒检测等短序列,GRU 的表现通常与 LSTM 一样好,提供了更高效的解决方案而不牺牲准确性。

    为了使用 GRU 识别动作,我们将每个动作表示为一系列单独的姿势 – 我们的基本单位。每个姿势都由与身体关节相对应的关键点列表组成。由于人类运动通常遵循平滑的轨迹,因此 GRU 学会捕捉时间模式而无需存储复杂状态,使其成为这项任务的更有效选择。

构建数据集

    为了构建数据集,我使用了YouTuber Kevin Parry的视频《50 Ways to Fall》。使用FFmpeg,我将视频分成三个部分:跌倒、正常时刻和完整视频。对于训练,我只使用了跌倒和正常运动部分,以确保数据集更加平衡。您可以在以下链接下载预处理和分类后的数据:

https://www.youtube.com/channel/UCzgkpehSWuFTQx9E8NkBqzw

    使用YOLOv11 Pose模型进行姿势提取,该模型检测人体的17个关键点,提供有关关节位置和运动的详细信息。

    我没有使用整个序列作为单个数据点,而是选择使用较小的子序列。人类动作识别通常只需要几个连续的帧(1 到 10 帧之间)即可有效。在这种情况下,我使用了20 帧的序列长度。

    如果视频少于20 帧,我们会通过复制最后检测到的姿势来填充序列。如果视频较长,我们只取最后 20 帧,以便更好地捕捉跌倒的最后阶段,这对于检测最为重要。

import osimport cv2import numpy as npfrom ultralytics import YOLOdef extract_keypoints_from_video(video_path: str, model, sequence_length: int = 10, save: bool = False, output_path: str = 'keypoints.npy'):    num_keypoints = 17 * 2  # Número de keypoints (17 puntos * 2 coordenadas x, y)    if not os.path.exists(video_path):        raise FileNotFoundError(f'El archivo de video {video_path} no existe')    cap = cv2.VideoCapture(video_path)    keypoints_buffer = []    while True:        ret, frame = cap.read()        if not ret:            break  # Video terminado        results = model(frame)[0]        if len(results.keypoints.xy) > 0:            keypoints = results.keypoints.xy[0].numpy().flatten()            if keypoints.shape[0] != num_keypoints:                keypoints = np.pad(keypoints, (0, num_keypoints - keypoints.shape[0]))        else:            # Si no se detectan keypoints, usar ceros            keypoints = np.zeros(num_keypoints, dtype=np.float32)        keypoints_buffer.append(keypoints)    cap.release()    # Manejar casos donde el video es más corto o más largo que sequence_length    if len(keypoints_buffer) < sequence_length:        # Si el video es más corto, repetir el último frame hasta completar sequence_length        last_frame = keypoints_buffer[-1]        while len(keypoints_buffer) < sequence_length:            keypoints_buffer.append(last_frame)    elif len(keypoints_buffer) > sequence_length:        # Si el video es más largo, tomar solo los últimos sequence_length frames        keypoints_buffer = keypoints_buffer[-sequence_length:]    keypoints_buffer = np.array(keypoints_buffer, dtype=np.float32)    if save:        np.save(output_path, keypoints_buffer)        print(f'Guardado en {output_path}')    return keypoints_buffer

    提取姿势数据时的一个常见问题是无法检测某些身体部位。最初,这些缺失关键点的 (x, y) 坐标设置为0,然后在后期处理期间替换为NaN。然而,GRU 无法处理 NaN 值,而留下零会通过引入误导性信息对模型性能产生负面影响。

    为了解决这个问题,我选择使用具有5 个最近邻居的K-Nearest Neighbors (KNN) 来填补缺失值。这种方法有助于适应非线性运动轨迹,提供更一致的输入数据并提高模型的准确性。

构建 GRU 模型

    利用预处理后的数据,我们现在可以构建基于 GRU 的模型,将动作分为两类:“跌倒”或“不跌倒”。我们选择基于 GRU 的架构而不是 LSTM,因为 GRU 的计算效率更高,并且可以更好地处理短序列而不会丢失重要信息。

    为了实现,我们使用了 PyTorch 并定义了一个顺序模型。第一层是 GRU,它处理从YOLO Pose中提取的关键点序列。然后,我们应用批量归一化来稳定训练,然后是具有 ReLU 激活的密集层,以捕获更复杂的模式。此外,我们在输出层中加入了 dropout 以减少过度拟合,并加入了 sigmoid 激活,因为这是一个二元分类问题。

import torchimport torch.nn as nnclass FallDetectionGRU(nn.Module):    def __init__(self, input_size, hidden_size, num_layers, output_size, dropout_prob=0.5):        super(FallDetectionGRU, self).__init__()        self.hidden_size = hidden_size        self.num_layers = num_layers        # Capa GRU (en vez de LSTM)        self.gru = nn.GRU(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout_prob if num_layers > 1 else 0)        # Batch Normalization        self.bn = nn.BatchNorm1d(hidden_size)        # Capas fully connected adicionales        self.fc1 = nn.Linear(hidden_size, hidden_size // 2)        self.fc2 = nn.Linear(hidden_size // 2, hidden_size // 4)        self.fc3 = nn.Linear(hidden_size // 4, output_size)        # Dropout para regularización        self.dropout = nn.Dropout(dropout_prob)        # Funciones de activación        self.relu = nn.ReLU()        self.sigmoid = nn.Sigmoid()        # Inicialización de pesos        self._init_weights()    def _init_weights(self):        """Inicialización de pesos para mejorar la convergencia."""        for name, param in self.gru.named_parameters():            if 'weight_ih' in name or 'weight_hh' in name:                nn.init.xavier_uniform_(param.data)            elif 'bias' in name:                param.data.fill_(0)        for m in self.modules():            if isinstance(m, nn.Linear):                nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')                m.bias.data.fill_(0.01)    def forward(self, x):        # Inicializar estado oculto (sin c0)        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)        # Pasar a través de la GRU        out, _ = self.gru(x, h0)        # Tomar la salida del último paso de tiempo        out = out[:, -1, :]        # Batch Normalization        out = self.bn(out)        # Capas fully connected con activaciones y dropout        out = self.fc1(out)        out = self.relu(out)        out = self.dropout(out)        out = self.fc2(out)        out = self.relu(out)        out = self.dropout(out)        out = self.fc3(out)        # Aplicar sigmoide para obtener probabilidades        out = self.sigmoid(out)        return out

模型训练

    对于训练,我使用BCELoss(二元交叉熵损失)作为损失函数,它非常适合这类二元分类问题。由于模型的输出是概率(经过S 型激活后),BCELoss 有助于最大限度地减少跌倒分类中的不确定性。

    作为优化器,我选择了AdamW ,这是Adam的一个带有权重衰减的变体。与标准 Adam 不同,AdamW 将 L2 正则化与梯度更新分离,使其在深度网络中更能抵抗过度拟合。此外,它比 SGD 更高效,因为它可以动态调整每个参数的学习率,从而加速收敛。

    为了防止过拟合,我实施了提前停止,耐心等待 10 个时期。如果验证损失在连续迭代中停止改善,则训练会自动停止。这有助于防止过度拟合并消除不必要的训练时间。此外,根据最低验证损失保存最佳模型,确保最终版本最适用于新数据。

import torchimport torch.nn as nnimport torch.optim as optimdef train_model(model, train_loader, val_loader, num_epochs=200, learning_rate=0.0001                weight_decay=1e-4, patience=10, device="cuda" if torch.cuda.is_available() else "cpu"):    """    Entrena un modelo de detección de caídas usando GRU.    Args:        model (torch.nn.Module): Modelo a entrenar.        train_loader (DataLoader): DataLoader para los datos de entrenamiento.        val_loader (DataLoader): DataLoader para los datos de validación.        num_epochs (int): Número de épocas de entrenamiento.        learning_rate (float): Tasa de aprendizaje.        weight_decay (float): Parámetro de regularización L2.        patience (int): Número de épocas sin mejora antes de early stopping.        device (str): Dispositivo a usar ("cuda" o "cpu").    Returns:        model: Modelo entrenado.        history: Diccionario con métricas de entrenamiento y validación.    """    model.to(device)    criterion = nn.BCELoss()  # Binary Cross Entropy Loss    optimizer = optim.AdamW(model.parameters(), lr=learning_rate, weight_decay=weight_decay)    # Listas para almacenar métricas    history = {"train_loss": [], "val_loss": [], "train_acc": [], "val_acc": []}    best_val_loss = float("inf")    patience_counter = 0    best_model = None    for epoch in range(num_epochs):        model.train()        train_loss, correct, total = 000        for batch_X, batch_y in train_loader:            batch_X, batch_y = batch_X.to(device), batch_y.to(device)            optimizer.zero_grad()            outputs = model(batch_X).squeeze()            loss = criterion(outputs, batch_y)            loss.backward()            optimizer.step()            train_loss += loss.item()            predicted = (outputs >= 0.5).float()            correct += (predicted == batch_y).sum().item()            total += batch_y.size(0)        avg_train_loss = train_loss / len(train_loader)        train_accuracy = correct / total        history["train_loss"].append(avg_train_loss)        history["train_acc"].append(train_accuracy)        # Evaluación en validación        model.eval()        val_loss, correct, total = 000        with torch.no_grad():            for batch_X, batch_y in val_loader:                batch_X, batch_y = batch_X.to(device), batch_y.to(device)                outputs = model(batch_X).squeeze()                loss = criterion(outputs, batch_y)                val_loss += loss.item()                predicted = (outputs >= 0.5).float()                correct += (predicted == batch_y).sum().item()                total += batch_y.size(0)        avg_val_loss = val_loss / len(val_loader)        val_accuracy = correct / total        history["val_loss"].append(avg_val_loss)        history["val_acc"].append(val_accuracy)        # Guardar el mejor modelo        if avg_val_loss < best_val_loss:            best_val_loss = avg_val_loss            best_model = model.state_dict()            patience_counter = 0        else:            patience_counter += 1        # Early Stopping        if patience_counter >= patience:            print(f"⏹️ Early stopping en la época {epoch + 1}")            break        if (epoch + 1) % 5 == 0:            print(f'Epoch {epoch + 1}: '                  f'Train Loss: {avg_train_loss:.4f}, Train Acc: {train_accuracy:.4f} | '                  f'Val Loss: {avg_val_loss:.4f}, Val Acc: {val_accuracy:.4f}')    # Restaurar el mejor modelo    if best_model:        model.load_state_dict(best_model)        print("✔️ Modelo restaurado al mejor estado guardado.")    return model, history

    为了优化基于 GRU 的模型的性能,我对几个关键的超参数进行了微调,包括 epoch 的数量、批量大小和学习率。

    由于实施了提前停止,因此epoch 的数量并不是一个关键因素,因此将其设置为300 epoch。关于批大小,我测试了8、16、32 和 64,发现8在模型收敛中提供了更大的稳定性。0.0005的学习率确保了稳定的训练。

    为了增强泛化能力并防止过度拟合,我采用了1e-6的权重衰减,在不影响性能的情况下调节模型的权重。最后,使用了0.6的dropout率来减少对特定模式的依赖,从而提高了模型对新数据进行泛化的能力。

from src.models import FallDetectionGRUfrom src.training import train_modelbatch_size = 8train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)model = FallDetectionGRU(    input_size=34    hidden_size=64    num_layers=2    output_size=1    dropout_prob=0.6)trained_model, history = train_model(    model,     train_loader,     val_loader,     num_epochs=300,    learning_rate=.0005,    weight_decay=1e-6    patience=15)

    该模型实际运行的一些示例:

测试结果

    测试集的结果是积极的,对可用数据的分类准确。但需要注意的是,使用的数据集很小,没有涵盖各种场景或姿势,这可能会影响模型推广到新情况的能力。

混淆矩阵 GRU 测试

    为了排除过拟合,我使用外部数据集中的视频测试了该模型,例如GMDCSA24:视频中人体跌倒检测的数据集。跌倒检测非常准确,可以正确识别跌倒视频和相关姿势。但是,在某些情况下,它会产生误报,在没有发生跌倒的场景中检测到跌倒,例如弯腰捡起物体、在地板上做俯卧撑或躺在床上。

应用场景

    跌倒检测在各种场景中都有多种应用,可以提供关键帮助。一些示例包括:

    • 老年人护理:能够自动监控行动不便的个人,一旦发生跌倒,即可向家人或医疗服务部门发出警报。

    • 医院和康复中心:帮助监督有跌倒风险的患者,减少房间或走廊内的事故。

    • 在危险环境中的工人:例如建筑或采矿,及时检测到跌倒可以防止致命事故。

    • 孩子们在游乐场上摔倒和受伤的情况很常见。

    在危急情况下,快速跌倒检测可以及时干预,避免造成严重甚至致命的后果。

源码下载:

https://github.com/Erik172/fall-detection-deep-learning?source=post_page-----2941db4c95a3---------------------------------------

—THE END—
觉得有用,麻烦给个赞和在看