【实战解析】HOG+SVM行人检测:从特征提取到模型部署的Python全流程
1. HOGSVM行人检测基础入门第一次接触行人检测时我被各种专业术语搞得晕头转向。直到亲手实现了HOGSVM方案才发现这个经典算法远比想象中简单实用。HOGHistogram of Oriented Gradients本质上就是统计图像局部区域的梯度方向分布而SVMSupport Vector Machine则是个擅长找分类界限的数学高手。两者结合就能让计算机学会识别图像中的行人。为什么选择这个组合我在实际项目中对比过多种方案深度学习虽然精度高但动不动就要GPU支持传统模板匹配又太死板。HOGSVM就像个折中的优等生——在普通笔记本上就能跑实时检测准确率也不差。特别适合需要快速搭建原型的场景比如智能监控、无人小车避障这些应用。这里有个生活化的理解把图像想象成黑白格子衬衫HOG就是统计每个小格子里的条纹走向。行人衣服的纹理走向往往很有特点比如垂直的裤缝、斜跨的背包带这些统计特征比直接看像素值靠谱多了。SVM则像经验丰富的安检员通过大量样本训练能快速判断哪些特征组合代表行人。2. 手把手实现HOG特征提取2.1 从零开始计算HOG让我们用Python还原HOG的计算过程。先准备一张64x128像素的行人图片INRIA数据集里直接有现成的import cv2 import numpy as np from matplotlib import pyplot as plt # 读取图像并转为灰度 img cv2.imread(person.png, cv2.IMREAD_GRAYSCALE)第一步计算梯度。我用Sobel算子来获取水平和垂直方向的梯度值# 计算x和y方向的梯度 gx cv2.Sobel(img, cv2.CV_32F, 1, 0) gy cv2.Sobel(img, cv2.CV_32F, 0, 1) # 计算梯度幅值和方向 magnitude, angle cv2.cartToPolar(gx, gy) angle np.rad2deg(angle) % 180 # 转换为0-180度这里有个坑要注意OpenCV的Sobel默认是8位输出会丢失负值。必须用CV_32F格式才能保留完整的梯度信息。我在第一次实现时就栽在这个细节上导致后续特征完全不准。2.2 分块统计梯度直方图接下来把图像划分成8x8的Cell每个Cell统计9个bin的直方图每20度一个区间# 初始化HOG特征向量 hog_features [] # 8x8的Cell大小 cell_size (8, 8) # 遍历所有Cell for y in range(0, img.shape[0], cell_size[0]): for x in range(0, img.shape[1], cell_size[1]): cell_mag magnitude[y:ycell_size[0], x:xcell_size[1]] cell_angle angle[y:ycell_size[0], x:xcell_size[1]] # 统计直方图 hist np.zeros(9) for i in range(cell_size[0]): for j in range(cell_size[1]): bin_idx int(cell_angle[i,j] / 20) % 9 hist[bin_idx] cell_mag[i,j] hog_features.extend(hist)这样得到的特征维度是(64/8)*(128/8)*93780维。但直接使用这些特征效果并不好还需要进行Block归一化——将相邻的2x2个Cell组成Block对Block内的特征做L2归一化。这个步骤很关键能减少光照变化的影响。3. SVM模型训练实战技巧3.1 数据准备与特征处理我用INRIA数据集做演示这个数据集已经标注好了行人区域。正样本是剪裁好的行人图像64x128负样本则是随机背景区域。建议自己生成负样本时多采集些困难样本比如路灯、树干等容易误检的对象。from sklearn.svm import LinearSVC from sklearn.model_selection import train_test_split import os # 加载正样本 pos_dir INRIAPerson/train_64x128_H96/pos/ pos_features [] for img_file in os.listdir(pos_dir): img cv2.imread(os.path.join(pos_dir, img_file), 0) hog compute_hog(img) # 前面实现的HOG计算函数 pos_features.append(hog) # 加载负样本类似代码省略 neg_features [...] # 合并数据集 X np.vstack([pos_features, neg_features]) y np.hstack([np.ones(len(pos_features)), np.zeros(len(neg_features))]) # 划分训练测试集 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2)3.2 模型训练与参数调优使用scikit-learn的LinearSVC训练模型# 初始化SVM svm LinearSVC( C0.01, # 正则化参数 losshinge, max_iter10000, random_state42 ) # 训练 svm.fit(X_train, y_train) # 评估 print(Test accuracy:, svm.score(X_test, y_test))这里有几个调参经验参数C控制分类严格程度。值太小会导致欠拟合太大可能过拟合。我通常从0.01开始网格搜索类别平衡行人检测通常负样本远多于正样本。可以设置class_weightbalanced自动调整特征标准化虽然HOG做过归一化但整体特征再做一次StandardScaler有时能提升效果4. 实时视频检测系统搭建4.1 滑动窗口检测实现要让模型检测任意尺寸的行人需要实现滑动窗口图像金字塔def detect(img, svm, hog, scale1.0): # 缩放图像 resized cv2.resize(img, (0,0), fxscale, fyscale) # 滑动窗口步长8像素 step_size 8 window_size (64, 128) # 存储检测结果 boxes [] for y in range(0, resized.shape[0] - window_size[1], step_size): for x in range(0, resized.shape[1] - window_size[0], step_size): # 提取窗口 window resized[y:ywindow_size[1], x:xwindow_size[0]] # 计算HOG特征 feat hog.compute(window).reshape(1, -1) # 预测 if svm.predict(feat)[0] 1: boxes.append(( int(x/scale), int(y/scale), int(window_size[0]/scale), int(window_size[1]/scale) )) return boxes4.2 使用OpenCV优化性能直接调用OpenCV的HOGDescriptor能大幅提升速度# 初始化OpenCV HOG hog cv2.HOGDescriptor() hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector()) # 实时检测 cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break # 检测行人 boxes, _ hog.detectMultiScale(frame, winStride(8,8), padding(16,16), scale1.05) # 绘制结果 for (x,y,w,h) in boxes: cv2.rectangle(frame, (x,y), (xw,yh), (0,255,0), 2) cv2.imshow(Detection, frame) if cv2.waitKey(1) 27: break这里有几个实用参数winStride滑动窗口步长越大速度越快但可能漏检scale图像金字塔缩放系数1.05是个平衡值padding边缘填充避免边缘目标被截断5. 工程实践中的常见问题在实际部署时我发现这些问题最常出现光照变化敏感虽然HOG对光照有一定鲁棒性但极端光照下还是会失效。解决方法是在训练集中加入不同光照条件的样本或者先用直方图均衡化预处理图像。小目标检测困难64x128的窗口设置对远处行人效果差。可以尝试以下方案训练多尺度检测器先用背景建模提取前景区域再在这些区域做检测改用DPM等部件检测算法误检率高特别是对栅栏、树木等垂直结构。我的解决方案是增加困难负样本加入颜色特征如HSV直方图与HOG特征融合使用非极大值抑制(NMS)合并重叠检测框# 非极大值抑制实现 def nms(boxes, threshold0.3): if len(boxes) 0: return [] boxes np.array(boxes) x1 boxes[:,0] y1 boxes[:,1] x2 x1 boxes[:,2] y2 y1 boxes[:,3] area (x2 - x1 1) * (y2 - y1 1) idxs np.argsort(y2) pick [] while len(idxs) 0: last len(idxs) - 1 i idxs[last] pick.append(i) xx1 np.maximum(x1[i], x1[idxs[:last]]) yy1 np.maximum(y1[i], y1[idxs[:last]]) xx2 np.minimum(x2[i], x2[idxs[:last]]) yy2 np.minimum(y2[i], y2[idxs[:last]]) w np.maximum(0, xx2 - xx1 1) h np.maximum(0, yy2 - yy1 1) overlap (w * h) / area[idxs[:last]] idxs np.delete(idxs, np.concatenate(([last], np.where(overlap threshold)[0]))) return boxes[pick]这个NMS实现能有效减少重复检测框我在实际项目中使误检率降低了约40%。