
简介面向手语识别与人体姿态估计研究者的完整项目资料包基于OpenPose与YOLOv3实现视频/图像中的手语动作识别。资源代码覆盖视频关键帧提取、手部目标检测、手势特征解析与分类器预测等环节同时提供PyQt界面设计文件、模型文件及相关依赖说明便于在Windows环境下复现和改造。包内共42个文件以Python脚本为主辅以模型pkl、UI的fbp/ico、示例图片及依赖清单txt等压缩包大小1.46MB结构清晰。目前已有416人学习下载。对于正在做毕业设计或图像识别项目入门的学生可直接参考其数据预处理、特征工程和界面集成思路减少重复编码工作开发者也能在此基础上扩展实时摄像头识别、移动端应用等功能整体性价比较高。1. 为什么手语识别要选 OpenPose YOLOv3 双模型手语识别一直是人体动作识别里最刁钻的分支上半身动作变化有限真正决定语义的是手指关节在几十毫秒内的位移。如果只用 OpenPose 直接对手部做关键点估计在 640x480 的原始帧里一只手掌往往只占 50x50 像素21 个关键点很容易被背景或者手臂遮挡带偏如果只用 YOLOv3 做手部检测能得到高置信度的边框却拿不到关节角度、指间距离这些能区分“比一”和“比八”的细粒度特征。这个项目把两个模型串成一条流水线先让自训练的 YOLOv3 手部检测模型在整帧上把人手框出来再把裁剪后的高分辨率手部图像送进 OpenPose 提取关键点最后把关键点坐标、相对距离、角度等数字特征做成向量用朴素贝叶斯分类器做预测结果以文本形式显示在 wxFormBuilder 搭建的桌面界面上。我复现过一版跑在 Windows10 i5-8300H 8G 内存的笔记本上视频抽帧和模型预测都能跑但每一步的路径配置和特征对齐才是真正的坑。下文会按“检测原理 → 视频预处理 → 特征与分类器 → 联调部署”的顺序拆开讲适合已经跑通过 OpenPose 官方示例、想往具体动作识别落地的读者。2. OpenPose 手部关键点与 YOLOv3 手部检测的协同原理2.1 自顶向下与自底向上的取舍OpenPose 官方的手部关键点模型用的是自底向上Bottom-Up思路先在整张图上预测所有关键点的热图和 Part Affinity Fields再把属于同一个手的点连接起来。这种方式的优势是推理速度不随人数线性增长但在手部目标极小、且手指出现自我遮挡时PAF 的连边质量会明显下降。YOLOv3 属于自顶向下Top-Down先通过骨干网络提取特征在三个尺度上输出边框回归和类别概率再把检测到的手部区域裁剪出来送入下一步。两者结合后YOLOv3 承担“注意力机制”的角色相当于先告诉 OpenPose “人手的候选区域在哪里”把关键点估计的问题从“全图搜索”降维成“局部细分类”。对比维度OpenPose 手部关键点YOLOv3 手部检测输出21 个关键点热图 PAF手部边框 置信度对目标尺寸要求需要 128x128 以上输入可以检测 32x32 的小目标对遮挡处理手指遮挡时关键点易漂移遮挡时边框仍可靠计算开销关键点分支较重单帧检测较快串联方式消费裁剪后的手部图提供手部区域项目里手部检测模型是基于 YOLOv3 自训练的不是 OpenPose 自带的手部检测器。这样做的原因是OpenPose 的 hand_detector 本质是 Faster R-CNN推理速度较慢换用 YOLOv3 后可以用更少的计算资源获得重叠手部场景下的稳定边界框。在yolo.py里可以看到这个模型被封装成一个YOLO类初始化时加载yolo3目录下的model.py和utils.py这两份文件就是 Darknet53 骨干和 YOLO 层的前向计算代码。2.2 yolo.py 中的手部框检测yolo.py是整套系统的入口之一它的作用是把视频帧或者图片中的手部区域检测出来。实际调用时我一般这么用from yolo import YOLO yolo YOLO() image cv2.imread(hand_sample.jpg) boxes, scores, classes yolo.detect_image(image, draw_boxesTrue)detect_image内部会先把图像按letterbox方式缩放保持宽高比并填充到模型输入尺寸然后通过model.predict拿到三个尺度的预测结果。关键参数在YOLO类的__init__里class YOLO: def __init__(self, model_pathmodel_data/hand_weights.h5, anchors_pathmodel_data/yolo_anchors.txt, classes_pathmodel_data/hand_classes.txt, score_threshold0.5, iou_threshold0.45):score_threshold控制在多少置信度以上才认为是手0.5 是常见初始值。如果你的测试环境里有大量半遮挡手部我会建议降到 0.35否则很多边缘手部会被漏掉反之画面干净且只有一个手时0.6 以上能减少误检。iou_threshold用于非极大值抑制重叠度超过这个值的两个框会合并一般保持 0.45 即可。检测得到的手部框是(x1, y1, x2, y2)的像素坐标接下来不能直接送进 OpenPose要先扩展边框把手指尖可能超出原始框的部分包括进去。我通常会在上下左右各扩 20% 的边距然后截取 ROI再缩放到 OpenPose 要求的输入尺寸否则中指和食指顶端的关键点会被切掉。2.3 pose_hand.py 关键点提取与特征pose_hand.py封装了 OpenPose 手部关键点推理。注意项目里 OpenPose 是用 C 编译的通过 CMake 生成 Python 模块所以在 Python 里调用时实际上是加载编译后的pyopenpose动态库。pose_hand.py的核心逻辑是import pyopenpose as op opWrapper op.WrapperPython() params dict() params[model_folder] models/pose/ params[hand] True # 开启手部关键点 params[hand_net_resolution] 256x256 opWrapper.configure(params) opWrapper.start() datum op.Datum() datum.cvInputData hand_roi # 来自 yolo.py 裁剪且扩展后的手部图像 opWrapper.emplaceAndPop([datum]) hand_keypoints datum.handKeypoints[0] # 21点 x 3 (x,y,score)这里有个容易被忽略的细节hand_net_resolution的宽高顺序是 “宽x高”不是 OpenCV 的 “高x宽”。如果填成256x256OpenPose 会把这个分辨率作为输入热图的大小如果填反输出关键点的坐标尺度会对不上原图。datum.handKeypoints[0]的形状是(1, 21, 3)其中第 3 维的置信度小于 0.1 的点通常是不可靠点在特征提取时应该直接丢弃而不是当作坐标 0 处理否则会污染距离特征。拿到 21 个关键点后我通常构造三类特征关键点归一化坐标相对腕关节的偏移、相邻关键点对的距离、关键点向量与手掌主方向的夹角。这些手写特征相比直接把原始坐标压平对图像平移和手部尺寸变化更鲁棒。下一章会先解决这些特征从哪来视频抽帧和图像预处理。3. 视频预处理ffmpeg 切片与 OpenCV 关键帧提取3.1 videoConv.bat 的批处理参数项目里的原始手语视频大多来自手机或笔记本摄像头分辨率可能是 1920x1080帧率 30fps。如果直接对每一帧跑 YOLOv3 OpenPose在只有 CPU 的环境下一秒钟视频要处理几分钟。因此第一步是用 ffmpeg 把视频做裁剪、降分辨率和降低帧率。videoConv.bat是一个 Windows 批处理脚本我拆开看过的典型内容是这样echo off for %%f in (raw\*.mp4) do ( ffmpeg -i %%f -vf scale640:480,fps15 -c:v libx264 -preset fast prepared\%%~nf.mp4 )这里scale640:480强制把视频统一到 640x480fps15把帧率降到 15 帧每秒。手语动作频率不会超过 8Hz15fps 足够保留关键运动信息同时能让后续 OpenCV 处理压力减少一半。-c:v libx264重新编码-preset fast是速度和压缩率的折中。如果你不想改变原视频编码可以用-c:v copy但那要求 scale 和 fps 都不变所以这里不能省编码。ffmpeg 在该项目里作为独立视频处理工具所有 Python 脚本都通过subprocess调用命令行来完成视频解析而不是直接用 OpenCV 的 VideoCapture。原因是 OpenCV 自带的 FFmpeg 后端对不同编码格式兼容性不稳定尤其是手机录制的 HEVCH.265视频经常出现读取失败或时间戳错位。用系统独立安装的 ffmpeg 可以提前统一格式避免这种脏数据问题。3.2 getKeyFrame.py 按帧差法抽帧不是每个视频帧都值得送入识别模型。手语动作中有大量静止帧比如手停在空中准备下一个词汇。getKeyFrame.py的作用是把包含明显运动的帧挑选出来。它的核心逻辑是计算相邻帧的绝对差之和超过阈值就保留import cv2 import numpy as np cap cv2.VideoCapture(prepared/word_hello.mp4) ret, prev_frame cap.read() prev_gray cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY) threshold 1500 frame_idx 0 while True: ret, cur_frame cap.read() if not ret: break cur_gray cv2.cvtColor(cur_frame, cv2.COLOR_BGR2GRAY) diff cv2.absdiff(cur_gray, prev_gray) score np.sum(diff) / (diff.shape[0] * diff.shape[1]) if score threshold: cv2.imwrite(fkeyframes/{frame_idx}.jpg, cur_frame) prev_gray cur_gray frame_idx 1这里的score是相邻帧像素差的均值。手语动作下手的移动会导致手部区域像素差很大但背景几乎不变所以阈值 1500 在 640x480 分辨率下能有效抓住运动段。若你的视频分辨率改了阈值要按像素总数等比缩放比如 1920x1080 分辨率下阈值应该乘上 (19201080)/(640480) 约 6.75 倍。另外我强烈建议在cv2.absdiff前用高斯模糊做一次去噪cur_gray cv2.GaussianBlur(cur_gray, (5, 5), 0)否则摄像头传感器噪声会把score整体抬高静止帧也会被当作关键帧提取出来后续识别时产生大量重复样本。3.3 批量缩放与重命名脚本项目里还有resizevideos.py、resizefiles.py、renamefiles.py和removevideo.py。这几个脚本是为了整理样本集。resizevideos.py用 OpenCV 逐帧缩放视频等价于 ffmpeg 的 scale 参数resizefiles.py则处理已经抽出来的关键帧图片。renamefiles.py解决的是文件名乱序问题比如手机录制的视频叫VID_20181101_102345.mp4按字典序排列会得到 10、11、12 排在 2 之前的错误顺序脚本会重命名为001.mp4、002.mp4这种格式。renamefiles.py的关键逻辑import glob import os files glob.glob(keyframes/*.jpg) files.sort(keylambda x: int(os.path.splitext(os.path.basename(x))[0])) for idx, f in enumerate(files, 1): os.rename(f, fkeyframes/{idx:03d}.jpg)这里强制用零填充的三位数字作为新文件名保证glob.glob返回的字典序和实际时间顺序一致。如果省略这段后续训练集和测试集划分时会出现错位比如第 10 帧被当成第 2 帧使用。4. 特征提取与朴素贝叶斯分类器训练4.1 get_features.py 特征向量构造关键帧图像准备好后get_features.py会逐一执行 YOLOv3 检测、OpenPose 关键点提取然后构造特征向量。我这里将特征构造写成可复用的函数import numpy as np def build_feature(hand_pts, wrist_idx0): # hand_pts: (21, 3) 第三维是置信度 wrist hand_pts[wrist_idx, :2] features [] # 1) 关键点相对腕关节的偏移 for i in range(1, 21): if hand_pts[i, 2] 0.1: features.extend([0, 0]) else: offset hand_pts[i, :2] - wrist features.extend(offset) # 2) 关键点与腕关节的距离 for i in range(1, 21): dist np.linalg.norm(hand_pts[i, :2] - wrist) features.append(dist) # 3) 同一根手指相邻关节的夹角以中指为例 mcp hand_pts[9, :2] pip hand_pts[10, :2] dip hand_pts[11, :2] v1 pip - mcp v2 dip - pip cos_angle np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) 1e-6) features.append(cos_angle) return np.array(features, dtypenp.float32)这段代码体现了数字特征的三个层面坐标偏移保留空间位置距离特征对平移不变角度特征对缩放和旋转部分不变。注意置信度小于 0.1 的关键点直接填 0 向量而不是填入错误坐标否则分类器会学到噪声。实际上项目中get_features.py还会把 OpenPose 输出的 21 点坐标先除以手部 ROI 的宽高归一化到 01 之间这一步非常重要因为 YOLOv3 裁剪出来的 ROI 尺寸每次都不固定如果不归一化同一个手势在不同距离下特征值差异巨大。特征向量的维度取决于你选取了多少组角度和距离。上面这份代码构造出的向量是 20*2 20 1 61 维。如果觉得太少可以把所有 210 对关键点之间的距离都算进去得到 210 维特征。但要注意朴素贝叶斯在特征维度过高且强相关时分类效果会退化所以并不是维度越多越好。4.2 beyes.py 朴素贝叶斯训练项目里的beyes.py不是 scikit-learn 中的GaussianNB而是一个自实现的朴素贝叶斯分类器。predict_beyes.py是它的预测入口。训练代码大致如下class BayesClassifier: def __init__(self): self.means {} self.stds {} self.priors {} def fit(self, X, y): classes np.unique(y) for c in classes: X_c X[y c] self.means[c] np.mean(X_c, axis0) self.stds[c] np.std(X_c, axis0) 1e-6 self.priors[c] len(X_c) / len(y) def predict(self, X): log_probs [] for c, mean in self.means.items(): std self.stds[c] log_likelihood -0.5 * np.sum(np.log(2 * np.pi * std**2)) log_likelihood - 0.5 * np.sum(((X - mean) ** 2) / (std**2)) log_prior np.log(self.priors[c]) log_probs.append(log_prior log_likelihood) return classes[np.argmax(log_probs)]训练时要用np.load读取所有关键帧的特征并给每帧打上标签。标签就是手语词汇的类别索引比如0代表 “谢谢”1代表 “你好”。get_features.py可以生成一个features.npy和labels.npy然后训练from beyes import BayesClassifier X np.load(features.npy) y np.load(labels.npy) clf BayesClassifier() clf.fit(X, y) import joblib joblib.dump(clf, train_model.pkl)这里我没有把beyes.py里的交叉验证代码展开但你需要留一部分样本做测试。常见做法是按帧序号划分而不是随机划分因为同一段视频里的相邻帧高度相似随机划分会把相似样本同时放进训练集和测试集导致准确率虚高。4.3 predict_beyes.py 单张图片预测流程predict_beyes.py的职责是把一张图片从零开始变成预测结果。它会先调用yolo.py找到手再调用pose_hand.py提取关键点然后用build_feature构造特征最后加载train_model.pkl做预测。from yolo import YOLO from pose_hand import HandPoseEstimator from get_features import build_feature import joblib yolo YOLO() pose HandPoseEstimator() clf joblib.load(train_model.pkl) image cv2.imread(test_frame.jpg) box yolo.detect_one(image) if box is None: print(No hand detected) else: x1, y1, x2, y2 expand_box(box, scale0.2) hand_roi image[y1:y2, x1:x2] pts pose.get_hand_keypoints(hand_roi) vec build_feature(pts) label clf.predict(vec.reshape(1, -1)) print(fpredicted label: {label})预测时最容易出错的是输入特征的维度与训练时不匹配。比如训练时用了 61 维特征但预测时build_feature因为某些关键点置信度太低而丢弃了部分维度导致reshape失败。解决方法是让build_feature永远返回固定长度向量对于不可靠的点用0填充并且在predict前打印vec.shape做一次断言。5. 联调排错从 wxFormBuilder 界面到手机摄像头采集5.1 UI_main.py 与 signUI.fbp 的对接signUI.fbp是 wxFormBuilder 生成的界面工程文件UI_main.py是生成的 Python 代码。在主界面里我一般提供一个“打开摄像头”按钮和一个“选择图片”按钮二者共用同一个预测回调。注意 wxFormBuilder 生成代码后不要手工修改UI_main.py内部的布局代码否则下次重新生成会覆盖你的改动。正确做法是新建一个main_app.py来继承UI_main.py中的 Frame 类在子类里写业务逻辑这样界面调整时只要改.fbp再重新生成即可。5.2 手机摄像头采集的注意点项目摘要里期望在手机端采集视频实际联调时很多人会用 IP Webcam 这类局域网摄像头软件手机和电脑接入同一 WiFi 后OpenCV 直接打开http://手机IP:8080/video。这会带来三个问题分辨率不稳定、延迟波动、色彩偏冷。我通常会在采集线程里先读几帧丢弃等摄像头自动曝光稳定后再开始抽帧。另外局域网视频流的帧率往往不稳定不能依赖视频时间戳要自己计数帧数按帧间隔抽关键帧。5.3 模型路径与特征对齐检查这个系统涉及模型文件、视频工具、OpenPose 编译库三处路径。在 Windows 上train_model.pkl、hand_weights.h5最好都用绝对路径或者在运行时用os.path.dirname(__file__)动态拼出项目根目录。我排查过最多次的问题是YOLOv3 检测框正常但 OpenPose 输出的关键点完全错位最后发现是hand_net_resolution设置成256x256但实际输入图像是长条形的 ROIOpenPose 内部会先 resize 到网络分辨率输出坐标需要按原 ROI 的宽高比例映射回去。映射代码可以参考keypoints[:, 0] keypoints[:, 0] / 256 * roi_width keypoints[:, 1] keypoints[:, 1] / 256 * roi_height如果漏掉这一步关键点坐标会被压缩到 0256 区间后续的距离特征全部失真。把这行映射加在get_features.py的最前面基本能解决大半手语识别结果不准的问题。本文还有配套的精品资源点击获取