单目深度估计评价指标详解:从Abs Rel到SILog的公式与避坑指南
2026/9/13 15:22:18 网站建设 项目流程

做单目深度估计的朋友应该都有这种体验:跑通一个模型以后,最头痛的反而不是损失函数调参,而是实验表格里那几张指标表。Abs Rel、Sq Rel、RMSE、RMSE log、δ1/δ2/δ3、SILog……光看名字就眼花,更麻烦的是不同论文的实现细节还不一样,你复现出来的 0.11 和论文里的 0.11 很可能根本不是一回事。这篇文章我不讲训练技巧,也不堆概念,只专注把单目深度估计实验里最常用的评价指标一次理清:每个指标在算什么、公式长什么样、数值方向是什么、代码怎么写,以及哪些细节最容易让审稿人挑出问题。不管你是刚入门的研究生,还是准备投稿的老手,按这篇文章的路径去整理实验表格,基本能少踩很多坑。

1. 先把评价指标的三类问题对齐:相对、绝对还是容差

1.1 从像素误差到统计量:深度指标不是“平均误差”这么简单

单目深度估计的任务输入是一张 RGB 图,输出是一张逐像素深度图。要和真值比,第一步就得把两个深度图逐像素相减,形成一个误差矩阵。问题在于,一张图上可能有几十万个有效像素,你不可能把误差矩阵扔给审稿人,所以必须压成少数几个统计量。指标本质上回答的是三个问题:

  • 我的误差平均来说有多大?这是“平均误差/绝对误差”家族。
  • 这个误差相对于真实深度有多大?这是“相对误差”家族。
  • 有多少比例的像素落在了可接受的偏差范围内?这是“阈值精度”家族。

这三个问题单独哪一个都不够。绝对误差能告诉你深度差了几米,但无法体现近处物体和远处物体的敏感度差异;相对误差能反映“尺度感”,但会被极小深度处的噪声放大;阈值精度看着直观,却会丢失大误差的具体量级。所以论文里的指标表才会同时列出七八个数字,因为它们是互补的。

1.2 三组指标的互补关系:为什么论文总是一张表列六七个

我记得自己第一次跑 KITTI 评测时,一个模型 Abs Rel 很不错,但 RMSE 很难看。当时不理解,后来才意识到:Abs Rel 对近处小真值的相对误差敏感,RMSE 对少数大距离离群点极其敏感。一个模型可能在 2 米内预测得很准,但在 40 米处有一小片像素预测成了 5 米,RMSE 一下子被拉爆。

另一个常见例子是 δ1 和 Abs Rel 的矛盾。两个模型,一个 Abs Rel 更低,另一个 δ1 更高,说明它们的误差分布形态不同:前者整体偏差小但有较多中高误差像素,后者整体偏差偏大但大多数像素误差落在容差内。审稿人看到这种表会默认你把两种误差都考虑了,反过来,如果你只报一个指标,反而容易被质疑结论不完整。所以别嫌表里数字多,做实验时宁可多算几个,也别只留对自己有利的那个。

2. 六个经典指标逐个拆开:公式、直觉和容易记错的地方

我先把最常用的指标整理成一张速查表,后面按组展开解释。这里的d表示预测深度,d*表示真值深度,所有计算都在有效像素集合上做。

指标常见公式数值方向对什么敏感
Abs Rel`mean(d - d*/ d*)`
Sq Relmean((d - d*)^2 / d*)越低越好大误差和离群点,平方放大
RMSEsqrt(mean((d - d*)^2))越低越好绝对尺度下的大误差
RMSE logsqrt(mean((log d - log d*)^2))越低越好对数空间误差,对远近物体更均衡
δ1/δ2/δ3max(d/d*, d*/d) < 1.25^k的像素比例越高越好预测值落在真值 ±25%、约 ±56%、约 ±95% 容差内的占比
SILog尺度不变对数误差越低越好去掉全局尺度影响后的逐像素一致性

2.1 Abs Rel 和 Sq Rel:相对误差的两种加权方式

Abs Rel 是单目深度估计论文里出现频率最高的指标,公式是:

mean( |d - d*| / d* )

