
简介基于支持向量机SVM与方向梯度直方图HOG特征实现姿势检测的完整项目资源面向计算机视觉、机器学习初学者及AI应用开发者适用于行人姿态识别、人机交互、安全监控等场景。压缩包内共22个文件其中5个Python脚本覆盖特征提取、SVM训练与视频检测全流程13张JPG图片为不同姿势的测试样本另有预训练模型、配置文件及AVI/MP4演示视频包体大小44MB。已有189人学习下载具备不错的参考价值。读者可通过该资源直观理解HOG特征向量的细胞单元、块归一化等构造步骤以及SVM核函数选择、惩罚参数调优、模型训练与调用的完整链路并能直接利用训练好的模型对静态图像和实时视频进行姿势检测实验。项目代码结构清晰适合作为机器学习入门实践的动手素材。1. 基于SVM分类器实现姿势检测HOG特征 线性SVM的落地工程包“基于SVM分类器实现姿势检测”这个项目把传统计算机视觉里最经典的一条路线——HOG特征描述人形轮廓再用SVM分类器决定“这里有没有人、是什么姿势”——做成了一个可以直接跑的完整工程包。里面不只有一份训练好的svm.model还有extract_features.py、train_svm.py、detector.py、video_detector.py四份脚本以及config.py集中管理全部参数。整条链路不需要GPU普通CPU上跑视频检测也能保持可用帧率。适合两类人一是刚入门目标检测、想理解HOG和SVM是怎么配合的开发者二是需要在轻量场景里快速验证传统CV方案的工程师。这套东西能解决的核心问题就是让你别从零搭起而是直接改参数、换数据、重训模型。2. 先拆检测链路HOG特征如何描述“人形”SVM如何分类姿势2.1 HOG特征提取从灰度图到方向梯度直方图的完整步骤HOGHistogram of Oriented Gradients的核心想法很朴素一张图里目标的边缘和形状可以用局部区域中梯度方向的统计分布来近似表达。一个人站着还是蹲着躯干和四肢的边缘朝向会有系统性差异把这种差异量化成特征向量分类器才有东西可学。实际提取时分为几步。先把图像灰度化再做Gamma归一化来压制光照和阴影的干扰然后对每个像素算梯度幅值和方向通常用一维梯度算子[-1, 0, 1]分别在X和Y方向做卷积。接着把图像切成若干8×8像素的细胞单元cell在每个cell内把360度方向的梯度投票到9个直方图bin里得到一个9维描述。最后把相邻的2×2个cell合成一个块block对这个块内串联后的直方图做一次L2范数归一化这一步主要消除局部光照和对比度差异带来的影响。OpenCV把这套流程封装在了cv2.HOGDescriptor里只需要设置五个参数就能干活我一般这样初始化import cv2 hog cv2.HOGDescriptor( _winSize(64, 128), # 检测窗口大小训练和推理必须一致 _blockSize(16, 16), # 块大小决定归一化的局部范围 _blockStride(8, 8), # 块滑动步长控制特征重叠程度 _cellSize(8, 8), # 细胞单元大小 _nbins9 # 梯度方向直方图的bin数量 )参数里最影响特征维度的是cellSize和blockStride。窗口64×128、cell 8×8、block 16×16、stride 8×8、nbins 9时整个窗口被划分成7×15个块每个块串4个cell的直方图最终特征维度是7×15×4×93780维。这个维度是固定的正好是SVM能直接吃进去的标准输入格式。cellSize从8改成4特征维度会涨到四倍以上训练和推理内存消耗都上来了但对小目标的细节捕捉会更好需要根据自己数据去权衡。2.2 为什么选线性SVM高维特征下核函数的取舍分类器的任务是找一个决策边界把正负样本在特征空间里划开。SVM的做法是找间隔最大的那个超平面边界由离它最近的那些样本点决定这些点称为支持向量。核函数决定这个边界在原始空间里是直线还是曲线。这个项目里选线性核就够了原因很实际HOG特征维度动辄几千高维空间里样本通常已经近似线性可分硬上RBF核或者多项式核容易过拟合训练时间也成倍增加。线性核需要调的参数主要就一个——惩罚系数C。C小说明对误分类容忍度高、决策边界更平滑C大说明模型更偏向把训练样本全部正确分开但泛化能力可能变差。样本量不大的姿势检测场景里我一般会先把C设在0.01附近观察交叉验证结果再往大调。还有一个现实约束直接影响选型OpenCV的HOGDescriptor做滑动窗口检测时底层只支持线性SVM的系数向量。用RBF核可能在离线测试里指标好看但落不到detectMultiScale的推理管线上。这也是为什么很多实际项目宁可牺牲一点精度也要用线性核。顺带说一个常被拿来对比的方案决策树分类器。决策树对高维连续型特征不太擅长容易过拟合而且它的输出是类别标签不像SVM给的是“样本到超平面的距离”这个连续置信度。置信度在做检测框过滤时很有用——score低于阈值的窗口可以直接丢弃决策树做不到这一点。2.3 项目文件结构四份脚本在检测链路中的分工打开这个源码包文件分两类。数据部分models目录下的svm.model是训练好的分类器test_image里放着13张测试图片另有pedestrians.avi和Esquina_Democratica.mp4两个视频用于帧级验证代码部分config.py集中管理路径和参数extract_features.py负责从训练图片中批量提取HOG特征train_svm.py读取特征并训练出svm.modeldetector.py和video_detector.py分别对图片和视频做推理。整条数据流是训练图片 →extract_features.py生成特征矩阵和标签 →train_svm.py训练并保存模型 → 推理时detector.py加载模型做滑动窗口检测。config.py里一个参数的改动会同时影响特征提取和训练两条路径。所以拿到这套代码的第一步不是急着训练而是把config.py从头到尾过一遍确认每个值都符合自己的预期。3. 把模型训出来config.py参数设置与训练全流程3.1 config.py参数解读窗口、细胞、块和惩罚系数C这个项目的config.py主要集中了四类参数数据路径、HOG参数、SVM训练参数、推理参数。我拿到任何一份目标检测源码包都会先读这几个值因为它们共同决定了特征维度和训练行为参数名推荐值作用WIN_SIZE(64, 128)检测窗口尺寸训练和推理必须一致CELL_SIZE(8, 8)细胞单元尺寸越小特征越密BLOCK_SIZE(16, 16)块尺寸控制归一化的局部范围BLOCK_STRIDE(8, 8)块滑动步长越小特征重叠度越高N_BINS9梯度方向直方图柱数SVM_C0.01惩罚系数控制误分类容忍度和过拟合程度这里重点说三个。第一是WIN_SIZE训练样本、extract_features.py、detector.py三处使用的窗口必须一致否则特征维度对不上模型加载后直接报错。第二是BLOCK_STRIDE如果从8改小到4特征维度会从3780暴涨到15000以上训练时间和内存都明显上升改大了又容易漏掉局部结构信息。第三是SVM_C在样本量小的时候最敏感0.01是一个稳妥起点。当训练样本超过几百张时我会把C往0.1方向调大多数情况下效果更稳。路径类参数也值得花一分钟核对。extract_features.py要读正负样本目录train_svm.py要写features.npy和models/svm.modeldetector.py要加载models/svm.model。源码包里的路径大概率是相对路径跨平台跑的时候容易因为路径分隔符不一致报FileNotFoundError。我习惯把config.py里的路径改成基于os.path.dirname(__file__)的方式拼接无论项目挪到哪个目录都不会跑丢。3.2 extract_features.py把训练图片批量转成HOG特征向量extract_features.py做的事不复杂遍历训练图片目录对每张图片统一缩放到WIN_SIZE计算HOG特征把特征和标签拼成一个特征矩阵保存。常见实现是这样的import cv2 import numpy as np import os from config import WIN_SIZE, CELL_SIZE, BLOCK_SIZE, BLOCK_STRIDE, N_BINS def compute_hog_feature(img): 计算单张图片的HOG特征向量 img cv2.resize(img, WIN_SIZE) # 统一窗口大小 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) hog cv2.HOGDescriptor( _winSizeWIN_SIZE, _blockSizeBLOCK_SIZE, _blockStrideBLOCK_STRIDE, _cellSizeCELL_SIZE, _nbinsN_BINS ) feat hog.compute(gray) # 返回 (N, 1) 列向量 return feat.flatten() # 拉成一维特征向量 def extract_data(pos_dir, neg_dir): features, labels [], [] # 正样本标签为1代表有人/有目标姿势 for fname in os.listdir(pos_dir): img cv2.imread(os.path.join(pos_dir, fname)) if img is None: continue features.append(compute_hog_feature(img)) labels.append(1) # 负样本标签为0代表背景 for fname in os.listdir(neg_dir): img cv2.imread(os.path.join(neg_dir, fname)) if img is None: continue features.append(compute_hog_feature(img)) labels.append(0) return np.array(features), np.array(labels) if __name__ __main__: feats, labs extract_data(dataset/pos, dataset/neg) np.save(features.npy, feats) np.save(labels.npy, labs) print(特征矩阵维度:, feats.shape)关键点在第一处hog.compute返回的是一个(N, 1)的列向量flatten()之后才变成Sklearn或OpenCV SVM能接受的(1, N)样本格式。第二个容易踩的坑是cv2.imread读入的图片尺寸如果比WIN_SIZE还小resize会强行拉伸出很多插值噪声。我一般在加载后检查img.shape[0]和img.shape[1]至少保证宽高不小于64和128否则这张样本要么丢掉要么先裁剪再缩放。正样本目录和负样本目录的图片数量差距也不要太大建议控制在3:1以内负样本过多时SVM的决策边界会偏保守后面推理阶段误检少了但漏检会明显变多。3.3 train_svm.py用OpenCV训练并保存svm.model训练脚本读入上一步保存的特征矩阵和标签构造SVM分类器设置核函数和C值训练完成后把模型存到models目录。推荐直接用OpenCV的SVM而不是Sklearn原因很直接OpenCV的模型保存格式可以和cv2.HOGDescriptor在推理侧直接配合省掉一次模型转换import cv2 import numpy as np from config import SVM_C, MODEL_PATH # 加载特征和标签类型必须严格匹配 features np.load(features.npy).astype(np.float32) labels np.load(labels.npy).astype(np.int32) # 创建SVM分类器 svm cv2.ml.SVM_create() svm.setType(cv2.ml.SVM_C_SVC) # C-SVC适合二分类 svm.setKernel(cv2.ml.SVM_LINEAR) # 线性核 svm.setC(SVM_C) # 惩罚系数 svm.setTermCriteria((cv2.TERM_CRITERIA_MAX_ITER, 1000, 1e-6)) # 训练 svm.train(features, cv2.ml.ROW_SAMPLE, labels) # 保存模型 svm.save(MODEL_PATH) print(f模型已保存到 {MODEL_PATH})这里有个容易被忽略的细节setTermCriteria指定了迭代上限和精度阈值不设置时某些OpenCV版本会用很小的默认值模型可能还没收敛就提前退出训练。另一个常见问题是数据类型OpenCV SVM要求特征必须转成np.float32标签必须是np.int32直接用numpy默认的int64或者float64会在训练时抛类型不匹配的异常。项目自带的svm.model就是这条管线产出的结果。如果你打算用自己采集的数据重训我强烈建议先把原来的svm.model备份一份——训练过程本身是随机的吗不是但你的数据变了模型效果大概率会比原作者调好的差这时候需要回到这个文件来对比验证。3.4 验证训练结果用test_image目录里的13张图片跑推理模型保存成功后用detector.py跑一下test_image里的13张测试图片确认模型不是“训完了不能用的纸面模型”。detector.py的常见实现思路是先加载SVM模型再把它转成HOGDescriptor能接受的检测器向量import cv2 import numpy as np from config import MODEL_PATH def load_detector(model_path): 从SVM模型构造HOG检测器向量 svm cv2.ml.SVM_load(model_path) # 取支持向量和决策函数系数 sv svm.getSupportVectors() rho svm.getDecisionFunction(0)[0] weight sv[0] if sv.shape[0] 1 else sv.reshape(-1) detector np.append(weight, -rho) return detector def detect_image(image_path, detector): hog cv2.HOGDescriptor() hog.setSVMDetector(detector) img cv2.imread(image_path) boxes, weights hog.detectMultiScale( img, winStride(4, 4), padding(8, 8), scale1.05 ) for (x, y, w, h), score in zip(boxes, weights): if score 0.5: cv2.rectangle(img, (x, y), (x w, y h), (0, 255, 0), 2) return img if __name__ __main__: detector load_detector(MODEL_PATH) result detect_image(test_image/8.jpg, detector) cv2.imwrite(test_result.jpg, result)load_detector函数是所有推理代码里最关键的拼图。svm.model本身不能直接喂给HOGDescriptor必须把支持向量和决策函数的截距拼成一个一维向量OpenCV的HOG检测器内部拿这个向量去计算每个滑动窗口的得分。rho就是决策函数里的偏移项在OpenCV里它返回的是负值拼接时取其相反数。detectMultiScale的winStride和scale是直接影响检测效果的两个参数。winStride控制滑动窗口步长步长越小检测越精细但计算量越大scale控制图像金字塔的缩放系数1.05 表示每层缩小5%对近距离大目标比较友好1.1 以上则更偏向检测中等大小目标。推理时先不追求速度把这两个值调保守一点确认13张测试图都能框出目标再去考虑性能优化。4. 避坑指南从svm.model到视频检测的五个高发坑4.1 模型训练好了detectMultiScale检出全是空框现象train_svm.py跑完模型文件正常生成detector.py也加载成功但detectMultiScale返回的矩形列表是空的13张测试图一张都检不出来。原因90%的情况是setSVMDetector用的向量构造错了。cv2.HOGDescriptor不接受普通SVM模型对象它要求把支持向量和决策函数截距拼成符合内部格式的一维数组。拼接顺序、正负号其中一个错了检测器得分全部是负值滑动窗口一个都过不了阈值。另外直接拿svm.model去predict而不是走detectMultiScale得到的输出也不能直接当作检测框。解决按上面3.4节load_detector的写法用getSupportVectors()取权重向量用getDecisionFunction(0)[0]取rho拼接时用np.append(weight, -rho)。每个OpenCV版本对getSupportVectors的返回结构有细微差异保险起见先print(sv.shape)确认维度如果返回多行只取对应决策函数那行。4.2 单张图片能检到视频里检测框疯狂抖动现象detector.py跑单张测试图片检测框位置基本合理换成video_detector.py跑pedestrians.avi相邻帧的框跳来跳去位置抖、尺寸也忽大忽小。原因detectMultiScale是逐帧独立检测的没有任何时间上下文。视频帧的压缩噪声、运动模糊、光照变化都会让同一个目标在相邻帧拿到略有偏移的检测框逐帧看都还行连起来看就非常难受。解决给框做时间维度的平滑最简单的办法是对检测框的中心坐标做指数移动平均或者直接限制检测框在一两帧内的位移幅度。另一个思路是把winStride从 (4,4) 调大到 (8,8)滑窗重叠度降低后检测结果的随机抖动也会明显减少代价是小目标更容易漏检。我会先用scale1.05观察抖动不够再往1.1调。4.3 站姿能检到、坐姿和背面漏检严重现象用源码包自带的svm.model检测站立、面朝镜头的行人能框出来坐着的、背对摄像头走过去的行人全部漏检。原因正样本里站姿和正面视角覆盖太多HOG特征对姿态和视角非常敏感训练集分布偏向某一类决策边界就对其他姿态产生系统性误判。这和SVM算法本身没关系是数据分布和测试场景不一致。解决扩大训练集的姿态覆盖把坐、蹲、弯腰、侧身、背面都收进正样本。每类姿势至少保证几十个样本不要为了凑数量重复裁剪同一张图的不同区域。原始项目没有提供姿势标签需要自己采集或从视频中截帧、裁剪、打标签再用extract_features.py重新生成特征并训练。4.4 误检多到没法看路灯杆、垃圾桶都被框出来现象跑Esquina_Democratica.mp4时除了行人路灯杆、垃圾桶、墙边的绿植都被画上了框。原因负样本太少或者背景太单调。detectMultiScale会在每个尺度的每个窗口都做一次分类训练时负样本如果只是网上找的几十张风景图和视频实际背景差异太大分类器无法区分“真实的背后背景”和“需要丢弃的背景”。解决从实际视频中截取误检帧把误检区域存成负样本加入训练集重新训练。这种“困难负样本挖掘”是目标检测里最经典的做法。做法很粗暴但有效跑一遍视频把所有误检框对应的图像区域存成jpg放进负样本目录重新extract_features.pytrain_svm.py通常一轮下来误检数量能砍掉一半以上。4.5 OpenCV版本不同svm.model加载失败现象在自己机器上训练好模型拷贝到同事电脑或者其他部署环境detector.py报模型加载失败。原因OpenCV从3.x到4.x的SVM模型序列化格式有过调整4.0之后的文件解析方式不完全兼容3.4生成的模型反过来也一样。直接拷贝.model文件不是跨环境传递模型的好方法。解决最保险的做法是不要传递.model文件而是把训练好的支持向量和rho另存为numpy的.npy文件在新环境里重新拼装检测器向量。另一个办法是在项目里用requirements.txt固定OpenCV版本部署时统一用同一个版本避免模型文件格式漂移。5. 进阶把二分类检测器扩展成多姿势分类器5.1 一对多策略为每个姿势训练一个SVM源码包默认解决的是“有人/没人”二分类真要识别“站、坐、蹲、走”这些具体姿势需要升级为多分类。最稳的方案不是改单个SVM为多核而是训练N个一对一多分类器——每个姿势类别训练一个独立SVM正样本是这个姿势负样本是其他姿势加背景。训练时三个SVM的config.py需要分别维护或者在config.py里加一个POSTURE_TYPE字段区分。每个分类器用相同的HOG参数和窗口尺寸保证特征维度一致。推理时对同一个滑动窗口分别调用三个SVM的得分取得分最高的作为最终姿势标签。5.2 多分类推理时的置信度比较与NMS三个SVM同时对一个窗口输出得分直接比较原始得分不严谨因为不同分类器的决策边界差异会导致分数分布不一致。常见做法是先对每个分类器的得分做归一化——用验证集统计每个分类器得分的均值再乘以一个校准系数。更简单的操作是只关注相对大小把阈值统一设定在同样的分位点这样误检不会跟着某个分类器跑偏。另一个绕不开的步骤是非极大值抑制NMS。同一目标可能被多个窗口覆盖导致多个姿势分类器同时产生高响应。先在每个姿势内部做NMS合并重叠框然后对最终保留的框比较不同姿势的归一化得分输出得分最高的标签。这一步在video_detector.py里用OpenCV的cv2.dnn.NMSBoxes就能完成不需要自己实现IoU计算。5.3 验证多分类模型的工程习惯多分类模型比二分类更容易出现“看起来效果很好、实际换个场景就崩”的情况。我的习惯是固定一个视频片段作为回归测试集每次重训之后跑一遍完整检测记录三类指标站立检出率、坐姿检出率、误检框数量。这三个数字同时记录不能只看其中一个。如果站立检出率涨了但误检框翻倍说明模型的决策边界过于激进需要调高阈值或补充负样本。从那以后我每次调整config.py或者补充训练数据都强制走完一遍回归测试流程把三个指标记录下来再决定是否保留修改。这一步能挡住很多“感觉差不多、实际上退步了”的改动。这个项目本身的代码结构已经把二分类链路搭得很干净往上扩展多分类并不需要重写只是在extract_features.py和detector.py里增加一个姿势维度的循环。希望这份拆解能让你少走几步弯路直接把这个工程包用起来。本文还有配套的精品资源点击获取