拿到深度图就直接按像素差反推物体高度,数值差一个量级甚至几个量级,通常不是模型不准,而是输出的尺度口径没确认。LingBot-Depth 2.0 这类深度模型,默认输出既可能是归一化相对深度(常见为 0~1 的视差式数值),也可能在特定配置下输出米制距离,两者在下游的用法完全不同。稳妥的顺序是:先固定一个已知距离的平面目标跑一次推理,看输出张量的数值分布,再决定下游能不能直接做尺寸计算。
先把相机与一块平面目标固定在卷尺能量到的距离上,跑一次推理并打印输出张量的 shape、min/max 与分位数。若数值集中在 0~1,且不同距离目标的输出不随距离线性变化,按相对深度处理,必须先做尺度换算才能算物理尺寸;若数值直接落在米量级且随距离单调变化,才可按米制使用,但仍要用第二个距离点复核比例系数。该判断只对当前权重、前处理与后处理管线成立,换输入分辨率或换后处理裁剪后需要重新校验。
用一张固定距离的平面照片跑一次推理
选一面平整墙面或一张白卡纸,正对相机,用卷尺量出镜头到目标面的实际距离并记下来。平面目标的好处是同一距离上所有像素的真值接近,输出分布只要出现明显分层或非线性拉伸,就能说明尺度口径不是米制。下面是一段通用调用骨架,模型加载与前处理按项目实际实现替换,重点不在接口名,而在必须把中间张量打印出来。
import numpy as np
from PIL import Image
import torch
# 替换为项目实际的加载函数与权重路径
model = load_depth_model('lingbot-depth-2.0')
model.eval()
img = Image.open('plane_1m.png').convert('RGB')
print('原图尺寸:', img.size)
# 替换为与模型配置一致的输入尺寸、插值与归一化方式
x = preprocess(img, size=(518, 518))
print('输入张量 shape:', tuple(x.shape))
with torch.no_grad():
depth = model(x)
d = depth.squeeze().detach().cpu().numpy().astype(np.float32)
print('输出 shape:', d.shape)
print('min/max:', float(d.min()), float(d.max()))
for q in (1, 10, 25, 50, 75, 90, 99):
print('P%d: %.4f' % (q, float(np.percentile(d, q))))
print('相机到目标实际距离(米):', 1.00)跑完后同时记录三样东西:卷尺读数、模型输入分辨率、前处理里是否做过 resize 与 crop。单点结果只能给出粗略区间,比如全部分位数都在 0.02~0.95 之间,那就先按相对深度看待;只有数值出现在米量级,才继续验证米制假设。风险边界在于,这一步只证明当前输入下输出落在某个区间,不能证明不同距离下都保持同一比例。
把深度值直方图与真实距离对照
直方图比 min/max 更能暴露分布形态。对正对相机的平面目标,米制输出的直方图应该是一个窄峰,峰值位置大致对应真实距离;相对深度输出的直方图往往峰值靠近某一端,并且同距离像素的离散度明显更大。除了整体直方图,还要把已知距离点在图像上的采样位置标出来,通常取画面中心正对目标的一小块区域取中位数。
hist, edges = np.histogram(d, bins=20)
for h, e0, e1 in zip(hist, edges[:-1], edges[1:]):
print('%.3f~%.3f: %d' % (e0, e1, h))
h0, w0 = d.shape
cy, cx = h0 // 2, w0 // 2
patch = d[cy-5:cy+5, cx-5:cx+5]
print('中心区域中位数:', float(np.median(patch)))
print('中心区域均值:', float(patch.mean()))判断规则可以这样用:数值集中在 0~1,且把同一平面分别在 1 米和 2 米各拍一次后,中心采样值不是近似 1:2 的关系,就按相对深度处理;相对深度只能用于远近排序、掩码生成、需要归一化深度输入的模块,要算物体高度必须补一层标定换算。若中心采样值本身就落在米量级并随距离单调变化,可按米制使用,但要注意后处理是否把无效值写成 0 或极大值,这些值不参与尺寸计算。
逐步打印前处理与后处理的数值范围
尺度信息经常在管线中途被压掉,所以不要只看模型输出,沿着数据流把每段的数值区间打出来。重点查三处:resize 插值、张量归一化、后处理反归一化。哪一步把数值压成 0~1,尺度换算就必须放在哪一步之后。
raw = np.asarray(img)
print('resize 前:', raw.dtype, raw.min(), raw.max())
resized = np.asarray(img.resize((518, 518)))
print('resize 后:', resized.dtype, resized.min(), resized.max())
t = torch.from_numpy(resized).permute(2, 0, 1).float() / 255.0
print('归一化后: min %.4f max %.4f mean %.4f' % (
float(t.min()), float(t.max()), float(t.mean())))
# 后处理端:模型原始输出与 clip 之后各打一次
print('后处理前: min %.4f max %.4f' % (float(d.min()), float(d.max())))
post = np.clip(d, 0, 1)
print('clip(0,1) 后: min %.4f max %.4f' % (
float(post.min()), float(post.max())))常见的压缩点有两个:一是前处理里的除以 255 或减均值除标准差,这会把像素量纲整体换成无量纲;二是后处理里的 sigmoid 或 clip(0,1)。如果模型内部输出的是视差或归一化逆深度,压缩发生在模型内部,外部打印只能看到 0~1 的结果,这种情况下只能靠标定反算回米制,不能指望改后处理恢复量纲。
写一个尺度校验函数并回归验证
把上面的判断逻辑固化成函数,以后换图片、换分辨率、换权重时自动跑一遍,避免靠肉眼记结论。函数输入原始深度图和卷尺参考距离,输出是否为米制以及比例系数,不满足条件时给出明确原因。
def check_depth_scale(depth, ref_distance_m, tol=0.3):
d = np.asarray(depth, dtype=np.float64)
finite = d[np.isfinite(d)]
if finite.size == 0:
return False, None, '输出全为非有限值,先检查后处理'
vmin, vmax = float(finite.min()), float(finite.max())
center = float(np.median(finite))
ratio = ref_distance_m / center if center > 1e-6 else None
looks_norm = vmin >= -0.05 and vmax <= 1.5
looks_metric = ratio is not None and (1 - tol) <= ratio <= (1 + tol)
if looks_norm and not looks_metric:
return False, ratio, '数值落在 0~1,按相对深度处理'
if looks_metric:
return True, ratio, '中心采样与参考距离同量级,疑似米制'
return False, ratio, '数值范围不属于已知形态,需人工查看直方图'验证时用两张不同距离的照片各跑一次,把结果填进下表,两次结论不一致就说明中间某一步有按输入自适应归一化的操作。
- 参考距离(卷尺读数)
- 中心区域中位数
- 整体 min / max
- 判定结果(相对深度 / 米制)
- 比例系数 ref_distance_m / center
决定换算步骤放在模型输出之后还是下游之前
换算位置只有两种接法,差别在误差来源。接在模型输出之后、落盘之前,下游拿到的都是米制值,口径统一,但标定参数会被固化进历史数据,之后换权重或换标定值就难以追溯。接在下游各模块内部,每个模块各自乘一个比例系数,误差来自各模块假设的标定值不同,同一张深度图可能出现两套尺寸结果。通用做法是把原始输出和后处理结果分开保存,在原始输出旁边带上元数据:是否为米制、比例系数、标定来源、输入分辨率。换算只在一次转换函数里做,位置放在所有下游模块的入口之前。
def to_metric(depth_raw, meta):
if meta.get('is_metric'):
return depth_raw.astype(np.float32)
k = meta.get('scale_factor')
if k is None:
raise ValueError('相对深度缺少标定比例系数,不能转米制')
return (depth_raw.astype(np.float32)) * k这样下游模块只需要处理米制输入,出现尺寸异常时也能回溯到具体的比例系数来源。边界提醒:比例系数必须来自可追溯的标定参考,不能用一个看起来顺眼的经验值冒充;相对深度在没有标定参考时,只能用于相对比较,不应输出任何物理尺寸。