分母是真值深度d*。这也意味着同样的绝对偏差,在近处物体身上的惩罚比远处大。比如 1 米处差 0.2 米,Abs Rel 是 0.2;10 米处差 0.2 米,Abs Rel 只有 0.02。这个指标特别适合衡量“用户感受到的深度比例对不对”,也是很多方法调参时最关注的数。

Sq Rel 看着像把 Abs Rel 的分子换成了平方:

mean( (d - d*)^2 / d* )

注意这里主流实现里的分母是真值深度本身,不是真值深度的平方。“平方相对误差”这个中文名很有误导性,我第一次写代码时按(d - d*)^2 / d*^2算,结果数值和论文完全对不上,排查了很久才发现是分母少了平方。Sq Rel 因为分子带平方,会把少数大误差的权重抬得很高,所以它和 RMSE 一样,都是抓“失控像素”的好手。

2.2 RMSE 和 RMSE log:绝对误差的对数变体

RMSE 是最直观的指标,把每个像素的误差平方后取平均再开方,单位和深度一样是米。它的缺点是离群点影响太大:一帧里只要有几十个像素预测成离谱的远处,RMSE 就能从 4.5 飙到 5.5。所以单独看 RMSE 容易误判模型的真实水平,必须配合 Abs Rel 和阈值精度一起看。

RMSE log 是在对数空间里算 RMSE:

sqrt(mean((log d - log d*)^2))

为什么要取对数?因为深度范围跨度很大,2 米和 40 米的误差在绝对尺度上不可比。取对数后,误差从“绝对差几米”变成“差几个对数单位”,近处和远处的权重被拉得更平衡。很多论文里写的是 RMSE log,但代码里叫 log RMSE,也是一个常见的小坑。

2.3 δ1/δ2/δ3 阈值精度:审稿人最喜欢看的第一行

阈值精度公式看起来简单:

max(d/d*, d*/d) < 1.25^k

其中k取 1、2、3,对应 δ1、δ2、δ3。max(d/d*, d*/d)这个比值有一个好处:不管预测比真值大 25% 还是小 25%,都会落在同一个容差区间里。比如真值 4 米,预测 5 米,比值是 1.25;预测 3.2 米,倒过来是 1.25,所以它也刚好在边界上。

δ1 更高,意味着有更多像素的预测深度和真值相差在 25% 以内。这是很多论文摘要里的“卖点数字”,因为它最贴近人的直觉。但同样要提醒,δ1 对离群点不敏感,哪怕有一小撮像素差到天际,只要占比不大,δ1 照样很高。论文里不会只看这个数,但写 abstract 时大家确实最爱拿它说事。

2.4 SILog 和 log10:尺度无关指标和“老牌”对数误差

SILog 最近在工业级模型和深度基础模型里用得越来越多。它的核心思想是先把全局尺度差异去掉,再看局部误差,公式可以写成:

100 * sqrt( mean(r^2) - mean(r)^2 ),其中r = log d - log d*

注意后面减掉的是mean(r)的平方,等价于去掉所有像素共有的一个常数尺度偏移。正因为这个设计,它特别适合评价那些输出相对深度、没有绝对物理尺度的模型。如果你训练的是归一化深度或逆深度估计网络,SILog 是做实验对比时很稳的一个指标。

早年的 KITTI 和 Make3D 评测还会用 log10 误差:

mean(|log10(d) - log10(d*)|)

现在一些论文把 log10 合并进 RMSE log 或 SILog,不再单独列。但如果你要和很老的方法对比,最好还是把 log10 一起补上,不然对方表格里那一列对不上,审稿人会觉得你做实验不严谨。

3. 数据集评测协议不是配角:裁剪、掩码和深度范围都会改变数字

3.1 Eigen split 和 Garg split:同一个数据集,不同玩家规则

评价指标不是只有公式,还有评测协议。同一个 KITTI 数据,有人用 Eigen split,有人用 Garg split,有人用官方分出来的验证集,结果是没法直接横比的。Eigen split 来自 Eigen 等人那篇经典工作,后来被 Monodepth2 等大量方法沿用;Garg split 来自另一条自监督路线,图像分辨率和中心裁剪方式不一样。两个协议下同样的模型,Abs Rel 能差出 0.01 到 0.02,看起来不多,但很多模型刷榜就是靠这一点点差距。

