ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenPose手语识别实战:关键点轨迹图与3D卷积双方案

OpenPose手语识别实战:关键点轨迹图与3D卷积双方案 简介面向计算机视觉或人工智能方向毕业设计的Python项目包基于OpenPose关节点检测与图像分类实现手语单词识别提供了两种可复现的算法路径一是将视频经OpenPose提取关节点轨迹图后送入图像分类网络二是将多帧关节点位置堆叠为三维张量并用三维卷积训练预测。包内共108个文件约16.68MB以Python脚本、C示例、OpenPose模型配置prototxt、说明文档和演示视频为主同时包含训练好的pth权重、环境配置说明Python 3.7、CUDA 10等以及项目使用说明便于在毕设或课设中直接学习与改造。目前已有496人学习下载适合具备一定Python和深度学习基础、需要快速搭建手语识别原型系统的学生参考。1. 为什么手语识别项目要用openpose把视频“压”成图片第一次接触这个基于openpose的手语识别毕设项目时我最直观的感受是真正的难点不在分类模型而在如何把一段连续动作变成模型能“一眼看完”的输入。直接拿整段视频训练3D卷积数据量和显存开销都压不住尤其当训练集只有几十个单词类别时而openpose方案把姿态检测前置视频先被拆成关键点序列再用两种完全不同思路压缩——要么拼成一张关节点运动轨迹图丢给图像分类网络要么把多帧关节点位置堆叠成三维张量走3D卷积。这个“先检测再压缩”的设计才是整个项目值得拆开看的地方。源码里同时给出两条完整实现路径环境锁定Python 3.7 CUDA 10非常适合毕业设计或课程设计作为可运行的姿态识别基线也适合想快速验证动作分类方案的工程师做二次开发。2. openpose检测器初始化的关键参数与关节点选型2.1 手语场景为什么不能只用BODY_25模型openpose自带的BODY_25模型输出25个身体关键点能覆盖手腕、手肘、肩膀但对指尖位置完全无效。手语表达的语义大量集中在手指弯曲和手掌翻面只看手腕和手肘轨迹很多相近词汇会混在一起。项目实现时常见做法是同时加载手部模型openpose官方C示例里专门提了07_hand_from_image.cpp这类的独立手部输入样例说明手部模型与身体模型在openpose中是分离的身体模型输出25点手部模型单只手输出21点从腕部到五个指尖。手语识别项目中优先保留BODY_25的上半身关节再叠加双手各自21个关键点坐标融合后作为后续轨迹图和三维张量的数据源。可以先用官方pybind跑通检测链路验证关键点输出格式import pyopenpose as op params { model_folder: models/, model_pose: BODY_25, net_input_size: 656x368, net_output_size: 656x368, number_people_max: 1, keypoint_scale: 3, render_pose: 2, cli_verbose: False, } op_wrapper op.WrapperPython() op_wrapper.configure(params) op_wrapper.start() datum op.Datum() datum.cvInputData frame op_wrapper.emplaceAndPop([datum]) keypoints datum.poseKeypoints # shape: (人数, 25, 3)net_input_size决定送入网络的图像分辨率656x368是官方默认值提升到656x368以上对远端小目标关键点召回有改善但会明显拉低帧率keypoint_scale设为3表示关键点坐标按输入图像尺寸放缩方便直接映射回原图坐标后续绘制轨迹时不用再做一次换算。2.2 手语数据流里必须保留的关节点集合实际做项目时不会把25点全部喂给下游手语语义集中在腕、肘、肩和指尖。BODY_25中手腕为第4点右手腕和第7点左手腕手肘为第5、8点肩膀为第2、5左与第1、6右附近手部模型21点位置相对固定点0是腕部基础第4、8、12、16、20分别对应拇指到小指的指尖。做轨迹图时我一般只保留7个点双腕、双肘、双肩加一个中心点因为指尖在快速运动时openpose偶尔會跳变混进轨迹会产生不自然的折线。下表是手语识别项目中常用的检测参数组合参数推荐值说明model_poseBODY_25 HAND身体与手部分开推理再将坐标拼到同一坐标系net_input_size656x368平衡速度与精度低于336x256时小目标关键点丢失严重number_people_max1手语单人场景设大会引入额外过滤逻辑keypoint_scale3对应net_input_size尺寸的缩放系数便于画图2.3 环境配置中Python版本选择与常见编译坑摘要里强调Python必须使用3.7这个说法并不过时。openpose仓库的pybind11接口在3.7下编译最稳定Python 3.8以上会出现Cython生成代码与pybind版本不匹配导致Segmentation Fault的情况。建议直接从anaconda创建独立环境python安装这一步先锁定3.7conda create -n slr python3.7 conda activate slr conda install numpy opencv pybind11 cd openpose mkdir build cd build cmake -DBUILD_PYTHON_APION -DPYTHON_EXECUTABLE$(which python) -DGPU_MODEGPU_ONLY .. make -j$(nproc)PYTHON_EXECUTABLE必须指向anaconda环境下3.7的python路径默认读取系统python会导致编译出的pyopenpose无法被导入cuda选择10系列而非11openpose对10的算子兼容性经受过大量验证cudnn用7和8都行但CUDA 11下必须同步换对应版本的cudnn不然运行时会报cudnnHandleCreate错误。编译完成后的冒烟测试不是导入pyopenpose而是对一张手部图片跑一次推理并打印poseKeypoints.shapeshape正确再进入下一步。3. 方法一关节点运动轨迹图与图像分类网络实战3.1 如何把帧序列画成一张可分类的轨迹图方法一的完整链路是视频每一帧经过openpose得到关键点坐标把N帧中同一个关节在画布上的坐标依次连线最终所有关节点轨迹叠加成一张单张图片再交给图像分类网络。这个方案的核心假设是手语孤立词有明确的起止范围轨迹图保留了空间的运动模式丢掉速度信息仍然可分类。实现时先逐帧采集关键点坐标再一次性绘图避免边读帧边绘制导致画面闪烁和坐标抖动def build_track_image(frames, joints_of_interest, canvas_size(224, 224)): canvas np.zeros((canvas_size[0], canvas_size[1], 3), dtypenp.uint8) prev_points {} for frame_idx, frame in enumerate(frames): keypoints get_openpose_keypoints(frame) for joint_name, joint_id in joints_of_interest.items(): x, y, conf keypoints[0][joint_id] if conf 0.6: continue # 归一化到画布坐标避免手部移出画面 norm_x int(x / frame.shape[1] * canvas_size[1]) norm_y int(y / frame.shape[0] * canvas_size[0]) color (frame_idx * 255 // len(frames), 128, 255 - frame_idx * 255 // len(frames)) cv2.circle(canvas, (norm_x, norm_y), 2, color, -1) if joint_name in prev_points: cv2.line(canvas, prev_points[joint_name], (norm_x, norm_y), color, 1) prev_points[joint_name] (norm_x, norm_y) return canvas代码里的conf阈值0.6来自openpose论文推荐的置信度截断值实际使用中手指部分经常低于这个阈值此时跳过该帧的该关节不画点也不连线能避免抖动点画出的长尾毛刺。颜色通道编码的是帧序号信息分类网络实际上可以用这个伪时间维度区分“动作顺序不同但轨迹形状类似”的样本。归一化部分将原图坐标等比映射到224x224画布如果不做这步不同视频分辨率下同一动作的轨迹图尺度差异会直接成为分类误差来源。3.2 图像分类模型选型ResNet18还是最新的图像分类模型轨迹图数据集通常规模不大一个包含20个手语词、每词50段视频的数据集只能产生1000张左右的图这个量级下直接选用EfficientNet-B0或ResNet18做迁移学习比ViT可靠。最新的图像分类模型Transformer架构在ImageNet这类千万级数据上效果好但在几百张图的轨迹图上会过拟合到背景的固定位置而CNN的局部归纳偏置对关键点轨迹这种稀疏线条更友好收敛速度快、调参成本低。默认使用ResNet18加载ImageNet预训练权重替换最后一层全连接输出为单词类别数import torchvision.models as models model models.resnet18(pretrainedTrue) model.fc torch.nn.Linear(model.fc.in_features, num_classes) transform_train transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])训练时采用Adam优化器初始学习率1e-4批大小8对单张224x224的轨迹图完全可以跑在6GB显存上。pretrainedTrue的网络对轨迹图这类非自然图像仍然有效因为浅层学习到的边缘和角点特征能复用到曲线上。数据增强里RandomResizedCrop的scale下限设为0.8防止裁剪把轨迹主干截断随机旋转15度相当于模拟不同手部朝向。3.3 训练流程与容易搞错的评测方式源码目录里应包含train.py或对应的notebook数据组织方式按ImageFolder格式建立dataset/ word_a/ 001.png 002.png word_b/ 001.png ...训练命令参考python train.py --data_root dataset/ --model resnet18 --epochs 50 --lr 1e-4 --batch_size 8评测时建议按视频维度切分训练集和测试集而不是按单个随机切帧。按帧或按轨迹图维度切会高估真实准确率因为同一个视频相邻几帧产生的轨迹图几乎相同泄漏到训练集后测试准确率虚高。我一般按视频文件ID做GroupShuffleSplit确保同一段视频的所有帧只出现在一侧数据集里。超参数推荐值说明输入尺寸224x224轨迹稀疏过大分辨率反而增加过拟合主干网络ResNet18 / EfficientNet-B0小数据量下的安全选型优化器Adam lr1e-4迁移学习场景下优于SGD冷启动epoch30~50超过50轮loss不再下降早停更有效4. 方法二多帧关节点堆叠与3D卷积的时序建模实现4.1 为什么轨迹图会丢失速度信息而三维张量不会方法一存在一个结构性缺陷轨迹图上只记录了经过的空间位置不记录每两帧之间的时间差和加速度。手语单词里“慢挥”和“快挥”的轨迹形状几乎一致但语义不同3D卷积方案把连续N帧的关键点骨架图沿时间轴堆叠构成(H, W, N)的三维张量两个空间维度描述单帧关键点分布时间维度保留运动速度。网络在三维空间里同时做空间卷积和时间卷积从原理上补齐了方法一的短板。4.2 从视频到三维张量的数据管道实现先对视频抽帧抽帧间隔决定动作在时间维上的压缩程度。手势动作通常持续1到2秒以20fps的openpose推理速度来看一个动作能拿到约20到40帧有效检测结果。我一般按每2帧取1帧的方式采样截取一个窗口长度N16的帧序列超过16帧的尾部直接丢弃不足16帧的组合进行末尾补零def build_voxel(frames, window_size16, canvas_size(96, 96)): stacked np.zeros((window_size, canvas_size[0], canvas_size[1]), dtypenp.float32) for i, frame in enumerate(frames[:window_size]): skeleton_map render_skeleton_map(frame, canvas_size) stacked[i] skeleton_map return stacked.transpose(1, 2, 0) # shape: (96, 96, 16)render_skeleton_map的作用是把某一帧检测到的关键点画成单通道的白色点与连线图背景保持全黑。与方法一的轨迹图不同这里的骨架图不保存帧序号颜色信息而是让时间维在通道位置上天然编码时序信息。多个关节的空间位置在同一帧内叠加在同一个2D图上整体张量形状是(canvas_w, canvas_h, time_len)与普通3D卷积网络结构对齐。4.3 轻量3D卷积网络结构与训练要点定义网络时不需要复杂的3D ResNet变体手语骨架图的语义层相对浅局部特征来自关节点的空间位置关系时序特征来自关节点的连续运动。一个三层Conv3d的网络就足够import torch.nn as nn class SkeletonConv3D(nn.Module): def __init__(self, num_classes20): super().__init__() self.features nn.Sequential( nn.Conv3d(1, 32, kernel_size(3, 5, 5), stride(1, 1, 1), padding(1, 2, 2)), nn.BatchNorm3d(32), nn.ReLU(inplaceTrue), nn.MaxPool3d(kernel_size(1, 2, 2), stride(1, 2, 2)), nn.Conv3d(32, 64, kernel_size(3, 3, 3), stride(1, 1, 1), padding(1, 1, 1)), nn.BatchNorm3d(64), nn.ReLU(inplaceTrue), nn.MaxPool3d(kernel_size(2, 2, 2), stride(2, 2, 2)), nn.AdaptiveAvgPool3d((1, 1, 1)) ) self.classifier nn.Linear(64, num_classes) def forward(self, x): x x.unsqueeze(1) feat self.features(x) return self.classifier(feat.view(feat.size(0), -1))上面的kernel_size设置为(3, 5, 5)第一维是时间后两维是空间。时间卷积核较小因为骨架点坐标在相邻几帧间的位移不大过大的时间核会把多个过采样帧揉成一片空间卷积核5x5能覆盖单只手范围内相邻关键点之间的相对关系。MaxPool3d在第一层只做空间池化不压缩时间长度避免过早丢弃时序精度。输入96x96的骨架图比方法一的224小很多主要原因是3D卷积显存占用随空间尺寸立方增长盲目提分辨率会直接显存溢出。训练配置上优化器选Adam、lr1e-3、batch_size8窗口长度16时显存占用约4GB。这个方案与图像分类有一个显著差异不能用随机截取单帧做数据增强而应该对整个16帧窗口做同样的水平翻转或随机小角度旋转时间轴上只做整体缩放不能随机抽走中间帧否则3D卷积学到的时间节奏会被打乱。两种方案在同样数据规模下的实际对比维度轨迹图图像分类多帧堆叠3D卷积输入形态(224, 224, 3) 轨迹图(96, 96, 16) 张量时间信息颜色编码非显式位置时间维显式建模速度差异表达无法区分可区分快速/慢速动作推理速度快约0.2s/样本慢约0.8s/样本数据需求量少500张以上可收敛多建议1000段以上5. 从heatmap到关键点归一化两个提升识别率的排错技巧项目交付后最容易被忽略的是验证环节直接跳过关键点可视化去训练分类器一旦识别率低会搞不清楚是检测模型的问题还是分类网络的问题。第一步应当把openpose的关键点叠加绘制回原始视频帧上生成一段带骨架线条的手语视频逐帧观察骨骼线是否贴合关节位置。这个步骤我一般用一个极短的脚本完成读取视频帧、调用op_wrapper检测、用cv2.line把相邻关节点连成骨架图。如果发现手指尖在某一帧跳到背景上说明置信度过低这时把conf阈值从0.6提高到0.75并丢弃低于阈值的帧而不是让错误坐标混入训练张量。第二个影响实际效果的细节是坐标归一化方式。轨迹图和骨架图虽然做了画布尺寸等比缩放但不同人的手部大小和视频中站位偏置会让相同动作的轨迹图在画布上产生偏移。常见做法是把所有关键点的坐标转换到以腕关节或肩膀为参考点的局部坐标系先用双手腕部中点的坐标作为平移基准把所有关键点减去该点再做缩放让手部包围盒固定到统一尺寸。这样即使做手语的人站的位置偏左偏右、身高不同、手离镜头距离远近不同输入特征都能对齐。对图像分类网络来说这一步消除的类内方差往往比更换更强的分类模型提升更明显。验证整体流程可以用一条命令完成把openpose检测到的关节序列按动作类别保存成npy文件再用np.load检查每个动作的帧数分布和时间长度找出因为openpose漏检导致有效帧数不足8帧的异常样本直接剔除重新采集。这个环节和轨迹图归一化配合能用少量数据逼平直接上大参数模型的识别效果。本文还有配套的精品资源点击获取
返回列表