资讯详情

基于PyTorch实现交警指挥手势识别:8类动作实战全流程

📅 2026/10/1 2:48:09 | 华诺云谱 👁 阅读
基于PyTorch实现交警指挥手势识别:8类动作实战全流程
简介这份资源面向深度学习入门者与计算机视觉方向的开发者提供一套基于PyTorch实现中国交通警察8种指挥手势识别的完整项目。内容围绕卷积神经网络展开涵盖数据集构建、模型训练、评估与推理全流程可用于智能交通场景下的手势自动化识别研究与实践。压缩包共34个文件以31个Python脚本为主覆盖数据预处理、关键点提取、模型定义与训练、预测评估等模块另含2个Markdown说明文档和1个演示动图整体约4.42MB结构清晰便于按模块查阅。项目已积累1345人学习下载具备一定参考热度。读者可获得可运行的训练与推理代码、手势识别模型、关键点与姿态估计相关实现以及详细的项目说明步骤便于复现实验、理解CNN在图像识别中的落地方式并在此基础上进行数据增强、模型优化与部署扩展。1. 从一段路口视频说起交警手势识别到底难在哪路口监控里交警站在车流中央左臂平举、右臂摆动、转身、再抬手。人眼一眼就能判断这是左转弯待转还是变道信号但把这段视频丢给一个刚训练好的分类模型它很可能给出一个自信满满的错误答案。原因不复杂交警手势是全身协同动作手臂角度、身体朝向、甚至帽子和反光背心的位置都在参与表达而相邻两个手势之间的差异可能只是某只手抬起的角度差了三十度。这就是基于 PyTorch 实现中国交通警察指挥 8 种手势识别这个方向真正要解决的问题。它要做的不是识别挥手这种粗粒度动作而是在停止、直行、左转弯、左转弯待转、右转弯、变道、减速慢行、靠边停车这 8 类标准手势之间做细粒度区分。整套方案通常包含四块数据集图像或视频帧 标签、模型CNN 或 CNN时序结构、训练源码、以及一份能让人照着跑通的说明。适合谁看如果你已经装好 PyTorch 环境想找一个数据量不大、单卡能跑、又能体现完整工程链路的实战项目练手这个方向非常合适。它比 MNIST 有挑战比 ImageNet 训练便宜而且落地场景清晰——智能交通、驾考辅助、路口违章抓拍都能用上。下面我按数据怎么来 → 模型怎么搭 → 怎么训 → 坑在哪 → 怎么调优的顺序把这条链路拆开讲。2. 数据集构建8 类手势的采集、清洗与划分2.1 为什么手势数据集不能直接拿现成的网上能搜到不少手势数据集但绝大多数是手部特写或者通用手势数字、OK、比耶和交警指挥动作完全不是一回事。交警手势的关键信息分布在全身比如停止信号是左臂向前上方直伸、掌心向前左转弯待转是左臂向左下方摆动。你如果只截手部区域这两类几乎无法区分。所以常见做法是自己采集 公开视频抽帧。采集时要注意三点一是多角度正面、侧面、斜 45 度都要有因为路口摄像头不会永远正对交警二是多光照白天、傍晚、阴天都得覆盖否则模型一到傍晚就翻车三是多背景不同路口、不同车流密度都要采样避免模型把背景里的红色车当成停止信号的特征。我一般会按下面的目录结构组织数据方便后面用ImageFolder或自定义 Dataset 直接读dataset/ ├── train/ │ ├── stop/ # 停止信号 │ ├── straight/ # 直行信号 │ ├── left_turn/ # 左转弯信号 │ ├── left_wait/ # 左转弯待转 │ ├── right_turn/ # 右转弯信号 │ ├── change_lane/ # 变道信号 │ ├── slow_down/ # 减速慢行 │ └── pull_over/ # 靠边停车 ├── val/ │ └── (同上 8 个类别) └── test/ └── (同上 8 个类别)划分比例建议train:val:test 7:1.5:1.5。注意 val 和 test 必须来自不同视频源不能从同一段视频里随机抽帧分到两边——同一段视频的相邻帧高度相似随机划分会导致验证集准确率虚高这是新手最容易踩的坑之一。2.2 用抽帧脚本把视频变成可用图像如果你手上是视频素材第一步是抽帧。下面这段脚本按固定间隔抽帧并做简单的去重用帧间差异过滤掉几乎静止的画面import cv2 import os import numpy as np def extract_frames(video_path, out_dir, interval5, diff_thresh8.0): video_path: 输入视频路径 out_dir: 输出帧目录 interval: 每多少帧取一帧 diff_thresh: 帧间平均像素差阈值低于此值视为重复帧丢弃 os.makedirs(out_dir, exist_okTrue) cap cv2.VideoCapture(video_path) idx, saved, prev 0, 0, None while True: ret, frame cap.read() if not ret: break if idx % interval 0: gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) if prev is not None: diff np.mean(cv2.absdiff(gray, prev)) if diff diff_thresh: # 画面几乎没变跳过 idx 1 continue prev gray cv2.imwrite(os.path.join(out_dir, f{saved:05d}.jpg), frame) saved 1 idx 1 cap.release() print(f共保存 {saved} 帧)逻辑说明interval5表示每 5 帧取一张30fps 的视频大约每秒出 6 张既能覆盖动作变化又不会让数据爆炸。diff_thresh是去重阈值交警站立不动时相邻帧差异很小这些帧对训练没有增益反而会让类别不平衡。参数怎么调如果动作快比如变道信号摆动幅度大把interval降到 3如果视频本身帧率低interval可以设成 2 或 1。抽完帧后人工过一遍是省不掉的。我一般用labelImg或者直接按文件夹归类把明显模糊、遮挡严重、标签错误的帧删掉。这一步花的时间远比后面调参省下来的时间值。2.3 数据增强让 8 类手势在低数据量下也能训起来自采数据通常每类只有几百到一两千张直接训容易过拟合。PyTorch 里用torchvision.transforms做在线增强是最省事的from torchvision import transforms train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), # 注意左右翻转会改变手势语义 transforms.RandomRotation(10), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])这里有个关键判断交警手势里左转弯和右转弯是镜像关系RandomHorizontalFlip会把左转弯翻成右转弯等于给模型喂错标签。所以要么关掉水平翻转要么在翻转的同时同步交换标签。我一般直接设p0.0改用RandomAffine做小幅平移和缩放安全得多。ColorJitter的亮度对比度扰动是为了模拟不同时段光照这个可以放心用。3. 模型选型从 ResNet 到 CNNLSTM 的两条路线3.1 单帧分类路线ResNet18 微调够不够用如果你的数据是单帧图像最直接的方案是拿预训练 ResNet18 或 MobileNetV3 做迁移学习。8 类分类输出层改成 8 维即可import torch.nn as nn from torchvision import models def build_resnet18(num_classes8, freeze_backboneTrue): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) if freeze_backbone: for p in model.parameters(): p.requires_grad False in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return model逻辑说明freeze_backboneTrue时只训练最后的全连接层适合数据量小于 2000 张的情况训练快、不容易过拟合。等 loss 稳定后可以解冻layer4做小学习率微调比如1e-4通常能再涨 2~4 个点。Dropout(0.3)是防止全连接层过拟合的常规操作数据量大的话可以降到 0.2。单帧路线的天花板在于它看不到动作过程。交警减速慢行是手臂上下摆动靠边停车是手臂指向路边如果只截一帧摆动到中间位置的画面可能和另一个手势很像。所以单帧模型在静态姿态区分上够用在动态手势上会吃力。3.2 时序路线CNN 提特征 LSTM 建模动作序列要解决动态手势就得引入时序。常见做法是用 CNN 逐帧提特征再把特征序列喂给 LSTM 或 GRUimport torch import torch.nn as nn from torchvision import models class CNNLSTM(nn.Module): def __init__(self, num_classes8, hidden256, num_layers2): super().__init__() backbone models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) self.cnn nn.Sequential(*list(backbone.children())[:-1]) # 去掉 fc self.lstm nn.LSTM(512, hidden, num_layers, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(hidden * 2, num_classes) def forward(self, x): # x: (B, T, C, H, W) B, T, C, H, W x.shape x x.view(B * T, C, H, W) feat self.cnn(x).view(B, T, 512) # 每帧 512 维特征 out, _ self.lstm(feat) return self.fc(out[:, -1, :]) # 取最后时刻输出逻辑说明输入是(batch, 时序长度 T, 3, 224, 224)先把 batch 和 T 合并送进 CNN得到每帧 512 维特征再 reshape 回序列送 LSTM。bidirectionalTrue让 LSTM 同时看前后文对动作识别有帮助。T一般取 16 或 32 帧太短抓不到完整动作太长显存吃不消。取out[:, -1, :]是用最后一个时刻的隐状态做分类也可以改成对时间维做平均池化后者对噪声更鲁棒。两条路线的选择建议数据是单帧图 → ResNet 微调数据是视频片段 → CNNLSTM。如果只有单帧数据但想做时序可以用滑动窗口把连续帧拼成序列但要注意窗口内必须属于同一手势。4. 训练、验证与推理把 8 分类跑通的完整命令4.1 训练脚本的关键参数怎么设下面是一个精简的训练循环重点看优化器、学习率和保存策略import torch from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder device torch.device(cuda if torch.cuda.is_available() else cpu) train_ds ImageFolder(dataset/train, transformtrain_tf) val_ds ImageFolder(dataset/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) model build_resnet18(num_classes8).to(device) criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4 ) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) best_acc 0.0 for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() # 验证 model.eval() correct total 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) pred model(imgs).argmax(1) correct (pred labels).sum().item() total labels.size(0) acc correct / total if acc best_acc: best_acc acc torch.save(model.state_dict(), best_gesture.pth) print(fepoch {epoch} val_acc {acc:.4f})参数说明AdamW比Adam多了正确的权重衰减实现weight_decay1e-4是常规起点。CosineAnnealingLR让学习率按余弦曲线下降比阶梯下降更平滑T_max30对应总 epoch 数。batch_size32在 8G 显存上跑 ResNet18 很稳显存小就降到 16。保存策略用验证准确率最高才存避免存下过拟合的最后一轮。4.2 推理单张图和视频流两种用法训练完拿到best_gesture.pth推理代码要能同时支持单图和视频from PIL import Image import torch.nn.functional as F CLASSES [stop,straight,left_turn,left_wait, right_turn,change_lane,slow_down,pull_over] def predict_image(img_path, model, tf, device): model.eval() img Image.open(img_path).convert(RGB) x tf(img).unsqueeze(0).to(device) with torch.no_grad(): prob F.softmax(model(x), dim1)[0] idx prob.argmax().item() return CLASSES[idx], prob[idx].item()逻辑说明unsqueeze(0)把单张图变成 batch1softmax后取最大概率类别。返回概率值很重要——如果最高概率只有 0.4说明模型不确定实际部署时应该触发人工复核而不是硬判。视频流的话按固定间隔抽帧、缓存最近 T 帧、送 CNNLSTM输出平滑后的结果。提示推理时的预处理必须和验证集完全一致同样的 Resize、Normalize否则准确率会莫名其妙掉一大截这是最常见的训练好但推理崩的原因。5. 避坑与排查8 类手势识别最容易翻车的 5 个地方5.1 现象验证准确率 95%实际部署只有 60%原因验证集和训练集来自同一段视频相邻帧高度相似模型其实在背视频而不是学手势。解决按视频源划分数据集确保 val/test 的视频和 train 完全不重叠。如果数据实在少至少按不同交警、不同路口分组划分。5.2 现象模型总把左转弯和左转弯待转搞混原因这两类动作手臂位置接近单帧特征区分度低。解决改用 CNNLSTM 引入时序或者在单帧模型里加入关键点特征用 MediaPipe 提取手臂角度作为额外输入。也可以对这两类做数据增强时加大旋转和遮挡扰动逼模型学更本质的特征。5.3 现象训练 loss 不下降一直卡在 2.0 左右原因常见有三种——学习率太大导致震荡、标签没对齐ImageFolder 按文件夹名排序类别索引可能和你的 CLASSES 列表不一致、或者 backbone 被冻结但学习率设太高。解决先打印train_ds.class_to_idx确认类别映射再把学习率降到1e-4试。如果是 CNNLSTM检查输入张量维度是不是(B, T, C, H, W)维度错了会静默出错。5.4 现象显存溢出CUDA out of memory原因CNNLSTM 的显存占用和T成正比T32加batch32很容易爆。解决把T降到 16或者用梯度累积模拟大 batch。也可以在 CNN 部分用torch.no_grad()预提取特征缓存到磁盘训练 LSTM 时直接读特征显存能省一大半。5.5 现象模型对傍晚和夜间画面几乎全错原因训练数据以白天为主模型没学过低光照分布。解决采集时强制覆盖不同时段或者用亮度扰动增强ColorJitter的 brightness 调到 0.5。更彻底的做法是先做一次直方图均衡化或 CLAHE 预处理把光照差异拉平再送模型。6. 进阶技巧用关键点分类头把准确率再抬一档纯 CNN 路线做到一定程度会遇到瓶颈因为背景、衣着、光照都在干扰。我后来习惯加一条姿态关键点分支用 MediaPipe 或轻量姿态模型提取人体 17 个关键点把关键点坐标和角度拼成特征向量和 CNN 特征做融合。这样模型关注的是手臂抬到哪、身体朝哪而不是背景里有没有红色车。具体做法是先用姿态模型离线提取所有帧的关键点存成(N, 17, 2)的数组训练时 CNN 分支和关键点分支各出一个特征向量拼接后送全连接分类。关键点分支用两层 MLP 就够参数量很小。实测在自采数据上这个融合方案比纯 ResNet18 高 5~8 个点尤其是左转弯 vs 左转弯待转这种易混类提升明显。验证方法上别只看总体准确率一定要打印混淆矩阵from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, xticklabelsCLASSES, yticklabelsCLASSES) plt.xlabel(Predicted); plt.ylabel(True) plt.savefig(confusion_matrix.png, dpi150)看哪两类互相错得最多再针对性补数据或加特征。这比盲目调参有效得多。最后说个我自己的习惯每次改完模型或数据先跑一个只训 3 个 epoch 的小实验看 loss 有没有正常下降、验证集有没有动。如果 3 个 epoch 都没动静八成是数据或标签出了问题别急着上大训练。这个三步点金式的快速验证帮我省过无数次通宵。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