ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SVM分类器姿势检测实战:特征工程、调参与部署避坑

SVM分类器姿势检测实战:特征工程、调参与部署避坑 简介一套基于支持向量机分类器实现姿势检测的完整项目适合机器学习、计算机视觉方向的开发者与初学者。项目采用方向梯度直方图特征提取与支持向量机分类相结合的技术路线包含数据预处理、特征提取、模型训练、实时检测等模块可应用于安全监控、人机交互、虚拟现实等场景。压缩包共22个文件以代码文件、图片、模型文件及示例视频为主其中代码覆盖从特征提取到模型训练与视频检测的完整流程另附模型文件便于直接调用整体约四十四兆字节。已有189人浏览学习资源注释清晰、模块划分明确便于实践支持向量机核心算法也可作为姿势识别项目的参考实现还可结合开源计算机视觉库快速部署。 用SVM分类器做姿势检测在今天的项目里常被当成过时方案但我在实际落地中还是偏向这种组合。它不需要GPU几百个标注样本就能训出一个能上线的分类模型单帧推理不到1毫秒部署时一个joblib文件拷过去就能跑。很多人一上来就上深度学习端到端方案结果为了三个动作的分类和采集数据较劲半个月。这篇文章会把用SVM分类器做姿势检测的完整路径讲清楚从特征构造、训练调参到部署避坑让新手能照着复现也让有经验的开发者少走一点弯路。2. 把姿势变成SVM能读的向量特征工程怎么做2.1 为什么不用原始图像而是先提骨架关键点SVM分类器本质上是在高维空间里找超平面它擅长处理的是低维、稠密、有区分度的特征向量而不是动辄几十万维的原始图像像素。直接把图像拉平喂给SVM特征维度上万样本量跟不上分类器很容易陷入维度灾难训练慢、过拟合快而且对光照、背景、衣服颜色极其敏感。常见做法是先跑一遍姿态估计模型拿到人体的关键点坐标再把这些坐标转换成特征向量。我一般用的是MediaPipe的Pose模块它能稳定输出33个关键点但我实际只取上半身和下半身的关节也就是肩膀、肘、手腕、髋、膝、脚踝这12个点已经足够区分站、坐、蹲、走这类大姿态。关键点坐标本身还不能直接用因为它依赖摄像头分辨率、人物距离画面的远近、拍摄角度。同一个动作人站得远一点坐标数值整体变小SVM分类器就会把距离远近当成特征差异这会让模型泛化能力很差。所以特征工程的第一步是把绝对坐标转换成相对几何特征。2.2 用关节夹角做特征对位置和尺度不敏感角度特征有个天然优势人在画面里平移、缩放角度基本不变。即使是旋转只要不是极端的俯仰视角关节角度也保持稳定。所以我把每个关节的夹角作为主要特征计算方式是用三个关键点的坐标以中间点为顶点求夹角。import numpy as np def compute_angle(a, b, c): 计算以b为顶点、a-b-c构成的夹角返回弧度值 v1 np.array(a) - np.array(b) v2 np.array(c) - np.array(b) norm1 np.linalg.norm(v1) norm2 np.linalg.norm(v2) if norm1 1e-6 or norm2 1e-6: return 0.0 cos_angle np.clip(np.dot(v1, v2) / (norm1 * norm2), -1.0, 1.0) return float(np.arccos(cos_angle))这个函数的输入是三个格式为(x, y)的坐标输出是0到π的弧度值。判断norm小于1e-6是为了防止两个点重合导致除零关键点检测在某些姿态下会出现坐标完全重叠的情况比如手贴在髋关节上这时候角度直接给0比让程序崩溃强。np.clip把余弦值限制在[-1, 1]之间因为浮点误差可能导致cos值略微超出这个区间arccos会返回nan这一步算是个防御性写法。2.3 特征向量不只是角度还要加入比例和倾角只有关节角度有些动作区分不出来。比如站立和站直但微微前倾肘部和膝部角度几乎一样真正的差异在躯干倾角。我还加了两个统计特征肩膀宽度和髋部宽度的比值以及躯干中轴与垂直方向的夹角。def build_feature(pts): 输入pts为12个关键点列表顺序为 0右肩 1右肘 2右腕 3左肩 4左肘 5左腕 6右髋 7右膝 8右踝 9左髋 10左膝 11左踝 输出14维特征向量 angles [ compute_angle(pts[0], pts[1], pts[2]), compute_angle(pts[3], pts[4], pts[5]), compute_angle(pts[6], pts[7], pts[8]), compute_angle(pts[9], pts[10], pts[11]), compute_angle(pts[1], pts[0], pts[3]), compute_angle(pts[4], pts[3], pts[0]), compute_angle(pts[7], pts[6], pts[9]), compute_angle(pts[10], pts[9], pts[6]), ] shoulder_width np.linalg.norm(np.array(pts[0]) - np.array(pts[3])) 1e-6 hip_width np.linalg.norm(np.array(pts[6]) - np.array(pts[9])) 1e-6 width_ratio shoulder_width / hip_width shoulder_mid (np.array(pts[0]) np.array(pts[3])) / 2.0 hip_mid (np.array(pts[6]) np.array(pts[9])) / 2.0 vertical_ref [shoulder_mid[0], shoulder_mid[1] - 1.0] torso_angle compute_angle(vertical_ref, shoulder_mid, hip_mid) feat np.array(angles [width_ratio, torso_angle], dtypenp.float32) return feat这14维特征里8个关节角度、1个肩髋宽度比、1个躯干倾角剩下4维是补充我实际还会把左右侧的角度差值和平均值也拼进去总共14维。这里的顺序只影响训练时的列名不影响svm分类器效果但必须固定下来否则推理时特征顺序对不上模型训练时的顺序结果会完全乱掉。特征维度不是越多越好。有人会把全部33个关键点两两组合算距离特征飙到几百维样本量不够时SVM很容易学偏。我建议从12到20维起步用特征重要性验证之后再决定要不要扩展。3. 数据准备与SVM分类器训练最小可放心代码3.1 数据集的CSV组织方式与样本平衡特征工程做完接下来就是把标注数据存成文件。我习惯用CSV一行一个样本前14列是特征最后一列是标签。feature_0,feature_1,feature_2,feature_3,feature_4,feature_5,feature_6,feature_7,feature_8,feature_9,feature_10,feature_11,feature_12,feature_13,label 0.52,1.21,0.46,1.31,0.87,0.76,0.68,0.72,1.15,0.22,0.44,0.18,1.02,0.35,stand 0.89,1.52,0.61,1.44,0.92,0.81,1.32,1.21,0.55,0.68,0.72,0.63,1.08,0.18,sit采集数据时要注意类别平衡。比如蹲这个动作采集了800帧站只采了200帧SVM分类器在训练时为了降低整体误差会倾向于把所有样本都预测成蹲因为这样准确率也有80%。我一般保证最少的类别不低于最多的类别的一半实在不够就用关键点坐标加随机扰动来扩充比如给每个关键点加±3像素的噪声生成新样本。3.2 训练脚本从CSV到可保存的SVM分类器读取数据后核心流程是拆训练测试集、标准化、训练这三步顺序不能乱。import pandas as pd import joblib from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC from sklearn.metrics import classification_report df pd.read_csv(pose_dataset.csv) X df.drop(label, axis1).values y df[label].values # stratify保证划分后各类别比例与原始数据一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) # 标准化只对训练集fit测试集只transform scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 使用RBF核的SVM分类器 clf SVC(kernelrbf, C10.0, gammascale, probabilityTrue, random_state42) clf.fit(X_train_scaled, y_train) y_pred clf.predict(X_test_scaled) print(classification_report(y_test, y_pred)) joblib.dump({clf: clf, scaler: scaler, features: list(df.columns[:-1])}, pose_svm.joblib)train_test_split里的stratifyy参数很多人会忽略但对类别不平衡的数据集它保证训练集和测试集里各类别占比和原始数据差不多评估结果更可信。test_size0.2意味着20%的样本留出来做测试如果总样本只有300建议把test_size调到0.25到0.3因为测试集太小会导致评估指标波动很大。标准化这里有个关键点fit_transform只用在训练集验证集和测试集用transform。原因是StandardScaler会计算每列的均值和标准差如果让测试集的数据参与计算相当于把测试集的分布信息泄露给了模型评估出来的结果会偏乐观真实场景里表现没有那么好。3.3 模型保存时把scaler一起打包推理才不会踩坑很多人在保存模型时只存了SVM推理阶段直接拿新数据丢给clf.predict()结果准确率暴跌。原因是训练时特征经过了标准化推理时数据还是原始尺度两头对不上。我在保存时把scaler和模型放在同一个字典里这样加载模型之后顺手把scaler也带出来还有一个好处是特征列名也一起保存推理时可以核对输入特征顺序。model joblib.load(pose_svm.joblib) clf model[clf] scaler model[scaler] # 新样本特征确保顺序和训练时一致 new_feat build_feature(keypoints) new_feat_scaled scaler.transform([new_feat]) pred clf.predict(new_feat_scaled)[0]注意scaler.transform接收的是二维数组一行是一个样本所以这里传的是[new_feat]而不是直接传一维向量。这个细节偶尔会让人翻车报错信息是Expected 2D array, got 1D array instead看到这个错就知道是少了一对方括号。4. 评估与调参让SVM分类器的准确率更可信4.1 混淆矩阵看漏检还是误检比准确率更有用只有准确率指标很难判断模型到底是把坐误判成站还是把站误判成坐。这两种错误在实际场景里代价完全不同比如跌倒检测场景里把跌倒漏检成弯腰是严重事故把弯腰误检成跌倒只是造成一次误报警。所以我会用混淆矩阵把各类别之间的错误分布看清楚。import numpy as np from sklearn.metrics import confusion_matrix labels np.unique(y_train) cm confusion_matrix(y_test, y_pred, labelslabels) vis pd.DataFrame(cm, indexlabels, columnslabels) print(vis)假设输出里stand这一行、列是sit的格子数值特别高说明站这个动作容易被误判成坐检测逻辑上通常是站和坐的躯干倾角以及膝部角度区间重叠太多。有时候模型把蹲漏检成站原因可能是蹲这个类别的样本里包含了不少半蹲过渡动作标注的人自己都摇摆不定模型自然也学不清楚。遇到这种混淆先不要急着调SVM分类器的超参数先回去看特征分布。把两个易混类别的特征用直方图画出来如果分布重叠严重说明是特征不够加角度差分或躯干倾角能拉开差距如果分布有明显分界但SVM还是错再考虑调C和gamma。4.2 用网格搜索调C和gamma不要凭感觉设RBF核的SVM分类器有两个参数最关键C控制误分类惩罚力度gamma控制单个样本的影响半径。C太大容易过拟合训练集准确率很高但测试集不行C太小模型会过于宽松连训练集都分不开。gamma同理取值很大时每个样本都只影响自己周边很小的区域决策边界弯弯曲曲。from sklearn.model_selection import GridSearchCV param_grid { C: [0.1, 1, 10, 100], gamma: [scale, 0.01, 0.1, 1], kernel: [rbf] } grid GridSearchCV( SVC(probabilityTrue, random_state42), param_grid, cv5, scoringf1_macro, n_jobs-1 ) grid.fit(X_train_scaled, y_train) print(grid.best_params_)cv5表示五折交叉验证每折都是80%训练20%验证模型对每一折都能得到一个指标最后取平均。scoringf1_macro适合多分类且类别不平衡的场景它会先算每个类别的f1再取平均比accuracy更稳评估。n_jobs-1让所有CPU核心并行参数组合一共是16个组合乘5折共80次训练一般十几秒就结束。网格搜索结果的解读有点玄学我见过很多次最佳参数是C100、gamma0.1但是换一批数据就变成C10、gammascale。所以网格搜索只用来确定数量级不要当成最优解。确定区间后再手工微调一下比如C在10附近试试15、20立刻就能提高零点几个点的f1。4.3 核函数选择RBF并不总是唯一答案RBF核是万金油能拟合非线性边界但它不是免费的。样本量大到几万条以上时RBF核的训练时间和模型体积都会明显膨胀。这时候线性核配合好的特征工程往往更实用。核函数适用场景训练时间模型可解释性我的用法linear特征维度高、样本量大、边界接近线性快可以看权重当基线模型先跑一遍rbf中小样本、边界复杂、角度特征非线性明显中等基本无法解释日常首选poly有明确的特征交叉需求慢一般很少用容易过拟合我在实际项目里的做法是先用LinearSVC训练一个简单分类器再看测试集f1。如果线性核的效果和RBF差不多优先用线性核因为它的模型体积小、推理速度快。只有在线性核明显不够用时才切换到RBF并做网格搜索。核函数的选择不是越复杂越好而是匹配数据规模。5. 姿势检测的避坑与常见问题排查5.1 训练集准确率99%验证集一塌糊涂现象SVM分类器在训练集上的准确率几乎满分但每次用验证集评估都掉到70%以下。原因数据泄露或过拟合。最常见的是采集数据时同一个人的连续视频帧被随机拆分到了训练集和测试集相邻两帧几乎长得一样模型其实是在记忆样本而不是学姿势规律。其次标准化时用全量数据fit也会让评估结果失真。解决按人物或按视频片段来划分数据集保证同一个人或者同一段连续动作只出现在训练集或测试集里。我当时是写了按视频目录名切分的函数python里用groupby之后shuffle再手动分配进train或test。标准化也必须只fit在训练数据上见3.2节。5.2 换摄像头或者换人之后准确率跌掉一半现象在自己工位上测试效果不错拿到客户那里同一个动作全被识别错。原因SVM分类器对输入特征分布很敏感。换摄像头后分辨率、帧率、画面裁切范围都不一样MediaPipe关键点的坐标数值范围变化很大。虽然用了角度特征削弱了缩放影响但摄像头广角导致的畸变和人物在画面中的比例变化依然会造成特征漂移。解决训练数据里加入多样化样本多找几个人、用不同分辨率录制。特征层面把角度特征再相对躯干倾角做一次差分相当于把坐姿和站姿的共同偏移量抵消掉。还有个笨方法但很有效就是拿着客户现场的截图补采几十个样本微调一次SVM很快就能拉回准确率。5.3 蹲和坐总是分不清现象蹲这个动作有接近30%的概率被识别成坐在混淆矩阵里这两个类别的格子特别亮。原因蹲和坐的下肢角度其实非常接近膝盖和髋部的夹角都在相近区间真正的差异在于重心高度和重心投影位置。而我这个特征向量只用了关节角度和肩髋比值没包含关键点的绝对高度信息等于把最关键的区分信息人为抹掉了。解决加入归一化高度特征比如胯部中点的y坐标除以图像高度这是一个0到1的相对值能捕捉重心高低。但不能直接加原始坐标因为离摄像头远近不同会让这个数值变化太大。用人物自身的身高比例来归一化会更稳我后面把每个关键点的y坐标减去踝关节y坐标再除以两肩到两踝的垂直距离这样重心高低被表达成占身高的比例对距离远近就不敏感了。5.4 训练时间长达20分钟参数怎么调都慢现象网格搜索跑了一个多小时还没出结果普通训练也要20分钟以上。原因样本量过万RBF核的SVM分类器在训练时需要计算样本两两之间的核矩阵复杂度大约是O(n²)n翻一倍时间就变成四倍。另一个隐藏原因是CSV里混有字符串类型的列df.values转换后整个数组变成了object类型sklearn还在里面做类型转换额外耗时间。解决先确认X.dtype是不是float32。如果是object就说明有脏列只选数值特征列再转一次类型。样本量超过两万条时我会改用LinearSVC或者SGDClassifier训练时间从分钟级降到秒级准确率损失通常在1%以内对落地项目来说完全值得。5.5 单帧推理要50毫秒帧率只有10几现象SVM本身的predict只要零点几毫秒但整体链路跑下来非常卡fps上不去。原因瓶颈根本不在SVM分类器而在MediaPipe关键点检测。MediaPipe的Pose模型在CPU上单帧大约要20到40毫秒如果再叠加视频解码和图像缩放掉到10几帧很正常。解决不要每帧都跑检测加SVM推理。常见做法是隔两到三帧跑一次姿态估计中间帧用上一帧的结果或者在检测线程和分类线程之间加一个有界队列。我实际项目里是视频30帧每3帧做一次完整推理最终能稳定到每秒10次判断已经够用。想要更快的话就把输入图像分辨率降到256宽度MediaPipe在低分辨率下速度提升明显关键点精度损失有限。6. 部署到真实环境稳定性的几个验证技巧6.1 单帧误判用滑窗投票解决SVM本身没有时序记忆单帧关键点抖动或者人体瞬间遮挡都会造成预测标签跳变。我在推理前加一个固定长度的deque每次用最近N帧的预测结果做多数投票能显著减少疯狂闪烁的情况。from collections import deque class PoseVoter: def __init__(self, clf, scaler, window5): self.clf clf self.scaler scaler self.buffer deque(maxlenwindow) def predict(self, feat): feat_scaled self.scaler.transform([feat]) pred self.clf.predict(feat_scaled)[0] self.buffer.append(pred) return max(set(self.buffer), keyself.buffer.count)window不是越大越好长度5到7比较平衡。窗口太长会导致动作已经切换了输出还停留在旧状态比如人已经站起来2秒投票结果仍然显示坐。max(set(self.buffer), keyself.buffer.count)是python里对deque做多数投票的紧凑写法统计出现次数最多的标签。这段代码在真实摄像头环境下能减少大约80%的单帧毛刺。6.2 用predict_proba设置置信度阈值给未知动作留余地真实场景里总会出现训练数据没覆盖的姿势比如用户伸懒腰、弯腰捡东西SVM分类器会强行把它们归到最接近的类别里。我加了置信度兜底逻辑用predict_proba输出每个类别的概率最大概率低于某个阈值就丢弃这一帧。def predict_with_threshold(feat, threshold0.5): feat_scaled scaler.transform([feat]) proba clf.predict_proba(feat_scaled)[0] best_idx int(np.argmax(proba)) if proba[best_idx] threshold: return unknown return clf.classes_[best_idx]阈值要靠实测校准我一般先采集几段包含目标动作的视频看正确分类时概率普遍在什么区间。若正确样本的概率通常在0.7以上阈值就设0.5若有些目标动作本身被识别得模棱两可正确概率只有0.55那阈值设太高反而会把正确结果丢掉。predict_proba在训练SVC时要求probabilityTrue这个是额外的Platt缩放过程会让clf.fit变慢不少但对落地阶段很有用。6.3 整体链路的验证方法部署前我习惯用一段真实的视频而不是单张图片来验证统计三个指标每帧平均推理耗时、标签跳变次数、以及每类动作的持续时长分布。标签跳变次数能直接反映滑窗效果是否到位持续时长分布则可以看出误检不是随机出现而是集中在某些动作切换的瞬间。我自己的习惯是每次改完特征或者调完参数都把固定的验证视频和运行日志存下来跟上一版结果对比。这个方向看起来简单但最后比的往往是谁的细节做得更扎实。希望这些踩过的坑和验证思路能给正在做SVM分类器姿势检测的你一些帮助。本文还有配套的精品资源点击获取
返回列表