拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于PyTorch和MediaPipe的交警手势识别:用LSTM实现8种动作分类

简介基于PyTorch实现的中国交通警察指挥8种手势识别项目整合源码、数据集、模型与详细说明面向计算机、人工智能、自动化等相关专业的学生、教师及从业者尤其适合作为毕业设计、课程设计或期末大作业的参考。资源共34个文件以31个Python脚本为核心覆盖数据预处理、模型训练、关键点估计、手势识别与结果评估等环节另附readme说明文档和gif演示动画压缩包仅4.42MB结构清晰、模块划分明确。已有227人学习/下载。项目源自个人毕设答辩评审得分98分代码均经过调试测试可确保运行内容预览显示其包含从数据处理到骨架特征构建、网络预测的完整流程并提供了多种训练与预测脚本便于理解PyTorch项目组织方式。无论是深度学习初学者还是需要快速搭建手势识别系统的开发者都能通过该项目获得从数据到模型落地的完整实操经验且具备良好的二次开发基础。1. 为什么用 PyTorch 啃下中国交通警察 8 种手势识别在路口视频里识别交通警察的指挥手势难点不是“有没有动作”而是“动作之间太像”。左转弯待转和左转弯手和臂的起点几乎一样只是一个多了停顿等待的环节直行和变道的手型类似只是手臂挥动方向不同。用传统图像分类思路很难稳定区分因为网络很可能记住了袖子颜色或背景里的车辆而不是手势本身。我把整个方案拆成两段先用 MediaPipe 提取上半身关键点把交警的手势变成一组随时间变化的坐标序列再用 PyTorch 写一个轻量 LSTM 分类器来完成 8 种手势的识别。这条路线的优势是特征维度低、训练快而且对采集设备不挑普通 30 帧摄像头就够。项目源码、数据集和训练好的模型能够覆盖“数据处理→训练→实时推理”整条链路下面按顺序说清楚每一步怎么落地。2. 制作手势识别数据集用 MediaPipe 提取关键点序列并构建 PyTorch Dataset2.1 从原始视频到关键点序列只保留上半身姿态信息我一开始试过直接把视频帧缩放到 224x224 扔进 ResNet训练几轮就发现一个严重问题模型把交警帽子和反光马甲当作核心特征换个人穿不同衣服准确率掉到及格线以下。这个现象在公开动作识别数据集上也存在原因在于手部动作被静态外观特征干扰。后来我把输入从 RGB 帧换成姿态关键点序列问题就迎刃而解。中国交通警察手势一共 8 种分别是停止、直行、左转弯、右转弯、左转弯待转、右转弯待转、变道、减速慢行。我给每个手势分配一个固定标签方便后续做交叉熵损失计算。标签手势0停止信号1直行信号2左转弯信号3右转弯信号4左转弯待转信号5右转弯待转信号6变道信号7减速慢行信号提取关键点时使用 MediaPipe Pose每帧会输出 33 个人体关键点。我只需要和手臂摆动、躯干朝向关系最大的 8 个点左右肩、左右肘、左右腕、左右髋。每个关键点包含 x、y、z 三个坐标z 是相对深度所以每帧的数据维度是 8×324。import cv2 import numpy as np import mediapipe as mp POSE_IDS [11, 12, 13, 14, 15, 16, 23, 24] SEQ_LEN 30 def extract_sequence(video_path): mp_pose mp.solutions.pose pose mp_pose.Pose(static_image_modeFalse, min_detection_confidence0.5, min_tracking_confidence0.5) cap cv2.VideoCapture(video_path) seq [] while True: ret, frame cap.read() if not ret: break frame_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(frame_rgb) if results.pose_landmarks: landmarks results.pose_landmarks.landmark frame_vec [] for idx in POSE_IDS: lm landmarks[idx] frame_vec.extend([lm.x, lm.y, lm.z]) seq.append(frame_vec) if len(seq) SEQ_LEN: break cap.release() return np.array(seq, dtypenp.float32)这段代码按固定窗口长度截取手势序列。SEQ_LEN 30表示取 30 帧假设视频是 30 帧每秒那么刚好覆盖 1 秒钟的指挥动作如果交警动作较慢可以调整到 45 或 60。POSE_IDS里的编号是 MediaPipe 官方定义11、12 分别是左右肩13、14 是左右肘15、16 是左右腕23、24 是左右髋。遍历完视频后返回一个形状为(实际帧数, 24)的数组后续训练时再补零或截断到固定长度。需要强调这里只保存姿态坐标不保存原图因此数据集体积很小。一段 30 帧的手势序列用 float32 存只有 24×30×4 字节大约 2.9 KB一万个样本也就 30 MB 左右比存视频帧少了几个数量级。我在采集数据时还会刻意避免两种极端情况人物距离镜头太远关键点评估置信度低于 0.5这种序列直接丢弃另一种是画面中出现多个行人MediaPipe 默认检测置信度最高的人可能导致接替跳动这时要固定距离并让人物尽量居中。2.2 写一个可复用的 PyTorch Dataset 与数据标注文件拿到关键点序列后我按“一个数字手势目录一个标签”的方式组织数据集并在根目录生成train.csv和val.csv两个描述文件每一行是路径和标签。这样在换机器或换数据集时不需要修改代码即能加载。import os import torch from torch.utils.data import Dataset class GestureSequenceDataset(Dataset): def __init__(self, csv_path, seq_len30): self.samples [] with open(csv_path, r) as f: for line in f: line line.strip() if not line: continue path, label line.split(,) self.samples.append((path, int(label))) self.seq_len seq_len def __len__(self): return len(self.samples) def __getitem__(self, index): path, label self.samples[index] seq np.load(path) # 归一化以左肩到右肩的中心为原点以肩宽为尺度 if seq.shape[0] 0: seq np.zeros((self.seq_len, 24), dtypenp.float32) else: seq normalize_sequence(seq) if len(seq) self.seq_len: pad np.zeros((self.seq_len - len(seq), 24), dtypenp.float32) seq np.vstack([pad, seq]) # 前置补零最后一个时间步还原成真实手势最后一帧 else: seq seq[:self.seq_len] return torch.from_numpy(seq).float(), torch.tensor(label, dtypetorch.long)GestureSequenceDataset的核心工作就是三个读.npy文件、补零到统一长度、返回张量。这里使用前置补零即真实动作序列被推到时间轴尾部。因为模型在训练时取out[:, -1, :]补零在序列头部模型读到的是从零帧开始进入真实动作最后的隐藏状态刚好落在手势结束帧不会因为尾部补零而丢失动作信息。这里用了最简单的方式处理不等长序列如果后续对边界帧敏感可以改用pack_padded_sequence或全局平均池化但先跑通基线。normalize_sequence是我自定义的一个函数作用是将肩膀中心平移到原点并除以左右肩距离消除绝对位置和人物高矮对判断的干扰。补零长度seq_len必须和提取特征时的SEQ_LEN一致否则模型的输入维度对不上。训练验证集划分不要用随机打散而是按拍摄者划分同一个人的同一种手势只出现在训练集或验证集避免数据泄漏。否则验证集准确率虚高换到真实场景立刻失效。这个细节在模型上体现不出来但对结果可信度影响很大。数据增强方面常见做法是在关键点坐标上加入小幅高斯噪声模拟摄像头抖动也可以随机沿着时间轴做轻微缩放让模型对手势速度快慢更鲁棒。这些在训练时动态做例如在__getitem__里以一定概率对seq乘以0.8 ~ 1.2的随机系数。由于关键点本身就是空间坐标这样的增强不破坏手势语义。3. 搭建 LSTM 时序手势识别模型并跑通训练3.1 为什么选 LSTM 而不用 Transformer 或 3D CNN交通指挥手势是天然的时间序列关键动作顺序极其重要。以左转弯待转和左转弯为例前者在手臂抬起后有一个明显停顿等待转后续后者在手臂抬起后直接连贯画圈。如果模型只看到单帧两个手势的某一瞬间几乎无法区分。我用 LSTM 处理这类具有时序依赖的任务每输入一帧关键点模型内部会更新隐藏状态把前几帧的信息带进当前判断。相比 3D CNNLSTM 的参数量小很多在只有几千个样本的交通手势数据上不容易过拟合相比 TransformerLSTM 对序列长度不敏感默认能处理变长输入部署时也更轻量。如果你的数据集达到数万段且包含大量角度变化可以换用 Transformer 编码器把 LSTM 替换成nn.TransformerEncoder输入位置编码后同样取最后一个 token 的分类结果。但在起步阶段我建议先用 LSTM 把 8 分类基线拉通再考虑更复杂的结构。3.2 模型定义与关键超参数模型结构很简单两层 LSTM 一个全连接分类层。由于输入没有复杂的空间结构不需要额外卷积。每一帧输入维度是 24LSTM 输出维度设为 128第二个 LSTM 层继续使用 128取最后一帧输出进入全连接层得到 8 类 logits。import torch.nn as nn class GestureLSTM(nn.Module): def __init__(self, input_dim24, hidden_dim128, num_layers2, num_classes8): super().__init__() self.lstm nn.LSTM(input_dim, hidden_dim, num_layers, batch_firstTrue, dropout0.3) self.fc nn.Linear(hidden_dim, num_classes) def forward(self, x): # x: (batch, seq_len, input_dim) out, _ self.lstm(x) # 取序列最后一帧等价于模型“看完”整个动作后做判断 last out[:, -1, :] return self.fc(last)batch_firstTrue意味着输入张量形状为(batch, seq_len, features)和 PyTorch 里大部分 CV 模型的布局一致省去维度转换。dropout只应用在两层 LSTM 之间防止最后一个时间步直接过拟合到训练集。out[:, -1, :]是取每个序列最后一步的隐藏状态如果样本里有补零段前置补零会让最后一个时间步落在真实手势结束帧不会让补零区域直接参与最终输出。训练时损失函数用交叉熵优化器用 Adam学习率初始值 1e-3配合 StepLR 在 20 个 epoch 后衰减到 1e-4。具体超参数如下表超参数取值说明input_dim248 个关键点 × 3 坐标hidden_dim128LSTM 隐藏层宽度num_layers2LSTM 堆叠层数dropout0.3LSTM 层间 Dropoutseq_len30每个手势样本包含 30 帧batch_size32每个 batch 包含的序列数lr1e-3Adam 初始学习率lr_step20学习率衰减轮数weight_decay1e-4L2 正则化强度训练循环里我通常会加早停机制每次验证集准确率刷新时保存当前参数连续 10 个 epoch 不增长就终止。训练结束后直接加载验证集上最好的那版权重而不是最后一个 epoch 的权重因为训练后期模型可能已经在小幅度过拟合。import torch import torch.optim as optim model GestureLSTM() criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.1) best_acc 0.0 for epoch in range(60): model.train() for X, y in train_loader: optimizer.zero_grad() logits model(X) loss criterion(logits, y) loss.backward() optimizer.step() scheduler.step() model.eval() correct, total 0, 0 with torch.no_grad(): for X, y in val_loader: pred model(X).argmax(dim1) correct (pred y).sum().item() total y.size(0) val_acc correct / total if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_gesture_lstm.pt)这段训练代码是标准的 PyTorch 流程每个 epoch 前model.train()开启 Dropout验证时model.eval()关闭 Dropout 和 BatchNorm 统计。保存模型只保存state_dict()不含优化器状态文件更小也便于部署到推理脚本。这里train_loader和val_loader使用上一节自定义的GestureSequenceDataset配合DataLoader创建例如DataLoader(dataset, batch_size32, shuffleTrue, num_workers4)。要观察训练是否健康除了关注 loss 下降更要看验证集每类的 recall 是否均衡。交通手势的 8 类不是均衡分布的停止和直行容易采集左转弯待转和右转弯待转样本少。遇到这种情况先把CrossEntropyLoss的weight参数设成样本数倒数的归一化权重再考虑数据增强。加权后模型不会因为某类样本多而无脑偏向高频手势。3.3 训练时的常见掉点原因与解决方式第一个常见问题是关键点序列里有大量零向量原因是视频起始和结束阶段人都没入镜但代码依然把空补丁当成了正常样本。判断方法是打印train_loader前几个样本的最大值和最小值如果很多帧全为 0说明特征提取阶段没有做好存在性检查。我在__getitem__里加了if seq.shape[0] 0分支但仍然建议在生成数据集时直接过滤掉这类坏样本。第二个问题是训练准确率很高而验证准确率低通常是因为不同拍摄者的身高、相机角度差异过大。我处理方式是加强对关键点坐标的归一化不只是肩宽还可以把 z 方向坐标减掉左肩和右肩的平均 z减少相机远近带来的尺度漂移。如果条件允许让数据尽量来自多个机位高度因为交警手势实际是在路口高位视角下观察的。第三个问题是模型只取最后一帧输出导致早期信息被遗忘。对于包含明显停顿待转的手势模型需要记住前段“手臂是否抬起”这属于长距离依赖。遇到这种情况可以将 LSTM 后接一个nn.AdaptiveAvgPool1d在时间维上做全局平均池化把 30 帧的输出综合起来再分类而不是只用最后一帧。我在验证集上对比过池化版本对左转弯待转和右转弯待转的准确率提高约 4~5 个百分点。4. 在实时视频流里加载模型做手势识别并处理预测抖动4.1 用 OpenCV MediaPipe 提取实时关键点序列训练完毕离“在路口看到手势给出结果”还差一步把离线处理和在线推理接到同一条流水线上。实时流程和离线提取特征几乎一样区别在于输入源从视频文件变成摄像头以及每次不是处理完整手势而是一个不断滑动的窗口。我一般用 OpenCV 打开摄像头读取每一帧交给 MediaPipe 得到关键点然后推进一个 30 帧缓冲区。import cv2 import numpy as np import mediapipe as mp import torch mp_pose mp.solutions.pose pose mp_pose.Pose(static_image_modeFalse, min_detection_confidence0.5) model GestureLSTM() model.load_state_dict(torch.load(best_gesture_lstm.pt, map_locationcpu)) model.eval() buffer [] cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(rgb) if results.pose_landmarks: frame_vec [] for idx in POSE_IDS: lm results.pose_landmarks.landmark[idx] frame_vec.extend([lm.x, lm.y, lm.z]) buffer.append(frame_vec) if len(buffer) SEQ_LEN: buffer.pop(0)buffer是一个普通 Python 列表保存最近 30 帧的关键点向量。每次加入新帧后如果超过SEQ_LEN就把最旧的一帧丢出去保证缓冲区永远只有最新 30 帧。这里没有用队列直接用pop(0)时间复杂度是 O(n)但 n 只有 30实时性没有压力。摄像头分辨率不需要开太高640x480 足够MediaPipe 在这个分辨率下跑得比 1080p 快很多。4.2 滑动窗口推理与多数投票平滑单帧模型输出波动很大因为手部轻微抖动或关键点检测误差会导致预测标签跳变。我在推理端加了两层平滑一是只对完整标定长度序列做预测二是对最近 N 次预测结果取众数作为最终输出。from collections import Counter recent_preds [] def predict_topk(seq): x torch.from_numpy(seq).unsqueeze(0).float() with torch.no_grad(): logits model(x) probs torch.softmax(logits, dim1) return torch.argmax(probs, dim1).item() if len(buffer) SEQ_LEN: seq np.array(buffer, dtypenp.float32) pred predict_topk(seq) recent_preds.append(pred) if len(recent_preds) 10: recent_preds.pop(0) final Counter(recent_preds).most_common(1)[0][0]recent_preds保存最近 10 次预测结果然后Counter统计出现次数最多的标签。投票窗口大小的选择很关键窗口太短抑制不了抖动窗口太长会让输出产生明显延迟。我在实际测试中取 10 次对应约 0.3 秒的延迟既能滤掉单个误判又不会让交警手势已被替换后还显示旧标签。除了标签投票也可以把概率平均后取最大值这样对类别间重叠更平滑。具体做法是维护一个长度为 8 的累积概率向量每来一个新预测就把 30 帧窗口的 softmax 概率加到向量里窗口滑动时减掉最旧的那个最后取最大概率的类别。两种平滑方法在正常摄像头下效果接近多数投票实现更简单。4.3 优化与部署ONNX 导出和异常输入处理如果想要在边缘设备或低配电脑上提升速度可以把 PyTorch 模型导出为 ONNX 格式再通过 ONNX Runtime 加速推理。这里不需要对模型结构做任何改动只需要给定一个 dummy 输入导出。dummy_input torch.randn(1, SEQ_LEN, 24) torch.onnx.export( model, dummy_input, gesture_lstm.onnx, input_names[seq], output_names[logits], dynamic_axes{seq: {0: batch_size}}, opset_version12 )dynamic_axes将 batch 维度标记为动态这样外部调用时可以不固定 batch size。ONNX Runtime 在 CPU 上的推理速度通常比 PyTorch 的 eager 模式快一倍左右而且对模型结构有优化。导出后加载方式变成onnxruntime.InferenceSession(gesture_lstm.onnx)输入是 NumPy 数组输出也是 NumPy 数组和 PyTorch 的 tensors 解耦。异常输入处理主要落在results.pose_landmarks为 None 的情况。如果在画面里检测不到人就跳过这一帧不清空缓冲区避免把空帧补成零向量喂给模型。还可以检查关键点的可见度值lm.visibility低于 0.7 的帧直接视为不可信同样不推入缓冲区。实际场景中交警可能背对摄像头或侧身这些情况的准确性会下降理想部署视角是摄像头正对交警正面俯视角度控制在 15 度以内。5. 手势识别训练中的几个调试技巧从混淆矩阵到失败样本当你把上面的 Pipeline 跑通后真正有价值的是找错。我在项目最后一步会画每个手势的混淆矩阵观察哪些手势容易被混在一起。交通手势里最常见的错误是左转弯信号和左转弯待转信号混淆因为两者前半段都是手臂从竖直挥到水平左侧只有指向停留时间不同。我遇到这种情况不会盲目加模型层数而是检查数据里是否真的包含足够的“待转暂停”帧往往需要把手势视频从 1 秒扩展到 1.5~2 秒即SEQ_LEN从 30 调到 45 或 60。另一个损失来源是同一手势的时间长度不稳定。同样是停止信号有的交警把动作做到位后手停 2 秒有的只做动作马上放下。模型对后者会缺少“手部静止”这一关键帧。我建议在数据增强里加入随机时间缩放以 0.9~1.1 的比例拉伸或压缩序列让模型对动作快慢更鲁棒。拉伸后长度变化要用插值补齐不能直接重复抽帧。如果某个类别训练集数量特别少比如右转弯待转只有不到 50 段我第一个做的不是去采集新数据而是用其余类别中动作相近的手势做部分迁移。例如用停止信号额外构造类把序列后半段的手部关键点保持不动就能模拟出待转等待的形态再放回右转弯待转的标签。这样生成的数据虽然和真实动作有差异但胜在能缓解极端类别不平衡。有时候这个办法比再加一种损失函数更有效。最后还要验证模型在“看不见”的人身上是否有效。我习惯留出 3 个完全没参与训练的人作为测试集而不是只用随机划分的验证集。如果测试集准确率比验证集低 10% 以上基本可以确定模型对特定穿着或身高等外观特征过拟合。回到数据层检查归一化是否只使用了肩宽手臂长度的个体差异也应该用肘腕距离做一次相对坐标校正。把调试重心放在失败样本上逐个看预测错误的关键点序列你会发现大部分问题来自数据标注错误和关键点缺失帧而不是模型结构。先把这两项整理干净8 种手势识别准确率往往能直接提升 5 个百分点以上。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门