LingBot-Depth 2.0 深度图边缘发虚 / 是分辨率不够还是后处理糊了?

文章导读
深度图边缘发虚,先要分清是推理前丢了细节,还是推理后被糊掉。判断路径是:固定同一张原图,只改输入分辨率跑三档,先在深度数值矩阵上看边缘梯度,再逐项检查上色、缩放、保存环节。只盯彩色上色图很容易误判,因为 colormap 和插值本身就会制造软边。
📋 目录
  1. 壹 固定原图只改输入分辨率跑三档
  2. 贰 用梯度值量化边缘锐度
  3. 叁 检查上色与保存环节是否二次平滑
  4. 肆 排除把无效值当模糊的情况
  5. 伍 整理成一张可复现的对照记录表
A A

深度图边缘发虚,先要分清是推理前丢了细节,还是推理后被糊掉。判断路径是:固定同一张原图,只改输入分辨率跑三档,先在深度数值矩阵上看边缘梯度,再逐项检查上色、缩放、保存环节。只盯彩色上色图很容易误判,因为 colormap 和插值本身就会制造软边。

先做数值对照:同一张原图跑三档输入分辨率,比较固定 ROI 上边缘梯度和过渡像素宽度。若梯度随分辨率升高而变大、过渡变窄,瓶颈多在推理前;若三档数值矩阵基本一致、只有上色图发虚,问题就在可视化或保存链路。风险边界:提高分辨率会增加显存与耗时,空洞和无效应被单独统计,不能当成模糊。

固定原图只改输入分辨率跑三档

同一张原图意味着不换帧、不换裁剪、不换归一化参数。三档分辨率只是 resize 目标不同,其余前处理完全一致。建议先跑 384x384、518x518、768x768,具体值按你的封装替换。

import numpy as np, cv2, torch

IMG = 'same_frame.png'                       # 固定同一张原图
LEVELS = [(384, 384), (518, 518), (768, 768)]  # 只改这一处

def preprocess(path, hw):
    bgr = cv2.imread(path, cv2.IMREAD_COLOR)
    rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
    img = cv2.resize(rgb, (hw[1], hw[0]), interpolation=cv2.INTER_AREA)
    x = torch.from_numpy(img).float().permute(2, 0, 1) / 255.0
    return x.unsqueeze(0)                    # mean/std 三档保持一致

for hw in LEVELS:
    x = preprocess(IMG, hw)
    print('model input:', tuple(x.shape))    # 确认分辨率真的透传进去了
    with torch.no_grad():
        depth = model(x)                     # 形状按你的封装取
    d = depth.squeeze().cpu().numpy().astype(np.float32)
    np.save(f'depth_{hw[0]}x{hw[1]}.npy', d)  # 先保原始数值矩阵
    cv2.imwrite(f'gray_{hw[0]}x{hw[1]}.png', d)  # 灰度仅作辅助

关键是把 .npy 留下来。上色图只用于给人看,数值矩阵才用于比较。若封装内部会统一 resize 到固定输入,需要打印输入张量 shape 或检查日志,确认目标分辨率确实生效,否则三档其实跑的是同一档。

LingBot-Depth 2.0 深度图边缘发虚 / 是分辨率不够还是后处理糊了?

用梯度值量化边缘锐度

在深度矩阵上算梯度幅值,再取固定的物体边缘 ROI 统计,就能把“看着糊”变成可比较的数字。另一种更贴近视觉的量是过渡宽度:沿垂直边缘的扫描线,数从 10% 到 90% 分位跨越了多少像素。

import numpy as np

def grad_stats(d, roi, valid=None):
    d = d.astype(np.float32)
    if valid is None:
        valid = np.isfinite(d)
    gy, gx = np.gradient(np.where(valid, d, np.nan))
    mag = np.sqrt(gx**2 + gy**2)
    y0, y1, x0, x1 = roi
    m = valid[y0:y1, x0:x1] & np.isfinite(mag[y0:y1, x0:x1])
    sub = mag[y0:y1, x0:x1]
    return dict(mean=float(np.nanmean(sub[m])),
                p90=float(np.nanpercentile(sub[m], 90)))

def trans_width(scanline, lo, hi):
    t = (scanline - lo) / (hi - lo)
    a, b = np.where(t >= 0.1)[0], np.where(t >= 0.9)[0]
    return None if len(a) == 0 or len(b) == 0 else int(b[0] - a[0])