NYU Depth V2 也是同理,不同论文对 654 张测试图的处理方式有差异。有的使用官方提供的填洞后的深度,有的只保留有效深度像素,有的会再做一次裁剪。所以看论文指标时,第一件事不是看数字大小,而是看它的数据集、split、裁剪方式、深度范围和有效掩码规则。

3.2 有效像素掩码和深度裁剪:写代码最容易漏的地方

深度图不是每个像素都能用。KITTI 的真值来自稀疏 LiDAR 投影,图像顶部是天空、底部是车盖,边缘还可能有投影噪声;NYU 的 Kinect 深度有传感器失效区域和空洞。所以评估前必须构建有效掩码。

常见的处理有这么几种:

  • 去掉深度为 0 或无穷大的像素。
  • 把深度截断到一个合理范围,KITTI 常用1e-380米,NYU 常用0.110米。
  • 去掉图像边界的裁剪区域,Garg 裁剪会去掉顶部和底部各约 20 行,Eigen 裁剪会用固定比例的中心矩形区域。
  • 去掉预测深度为非正数的像素,因为深度值语义上不可能小于等于 0。

这些细节单独看都不起眼,但它们组合起来会明显影响指标。比如在 KITTI 上,如果把边界像素也算进去,RMSE 可能直接涨 0.3 以上;如果深度范围改成 50 米而不截断到 80 米,结果也会变。所以在实验报告里,我建议每次跑完评估都顺手把valid_mask的像素数量和min/max depth存下来,后面排查问题会省很多时间。

3.3 中值缩放与尺度-平移对齐:什么时候该做

单目深度估计有个绕不开的歧义:从单张图很难恢复物理尺度。有的模型输出的是真实米制深度,比如某些监督模型;有的输出的是相对深度,比如很多自监督方法和深度基础模型。对于相对深度模型,直接拿预测值和真值算 RMSE 没有意义,因为全局尺度差一个常数,RMSE 就会爆炸。

两种最常见的对齐方法:

  • 中值缩放:计算真值中位数和预测中位数的比值,然后把预测深度乘上这个比例。
  • 尺度-平移对齐:更严格一点,用最小二乘拟合d_pred_align = scale * d_pred + shift,把预测深度整体搬到真值尺度上。这个方法在相对深度、逆深度模型评测中越来越常见。

有一点必须说清楚:如果你的方法声称自己输出的是米制绝对深度,那就不该做中值缩放。做了,测试时看到的好指标是假的,审稿人一旦拿你代码跑一遍就会露馅。反过来说,如果你做的是相对深度估计,不做对齐也不对,因为你的网络根本没被约束去预测绝对尺度。论文里要把对齐操作写清楚,这是协议的一部分,不是可羞耻的补丁。

4. 一份能直接用起来的指标脚本,以及踩过的三个坑

4.1 核心评价脚本:PyTorch + NumPy

下面这个函数是我在实验里经常改来用的版本,支持常见的有效掩码、深度裁剪、中值缩放和尺度-平移对齐。它接收predgt两张深度图,输出一整套指标。用 PyTorch 训练时,可以先把 tensor 转为 numpy 再调用。

