资讯动态

Haar特征计算全解析:从模板定义、积分图加速到滑窗应用

发布时间:2026/10/16 1:53:46 来源:尧图企业网站定制
我最近把Haar特征的计算过程完整推导了一遍顺着经典的人脸检测思路从模板定义、积分图加速到滑窗扫描都复现了过程中踩了不少坑也把之前模糊的概念彻底理清了。这篇博文就当作这次学习过程的记录吧。Haar特征是传统计算机视觉目标检测里最经典的一类特征它的计算链路——模板怎么定义、特征值如何计算、积分图怎样做到O(1)求矩形和——几乎决定了后面所有滑窗检测方案的运行效率。无论你之后是继续做传统视觉还是转向深度学习目标检测这段内容都值得踏踏实实推一遍。这篇博文适合刚开始接触目标检测、或者第一次学习Haar特征的读者也可以作为听课后的复习笔记收藏。1. 先搞懂Haar特征是什么再谈计算过程1.1 特征模板不是“提取”出来的而是人为定义的很多初学者第一次接触Haar特征时会有个困惑它到底是从图片里“提取”出来的还是一种人为构造的描述方式答案其实是后者。Haar特征本质上是一组预先定义好的矩形模板每个模板由若干个白色矩形和黑色矩形组成特征值就是白色区域像素和减去黑色区域像素和。可以这样理解图像里的原始像素是“最底层的信息”信息量太大也没法直接用来比较Haar特征相当于对某个局部区域做了一次“摘要”把一块区域的灰度和差异浓缩成一个数值。比如人的眼睛区域通常比脸颊周围暗鼻梁两侧也比鼻梁本身暗这种局部明暗差异恰好能用矩形模板的灰度差来刻画。这里有个经典问题为什么Haar特征用的全是矩形而不是圆形或者其他形状原因有两个。第一矩形模板能通过积分图实现“任意位置、任意大小的矩形区域求和都是常数时间”这是Haar特征能在大规模滑窗检测中实际落地的前提第二早期检测方案在特征设计上追求简单、可计算、可枚举矩形模板的组合方式有限且规律清晰便于做穷举和筛选。换句话说矩形不是一个完美选择但它让整个计算过程变得极其高效。1.2 一次滑窗扫描里发生了什么要理解Haar特征的计算过程必须把它放到一个完整的检测流程里去。经典的目标检测方案会先定义一个固定尺寸的检测窗口比如24×24像素然后在输入图像上从左到右、从上到下移动这个窗口每移动到一个位置就对窗口内的图像区域计算一组Haar特征值。这组特征值会拼成一个特征向量交给提前训练好的分类器去判断“这个窗口里有没有目标”。所以一次完整的目标检测包含三个环节滑窗扫描、特征计算、分类器判断。Haar特征负责的就是第二个环节而且通常是最耗时间的环节。你可能已经想到如果检测窗口是24×24模板又有那么多尺度变化每个窗口位置都要算一大堆模板那计算量会相当惊人。这时候就需要积分图来加速。我在实际复现时的体会是如果只盯着公式看很难理解积分图为什么这么重要但只要自己写一次朴素实现再对比积分图实现你就知道“每次矩形求和从几十次加法变成四次查表”意味着什么了。2. 特征模板与积分图Haar特征计算的两个核心2.1 五种常见模板与特征值公式经典Haar特征通常被归纳为以下几类模板每一类都有明确的形状和黑白区域划分模板类型形状示意描述两矩形水平边缘左白右黑检测垂直边缘强调左右灰度差两矩形垂直边缘上白下黑检测水平边缘强调上下灰度差三矩形线性白-黑-白横向或纵向检测条形结构比如眼睛与眉骨区域四矩形对角斜向黑白相间检测对角方向灰度变化四矩形中心环绕周围黑、中间白强调中心与周围的差异类似斑点检测以最简单的两矩形垂直边缘模板为例特征值的计算公式是F sum(白区域像素) - sum(黑区域像素)用坐标表示就是假设白色区域覆盖第top到bottom行、第left到中间列黑色区域覆盖同一高度、中间到right列那么F rect_sum(top, left, bottom, mid) - rect_sum(top, mid, bottom, right)。这里有个细节容易被忽略如果白色和黑色矩形的面积不相等比如三矩形线性模板里两侧白色矩形面积是中间黑色矩形的两倍直接用“白减黑”会引入面积偏差。但经典流程里通常会把权重设计成让黑白区域对特征值的影响“面积归一化”比如白色矩形权重为1.0、黑色矩形权重为-1.0如果黑色面积是白色的一半就再乘一个系数。实际工程里正负矩形权重会按照“面积倒数归一化”来设定确保特征值不受模板绝对面积影响。很多开源实现里并没有严格区分这一点因为矩形模板在设计时黑白区域通常是对称等面积的。2.2 积分图四次查表完成矩形求和的原理积分图是Haar特征计算中最大的一个加速思想理解它的关键是“空间换时间”。积分图的定义很简单对于原图像I积分图II在位置(i, j)上的值等于原图像中从左上角(0,0)到(i,j)这个矩形区域内所有像素值的累加和即II[i][j] sum(I[x][y])其中0≤x≤i、0≤y≤j。换句话说积分图里每个点存储的是“原图左上角到该点的矩形像素总和”。积分图可以通过递推计算得到II[i][j] I[i][j] II[i-1][j] II[i][j-1] - II[i-1][j-1]。这个公式的原理可以这样理解II[i-1][j]已经包含了左上角到(i-1,j)的所有像素和II[i][j-1]包含了左上角到(i,j-1)的所有像素和两者相加会把左上角到(i-1,j-1)的区域计算两次所以减去II[i-1][j-1]一次最后再加上当前像素I[i][j]。这个过程对应到一个重复求和区域画一张2×2的表格去推就一目了然。有了积分图之后计算任意矩形区域D的像素和只需要四次查表和三次加减。假设矩形D的左上角为(top, left)、右下角为(bottom, right)坐标都采用“开区间”表示即包含top行到bottom-1行包含left列到right-1列那么rect_sum II[bottom][right] - II[top][right] - II[bottom][left] II[top][left]为什么要这样组合因为II[bottom][right]是整个大矩形从左上角到bottom行right列的累加减去顶部之外的部分和左侧之外的部分后多减了一次左上角重叠区域所以再加回来。这个“容斥原理”是积分图最核心的思想。实际工程代码里通常会把积分图的尺寸设为(h1)×(w1)并在第0行、第0列全补0这样就不用在边界处做额外判断。这也是很多初学实现里最容易出错的点后面我会专门讲。2.3 从24×24窗口到几十万特征的“数字爆炸”当检测窗口固定为24×24时如果把两矩形、三矩形、四矩形模板的所有可能尺寸和所有可能位置都列举一遍得到的特征数量并不是几千而是十几万级别。我记得不同教材对具体数字的统计口径不太一样有说超过16万的也有统计到18万左右的但数量级都是“十万级”。这个数字为什么会这么大以两矩形水平模板为例模板宽度w可以在2~24之间取偶数模板高度h可以在1~24之间取任意值然后在24×24窗口内水平方向可以有24-w1个起始位置垂直方向有24-h1个起始位置。把所有可能的w和h的排列组合都累加起来就已经成千上万了再把五种模板类型都算进来就膨胀到十几万。我第一次算出这个数字时也吓了一跳如果每个滑窗位置都要计算十几万个特征值那计算量根本扛不住。这里有两个解决办法第一积分图把每个矩形求和的成本压到常数级所以单纯算特征值并没有想象中慢第二十几万个特征不是全都用来判断而是通过AdaBoost这类分类器训练流程从海量特征中挑出一小部分“最有区分度”的特征组成一个级联的分类器。真正检测时大部分窗口在前几级就被快速丢掉根本不需要算完所有特征。3. 用Python把Haar特征计算过程完整跑一遍3.1 构造积分图的代码与坐标约定纸上谈兵聊完原理下面用一段纯Python代码把积分图构造出来。这里不依赖任何第三方库只用最基本的数据结构方便你看清每一步发生了什么。def build_integral_image(img): h len(img) w len(img[0]) ii [[0.0] * (w 1) for _ in range(h 1)] for i in range(1, h 1): row_sum 0 for j in range(1, w 1): row_sum img[i - 1][j - 1] ii[i][j] ii[i - 1][j] row_sum return ii这里我选择ii的尺寸比原图多一行一列第0行和第0列全为0。这样处理边界的好处是后续求矩形和时坐标可以直接从1开始索引不需要一堆if判断。注意代码中每次循环只维护一个row_sum它就是当前行从左到右的累加值加上上一行的积分图值就能得到当前点的积分值不需要每次都重新累加整个左上角区域。用一个3×3小图来验证img [ [5, 4, 2], [1, 3, 8], [6, 7, 9] ] ii build_integral_image(img) for row in ii: print(row)输出结果[0, 0, 0, 0] [0, 5, 9, 11] [0, 6, 13, 23] [0, 12, 26, 45]可以看到ii[3][3]45正好等于原图所有像素之和。ii[2][2]13对应原图左上角2×2区域的和541313。这里强烈建议你拿笔画一遍因为积分图的坐标约定直接决定了后面矩形求和函数是否正确。3.2 矩形求和函数与特征模板定义有了积分图下一步实现矩形求和函数。坐标采用“开区间”约定传入的top、left、bottom、right表示矩形的行和列范围实际取到的是top到bottom-1行、left到right-1列。def rect_sum(ii, top, left, bottom, right): return (ii[bottom][right] - ii[top][right] - ii[bottom][left] ii[top][left])比如我想求原图中行0~2、列0~1这个2×3矩形的和也就是54136726用这个函数调用rect_sum(ii, 0, 0, 3, 2)返回ii[3][2]26减去一堆0结果正确。再比如求中间那个2×2子块原图第1~2行、第1~2列即3、8、7、9调用rect_sum(ii, 1, 1, 3, 3)得到ii[3][3]-ii[1][3]-ii[3][1]ii[1][1]45-11-12527而387927完全一致。3.3 定义两矩形模板并计算特征值矩形求和函数有了之后定义Haar模板就很容易了。下面是一个两矩形垂直边缘模板的实现白色区域在左黑色区域在右两个矩形等宽等高特征值等于左侧区域像素和减去右侧区域像素和。def haar_2_rect_vertical(ii, top, left, h, w): half_w w // 2 left_sum rect_sum(ii, top, left, top h, left half_w) right_sum rect_sum(ii, top, left half_w, top h, left w) return left_sum - right_sum可以回到前面的3×3小图上试一下取窗口为整幅图top0、left0、h3、w2这里w2表示模板宽度为2列白色1列、黑色1列。白色区域是原图第0~2行、第0列和是51612黑色区域是第0~2行、第1列和是43714。特征值为-2。这个负值会吓到不少人但它是完全正常的。Haar特征表示的是局部灰度差差值方向本身也很重要所以特征值是正还是负都有意义分类器在训练时自然会学到如何利用正负特征值。3.4 在整幅图上扫描并统计特征分布单次特征计算跑通后把它放到滑窗扫描里。下面这段代码用刚才的3×3图像以2×2窗口步长1做遍历计算每个位置上的两矩形垂直模板特征值def scan_haar_feature(ii, img, window_h, window_w, template_h, template_w): h len(img) w len(img[0]) results [] for top in range(0, h - window_h 1): for left in range(0, w - window_w 1): val haar_2_rect_vertical( ii, top, left, template_h, template_w ) results.append((top, left, val)) return results对于小图来说这种循环效率没有参考价值但它把计算过程的逻辑完整体现出来了。真实检测场景里还要在“图像金字塔”上做扫描因为目标有远有近、大小不一样不可能只用固定大小的窗口去找所有尺度的目标。经典思路是先把图像按比例缩小成一组金字塔然后用固定尺寸的窗口在这些不同尺度的图像上滑动。在扫描时还有一步不能省特征值归一化。由于不同图像块的亮度、对比度差异很大直接用原始像素差算出来的特征值没有可比性。通常做法是计算检测窗口内所有像素的均值μ和标准差σ然后用窗口内每个像素减去μ后再除以σ或者直接把特征值除以窗口标准差。这个操作能让同一目标在不同光照下得到接近的特征值。我看过一些复现代码只做了积分图却不做归一化结果检测效果忽上忽下大部分问题就出在这。4. 复现过程中我踩过的坑和排查清单4.1 积分图边界坐标对不齐特征值突然差了一个像素这是我在复现时踩过最大的坑。刚开始我习惯用“闭区间”表示矩形范围也就是把bottom和right理解成“包含最后一个像素的行列”然后矩形求和公式变成了另一套。结果同一个矩形用两种坐标约定算出来的特征值差了一个像素的数值排查了很久才发现。后来总结出两个解决方案。方案一是坚持用“开区间”top、left包含bottom、right不包含。这样矩形行数bottom-top、列数right-left代码循环里不容易漏。方案二是在设计接口时就定死约定甚至在rect_sum函数里打印日志把传入的坐标和解析出的实际范围打出来做小图手算对照。任何一本书上的公式脱离了坐标约定都没有意义坐标对齐后再谈加速。4.2 特征值和矩形和的数据类型溢出如果原图是8位灰度图单点像素值范围是0~255一张尺寸稍大的图积分图里的值可能非常大。比如一张1090×1080的图像所有像素累加轻松超过2的31次方如果你用32位有符号整数保存积分图一不注意就溢出了。我的建议是积分图和特征值统一使用64位浮点数或者使用Python原生的高精度整数类型。用浮点数可能会引入极小的精度误差但对分类器判断几乎没有影响如果用C/C实现至少用int64_t不要用int。另一个容易被忽视的点是特征值的范围白矩形减黑矩形结果可能是负数所以不要把它声明成无符号类型。4.3 特征数量太多、算力吃紧怎么办十几万个特征对训练阶段来说压力很大。我复现时先尝试把所有特征一次性算出来存到内存里结果发现窗口数量和特征数量相乘数据量很容易超过几百万条记录程序跑起来非常吃力。我的改进思路是先做一轮粗筛。在AdaBoost训练流程中每一轮只需要在当前样本权重下找到“错误率最低”的那个特征所以不需要把所有特征都放在内存里而是每轮都遍历一遍全部特征计算当前权重下的加权错误率选最好的那个。这样做虽然每轮遍历耗时但内存压力很小。实际工程里还会对特征做预排序或者只抽取部分随机子集来加速。另一个很重要的优化是级联结构。级联分类器的前几级用少量特征就能把绝大多数非目标窗口丢掉真正需要计算完整特征集的窗口数量很少所以整体检测速度并不慢。这也是当初这套方案能在普通硬件上实时检测人脸的关键。4.4 为什么现在的目标检测很少直接用Haar特征复现过程中会有一个很自然的疑问既然Haar特征加AdaBoost在当年效果不错为什么现在很少直接用了我的理解是Haar特征本质上是人工设计的“固定模板”它对边缘、直线、条纹这类简单结构敏感能捕捉的特征模式有限。对于人脸这种结构相对固定的目标它能取得不错效果但目标种类一旦增多、环境一旦复杂这种固定模板的表达能力就很吃力。深度学习方法则不需要人工设计特征卷积核在训练过程中自动学习图像特征从边缘纹理到部件语义一层层抽象表达能力和泛化能力都强得多。不过Haar特征计算过程中用到的很多思想并没有消失滑窗扫描演变成了滑动卷积和区域提议积分图的区域求和思想在空间金字塔池化里也有影子。所以学Haar特征计算过程并不是为了“怀旧”而是为了理解目标检测这条技术脉络的发展逻辑。5. 我还想继续深挖的几条线把Haar特征计算过程完整复现一遍之后我觉得有几个方向可以继续深入。第一个方向是特征可视化把每个模板在图像不同位置产生的特征值映射成一张热力图你能直观看到哪些区域对这个特征响应最强这会加深对“特征筛选”的理解。第二个方向是尝试把积分图推广到积分直方图、积分梯度等很多传统图像处理任务里都有类似的加速思路。第三个方向是结合现代分类模型做个对比实验同一批人脸/非人脸样本一边用Haar特征加传统分类器一边用简单的卷积网络比较两者的精度和速度差异。这件事能帮你直观理解手工特征和自动学习特征之间的差距究竟在哪。最后分享一个我个人的体会看公式看十遍不如手推一遍、手写一遍。我在复现过程中遇到坐标偏移问题后来画了一张2×2的积分图手算把每个坐标的对应关系写在纸上才彻底搞明白。建议你也拿一张很小的图像亲手验证一下几行代码的事却能把Haar特征的计算过程印在脑子里。

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

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

免费获取报价 →
↑