读法:三档里梯度均值越高、过渡像素越少,边缘越锐。如果梯度随分辨率升高而升高,倾向分辨率不足;如果三档统计接近、只有上色图差异明显,就转向后处理排查。ROI 三档必须用同一坐标,且先掩掉无效值再算,否则空洞会被当成高梯度。

检查上色与保存环节是否二次平滑

逐项核对三处,每改一处立刻重算同一 ROI 的梯度。

LingBot-Depth 2.0 深度图边缘发虚 / 是分辨率不够还是后处理糊了?
  • colormap:把 8-bit 彩色图和 float 矩阵并排看。有些色带在低对比区颜色接近,会把真实梯度在视觉上压平。改成线性灰度或归一化灰度图,如果灰度图上边缘清楚、彩色图上发虚,那是观感问题,不是深度值问题。
  • 插值缩放:cv2.resize、PIL.resize 的双线性、双三次、LANCZOS 都会平滑边缘。切到 INTER_NEAREST 后边缘变清晰,说明之前是放大插值造成的软边。
  • JPEG 有损压缩:JPEG 在边缘附近有块效应和振铃,会削弱梯度。同一矩阵分别存 PNG 和 JPEG,读回来重算梯度,差多少就知道了。
d = np.load('depth_518x518.npy')
lo, hi = np.nanpercentile(d, [1, 99])
d8 = np.clip((d - lo) / (hi - lo), 0, 1)
vis = (d8 * 255).astype(np.uint8)
cv2.imwrite('gray_518.png', vis)                       # 无损灰度
cv2.imwrite('gray_518_nearest.png',
            cv2.resize(vis, (1024, 1024), interpolation=cv2.INTER_NEAREST))
cv2.imwrite('color_518.jpg',
            cv2.applyColorMap(vis, cv2.COLORMAP_INFERNO),
            [cv2.IMWRITE_JPEG_QUALITY, 90])
d16 = np.clip((d - lo) / (hi - lo) * 65535, 0, 65535).astype(np.uint16)
cv2.imwrite('depth_518_16bit.png', d16)                # 无损数值存档

对照做法是:数值比较只用 .npy 或 16-bit PNG,上色图单独出一份,不参与结论。若关闭 colormap、切最近邻、改存 PNG 后边缘梯度回升,就是可视化链路二次平滑,换分辨率不会解决。

排除把无效值当模糊的情况

无效值、空洞、越界截断在可视化里通常被画成背景色或色带某一端,看起来像边缘糊成一片。先统计再说。

LingBot-Depth 2.0 深度图边缘发虚 / 是分辨率不够还是后处理糊了?
import numpy as np
d = np.load('depth_518x518.npy')
total = d.size
finite = np.isfinite(d)
print('nan  :', int(np.isnan(d).sum()) / total)
print('inf  :', int(np.isinf(d).sum()) / total)
print('zero :', int((d == 0).sum()) / total)
print('top  :', int((d >= 0.99 * np.nanmax(d)).sum()) / total)
mask = (~finite) | (d == 0)
np.save('invalid_mask.npy', mask)

把 mask 叠加成纯红,覆盖回原图;如果发虚区域正好落在无效像素上,那问题是有效深度缺失,换分辨率未必有效。需要注意的是,0 是否代表无效依赖模型的约定,需要结合封装和取值范围确认,不要默认所有 0 都是空洞。

整理成一张可复现的对照记录表

每次只改一个字段,其余保持,否则没法归因。建议固定字段如下表,一行等于一次实验。

分辨率输入尺寸边缘梯度均值p90无效值占比保存格式本行只改的字段
384x3841x3x384x384待填待填待填npy / 16bit PNG基线
518x5181x3x518x518待填待填待填npy / 16bit PNG仅分辨率
768x7681x3x768x768待填待填待填npy / 16bit PNG仅分辨率
518x5181x3x518x518待填待填待填JPEG q90仅保存格式

填表规则:同一 ROI 坐标、同一梯度算法、同一无效值判据。梯度均值、p90、无效值占比都从 .npy 上算,保存格式那一列只在判断可视化影响时才单独开一行。这样下次调参不用凭感觉,直接和基线行对比就能看出是哪一环在变。