深度图边缘发虚,先要分清是推理前丢了细节,还是推理后被糊掉。判断路径是:固定同一张原图,只改输入分辨率跑三档,先在深度数值矩阵上看边缘梯度,再逐项检查上色、缩放、保存环节。只盯彩色上色图很容易误判,因为 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 或检查日志,确认目标分辨率确实生效,否则三档其实跑的是同一档。
用梯度值量化边缘锐度
在深度矩阵上算梯度幅值,再取固定的物体边缘 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 的梯度。
- 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 后边缘梯度回升,就是可视化链路二次平滑,换分辨率不会解决。
排除把无效值当模糊的情况
无效值、空洞、越界截断在可视化里通常被画成背景色或色带某一端,看起来像边缘糊成一片。先统计再说。
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 | 无效值占比 | 保存格式 | 本行只改的字段 |
|---|---|---|---|---|---|---|
| 384x384 | 1x3x384x384 | 待填 | 待填 | 待填 | npy / 16bit PNG | 基线 |
| 518x518 | 1x3x518x518 | 待填 | 待填 | 待填 | npy / 16bit PNG | 仅分辨率 |
| 768x768 | 1x3x768x768 | 待填 | 待填 | 待填 | npy / 16bit PNG | 仅分辨率 |
| 518x518 | 1x3x518x518 | 待填 | 待填 | 待填 | JPEG q90 | 仅保存格式 |
填表规则:同一 ROI 坐标、同一梯度算法、同一无效值判据。梯度均值、p90、无效值占比都从 .npy 上算,保存格式那一列只在判断可视化影响时才单独开一行。这样下次调参不用凭感觉,直接和基线行对比就能看出是哪一环在变。