ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

基于深度学习的交警手势识别:从关键点提取到时序分类实战

基于深度学习的交警手势识别:从关键点提取到时序分类实战 简介基于Python与深度学习实现的中国交通警察指挥手势识别项目面向毕业设计、课程设计及项目开发场景提供完整源码与配套数据集帮助学习者快速掌握图像分类、手势识别等计算机视觉任务的工程实现流程。压缩包共37个文件核心为31个Python脚本涵盖模型定义、训练、测试及基础功能测试另含2个Markdown说明文档和1个TXT文件用于指导环境配置与项目运行附带GIF演示、gitignore及license文件整体仅4.43MB轻量易获取。目前已有499人学习参考适合计算机视觉、深度学习方向的学生作为课设或毕设的起步模板。资源内目录结构清晰主模块与训练、预测代码分离模型文件和文档一目了然项目源码经过严格测试可直接运行并在此基础上扩展功能。无论用于课堂展示、课程报告还是个人项目都能帮助理解交警手势识别从数据准备到模型部署的完整链路是兼顾实用性与学习价值的参考资料。1. 把“基于Python和深度学习开发的中国交通警察指挥手势识别”当普通图像分类来做大概率会翻车把“基于Python和深度学习开发中国交通警察指挥手势识别”这类题目当普通图像分类来做大概率会在答辩现场翻车。交警手势识别的核心难点不在“认出画面里有个人”而在把连续骨架动作切分成有语义的手势片段再判定是哪一种指挥动作。网上躺着很多相关源码但能从头训练到实时演示的完整方案很少数据集也大多是零散视频或未清洗的图片。这篇文章面向毕业设计、课程设计和想快速落地这个方向的开发者我会把数据标注、关键点训练、时序分类、界面打包四块依次讲清楚参数怎么设、坑在哪一次说透。2. 数据集与标注流程评分点的第一块拼图2.1 公开数据很难直接复用自建为主官方视频为辅交警手势识别没有像COCO那样标准的公开数据集网上能搜到的一些“yolo手势识别数据集”普遍存在三类问题样本量太小、标签体系不统一有人标8类有人标6类还有人把左转弯待转信号和左转弯信号混在一起、视频拍摄视角都是正面固定机位。如果你带着这些数据去做课程设计训练出来的模型一拿到实验室摄像头前就废因为角度、距离、光照全变了。所以我一般建议用“自建为主、官方宣传视频为辅”的路子。自建数据具体分两步找几段交警手势教学视频或者自己比划着录用手机三脚架固定机位分别拍正面、左侧面、右侧面三个角度然后按《道路交通安全法实施条例》规定的8种手势来建标签——停止信号、直行信号、左转弯信号、左转弯待转信号、右转弯信号、变道信号、减速慢行信号、示意车辆靠边停车信号。数据量的底线是每类150到300张有效单帧8类合计1500到2500张这足够训练一个能交差的小模型。如果条件允许每类做到500张配合数据增强泛化能力会明显上一个台阶。注意采集时人的身高、服装、距离要有点变化否则后面推理阶段一换环境就崩这个问题我在第五章展开讲。2.2 从视频里切出训练图片半小时做出一批干净样本拿到视频后第一步是切帧。这里有个关键细节不要每帧都存连续帧之间相似度太高训练时会让模型过拟合到背景上。我习惯每隔2到3帧抽一张这样同样的动作量下样本多样性会好很多。还要按标注好的时间段切只保留“手势开始到手势结束”这一段。切帧代码非常简单核心是一个循环加一个区间判断import os import csv import cv2 def load_annotations(csv_path): 读取人工标注的手势时间区间表 items [] with open(csv_path, newline, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: items.append({ start: int(row[start_frame]), end: int(row[end_frame]), label: row[label] }) return items def cut_frames(video_path, csv_path, save_dir, interval2): 按标注区间抽样出单帧图片 interval: 每隔几帧取一张, 建议2或3 cap cv2.VideoCapture(video_path) annotations load_annotations(csv_path) frame_idx 0 saved_count 0 while True: ret, frame cap.read() if not ret: break # 判断当前帧落在哪个手势区间 label None for ann in annotations: if ann[start] frame_idx ann[end]: label ann[label] break if label and frame_idx % interval 0: label_dir os.path.join(save_dir, label) os.makedirs(label_dir, exist_okTrue) cv2.imwrite(os.path.join(label_dir, f{saved_count:05d}.jpg), frame) saved_count 1 frame_idx 1 cap.release()这个脚本的参数就三个值得调interval控制抽样密度动作慢的手势比如“减速慢行信号”建议设成2动作快的“直行信号”设成3问题也不大save_dir下按标签自动建子目录后面做数据集划分时直接按目录操作start_frame和end_frame在做标注表时按视频帧号填千万不要填时间秒数OpenCV的read()是按帧走不按时间走。标注表CSV用Excel就能做三列就够了start_frame,end_frame,label。如果视频太长不好人工数帧号可以先用OpenCV把视频逐帧预览找到动作开始帧和结束帧的帧号再填表。这个过程很枯燥但值得耐心做因为标签边界错10帧切出来的图里就有大量“半动作”样本后面训练出来的模型会在动作过渡处反复误判。2.3 关键点标注Labelme标注后转成YOLO-pose格式有了单帧图片后下一步是标注人的骨架关键点。这里我建议直接用Labelme做多边形/点标注把交警的上半身关键点标出来。标注点数的选择有讲究做手势识别时双腿和脚踝基本用不上交警手势的动作语义集中在大臂、小臂、肩膀和头部所以标13个点或17个点都可以。我习惯用17个点顺序对齐COCO格式这样后面能直接套用YOLOv8-pose的预训练权重不用改网络输出头。Labelme导出的JSON格式里shapes数组每一项记录一个关键点的label和points坐标。转换到YOLO-pose格式时需要把每个点的像素坐标除以图片宽高归一化并且把所有点写在一行里行首是类别编号。import json import glob import os def labelme_to_yolo_pose(labelme_dir, out_txt_dir): 把Labelme标注的JSON转成YOLO-pose需要的txt格式 关键点顺序保持COCO 17点顺序, 类别编号0代表person os.makedirs(out_txt_dir, exist_okTrue) for json_path in glob.glob(os.path.join(labelme_dir, *.json)): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] shapes data[shapes] if not shapes: continue points shapes[0][points] # shapes[0]是person norm_points [] for x, y in points: norm_x round(x / img_w, 6) norm_y round(y / img_h, 6) norm_points.append(str(norm_x)) norm_points.append(str(norm_y)) out_name os.path.basename(json_path).replace(.json, .txt) out_path os.path.join(out_txt_dir, out_name) with open(out_path, w, encodingutf-8) as f: f.write(0 .join(norm_points) \n)转换脚本里最容易被忽略的就是坐标归一化。YOLO训练时会把图片自动缩放到imgsz指定的尺寸如果标注坐标还是原始像素值而非归一化值缩放后关键点位置全错模型根本收敛不了。另外Labelme里points的坐标顺序要和COCO约定一致比如第0个点是鼻子、第1个点是左眼、第2个点是右眼。如果标注时点乱了训练出来的模型关键点全是错位的这个错误在图上肉眼都看不出来只能靠画关键点连线检查。3. 关键点提取与训练用YOLOv8-pose跑通骨架识别3.1 三个主流方案对比YOLOv8-pose、MediaPipe、OpenPose怎么选关键点提取是这个项目的地基选型直接决定了后面时序分类的输入质量。目前做姿态估计有三个常用选择MediaPipe、OpenPose、YOLOv8-pose。很多人在网上搜“mediapipe手势识别”教程拿MediaPipe直接跑确实开箱即用但它的33个手部关键点做的是“手指级”识别交警手势需要的是“整条手臂和躯干”的骨架信息MediaPipe的人体姿态模式只有上半身贴合度还不错一旦动作幅度大比如左转弯信号手臂完全伸展关键点会抖动得很厉害。OpenPose是老牌方案但配置环境比较痛苦CPU推理帧率只有个位数答辩现场的电脑不一定扛得住。我最后的选型是YOLOv8-pose原因有三检测和姿态估计在同一个模型里完成不需要先跑一个行人检测器再跑姿态模型训练和推理接口统一换数据集只需改一个YAML文件平时大家搜“yolov8训练自己的数据集”时找到的部署资料也最多出了问题好查。3.2 基于YOLOv8-pose训练自己的关键点模型YAML和数据准备训练前要把数据集整理成YOLO格式的目录结构images/train放训练图片images/val放验证图片labels/train和labels/val放对应的txt标注文件。建议按8:2划分每一类的图片都要均匀分到两边不能按整个文件夹乱切。模型配置文件是我调试时改动最多的地方核心是kpt_shape和names# hand_pose.yaml path: ./dataset train: images/train val: images/val # COCO 17点, 每点包含(x, y, visible) kpt_shape: [17, 3] names: 0: person这里有个坑很多人会把[17, 2]当成默认配置但如果你的标注txt里只有归一化坐标没有visible标志训练时不报错模型输出的keypoints.data最后一维变成2后面提取关键点坐标的代码就要改。我建议标注时统一写17点没有的点用0或-1占位换到推理阶段再按置信度过滤。训练命令用Ultralytics的标准入口参数对新手友好yolo pose train \ datahand_pose.yaml \ modelyolov8n-pose.pt \ epochs120 \ imgsz640 \ batch16 \ device0几个参数值得解释一下。modelyolov8n-pose.pt是预训练权重它会把你标注的17点和COCO的17点对应起来千万不要用yolov8n.pt检测权重或者yolov8n-cls.pt分类权重它们没有姿态输出头。imgsz640是训练分辨率交警手势视频里人通常占画面比例不大直接降到640会丢细节如果显存够可以上imgsz896我实测能提升5到8个点的AP。batch16在6G显存以下要降到8否则会OOM。训练完成后看两个指标keypoints P和keypoints mAP。P到0.9以上、mAP50到0.85以上基本够用。如果P一直卡在0.7上不去回头查标注大概率是有几张图的点顺序错了。3.3 推理阶段的关键点预处理归一化、置信度过滤与可视点判断训练完模型进入推理阶段。推理不只是把模型跑的keypoints拿过来用还要做两步预处理过滤低置信度点然后把坐标从像素值转成相对位置特征。这一步做得不好后面LSTM学到的就是“某个像素位置的手势”换个人站远一点全废。from ultralytics import YOLO import numpy as np model YOLO(runs/pose/train/weights/best.pt) def extract_keypoints(frame, conf_threshold0.5): 从单帧提取17个关键点坐标 返回: (17, 3) 每行是(x, y, visible) results model(frame, verboseFalse) if not results or results[0].keypoints is None: return None kpts results[0].keypoints.data[0].cpu().numpy() # (17, 3) # 低置信度点直接置为不可见, 坐标置0, 避免把噪声喂给LSTM visible kpts[:, 2] kpts[visible conf_threshold, 0] 0 kpts[visible conf_threshold, 1] 0 kpts[visible conf_threshold, 2] 0 # 至少要有6个可见点, 否则认为这一帧没有有效的人 if (kpts[:, 2] 0).sum() 6: return None return kpts关键点归一化的方式是整个预处理里最影响效果的一步。常见做法是选两个稳定点做基准——我一般用左肩和右肩的中心点作为原点再除以肩宽这样能把不同身高、不同画面距离的人拉到同一个坐标系里。注意不要用图片宽度做归一化因为人站近站远会直接影响坐标尺度LSTM学出来的特征就不稳定。手肘和手腕的坐标要根据肩膀中心做平移而不是直接减图像中心点。4. 从关键点序列到指挥动作LSTM与滑窗投票的时序分类4.1 手势是时序动作为什么逐帧分类不可行拿单帧关键点直接分类十个里有九个会在“停止信号”和“左转弯待转信号”之间反复横跳。原因很简单这两个手势在某个瞬间的骨架形态几乎一样区别在于手臂是从“举起到放下”还是“保持不动”。也就是说交警手势的语义信息藏在“动作过程”里不在“某一帧的快照”里。所以这个项目的第二层模型要处理序列数据。一段手势从开始到结束通常持续0.5到2秒在30fps的摄像头下就是15到60帧。我采用的方案是固定30帧的滑窗每30帧关键点序列作为一条输入模型判断这30帧对应哪个手势。选LSTM而不是Transformer不是LSTM更好而是这个任务数据量只有几千条序列LSTM更容易收敛训练时间短答辩现场改参数重训也来得及。你要是用Transformer序列长度短根本发挥不出注意力优势还容易过拟合。4.2 34维输入、8类输出一个能跑通的LSTM模型输入维度17个关键点×2维坐标x和y共34维输出8类手势。我把网络定义和训练循环写在一起方便直接复用import torch import torch.nn as nn class GestureClassifier(nn.Module): 输入 (batch, seq_len30, input_dim34), 输出8类手势 def __init__(self, input_dim34, hidden_dim128, num_layers2, num_classes8): super().__init__() self.lstm nn.LSTM( input_dim, hidden_dim, num_layers, batch_firstTrue, dropout0.3 ) self.head nn.Sequential( nn.Linear(hidden_dim, 64), nn.ReLU(), nn.Dropout(0.5), nn.Linear(64, num_classes) ) def forward(self, x): out, _ self.lstm(x) last_hidden out[:, -1, :] # 取最后一个时间步 return self.head(last_hidden)这个模型有两个参数直接影响效果。hidden_dim128对8类手势足够加到256收益很小但训练时间翻倍。num_layers2是平衡点1层学不到复杂时序关系3层在这个数据量下容易过拟合。注意nn.LSTM里的dropout0.3只在层数大于1时生效所以至少用2层。训练循环用交叉熵损失加Adam优化器学习率调度用StepLR# train_loader返回 (batch, 30, 34) 的关键点序列和对应标签 optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.5) for epoch in range(80): model.train() total_loss 0 for seqs, labels in train_loader: optimizer.zero_grad() logits model(seqs) loss criterion(logits, labels) loss.backward() optimizer.step() total_loss loss.item() scheduler.step() print(fepoch {epoch:03d} loss {total_loss / len(train_loader):.4f})step_size30, gamma0.5的意思是每30个epoch学习率减半我试过不调学习率训练后期loss会上下波动。如果数据量少于2000条序列建议把epochs降到50防止过拟合。4.3 滑窗投票与置信度阈值不让识别结果来回跳LSTM输出的分类概率在单帧级别是不稳定的连续几帧可能一会儿判断成“停止信号”一会儿又跳成“变道信号”。这是因为相邻时间窗的内容几乎一样模型在边界处摇摆。解决办法是加一个投票器把过去15次预测结果放在一个队列里少数服从多数并且要求得票率超过60%才输出。from collections import Counter class GestureVoter: def __init__(self, window_size15, min_ratio0.6): self.window_size window_size self.min_ratio min_ratio self.history [] def update(self, gesture_id): self.history.append(gesture_id) if len(self.history) self.window_size: self.history.pop(0) def get_vote(self): if len(self.history) self.window_size // 2: return None counter Counter(self.history) best_id, count counter.most_common(1)[0] if count / len(self.history) self.min_ratio: return None return best_idwindow_size15在30fps下代表0.5秒的投票窗口这个长度对手势识别刚刚好太短滤不掉抖动太长会让动作切换的响应慢半拍。min_ratio0.6是经验值想更稳定就调到0.7但动作切换时的延迟会明显增加。实际使用中我还会在投票器前面加一道置信度门槛只有LSTM输出的最大概率超过0.4才进投票队列否则直接丢弃这一帧能滤掉很多背景噪声造成的误判。5. 避坑与常见问题5个让新手翻车的典型坑5.1 训练loss不降精度停留在20%上下现象LSTM训练跑了几十个epochloss稳定在2.0左右不下降准确率跟随机猜差不多。原因最常见的是关键点序列没有归一化。像素坐标直接喂给LSTM模型要去拟合“这个人站在画面左边还是右边”这种无关信息。其次是把不可见点的坐标填了0但可见点坐标是几百的像素值0和几百之间的数值差距让梯度更新失衡。解决把关键点坐标以肩部中心为原点做平移并除以肩宽。不可见点全部置0同时把可见点的x、y都做同样的平移和缩放变换。做好这一步loss通常在10个epoch内就能降到1以下。5.2 左右手镜像反转识别结果跟实际动作左右互换现象训练集和测试集都是正面拍的准确率很高一到答辩现场用摄像头实时演示左手动作被识别成右手动作。原因手机前置摄像头拍出来的画面是镜像的后置摄像头和电脑摄像头不是。如果你采集数据时用的是手机前置录像推理时用的是电脑摄像头左右手天然是反的。这是个很隐蔽的坑因为人眼看不出差异模型对左右手非常敏感。解决训练和推理统一摄像头类型。如果确实无法统一就在预处理阶段做一次水平翻转用cv2.flip(frame, 1)把画面镜像回来再做关键点提取。注意翻转后关键点的左右标签也要交换否则语义就乱了。5.3 显存溢出或内存持续上涨现象训练时CUDA out of memory或者推理时程序跑几分钟后内存占用一路飙升直到卡死。原因显存溢出通常是batch太大或者imgsz太高但我见过最奇葩的原因是有人在推理循环里不断调用模型而不释放中间变量。内存持续上涨多是被视频帧引用没释放cap.read()读的frame如果没有被后续处理替换旧帧一直留在内存里。解决训练时6G显存用batch8, imgsz64012G显存可以batch16, imgsz896。推理时把extract_keypoints里模型推理的结果及时转成numpy数组然后用del和gc.collect()清掉不再用的中间量。视频循环里每处理完一帧就把frame重新赋值不要保留引用。5.4 测试集准确率95%现场一换环境就全乱现象自己录的数据测出来接近满分换个教室、换个摄像头角度识别准确率掉到50%以下甚至某个手势永远识别不出来。原因这是数据集单一视角、单一背景导致的过拟合。我见过最快的翻车方式是训练数据全是在同一面白墙前录的模型表面在学手势实际在学“白墙固定机位下的人形轮廓”。换到实验室各种杂物背景、不同距离下骨架提取本身没问题但LSTM学到的关键点相对位置分布和现场不一样。解决采集数据时至少覆盖三个距离近、中、远、两种背景、两种光照。如果来不及补数据可以做数据增强对关键点序列加高斯噪声、随机缩放、随机偏移模拟不同距离和画面抖动。注意不要在序列维度上做时间反转那会让手势语义颠倒。5.5 答辩演示时环境崩溃模型加载失败现象在本地跑得好好的代码拿到答辩用的电脑上要么导入包报错要么模型权重路径找不到要么摄像头打不开。原因把项目从一台机器搬到另一台机器最常出问题的是相对路径缺失和Python环境不一致。很多人把权重文件放在runs/pose/train/weights/这种嵌套目录里一旦复制项目时漏掉一层代码就找不到文件。另外答辩机器没装GPU版本的PyTorch或者装的是另一个Python版本导入torch直接报错。解决交付前把项目做成“一个文件夹拖走就能跑”的形态权重文件和代码放同一级目录代码里用os.path.join(os.path.dirname(__file__), best.pt)定位。用Anaconda导出一个environment.yaml答辩前先在一台干净电脑上按这个文件重建环境跑一遍demo把缺失的包补齐再带去现场。6. 把模型包装成可演示的桌面工具摄像头实时识别与准确率验证6.1 PyQt5实时识别界面从摄像头取流到输出八种手势模型训练好了最后一步是封装成能现场演示的工具。我习惯用PyQt5做界面逻辑简单清晰OpenCV负责取流YOLO模型抽骨架LSTM分类结果用标签控件显示比写终端版demo的观感好很多。import cv2 import torch import numpy as np from PyQt5 import QtWidgets, QtGui # 初始化模型 keypoint_model YOLO(best_pose.pt) gesture_model GestureClassifier() gesture_model.load_state_dict(torch.load(gesture_lstm.pth, map_locationcpu)) gesture_model.eval() voter GestureVoter() seq_buffer [] def process_frame(frame): kpts extract_keypoints(frame) if kpts is not None: seq_buffer.append(normalize_keypoints(kpts).flatten()) else: seq_buffer.append(np.zeros(34, dtypenp.float32)) if len(seq_buffer) 30: seq_buffer.pop(0) if len(seq_buffer) 30: seq torch.tensor(np.array(seq_buffer), dtypetorch.float32).unsqueeze(0) with torch.no_grad(): probs torch.softmax(gesture_model(seq), dim1) gesture_id int(torch.argmax(probs[0])) conf float(torch.max(probs[0])) if conf 0.4: voter.update(gesture_id) result voter.get_vote() if result is not None: cv2.putText(frame, CLASS_NAMES[result], (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)这段代码里有三个交付要点map_locationcpu确保目标机器没有GPU也能加载权重seq_buffer只保留最近30帧避免内存膨胀投票器输出前必须经过conf 0.4过滤否则背景状态下模型也会强行输出一个手势界面上会一直跳字。6.2 验收方法建议每个手势录30段测试视频最后别急着交差先自己验收一轮。我建议每种手势录30段不同角度、不同距离的测试视频每段2秒左右。跑一遍完整流程统计两类指标一是单段视频内投票器输出的正确率二是动作切换延迟——也就是从停止信号切到直行信号界面需要多久才跟着变。正确率90%以上、延迟不超过1秒这个项目拿去答辩就有底气了。这整套流程我前后带过几届学生走最大的体会是这个题目拿到一个“能跑出结果”的模型不难难的是现场演示稳定不翻车。数据集采集和预处理阶段多花的每一小时最后都会在答辩现场还给你。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表