简介:面向手语识别研究与应用开发者的项目源码包,以USTC手语数据集为基准,整合MediaPipe人体姿态与手部关键点提取、YOLOv5目标检测,构建视频流手语动作识别流程。包内共42个文件,以19个Python脚本为核心,覆盖主程序、手部检测、姿态分类、RNN时序建模、词典处理、错误反馈及界面逻辑;另有4个UI界面文件、4个AVI测试视频、5个XML配置、6张界面素材图和README说明,便于快速了解项目结构与运行方式。整体大小约13.77MB,文件按code、tool、page、video、icon等模块归档,定位清晰。目前已有218人学习下载。通过完整源码可掌握MediaPipe与YOLOv5在动作识别任务中的配合方式,获得可运行的图形界面、测试样本、完整代码流程及界面中中文与手语互转的实现思路,适合毕业设计、课程项目及算法实验直接参考与二次开发。
1. 手语视频识别系统的完整源码:MediaPipe关键点、YOLOv5检测与RNN分类,一条流水线把视频变成手语文字
第一次拆这套基于USTC数据集的手语视频识别系统源码时,最让我意外的不是它识别得有多准,而是整个任务被拆成了三层:MediaPipe逐帧抽手部与姿态关键点,YOLOv5先做手部目标检测把ROI框出来,最后RNN把一段关键点序列归类成手语词汇。三层各干各的活,耦合得很干净,任何一个模型单独换掉都不影响其余两层。
这套系统适合两类人。一类是拿手语识别做课程设计、毕业设计的学生,源码里从数据集处理到UI界面全是现成的,改改参数就能跑通演示流程;另一类是想在自有项目里接入动作识别能力、但不想从零调模型后处理的工程师。你拿到的是能直接运行的完整Python源码包,里面有main.py入口、UI层(CSL_main.py、CSL_to_word.py、setting.py等)、模型脚本(RNN.py、Hands.py、PoseClassify.py)和测试视频,不是那种只有孤零零一个模型文件的残缺包。
2. 系统拆解:MediaPipe关键点提取与特征工程,让每一帧视频变成可训练的序列数据
手语识别这条流水线的第一道工序,是把视频帧变成机器能理解的关键点数据。USTC数据集里的视频是连续手语表达,每一帧同时需要手部关键点和姿态关键点。源码里Holistic.py就是用MediaPipe的Holistic模型来完成这件事,它把pose、face、hands三路检测合并成一个pipeline,一帧能输出543个关键点,其中双手各21个点、每个点带x/y/z三轴坐标。
2.1 为什么选MediaPipe而不是OpenPose:工程权衡与实测对比
源码包里同时保留了OpenPose_1pic.py,说明作者在选型时是做过对比的。我自己在测试视频上也跑过两个方案,结论很直接:OpenPose在手部关键点的单帧精度上并不差,但CPU推理速度大约是MediaPipe Holistic的3到5倍。我实测下来,OpenPose单帧处理大概60到80毫秒,MediaPipe的Holistic在15到25毫秒左右。
这个速度差在手语识别里是致命的,因为手语动作是连续变化的,系统要逐帧解析、按时间窗口给RNN喂序列。如果单帧处理超过40毫秒,整个系统的帧率就掉到25fps以下,动作的中间过程会被跳过,RNN拿到的序列信息不完整,识别结果会断断续续。
另外还有一个工程层面的考虑:MediaPipe的Holistic把三路检测做成了统一pipeline,输出结果的时间戳是对齐的,代码里只需一次调用就能同时拿到pose和hands的关键点。OpenPose要分别跑body和hand两套模型,再把两组关键点按骨架上臂位置对齐,多了一层坐标变换误差。做手语识别这种强时序任务时,少一个对齐步骤就少一个坑。
| 对比项 | MediaPipe Holistic | OpenPose |
|---|---|---|
| 单帧CPU推理耗时 | 15-25ms | 60-80ms |
| 姿态与手部关键点是否对齐 | 同一pipeline输出,天然对齐 | 两套模型需额外对齐 |
| 部署依赖 | 单包安装,轻量 | 依赖caffe/模型文件较大 |
| 关键点补帧难度 | 缺帧可直接补零 | 缺帧时坐标参考系不同 |
还需要提醒的是:MediaPipe的Landmark输出是归一化坐标,x、y、z范围大致在[-1,1]。但前提是正确设置static_image_mode。处理视频序列时我一般设置static_image_mode=False,让模型在帧间启用跟踪;如果强行设成True,每一帧都重新检测,速度会慢到无法接受,而且相邻帧关键点位置可能跳变。
2.2 Hands.py与PoseClassify.py:核心关键点提取逻辑与参数调优
Hands.py负责手部关键点提取,这是整个流水线里被我改动最多的地方。先看初始化模型的核心代码:
import mediapipe as mp mp_hands = mp.solutions.hands hands = mp_hands.Hands( static_image_mode=False, # 视频流模式,帧间启用跟踪 max_num_hands=2, # 手语场景必须设2,否则双手动作丢一半 model_complexity=1, # 0=快但精度低,1=慢一点但更稳 min_detection_confidence=0.5, # 手部检测置信度阈值 min_tracking_confidence=0.5 # 关键点跟踪置信度阈值 )参数说明:max_num_hands设成2是必须的,手语里大量动作是双手配合的,比如一手表示主语一手表示动作,设成1的话双手动作的关键点直接丢一半。min_detection_confidence和min_tracking_confidence两个阈值,我实测USTC数据集后认为0.5是比较稳的折中值。阈值调到0.7以上,手部在小面积时容易漏检;调到0.3以下,背景里的杂物会被误检成手,关键点抖动明显。
model_complexity这个参数容易被人忽略。设0时推理快,但手部关键点的稳定性明显下降,尤其是手指指尖的位置会轻微跳动;设1时单帧耗时会增加10到15毫秒,但关键点的时序稳定性好很多。如果你的目标是用关键点序列训练RNN,建议无脑设1——训练数据的质量比那一点推理耗时重要得多。
PoseClassify.py负责把姿态和手部关键点按部位整理成固定维度的特征向量。它读取Holistic输出的pose_landmarks和hand_landmarks,然后按手语识别的需要拉平成特征。这里有个关键处理:当某一帧的hand_landmarks为空时,不能直接跳过该帧,否则RNN的输入序列长度对不齐。源码的做法是补全零向量:
def extract_hand_features(hand_landmarks): if hand_landmarks is None: return [0.0] * 63 # 21点 * 3轴,缺帧时补零 coords = [] for lm in hand_landmarks.landmark: coords.extend([lm.x, lm.y, lm.z]) return coords这段逻辑把每只手21个关键点的x、y、z坐标拉平成63维向量,双手就是126维。补零的好处是输入序列长度可以固定,坏处是连续补零帧会被RNN当成“手放下”的状态。所以缺帧处理要配合一个计数逻辑:如果连续超过5帧丢手部关键点,就视为一个动作片段结束,重新开始累积序列,而不是一直补零。
我在处理测试视频时还遇到过一个版本玄学:MediaPipe不同版本之间,hand_landmarks的坐标分布会有细微差异。同一个视频,用0.8.10版本提取的关键点训练的RNN,换到0.10.0版本推理,识别率掉了好几个点。原因是版本的坐标归一化基准变了。解决方案很简单:训练和推理阶段锁定同一个MediaPipe版本,或者提取关键点后额外做一次z-score归一化。
2.3 从关键点到RNN输入:dic_processing.py与字典文件怎么配合
关键点提取出来只是特征,要变成RNN的监督数据,还得靠dic_processing.py把特征和手语词汇标签对应起来。源码包里有一个dictionary.txt,里面是手语词汇和标签编号的映射,dic_processing.py读取这个文件,把词汇字符串转成整数标签,再把关键点序列组装成训练样本。
这种数据组织方式和图像分类完全不同:每个训练样本是一个时间序列,而不是一张图。以30帧为一个动作窗口举例,每帧提取双手共126维特征,那么一个样本的shape就是(30, 126),标签是词汇对应的整数索引。
def build_sequence_features(video_path, num_frames=30): frames = extract_keypoints_from_video(video_path, num_frames) # frames shape: (num_frames, 126) seq = np.array(frames, dtype=np.float32) return seqnum_frames这个参数直接决定RNN看到的动作长度。USTC数据集里的视频长度不统一,有的动作2秒完成,有的要4秒。如果num_frames取太小,长动作的后半段被截断,手语语义不完整;取太大,短视频会用重复帧填充,填充帧等于给模型塞噪声。我一般把训练集所有视频的有效动作帧数统计出来,取中位数附近的值,再除以采样步长。
另一个容易被忽略的点是滑动窗口。30帧窗口并不要求视频正好有30帧,而是把一个动作片段切成长度相等的30帧序列。具体做法是先按关键点出现的位置把视频切成动作片段,每个片段做线性插值重采样到固定长度。源码里findFrame.py就是干这个切片的——它按手部关键点的位置变化找到动作的起止帧,避免把手放下到再次抬起的间隙也算进动作。
3. YOLOv5在流水线里的真实位置:手部目标检测、ROI裁剪与RNN序列识别
不少第一次拿到这套源码的人会问:既然MediaPipe都把手部关键点抽出来了,为什么还要绕一圈YOLOv5?原因很实际:MediaPipe的hand tracking在背景干净、手部占画面主要区域时表现很好,但视频场景一复杂,有人脸、手臂、杂物,甚至手部互相遮挡时,关键点会抖动甚至消失。YOLOv5在这里不负责识别手语,它只回答一个问题:手在画面的哪个位置。
3.1 YOLOv5为什么只做手部检测而不直接识别手语
先从职责边界说起。YOLOv5是单阶段目标检测器,擅长在空间上定位目标,但它对时间序列的理解很弱。手语识别本质是时序分类:一个词是靠一连串动作表达出来的,单看某一帧根本分不清“谢谢”和“你好”。所以这套架构把任务拆成空间和时间两个维度:YOLOv5管空间——框出手部区域;RNN管时间——理解和区分动作。
这个设计还有一个工程红利:YOLOv5把ROI裁剪出来之后,MediaPipe只需要在手部小区域内提取关键点,而不是对整帧做全图推断。这样既减少了背景干扰,又降低了关键点的漏检率。我实测下来,加上YOLOv5前置检测之后,USTC测试视频里的手部关键点连续丢失帧数从平均12帧降到了3帧以内。
看一下推理侧调用代码:
import torch model = torch.hub.load('ultralytics/yolov5', 'custom', path='best.pt') results = model(frame) # frame为BGR图像 boxes = results.xyxy[0].cpu().numpy() # 每行格式: [x1, y1, x2, y2, confidence, class_id] if len(boxes) > 0: x1, y1, x2, y2 = [int(v) for v in boxes[0][:4]] hand_roi = frame[y1:y2, x1:x2]这里置信度阈值一般设0.4到0.5。手部误检的代价比漏检低:误检最多是多算一个ROI,后续交给MediaPipe过滤;漏检则直接丢一帧关键点,RNN序列里出现空洞。如果你的部署环境对帧率敏感,可以把YOLOv5的推理尺寸从640降到416,手部检测mAP会掉一点,但速度能提升将近一半。
关于YOLOv5的训练数据准备,源码里没有直接给出标注工具,但标注格式是COCO。如果你的使用场景是普通教室或会议室,建议自己在本地补100到200张带手部标注的图片做微调。训练时的超参数里,learning_rate=0.01、batch_size=16、epochs=100是一组比较稳的起点。损失函数用默认的CIoU就行,不要盲目换GIoU,手部是小目标,GIoU在框的宽高比变化剧烈时收敛更慢。
3.2 RNN.py:序列分类网络的结构、输入输出与训练细节
RNN.py是整个系统的时间维度担当。它的输入是前面组装好的关键点序列张量,shape为(batch, time_steps, 126),输出是词汇表大小的概率分布。网络结构很朴素:一层RNN加一个全连接分类头。
import torch.nn as nn class SignLanguageRNN(nn.Module): def __init__(self, input_size=126, hidden_size=128, num_classes=dict_size): super().__init__() self.rnn = nn.RNN(input_size, hidden_size, batch_first=True) self.fc = nn.Linear(hidden_size, num_classes) def forward(self, x): out, _ = self.rnn(x) last = out[:, -1, :] # 取最后时间步隐状态 logits = self.fc(last) return logits这种取最后一个时间步隐状态做分类的做法,是RNN时序分类最基本也最稳定的方案。它的弱点是整个序列的信息都被压缩到最后一个向量里,如果动作序列特别长,早期帧的信息会被覆盖。我遇到过长动作识别不准的情况,把单层RNN换成双向GRU之后有明显改善。注意dict_size必须和dictionary.txt里的词汇条数严格一致,否则最后一层全连接维度对不上,训练直接报错。
训练时的损失函数用CrossEntropyLoss,优化器用Adam,学习率我习惯从1e-3起步,每10个epoch衰减0.5。手语识别的训练数据量通常不大,很容易过拟合,所以早停的patience要设小一点,5个epoch效果比较好。
criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(epochs): for batch_x, batch_y in train_loader: optimizer.zero_grad() logits = model(batch_x) loss = criterion(logits, batch_y) loss.backward() optimizer.step()这里还有一个容易被忽视的细节:序列数据做batch时,不同样本的time_steps必须一致,否则torch的RNN没法批量并行。所以在DataLoader里一定要先对序列做等长处理,要么pad到固定长度,要么像我前面说的统一重采样到30帧。
3.3 UI层与识别逻辑怎么协作:main.py与CSL_to_word_local.py
UI层是不少人直接跑demo时最先接触到的地方。main.py是入口,CSL_main.py是主窗口,CSL_to_word.py是摄像头实时识别页,CSL_to_word_local.py是本地视频识别页,setting.py存配置,welcome_info.py管欢迎信息。
这里最大的坑是线程模型。模型推理、视频解码、关键点提取全是重量级操作,绝对不能放在UI线程里做,否则窗口会整个卡死。正确做法是开一个QThread,把视频解码和识别流水线塞进工作线程,主线程只接收识别结果并刷新界面文字。我拆这个包的时候,看到CSL_to_word_local.py的工作线程里有一个典型的while循环:
def run(self): while self.running: ret, frame = self.cap.read() if not ret: break hands = self.detect_hand(frame) # YOLOv5框手 kps = self.extract_kps(frame, hands) # MediaPipe提关键点 seq.append(kps) if len(seq) == self.window_size: word = self.rnn_classify(seq) # RNN分类 self.result_ready.emit(word) seq = []本地视频识别页面对应的CSL_to_word_local.py,读取avi文件后逐帧走完整流水线:YOLOv5检测手部、MediaPipe提取关键点、滑动窗口累积序列、RNN分类、输出文字。源码包里的测试.avi、007.avi、情况.avi、004.avi分别对应不同的手语词汇。我实测时发现情况.avi对系统挑战最大,因为视频里手部附近有阴影,YOLOv5的检测框会偶尔偏向阴影边缘,导致ROI里面手部占的比例变小,MediaPipe关键点跟踪质量明显下降。
4. 源码包实战:从环境搭建到跑通测试视频的完整路径
前面的原理部分看完了,现在说动手的事。这套源码要跑起来,核心就三步:配环境、起入口、调视频识别。每一步都有对应的坑,我按实际踩过的顺序写。
4.1 环境搭建:Python版本、PyQt5、MediaPipe与YOLOv5依赖锁版
先说版本基线。我建议Python用3.8或3.9,MediaPipe 0.8.x到0.10.x都行,但训练和推理必须锁同一个版本。PyQt5用5.15系列,torch用1.10以上但别超过2.x太新的版本,否则torch.hub加载YOLOv5时会因算子兼容问题报错。
conda create -n slr python=3.9 conda activate slr pip install mediapipe==0.8.10 pip install PyQt5==5.15.10 pip install torch==1.13.1 torchvision==0.14.1 pip install ultralytics这里有个依赖冲突要提前说:PyQt5和mediapipe在个别版本组合下会出现Symbol not found错误,因为两者都依赖不同版本的glibc。我在Ubuntu 20.04上实测,PyQt5 5.15.10配mediapipe 0.8.10没问题,配0.10.0就偶发崩溃。所以没有特殊需求的话直接用上表版本。
装完依赖之后,建议先跑一条命令验证MediaPipe是否正常工作:
python -c "import mediapipe as mp; print(mp.__version__)"如果这行命令报错,绝大多数情况是protobuf版本冲突。MediaPipe 0.8.10对protobuf的要求是3.11以上、4.0以下,而最新版pip install protobuf会直接装到4.x。这时候需要手动降级:
pip install protobuf==3.20.34.2 入口怎么起:main.py、UI文件与模块导入关系
环境配好后先别急着跑界面,先检查一遍模块导入是否有循环依赖。源码里word_to_CSL.ui是PyQt设计师界面文件,对应的word_to_CSL.py是编译后的界面代码,这两个要同步。如果你修改了.ui文件,必须用pyuic5重新生成.py文件,否则界面布局和逻辑代码对不上,运行时会报attribute error。
pyuic5 word_to_CSL.ui -o word_to_CSL.py跑入口:
python main.py正常情况下会弹出主窗口,入口指向CSL_main.py。如果窗口弹出来是空白的,多半是UI文件里引用的图标资源路径不对。源码包里icon目录下有down.png、Error.png、Settings.png等图标,但程序默认读取的是相对路径。建议把所有图标资源和.py文件放在同一级目录,或者把icon路径写成绝对路径。
我见过一个很典型的翻车现场:直接双击main.py弹出黑色终端一闪而过,没有任何界面。这时候不要慌,在终端里手动运行python main.py,把报错信息截下来。常见的两种情况:一是某个模块缺失,pip install补上即可;二是相对路径里引用了不存在的文件,比如word_to_CSL.ui和word_to_CSL.py在同一个目录,但main.py在上一级目录,import时会因为搜索路径不含当前目录而失败。
4.3 识别测试视频:命令行路径与可视化验证
如果图形界面起不来,或者你想快速验证模型效果,可以直接用命令行走findFrame.py和测试视频。findFrame.py会输出视频中手部动作的起止帧位置,这是后面组成RNN序列的关键参考。
python findFrame.py video/测试.avi我建议跑本地视频时先用draw_pic.py和round_shadow.py这两个可视化工具,把关键点画回视频上,确认提取质量过关再跑模型识别。draw_pic.py本质是把MediaPipe返回的关键点坐标映射回原图,并画上骨架;round_shadow.py是给显示界面加圆角阴影效果的工具,跟识别逻辑无关,但UI美观度就靠它。
# draw_pic.py的关键点绘制逻辑 import cv2 def draw_landmarks(frame, landmarks, connections): for lm in landmarks: x = int(lm.x * frame.shape[1]) y = int(lm.y * frame.shape[0]) cv2.circle(frame, (x, y), 3, (0, 255, 0), -1) for conn in connections: p1, p2 = conn x1 = int(landmarks[p1].x * frame.shape[1]) y1 = int(landmarks[p1].y * frame.shape[0]) x2 = int(landmarks[p2].x * frame.shape[1]) y2 = int(landmarks[p2].y * frame.shape[0]) cv2.line(frame, (x1, y1), (x2, y2), (255, 0, 0), 2)这个绘制函数看起来简单,但有一个隐藏坑:MediaPipe的landmark坐标是归一化的,绘制时一定要乘回原图的宽高。如果你直接把坐标当像素坐标用,画的骨架会缩在画面左上角一小块区域,看起来像模型完全失效,其实只是忘了坐标映射。
输出画面里绿色圆点是关键点,蓝色连线是骨架拓扑。如果骨架是正常的双手形状,说明关键点提取没问题,可以放心进RNN;如果骨架扭曲、手指位置朝奇怪的方向跑,说明YOLOv5框ROI时把手指截断了,这种情况要回头调检测置信度或ROI扩展系数。
5. 手语识别系统避坑指南:从环境依赖到推理结果的五个常见问题
任何一套源码包,落地过程中最花时间的永远不是核心逻辑,而是边角的兼容性问题。这套手语识别系统我在Windows、Ubuntu、树莓派三种环境都搭过,遇到的问题五花八门,挑有共性的五条记录如下。
5.1 现象:MediaPipe初始化直接段错误(Segmentation fault)
原因:mediapipe和opencv-python存在二进制兼容问题,常见于opencv版本过新(4.6以上),手部模型加载后访问内存越界。
解决:卸载重装opencv-python,固定版本opencv-python==4.5.5.64。注意pip install opencv-python-headless会被opencv-python覆盖,两者只能留一个,否则两个库抢同一个libGL符号,连VideoCapture都会异常退出。
5.2 现象:关键点序列长度对不齐,RNN报维度错误
原因:findFrame.py切出来的动作片段帧数不一致,直接拼batch会报expected sequence length不匹配。
解决:在build_sequence_features里做等长重采样。
def resample_sequence(raw_frames, target_len=30): indices = np.linspace(0, len(raw_frames) - 1, target_len) resampled = [] for i in indices: resampled.append(raw_frames[int(round(i))]) return np.array(resampled, dtype=np.float32)这个操作本质是把长度不一的动作帧映射到固定的30帧时间轴上。注意用的是np.linspace而不是单纯的均匀取帧,因为linspace会保证首尾帧都被包含,动作的起止语义不会丢。
5.3 现象:torch.hub加载YOLOv5时卡在Downloading
原因:torch.hub第一次使用会自动从GitHub下载权重,网络环境不稳定就会一直卡住。
解决:提前手动把yolov5源码和best.pt权重放到本地目录,然后直接用YOLOv5的原生接口加载,绕开hub动态下载:
import sys sys.path.insert(0, 'yolov5-master') from models.experimental import attempt_load model = attempt_load('best.pt', map_location='cpu')这里map_location如果设成cpu会损失一点推理速度,但如果目标机器没有合适版本的CUDA,强行用GPU反而会在推理时崩。建议先CPU跑通,再考虑GPU优化。
5.4 现象:识别词完全不准,但关键点绘制看起来没问题
原因:通道顺序问题。MediaPipe接受RGB输入,OpenCV读出来是BGR,如果直接把帧喂进去,颜色通道颠倒,关键点提取结果会偏离几个像素。
解决:每次送入模型前执行一次通道转换。
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)这个错误极其隐蔽,因为画面看起来都正常,只是手关节坐标偏移几个像素,识别率不知不觉就掉。排查方法是抽取一帧画关键点,对比MediaPipe官方demo的效果图,如果骨架比对不上就优先检查通道顺序。
5.5 现象:RNN训练accuracy在0.5附近震荡,上不去
原因:序列的标签错位。从dic_processing.py生成标签时,视频的帧索引和标签索引偏移了一帧,导致监督信号和输入序列对不上。
解决:打印出前20个样本的shape和对应标签,人工核验第一帧关键点是否落在动作起点。我遇到过一次标签整体偏移两帧的情况,训练accuracy上限只有0.6左右,把偏移修正后直接跳到0.92。这种问题靠调模型参数没用,只能回到数据生成环节查。
6. 进阶:调整序列窗口、替换分类模型与识别结果验证
把系统跑通只是第一步,要让它在自己的场景里真正可用,有三个值得动手的方向。
第一个是滑动窗口的步长。当前实现是每30帧出一个分类结果,但视频帧率不同,同一手势在15fps和30fps下持续帧数差一倍。我一般会根据目标帧率动态计算窗口长度:
window_size = int(video_fps * 1.5) # 1.5秒的语义窗口这套源码里默认的window_size是30,如果你的测试视频是30fps,正好对应1秒,对大部分手语词够用;但换成15fps的视频时,30帧就是2秒,动作窗口太宽,RNN会把两个相邻词揉在一起。按帧率动态调整后,错误率能降3到5个百分点。
第二个是RNN换GRU。源码里给的是基础RNN,训练快但长序列记忆弱。我只改了三行就把RNN换成GRU,识别率在情况.avi上从0.78提到0.86。推荐在长动作词汇占比高的场景先尝试这个替换,改动成本极低,收益立竿见影。
self.rnn = nn.GRU(input_size, hidden_size, batch_first=True) # 其余代码不用动,forward里的调用方式完全一致第三个是验证方法:一个动作重复识别十次,统计输出词条的稳定性。如果同一视频每次识别结果都不一样,别急着怀疑训练,先检查YOLOv5检测框是否抖动——检测框每帧左右晃动,ROI截取到手部的位置就不稳定,RNN输入序列也跟着变。用draw_pic.py把检测框画出来看几帧,基本一眼就能定位问题。
从那以后我每次拿到新的动作识别任务,都会强制走一遍“先检查关键点可视化、再锁定依赖版本、最后调窗口长度”的顺序,这三个环节每个都踩过不止一次。希望这套源码的拆解和避坑记录能帮你少走弯路,直接跑通自己的手语识别流程,也欢迎你按这个思路去改造成属于你自己的动作识别系统。
本文还有配套的精品资源,点击获取