import numpy as np def compute_depth_metrics(pred, gt, valid_mask=None, min_depth=1e-3, max_depth=80.0, align="none"): """ pred, gt: 同一形状的深度图,单位是米 valid_mask: bool 类型的有效像素图 align: "none" / "median" / "affine" """ pred = np.asarray(pred, dtype=np.float32).squeeze() gt = np.asarray(gt, dtype=np.float32).squeeze() if valid_mask is None: valid_mask = np.ones_like(gt, dtype=bool) else: valid_mask = np.asarray(valid_mask, dtype=bool).squeeze() if pred.shape != gt.shape: raise ValueError(f"pred shape {pred.shape} != gt shape {gt.shape}") valid = ( valid_mask & np.isfinite(pred) & np.isfinite(gt) & (gt >= min_depth) & (gt <= max_depth) & (pred > 0) & (gt > 0) ) # 防止个别极端情况下有效像素太少 if valid.sum() < 10: return {} p = pred[valid].astype(np.float32) g = gt[valid].astype(np.float32) if align == "median": scale = np.median(g) / (np.median(p) + 1e-6) p = p * scale elif align == "affine": # 最小二乘拟合: p_aligned = scale * p + shift A = np.stack([p, np.ones_like(p)], axis=1) coef, _, _, _ = np.linalg.lstsq(A, g, rcond=None) p = coef[0] * p + coef[1] # 对齐后理论上应该都是正深度,但为了数值稳定性,给一个下界 p = np.clip(p, 1e-3, None) thresh = np.maximum(p / g, g / p) a1 = (thresh < 1.25).mean() a2 = (thresh < 1.25 ** 2).mean() a3 = (thresh < 1.25 ** 3).mean() abs_rel = np.mean(np.abs(g - p) / g) sq_rel = np.mean((g - p) ** 2 / g) rmse = np.sqrt(np.mean((g - p) ** 2)) log_diff = np.log(g) - np.log(p) rmse_log = np.sqrt(np.mean(log_diff ** 2)) silog = 100 * np.sqrt(np.mean(log_diff ** 2) - np.mean(log_diff) ** 2) return { "a1": a1, "a2": a2, "a3": a3, "abs_rel": abs_rel, "sq_rel": sq_rel, "rmse": rmse, "rmse_log": rmse_log, "silog": silog, }

这段代码里我默认sq_rel的分母是真值深度本身,沿用 Eigen 那套开源实现。如果你看到某个实现里分母是真值平方,不要慌,先确认它引用的是哪篇论文的协议。指标这东西,最怕的就是“看起来一样,算起来不一样”。

4.2 单位、NaN、预测深度上下界的处理

我踩过的第一个坑是单位。KITTI 真值深度单位是米,但有些预处理脚本会把深度保存成毫米,或者归一化到 0-1。如果不统一,RMSE 和 Sq Rel 直接变成天文数字,而 Abs Rel 和 δ1 可能还不受影响,因为相对误差对全局缩放不敏感。所以写评测脚本第一步就是确认单位,并且在整个流程里保持住。

第二个坑是 NaN 和 Inf。传感器真值里经常出现无效点,网络输出有时也会出现 NaN。你必须在计算指标之前把这些点从有效掩码里清掉,否则np.mean会把 NaN 一路传播到所有指标里。很多时候模型训练 Loss 都没问题,但评测一跑就全 NaN,十有八九是 mask 没做干净。

第三个坑是预测深度上下界。有人喜欢在进入指标计算前把预测深度np.clip(pred, 0, 80),理由是深度不可能为负或超过 80 米。这样做不是不行,但要注意:如果真值深度范围是 10 米,你预测值却被裁到 0-80,剪不剪对结果影响很小;如果真值范围是 80 米,预测值很多在 100 米以上,那clip就会显著改变 RMSE。比较严谨的做法是只对有效像素做 mask,不要偷偷给预测值开一个和真值范围无关的“舒适区”。

4.3 定性可视化:别让“颜色好看”骗了你自己

除了量化指标,论文里还得放定性图。很多人为了可视化好看,会把不同模型的深度图用不同的颜色范围显示,这是大忌。一眼看上去第三张最亮,但实际可能是它的颜色标尺被压缩了。正确的做法是:所有对比模型使用同一套颜色映射、同一个深度色条范围,至少同一个数据集内要完全统一。

错误图也是很好的辅助判断工具。我会把每个像素的绝对误差算出来,用 hot 色系画一张 error map,clip 到 0 到 0.2 倍最大深度,所有模型共用同一 colorbar。这样审稿人能一眼看出误差是集中在物体边缘、远处路面还是天空区域。

如果有条件,还可以把预测深度转成点云,和真值点云叠在一起看。点云能暴露很多 2D 误差图上不明显的问题,比如结构漂移、尺度漂移、物体边缘“拉丝”。这部分不写进指标表,但对你判断模型 next step 怎么优化非常有用。

5. 审稿人视角:指标表做得不严谨,等于给论文埋雷

5.1 指标对不齐:先查协议差距再查代码

