
简介一份面向机器学习与计算机视觉初学者的完整代码包围绕基于SVM分类器实现姿势检测解决图像、视频中人体姿态识别与分类问题。内容涵盖HOG特征提取、SVM模型训练与推理等核心环节配有可运行的Python脚本、已训练好的svm.model模型、13张jpg测试图片以及avi与mp4演示视频压缩包共22个文件整体44MB结构清晰便于按流程复现实验。资源特别演示了HOG特征的预处理、细胞单元划分、梯度直方图构建与块归一化等关键步骤并说明SVM如何通过核函数映射、最大化间隔来区分不同姿态类别。已有189人学习下载。通过该项目可掌握从特征工程到分类器调参的完整实践路径包括方向梯度直方图计算、核函数选择、惩罚因子C与gamma参数调整并可直接基于OpenCV对视频帧进行实时姿态检测适合课程设计、毕业设计或入门AI项目实战。1. 基于SVM分类器实现姿势检测为什么在深度学习时代还要翻它的牌在嵌入式设备上做一次姿势分类深度学习模型还在排队加载权重的时候SVM方案已经跑完一整轮检测了。基于SVM分类器实现姿势检测本质上不是把原始图像丢给分类器而是先提取人体关键点再把关键点转成归一化坐标与关节角度特征最后用一个核SVM完成站、坐、蹲、躺这类离散姿势的判别。这个方案的优势非常务实小样本能训、训练只要几秒钟、模型只有几百KB、分类依据还能翻出支持向量来给人讲清楚。它适合的场景也很明确——边缘设备部署、样本量在几千量级、业务只需要区分有限几种姿势状态。反直觉的是在深度学习几乎包揽视觉任务的今天SVM姿势分类在工业落地里从未真正退场。2. 把姿势翻译成SVM吃得下的特征关键点归一化与角度构造SVM分类器吃的是固定维度的特征向量不是图像。你在这一步要做的第一件事就是把一张人像变成一组数字。常见做法是先用MediaPipe Pose或OpenPose这类关键点检测器提取人体的肩、肘、腕、髋、膝、踝坐标然后把这些坐标加工成特征。这里有个新手最容易踩的误区以为把坐标直接拼成向量喂给SVM就行。实际完全不是这么回事原始坐标里有大量与“姿势”无关的干扰信息直接喂进去模型会把画面的位置、人物的高矮、离摄像头的远近都当成分类依据换个人换个位置就全盘崩溃。2.1 为什么关键点坐标不能直接扔给SVM先想清楚三个问题。第一平移问题同样一个站姿人站在画面左边和右边所有关键点的x坐标完全不同第二尺度问题同一个动作人离摄像头近时坐标数值大离得远时数值小第三人体比例差异高个子和矮个子同一姿势下肩到髋的距离、手臂长度都不一样。SVM的决策边界在特征空间里是固定的坐标一飘特征点就在空间里乱窜边界再优美也拦不住。解决方案是平移归一化加尺度归一化。我一般以髋关节中心为原点——髋是躯干的支撑点受四肢摆动影响最小——把所有关键点坐标减去髋中心坐标完成平移再以肩中心到髋中心的距离作为尺度因子把所有坐标除以这个距离。这样处理之后特征与人的画面位置、离摄像头远近基本无关只剩姿势本身的信息。有一个细节值得注意肩髋距离在站立和坐姿下有明显差异但蹲下时躯干前倾也会压缩这个距离所以它本身也可以作为一个特征而不是只当分母用。2.2 角度特征与比例特征SVM姿势检测的特征工程实操坐标归一化之后能用但还不够稳。比如一个人抬手到半空和完全举起手腕坐标的变化受手臂长度影响很大换个臂长不同的人同一动作的坐标特征就变了。角度特征没有这个问题——肩、肘、腕三点算出的肘关节角度与臂长、距离、画面位置都无关它只描述“弯曲了多少”。这是SVM姿势检测特征工程里最值得花时间的地方。我常用的特征向量是7维左右肘关节角、左右膝关节角、躯干倾角、手到髋的归一化距离、髋到踝的归一化距离。膝角能区分站与蹲肘角能区分举手和平举躯干倾角能区分弯腰和直立髋踝距离用来区分坐姿和蹲姿——这两者膝角都接近九十度附近但坐姿髋踝距离大、蹲姿髋踝距离小。角度计算用余弦定理以肘关节为例先算肩到肘的向量和腕到肘的向量再求夹角的余弦值最后取反余弦得角度。import cv2 import mediapipe as mp import numpy as np mp_pose mp.solutions.pose pose mp_pose.Pose(static_image_modeTrue, model_complexity1, min_detection_confidence0.5) def extract_features(landmarks, img_w640, img_h480): # landmarks: MediaPipe 返回的 33 个关键点列表 pts np.array([[lm.x * img_w, lm.y * img_h] for lm in landmarks], dtypenp.float32) # 以髋关节中心为原点做平移归一化 left_hip pts[mp_pose.PoseLandmark.LEFT_HIP] right_hip pts[mp_pose.PoseLandmark.RIGHT_HIP] hip_center (left_hip right_hip) / 2.0 pts pts - hip_center # 以肩髋距离为尺度因子做缩放归一化 left_shoulder pts[mp_pose.PoseLandmark.LEFT_SHOULDER] right_shoulder pts[mp_pose.PoseLandmark.RIGHT_SHOULDER] shoulder_center (left_shoulder right_shoulder) / 2.0 scale np.linalg.norm(shoulder_center) # 已平移到原点直接取模 if scale 1e-6: return None pts pts / scale # 计算肘关节角、膝关节角、躯干倾角、归一化距离 angle lambda p1, p2, p3: np.degrees(np.arccos( np.clip(np.dot(p1 - p2, p3 - p2) / (np.linalg.norm(p1 - p2) * np.linalg.norm(p3 - p2) 1e-6), -1.0, 1.0))) left_elbow angle(pts[mp_pose.PoseLandmark.LEFT_SHOULDER], pts[mp_pose.PoseLandmark.LEFT_ELBOW], pts[mp_pose.PoseLandmark.LEFT_WRIST]) right_elbow angle(pts[mp_pose.PoseLandmark.RIGHT_SHOULDER], pts[mp_pose.PoseLandmark.RIGHT_ELBOW], pts[mp_pose.PoseLandmark.RIGHT_WRIST]) left_knee angle(pts[mp_pose.PoseLandmark.LEFT_HIP], pts[mp_pose.PoseLandmark.LEFT_KNEE], pts[mp_pose.PoseLandmark.LEFT_ANKLE]) right_knee angle(pts[mp_pose.PoseLandmark.RIGHT_HIP], pts[mp_pose.PoseLandmark.RIGHT_KNEE], pts[mp_pose.PoseLandmark.RIGHT_ANKLE]) # 躯干倾角肩中心到髋中心向量与竖直方向的夹角 torso_vec shoulder_center - hip_center # 已平移肩中心坐标即可 torso_angle np.degrees(np.arctan2(abs(torso_vec[0]), abs(torso_vec[1]))) # 手到髋归一化距离取左右手腕到髋中心的平均距离 left_hand_dist np.linalg.norm(pts[mp_pose.PoseLandmark.LEFT_WRIST]) right_hand_dist np.linalg.norm(pts[mp_pose.PoseLandmark.RIGHT_WRIST]) # 髋到踝归一化距离取左右髋踝距离平均值的倒数关系由数值本身表达 left_leg_len np.linalg.norm(pts[mp_pose.PoseLandmark.LEFT_ANKLE] - pts[mp_pose.PoseLandmark.LEFT_HIP]) right_leg_len np.linalg.norm(pts[mp_pose.PoseLandmark.RIGHT_ANKLE] - pts[mp_pose.PoseLandmark.RIGHT_HIP]) return np.array([left_elbow, right_elbow, left_knee, right_knee, torso_angle, (left_hand_dist right_hand_dist) / 2.0, (left_leg_len right_leg_len) / 2.0])这段代码里有几个关键点需要解释。static_image_modeTrue表示对独立帧做检测而不是当成视频流做跟踪如果输入的是连续视频帧把这个参数改成False并同时传入min_tracking_confidence检测会更稳定但会引入跟踪延迟。角度计算中的np.clip是为了防止浮点误差导致余弦值超出[-1,1]这在实际坐标里很常见不裁剪会出现大量nan。最后返回的7维数组就是喂给SVM的特征向量每一维的物理含义都明确后面模型不好用时可以直接回溯到具体某个特征上去排查问题。2.3 数据采集与标注足够多样本的最小规范特征流程定下来之后下一步是采集训练数据。我一般建议每个姿势类别采集不少于500条有效帧类别越多这个基数再适当放大。采集时不要只让一个人站得端端正正做标准动作那样训练出来的模型只能在同一个人身上用。正确做法是找3到5个人身高体型有差异站在距离摄像头1.5米到3米的不同位置每个姿势做几组变速动作——比如站起来的时候稍微左右晃动一下坐下的时候往前倾一下再坐直。这些看似不标准的动作恰恰是SVM泛化能力的主要来源。采集节奏上视频抽帧比逐张拍照效率高很多。录一段30秒的视频按5到10帧每秒抽帧一个姿势轻松拿到两三百样本。抽完帧打标签时建议把“不确定”的帧直接丢掉不要硬标。训练集里混入错标样本比混入噪声特征危害更大——SVM会对错标点生成诡异的局部边界。另外连续视频帧之间有强相似性训练前做一次帧间隔过滤或者分组可以避免同类样本过度集中影响决策边界。3. 训练SVM姿势分类器核函数、C、gamma的调参路线特征准备好了接下来是训练SVM分类器。这一步的核心不是把代码跑通而是理解三个直接影响分类效果的量核函数、正则化参数C、核宽度gamma。很多人对SVM调参的理解停留在“用网格搜索随便跑一下”但不懂这三个参数的物理含义网格设得再大也是在碰运气。3.1 RBF核为什么是默认选项一次线性核对照SVM的原理是间隔最大化——找一个超平面让两类样本到平面的距离最大。但姿势分类的边界几乎不可能是线性的。举个例子站姿和蹲姿在“膝角-髋踝距离”这个二维平面上的分布是一条弯的边界站姿膝角大髋踝距离大蹲姿膝角小髋踝距离也小但中间还有坐姿这个夹在两者之间的类别。线性核只能画直线遇到这种三分类重叠区域直接失控通常准确率只能到八成上下就上不去了。RBF核的作用是把原始特征映射到高维空间在高维空间里原本弯弯曲曲的边界被拉直SVM再去找线性超平面。映射是隐式的不需要你手动构造高维特征代价是多了gamma这个旋钮。我自己的经验是姿势检测这个任务无脑先上RBF核除非你的分类数只有两类且特征分布明显线性可分才值得试线性核。RBF默认就能覆盖线性核的效果但反过来不行。3.2 特征缩放与类别不平衡两个容易被忽视的前置处理RBF核内部计算的是样本之间的距离依赖特征向量的范数。如果某一维特征数值范围比其他维度大一个量级它会主导整个距离计算其他维度的区分能力等于被废掉。7维特征里角度量纲是0到180而归一化距离量纲是0到几差异不算大但依然建议做标准化。注意标准化必须放进Pipeline里做不能先在整个X上fit再切分训练集和验证集否则验证集的信息会泄漏进训练过程得到虚高的指标。这个坑后面我会专门展开。类别不平衡同样要提前处理。在真实的姿势检测场景里“站”这个状态往往占了六成以上“蹲”可能只有百分之十几。SVM的间隔最大化会被大样本类别牵着走决策边界整体偏向多数类。最省事的做法是训练时指定class_weightbalanced让SVM按类别频率反比调整错误代价。评估指标也要同步换掉——不要盯着accuracy看改用macro F1和混淆矩阵逐类检查召回率否则整体准确率九成但蹲姿完全没识别出来的情况光看总分根本发现不了。3.3 GridSearchCV搜索C和gamma完整训练流程示例下面给出我惯用的训练骨架。特征矩阵X是N行7列来自上一章的特征提取流程y是标签数组类别用整数编码。把StandardScaler和SVC放进同一个Pipeline再用GridSearchCV做交叉验证这样scaler和SVM的参数搜索在同一个流程里完成不会出现泄漏。from sklearn.svm import SVC from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import GridSearchCV, StratifiedKFold from sklearn.metrics import classification_report import numpy as np # X: (n_samples, 7) 特征矩阵, y: (n_samples,) 标签数组 X np.array(feature_list) # feature_list 由 extract_features 逐帧生成 y np.array(label_list) pipeline Pipeline([ (scaler, StandardScaler()), (svm, SVC(kernelrbf, class_weightbalanced, probabilityTrue, random_state42)) ]) param_grid { svm__C: [0.1, 1, 10, 100], svm__gamma: [0.001, 0.01, 0.1, 1] } cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) search GridSearchCV(pipeline, param_grid, cvcv, scoringf1_macro, n_jobs-1) search.fit(X, y) print(best params:, search.best_params_) print(best CV F1:, search.best_score_) final_model search.best_estimator_ y_pred final_model.predict(X) print(classification_report(y, y_pred))这段代码有三个值得展开的细节。第一scoringf1_macro对类别不平衡更敏感比默认的accuracy更能反映小类别的表现如果你特别在意某一类姿势的误报可以改成f1_weighted或者自定义评分器。第二param_grid里的C和gamma都按数量级搜索这是SVM调参的基本节奏——先粗略摸到量级再在最优值附近细化搜索。比如这里C搜到10最优下一步可以把C设为[5, 10, 20]再跑一轮。第三probabilityTrue会启用Platt缩放让SVM额外输出概率值代价是增加训练时间和模型体积如果只做硬分类不需要置信度可以关掉这选项模型会更小推理更快。C和gamma的物理含义直接影响你选择网格范围。C是正则化参数C越大SVM越努力让每个训练样本都分类正确边界越复杂越容易过拟合C越小边界越平滑但欠拟合风险增加。gamma是RBF核的宽度gamma越小决策边界越平滑过大的gamma会在每个样本周围画“孤岛”训练集指标好看来一个新样本就翻车。在姿势检测这个场景C落在1到100之间、gamma落在0.01到1之间是覆盖大多数情况的起始搜索区间。gamma超过1时我就开始警惕过拟合几乎不会用到10以上的值。提示网格搜索跑完先看交叉验证分数和训练集分数的差距。如果训练集F1接近1.0而交叉验证明显低说明C或gamma过大边界过拟合了两个分数都很低说明特征本身区分度不够优先回特征工程调不要执着于调参。4. 姿势检测里最常见的5个翻车现场现象、原因与修复做完特征和训练你以为模型能直接上线了这里分享五个我自己和身边团队真实遇到过的坑按“现象→原因→解决”的顺序写。每一条背后都是一个完整的排错经历有些问题甚至能毁掉整个方案的可信度。4.1 归一化统计量泄漏训练分数虚高得离谱现象训练集F1高达0.97交叉验证分数0.96模型一上真实数据准确率直接掉到0.6。整个团队对着一模一样的数据找了两天原因。原因代码里先对全部特征矩阵X做了StandardScaler().fit(X)然后再切分训练集和验证集。scaler在全体数据上计算的均值和方差包含了验证集的信息等于验证集在考试之前已经拿到了答案。特征标准化后验证集被平移到了训练集附近SVM分起来当然容易。解决把StandardScaler放进Pipeline跟着GridSearchCV一起走。Pipeline保证每一折交叉验证里scaler只在当前训练折上fit验证折完全不参与统计量计算。这也是为什么前一章的代码要把scaler和SVC揉进同一个Pipeline的根本原因。4.2 角度接近0°或180°时噪声放大现象站姿时膝关节接近完全伸直角度在168°到180°之间来回抖模型在“站”和“微蹲”之间跳变画面里人明明没动标签却在闪。原因三点近共线时坐标只要抖动一两个像素反余弦函数计算出的角度就会发生很大变化。关节越接近伸直这种放大效应越明显。这是三角函数本身的数值特性不是SVM的问题。解决两个常见做法我更推荐第一个。一是直接改用cos值替代角度值进入特征向量——余弦函数在0°和180°附近导数趋近于0天然抑制小扰动二是对角度做饱和截断比如大于175°一律按175°算但这只是治标。注意改了特征之后要重新跑一遍交叉验证不要只改计算不改评估。4.3 训练数据太干净单场景泛化的血泪经验现象训练时全部站在同一面白墙前、同一光照、同一距离采集测试时换成走廊、工位或者户外场景误判率急剧上升。原因不是特征没有过滤背景而是采集时人的动作范围太窄。所有人只在固定位置做标准动作很多合法的姿态空间在训练集里完全没有被覆盖。SVM对新特征区域的判断本质上是外推边界画到没见过的地方就是黑匣子给什么结果全靠运气。解决采集阶段故意制造扰动——前后走动、上下蹲起、左右倾斜身体、靠近再远离摄像头。训练数据“脏”一点模型上线才稳。这是我吃过亏换来的血泪经验你采集时偷的懒模型上线后会加倍还给你。4.4 多人同框导致关键点错配现象画面里同时出现两个人时特征向量出现跳变同一个姿势一会识别成站一会识别成坐完全不可用。原因MediaPipe Pose默认只输出一个人多人入画时它会锁定一个目标但在遮挡、交叉、走出画面等情况下会在多人之间跳切关键点坐标跟着错配特征自然乱掉。解决最简单的方案是业务上限制单人入画。如果多人不可避就在关键点检测之前先跑一个人体检测器裁剪出最大目标的ROI再提关键点。先检测再提点而不是直接在整幅图上提点能消除九成以上的多人干扰。这也是为什么我前面强调SVM分类器本身不复杂复杂的是前置的那条数据链路。4.5 类别不均衡把决策边界带偏现象整体准确率93%看起来不错翻混淆矩阵才发现“蹲”的召回率只有35%大量蹲姿样本被分到了“站”。原因训练数据里“站”占了70%以上SVM为了最大化整体间隔牺牲少数类是常规操作。多数类的密度高边界被推挤到少数类一侧这是间隔最大化策略下的必然结果。解决训练时加上class_weightbalanced让少数类样本的错误代价提高。评估改用macro F1逐类看召回率而不是只看总分。如果加了balanced之后少数类召回上来了但精确率掉得厉害说明两类特征在原始空间重叠太严重这时候要做的是回特征工程加维度而不是继续调参。5. 跨场景验证与置信度输出让SVM姿势检测真正可交付模型训好不是终点能不能经得起换人、换场景的考验才决定这个方案值不值得投入。我见过太多团队拿随机切分的交叉验证分数去汇报上线第一天就被真实数据打脸。5.1 按人切分的LOSO验证而不是按帧随机切随机切分训练集和验证集时同一个人的相邻帧有极强的相似性模型相当于已经见过这个人了验证分数虚高。正确的做法是按人分组做验证——把所有样本按人员ID分组训练时彻底排除一个人的全部数据用剩下的人训练再在这个人身上测。这叫做留一受试者交叉验证LOSO。from sklearn.model_selection import LeaveOneGroupOut from sklearn.model_selection import cross_val_score # groups: 与 y 等长的数组每个样本对应的人员ID logo LeaveOneGroupOut() scores cross_val_score(search.best_estimator_, X, y, groupsgroups, cvlogo, scoringf1_macro) print(LOSO F1 per subject:, scores) print(mean LOSO F1:, scores.mean())注意这里用的是上一步网格搜索得到的最优模型且在交叉验证过程中Pipeline会在每一折重新fit scaler。LOSO分数才是这个方案真实上线的预期值。如果你的LOSO分数比随机切分低一大截说明模型对特定人物产生了过拟合回去检查一下你的训练集里是不是某个人占了太多比例。5.2 decision_function做低置信抛弃宁可不判断不要错判断SVM本身输出的是样本到决策超平面的距离不是概率。probabilityTrue会把这个距离通过Platt缩放转成概率但在小样本上这个校准经常不稳定输出的概率值不可全信。我习惯直接用decision_function的距离绝对值作为置信度参考——离超平面越远分类越确定。import numpy as np # 用最优模型对一段视频逐帧预测 distances final_model.decision_function(frame_features) scores distances.max(axis1) # 多分类时取最大间隔对应的分数 pred_classes distances.argmax(axis1) confidence_threshold 0.35 for i, score in enumerate(scores): if score confidence_threshold: pred_classes[i] -1 # -1 表示低置信丢弃该帧阈值0.35不是拍脑袋定的我一般会在验证集上画出正确分类和错误分类的距离分布取两者交集最小的位置。阈值设太严会频繁丢帧设太松又拦不住错判。在交互式应用里输出一个“不确定”状态远比输出一个错误姿势状态要安全前者让上层逻辑有机会兜底后者直接误导决策。这也是SVM方案相比深度学习方案的一个隐形的优势决策边界的几何距离天然就是置信度不需要额外训练一个置信度网络。5.3 特征维度小幅扰动测试上线前最后一关模型交付前我习惯给特征向量加一点小幅度的随机扰动跑一批鲁棒性测试。比如在角度维度上加±5°的噪声在距离维度上加±0.1的噪声观察分类结果是否稳定。SVM对近决策边界的样本本就敏感如果加5%的噪声就有大量样本跨边界翻转说明这个分类边界在真实环境中会因为关键点抖动而不稳定需要回头调整特征或者采集更多边界样本。至于部署sklearn模型用joblib.dump保存边缘设备上用对应的joblib.load加载推理即可模型本身只是支持向量和核函数参数的组合几百KB的大小对嵌入式设备非常友好。推理时只需算一遍特征向量再跑一次决策函数CPU占用率几乎可以忽略这也是SVM方案在工业场景里至今没被淘汰的根本原因。我早期的姿势检测项目就是吃了没做LOSO验证的亏汇报时指标漂亮用户一换立刻露馅从那以后我给自己定了个规矩任何分类模型先按人切分验证再谈上线。特征、参数、验证这条路跑通之后SVM姿势检测这套方案的价值就很清楚了——它不跟深度学习拼上限拼的是在有限资源、有限数据、需要解释的场景里稳稳兜住底线。希望帮到你。本文还有配套的精品资源点击获取