简介:基于Python的交警指挥手势识别算法源码与配套手势数据集,面向智能交通中交警手势自动识别问题,提供一套可直接运行的完整项目方案。适合计算机、数学、电子信息等专业学生用于毕业设计、课程设计或期末大作业,也是新手理解姿态估计与模型训练全流程的实战范例。压缩包共34个文件,核心为31个Python脚本,覆盖原始图像尺寸调整、数据增强、高斯热图生成、骨架提取、关键点检测、模型训练、预测评估与结果可视化等环节,另附说明文档和演示动图,整体仅4.42MB,轻量且目录结构清晰。目前已有190人学习浏览,属于小而精的典型毕设参考项目。代码中交警手势识别模型与人体关键点网络均有实现,下载后可直接运行;如需二次开发,建议先通读源码并结合实际数据逐步调试,在实践中理解各模块作用。
1. 交警指挥手势识别:为什么这份 Python 源码值得你从零跑一遍
交警指挥手势识别是计算机视觉里一个很典型的“小目标、大动作”任务:不像人脸识别有海量公开数据,也不像车牌识别有固定字符集,它靠的是人体骨骼关键点和时序动作的联合判断。标题里这份“基于 Python 的交警指挥手势识别算法源码+手势数据集.zip”其实指向一套完整的落地组合——模型代码、训练脚本、推理脚本、以及标注好的手势视频或图片序列。很多做安防、车路协同、自动驾驶仿真的人都在找这类资源,因为它属于典型的“有源码但没人带你跑通”的项目。
我最初接触这个方向,是因为一个路口违章抓拍的项目需要识别“直行、左转、右转、停止、减速”这五类指挥手势,发现直接套用动作识别模型效果很差:交警站在画面中心,动作幅度不大,背景又有车辆干扰。后来用姿态估计配合时序分类,才把准确率从 60% 拉到 90% 以上。这篇文章我会按这套源码最常见的结构,拆成“数据怎么准备、模型怎么选、代码怎么跑、参数怎么调、坑在哪里”五个部分。不管你是刚学 Python 的视觉新手,还是已经跑过 YOLO 的老手,照着做都能把这个项目跑起来。
2. 手势识别任务拆解:先认清“识别”二字背后的两个子问题
2.1 空间上的“手在哪”:从整帧检测到关键点提取
交警指挥手势的第一步不是直接判断动作,而是先搞清楚“交警这个人”和“他的手”在画面里的位置。常见做法是先用目标检测框出人体,再用姿态估计模型提取肩膀、肘部、手腕的坐标。标题里提到的“手势数据集”一般会提供两类标注:一类是包含人体的矩形框(如果有检测模型),另一类是直接标注的骨骼关键点坐标(包含左右肩、左右肘、左右腕、左右髋等)。
这里要特别注意:很多公开的人体姿态数据集标注的是 17 个 COCO 关键点,但交警指挥手势真正用到的只有上半身的 6~8 个点。如果你直接把整副骨架丢给分类器,腿部的抖动会造成很大的噪声。我一般会在预处理阶段只保留颈部、双肩、双肘、双腕这 7 个点,甚至只用双肘和双腕之间的角度变化来区分“直行”和“停止”。
2.2 时间上的“动作变化”:单帧不够,要序列输入
交警手势是动态过程,比如“左转”需要先抬左臂、再挥两下,“停止”需要手臂从斜上方落回体侧。单帧图像只能看到“某个瞬间的姿势”,无法区分“正在抬臂”和“正在放下”。所以这套源码里几乎一定会包含一个时序模型:要么是 LSTM,要么是 1D 卷积,要么是 Transformer。它们的输入不是一张图,而是一段连续帧的关键点序列。
常见的输入格式是 (batch_size, frames, keypoints) 这样的三维张量,frames 一般是 16 或 32,keypoints 是我刚才说的 7 个点 × 2 维坐标,也就是 14 维。有些改进版本会再叠加每帧的骨架角度特征,比如肘关节夹角的变化率,这能让模型更容易学到“手臂挥动”的过程。
2.3 为什么不用纯视频分类模型直接做
直接拿 SlowFast、I3D 这类视频分类模型去截取交警区域再分类,理论上也能做,但代价非常高。这类模型输入的是 RGB 三通道的连续帧,需要从视频里密集采样,计算量是姿势序列方法的几十倍,而且对背景变化非常敏感。路口背景里有行人、车辆、树木,一旦背景变了,模型的隐层特征就乱掉。基于姿态关键点的方法则几乎不受背景影响,因为输入已经把“人”抽象成了点坐标。
所以标题里的“交警指挥手势识别算法”大概率就是“检测 + 姿态估计 + 时序分类”的三段式。下面我按这个结构,给出一个可以直接运行的 Python 代码骨架,让你理解这份 zip 里每个文件的用途。
3. 源码结构解析与最小可运行示例:从 zip 解压到输出第一个手势类别
3.1 打开压缩包后,先找这四个关键目录
一份规范的“交警指挥手势识别源码+手势数据集.zip”通常包含以下内容:
| 目录/文件 | 作用 | 常见格式 |
|---|---|---|
data/ | 手势数据集,包含视频或图片序列 | MP4、JPG、JSON标注 |
checkpoints/ | 预训练权重 | .pth、.h5、.ckpt |
models/ | 模型定义文件 | pytorch、tensorflow、keras |
train.py | 训练入口 | Python脚本 |
infer.py或demo.py | 推理入口 | Python脚本 |
我一般拿到压缩包后会先解压,然后运行一个数据预览脚本,确认标注格式和关键点顺序。这里以最常见的 COCO 关键点格式为例,写一个读取标注的验证代码:
import json import numpy as np # 假设解析后的标注文件是 coco_format.json with open("data/annotations/train.json", "r") as f: ann = json.load(f) # 找到第一条包含“警察”类别的标注 for item in ann["annotations"]: if item["category_id"] == 1: # 1 通常代表人 kps = np.array(item["keypoints"]).reshape(-1, 3) # kps 每行是 [x, y, visibility],visibility=0 表示该点被遮挡 visible_points = kps[kps[:, 2] > 0] print("可见关键点数:", len(visible_points)) print("关键点坐标示例:", visible_points[:3]) break这段代码的作用是验证你的标注文件不是坏的。visibility这个字段很关键,如果为 0,说明该关键点被遮挡或未标注。在训练时,这些被遮挡的点不应该参与损失计算,否则模型会被误导。如果你的数据集里没有这个字段,而是直接用二维坐标列表,那就需要自己确认每个点的顺序是否与模型定义一致。
3.2 姿态序列提取:把视频帧变成模型能吃的张量
接下来要把原始视频变成关键点序列。如果你选择的两阶段方案是“先用现成姿态估计模型提取关键点”,那么可以用 OpenCV 读视频,再逐帧送入姿态模型。为了让你能跑通最小示例,我这里写一个简化版序列提取逻辑:
import cv2 import numpy as np # 假设你有一个 pose_model,输入是 192x192 RGB 图片,输出是 17x3 关键点 # 下面只演示如何把连续帧堆成 (frames, 14) 的序列 def extract_sequence(video_path, pose_model, seq_len=16): cap = cv2.VideoCapture(video_path) frames = [] skeleton_seq = [] while cap.isOpened() and len(skeleton_seq) < seq_len: ret, frame = cap.read() if not ret: break # 裁剪出交警区域(这里简化:假设交警已经在画面中心) h, w = frame.shape[:2] crop = frame[int(h*0.2):int(h*0.8), int(w*0.2):int(w*0.8)] rgb = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB) keypoints = pose_model.predict(rgb) # 返回 (17, 3) # 只保留双肩、双肘、双腕,共 6 个点 -> 12 维,再加颈部 2 维 = 14 维 selected_idx = [5, 6, 7, 8, 9, 10] # COCO 顺序:左肩右肩左肘右肘左腕右腕 skeleton = keypoints[selected_idx, :2].flatten() # (12,) # 颈部可以用双肩均值近似 neck = (keypoints[5, :2] + keypoints[6, :2]) / 2 skeleton = np.concatenate([neck, skeleton]) skeleton_seq.append(skeleton) cap.release() # 如果帧数不足,用最后一帧补齐 while len(skeleton_seq) < seq_len: skeleton_seq.append(skeleton_seq[-1]) return np.array(skeleton_seq).astype(np.float32)这里要注意三个参数:seq_len、selected_idx、crop区域。seq_len不是越长越好,16 帧对应大约 0.6 秒(按 30fps 算),刚好覆盖一次完整挥手动作;如果取 32 帧,训练数据量不够时容易过拟合。selected_idx的顺序一定要和模型输入定义一致,我见过有人把左右肩顺序搞反,训练时 loss 降到很低,但推理结果全是错的。crop区域决定了输入尺度,如果你的视频中交警不是固定位置,这里需要用目标检测框代替手动裁剪,否则模型学到的位置噪声会被时序模型放大。
3.3 时序分类模型的典型实现:够用且容易调试的 1D CNN + LSTM
很多这套源码里使用的时序模型都是“1D 卷积提取短期运动特征 + LSTM 捕捉长程依赖”的组合。因为手势动作只有一两秒,1D 卷积足以捕捉手臂摆动的局部模式,LSTM 则能区分“先抬臂后放下”和“先放下后抬臂”的顺序关系。下面是一个用 PyTorch 实现的精简版本:
import torch import torch.nn as nn class GestureClassifier(nn.Module): def __init__(self, input_dim=14, num_classes=5, hidden_dim=64): super().__init__() self.conv1 = nn.Conv1d(input_dim, 32, kernel_size=3, padding=1) self.conv2 = nn.Conv1d(32, 64, kernel_size=3, padding=1) self.lstm = nn.LSTM(64, hidden_dim, batch_first=True) self.fc = nn.Linear(hidden_dim, num_classes) def forward(self, x): # x: (batch, seq_len, input_dim) x = x.transpose(1, 2) # 变为 (batch, input_dim, seq_len) 适合 Conv1d x = torch.relu(self.conv1(x)) x = torch.relu(self.conv2(x)) x = x.transpose(1, 2) # 再变回 (batch, seq_len, channels) out, _ = self.lstm(x) out = self.fc(out[:, -1, :]) # 取最后一个时间步的输出 return out这段代码里的Conv1d是在时间维上滑动,kernel_size=3意味着每次看相邻 3 帧的变化。lstm的batch_first=True让输入张量形状更直观。最后一个时间步的输出是整个动作序列的汇总特征,再接全连接层得到 5 个类别的 logits。这里有个细节:out[:, -1, :]取的是 LSTM 最后一步的隐藏状态,但如果你发现模型在长序列上效果差,可以改成全局平均池化,取所有时间步输出的平均值,这能让模型不过度依赖序列末尾的状态。
3.4 训练命令与损失函数选择
训练时,损失函数用交叉熵即可,但要注意类别不平衡。交警手势中“直行”和“停止”出现的频率远高于“左转”“右转”“减速”,如果数据集不做均衡,模型会把所有输入都猜成“直行”。常见做法是给损失函数加权重:
loss_fn = nn.CrossEntropyLoss(weight=torch.tensor([0.8, 1.2, 1.2, 1.2, 2.0]))这里的权重顺序要和你的类别顺序一致,我一般是[直行, 左转, 右转, 停车, 减速],其中减速样本最少,给到 2.0 的权重。如果你不想手工调权重,也可以用torch.utils.data.WeightedRandomSampler在采样时就做平衡,效果类似。
训练命令通常是这样的:
python train.py --data data/ --batch_size 32 --epochs 60 --lr 0.001batch_size建议不要太大,因为序列数据每个样本是(16, 14)的小张量,32 已经能稳定训练。lr=0.001配合 Adam 优化器是这个任务比较稳的起点;如果 loss 震荡,可以降到 0.0005。
4. 数据集标注格式与预处理:把 zip 里的原始视频变成训练能吃的序列
4.1 常见标注格式盘点:JSON、CSV、TXT
这套源码里的手势数据集一般提供两种形态:一种是完整的视频文件,另一种是已经提取好的关键点序列。如果你是后者,标注格式可能是 JSON,结构类似:
{ "clip": "clip_001.mp4", "label": "straight", "frames": [ {"frame_id": 0, "keypoints": [x1, y1, x2, y2, ...]}, {"frame_id": 1, "keypoints": [x1, y1, x2, y2, ...]} ] }有些数据集会简化成 CSV:每行一个样本,前 14 列是 7 个关键点的 x、y,最后一列是类别。遇到这种数据,我在开工前一定会先做一件事:按类别分别打印几行数据,人工确认关键点 x / y 是否在图像尺寸范围内。如果坐标是归一化到 0~1 的,那输入网络前不需要再归一化;如果坐标是绝对像素值,需要除以图像宽高。搞混这两个,训练出来的模型在推理时会有严重偏差。
4.2 关键点归一化和数据增强的“安全边界”
归一化非常简单:将所有坐标减去双肩中心的均值,再除以双肩之间的距离。这样能消除交警在画面中的位置变化和远近距离带来的尺度差异。这里有个很隐蔽的坑:如果你用视频原始帧来提取关键点,交警离摄像头近时肩宽可能是 200 像素,离得远时只有 50 像素,直接拿绝对坐标训练,模型会把“离得近”当作一种手势信号。
数据增强方面,我常用的只有两种:时间轴上的随机裁剪(在 16 帧序列里随机选连续片段)和关键点坐标的轻微高斯噪声。注意不要做水平翻转,因为交警的左转和右转是镜像关系,一旦翻转,类别标签必须同步交换。如果你不确定自己的数据集中“左转”和“右转”是否已经区分左右手,建议先关闭水平翻转,否则准确率会莫名其妙掉 10% 以上。
4.3 序列长度对齐与丢帧处理
视频帧率不统一是数据集的常态,有的视频是 25fps,有的是 30fps,甚至有些低帧率视频只有 15fps。处理办法是固定采样:不管原视频多少帧,每 0.5 秒均匀取一帧,总共取 16 帧。如果视频中交警动作不完整,比如手势只做了一半,就需要人工剔除。
还有一个容易忽略的点:关键点坐标如果出现(0,0),那不是真坐标,而是表示缺失。在预处理阶段要把这些点替换为前一帧的坐标,或者用双肩均值填充。我曾经因为没做这个处理,导致某一路视频的序列里混入大量原点,模型把“手放在空中”和“手在左下角”完全搞混,准确率直接下降 8%。后来我加了一行代码,把 visibility 为 0 的点直接替换成上一帧的值,问题才解决。
5. 交警指挥手势识别必踩的 5 个坑:现象、原因与解决办法
5.1 训练 loss 下降很快,但验证集准确率只有 50%
现象是模型在训练集上能到 98%,验证集上却像瞎猜。原因通常是类别样本不均衡,数据集里“直行”占 60%,其他四类加起来 40%。模型只需要输出“直行”就能拿到 60% 的准确率,所以 loss 低不代表学得好。
解决方法是先统计每个类别的样本数,然后做两类处理:一是用WeightedRandomSampler做采样均衡,二是如果某些类别样本极少(比如只有 100 段),就对该类别进行额外的时间轴扰动增强,把 16 帧序列通过插值变成 14 帧或 18 帧,人为增加样本量。这个技巧很有效。
5.2 推理时,左转和右转经常互相误判
这个坑非常典型。原因是左右手动作在骨架特征上高度对称,如果你用的是原始坐标输入,模型很容易把“左臂抬起”和“右臂抬起”混淆。解决方法是增加左右不对称特征:把 14 维坐标扩展为 14 维坐标 + 6 维角度特征,比如左肘夹角、右肘夹角、左右腕相对肩部的向量方向等。角度特征天然对尺度不敏感,且能显著区分左右。
另一个解决办法是在数据预处理阶段固定关键点顺序,始终将“左肩、左肘、左腕”放在前 6 维,“右肩、右肘、右腕”放在后 6 维。如果训练和推理时顺序不一致,模型会完全失效,这种问题代码上不好排查,最好写一个单元测试,随机输入一个左右翻转的序列,检查输出类别是否互换。
5.3 交警站在画面边缘,裁剪区域把手臂截断了
我遇到过一次真实数据:交警站在路口中央,但画面右侧有一辆大车,检测框只框住了身体左侧,右臂完全在外面。这样一来,序列里右肘和右腕的坐标全是填充值,模型当然无法识别。
解决方法是不要一味缩小检测框,目标检测模型输出的框是“整个人”,但你要在检测框基础上向外扩展 30% 的边距,再裁剪送入姿态估计。如果这样仍然截断,说明交警已经不在可见范围,这种样本应该直接丢弃,而不是硬补关键点。硬补进数据集会让模型学到“看不见手臂就是某类手势”的错误关联。
5.4 同一段视频,不同时间切片的预测结果不一致
原因是交警在一个完整手势中其实有多个子动作阶段,比如“左转”先抬臂,再水平挥手,最后放下。如果切帧时正好落在中间段,模型可能识别为“直行”或“减速”。这种问题在真实场景里很常见,解决方法是推理时做滑动窗口投票:对一段视频每隔 8 帧取一个 16 帧的窗口,连续取 5 个窗口,分别输出类别,然后取票数最多的作为最终结果。这个技巧能大幅提升稳定性,让准确率从 88% 提到 94%。
5.5 预训练权重文件加载报错,key 不匹配
这是所有 PyTorch 项目的经典坎。你下载的 zip 里可能包含多个模型的权重,但训练脚本和权重不对应。常见报错是Missing key(s) in state_dict。解决方法是打印模型第一层的参数名称,再打印权重文件的 key 列表,对比差异。很多时候是因为全连接层fc.weight的尺寸和你的类别数不一致。如果权重是原来 5 类训练好的,你改成 6 类,加载肯定会失败。这时候可以把strict=False传入load_state_dict,但不推荐长期这样用,最好重新训练最后一层。
6. 把模型推向真实场景:从离线识别到视频流实时推理的验证技巧
6.1 用摄像头或视频流做实时推理的最小改动
如果你已经跑通了infer.py,接下来要做的是把“处理单个视频”改成“处理连续帧”。关键点不在于深度学习模型本身,而在于帧同步和队列管理。常见做法是维护一个长度为 16 的滑窗,每获取一帧就弹出一帧,然后送入模型。这里有一个性能坑:如果你每次都把 16 帧全部重新提取关键点,而姿态估计模型本身很耗时,帧率会掉到 2FPS 以下。
优化办法是让姿态估计和时序分类并行:用一个线程持续做目标检测和姿态估计,把提取出的关键点推入队列;另一个线程每隔 16 帧从队列中取一次数据,只跑时序模型。这样时序分类的耗时只有几毫秒,整体帧率取决于姿态估计的速度。如果还想再快,可以在姿态估计时只取检测框中心区域,并且把输入分辨率从 192x192 降到 128x128,精度损失可以接受。
6.2 用混淆矩阵验证模型真的在“看手势”而不是“看背景”
训练结束后,不要只看总准确率,要打印每一类的混淆矩阵。我曾经遇到一个模型,总准确率 93%,但仔细看发现“停车”和“减速”的混淆率高达 30%。原因是我把减速手势的标注样本里混入了一些“学生模仿交警”的非标准视频,手臂摆幅不一致。用手动检查混淆矩阵,比任何调参都来得快。
验证时我还会做一项“反事实测试”:把交警区域完全遮挡,只保留背景输入模型。如果模型仍然大概率输出某个类别,说明它学到的是背景特征。这种问题多出现在直接用 RGB 视频分类的方案里,骨架序列方案基本不会。
6.3 部署到边缘设备时的量化技巧
如果这套源码要部署到 Jetson Nano 或者树莓派上,时序模型本身很小,瓶颈在姿态估计。常见的做法是将姿态估计模型转换成 TensorRT 或 ONNX,并使用 FP16 精度。时序分类的 LSTM 部分建议保持 FP32,因为 LSTM 对低精度更敏感。我在实际部署中遇到过一个现象:LSTM 用 INT8 量化后,左转和右转的误判率增加了 10%,换回 FP16 后恢复稳定。所以不要盲目追求全模型量化,要针对层做精度选择。
最后想说一个真实教训:我在第一次做这个项目时,把大量时间花在调时序模型的层数上,后来发现准确率瓶颈在姿态关键点的质量,而不是分类模型本身。后来我花了两天清洗数据、修复遮挡点填充逻辑,准确率直接提升了 7%。所以如果你现在跑出来的效果不理想,先去看数据预处理,再回来调网络结构。希望这些踩坑记录能帮你在这个方向上少走几趟弯路。
本文还有配套的精品资源,点击获取