单目深度估计评价指标详解:从Abs Rel到SILog的公式与避坑指南 做单目深度估计的朋友应该都有这种体验跑通一个模型以后最头痛的反而不是损失函数调参而是实验表格里那几张指标表。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 Relmean(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.210 米处差 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 是在对数空间里算 RMSEsqrt(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-3到80米NYU 常用0.1到10米。去掉图像边界的裁剪区域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下面这个函数是我在实验里经常改来用的版本支持常见的有效掩码、深度裁剪、中值缩放和尺度-平移对齐。它接收pred和gt两张深度图输出一整套指标。用 PyTorch 训练时可以先把 tensor 转为 numpy 再调用。import numpy as np def compute_depth_metrics(pred, gt, valid_maskNone, min_depth1e-3, max_depth80.0, alignnone): pred, gt: 同一形状的深度图单位是米 valid_mask: bool 类型的有效像素图 align: none / median / affine pred np.asarray(pred, dtypenp.float32).squeeze() gt np.asarray(gt, dtypenp.float32).squeeze() if valid_mask is None: valid_mask np.ones_like(gt, dtypebool) else: valid_mask np.asarray(valid_mask, dtypebool).squeeze() if pred.shape ! gt.shape: raise ValueError(fpred 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)], axis1) coef, _, _, _ np.linalg.lstsq(A, g, rcondNone) 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 mapclip 到 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。原因很简单实验做多了数据集预处理代码很可能被改过一版而你记不清当时用的到底是不是这版。把数据和脚本固化下来指标表才有可复核的基础。等你再看到“和原论文对不齐”的问题时不用猜直接重新跑一遍就能定位问题出在哪。