资讯动态

HOG特征详解:方向梯度直方图与行人检测实战

发布时间:2026/10/14 1:26:45 来源:尧图企业网站定制
1. 论文到底解决了什么问题做视觉的同学应该都绕不开这篇东西。我在刚接触目标检测那会儿导师直接把这篇论文甩给我说“把这套特征吃透你就能看懂后面一大半的检测算法”。当时不以为意后来真踩了不少坑才明白这句话的含金量。这篇论文做的核心事情用一句话说就是**设计出一种叫做方向梯度直方图Histogram of Oriented Gradients简称HOG的特征描述子用它对图像中的行人进行检测并且把误检率压到了极低的水平。**在2005年那个还没有深度学习称霸的年代这个成果几乎是行人检测领域的天花板后来的DPMDeformable Parts Model等重要工作都深受它的影响。在它出现之前行人检测主要靠什么一种是基于小波变换的特征一种是基于图像亮度归一化后的边缘模板匹配。这些方法的问题很明显稍微遇到光照变化、背景杂乱、行人姿态变化特征就失效了。而HOG的思路完全不同——它不去管颜色和亮度只看“梯度方向”的分布。什么是梯度方向通俗点说就是图像中明暗变化最剧烈的方向。比如一件白色T恤和黑色裤子的交界处会形成一个很强的边缘这条边缘的走向和强度就是梯度信息。HOG把图像划分成很多小格子在每个格子里统计梯度方向出现的频次形成一个小直方图再把多个小直方图组合起来就构成了整张图的特征。这篇文章合适谁来读如果你是做计算机视觉的学生想从传统特征过渡到现代检测算法这篇是你的必修课如果你是搞工程实现的想在嵌入式设备上做轻量级目标检测HOGSVM这套组合至今仍有很强的实战价值哪怕你只是好奇“手机相册里的人脸框和行人框是怎么来的”这篇文章也能让你对背后的原理有个扎实的认知。我先把结论放这儿HOG特征的本质是对图像局部区域梯度方向分布的“统计指纹”。它不关心“这个像素是什么颜色”只关心“这个位置朝哪个方向变化”这让它对光照和颜色变化天然鲁棒。下面我把这套东西拆开揉碎讲清楚每一步是怎么做的为什么这么做以及你自己动手时会踩到哪些坑。2. 整体设计思路为什么梯度方向能描述目标形状2.1 从“看颜色”到“看轮廓变化”的思维升级要理解HOG得先把思维从“像素值”切换到“梯度场”。想象你站在一个操场上想给远处的同学拍一张照片然后通过照片描述出他的姿势。你会怎么描述大概率不会说“他穿的那块区域是深蓝色、那块是浅灰色”因为光线一变颜色就全变了。更可靠的方式是描述“他的肩膀这条线是斜向上的手臂轮廓是往下垂的”也就是描述物体“边缘的走向”。梯度恰好就是计算机里的“边缘走向”。梯度在图像里的数学定义并不复杂。对于像素点(x, y)它在水平方向的梯度就是相邻像素亮度的差值Gx I(x1, y) - I(x-1, y)垂直方向同理Gy I(x, y1) - I(x, y-1)。有了这两个分量就能算出这一点的梯度幅值表示这个位置变化有多剧烈以及梯度方向表示变化的朝向。论文里把方向范围分成若干个区间bin统计每个区间里梯度幅值的累加值就得到了直方图。这套思路有个很关键的优点梯度只对亮度变化敏感对亮度本身的绝对值不敏感。换句话说无论这个像素是黑色还是白色只要它和周围有明显的明暗差异就会产生显著的梯度。这就像你通过“这个人肌肉线条的走向”来认人而不去在意他穿的是红衣服还是蓝衣服。2.2 HOG三大设计支柱局部性、方向统计、归一化HOG能成功不只是因为“用了梯度”而是因为它在三个层次上做了巧妙的组合。第一局部性Locality。HOG不是一个全局性的描述子。它先把图像分成很多8x8像素的小格子cell每个格子独立统计直方图。局部统计的好处是即使行人的手臂或腿部发生弯曲只要局部的纹理结构还在这个局部特征仍然有效。全局特征遇到局部形变时整个向量都会乱掉而局部特征互相独立形变只影响少数格子。第二方向统计Orientation Statistics。每个格子统计的不是某个特定梯度方向的值而是9个方向区间上的分布。这样做的好处是可以容错——同一块区域哪怕因为姿态变化产生了一定程度的梯度偏移直方图上的分布形态不会发生剧烈改变。用生活类比就是你统计一个班级学生成绩的分布关心的是“及格人数大概占多少”而不是“某个人具体考了多少分”。第三块级归一化Block Normalization。这是整篇论文的点睛之笔也是作者对比了大量实验才确定的关键步骤。相邻的格子会被组合成更大的块block块内的特征向量会做一次归一化处理。归一化的作用是压缩光照和对比度带来的噪声差异。同一个行人站在阳光下和站在阴影里梯度幅值可能差好几倍但归一化之后梯度分布的比例关系就趋于一致了。2.3 和当时的其他特征方法相比优势到底在哪当时的经典方法比如Haar-like特征加Adaboost在人脸检测上已经跑通了但直接用到的行人检测上效果并不理想。原因在于人脸的结构非常规则眼睛鼻子嘴巴的相对位置稳定而行人属于“高柔性”目标四肢姿态变化极大Haar特征这种表达“矩形区域亮度差异”的方式表达不了这么复杂的姿态变化。HOG用“梯度方向分布”代替“亮度差异模板”表达能力更强同时对局部形变的容忍度更高。论文中有个很直观的结论在同样的数据集上HOG特征线性SVM的分类器能把行人的漏检率压到比当时最好的小波方法低一个数量级。这个差距不是一点半点是压倒性的。还有一个容易被忽略的设计优势HOG特征提取过程完全没有“学习”成分所有的滤波器、统计区间、归一化方式都是人工设计好的。这意味着特征提取阶段可以做到非常快的运行速度配合一个简单的线性SVM就能完成检测这在当年算力有限的硬件上是巨大的实用优势。现在不少边缘设备上的传统检测方案依然沿用这个思路。3. 特征提取全流程逐步拆解每一步为什么这么做3.1 第一步Gamma校正与颜色空间归一化论文里正式的特征提取流程第一步不是算梯度而是做颜色空间归一化。这一小步经常被初学者忽略但它决定了整个特征对光照的鲁棒性上限。具体操作上作者对比了RGB、LAB、灰度等不同颜色空间的实验效果最终选用了简单的做法先把彩色图转成灰度图然后对每个像素的亮度值做平方根或者对数变换。这就是所谓的Gamma校正。为什么这么做因为图像的亮度值和场景真实辐照度之间不是线性关系Gamma校正能压缩高光区域的动态范围让暗部细节更突出。我当初做复现时偷懒跳过这一步结果在实验室灯光下测试没问题一到户外阳光下检测率直线下降。后来老老实实按论文内容加上Gamma校正再用同一批测试图跑效果立刻就不一样了。这里提一句我自己的经验如果你用的是OpenCV读图记得把像素值归一化到0~1的浮点数再做Gamma变换直接对0~255的整数做平方根会丢失精度效果差不少。3.2 第二步梯度计算的两维卷积核论文里的梯度计算方式也很讲究。作者没有用常见的Sobel算子这类带平滑效果的滤波器而是直接用了一维的离散微分模板水平方向用[-1, 0, 1]垂直方向用[-1, 0, 1]的转置。为什么不用Sobel因为Sobel包含一定的平滑成分会模糊掉细小的边缘结构而行人检测恰恰需要保留这些精细轮廓。实际实现时这一步在OpenCV里可以直接用cv2.Sobel但要把ksize设为1对应论文中的原始模板。如果用cv2.Canny那种带高斯平滑的算子提取出来的边缘会“变粗”HOG直方图的分辨率反而会下降。这个细节我现在每次讲都提醒人梯度算子越简单越直接越好不要叠一堆预平滑操作。另外对彩色图像的处理也有讲究。直观的想法是对RGB三个通道分别算梯度再合并作者实验过后发现这样效果反而不如灰度图。原因在于彩色通道之间的梯度方向会相互干扰导致噪声。所以正确的做法是先把彩色图转为灰度再做梯度计算。3.3 第三步Cell内方向直方图的统计方式计算完整图的梯度幅值和方向后就进入核心统计环节。整幅图被分割成很多个8x8像素的cell。每个cell内每个像素都有自己对应的梯度方向和幅值。接下来要做的事情把360度方向平均分成9个区间bin每个区间40度。然后根据每个像素的梯度方向把它对应的梯度幅值累加到相应的bin上。这里有个容易理解的坑梯度方向是有“符号”的也就是说0度和180度被视作不同方向。但论文里在行人检测场景下选择了“无符号”直方图把0~180度分成9个区间0度和180度视为同一个方向。因为人在行走时一个白背景里的黑色裤腿边缘和白背景里的黑色上衣边缘从视觉形状上是类似的但梯度方向可能恰好相反。如果不把方向折叠成180度四个方向的歧义就会导致特征不稳定。每个像素在往bin里累加幅值时论文采用了“软分配”策略即一个像素点的梯度方向落在两个相邻bin的边界附近时不要只塞给其中一个bin而是按距离线性分配权重给两边。这个操作能显著减少直方图量化带来的突变噪声。我第一次实现时忽略了软分配结果特征向量在小角度旋转下剧烈抖动加了软分配以后立刻稳定下来。3.4 第四步Block归一化的核心机理这是整篇论文最值得反复琢磨的部分也是作者耗时最多的调参对象。每个8x8的cell统计出9维的直方图后相邻的2x2个cell也就是16x16像素的区域会被组合成一个blockblock内的36维特征向量4个cell x 9维拼接在一起然后做一次归一化。归一化方法作者对比了好几种包括L2-norm、L2-HysL2-norm后截断再归一化、L1-norm、L1-sqrt等。实验结论是L2-Hys表现最好L2-norm略微逊色但差距不大计算上更简单所以后来大多数开源实现都用L2-norm。L1-norm效果最差直接不用考虑。理解block归一化的意义关键要理解“局部对比度归一化”这个概念。想象一场舞台剧追光灯打在主角身上主角的人体边缘梯度幅值很大周围背景区域很暗。如果没有归一化同一套动作在不同光照角度的条件下直方图的幅值会整体缩放特征向量长度差异巨大SVM分类器的决策边界就会被干扰。Block归一化对每个局部区域做向量长度的缩放相当于把“音量”统一了保留的是不同方向之间的“比例关系”。由于block是滑动的每个cell往往会被多个block覆盖参与多次归一化。比如行列方向的步长通常设为8像素那么一个cell会和上、下、左、右的邻居组成不同的block最终这个cell的特征会以归一化后的形式出现多次。这种重叠设计让特征拥有一定冗余性增强了鲁棒性——所谓“一个信息被多个上下文同时印证”就是这个道理。3.5 第五步特征向量拼接与整体结构做完所有block的归一化后把整个检测窗口内的所有block特征按顺序拼接起来就得到了最终的HOG特征向量。以论文里的默认参数来算检测窗口大小为64x128像素cell是8x8block是2x2个cell即16x16像素block滑动步长是8像素。那么在水平方向block滑动次数是(64 / 8 - 1) 7次垂直方向是(128 / 8 - 1) 15次。总block数 7 x 15 105个。每个block有36维特征所以最终特征向量维度 105 x 36 3780维。这3780维特征就是这个64x128窗口的“指纹”。如果图像里有一个行人这些维度上会呈现出某种特定的统计模式如果没有行人就会呈完全不同的模式。后面用SVM分类器就是去学一个超平面把这两种模式分开。4. 关键参数选型论文实验揭示的调参玄机4.1 参数选择的“实验史”很多博客讲解HOG时直接就把8x8 cell、9个bin、2x2 block这些参数当成定式念一遍但从不说为什么。实际上这些参数是作者在完整行人数据集上反复测试刷出来的。论文里给出了系统的消融实验对比这块内容非常有价值堪称“调参教科书”。我整理过一组关键参数的效果对比这里直接分享出来参数项测试范围最终推荐实验结论摘要Cell尺寸4x4、8x8、12x12、16x168x8太小则特征过于碎片化抗噪差太大则丢失局部细节方向bin数6、9、12、15、1896个bin漏检率明显上升9个后继续增加改善有限Block尺寸1x1、2x2、3x32x21x1没有归一化覆盖效果差3x3计算量涨但收益微乎其微Block步长4、8、1684会导致特征维度爆炸且重叠过多16会明显丢失空间信息归一化方式L1、L1-sqrt、L2、L2-HysL2-HysL1类效果最差L2类基本持平L2-Hys略胜梯度算子一维模板、Sobel、高斯导数一维模板[-1,0,1]简单模板性能最优平滑算子反而有害4.2 为什么bin数选9而不是6或者18这是一个很多初学者会问的问题。方向区间太少直方图对方向的分辨率不足识别不出精细的边缘走向差异方向区间太多每一格里的统计样本变少噪声就会淹没信号。9个bin对应每40度一个区间这个粒度对行人这种大体量目标来说刚刚好。我做过一个简单的对比测试把bin数改成6时在同样数据集上漏检率上升了大约2个百分点改成18时漏检率几乎没变但特征维度涨了一倍训练和推理时间都增加了。这就是典型的边际效益递减。如果你是在做精度优先的离线任务可以把bin数调到12试试如果是做实时检测9是最稳的平衡点。4.3 检测窗口为什么是64x12864x128这个尺寸不是随便定的它对应的是行人目标在图像中的常见宽高比。成年人的身体比例大致是宽:高 1:264x128正是这个比例。虽然行人本身高矮胖瘦各异但比例区间基本稳定所以用这个固定比例去滑动扫描能覆盖绝大多数正常尺度下的行人。实际工程里如果你检测的场景比较特殊比如主要是儿童或者坐姿行人建议按实际目标尺寸比例重新设定窗口大小。我曾经在一个监控场景里检测排队人群默认64x128的窗口严重框不准坐着的行人后来改成64x96效果立刻立竿见影。参数不是死的理解设计意图才能灵活变通。4.4 多尺度检测与窗口滑动策略有了一个64x128的固定窗口怎么在一整张大图里找行人答案是滑窗加图像金字塔。图像金字塔就是把原图逐层缩小生成多个分辨率在每一层上用固定窗口以一定步长滑动取每个位置下的图像块提取HOG特征送进SVM分类器。如果一个尺度的某个位置被判为正样本就说明这里可能存在一个行人。滑动步长的选择也影响速度和精度。步长太大容易漏掉目标步长太小计算开销巨大。论文测试了不同步长最终结论是8像素步长在精度和速度上比较均衡。如果你想要更快的速度可以接受一些精度损失把步长调到16像素能减少大约一半的计算量。后处理阶段还要用非极大值抑制NMS把同一个目标的多个重叠框合并成一个这个步骤必不可少否则一个行人会被框三四次。5. 分类器训练细节与难例挖掘策略5.1 用线性SVM为什么够用HOG特征提取出来之后论文用的是线性SVM做分类。有人会问为什么不直接上核SVM表达能力更强原因有两个层面。第一HOG特征本身已经是高度抽象的统计特征线性可分性在大多数场景下已经满足。特征向量里包含的是目标的形状和纹理模式不是原始像素所以用一个线性超平面就能把这些模式分开。第二线性SVM的推理极快。检测需要在整张图片的所有位置、所有尺度上做分类任何一点非线性计算都会带来巨大的时间开销。线性SVM只有一个向量内积运算在当时的硬件上能保持实时的可能。作者的实验结果也证实线性SVM的精度和核SVM几乎没差但速度快了几十倍。5.2 难例挖掘到底在挖什么这一节我想多说几句因为很多新手复现论文时完全忽略了难例挖掘导致训练出的模型在真实场景里表现很差。难例挖掘Hard Negative Mining的核心思路是先用负样本粗训一版SVM然后在海量不含行人的负样本图像上跑检测器把被误检成“行人”的窗口收集起来作为额外的负样本加入训练集重新训练。如此迭代若干轮让SVM不断见识那些“容易犯错”的负样本从而收紧决策边界。什么叫难例举个典型例子建筑物外立面的窗户格栅、树干、路灯杆这些目标局部看起来都有竖直边缘和横向纹理和行人的局部轮廓高度相似。第一版SVM往往会疯狂地把它们误报为行人。把它们当成负样本重新喂给SVM分类器就会慢慢学会“这种纹理组合虽然某些局部特征像人但整体空间结构不是人的形状”。我第一次独立做完整训练流程时跳过难例挖掘模型在训练集上准确率98%到了真实街道视频里满屏飘误检框。后来老老实实按论文方法跑三轮难例挖掘误检直接降了一个数量级。这一步不是锦上添花是救命级别的重要操作。5.3 训练数据的组织方式论文里正样本是直接从训练图像里裁剪出来的行人区域统一缩放到64x128大小。负样本有两种来源一种是从不含行人的图像里随机裁剪窗口另一种就是难例挖掘得到的误检块。训练时正负样本的比例也要注意正样本数量少几千张负样本可以很多几万甚至几十万个也不夸张。SVM对样本比例不敏感但训练集太失衡会导致训练时间变长。实际操作中我会在难例挖掘的每一轮控制负样本总量在正样本的10倍以内。样本太多时做随机下采样保证每轮训练的数据规模可控。6. 代码落地用Python完整实现一遍HOG6.1 结合OpenCV的极简实现了解了每个环节的原理代码其实非常简洁。下面给一个基于OpenCV和scikit-image的极简实现适合用来跑通流程import cv2 import numpy as np from skimage.feature import hog from skimage import exposure # 方案一直接用OpenCV的HOGDescriptor hog_cv cv2.HOGDescriptor() image cv2.imread(pedestrian.jpg) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) features_cv hog_cv.compute(gray) # 默认窗口64x128输出3780维 # 方案二用scikit-image的hog提取参数可调更灵活 features_sk hog( gray, orientations9, # bin数 pixels_per_cell(8, 8), # cell大小 cells_per_block(2, 2), # block内cell数 block_normL2-Hys, # 归一化方式 visualizeFalse, feature_vectorTrue, )可以看到真实世界里封装好的库已经帮你把论文里的流程全实现了。但强烈建议你在跑通成品API之后还是自己手动实现一遍梯度计算和直方图统计。这个过程能让你对每一维特征的含义有直感而不是把HOG当作一个黑盒函数。我带的不少同学一开始只会调包遇到特征维度对不上、检测效果异常时完全无从排查就是吃了没有手动实现过的亏。6.2 手动实现核心步骤的伪代码脉络如果你想手动实现核心步骤的伪代码思路整理如下读取灰度图像素值归一化到0~1做Gamma校正开平方。计算梯度幅值和方向gx cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize1) gy cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize1) magnitude np.sqrt(gx**2 gy**2) angle np.arctan2(gy, gx) * 180 / np.pi % 180 # 折叠到0~180度将图像切分为8x8的cell对每个cell统计9维方向直方图注意用软分配策略。按2x2 cell组成block将4个cell的直方图拼接成36维向量做L2归一化再截断再归一化L2-Hys。按8像素步长滑动block收集所有block特征拼接成最终向量。6.3 提取特征后如何配合SVM使用拿到3780维特征后接SVM是水到渠成的事。如果用scikit-learn直接训练线性SVM即可from sklearn.svm import LinearSVC clf LinearSVC() clf.fit(X_train, y_train) # X_train是样本的特征矩阵OpenCV还提供了一个把SVM模型转换成检测器格式的方法转换之后就能直接调用hog.setSVMDetector配合detectMultiScale实现一整张图的检测完整走通“特征加分类器”到“检测输出”的整个流程。这个方案虽然走的是传统路线但胜在训练和推理都轻量很多人脸检测和简单行人检测的小型项目至今还在沿用。7. 复现过程中的高频问题和排查心得7.1 漏检多、误检少的排查思路如果你的检测器漏检多通常说明正样本特征表达不足或者分类器阈值太严。排查方向有三一是检查训练集里正样本的姿态和尺度是否太单一比如全是站立正面的人那侧面行走的行人就容易漏二是检查HOG窗口是否与目标实际尺度比例匹配三是尝试降低SVM决策阈值——LinearSVC里可以通过调整intercept或者在后处理时直接降低判决分值来实现。7.2 误检多的几大根源误检多的原因通常是负样本不够“硬”。如果训练时只用随机裁剪的负样本模型没见过那些和行人局部特征相似的事物自然会在真实场景里把窗户格栅、树干、电杆误判为人。解决办法就是前面重点提过的难例挖掘迭代三轮左右误检数量会直线下降。另外一个经常被忽略的根源是NMS参数没调好。如果两个检测框的重合阈值设得太宽松同一个行人会被多个框重复覆盖看起来像是误检爆发。调试NMS的抑制阈值时建议把重合度阈值先设为0.3左右再根据实际输出框的密集程度微调。7.3 特征维度爆炸问题如果你改动滑动步长或者block大小特征维度的变化会非常剧烈。比如窗口64x128不变把block步长从8改成4block数就变成近400个特征维度直接从3780涨到14000以上。维度爆炸会导致训练变慢而且特征稀疏化SVM容易过拟合。我在实际调试中会先固定其它参数单独调节一个变量同时记录精度和推理时间的变化曲线用数据来决定取舍。还有一个实战中的重要经验计算梯度时确保gx和gy的数据类型是浮点数。用np.uint8直接做减法会有溢出梯度幅值会变成雪花噪点特征基本全废。我当时排查了很久才意识到是这个细节问题现在每次都要专门提醒。7.4 一张速查表帮你定位问题现象可能原因排查与对策训练集上准确率高测试集上很差过拟合增加负样本数量特别是难例检查特征维度是否过大光照变化时检测率剧烈下降漏了Gamma校正或归一化确认block归一化参数正确使用L2-Hys行人形变大的场景漏检多cell过大姿态敏感性不足把cell从8x8降到6x6试试检测窗口保持1:2比例误检集中在竖直纹理区域负样本缺乏树干、窗户类样本启动难例挖掘从未检测图中收集误检块同一目标被多个框覆盖NMS参数不合理调低重叠抑制阈值从0.5向0.3方向尝试特征向量数值全部接近0图片尺寸或颜色空间处理出错确认输入图片不小于窗口尺寸转灰度后像素归一化到0~18. 这篇论文对后来算法的影响与我的使用体会HOG这篇论文的价值至少体现在两个维度。第一个维度是它本身作为行人检测的即战力是那个时代的标杆第二个维度是它作为一种设计范式告诉后来者手工设计的特征也可以做到很高的精度关键要抓住目标最本质的视觉线索。后来的DPM模型里用到的HOG变种把根滤波器、部件滤波器组合起来表达更精细的空间结构本质上是继承了“局部统计加整体组合”的思路。即使到了深度学习时代HOG的归一化思想在特征金字塔网络里也能看到影子。我个人在实际项目中多次用过HOG。印象最深的一次是在一块算力极低的嵌入式板子上做简单行人检测深度学习模型跑不起来最后就是用HOG加SVM的方案稳定跑住了实时检测。当时把窗口参数从64x128调整为目标实际比例加上难例挖掘框定场景里的干扰源效果相当能打。HOG这个特征最大的优点是透明、可控特征哪里不对可以逐维去查哪里有噪声可以针对性地改参数。这种调试体验和深度学习那种“黑盒炼丹”的感觉完全不同对于入门理解视觉特征的本质非常有帮助。最后分享一个小技巧在正式编写代码之前建议先用cv2.HOGDescriptor配合默认参数把一张只含单一行人的图片特征可视化出来OpenCV里可以用createHOGDescriptor然后计算hist并绘图。你会直观地看到行人轮廓集中的区域在特征向量上呈现明显的“高能量块”而空白背景区域则几乎为零。把这张“特征图谱”记在脑子里后面无论做调参、debug还是撸深度学习模型都会多一层难得的直觉。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