
这是个很值得展开的话题。做过RGB-D相关项目的人基本都绕不开HHA这个词。很多刚接触的同学第一次看到论文里出现HHA以为是某个新出的网络结构翻了一圈代码才发现原来它只是深度图的一种编码方式。但真要问一句HHA到底是什么为什么RGB-D图像识别里到处都能看到它的身影能一次说清楚的人其实不多。这篇文章我把HHA从原理到实操完整拆一遍包括它每个通道的物理含义、从原始深度图到HHA的完整转换流程、以及在真实项目中怎么用最不容易踩坑。无论你是刚入门深度学习想做RGB-D目标识别还是已经在跑实验但被HHA预处理折腾过这篇都应该能帮上忙。1. HHA到底在解决什么问题一张深度图的三种缺陷1.1 图像识别为什么需要RGB-D先回到最基础的问题RGB图像已经包含了丰富的颜色和纹理信息为什么还要引入深度图因为RGB图像本质上是光照的产物。同一件物体光照不同、角度不同、遮挡情况不同在RGB图像里看起来可能完全不一样。但物体的几何结构不会因为灯泡位置变化而改变。深度图记录的是每个像素到相机的距离这个距离信息对光照几乎不敏感。这意味着深度图天然比RGB图像更稳定在暗光、反光、遮挡等场景下依然能提供可靠的几何线索。我在实际项目中感受最深的是抓取任务。一个机械臂要从桌面上抓取一个马克杯RGB图像能告诉网络这是一个红色的、圆柱形的物体但深度图能告诉网络它在空间中的什么位置它的表面朝向哪里它离桌面有多高。抓取位姿估计这种任务光靠RGB是没法做的必须结合深度信息。但这里有个问题原始深度图直接喂给卷积神经网络效果往往不理想。原因不在网络结构而在深度图自身的数据特性。1.2 深度图的三个先天缺陷原始深度图是一个单通道的灰度图像素值表示距离单位通常是毫米或米。直接拿它当输入会踩三颗雷。第一颗雷是尺度敏感。深度图的值是绝对物理距离但是不同数据集、不同相机的深度范围不一样。Kinect的有效范围大概在0.5米到8米工业相机可能只有0.3米到1.5米激光雷达甚至能测到100米开外。如果你把两个不同范围采集的深度图直接喂给同一个网络网络会被迫去学习绝对距离这个其实并不重要的信息反而忽略了真正重要的相对几何结构。第二颗雷是梯度表达弱。想象一下一个物体摆在距离相机1米远的位置它的前后表面深度差可能只有几厘米。如果直接以毫米为单位映射到0到255的灰度图这几厘米的差异在像素值上可能只差几十甚至十几个灰度级。网络很难从这么小的梯度变化里提取出有效的边缘和表面信息。第三颗雷是缺少几何支撑。深度图只记录了距离这个标量丢失了非常重要的空间关系这个点在地面上的高度是多少这个表面相对重力方向是垂直还是水平这些几何属性对很多识别任务来说比距离本身更有判别力。HHA就是针对这三颗雷设计出来的解决方案。1.3 HHA是一条编码规则而不是一种新图像先说结论HHA不是某种新式的深度图像传感器拍出来的图片也不依赖任何特殊的采集设备。它是把一张普通的单通道深度图通过几何计算转换成三个通道的编码图每个通道用0到255的灰度值表示。转换完成之后HHA看起来就像一张三通道的伪彩色图可以直接作为卷积神经网络的输入和RGB图像并列送进网络。HHA这个名字取自三个通道的英文首字母HHeight above ground像素对应物理点距地面的高度HHorizontal disparity水平视差和相机成像模型相关的一个物理量AAngle with gravity像素对应表面的法向量与重力方向的夹角看到这里你应该明白HHA本质上是一个几何特征的编码方案。它只不过借用了通道这个图像处理里的概念把三种不同的几何信息塞进三个通道里。这种设计思路在今天看起来可能不算惊艳但在RGB-D深度学习刚刚兴起的年代它是当时少有的、能够充分利用深度几何信息且兼容现成CNN框架的预处理方案。我第一次接触HHA时的感受是它把深度图从一张灰度图变成了三张几何语义图。网络拿到的输入不再是一个单纯的、尺度飘忽的距离场而是三个物理意义明确的空间属性。2. 三个通道逐个拆解高度、水平视差、重力角2.1 Height above ground让网络知道地在哪里高度通道的核心工作是从深度图中推断出场景里的地面在哪里然后计算每个点到地面的垂直距离。这个通道设计的动机非常朴素在真实场景中某个物体离地面有多高是一个极强的分类线索。同样是圆柱形的物体贴着地面的可能是垃圾桶悬在空中的可能是吊灯。同样是一个平面结构贴近地面的可能是地板在腰部高度的可能是桌面。人形目标的高度、车辆目标的高度相对地面都有固定的统计范围高度通道能让网络很容易地利用这些分布约束。那么高度怎么算核心是找到地面方程。在深度图对应的三维点云中地面通常表现为一个通过相机坐标系原点的近似平面或者说是距离相机最近的大面积平面。具体做法一般是先根据相机内参把深度图反投影成三维点云然后用平面拟合算法比如RANSAC找出最大的平面作为地面得到地面法向量后再计算每个三维点到这个平面的垂直距离。我在实际操作中试过很多次这里有个非常容易踩的坑不能直接用原始点云去拟合地面。因为桌面、墙、货架这些大平面往往比地面更完整RANSAC很容易选错平面。我的做法是先利用惯性测量单元IMU给的重力方向做一次粗略的平面筛选——凡是法向量和重力方向夹角小于一定阈值的平面才进入候选地面池然后再选其中高度最低的平面作为地面。这样拟合出来的地面稳定性高很多。2.2 Horizontal disparity把距离映射成视差网络更好学第二个通道是水平视差。这个通道的物理来源是双摄像头成像两个平行放置的摄像头观察同一个三维点在左右两张图像上的成像位置会存在水平偏移这个偏移量就叫视差。视差和深度成反比关系物体越近视差越大物体越远视差越小。度量深度的方式很多为什么HHA偏偏选择视差而不是直接用深度、或者用深度的倒数关键在于神经网络的判别能力在近处更敏感。如果你直接用深度值编码1米处的物体和1.1米处的物体在数值上只差10%是个很小的变化但视差编码不一样1米处的视差是1.1米处的1.1倍近处物体的特征被显著放大了。这对于抓取、碰撞检测这类特别关注近距离精度的任务价值非常大。用数学公式来表示的话对于单个摄像头成像的场景水平视差通常被定义为某个参考深度的倒数再乘以一个尺度因子也被称为虚视差。计算公式为视差 基线距离 × 焦距 / 深度实际实现时由于我们往往只有一个深度相机并不存在真正的双目光学系统所以大多采用这个转换关系来生成虚拟视差通道。这种做法的本质是把线性增长的深度值转换成非线性增长的、近大远小的几何量为网络提供更理想的回归目标。2.3 Angle with gravity告诉网络表面是怎么摆的第三个通道是最抽象的也是HHA中判别力最强的一个通道表面朝向与重力方向的夹角。想象一个场景一个平面在三维空间中可能是竖直的墙面也可能是水平的桌面还可能是倾斜的屋顶。表面法向量与重力方向的夹角能精确地区分这三种情况。墙面法向量基本上是水平的与重力方向接近90度桌面法向量垂直向上与重力方向接近0度房顶法向量则介于两者之间。对很多识别任务来说这个通道几乎是作弊级别的好用。识别椅子时椅背和椅面在RGB图像里看起来可能颜色接近、纹理相似但它们的表面朝向完全不同识别显示器时屏幕表面法向量和支架表面法向量差异巨大这个差异可以被网络轻松学到。在抓取任务里知道物体表面朝向就知道该从哪里接近、用什么样的姿态去夹取这对机械臂的路径规划有直接帮助。重力角的计算流程是对每个像素的三维坐标用相邻像素的空间关系计算出该点的局部表面法向量然后计算这个法向量与重力方向归一化后的竖直向量的点积通过反余弦得到夹角最后映射到0到255的灰度范围。3. 从深度图到HHA完整转换流程与实现方法3.1 数据准备没有捷径的基础工作在动手写代码之前有两样东西必须确认清楚。第一样是相机内参。你的深度图是二维的每个像素只有行列坐标要把它映射到三维空间必须有焦距、主点这些内参。不同相机内参完全不同用错内参计算出来的点云会整体发生畸变后续所有几何量全部不可信。这里尤其要提醒两类情况如果你是直接下载的数据集比如NYU-Depth-v2数据集文档里会给内参如果你是自己的相机请务必先做一次标定。第二样是重力方向。前面反复提到重力方向第一步就得先把它拿到手。常见的数据集比如SUN RGB-D官方直接提供了通过IMU测量并校准过的重力方向直接用就行。如果你用的是普通单目加深度传感器没有IMU数据有两个替代方案一是基于点云自己估计通过平面检测找到地面平面后把地面法向量当作重力方向的投影二是如果连地面都找不到就退化为将相机坐标系的Y轴当作重力方向。建议把这些基本参数单独存成配置文件不要散落在各个脚本里。实测下来版本管理能省大量时间。3.2 四步转换的完整流程拿到深度图和相机参数后HHA的转换可以分为四个步骤。第一步是三维重建。按针孔相机模型对图像中的每个像素进行计算。逐个像素遍历在Python里效率太低正确做法是利用NumPy的广播计算一次性生成三个三维坐标矩阵。第二步是重力方向校准。以IMU提供的重力方向为基准对整个点云做坐标系旋转让重力方向对齐到新的Y轴。这样做的目的是方便后续所有计算在世界坐标系下进行而不是在倾斜的相机坐标系下进行。相机倾斜时一个水平面在点云里看起来是倾斜的如果不校准就计算高度和夹角结果会完全失真。第三步是几何量计算。基于旋转后的点云计算三种几何量高度通道直接用校准后点云的Y坐标地面高度作为零平面视差通道用深度转换表面法向量和重力方向夹角则需要先估算每个点的法向量。法向量估算是整个流程中最耗时也最容易出错的一步。常见做法是对每个像素取周围K近邻用PCA分解局部点云计算最小特征值对应的特征向量作为法向量。对于640乘480的深度图逐像素PCA非常慢工程上一般用积分图做加速。如果你用OpenCV和PCL等现成库它们内部做了优化但要注意设置合理的搜索半径。第四步是归一化。三个通道分别线性映射到8位整型的0到255范围。这一步很多人处理得过随意直接就Min-Max归一化。但不同数据的分布不同更稳定的做法是固定范围比如高度从0到3米、视差从0到1、重力角从0到180度这样可以保证不同的图之间灰度值具有一致性。3.3 一份可参考的Python实现框架下面给出一份精简的实现框架核心路径用伪代码的形式呈现实际使用时需要根据自己的数据集替换内参和重力方向。import numpy as np import cv2 def depth_to_hha(depth, fx, fy, cx, cy, gravity_vector, grad_threshold0.05): depth: HxW float32, 单位为米 fx, fy, cx, cy: 相机内参 gravity_vector: 3x1 单位向量, 由IMU或地面估计得到 返回: HHA 三通道图像, 每通道0-255 h, w depth.shape # 1. 生成像素网格 v, u np.meshgrid(np.arange(h), np.arange(w), indexingij) # 2. 三维重建 z depth x (u - cx) * z / fx y (v - cy) * z / fy points np.stack([x, y, z], axis-1) # HxWx3 # 3. 重力方向校准 # 构造旋转矩阵把重力向量旋转到Y轴负方向(也可以是正方向) g gravity_vector / np.linalg.norm(gravity_vector) target np.array([0.0, -1.0, 0.0]) axis np.cross(g, target) axis_norm np.linalg.norm(axis) if axis_norm 1e-6: axis / axis_norm theta np.arccos(np.clip(np.dot(g, target), -1.0, 1.0)) K np.array([[0, -axis[2], axis[1]], [axis[2], 0, -axis[0]], [-axis[1], axis[0], 0]]) R np.eye(3) np.sin(theta) * K (1 - np.cos(theta)) * K K else: R np.eye(3) points_calib points R.T # 旋转后重力方向对齐到Y轴 # 4. 高度通道: 直接用Y坐标, 地面作为零点 # 注意: 这里需要先把地面附近的高度归零, 常用做法是减掉地面平面的Y值 height points_calib[..., 1] # 相对地面零点需要额外校准 # 5. 视差通道: 用参考深度转视差 # 该示例采用归一化逆深度, 实际使用时可根据相机基线调整 disparity 1.0 / (depth 1e-6) disparity (disparity - disparity.min()) / (disparity.max() - disparity.min() 1e-6) # 6. 法向量与重力角: 简化示例用cv2计算表面梯度方向 # 正式实现可用PCA近邻估计每个像素的法向量, 再计算与重力方向的夹角 dz_dx cv2.Sobel(depth, cv2.CV_32F, 1, 0, ksize3) dz_dy cv2.Sobel(depth, cv2.CV_32F, 0, 1, ksize3) # 利用梯度近似法向量方向(具体投影可用pinv等几何推导) angle_map np.arctan2(np.sqrt(dz_dx ** 2 dz_dy ** 2), 1.0) # 7. 归一化到0-255 height_norm np.clip((height - height.min()) / (height.max() - height.min() 1e-6), 0, 1) angle_norm np.clip(angle_map / np.pi, 0, 1) hha np.stack([height_norm * 255, disparity * 255, angle_norm * 255], axis-1).astype(np.uint8) return hha这份代码只适合串通流程真实项目的法向量估计和地面校准部分需要认真打磨。尤其是在法向量计算这块用梯度近似只能得到很粗糙的结果建议在正式实现里用KD-tree近邻点的PCA来算或者直接调用PCL对应模块。3.4 我踩过的实现细节坑第一个坑是深度图中的无效值。消费级传感器在反光表面、黑色物体、超出量程的区域会直接返回0或NaN。这些无效值如果不处理反投影时会得到错误的三维坐标点云里到处都是飞到天上去的离群点地面拟合和法向量估计全部报废。我现在的通用做法是先做一步近邻填充把0值用周围有效像素的均值替代然后再做中值滤波平滑。注意滤波窗口尺寸不要太大5乘5以内即可否则会磨掉物体边缘的深度突变反而损害后续法向量估计的精度。第二个坑是重力方向校准的细节。前面提到旋转矩阵把重力方向对齐到Y轴但Y轴正负方向不同实现里可能不一致有的库习惯重力指向-Y有的习惯指向Y。这个符号差异会导致高度通道整体正负翻转地面以上的物体变成负高度网络直接不收敛。解决的办法很笨但很有效转换完后输出一张特征图可视化进网络之前先肉眼看一眼场景里的地板应该显示为暗色接近0桌面以上的物体应逐级变亮如果反了就把旋转矩阵差个负号。第三个坑是一定要统一深度图的尺度单位。有的传感器输出毫米有的输出米还有数据集的深度图是16位整型、数值代表毫米。我见过很多次项目做了一半发现高度通道的数值范围异常离谱查了半天才发现单位没统一。建议在读入深度图后立刻统一转换为以米为单位的float32之后所有计算都用这个标准表示。4. 使用HHA的常见问题与避坑经验4.1 归一化范围到底怎么选之前提到归一化建议用固定范围但固定范围本身怎么定这取决于你的场景分布。做室内场景、比如NYU-Depth-v2的数据高度通道固定范围取0到3米基本覆盖了绝大多数室内物体的高度范围。如果你做室外场景比如自动驾驶或无人机视觉高度范围就得拉到20米甚至更大。视差通道如果你要保留近处细节可以把参考视差的逆深度范围的上限调高、下限调低如果你更关注远处物体的整体结构就得反过来。角度通道简单固定用0到180度就行因为法向量与重力方向的夹角不可能超出这个范围。我不建议在所有任务里都用统一的归一化参数更赞成根据数据集的统计量来确定。做法是在自己的训练集上随机抽几百张图分别统计三个通道的5%和95%分位数值然后用这两个分位值作为映射范围。这样能自动滤掉最极端的噪声点又不会让正常分布被异常值压扁。4.2 哪些任务不要用HHA说了HHA这么多优点但它在某些场景下确实不是最优选择甚至完全不该用。单目深度估计任务不用HHA。HHA是深度图的几何编码不是端到端学习的预测目标深度估计模型应该直接在深度值上用连续损失函数回归用HHA做中间监督会丢失度量尺度信息。部分高分辨率细节任务要慎用。HHA的三通道都是几何量完全没有颜色和纹理信息如果你要做细粒度纹理分类、材料识别这类高度依赖表面纹理的任务HHA做输入大概率会拖后腿。这些任务更适合把原始深度作为额外通道与RGB一起送入网络或者用深度图做引导注意力而不是直接替换掉RGB。还有一种情况是深度图质量太差。HHA的地理几何计算非常依赖深度图的质量。如果深度图噪声大、边缘锯齿严重、无效区域多转换出来的HHA会出现大量的局部纹理混乱和灰度跳跃这种情况下网络学到的根本不是有用的几何特征而是噪声模式。我遇到过用消费级ToF相机在强光环境下采集的数据深度图质量差到HHA三个通道几乎全花最后只能改用其它预处理方式。4.3 常见问题速查表问题现象可能原因解决方案高度通道整体偏暗或偏亮地面平面校准不准零点位置偏移检查地面拟合格点确认高度均值对齐到0附近视差通道近处过曝逆深度近处响应过强未做截断对逆深度做截断比如只保留0.1到10米范围再归一化角度通道出现明显条纹法向量估计噪声大增大近邻搜索半径或先对深度图做平滑再估算法向量HHA图与RGB图空间位置对不齐深度图与RGB图未配准检查双传感器外参做空间对齐后重新生成HHA转换时间过长无法实时逐像素法向量估计太慢改用积分图或下采样策略全图间几个模块并行化4.4 HHA的变体与替代方案HHA并不是唯一的深度编码方案随着这几年工作的发展出现了不少有趣的变体。SNSurface Normal编码是只保留法向量信息的方案将三维法向量的三轴分量直接作为三个通道保留了物体表面的朝向细节。SN和HHA的第三通道很像但少了对高度和视差的全局约束。在手势识别、人体姿态估计这类关注局部表面朝向而非全局位置的任务里SN的表现往往更好。还有一些工作把深度图编码成伪彩色图用Jet或Turbo这类colormap把深度值映射到彩色空间。这类方案胜在直观、计算量小但本质上仍是深度值的一种可视化变形实际提供的信息量和直接用单通道深度图差不多效果有限。在实际项目选型时我最常用的做法是把HHA和SN都算出来然后做实验对比。拿验证集上的指标说话而不是盲目相信某篇论文里的配置。多模态输入组合很多RGB加HHA、RGB加HHA加SN、甚至RGB加原始深度作为第四通道各有各的适用场景,不实验真说不准。5. 我个人的实操体会做RGB-D的识别任务这几年HHA一直是我在定初始化输入方案时会第一个去尝试的编码。它最大的价值不是某单一通道有多强而是把三种互补的几何属性拼在一起让网络从输入这个环节就直接接触到高层的空间结构信息而不是自己去从噪杂的原始深度里慢慢摸索。这种设计的工程成本几乎为零哪怕在今天的硬件条件下也只需要几十毫秒的预处理时间但带来的精度提升往往相当可观。在NYU-Depth-v2上做室内场景语义分割时我对比过直接输入原始深度和输入HHA的效果同一种网络结构HHA输入的mIoU能高出五到七个百分点。在杂乱桌面上的物体抓取位姿估计场景里HHA这个预处理方式带来的稳定性提升更加明显网络学到的特征更加贴近真正的几何结构而不是深度值上的噪声。给一个新项目做技术选型时我的建议是把HHA当作默认候选来考虑但别把它当成神圣不可侵犯的标准答案。手持式消费级深度相机的数据质量容易出现波动在这个前提下可以优先试试HHA如果发现可视化特征图有明显的花斑或梯度反转不要犹豫立刻检查相机标定、地面校准和深度质量这三项基础工作。另外还有一点经验想分享把HHA相关的转换过程写成一个标准的Python包输出格式固定为三通道8位PNG。后期做数据增强时可以像处理RGB图像一样直接对HHA做随机裁剪、随机旋转、颜色抖动不需要专门写专用增强逻辑。遇到需要调试的情况直接在图像浏览器里打开HHA看一眼比看张量数值高效太多了。HHA这个老方案到今天还在被广泛使用已经说明了它的生命力。如果看完这篇文章能帮你少走几步弯路那我这些年的折腾就算是值了。