HHA编码全解析:深度图如何化身三通道伪RGB助力RGB-D识别

发布时间:2026/9/23 13:43:39
HHA编码全解析:深度图如何化身三通道伪RGB助力RGB-D识别 做RGB-D识别的人十有八九都遇到过这个问题明明深度图就是个单通道的灰度距离图为什么很多人非要把它变成HHA再喂给网络HHA全称是Horizontal Disparity、Height above Ground、Angle with Gravity三个通道的缩写最早来自Gupta等人在ECCV 2014上提出的经典工作。简单说它把一张深度图拆成了水平视差、离地高度、表面法向量与重力方向夹角这三个物理量然后组合成一张三通道图方便直接用ImageNet预训练的CNN模型来做物体检测和分割。如果你正在做深度图像识别、语义分割或者目标检测又不想从零开始训练一个深度模型那HHA就是绕不开的一个工具。这篇文章我就把HHA的来龙去脉、三个通道的原理、完整计算流程、复现代码以及我实际踩过的坑全部整理出来省得你再走弯路。1. 为什么RGB-D图像识别需要HHA这种特殊编码1.1 深度图不是自然图像直接喂CNN效果很差先聊聊背景。RGB-D图像识别这个方向本质上是想把颜色信息和深度信息结合起来让模型既看得见纹理又量得出空间距离。深度图本身长什么样呢它其实就是一张灰度图每个像素值表示该点到相机的距离单位是米或者毫米。问题在于CNN这套东西尤其是当时主流的AlexNet、VGG这些模型全都是在ImageNet上预训练出来的它们对输入数据的预期是“三通道的彩色图像通道间的分布符合自然图像的统计规律”。你说我把深度图复制三次变成三通道行不行能跑但效果很拉。原因在于深度图和自然图像有本质区别自然图像三通道之间是部分独立、部分相关的R、G、B各有各的分布而你把深度值复制三次三个通道完全相关CNN底层的滤波器从来没见过这种输入的统计模式。更核心的问题是深度图的像素值是物理距离它不包含表面朝向、相对位置这类高层几何线索。一个桌子表面和一面墙如果它们距离相机一样远在深度图上就几乎无法区分——但它们的离地高度和表面朝向完全不同。这就是HHA要解决的核心矛盾。1.2 早期做法的局限与HHA的思路在HHA之前做RGB-D识别的人一般用两种方案。第一种是手工特征派比如POSHPoint-Oriented Histograms、SPHSpherical Harmonic Features这些只对局部几何结构敏感但表达能力有限而且特征工程做得再辛苦遇到大数据集还是干不过端到端的CNN。第二种是把深度图当做灰度图直接送进CNN效果我刚才说过网络学不进去。HHA的思路其实很聪明。它并不去追求把深度图的所有信息塞进一个多么复杂的编码而是从物理场景中抽出三个“有明确几何含义”的通道组合起来伪装成RGB图像。这相当于把一个距离测量值翻译成“离地多高”、“表面朝向哪里”、“水平视差多大”这三类信息。这样做有两个实实在在的好处第一这三个通道的形态更像自然图像。离地高度在平滑地面上是渐变的表面夹角在平面区域是稳定的视差在物体边缘有清晰的跳变——这些都会让CNN的低层滤波器边缘检测器、纹理检测器更容易激活。第二每个通道都带有物理意义模型不需要花大量参数自己去猜测“这个深度值到底意味着什么”预训练模型的高层语义特征也能以更小的代价迁移过来。我当时第一次看论文里的可视化图时觉得HHA编码出来的图就特别“鲜艳”有点像抽象艺术但从识别任务的效果来看它在NYU Depth v2、SUN RGB-D这类室内数据集上比直接用深度图强得多。所以HHA不是花架子它是那个时代把手工先验和深度学习有效结合的一个里程碑。2. HHA三个通道的物理含义与计算机制2.1 HHeight above Ground离地高度通道先看第二个字母HHeight above Ground也就是离地高度。这个通道要回答的问题是场景中每个像素对应的三维点距离地面有多高。为什么这个信息对识别有用在室内场景里物体类别和高度强相关。桌子、椅子、床、柜子各有各的典型高度分布地面本身的高度是0墙壁从地面一直延伸到天花板橱柜悬浮在半空。如果你只看深度图一把放在桌子上的咖啡杯和放在地上的咖啡杯深度上可能一模一样但离地高度完全不同。模型有了这个通道就能更快地把“高度”这个强区分性特征学出来。具体怎么算呢两步。第一步从深度图和相机内参恢复出每个像素对应的三维坐标点云。第二步在点云中估计一个地面平面方程。有了地平面法向量和平面上的任意一点任意三维点到这个平面的带符号距离就是离地高度。实际代码里地面平面通常用RANSAC在点云中拟合出来平面的法向量归一化后就作为“重力方向”。2.2 HHorizontal Disparity水平视差通道第一个HHorizontal Disparity很多人第一次看到会问我们用的不是单目深度相机吗哪来的视差这是个好问题。在双目立体视觉里视差是左右相机图像上对应点的水平位置差和深度成反比。HHA的作者之所以用视差而不是用深度值是因为深度越近的地方视差变化越剧烈这和人类视觉对近处物体的敏感度是一致的。换成数学语言disparity ≈ baseline * focal_length / depth也就是说同样的距离变化量在近处会带来更大的视差变化、在远处则更小。但在实际工程中我们手上通常只有一张深度图没有左右图。这时候怎么做两种处理方式。有相机内参且是双目或结构光设备的话可以按基线长度换算成真实视差没有的话很多开源实现直接用逆深度变换1/depth再做一个线性映射来近似。还有更简单的做法是从深度图计算水平方向梯度不过这个会丢掉一些恢复信息我实测下来效果不如逆深度映射稳。需要强调的是理论上的“视差”和工程上的“近似视差”在HHA的整体流程中差别不是致命的因为后续还有归一化模型关心的更多是通道内的相对变化模式。2.3 AAngle with Gravity表面法向量与重力方向的夹角第三个通道AAngle with Gravity是表面朝向信息。它要计算的是每个像素点所在的局部表面法向量和重力方向之间的夹角。试想一个室内场景桌面是水平的它的法向量指向上方和重力方向指向下方夹角接近180度如果你把重力方向定义为向下向量或者接近0度如果定义为向上。墙壁是竖直的法向量与重力方向夹角接近90度。这个区分度极其清晰。天花板、地板、各种斜面都能用这个夹角快速区分。计算过程分两块。表面法向量可以从深度图的局部邻域中估计取某个像素和它相邻像素的三维坐标做向量叉积再进行归一化。这个过程对深度图的噪声特别敏感所以计算前通常要做一个平滑计算后还会再做一次中值滤波。重力方向则直接用地平面法向量代替——地面法向量就是重力方向的相反向量。然后每个像素的夹角就是angle arccos( clamp( dot(n, g), -1, 1 ) )最后把角度映射到某个区间比如0到180度再归一化到0到255。2.4 为什么把这三个通道放在一起就是“伪RGB”三通道合在一起正好凑成一张三通道图可以喂给任何为RGB图像设计的预训练模型。我们看一个HHA通道对比通道物理量对识别的意义可视化特征H水平视差与深度成反比凸显近处物体、边缘跳变清晰类似自然图像的高频细节H离地高度点离地平面的距离区分桌面物体与地面物体大块平滑渐变区域A表面朝向法向量与重力方向夹角区分平面与斜面、墙面与地面区域结构明显类似纹理信息这正好模拟了自然图像通道间的结构有的通道承载高频细节有的通道承载平滑区域信息有的通道承载局部纹理。所以在迁移学习中预训练CNN的底层滤波器能“兼容理解”HHA这是一个非常巧妙的工程化思路。3. HHA完整计算流程与参考实现3.1 输入数据与相机参数无论你用原版MATLAB代码还是自己重写输入都逃不开三类东西深度图单通道要确认单位是毫米还是米这个直接影响后面所有计算一定要先统一。相机内参包括焦距fx、fy光心cx、cy。如果你用的是Kinect、RealSense这类设备记得查官方标定参数不同镜头之间可能有微小的差异。重力方向严格来说HHA需要知道地平面方向。很多代码实现的默认假设是“相机光轴基本与地面平行”室内场景一般满足但手持设备倾斜太大就会出问题。3.2 预处理补洞和去噪这一步我把它放在第一位因为不做后面全崩。结构光或者ToF深度相机常见的问题就是反光表面和近距离物体会产生空洞深度值直接是0或者NaN。而空洞如果进入点云地面拟合会被污染法向量计算也会在空洞边缘出现假跳变。我的习惯做法是先把无效深度值设为NaN再做一个交叉双边滤波器cross bilateral filter填洞。这个滤波器用彩色图作为引导图在空洞区域把深度“补”成周围相近颜色区域的深度。原版论文里用的就是填充后的深度图。如果不想引入彩色图依赖至少要用一个中值滤波或者形态学闭运算把小的空洞盖住。注意滤波器窗口别太大3x3或5x5就够窗口太大会把物体边缘磨平导致后续法向量在边缘处失真。预处理做完检查一下深度图是否还有残留的NaN和0值如果有后续计算时先用掩膜过滤掉。3.3 估计地面平面并得到重力方向地面平面估计是HHA流程中最容易出问题的环节。原版论文的MATLAB代码里用了Hough变换来检测地平线然后再做平面拟合依赖的是“地面是图像中最大、最平滑、位置最低的平面”这一假设。OpenCV的RANSAC也能做而且更通用。我的简化实现是这样的思路把点云限制在图像下半部分比如y h*0.5的点因为地面一定出现在图像底部这样可以减少桌面、沙发面等非地面平面的干扰。然后在这些点上做一个RANSAC平面拟合。拟合出的平面法向量如果它朝上y分量大于0就取反作为重力方向否则直接用。如果你的场景没有完整地面比如机器人桌面抓取场景点云里主要平面是桌面此时你应该把“桌面”当成参考平面HHA的“离地高度”就变成“离桌面高度”含义会有偏移但相对结构依然是有效的。这是个经验之谈很多论文不会写。3.4 三通道逐像素计算的核心代码这里我分享一个简化的Python参考实现基于NumPy和OpenCV可以让你快速跑通HHA的主流程。注意这不是生产级代码生产环境建议用原版MATLAB代码或者封装好的深度工具箱。import numpy as np import cv2 def depth_to_hha(depth, K, max_depth10.0): depth: HxW float数组单位米无效值为0或NaN K: 3x3相机内参 h, w depth.shape fx, fy K[0, 0], K[1, 1] cx, cy K[0, 2], K[1, 2] # 1. 预处理深度裁剪无效值置为NaN depth np.clip(depth, 0.1, max_depth).astype(np.float64) depth[depth 0.1] np.nan # 用5x5中值滤波做轻量去噪 depth cv2.medianBlur(np.nan_to_num(depth).astype(np.float32), 5).astype(np.float64) # 简单填洞用最近有效值填充生产环境建议用交叉双边滤波 mask depth 0 depth[mask] np.nan ys, xs np.meshgrid(np.arange(h), np.arange(w), indexingij) z depth.copy() x (xs - cx) * z / fx y (ys - cy) * z / fy points np.stack([x, y, z], axis-1) # HxWx3 # 2. 地面估计取图像下半部分RANSAC简化为最小二乘 sub_mask (ys h * 0.5) np.isfinite(z) sub_pts points[sub_mask].reshape(-1, 3) A np.stack([sub_pts[:, 0], sub_pts[:, 1], np.ones_like(sub_pts[:, 0])], axis-1) b sub_pts[:, 2] coef, _, _, _ np.linalg.lstsq(A, b, rcondNone) normal_g np.array([coef[0], coef[1], -1.0]) normal_g normal_g / np.linalg.norm(normal_g) if normal_g[1] 0: normal_g -normal_g centroid sub_pts.mean(axis0) # 3. 通道1离地高度 plane_dists ((points - centroid) * normal_g).sum(axis-1) height np.clip(plane_dists, 0, None) # 4. 通道2水平视差用逆深度近似 disparity 1.0 / (z 1e-6) disparity[np.isnan(disparity)] 0 # 5. 法向量与通道3与重力方向的夹角 # 用相邻像素叉积求法向量 # 先转成点云图取右、下两个邻域做差分叉积 valid np.isfinite(z) p_shift_right np.roll(points, shift-1, axis1) p_shift_down np.roll(points, shift-1, axis0) dx p_shift_right - points dy p_shift_down - points normals np.cross(dx, dy) norm_len np.linalg.norm(normals, axis-1, keepdimsTrue) normals normals / (norm_len 1e-8) dot_n_g (normals * normal_g).sum(axis-1) angle np.degrees(np.arccos(np.clip(dot_n_g, -1, 1))) angle[~valid] 0 # 6. 各通道归一化到[0,255] def norm_to_255(ch): ch np.nan_to_num(ch) mn, mx np.percentile(ch, 1), np.percentile(ch, 99) ch_norm (ch - mn) / (mx - mn 1e-6) return np.clip(ch_norm, 0, 1) * 255 hha np.stack([ norm_to_255(disparity), norm_to_255(height), norm_to_255(angle) ], axis-1).astype(np.uint8) return hha代码里的地面估计用了最小二乘代替RANSAC是为了让示例更短但实际使用中如果场景里有大量桌椅腿最小二乘会被离群点带偏很多。我强烈建议用OpenCV的cv2.findEssentialMat那种思路或者直接用Open3D的segment_plane函数做RANSAC。法向量计算用的是相邻像素差分叉积对噪声相当敏感所以预处理和后续的中值滤波都别省。归一化这里用了1%到99%的百分位裁剪避免极端值把灰度范围撑爆也是个实用小技巧。3.5 原版MATLAB代码与Python复现的差异我一开始复现HHA时就是被原版MATLAB代码折腾得够呛。原版代码里有很多细节比如地面估计前会先做一个“地平线检测”假设深度点云中每一列深度变化最大的位置就是地面和墙壁的分界再用Hough变换把地平线直线求出来最后在地平线下方的点云中拟合地面。这个假设在标准室内场景下是挺稳的但一旦相机俯仰角太大地平线检测就会失败。Python复现如果想尽量贴近原版可以参考rgbd-multimodal这类开源项目中的depth2hha实现它们基本是MATLAB代码的直接翻译。但在实际落地时我更推荐你根据自己的场景做简化如果你的相机安装高度固定、俯仰角固定那还不如直接离线标定一次地平面然后在运行阶段固定使用省掉每帧都做RANSAC的开销。4. 实操踩坑与效果调优经验4.1 地平面估计失效的几种情况HHA最脆弱的一环就是地平面。我有一次在实验时把机器人放在一个铺满毛绒地毯的房间里深度相机在近距离对毛绒表面测得的深度噪声很大RANSAC拟合出来的平面歪了导致离地高度通道一片混乱识别效果断崖式下降。排查了很久才发现问题出在预处理上——中值滤波窗口太小没有把毛绒表面的噪声压下去。还有一次是场景中地面被一张很大的茶几遮住大半图像下半部分全是桌面而不是地面RANSAC把桌面当成了参考平面。这种情况下HHA的输出并不是完全不可用因为桌面高度是固定的离地高度会整体偏移一个常数但物体之间高度的相对关系还在只是模型对“绝对高度”的感知会失真。如果你知道场景里有更大的平面可以调整RANSAC的区域选择或者预设一个平面模型作为先验。4.2 相机内参、深度单位与坐标系必须统一很多初学者在复现HHA时最容易忽略的坑是单位。Kinect一代输出的深度图单位是毫米二代和很多RGB-D数据集用的是米如果你的代码默认“深度值是米”数据却是毫米那三维点云会直接放大1000倍地面拟合会一塌糊涂。我的建议是进入HHA流程前先统一成米并且在预处理阶段把深度值裁剪到0.1米到10米之间太近的太远的都算无效值避免极端值干扰。相机内参也不能想当然。RealSense的默认内参和你实际分辨率下用官方API查到的内参可能不一样如果内参偏差超过几个像素三维坐标在10米距离处就会偏出几十厘米地面拟合和法向量计算都会受影响。我现在做RGB-D相关项目时会先跑一个标定棋盘把当前分辨率的fx、fy、cx、cy确认一遍再开始跑算法。4.3 HHA的数据增强有讲究颜色增强别乱用这一点特别值得强调。HHA虽然被喂进了为RGB设计的CNN但它终究不是RGB。你如果按照彩色图像的惯例对它做颜色抖动、色调翻转、通道随机交换那就彻底废了。因为三个通道是三个物理量通道间的次序是固定的通道内每个数值都有明确的物理意义。我做实验时试过对HHA加颜色抖动结果在NYU Depth v2的分割任务上掉了好几个点。但你也不是完全不能做增强。HHA的水平和垂直翻转是可以的只要同时翻转通道图本身随机缩放、裁剪也适用因为这些只是几何变换。不过有一点要注意很多分割和检测框架的增强管线里会连带做亮度对比度调整这个对HHA不友好建议单独写一个数据加载器把HHA的增强链和RGB的增强链分开。4.4 批量生成HHA的速度问题HHA的计算链路长尤其是交叉双边滤波补洞和RANSAC地面拟合这两步在Python里逐帧跑会慢得让人怀疑人生。在我自己的项目里处理NYU Depth v2数据集大概几百张图单帧跑出HHA要一两秒跑一整个数据集得几个小时。如果只是做实验忍一忍也就过去了如果要上生产我建议分两步优化第一步地面平面离线标定。只要相机安装位置和姿态不变地面平面是固定的。离线跑一次RANSAC得到平面参数运行时直接用它算离地高度和重力方向省去每帧拟合。第二步补洞和去噪用GPU加速。OpenCV的CUDA版本里有双边滤波和形态学的GPU实现速度能提升一个数量级。实在不行预处理阶段用简单的最近邻填洞替代交叉双边滤波虽然边缘质量差一点但很多场景下对识别结果的影响不大。4.5 从HHA到后来的深度编码方法它过时了吗现在再看HHA它当然不是最优解了。深度估计网络和Transformer兴起后很多人直接输入原始深度图甚至点云特征靠大模型自己去学表征HHA作为“手工特征”听起来确实有点老派。但HHA的思想并没有过时——它告诉我们一件很重要的事底层特征的物理含义能显著影响迁移学习的效果。这种“给数据一个有物理意义的编码”的思想在后来很多多模态方法里都能看到影子。比如最近RGB-D显著缺陷检测里流行的三分支交叉模式交互网络本质上就是在做模态间的特征互补。所谓三分支往往是RGB分支、深度分支、融合分支深度分支如果直接用HHA或者表面法向量作为输入模型就能更早地利用几何信息而不必自己去猜“深度到底意味着什么”再比如IMU和力觉参与的多模态融合工作里同样需要把不同物理量的信号编码成模型容易理解的形式。HHA作为深度图编码的经典方案至今仍经常出现在这些工作的baseline对比里。所以它不是“过时”而是被吸收成了更复杂方案的组成部分。5. 关于HHA的再思考与一点个人建议5.1 什么场景下HHA依然值得用按照我这几年的使用经验如果你在下面几种场景中做RGB-D识别HHA仍然是一个值得优先尝试的baseline训练数据量不大又想用ImageNet预训练模型做迁移学习的场景。HHA的物理编码能让预训练权重更快适配深度数据。你的模型架构是双流结构RGB一个流、深度一个流两个流都希望使用预训练权重。此时HHA比单通道深度图复制三次的效果好不少。做公开数据集NYU Depth v2、SUN RGB-D上的基准对比HHA是绕不开的经典baseline。对深度特征的可解释性有一定要求的工业检测场景。HHA的通道含义明确可视化时能直观看到“模型关注的到底是高度还是表面朝向”。5.2 在HHA基础上有哪些值得做的尝试我在实际项目中用过几种HHA的变体效果各有不同简单列一下供你参考HHA加颜色通道组成四通道输入这在原论文里就是默认做法。如果你的任务和颜色关系密切这个方法值得优先试。用表面法向量的三个分量替换角度通道。角度通道压缩了很多信息法向量三分量能保留更多朝向细节但代价是输入从三通道变成更多通道预训练模型通常需要调整第一层卷积。HHA与原始深度图并行输入中间层做特征融合。我试过在分割任务上比单用HHA稳定一些代价是网络参数增加。使用Transformer架构时把HHA当作一个“几何tokenizer”的输入。因为Transformer不依赖卷积的局部先验HHA的物理先验反而能帮它更快收敛。5.3 写到最后的一点体会说实话HHA对今天的研究者来说算不上什么高深技术但它是我见过最典型的“工程智慧”案例。它没有花哨的数学推导有的只是对传感器数据本质的深刻理解深度图不应该是背着一个距离值的灰度图而应该被解构成一系列具有明确物理语义的属性。我在做RGB-D项目时很多次都因为一个看似不起眼的数据编码选择而让模型效果天差地别HHA就是其中印象最深刻的一个。如果你刚接触RGB-D图像识别我的建议是先在标准数据集上完整跑一遍HHA的生成、可视化和对比实验亲眼看看同一个深度图在不同的编码方式下对模型收敛速度的影响。这个过程会比看十篇论文都有用。等你真正理解了为什么一个简单的物理编码能提升这么大的识别效果再去考虑那些更复杂的网络结构思路会清晰很多。