简介基于USTC数据集、整合MediaPipe与YOLOv5算法的手语视频识别系统Python源码面向计算机视觉学习者和手势识别项目开发者可用于复现手语识别流程、理解视频级姿态估计与分类模型的串联方式也可作为毕业设计或课程项目的参考工程。代码共42个文件其中19个py脚本覆盖主程序、界面逻辑、模型加载和工具函数4个ui文件对应交互窗口6个png图片用于界面图标另有avi测试视频、XML配置、说明文档和词典文本整体压缩包约13.77MB。项目包含RNN.py、Holistic.py、Hands.py、PoseClassify.py等核心模块以及手部抬起检测、画图、词典处理等辅助脚本配有测试视频便于直接运行和验证也能帮助读者掌握MediaPipe关键点提取与YOLOv5目标检测在手语场景中的集成思路。目前已有219人学习适合需要参考完整工程结构或进行二次开发的入门及中级学习者。1. 手语视频识别为什么绕不开USTC数据集、MediaPipe和YOLOv5这条路线把手语视频识别当成“摄像头拍到手在动然后翻译成文字”听起来简单真做起来十个项目有九个会翻车——直接拿视频帧丢给3D CNN分类背景稍微复杂一点、手部动作再快一点准确率就从九成掉到六成。而基于USTC数据集、MediaPipe和YOLOv5算法实现的手语视频识别系统避开“整段视频端到端学习”的黑匣子思路先用MediaPipe在人手视频帧上抽手部骨架关键点把高维视觉信息压成21个点的坐标再把关键点所在的局部区域交给YOLOv5做检测与分类。这条路的好处是每一步都能肉眼检查、能单独调参适合做HCI方向的研究生、无障碍产品开发工程师还有想把手势控制接到嵌入式或树莓派上的人。本文按这个标题把整条链路拆开讲清楚照着做就能跑通。2. 从视频到关键点MediaPipe在USTC手语数据集上的预处理与特征提取2.1 为什么是MediaPipe而不是OpenPose或端到端3D CNN手语视频识别里最费劲的不是分类模型而是怎么把手部动作“干净地”从视频帧里捞出来。早期做法依赖OpenPose的hand模块做关键点检测但OpenPose设计上是先检测全身骨骼再细化出手部如果镜头只对准手部特写、画面里没有完整人体它就经常罢工。端到端3D CNN看着省事但要吃大量成对标注视频训练完还是个黑匣子某个手势错了根本说不清是背景干扰还是动作理解错位。MediaPipe Hands的优势在于它是一条独立的轻量级关键点检测管线不依赖全身姿态模型尺寸只有几MB普通CPU上都能跑到20FPS以上。它在一帧里输出每只手的21个关键点每个点带归一化的x、y和相对深度z坐标系是以手腕为原点的局部坐标。这个“局部坐标”对手语识别非常关键——手在画面里左右平移时全局坐标全都在变但手语语义往往由手指相对姿态决定局部坐标天然把平移这个干扰消掉了。还有一个工程上的好处MediaPipe的检测是全平台可跑的Windows、Linux、树莓派都有预编译轮子不用自己编译CUDA版OpenPose。这正好对上标题里的“python源码.zip”这种交付方式——拿到源码后只要pip安装mediapipe和torch就能把整个流程拉起来。2.2 用MediaPipe批量抽取USTC视频的手部关键点完整脚本我一般会先把USTC数据集按词条整理成“每个视频对应一个类别标签”的目录然后写一个批量抽关键点的脚本把每个视频逐帧过MediaPipe。别着急上YOLOv5先保证关键点数据是完整、可复现的。import cv2 import mediapipe as mp import numpy as np import json import os mp_hands mp.solutions.hands hands mp_hands.Hands( static_image_modeFalse, # 输入是视频流用跟踪模式 max_num_hands2, model_complexity1, # 0轻量 1完整手语建议用1 min_detection_confidence0.65, # 低于该置信度视为漏检 min_tracking_confidence0.5 ) def extract_frames(video_path, sample_stride2): cap cv2.VideoCapture(video_path) frames [] idx 0 while True: ret, frame cap.read() if not ret: break if idx % sample_stride 0: # 每2帧取1帧降低冗余 frames.append(frame) idx 1 cap.release() return frames def process_video(video_path, output_json): frames extract_frames(video_path, sample_stride2) records [] for frame in frames: rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results hands.process(rgb) if not results.multi_hand_landmarks: records.append({landmarks: None, bbox: None}) continue hand results.multi_hand_landmarks[0] lm_list [[lm.x, lm.y, lm.z] for lm in hand.landmark] xs [lm.x for lm in hand.landmark] ys [lm.y for lm in hand.landmark] # bbox 是归一化坐标下的手部外接矩形 bbox [min(xs), min(ys), max(xs), max(ys)] records.append({landmarks: lm_list, bbox: bbox}) with open(output_json, w, encodingutf-8) as f: json.dump(records, f, ensure_asciiFalse) print(fdone: {video_path}, valid frames: {sum(1 for r in records if r[landmarks])})这段代码做的事情很简单读视频、按步长抽帧、逐帧提取手部21个关键点和外接框最后把整个词条的结果存成一个JSON文件。逻辑上要关注三点sample_stride设为2是为了减少相邻帧的冗余动作手语一个动作周期通常跨20到40帧每2帧取1帧仍能保住动作连贯性model_complexity1在CPU上每帧大约12ms如果笔记本比较老可以降到0但手指交叉这种细粒度手势容易丢关键点min_detection_confidence0.65是经验值设得太高手部快速运动时漏检率飙升太低则会把类似手型的背景区域误判成手。2.3 USTC数据集的目录结构、采样策略与特征保存格式我拿到过的USTC手语词条库是按动作词分类的内部结构一般是“类别名/若干样本视频”的布局。每个类别下会有多个视频每个视频长度约1到3秒拍摄背景相对单一。但单一背景不等于光照不变离摄像头近的帧和远一点的帧手部尺度差异很大这会影响YOLOv5的检测效果。所以我在处理时会做三件额外的事第一把MediaPipe漏检率高于30%的视频直接标记出来重建不硬凑。第二按视频的FPS动态决定抽帧步长而不是整批用同一个stride——如果源视频是30FPS取stride2如果是15FPSstride就得降到1否则关键动作帧会被跳过去。第三关键点坐标保存为JSONbbox坐标保持归一化之后转YOLO标签时不会引入像素级误差。这里还要留一个心眼MediaPipe每帧只输出单帧的关键点它不会告诉你“这个动作属于哪个手语词”。语义标签要由上游的目录名绑定。我在代码里会把视频对应的类名写进输出文件的父目录名里例如json/talk/001.json这样后续生成YOLOv5标签时只需遍历目录即可。3. 把关键点喂给YOLOv5训练数据转换与模型选型3.1 YOLOv5网络结构与手语目标检测任务的匹配点YOLOv5能频繁出现在各类识别项目中不只是因为它“开箱即用”。它的网络结构分三段Backbone用CSPDarknet提取特征Neck用FPNPAN把不同尺度特征融合Head输出三个不同尺度的检测头。这个结构对手语识别的好处在于手部动作跨尺度很大——一个拇指轻触食指的动作在整个画面里可能只占30×30像素而整个手掌张开能占到300×300像素。三个检测头分别负责小、中、大目标能同时照顾这两种极端。即便你说“我已经用MediaPipe提取了关键点为什么还要上YOLOv5”——因为YOLOv5在这里承担的是“找出手在哪并把手势类别分出来”的任务。MediaPipe的关键点告诉我们手部的姿态结构但仅凭关键点坐标做分类仍然受背景框偏移和手部运动轨迹影响把关键点连同原始帧的局部区域喂给YOLOv5让它在带纹理的RGB小图上做检测分类鲁棒性会明显更好。我常见的一个方案是训练YOLOv5检测手语手势类别类别标签就是USTC数据集的词条名。检测目标不是“手掌”这种泛化概念而是具体的动作类别例如“你好”“谢谢”“再见”各算一个类。这样推理时模型的输出框直接对应“识别结果”本身省掉分类头再拼一层的工程复杂度。3.2 关键点坐标转YOLO标签格式的转换脚本与边界处理YOLOv5的训练标签格式是每个图像对应一个同名txt文本每行写“类别id 中心x 中心y 框宽 框高”全部归一化到0到1之间。我们可以从JSON里拿到手部的bbox然后生成对应的txt文件。同时要把检测区域从整帧缩小到手部周围这样YOLOv5学到的目标更集中。import os import json # 输入上一个阶段生成的json目录 视频抽帧保存为图片的目录 # 输出YOLOv5格式的images/ 和 labels/ 两个文件夹 def json_to_yolo(json_path, label_dir): with open(json_path, encodingutf-8) as f: records json.load(f) lines [] for i, rec in enumerate(records): if rec[bbox] is None: continue x_min, y_min, x_max, y_max rec[bbox] # 边界裁剪归一化坐标可能因手部运动出界 x_min max(0.0, x_min) y_min max(0.0, y_min) x_max min(1.0, x_max) y_max min(1.0, y_max) if x_max - x_min 0.05 or y_max - y_min 0.05: continue # 框太小视为无效检测 x_center (x_min x_max) / 2 y_center (y_min y_max) / 2 width x_max - x_min height y_max - y_min lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) label_file os.path.join(label_dir, os.path.basename(json_path).replace(.json, .txt)) with open(label_file, w, encodingutf-8) as f: f.write(\n.join(lines))在跑这个脚本之前要先确定class_id的编号我习惯在训练前把类别名排序后生成一个classes.txt再逐类别映射。代码里裁剪和最小框过滤两条规则是血泪经验MediaPipe在某些帧里会把手指尖算出负坐标不做clip会产生大量越界框框宽小于0.05归一化值的检测基本是误检和抖动喂进YOLOv5只会拉低mAP。还要注意一个容易忽略的点每张用于训练的图片必须跟JSON里抽帧顺序完全对应。我常看到有人单独保存图片和标签但序列号对不上训练时模型看到的是“图片是猫标签是狗”这种错位数据loss根本不收敛。这里的解决办法是让图片的命名规则跟JSON的文件名完全一致用同一个遍历顺序生成。3.3 YOLOv5四个必调超参数与经验初始值初始训练我不会去动网络结构只调四个超参数第一是imgUSTC视频原始帧通常是1920×1080或1280×720直接训练显存吃紧统一缩放到640×640手部区域小别降到320否则小手势细节全被压没了。第二是batch在显存允许范围内尽量大12GB显存跑YOLOv5s用batch32没问题。第三是epochs手语数据集的类别多但每类样本少我一般先跑150个epochs开着早停观察验证集mAP。第四是hyp超参文件里的lr0用默认的0.01就行但一旦发现训练初期loss震荡特别大就降到0.005重新来。“训练自己的数据集”时最常见的翻车点是类别不平衡。USTC里“谢谢”这种高频词可能有几十段视频“犹豫”这类低频词可能只有五六段YOLOv5默认按随机采样低频类训练不足。我在训练前会对低频类做复制增强——对同一视频提取的关键点JSON做轻微坐标扰动生成额外训练样本而不是直接复制图片这样能维持视觉多样性。训练时把权重文件初始化为yolov5s.pt这是官方在COCO上预训练过的模型。手部检测任务跟COCO里的“person”类有语义重叠迁移学习能显著加速收敛从零训练的话模型需要更多epochs才能让Backbone学会基础边缘特征浪费时间。4. 训练自己的手语识别模型数据划分、命令行参数与断点续训4.1 数据集目录组织与train/val划分YOLOv5训练时通过一个yaml文件告诉它数据在哪目录组织按官方约定images/train、images/val、labels/train、labels/val。手语视频抽帧后图片量很大我一般按9:1随机划分同时保证同一视频的帧不会被同时分到训练集和验证集——否则验证集里全是训练集的“复读”mAP虚高到0.95以上一到真实摄像头就现原形。# hand_sign.yaml train: ./datasets/USTC_hand/images/train val: ./datasets/USTC_hand/images/val nc: 20 names: [hello, thanks, bye, sorry, good, morning, night, please, yes, no, who, what, why, when, which, how, old, new, hurry, wait]这个yaml文件里的nc必须跟names列表长度一致不然YOLOv5会直接报错。names顺序需要跟之前生成标签txt时用的class_id顺序完全对应。我常看到有人改了类别顺序没重新生成标签结果“hello”的框被标成“thanks”训练完推理结果全乱套。这里建议把所有类别的标签生成和yaml文件都锁在同一个配置脚本里避免手工维护两处映射。4.2 用train.py启动训练命令行参数逐项拆解进入yolov5目录执行下面的命令启动训练。注意先确认当前环境里torch版本和CUDA版本匹配否则代码不报错训练速度也会慢得离谱。python train.py \ --data hand_sign.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 32 \ --epochs 150 \ --patience 20 \ --cache ram \ --name hand_sign_run1逐项拆解这些参数--data指定刚才写好的yaml文件--weights用官方预训练的COCO权重做冷启动--img 640是训练输入尺寸不匹配时YOLOv5会自动resize代价是手部小目标信息损失--batch 32回合金稳--epochs 150保证低频类别也能看到足够多轮次--patience 20是早停耐心值验证集mAP连续20个epoch不涨就自动停省电省时间--cache ram把训练图片预加载到内存里如果机器内存小于32GB建议改--cache disk否则内存爆掉进程被杀。训练第一个epoch时我会盯着三个东西Loss值曲线、mAP_0.5、mAP_0.5:0.95。如果loss逐渐下降但mAP在30个epoch内纹丝不动多半是标签和图片对应关系错了马上停下去检查数据不要盲目加大epochs。如果mAP爬上去了但到某个数值后开始震荡说明学习率太高或者数据里的噪声样本太多可以调--cos-lr让学习率按余弦曲线衰减通常能把震荡压下来。4.3 训练中断、loss不降、权重大小怎么选断点续训与评估指标训练跑到一半停电或显存被别的进程抢走是家常便饭。YOLOv5会在runs/train/hand_sign_run1/weights/里留下last.pt和best.pt两个权重last.pt是每个epoch末尾都会覆盖的最新权重best.pt是验证集mAP最高的一次。续训时执行python train.py \ --data hand_sign.yaml \ --weights runs/train/hand_sign_run1/weights/last.pt \ --img 640 \ --batch 32 \ --epochs 150 \ --resume--resume会从runs/train/hand_sign_run1读取上次训练的epoch数、优化器状态和学习率调度不需要人工改--epochs用户只传同一个--name路径即可。注意续训时如果换了batch大小某些优化器的统计量会错位建议保持batch不变。评估时不只看mAP。手语识别讲的是“实时性”还要关注单帧推理延迟和漏检率。我习惯在验证集里单独统计“连续N帧关键点全部丢失”的片段数因为一个动作中哪怕有3帧没检出下游分类结果就会断掉。YOLOv5的val.py输出的是mAP相关指标单帧延迟要自己跑一段推理脚本计时这个放到第6章专门讲。训练完取最优权重时我一般用best.pt它是验证集mAP最高的泛化性比last.pt稳。除非你的数据划分有误否则last.pt在训练末期的mAP可能跟best.pt接近但可能发生过拟合所以保险起见永远用best.pt做推理和部署。5. 避坑与排查MediaPipe漏检到YOLOv5收敛异常的5条踩坑记录5.1 MediaPipe在快速手部运动帧上漏检关键点序列断了一截现象抽帧时某个词条的视频有一大半帧的multi_hand_landmarks是None导致后续JSON里没数据YOLOv5训练样本数量锐减。 原因MediaPipe的跟踪模式对当前帧依赖前一帧的手部位置如果手部动作速度太快跟踪框跟不上检测置信度跌破阈值。 解决把min_detection_confidence从0.5降到0.4如果仍然漏检明显改用static_image_modeTrue逐帧单帧检测牺牲速度换稳定。手语视频中快速的手指翻转很常见我最后是混合模式——每隔5帧做一次静态检测来“重置”跟踪状态中间帧用跟踪模式补全。5.2 YOLOv5训练loss下降但mAP一直低背景框把正样本淹没了现象训练loss降得挺漂亮但验证集mAP_0.5始终在0.3以下。 原因训练集中大量图片的标签框是手部的小区域但全尺寸640图上背景占了大部分面积模型学到的是“把背景当目标”也能降低loss因为背景框多负样本在损失函数里占比太大。 解决数据预处理时用手部bbox裁剪出局部区域只用裁剪区域参与训练图片尺寸保持640不变同时开启YOLOv5的--noautoanchor并手动设置锚框。手语手势的框形状相对固定默认锚框是按COCO的宽高比设计的不匹配导致正样本的IoU普遍低于阈值。5.3 标签归一化坐标越界导致训练报错现象训练中断提示assertion failed: 0 x 1或者出现负的loss。 原因MediaPipe在手指指向画面边缘时归一化坐标会出现小于0或大于1的值如果第3.2节转标签时没做clip这些越界值直接写进txtYOLOv5加载标签时会出数组越界。 解决第一道防线是转换脚本里加clip第二道防线是训完第一个epoch后立即检查runs/train/exp/labels.jpg和train_batch*.jpg图片上的锚框如果画到了图像外面说明剪裁还没做干净回炉重新转标签。5.4 抽帧关键点数据文件数量巨大训练加载卡顿现象数据准备完发现图片有几十万张启动训练要等两三个小时一个epoch都要跑二十多分钟。 原因大量冗余帧导致图片和标签数量爆炸而YOLOv5默认在训练启动时会对全部图片做cache磁盘IO和内存吃紧。 解决不要整段视频全部抽帧改成按动作关键帧抽——手语一个词条动作的起、中、止三帧代表全部语义意图再加上中间过渡帧每段视频抽8到12帧就够。同时用--cache disk代替--cache ram避免内存被占满触发OOM killer。如果数据仍然太多就按类别均衡地欠采样别追求把所有帧都硬塞给模型。5.5 GPU显存占用率不高但训练速度慢数据加载在拖后腿现象nvidia-smi能看到显存占用70%上下但GPU利用率只有30%训练每epoch耗时特别长。 原因数据管线和GPU训练不同步CPU实时解码JPEG、做Mosaic增强的耗时超过了GPU前向和反向计算的耗时即使开了--cache ram如果机器内存不够缓存会频繁触发页交换。 解决缩小数据规模人均样本量控制在每类100到300张图在yolov5的data/hyps配置文件里把mosaic从默认的1.0降到0.5减少Mosaic拼接的计算量检查一下机器内存按一个样本约0.5MB估算12GB显存对应需要至少64GB内存才能顺畅跑--cache ram。6. 把模型落到摄像头实时推理FPS验证与结果落盘6.1 用detect.py和自训练权重跑通摄像头视频流YOLOv5自带detect.py脚本直接用训练好的best.pt跑摄像头视频流python detect.py \ --weights runs/train/hand_sign_run1/weights/best.pt \ --source 0 \ --conf-thres 0.35 \ --iou-thres 0.45 \ --view-img \ --save-crop--source 0调用笔记本摄像头--conf-thres 0.35是识别置信度阈值手语手势本身动作幅度大阈值设太高容易漏掉真正的动作0.35是个比较稳的起点--save-crop会把手部检测框的区域单独裁剪保存这能帮你验证模型到底看到了什么。跑起来就能看到画面里的手势框和类别标签实测单帧推理时间在GTX 1660上大概15到20ms加上摄像头取帧的干扰整体FPS能维持在30左右。如果FPS低于15最常见的原因是摄像头取帧分辨率太高直接把--imgsz 416调低推理分辨率能明显提速。6.2 验证手语识别系统效果的三个技巧第一个技巧是录制自己的动作去测不要只用USTC数据集的视频。USTC采集时背景干净你的办公室或客厅环境完全不同——光照变弱、手部有投影、背景有纹理都可能导致MediaPipe漏检进而让YOLOv5看不到手。我自己的习惯是先在目标环境录10个手势各20次跑一遍完整流程统计F1分数而不是只看准确率。第二个技巧是时序验证。手语是靠动作过程表达语义的单个静态帧的识别率再高动作中间帧出错也会导致整个词条错乱。我在推理脚本里加了一个三帧投票逻辑连续三帧里出现同一手势类别两次才把这个词条判为有效。这个小技巧能过滤掉大部分因运动模糊导致的单帧误检。第三个技巧是混合MediaPipe的骨架可视化叠加到YOLOv5的检测框上做成一个“既能看框又能看骨架”的调试视图。两套几何信息交叉验证能快速定位错误出现在哪一步如果骨架画出来是乱的但YOLOv5框没乱说明MediaPipe的跟踪出了问题如果骨架正常但框分类错了问题出在YOLOv5的数据标签上而不是推理流程上。这套经验也是在我自己的项目里反复折腾出来的先单独调通MediaPipe抽帧再上YOLOv5训练每一步都肉眼检查中间结果很多“玄学”问题其实都是数据管线的低级错误。希望帮到你。本文还有配套的精品资源点击获取