我做复审的时候,最常见的返修意见就是“你表格里的值和原论文不一致”。这种不一致不一定是作者造假,更可能是协议差异:split 不一样、mask 不一样、深度范围不一样、聚合方式不一样。

指标计算有两种聚合方式:像素池化和图像池化。

  • 像素池化:把所有测试图像的有效像素拼成一个大集合,再计算均值类指标。KITTI 这类点云真值场景常用这种方式,因为每张图的 LiDAR 点数不同,像素池化会让点多的图像在指标中占比更大。
  • 图像池化:先对每张图算出一组指标,再对所有图像取平均。这种方式每张图权重一样,对有效像素少的图更公平。

这两种方式算出来的 Abs Rel 可能差零点零零几,但在 0.110 和 0.113 这种精度的对比里已经算明显差异了。所以论文的实验设置部分一定要写清楚:是像素池化还是图像池化,裁剪区域怎么选的,深度截断范围是多少。不要怕啰嗦,这部分越透明,审稿人的挑刺空间越小。

5.2 只报均值不汇报方差:极端场景会反噬

深度模型的测试结果其实很不稳定。同样训练配置,换一个随机种子,KITTI 上 Abs Rel 可能浮动 0.005 到 0.02;如果测试集里恰好有几帧特殊场景,比如大雨、强反光、镂空围栏,RMSE 会被单帧拉得很高。只报一个均值,看起来简单,但恰恰容易被质疑。

我现在做实验的习惯是:同一个方法至少跑 3 个随机种子,报告均值和标准差。如果计算资源有限,至少把指标按场景分桶报出来,比如近距、中距、远距三档的 Abs Rel 和 δ1。这样写出来的表格虽然多几行,但能直接证明你的改进不是靠一帧运气刷出来的。

还有一个容易被忽略的点:如果数据集本身就有明显的分布差异,比如 KITTI 的训练序列大多是晴天白天,雨天黑夜样本很少,那么只报全局均值会掩盖泛化问题。很多大模型评测现在开始强调“在多个数据集上做 zero-shot 测试”,本质上也这个道理——全局均值好看不等于真的稳。

5.3 从单目深度到深度+位姿、深度大模型:评价指标的新变化

近两年“单目深度+姿态估计”和深度基础模型越来越热,评价指标也随之出现了一些新玩法。

先说深度+姿态联合估计。常见框架是训练一个网络同时输出深度图和相机位姿,比如自监督的 Monodepth 系列和很多神经 SLAM 变体。在这种设置下,深度图质量仍然用上面的深度指标评估,但位姿部分要额外报 ATE 或 RPE 这样的轨迹误差。很多新手会把深度指标和位姿指标混在一张表里,却不说清楚各自的计算对象,审稿人看到会立刻追问。深度指标算在像素上,位姿指标算在相机轨迹上,两者不是同一个评价维度。

深度大模型这边,大家讨论的“大模型评价指标”其实并没有发明一套全新的深度指标,而是改了两件事。第一,强调 zero-shot 跨数据集泛化,通常在 NYU、KITTI、ETH3D、DIODE、Sintel 等多个数据集上分别报指标,而不是只在训练集分布内测。第二,引入更严格的对齐方式,尺度-平移对齐几乎成了相对深度模型标配,因为这类模型本来就不输出绝对尺度,硬套 RMSE 没有意义。

另外,新的深度基础模型论文还会额外关注预测深度的“结构排序”质量,有时候会加上梯度误差或边缘一致性指标,比如预测深度图的梯度与真值梯度之间的平均误差。这个指标不在经典六件套里,但能反映深度图对物体边界的保真度,对后续下任务比如 3D 重建、机器人抓取特别重要。

最后说一个我自己的习惯:每次提交实验结果前,我会把预测深度、真值深度、valid mask 各自单独存成一个 npz,然后用同一份评测脚本重新跑一遍指标。别嫌这一步麻烦,它帮我挡掉过至少两轮 revise。原因很简单:实验做多了,数据集预处理代码很可能被改过一版,而你记不清当时用的到底是不是这版。把数据和脚本固化下来,指标表才有可复核的基础。等你再看到“和原论文对不齐”的问题时,不用猜,直接重新跑一遍就能定位问题出在哪。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询