资讯动态

从原理到代码:HOG方向梯度直方图与行人检测实战解析

发布时间:2026/10/13 18:05:10 来源:尧图企业网站定制
1. 为什么我还要写一篇十多年前的老人HOG方向梯度直方图先交代一下背景。我一向对传统图像处理的技术保持高度兴趣前阵子为了给一个嵌入式设备实现行人检测功能绕不开HOG方向梯度直方图这个经典特征描述子。说实话刚接到这个需求的时候我有点犯嘀咕都什么年代了深度学习目标检测早就满天飞了YOLO系列一茬接一茬谁还在意2005年前后提出的手工特征但在实际调完、上板、跑到稳定之后我得承认HOG不仅没过时它在特定场景下的稳定性和可解释性仍然值得每个做视觉的开发者认真吃透。尤其是算力受限、需要低时延、数据量不大、还要能解释误检原因的场景HOG反而比端到端的深度模型更合适。这篇文章我就从原理到代码、从参数到踩坑把它一次讲透。适合正在学传统图像处理的初学者也适合在项目里被检测精度折磨、想换个思路的老手。我预设你至少知道一点图像处理的基础概念比如像素、灰度图、卷积。如果不小心越过了这个概念我也会在对应位置补上通俗说明整篇读下来不会太吃力。2. 从直觉到数学HOG到底在表达图像的什么信息2.1 为什么看梯度方向而不是直接看像素值先想一个问题如果让你用一组长数字来描述一幅图里的人你会选什么特征大多数人第一反应是像素值也就是图里每个点的亮度。可像素值有个致命的弱点光照一变同一个物体的像素值全变了但物体的轮廓和形状并没有变。比如一个穿深色衣服的人站在阴影里和一个穿浅色衣服的人站在阳光下两张图的像素分布完全不同但人的形状却是相似的——轮廓线、躯干和四肢的相对位置没有变。HOG想法很直接不看绝对亮度改看亮度变化的方向和强度。这种变化在数学上就是梯度梯度的方向对应物体边缘的走向梯度幅值对应边缘的明显程度。对图像整体做统计就得到一张方向梯度直方图。它描述的不是某个像素长什么样而是整块区域里边缘的走向分布规律。这天然比原始像素更抗光照干扰也更能抓住形状这个关键信息。这里有一个容易忽略的细节HOG统计的是梯度方向不是梯度大小本身直接作为特征。方向才是形状的语言。举个生活中的类比你描述一个人说他今天穿了红色衣服和他身高一米八、肩膀宽前者是像素级信息后者才是形状级信息。HOG就是丢掉颜色专注于形状的语言。2.2 一幅图如何被切分成Cell和Block拿到一张灰度图第一件事往往不是直接提HOG而是先做Gamma校正也就是对每个像素做一次幂运算。这一步可以进一步削弱光照影响实际效果有限但它便宜、实现简单很多工程中会保留。不过要注意从这个环节起整个HOG一波算下来中间任何一步操作都不能随意去掉不然最后的特征语义就变了。接下来把图像划分成一堆互不重叠的小格子叫Cell通常取8x8像素。再以3x3Cell为一个单元组形成Block。相邻Block之间互相重叠重叠的Cell会参与多个Block的直方图统计。正是这个重叠设计让后续的归一化能起作用。你可能会问为什么非要引入重叠的Block答案是为了做局部归一化。光照在图像上的不均匀分布往往是渐变的在一个小范围内差异不大但全图范围差异明显。如果只看单个Cell的直方图不同区域的Cell受光照影响程度不同特征就不稳定。把多个Cell组合成Block做归一化相当于在对局部区域的边缘方向分布做标准化相当于给每个局部区域做一个亮度均衡使得阴影、渐变光照的影响被大幅压制。这个策略和人眼感知亮度的方式也有类似之处我们判断物体边缘更多依赖局部对比度而不是绝对亮度。2.3 直方图怎么统计角度区间与投票权重每个Cell的8x8像素会产生64组梯度值每一组都有方向和幅值。直方图统计时把方向空间分成9个区间。这里有一个重要约定HOG默认使用无符号梯度方向也就是角度范围是0到180度每20度一个区间一共9个bin。为什么是180度而不是360度因为无符号方向认为边缘从左上到右下和边缘从右下到左上是同一个方向对人、车这类外形对称的目标更稳定对噪声也不那么敏感。每一个像素的梯度方向落入哪个区间就给它落到的区间投一票票的权重就是对应位置的梯度幅值。这里还有一个人门细节投票不是只投给方向最邻近的一个bin而是按距离分配到相邻两个bin上这叫方向插值。比如角度是85度它介于80到100度的bin之间就更靠近80度于是给80度bin多一点权重给100度bin少一点。这样做能减少量化误差让特征更平滑。有研究者会加上高斯窗口对离Cell中心远的像素做降权也就是位置插值。从工程实现角度插值带来的收益通常不超过个位数百分点但代码复杂度和时间开销上去了。我见过的不少落地实现都省略位置插值只保留方向插值效果完全可接受。这一点后面讲代码时会再次提到。2.4 归一化公式选择L2-Hys为什么最后胜出Block归一化是整个HOG算法里最关键的一步也是决定特征鲁棒性的核心。假设一个Block有四段9维直方图拼在一起形成一个36维向量v归一化公式用的是L2-norm除以向量长度的平方根。加一个极小量e防止除零。实际操作中有L1-norm、L1-sqrt、L2-norm、L2-Hys几种选法。L1是直接除以所有元素绝对值之和数值不稳定效果最差L1-sqrt是对L1结果开根号略好L2-norm是最经典的选择L2-Hys则是在L2归一化之后把向量每个元素裁剪到不超过0.2再做一次L2归一化。后一步裁剪操作是为了防止某个局部方向出现极端大的梯度把整段特征主导掉。L2-Hys效果好、稳定性高所以OpenCV的HOG实现和经典论文中的实验基本都以L2-Hys为主。看到这里你也应该明白HOG是一整套操作链每个环节的选择都有明确理由不是为了复杂而复杂。3. 手写一遍才是真懂Python逐段实现HOG特征提取原理说得再多不动手写一遍很多细节还是飘的。我的经验是先用Python从零实现一遍HOG能让后续痛苦减半。下面给出一个可运行的精简版本不用任何现成的HOG封装只依赖NumPy和OpenCV基础接口每个环节都保留关键步骤本身供你修改和调试。3.1 计算梯度用最简单的卷积核HOG论文里推荐使用一维离散差分模板[-1, 0, 1]分别计算水平和垂直梯度。这个模板本质上就是在做相邻像素的差值不需要额外平滑。对噪声的抑制则通过后续的归一化和统计来搞定。算完之后我们用arctan函数计算每个像素的方向和幅值把角度限定在[0, 180)区间内。import numpy as np import cv2 def compute_gradient(img): img np.float32(img) / 255.0 gx cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize1) gy cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize1) mag np.sqrt(gx * gx gy * gy) ang np.rad2deg(np.arctan2(gy, gx)) % 180 return mag, ang有一点要特别提醒角度取模180度用的是NumPy的按元素取模操作不是Python常规的%。在实现的时候老手也容易在这里写错一旦错了整条特征链都会偏。如果你的实现和OpenCV对不上优先检查取模。3.2 Cell直方图与Block拼接按照8x8的Cell划分每个Cell内部统计9维直方图然后每2x2个Cell组成一个Block步长8像素滑动最后把所有Block拼接成特征向量。代码仍然保持可读性def extract_hog(mag, ang, cell_size8, block_size2, nbins9): h, w mag.shape cells_h, cells_w h // cell_size, w // cell_size hist np.zeros((cells_h, cells_w, nbins), dtypenp.float32) bin_width 180.0 / nbins for i in range(cells_h): for j in range(cells_w): cell_mag mag[i*cell_size:(i1)*cell_size, j*cell_size:(j1)*cell_size] cell_ang ang[i*cell_size:(i1)*cell_size, j*cell_size:(j1)*cell_size] for y in range(cell_size): for x in range(cell_size): a cell_ang[y, x] % 180 b int(a // bin_width) % nbins next_b (b 1) % nbins ratio (a - b * bin_width) / bin_width hist[i, j, b] cell_mag[y, x] * (1 - ratio) hist[i, j, next_b] cell_mag[y, x] * ratio block_stride cell_size blocks_h cells_h - block_size 1 blocks_w cells_w - block_size 1 features [] for bi in range(blocks_h): for bj in range(blocks_w): block_vec hist[bi:biblock_size, bj:bjblock_size, :].ravel() norm np.sqrt(np.sum(block_vec ** 2) 1e-5) block_vec block_vec / norm block_vec np.clip(block_vec, 0, 0.2) norm2 np.sqrt(np.sum(block_vec ** 2) 1e-5) block_vec block_vec / norm2 features.append(block_vec) return np.concatenate(features).ravel()这段代码把L2-Hys完整呈现了。注意第一次归一化、裁剪、再归一化的顺序不能乱。裁剪只有在第一次归一化之后做才有意义因为此时每个维度的相对尺度才是统一的。这属于典型的实现细节点缺少了效果就会打折扣。3.3 特征可视化你会真的看HOG图吗特征向量本身是一串数字怎么验证提取的合理与否答案是可视化。可视化方法通常是把每个Cell的方向投票按角度画出来叠加成一张方向图。一个高质量的可视化图里人的身体轮廓周围会出现明显的线条分布背景区域则相对稀疏。OpenCV的原生HOG实现也提供了compute方法能直接返回特征向量和上面自己写的版本做对比时通常能对到浮点误差量级。我建议你写完后拿同一张图分别用两种方式提取特征算一下欧氏距离。如果误差在1e-3以下说明你的实现链路是通的。这一步值得花时间做因为在后面的训练和检测流程里一旦出现维度对不上、特征错位的问题你至少能确认特征提取本身没有坑。4. 特征交给分类器从HOG到行人检测器的完整链路4.1 数据准备正负样本的比例和尺寸是第一个大坑HOG特征提取完成后下一步就是找个分类器来学习人和非人的边界。经典搭配是线性SVM。数据准备阶段正样本即人物图像区域负样本即不含人物的任意背景区域。可能有人觉得样本越多越好但第一个坑往往在尺寸上。HOG检测器的窗口尺寸是固定的比如64x128像素。训练时所有正样本都必须裁剪到这一尺寸负样本也尽量裁剪成相同尺寸。如果你随便从网上找一堆不同尺寸的人像直接resize或拉伸人的宽高比会畸变特征随之失真分类器学到的东西就不是人形了。正确的做法是把正样本按照人体边界框裁好再统一缩放至64x128负样本则从各种自然图像中随机裁出同样尺寸的区域。负样本数量上有一个实用的经验值正样本通常准备几千张负样本初始可以准备和正样本相近的数量但后面要通过难例挖掘去补。数据不平衡问题在HOG训练里非常常见纯随机提出的负样本往往太好分了导致模型对困难背景几乎没有辨识力。4.2 最难理解也最重要的一步难例挖掘难例挖掘可以理解为让模型做错题然后重新训练。流程是这样的拿初始正负样本训练一个初级SVM。用这个SVM去遍历一整批完全没参与训练的大图找到被误判为人身的区域这些区域就是难负样本也叫难例。把这些难例加进负样本集重新训练SVM。重复第2和第3步直到误检数量不再明显下降。为什么要这样反直觉地做原因是HOG特征再强也挡不住背景中恰好长得像人形的物体组合。比如一棵树的分叉、几根电线杆的交叉、商店橱窗里的倒影都有可能被误检。与其期望一次性把背景全学完不如有策略地让模型在最像人的非人区域上接受惩罚。经过两到三轮难例挖掘误检率往往能成倍下降这个方法比单纯堆正样本数量便宜得多也有效得多。我第一次做的时候首次训练检测器在测试视频上大约每分钟出现5到6次误检做完第三轮难例挖掘误检降到每分钟0.2次以下差距非常直观。4.3 滑动窗口、多尺度金字塔与置信度阈值训练完成之后要拿来检测检测策略是三件套滑动窗口、图像金字塔、非极大值抑制。滑动窗口是指用一个64x128的窗口按固定步长扫过整幅图。步长过大容易漏检过小则计算量爆表通常在8到16像素之间调。图像金字塔则是把原图按比例缩小生成多尺度图像序列窗口保持不变以此适配不同远近的目标。窗口扫描完以后分类器会输出一堆置信度真正有效的目标周围往往有多个相邻窗口后同时给出高响应。非极大值抑制简称NMS就是对这些响应进行去重保留置信度最高且局部重合区域最大的那个检测框。很多初学HOG的同学只关注特征本身的精度忽略了NMS的重要性结果就是同一个目标被框了十几层看起来精度很高其实全是重叠框。检测链路里NMS和窗口步长对最终体验的影响往往不下于特征本身。5. 参数记忆表与调参方向这些数字务必带走5.1 OpenCV默认参数组合的实际含义OpenCV里的HOGDescriptor类提供了一整套默认参数组合起来是检测窗口64x128、Block大小16x16、Block滑动步长8x8、Cell大小8x8、梯度方向Bin数9。这个组合几乎是HOG落地项目的事实标准原因是它的特征维度恰好为3780维且对于行人这类直立目标尺寸比例和形状记忆都已经覆盖得比较全面。需要分清每个参数的作用Cell大小决定局部细节的粒度Block大小决定归一化区域范围Block步长决定重叠程度和最终维度Bin数决定方向分辨率。表里面写得很清楚参数典型值作用调小的影响调大的影响winSize64x128检测窗口尺寸丢失整体形状提高计算量cellSize8x8直方图统计单元细节更细但特征不准抗噪变强但细节丢失blockSize16x16归一化区域局部对比度弱光照均衡能力下降blockStride8x8Block滑动间距特征重叠少维度爆炸nbins9梯度方向区间数方向区分度低对噪声敏感5.2 调参优先级和性价比调参有个优先级顺序这比参数本身更重要。我的经验是样本质量影响最大特征参数其次SVM参数最后。如果你的误检多优先检查负样本够不够难、类别够不够丰富如果你的漏检多优先检查正样本的覆盖性是不是只有几个固定姿态的人物。排除这些问题之后再动Cell大小和Block大小。调Cell大小通常一次只动一种小步快跑看验证集指标变化。Cell从8降到4特征维度会翻四倍训练和推断时间也翻倍而不一定能带来精度提升所以不要为了准确率无限缩小Cell。方向Bin数从9调到18时很多项目在行人检测上的收益并不明显但计算量明显上升因此默认9就够了。SVM里最值得调的是C值C太小模型太宽松误检多C太大模型过拟合泛化差。以我的经验先用默认值训练难例挖掘阶段结束后再把C适当调小一点通常能得到更稳的结果。5.3 和LBP、Haar这些同类特征比HOG赢在哪里放在传统图像处理特征的大谱系里看HOG和LBP、Haar是三个常被拿来做对比的经典特征。Haar特征擅长边缘和纹理的快速判断通过积分图能极速计算适合人脸检测这样结构较规则的目标LBP刻画局部纹理模式对均匀区域很友好在人脸识别中有经典地位。HOG则偏重物体边缘的形状走向对直立人形这类轮廓清晰的目标来说是三者中最对口的。做个不恰当的类比Haar像你用手掌大致摸一下桌子的轮廓LBP像拿着放大镜看木头的纹理而HOG是素描师在画面上沿着轮廓描线。三者给的信号维度不同适用范围也就不同。实际工程中也有把它们做级联或者融合的做法简单说就是用HOG筛形状明显的候选区域再用LBP来排除纹理不合规的误检效果可以互补。6. 踩坑记录与调试链路这些坑我替你踩过了6.1 同一个HOGOpenCV和手写版为什么维度对不上这是新手的经典思维陷阱。自己算了一遍特征维度公式是固定的按公式可以算出来为什么一用OpenCV的hog.compute输出长度却不一样原因通常出在参数是否成套。OpenCV要求winSize的大小必须能被Block步长整除并产生整数个巡检点Block步长又必须是Cell大小的整数倍这些约束一旦被打破特征维度就会静静地变化。特别是当你只改了检测窗口尺寸没改Block参数时维度变化最容易让人摸不着头脑。这类问题解决起来不难关键是建立统一的维度计算公式且时刻验证别等训练报错再回去查。防范方法简单直接在训练前写一个单元测试对任意一张固定尺寸图计算特征向量的长度并打印出来确认其符合预期。这是我可以给的最小却最有效的防御手段。6.2 归一化除零与角度边界看起来最小的坑影响最大如果说维度对不上是明坑那归一化除零和角度边界就是暗坑踩了只会表现为精度莫名其妙低一点很难定位。归一化除零发生在某Block的四段直方图全部为零时常见于纯黑或纯白区域。如果不加极小量e直接除0结果会是NaN或InfSVM训练直接崩。即便加了e也有顺序问题有些实现先裁剪然后再归一化这在前面提过顺序错了特征质量会恶化。角度边界问题则在于角度恰好落在bin边界上时比如180度会被取模为0如果代码里没处理好会造成这个方向在直方图里消失直观表现就是物体垂直边缘特征缺失检测对侧面偏斜的人容易漏检。这两个问题都不是原理层面的大问题而是实现层面的精细活恰恰是这类环节决定了一个特征提取器能不能用。6.3 光照变化、阴影和误检传统特征的固有边界前面说了HOG很抗光照变化但别因此得出一个错误想象以为HOG对光照完全免疫。它抗的是渐变光照挡不住的是强烈阴影和低对比度区域。比如一个人站在强烈的逆光里身体和背景几乎融为一体边缘梯度变得非常微弱HOG的响应会大幅下降容易漏检这是任何手工特征都无法根治的问题。误检的典型场景则是远处的车辆、密集的树干、栏杆这类块状纹理区域在某些尺度下与人的轮廓统计非常接近容易触发高置信度误判。应对手段除了难例挖掘还可以在NMS阶段加入基于高宽比的先验过滤人的检测框宽高比大约1比2明显偏离这一比例的可直接丢弃。这类业务规则叠加特征会让系统更稳健也不用付出额外的模型复杂度可以说是工程老手的常见技巧。7. 写在最后传统图像处理的现代使用心得我顺手多讲一点对传统图像处理方法的个人态度。做传统图像处理要放平心态。HOG的精度上限放在今天确实比不过好的深度模型在复杂场景下的鲁棒性也有限。但我在实际项目里多次体会到它的几个优势是深度学习很难替代的一是完全不需要GPU在单片机上也能跑二是每一维特征都可解释某个Cell的某个方向响应过高多半就是局部纹理异常三是调试链路短样本量要求低小规模场景下从拿到数据到出demo可能只需要一天。这也让我在遇到算力有限、场景固定、需要快速上线的项目时第一反应常常还是回头翻HOG。如果你现在正处在学深度学习学得一头雾水回来看传统方法总觉得过时的迷茫期我的建议是不要功利地从工具新旧来判断价值。把HOG手写一遍把从梯度到直方图到归一化再到SVM训练的链路走通一遍你对特征到底是什么这个概念的理解会完全不同。这份理解放在今天读任何深度学习模型的论文时都会反过来帮你看懂卷积核到底在提取什么池化又在保留什么。技术会迭代但图像处理的内核逻辑是连续的。手里有一套能通盘掌握的传统工具什么时候都不亏。

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

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

免费获取报价 →
↑