目录
[一、HOG 是什么](#一、HOG 是什么)
[二、HOG 特征提取原理](#二、HOG 特征提取原理)
[2.1 核心思路](#2.1 核心思路)
[2.2 OpenCV 的 HOGDescriptor](#2.2 OpenCV 的 HOGDescriptor)
[2.3 提取 HOG 特征](#2.3 提取 HOG 特征)
[3.1 用内置检测器](#3.1 用内置检测器)
[3.2 参数调优](#3.2 参数调优)
[3.3 去掉重叠框](#3.3 去掉重叠框)
[四、自定义 HOG + SVM 检测器](#四、自定义 HOG + SVM 检测器)
[4.1 准备数据](#4.1 准备数据)
[4.2 提取特征并训练](#4.2 提取特征并训练)
[4.3 使用自定义检测器](#4.3 使用自定义检测器)
[五、可视化 HOG 特征](#五、可视化 HOG 特征)
[六、HOG 的优缺点](#六、HOG 的优缺点)
一、HOG 是什么
HOG 全称 Histogram of Oriented Gradients,方向梯度直方图。说白了就是:把图像切成小块,统计每个小块里梯度方向的分布,然后用这些直方图拼起来描述整张图(或者图里的某个区域)。
这个特征最早是 Navneet Dalal 在 2005 年提出的,用来做行人检测,效果很好。后来被广泛用在各种目标检测任务里。虽然现在深度学习大行其道,但 HOG + SVM 这套经典方法在某些场景下依然有用------特别是数据量少、需要快速部署、或者嵌入式设备跑不动深度学习模型的时候。
二、HOG 特征提取原理
2.1 核心思路
人站立的时候,身体轮廓大致是竖直的,手臂腿部的边缘梯度方向也比较规律。HOG 就是利用这种"梯度方向分布有规律"的特性来描述目标。
整个流程分四步:
计算梯度:算出每个像素的梯度幅值和方向
划分 Cell:把图像分成 8×8 像素的小块(cell)
统计直方图:每个 cell 里统计梯度方向的直方图(通常 9 个 bin,0-180°)
Block 归一化:把相邻几个 cell 组成一个 block,做归一化(消除光照影响)
2.2 OpenCV 的 HOGDescriptor
OpenCV 用 cv2.HOGDescriptor 实现 HOG 特征提取。关键参数:
hog = cv2.HOGDescriptor(
winSize=(64, 128), # 检测窗口大小(行人检测默认 64×128)
blockSize=(16, 16), # block 大小
blockStride=(8, 8), # block 滑动步长
cellSize=(8, 8), # cell 大小
nbins=9 # 梯度方向 bin 数量
)
这些参数有默认值,行人检测直接用默认的就行。如果你要自定义检测其他目标,需要调整。
2.3 提取 HOG 特征
python
import cv2
#读取图像并缩放到标准尺寸
image = cv2.imread('person.jpg')
image = cv2.resize(image, (64, 128))
#创建 HOG 描述子
hog = cv2.HOGDescriptor()
#计算 HOG 特征
features = hog.compute(image)
print(f"特征维度: {features.shape}") # 默认是 (3780, 1)
64×128 的图像,默认参数下会得到 3780 维的特征向量。这个向量就是这张图的 HOG 描述。
三、行人检测
3.1 用内置检测器
OpenCV 的 HOGDescriptor 内置了一个行人检测器,直接用就行:
python
import cv2
image = cv2.imread('street.jpg')
#创建 HOG 描述子
hog = cv2.HOGDescriptor()
#设置 SVM 分类器为内置的行人检测器
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
#检测行人
rects, weights = hog.detectMultiScale(
image,
winStride=(8, 8), #滑动窗口步长
padding=(4, 4), #图像边缘填充
scale=1.05 #图像金字塔缩放比例
)
#画检测结果
for (x, y, w, h), weight in zip(rects, weights):
cv2.rectangle(image, (x, y), (x+w, y+h), (0, 255, 0), 2)
cv2.putText(image, f"{weight:.2f}", (x, y-5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
cv2.imshow('pedestrian detection', image)
cv2.waitKey(0)
detectMultiScale会在不同尺度的图像上滑动检测窗口,所以能检测不同大小的行人。
3.2 参数调优
detectMultiScale的几个参数直接影响检测效果:
winStride:滑动窗口的步长。步长越小检测越精细,但越慢。默认 (8, 8) 是个不错的平衡点。如果追求速度,可以设 (16, 16)。
padding:在图像边缘填充像素。有时候边缘的行人会被截断,padding 能缓解这个问题。
scale:图像金字塔的缩放因子。1.05 表示每层缩小 5%。值越小,检测尺度越密集,越慢但越准。一般 1.03-1.1 之间。
#精细检测(慢但准)
rects, weights = hog.detectMultiScale(image,
winStride=(4, 4), padding=(8, 8), scale=1.03)
#快速检测(快但可能漏检)
rects, weights = hog.detectMultiScale(image,
winStride=(16, 16), padding=(0, 0), scale=1.1)
3.3 去掉重叠框
跟模板匹配一样,多尺度检测会导致同一个行人被框多次。可以用 NMS 去重:
python
import numpy as np
def nms_boxes(rects, weights, thresh=0.65):
"""对检测框做非极大值抑制"""
if len(rects) == 0:
return [], []
boxes = np.array(rects, dtype=float)
scores = np.array(weights)
x1 = boxes[:, 0]
y1 = boxes[:, 1]
x2 = boxes[:, 0] + boxes[:, 2]
y2 = boxes[:, 1] + boxes[:, 3]
areas = boxes[:, 2] * boxes[:, 3]
order = scores.argsort()[::-1]
keep_boxes = []
keep_scores = []
while order.size > 0:
i = order[0]
keep_boxes.append(boxes[i].astype(int).tolist())
keep_scores.append(scores[i])
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
w = np.maximum(0.0, xx2 - xx1)
h = np.maximum(0.0, yy2 - yy1)
inter = w * h
iou = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(iou <= thresh)[0]
order = order[inds + 1]
return keep_boxes, keep_scores
#使用
rects, weights = hog.detectMultiScale(image, winStride=(8, 8), scale=1.05)
filtered_rects, filtered_weights = nms_boxes(rects, weights, thresh=0.65)
result = image.copy()
for (x, y, w, h), weight in zip(filtered_rects, filtered_weights):
cv2.rectangle(result, (x, y), (x+w, y+h), (0, 255, 0), 2)
四、自定义 HOG + SVM 检测器
内置的行人检测器只能检测人。如果你想检测其他目标(比如车辆、动物),需要自己训练 SVM 分类器。
4.1 准备数据
需要两类图片:正样本(包含目标)和负样本(不包含目标)。
dataset/
├── positive/ # 正样本,裁剪好的目标图片,统一尺寸
│ ├── p001.jpg
│ ├── p002.jpg
│ └── ...
└── negative/ # 负样本,不包含目标的背景图片
├── n001.jpg
├── n002.jpg
└── ...
正样本需要裁剪到统一尺寸(比如 64×64),负样本可以任意大小。
4.2 提取特征并训练
python
import cv2
import numpy as np
import os
from pathlib import Path
def extract_hog_features(image, hog, target_size=(64, 64)):
"""提取单张图片的 HOG 特征"""
image = cv2.resize(image, target_size)
return hog.compute(image).flatten()
def prepare_dataset(pos_dir, neg_dir, target_size=(64, 64)):
"""准备训练数据"""
hog = cv2.HOGDescriptor(
winSize=target_size,
blockSize=(16, 16),
blockStride=(8, 8),
cellSize=(8, 8),
nbins=9
)
features = []
labels = []
#正样本
for img_path in Path(pos_dir).glob('*.jpg'):
img = cv2.imread(str(img_path))
if img is None:
continue
feat = extract_hog_features(img, hog, target_size)
features.append(feat)
labels.append(1) #正样本标签为 1
print(f"正样本: {img_path.name}")
#负样本
for img_path in Path(neg_dir).glob('*.jpg'):
img = cv2.imread(str(img_path))
if img is None:
continue
feat = extract_hog_features(img, hog, target_size)
features.append(feat)
labels.append(-1) #负样本标签为 -1
print(f"负样本: {img_path.name}")
return np.array(features, dtype=np.float32), np.array(labels, dtype=np.int32)
#提取特征
features, labels = prepare_dataset('dataset/positive', 'dataset/negative')
print(f"特征矩阵: {features.shape}, 标签: {labels.shape}")
#训练 SVM
svm = cv2.ml.SVM_create()
svm.setType(cv2.ml.SVM_EPS_SVR) # 用于 HOG 检测器的 SVM 类型
svm.setKernel(cv2.ml.SVM_LINEAR)
svm.setTermCriteria((cv2.TERM_CRITERIA_MAX_ITER, 1000, 1e-6))
svm.train(features, cv2.ml.ROW_SAMPLE, labels)
#获取SVM参数
sv = svm.getSupportVectors()
rho, _, _ = svm.getDecisionFunction(0)
#构造HOG检测器所需的参数
#格式: [rho, w1, w2, ..., wn]
hog_detector = np.append(-rho, sv[0])
#设置到HOG描述子
hog = cv2.HOGDescriptor(
winSize=(64, 64),
blockSize=(16, 16),
blockStride=(8, 8),
cellSize=(8, 8),
nbins=9
)
hog.setSVMDetector(hog_detector)
#保存检测器
np.save('my_detector.npy', hog_detector)
print("检测器已保存")
4.3 使用自定义检测器
python
import cv2
import numpy as np
#加载检测器
hog_detector = np.load('my_detector.npy')
#创建 HOG 描述子
hog = cv2.HOGDescriptor(
winSize=(64, 64),
blockSize=(16, 16),
blockStride=(8, 8),
cellSize=(8, 8),
nbins=9
)
hog.setSVMDetector(hog_detector)
#检测
image = cv2.imread('test.jpg')
rects, weights = hog.detectMultiScale(image, winStride=(8, 8), scale=1.05)
for (x, y, w, h) in rects:
cv2.rectangle(image, (x, y), (x+w, y+h), (0, 255, 0), 2)
cv2.imshow('custom detection', image)
cv2.waitKey(0)
五、可视化 HOG 特征
有时候你想看看 HOG 特征到底提取了什么信息。OpenCV 没有直接提供可视化函数,但可以自己实现:
python
import cv2
import numpy as np
import math
def visualize_hog(image, cell_size=8, nbins=9):
"""可视化 HOG 特征"""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
h, w = gray.shape[:2]
#计算梯度
gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=1)
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=1)
#梯度幅值和方向
mag, angle = cv2.cartToPolar(gx, gy, angleInDegrees=True)
#创建可视化图像
#每个 cell 画一个"星形"表示梯度方向分布
s = 20 # 可视化中每个 cell 的显示大小
vis = np.zeros((h // cell_size * s, w // cell_size * s, 3), dtype=np.uint8)
for cy in range(0, h, cell_size):
for cx in range(0, w, cell_size):
#统计这个 cell 的梯度方向直方图
cell_mag = mag[cy:cy+cell_size, cx:cx+cell_size]
cell_angle = angle[cy:cy+cell_size, cx:cx+cell_size]
hist = np.zeros(nbins)
for b in range(nbins):
bin_low = b * (180 / nbins)
bin_high = (b + 1) * (180 / nbins)
mask = (cell_angle >= bin_low) & (cell_angle < bin_high)
hist[b] = cell_mag[mask].sum()
#画星形
center_x = (cx // cell_size) * s + s // 2
center_y = (cy // cell_size) * s + s // 2
max_val = hist.max()
if max_val == 0:
continue
for b in range(nbins):
theta = math.radians(b * (180 / nbins))
length = int(hist[b] / max_val * (s // 2 - 1))
if length < 1:
continue
x1 = int(center_x + length * math.cos(theta))
y1 = int(center_y + length * math.sin(theta))
x2 = int(center_x - length * math.cos(theta))
y2 = int(center_y - length * math.sin(theta))
cv2.line(vis, (x1, y1), (x2, y2), (0, 200, 0), 1)
return vis
#使用
image = cv2.imread('person.jpg')
vis = visualize_hog(image)
#拼接原图和可视化
combined = np.hstack([cv2.resize(image, (vis.shape[1], vis.shape[0])), vis])
cv2.imshow('HOG visualization', combined)
cv2.waitKey(0)
可视化结果里,每条短线表示该区域的梯度方向,线越长表示该方向的梯度越强。人形区域的短线会呈现出明显的竖直方向分布。
六、HOG 的优缺点
优点
对光照变化鲁棒:通过 block 归一化,能很好地处理光照差异
对几何形变有一定容忍度:小的平移和旋转不影响整体特征
不需要大量训练数据:SVM 在小样本上表现不错
计算速度快:适合实时检测和嵌入式部署
缺点不支持大角度旋转:HOG 特征对旋转敏感,目标旋转大了就检测不到
不适合非刚体:对于形状变化大的目标(比如不同姿势的人),效果下降
需要手工设计:特征提取和分类器分开,不如端到端的深度学习方法
精度上限有限:复杂场景下精度不如 YOLO、SSD 等深度学习方法
什么时候用 HOG?数据量少,深度学习模型容易过拟合
嵌入式设备,跑不动深度学习
目标外观比较固定(比如固定姿态的行人)
需要快速原型验证
如果数据充足、算力够,直接用深度学习。HOG + SVM 更适合"轻量级"场景。
七、常见问题
Q:内置行人检测器检测不到人?
图像分辨率太低,人太小。先放大图像
人不是站立的。内置检测器对坐姿、弯腰等效果差
遮挡严重。检测器需要看到比较完整的人形
调低 scale(比如 1.03),增加检测尺度
Q:误检太多?提高 NMS 的 IoU 阈值
过滤掉权重(weight)低于某个值的检测框
检查图像里有没有跟人体形状相似的物体(比如树干、路灯杆)
Q:自己训练的检测器效果很差?正样本数量不够,至少几百张
正样本没有裁剪对齐,大小和位置差异太大
负样本不够多样,需要包含各种背景
正负样本比例失衡
Q:HOG 特征能用来做分类吗?能。提取 HOG 特征后接任何分类器都行(SVM、随机森林、KNN 等)。不过现在一般直接用深度学习提取特征,HOG 在分类任务上已经不太用了。
八、小结
HOG + SVM 是目标检测领域的经典方法。OpenCV 内置了行人检测器,几行代码就能用。如果要做自定义目标检测,需要自己准备数据、训练 SVM、构造检测器。
虽然现在深度学习是主流,但 HOG 在某些场景下依然有它的价值:轻量、快速、不需要大量数据。理解 HOG 的原理也有助于理解后续的特征提取方法。