
簡介人體動作識別是計算機視覺領域的一項關鍵技術其核心原理是通過分析視頻序列中的人體姿態變化來理解和分類動作。該技術融合了姿態估計與時間序列分析在人工智能和人機交互中具有重要價值廣泛應用于體感游戲、智能健身、安防監控和虛擬現實等場景。本文聚焦于兩種核心時序建模方法動態時間規整DTW和長短期記憶網絡LSTM。DTW作為一種經典的序列對齊算法無需訓練即可進行高效的模板匹配適合快速原型開發而LSTM作為深度學習模型能夠學習復雜的時空依賴關系具備更強的泛化能力。通過結合MediaPipe提供的實時、輕量級人體關鍵點本指南提供了從數據預處理、特征工程到模型集成的完整工程實踐路徑為開發輕量級到復雜度的動作識別應用提供了切實可行的解決方案。1. 項目概述從姿態到動作的智能識別最近在整理一個舊項目發現當時為了一個手勢交互應用折騰了好一陣子的人體動作識別。核心需求很簡單讓電腦能“看懂”人做的一系列連貫動作比如一套健身操、一段手語或者一個舞蹈動作。這不僅僅是識別某個瞬間的姿勢而是要理解一個完整的、有時間順序的動作序列。當時市面上很多方案要么精度不夠要么對硬件要求太高要么就是延遲感人。最后我選擇了一條現在看來依然很實用的技術路線用MediaPipe來實時、高效地提取人體關鍵點然后用動態時間規整DTW和長短期記憶網絡LSTM這兩種算法來識別動作。前者適合做快速、輕量的模板匹配后者則能學習更復雜的時序模式。這個組合拳在資源有限又想達到不錯效果的場景下特別香。今天就把這個項目的核心思路、代碼實現和踩過的坑系統地梳理一遍無論你是想做個體感游戲、健身指導應用還是人機交互原型相信都能找到可以直接“抄作業”的部分。簡單來說這個項目就是一個完整的Python解決方案輸入是一段視頻或者實時攝像頭流輸出是識別出的動作類別。它的價值在于提供了一個從基礎姿態提取到高級動作理解的端到端實現并且對比了兩種經典的時序建模方法。你不需要是深度學習專家只要對Python有基本了解就能跟著一步步搭建起來。2. 核心思路與技術選型解析2.1 為什么是MediaPipe DTW/LSTM做動作識別第一步永遠是“看見”人體。這里有幾個備選OpenPose、AlphaPose、MMPose等。我最終選擇MediaPipe主要是基于以下幾點實戰考量輕量與實時性MediaPipe的輕量級模型在普通CPU上也能跑到實時30FPS這對于需要快速響應的交互應用至關重要。OpenPose雖然精度高但速度慢對GPU有依賴。開箱即用的易用性MediaPipe提供了極其簡潔的Python API幾行代碼就能獲取到33個全身關鍵點的3D坐標盡管Z軸是相對深度。這讓我們能把精力集中在動作識別算法本身而不是在姿態估計這個前期環節耗費大量時間調試。足夠的精度對于大部分日常動作識別如揮手、深蹲、跳躍MediaPipe的精度已經足夠。它的關鍵點穩定性很好抖動相對較小為后續時序分析打下了好基礎。拿到關鍵點序列后就要處理核心問題如何判斷兩段長度不同、速度不一的關鍵點序列代表同一個動作這里我引入了兩種互補的方案DTW動態時間規整這是一種經典的序列對齊算法。想象兩個人在做同一個“高舉雙手”的動作一個人做得快一個人做得慢直接逐幀比較會對不上。DTW能自動“拉伸”或“壓縮”時間軸找到兩個序列間的最佳匹配路徑并計算一個距離值。距離越小動作越相似。它的優點是無需訓練數據你只需要預先錄制一個“標準動作”作為模板運行時計算輸入序列與模板序列的DTW距離即可判斷。非常適合動作庫固定、且需要快速上線的場景。LSTM長短期記憶網絡這是循環神經網絡RNN的一種變體專門為處理時序數據而生。與DTW的模板匹配不同LSTM是數據驅動的。你需要準備一個標注好的動作數據集比如“深蹲”的視頻片段標為0“開合跳”標為1然后用這個數據集來訓練LSTM網絡。訓練好的網絡能夠理解動作序列中復雜的時空依賴關系從而對新的序列進行分類。它的優點是能學習更抽象、更復雜的模式泛化能力更強但需要收集和標注數據。為什么兩者都做在實際項目中這給了我們靈活性。項目初期動作種類少可以用DTW快速實現原型驗證。隨著數據積累和需求復雜化可以平滑過渡到使用LSTM模型獲得更好的識別效果和魯棒性。這個項目源碼就包含了這兩套方案的完整實現。2.2 項目整體架構設計整個項目的代碼流程可以清晰地分為四個階段我把它畫成了一個簡單的處理流水線[視頻/攝像頭輸入] | v [MediaPipe姿態估計] |-- 逐幀提取33個關鍵點(x, y, z, visibility) | v [數據預處理與序列構建] |-- 1. 歸一化消除人物位置、體型影響 |-- 2. 平滑濾波降低關鍵點抖動 |-- 3. 構建等長時序序列供LSTM使用 | v / \ / \ / \ / \ [DTW實時匹配] [LSTM模型推理] (與預存模板比較) (加載預訓練模型) \ / \ / \ / \ / v [動作類別輸出]這個架構的關鍵在于數據預處理模塊。MediaPipe輸出的原始關鍵點坐標是依賴于人在圖像中的位置的。一個人站在畫面左邊和右邊做同一個動作坐標值會完全不同。因此必須通過歸一化例如以髖部中心為原點或以軀干長度為尺度單位來得到只反映肢體相對角度的特征這對提升識別率至關重要。3. 核心模塊實現與代碼精講3.1 MediaPipe姿態提取模塊這是所有工作的基礎。我們首先需要穩定地獲取每一幀的人體關鍵點。import cv2 import mediapipe as mp import numpy as np class PoseEstimator: def __init__(self, static_image_modeFalse, model_complexity1, smooth_landmarksTrue): 初始化MediaPipe姿態估計器。 參數 static_image_mode: 設為False用于視頻流True用于靜態圖片。 model_complexity: 模型復雜度0,1,2越高越準但也越慢。1是平衡之選。 smooth_landmarks: 是否平滑關鍵點減少抖動。 self.mp_pose mp.solutions.pose self.pose self.mp_pose.Pose( static_image_modestatic_image_mode, model_complexitymodel_complexity, smooth_landmarkssmooth_landmarks, min_detection_confidence0.5, min_tracking_confidence0.5 ) self.mp_drawing mp.solutions.drawing_utils def process_frame(self, image): 處理一幀圖像返回關鍵點列表和繪制了姿態的圖像。 # MediaPipe需要RGB格式的圖像 image_rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image_rgb.flags.writeable False # 提升性能 results self.pose.process(image_rgb) # 轉換回BGR用于OpenCV顯示 image.flags.writeable True image cv2.cvtColor(image_rgb, cv2.COLOR_RGB2BGR) landmarks [] if results.pose_landmarks: # 繪制姿態連接線 self.mp_drawing.draw_landmarks( image, results.pose_landmarks, self.mp_pose.POSE_CONNECTIONS ) # 提取33個關鍵點的坐標和可見度 for lm in results.pose_landmarks.landmark: # lm.x, lm.y 是歸一化到[0,1]的圖像坐標 # lm.z 是相對深度值越小表示離攝像頭越近 # lm.visibility 是該點可見度的置信度 landmarks.append([lm.x, lm.y, lm.z, lm.visibility]) return np.array(landmarks), image注意lm.x和lm.y是歸一化坐標相對于圖像寬高在做后續計算時通常需要根據圖像實際尺寸還原或者直接使用歸一化坐標以保持尺度不變性。lm.z是相對深度單位不是物理尺度僅用于比較同一畫面中不同關鍵點的前后關系。3.2 關鍵數據預處理與歸一化原始關鍵點數據不能直接扔給DTW或LSTM。這里有幾個必須做的處理步驟1. 歸一化Normalization目標是消除人物在畫面中位置和個體體型差異的影響。最常用的是以軀干為中心進行歸一化。def normalize_landmarks(landmarks): 對單幀關鍵點進行歸一化。 策略以左右髖關節的中點骨盆中心為新的原點。 if landmarks.size 0: return landmarks # MediaPipe Pose的索引23-左髖24-右髖 left_hip landmarks[23, :2] # 只取x, y right_hip landmarks[24, :2] hip_center (left_hip right_hip) / 2.0 # 計算軀干長度作為尺度參考例如髖中心到肩中心 # 11-左肩12-右肩 left_shoulder landmarks[11, :2] right_shoulder landmarks[12, :2] shoulder_center (left_shoulder right_shoulder) / 2.0 torso_length np.linalg.norm(hip_center - shoulder_center) # 防止除零 if torso_length 1e-7: torso_length 1.0 normalized [] for lm in landmarks: # 平移減去髖中心 translated_xy lm[:2] - hip_center # 縮放除以軀干長度 scaled_xy translated_xy / torso_length # 保留z和visibility信息可以簡單保留或做類似處理 normalized.append([scaled_xy[0], scaled_xy[1], lm[2], lm[3]]) return np.array(normalized)2. 平滑濾波SmoothingMediaPipe輸出雖然已經內置了平滑但在快速運動時關鍵點仍會有抖動。我們可以加一個簡單的時間域濾波器比如移動平均或一階低通濾波器。class LandmarkSmoother: 使用簡單移動平均平滑關鍵點序列。 def __init__(self, window_size5): self.window_size window_size self.history [] def smooth(self, landmarks): self.history.append(landmarks.copy()) if len(self.history) self.window_size: self.history.pop(0) # 對歷史窗口內的關鍵點求平均 smoothed np.mean(self.history, axis0) return smoothed3. 序列構建Sequence Building對于LSTM我們需要構建固定長度的輸入序列。通常采用滑動窗口法。def build_sequences(frames_landmarks, sequence_length30, step5): 將連續的關鍵點幀構建成供LSTM訓練的序列樣本。 參數 frames_landmarks: 所有幀的關鍵點列表每幀是(33, 4)的數組。 sequence_length: 每個序列的幀數時間步長。 step: 滑動窗口的步長。 sequences [] for i in range(0, len(frames_landmarks) - sequence_length 1, step): sequence frames_landmarks[i:i sequence_length] # 此時sequence形狀為 (sequence_length, 33, 4) # 為了輸入LSTM我們通常將其展平或選擇關鍵特征。 # 例如只使用x,y坐標并展平(sequence_length, 33*2) seq_features sequence[:, :, :2].reshape(sequence_length, -1) sequences.append(seq_features) return np.array(sequences)3.3 DTW動作識別器實現DTW的核心思想是計算兩個時間序列的最小對齊成本。我們使用fastdtw庫它比純Python實現快得多。from scipy.spatial.distance import euclidean from fastdtw import fastdtw class DTWRecognizer: def __init__(self): self.templates {} # 鍵動作名 值歸一化后的關鍵點序列列表 def add_template(self, action_name, landmark_sequence): 添加一個動作模板。landmark_sequence是形狀為 (N, 33, 4) 的數組。 if action_name not in self.templates: self.templates[action_name] [] # 對模板序列也進行歸一化處理 normalized_seq [normalize_landmarks(frame) for frame in landmark_sequence] # 通常我們存儲序列的緊湊特征例如只保留身體角度或相對坐標 feature_seq self._extract_features(normalized_seq) self.templates[action_name].append(feature_seq) def _extract_features(self, landmark_sequence): 從關鍵點序列中提取用于DTW比較的特征。這里簡化為例使用軀干和四肢的相對向量。 features [] for frame in landmark_sequence: # 示例計算左肘角度肩-肘-腕 # 獲取左肩(11), 左肘(13), 左腕(15)的x,y坐標 shoulder frame[11, :2] elbow frame[13, :2] wrist frame[15, :2] vec1 elbow - shoulder vec2 wrist - elbow # 計算余弦相似度作為角度特征簡化 cos_angle np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2) 1e-7) # 可以添加更多特征如右肘、左膝、右膝的角度或四肢相對于軀干的向量 feature_vector np.array([cos_angle]) # 這里僅為示例實際應更豐富 features.append(feature_vector.flatten()) return np.array(features) def recognize(self, test_sequence): 識別輸入序列。返回最匹配的動作名和距離。 if not self.templates: return No template, float(inf) test_features self._extract_features(test_sequence) best_action None min_distance float(inf) for action_name, template_list in self.templates.items(): for template in template_list: # 使用fastdtw計算距離 distance, _ fastdtw(test_features, template, disteuclidean) if distance min_distance: min_distance distance best_action action_name # 設置一個距離閾值超過則認為“未知動作” threshold 50 # 這個閾值需要根據特征尺度通過實驗確定 if min_distance threshold: return Unknown, min_distance return best_action, min_distance實操心得DTW的性能和效果極度依賴于特征提取。直接使用所有關鍵點的原始坐標效果往往很差因為包含了太多噪聲和不相關信息。好的特征應該對同一動作的不同執行速度、幅度具有不變性。上面示例中的關節角度是一個很好的起點。在實際項目中我通常會計算10-15個關鍵關節的角度組成一個特征向量這樣DTW的識別魯棒性會大大提升。3.4 LSTM模型構建與訓練LSTM部分我們使用PyTorch來實現。相比DTW這是一個“學習”的過程。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader # 1. 定義數據集類 class ActionDataset(Dataset): def __init__(self, sequences, labels): sequences: numpy數組形狀 (樣本數, 序列長度, 特征維度) labels: 對應的動作標簽整數 self.sequences torch.FloatTensor(sequences) self.labels torch.LongTensor(labels) def __len__(self): return len(self.labels) def __getitem__(self, idx): return self.sequences[idx], self.labels[idx] # 2. 定義LSTM模型 class ActionLSTM(nn.Module): def __init__(self, input_size, hidden_size, num_layers, num_classes): super(ActionLSTM, self).__init__() self.hidden_size hidden_size self.num_layers num_layers # batch_firstTrue 表示輸入數據的第一個維度是batch_size self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue, dropout0.3) self.fc nn.Linear(hidden_size, num_classes) self.dropout nn.Dropout(0.5) def forward(self, x): # 初始化隱藏狀態和細胞狀態 h0 torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) c0 torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) # LSTM前向傳播 out, _ self.lstm(x, (h0, c0)) # out shape: (batch_size, seq_length, hidden_size) # 我們通常取最后一個時間步的輸出作為序列的表示 out out[:, -1, :] out self.dropout(out) out self.fc(out) return out # 3. 訓練函數簡化版 def train_model(model, train_loader, val_loader, num_epochs50, lr0.001): device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lrlr) for epoch in range(num_epochs): model.train() running_loss 0.0 for sequences, labels in train_loader: sequences, labels sequences.to(device), labels.to(device) optimizer.zero_grad() outputs model(sequences) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 每個epoch后在驗證集上評估 model.eval() val_correct 0 val_total 0 with torch.no_grad(): for sequences, labels in val_loader: sequences, labels sequences.to(device), labels.to(device) outputs model(sequences) _, predicted torch.max(outputs.data, 1) val_total labels.size(0) val_correct (predicted labels).sum().item() val_acc 100 * val_correct / val_total print(fEpoch [{epoch1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}, Val Acc: {val_acc:.2f}%) print(Training Finished.) return model關于輸入特征在構建LSTM的訓練數據時我強烈建議不要簡單地將所有關鍵點的x,y坐標展平。更好的做法是進行特征工程例如相對坐標將所有關鍵點坐標減去骨盆中心坐標。關節角度計算主要關節肘、膝、肩、髖的角度。骨骼長度比計算幾段主要骨骼的長度比例。速度與加速度計算關鍵點在連續幀間的運動速度和加速度需要較高幀率。將這些特征組合起來形成一個更有信息量的特征向量能顯著提升LSTM模型的性能和收斂速度。4. 系統集成與實時識別流程將以上模塊串聯起來形成一個完整的實時動作識別系統。主程序邏輯如下import time from collections import deque def main(): # 初始化 pose_estimator PoseEstimator() smoother LandmarkSmoother(window_size3) # 選擇識別器 use_dtw True # 切換為False則使用LSTM if use_dtw: recognizer DTWRecognizer() # 加載預存的DTW模板 recognizer.load_templates(dtw_templates.pkl) else: # 加載訓練好的LSTM模型 lstm_model ActionLSTM(input_size66, hidden_size128, num_layers2, num_classes5) lstm_model.load_state_dict(torch.load(action_lstm.pth)) lstm_model.eval() cap cv2.VideoCapture(0) # 打開攝像頭 sequence_buffer deque(maxlen30) # 存儲最近30幀的關鍵點序列 while cap.isOpened(): ret, frame cap.read() if not ret: break # 1. 姿態估計 landmarks, annotated_frame pose_estimator.process_frame(frame) if landmarks.size 0: # 2. 平滑處理 smoothed_landmarks smoother.smooth(landmarks) # 3. 歸一化 normalized_landmarks normalize_landmarks(smoothed_landmarks) # 4. 存入緩沖區 sequence_buffer.append(normalized_landmarks) # 當緩沖區滿時進行動作識別 if len(sequence_buffer) sequence_buffer.maxlen: current_sequence np.array(sequence_buffer) # (30, 33, 4) if use_dtw: # 5. DTW識別 action, distance recognizer.recognize(current_sequence) cv2.putText(annotated_frame, fDTW: {action} ({distance:.1f}), (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) else: # 5. LSTM識別 # 提取特征并構建模型輸入 feature_sequence extract_lstm_features(current_sequence) # (30, 特征維) feature_tensor torch.FloatTensor(feature_sequence).unsqueeze(0) # 增加batch維度 with torch.no_grad(): output lstm_model(feature_tensor) _, predicted torch.max(output, 1) action_idx predicted.item() action_name [Squat, Jump, Wave, Punch, Kick][action_idx] # 示例動作名 cv2.putText(annotated_frame, fLSTM: {action_name}, (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow(Action Recognition, annotated_frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()這個主循環清晰地展示了從視頻流到動作標簽的完整過程。你可以通過use_dtw標志位輕松切換兩種識別算法對比它們在實際場景中的表現。5. 實戰避坑指南與調優經驗在實際開發和調試這個系統的過程中我遇到了不少坑也總結出一些提升效果的關鍵點。5.1 MediaPipe使用中的常見問題關鍵點丟失或抖動原因光照不足、遮擋、人物部分出框、攝像頭分辨率太低或運動模糊。解決確保環境光線充足均勻。嘗試調整min_detection_confidence和min_tracking_confidence參數。降低它們可以提高檢測率但可能引入更多噪聲提高則更嚴格但可能丟失目標。通常從0.5開始調整。使用上文提到的LandmarkSmoother進行后處理平滑。如果場景固定可以設置static_image_modeFalseMediaPipe會在檢測到人體后啟用跟蹤模式比逐幀檢測更穩定。多人場景MediaPipe的Pose解決方案默認只檢測畫面中最顯著的一個人。如果需要多人需要使用mp.solutions.pose.Pose的static_image_modeTrue模式進行逐幀多人檢測但這會顯著降低速度。對于實時多人需要考慮其他方案或對畫面進行裁剪分割。5.2 DTW實戰技巧特征選擇是成敗關鍵DTW計算的是序列間的距離垃圾特征進去垃圾結果出來。務必花時間設計好的特征。關節角度、相對軀干的標準化坐標都是經過驗證的有效特征。避免直接使用絕對坐標。模板的數量和質量一個動作只錄一個模板是不夠的。應該從不同身高、體型、速度的多次執行中錄制多個模板。識別時計算輸入序列與所有模板的距離取最小值。這能大大提高泛化能力。距離閾值需要精心設定threshold值不是拍腦袋定的。最好收集一批“正樣本”目標動作和“負樣本”其他動作或無關動作分別計算它們到模板的距離觀察分布然后選擇一個能較好區分的閾值如正樣本距離的均值加兩倍標準差。計算效率DTW的計算復雜度是O(n*m)。當序列很長或模板很多時實時性會受影響。可以降低序列采樣頻率例如從30FPS降到15FPS。使用更快的DTW實現如fastdtw。在動作開始時才觸發DTW計算而不是每幀都算。5.3 LSTM訓練與優化數據數據還是數據LSTM的性能嚴重依賴于訓練數據的質量和數量。對于每個動作至少需要數百個樣本序列。數據要盡可能覆蓋不同的執行速度、幅度、視角如果可能和人物。數據增強這是提升模型魯棒性的廉價方法。可以對關鍵點序列進行以下增強時間扭曲輕微加快或放慢序列通過插值。空間擾動對關鍵點坐標添加微小的高斯噪聲。隨機丟棄隨機丟棄序列中的少量幀模擬遮擋或檢測失敗。序列長度與步長sequence_length時間步長需要覆蓋一個完整動作的周期。太短則信息不足太長則包含冗余且增加計算量。通常1-2秒的幀數30-60幀是個不錯的起點。step滑動窗口步長決定了樣本的重疊程度較小的步長可以產生更多訓練樣本但可能導致過擬合。模型結構從一個簡單的單層LSTM開始。hidden_size可以從64或128開始嘗試。過大的模型在小數據集上容易過擬合。熟練使用Dropout層如我在代碼中添加的是防止過擬合的有效手段。類別不平衡如果“揮手”動作的樣本數是“深蹲”的10倍模型會偏向于預測“揮手”。需要在數據收集階段注意平衡或在損失函數中使用類別權重CrossEntropyLoss的weight參數。5.4 系統集成與部署提示異步處理實時視頻流中姿態估計和動作識別是比較耗時的。為了避免界面卡頓可以將識別任務放在單獨的線程或進程中進行。主線程負責采集和顯示子線程負責處理隊列中的幀并進行識別然后將結果傳回主線程顯示。狀態機管理對于連續的動作流簡單的逐段識別可能會產生閃爍的結果如“深蹲-未知-深蹲”。可以引入一個簡單的狀態機或滑動窗口投票機制。例如連續5次識別結果為“深蹲”才最終判定為深蹲動作并進入“已識別”狀態忽略接下來短時間內的其他結果直到檢測到動作結束。資源占用監控在樹莓派或移動端等資源受限的設備上部署時務必監控CPU和內存使用情況。MediaPipe的輕量級模型是優勢但LSTM推理也可能成為瓶頸。可以考慮將PyTorch模型轉換為ONNX格式并使用ONNX Runtime進行推理可能獲得性能提升。對于極度輕量的需求甚至可以嘗試用TinyLSTM等簡化結構。這個項目從構思到實現是一個典型的從理論到實踐的過程。MediaPipe提供了強大的感知能力而DTW和LSTM則代表了兩種不同哲學的動作理解路徑一種是基于規則和相似度的快速匹配另一種是基于數據驅動的深度建模。在實際應用中我常常會根據項目的階段和需求混合使用它們。比如用DTW做快速原型和觸發條件用LSTM來做更精細的分類。希望這份詳細的拆解能幫你避開我當年踩過的那些坑更順暢地搭建起屬于你自己的人體動作識別應用。本文還有配套的精品資源點擊獲取