DIODE数据集全解析:室内外稠密深度与法线标注 做单目深度估计和表面法线估计的应该都遇到过一个问题公开数据集要么只给深度要么只在室内要么标注稀疏到没法用。今天要聊的DIODE是一个“全新RGB-D及平面法线数据集”把室内、室外、稠密深度、平面法线一次性凑齐了而且RGB图像和标注的对齐精度很高。若你正在准备深度估计、法线估计、三维重建或者多任务学习的数据集而不想自己抠图这篇文章应该能帮你省掉不少时间。我会从数据集的设计思路、目录结构、加载代码、损失函数到常见坑全部梳理一遍末尾还有一些只有实操才会发现的经验。1. 先说清楚DIODE到底是什么1.1 全称和核心定位DIODE的全称是“Dense Indoor/Outdoor DEpth”直译就是“稠密室内外深度数据集”。注意它全称里既有Indoor又有Outdoor这是它的最大亮点把室内和室外两类深度数据放在同一个数据集里并且每张RGB图像都对应一份稠密深度图和一份表面法线图。和很多用消费级深度相机采集的数据集不同DIODE的深度不是直接“拍”出来的。它主要依赖高精度激光扫描设备对场景进行三维重建得到稠密点云或网格然后把三维几何投影到相机视角生成像素级对齐的深度图。表面法线则是在三维几何上拟合局部平面计算出来的因此整体标注质量比直接对深度图做差分可靠得多。项目里的“全新”其实有两层含义一是发布时相比NYUv2、KITTI这些“老前辈”它在分辨率、场景跨度、法线标注上都是明显升级二是它把室内和室外放在统一的标注框架下方便研究者直接用它来验证跨场景泛化能力。现在很多工作喜欢把深度和法线联合起来训练DIODE正好是少有的“原生支持”这种多任务需求的开源数据集。1.2 为什么需要专门做一个这样的数据集先看几个常见数据集的“痛点”。NYUv2是目前室内深度估计最常用的数据集但它是用Kinect一类深度相机采集的存在深度空洞、边缘噪声大、分辨率不高等问题。KITTI是自动驾驶领域的重要基准但它用车载激光雷达采集深度只覆盖路面附近几条线属于稀疏标注做稠密深度估计时需要复杂插值而且没有官方法线标签。ScanNet有大规模室内重建数据但很多帧的质量参差不齐法线标签也需要自己从Mesh里算。简单说过去做深度估计的研究者要面临一个选择要室内就没有高密度法线要室外深度又稀疏想同时研究多个任务就得自己花大量时间清洗数据。DIODE把室内外场景、稠密深度、平面法线都整合到一起至少让研究者在“数据准备”这一环省掉很多精力。从模型角度看有了同时覆盖室内外的数据模型才有机会学到更通用的几何规律而不是只记住某个房间的结构。1.3 和主流数据集的横向对比这里我用一个表格做一个直观对比方便你快速理解DIODE的定位。数据集场景范围深度类型官方法线标签大致分辨率传感器/采集方式NYUv2室内稠密但有空洞无单独官方标签640x480Kinect深度相机KITTI室外道路稀疏无1240x376车载激光雷达ScanNet v2室内稠密可从Mesh推导1296x968深度相机重建Matterport3D室内稠密可从Mesh推导1280x1024RGB-D结构光DIODE室内室外稠密官方直接提供1080p级别激光扫描投影从表格能看出来DIODE不是在所有指标上“最大”而是在“深度稠密、室内外都有、法线官方提供”这三个维度同时占优势。尤其是法线标签它不是事后用OpenCV算出来的近似而是从三维几何表面拟合出来的对于训练法线估计网络非常关键。1.4 适合哪些研究场景如果你正在做下面这些方向DIODE会比较对胃口单目深度估计输入RGB图输出逐像素深度在DIODE上训练可以直接看室内外泛化。表面法线估计官方法线标签省去自己从深度图反推的麻烦。深度和法线联合估计多任务学习一个网络同时输出两个几何量。三维重建和补全高分辨率稠密深度可用于补全实验。具身智能/机器人感知机器人需要在室内外不同环境中做深度感知DIODE适合做预训练数据。2. 深入拆解数据集的内部结构2.1 目录与文件命名DIODE官方发布后大家从官网下载的数据包通常会按“train/val”划分里面再按室内外场景类型组织。一个典型的目录结构长这样DIODE/ ├── train/ │ ├── indoors/ │ │ ├── scan_1/ │ │ │ ├── 000.png │ │ │ ├── 000_depth.npy │ │ │ ├── 000_normal.npy │ │ │ ├── 001.png │ │ │ ├── 001_depth.npy │ │ │ └── 001_normal.npy │ │ └── scan_2/ │ └── outdoors/ │ └── scan_3/ │ ├── 000.png │ ├── 000_depth.npy │ └── 000_normal.npy └── val/ ├── indoors/ └── outdoors/不过不同时期下载的压缩包命名可能会有一点点差异建议解压后先执行一条tree或者find命令把结构看明白再写加载器。我自己就吃过亏直接按网上老教程的路径写结果只在训练集里跑通了验证集的目录层级多了一层最后排查了很久才发现是文件路径前缀的问题。2.2 深度图与法线图的存储格式DIODE的深度图和法线图通常都是.npy格式对应一个numpy数组。深度图的数值单位是米比如某个像素的值是3.5就代表该点到相机平面的距离大约是3.5米。数组的形状和RGB图像完全一样都是(H, W)的二维矩阵。法线图则是(H, W, 3)的三维数组每个像素是一个三维向量。这里有个特别容易踩的坑不同数据集对法线向量的存储范围不一样。有的直接存[-1,1]的原始向量有的为了可视化会把向量缩放到[0,1]DIODE官方不同版本也可能有差异。所以读取之后不要直接输入网络先打印一下depth.min()、depth.max()、normal.min()、normal.max()确认取值范围再做后续处理。RGB图像方面DIODE提供的是高分辨率PNG图片有的版本可能是8位有的可能是16位。加载后先检查一下image.dtype如果模型需要8位输入需要做好转换。2.3 室内与室外场景划分DIODE把场景分成indoors和outdoors两个大子集这不是简单的“换了个背景”而是深度特性差异很大的两类数据。室内场景通常深度范围在几十厘米到十几米墙面、地面、桌子、家具会形成大量平面结构法线方向比较“规整”很多像素的朝向接近垂直或水平。室外场景则复杂得多可能有几百米远的建筑、树木、车辆深度动态范围特别大法线的分布也更加离散。如果直接把所有样本混在一起训练而不做采样调整室内样本很快会被“学明白”室外样本则可能一直欠拟合。因此很多论文会在训练时按场景类型做均衡采样比如每个batch里室内室外各占一半或者按深度范围重新加权。我们在构建自己的Dataloader时也可以这样做避免模型被某一种场景主导。2.4 数据质量背后的采集逻辑DIODE的样本量虽然不像ImageNet那样庞大但它的每帧标注都来自激光扫描和三维重建这带来两个直接好处。第一个好处是对齐精度高。RGB相机和扫描仪经过标定后三维点云重投影到相机坐标系生成深度图和平面法线图时不会出现“深度边缘和图像边缘差几像素”的问题。第二个好处是深度是稠密的没有传统深度相机常见的黑色空洞。你可以把DIODE理解成“用昂贵的离线设备做高质量标注再用这些标注去训练轻量级的在线估计模型”和很多依赖传感器直接采样的数据集在质量逻辑上完全不同。3. 从零开始加载和使用DIODE3.1 下载与工程部署下载DIODE需要到官网填写申请信息一般审核通过后会给一个链接。这里先提醒一句整个数据集体积不小尤其是高分辨率RGB图加npy数组解压后可能占用几十GB甚至更多。下载的时候建议用支持断点续传的下载工具比如wget -c或者图形化下载器避免网络波动导致前功尽弃。解压后的数据目录最好不要放在机械硬盘的默认位置DIODE的高随机IO会让数据加载变成瓶颈。实际使用中我会先把数据放到本地SSD训练过程中再用多进程读取。如果项目组有共享存储也要注意同时多个Worker读取会不会造成IO抖动。3.2 最简加载器代码下面我用Python给你写一个最简加载器的雏形。这里假设每个场景目录下RGB文件形如000.png对应深度文件是000_depth.npy法线文件是000_normal.npy。如果你的命名不同先按实际路径改一下文件后缀规则。import os import cv2 import numpy as np from pathlib import Path def load_diode_record(rgb_path): rgb cv2.imread(str(rgb_path)) rgb cv2.cvtColor(rgb, cv2.COLOR_BGR2RGB) stem rgb_path.stem # 例如 000 depth_path rgb_path.with_name(stem _depth.npy) normal_path rgb_path.with_name(stem _normal.npy) depth np.load(depth_path) normal np.load(normal_path) # 打印范围检查存储格式 print(depth range:, depth.min(), depth.max(), depth.dtype) print(normal range:, normal.min(), normal.max(), normal.shape) return rgb, depth, normal if __name__ __main__: root Path(DIODE/train/indoors/scan_1) first_png list(root.glob(*.png))[0] rgb, depth, normal load_diode_record(first_png)这个加载器只是用来验证数据通路真正训练时还需要加缓存、数据增强、Mask生成等逻辑。不过先跑通这一步能帮你确认文件路径和npy内容是否和预期一致。3.3 深度与法线可视化的小技巧数据和模型打交道第一件事不是训练而是可视化。深度图因为动态范围大直接用matplotlib默认的cmapgray看可能一片黑。我会用inferno颜色映射并且对深度取对数让近处和远处的细节都可见。import matplotlib.pyplot as plt def visualize_depth(depth, valid_maskNone): depth_log np.log1p(depth) plt.imshow(depth_log, cmapinferno) plt.colorbar() plt.title(log depth) plt.show()法线可视化稍微麻烦一点。如果法线向量的值域是[-1,1]需要先变换到[0,1]才能正常显示normal_vis (normal 1.0) / 2.0 normal_vis np.clip(normal_vis, 0.0, 1.0) plt.imshow(normal_vis) plt.show()如果法线值域本来就是[0,1]那就直接显示。这里强调一句显示漂亮不意味着存储正确一定要同时检查取值范围。3.4 预处理和工程化建议把加载器变成真正可用的Dataloader时有几个工程问题需要提前想清楚。第一是Resize策略。RGB图像可以用cv2.resize配双线性插值但深度图如果要缩放到模型输入大小我建议用最近邻插值避免插值把边缘深度“抹”平滑。第二是法线图。法线是单位向量双线性插值之后很可能出现模长不等于1的情况所以Resize之后最好再归一化一次或者干脆也用最近邻。第三是Mask的处理。数据集中可能存在无效深度区域通常定义一个valid_mask depth 0后续在计算损失时只让有效像素参与。如果训练过程中发现读取npy太慢可以把所有标注预先转成.npz或缓存成内存再配合num_workers8效果会好很多。4. 在DIODE上训练深度和法线估计模型4.1 输入标准化与增强模型输入通常要把RGB标准化到ImageNet的均值和方差这是CNN预训练模型的通用习惯。深度和法线也需要归一化但不能粗暴地“除以255”。对于深度我一般会先取对数depth_log log(depth 1)再缩放到[0,1]或者[-1,1]。因为室内外深度差异很大线性缩放会让远距离像素的数值压迫过来导致模型很难在近距离区域做精细预测。对数变换能缓解这个问题。当然如果你用的是仿射不变深度损失也可以不做全局归一化。数据增强方面水平翻转是比较安全的操作。RGB图像水平翻转后深度图也水平翻转法线图在水平翻转的同时其x分量需要取反。这是因为法线向量和相机的左右方向是关联的。用代码表达就是normal_flip normal[:, ::-1, :] normal_flip[..., 0] * -1随机裁剪、缩放也可以做但要同步处理深度和法线不能只增强RGB。颜色抖动对深度估计任务影响不大有时反而会增加训练难度我自己一般不开或者只加很小的亮度扰动。4.2 损失函数实现DIODE同时提供了深度和法线所以最常见的做法是设计一个多任务损失。深度部分可以选择L1、L2或者更鲁棒的Berhu损失。简单实现时L1就够用了import torch def depth_loss(pred_depth, gt_depth, mask): diff torch.abs(pred_depth - gt_depth) diff diff * mask.float() loss diff.sum() / (mask.float().sum() 1e-8) return loss法线部分一般用余弦相似度损失。给定预测法线pred_n和真值法线gt_n损失是1 - cos(夹角)夹角越小损失越低def normal_loss(pred_normal, gt_normal, mask): pred_normal torch.nn.functional.normalize(pred_normal, p2, dim1) gt_normal torch.nn.functional.normalize(gt_normal, p2, dim1) cos_sim (pred_normal * gt_normal).sum(dim1) # (B, H, W) loss (1.0 - cos_sim) * mask.float() return loss.sum() / (mask.float().sum() 1e-8)多任务加权时深度和法线的损失尺度差异很大。我一般先分别观察两个loss的初始量级再设定权重比如total_loss 1.0 * depth_loss 0.5 * normal_loss。如果法线loss明显偏大可以调小法线权重。4.3 评估指标评估深度预测质量多使用绝对相对误差(Abs Rel)、均方根误差(RMSE)、对数误差等。这些指标需要在有效Mask内计算。法线估计则常用角度误差即预测法线和真值法线之间的夹角统计平均角度误差、中位数角度误差以及小于11.25度、22.5度、30度的像素比例。这里有一个容易忽略的细节计算法线角度误差前预测法线和真值法线都需要归一化。否则夹角会偏大指标自然不好看。很多开源代码里会在输出层之后直接取softmax或归一化但评估时仍然建议再算一次避免测试时忘了对输出做后处理。4.4 Baseline 配置思路如果你想快速在DIODE上跑一个基础模型可以先把输入分辨率设为256x192或256x256使用ResNet编码器作为主干再用简单的Decoder输出深度图和一个3通道法线图。BatchSize可以设成16或32具体看显存。不用担心模型结构太简单先让整个训练闭环跑通再逐步替换更复杂的模块。数据准备时要注意同一个scan_id下的连续帧不要同时出现在训练集和验证集中。因为同一场景的帧高度相似如果不按场景划分验证集指标会虚高。DIODE官方已经给了train/val划分用官方划分是最稳妥的。5. 常见问题与踩坑排查实录5.1 读出的深度图一片黑或全为0这个问题最常见的原因是把深度文件路径读错了或者把法线文件当成了深度文件。还有一种可能是npy数组是float16类型显示时matplotlib没有正确处理导致看起来是黑的。建议先检查depth.dtype、depth.shape、depth.min()、depth.max()再用np.unique看是否有非0值。如果整个数组确实全为0那就是文件对应关系错了回头核对命名规则。5.2 法线图偏色或出现彩色条纹法线图偏色通常有两个原因一是法线值的取值范围不是预期的[-1,1]你把[-1,1]的向量当成[0,1]直接显示了二是法线可视化时通道顺序不对。检查顺序应该是先打印normal.min()和normal.max()确认是[-1,1]还是[0,1]再把前几个像素打印出来看x、y、z分量是否符合直觉。彩色条纹往往是Resize过程中用了双线性插值导致相邻像素法线方向被“平均”出现伪影。遇到这种情况就改用最近邻插值。5.3 训练时Loss爆高或NaN深度估计的NaN问题十有八九是没有屏蔽无效像素。DIODE虽然稠密但不代表每一个像素都有有效深度。如果有效Mask没做好无效区域的0值或异常值会被计入损失梯度自然就爆了。法线同理如果法线数组在无效区域是NaN计算normalize时会出现NaN传播最后整个loss变成NaN。解决的思路是定义统一的valid_mask这个Mask叠加深度有效区域和法线有效区域在损失计算前相乘。5.4 加载速度严重拖慢训练DIODE是高清图加npy单帧数据量比MNIST这类小图标大得多。如果直接在Dataset里每次读取npyGPU可能每时每刻都在等数据。建议把这个数据集预先处理成更适合训练的文件格式。最省事的是把所有样本存成.npz也可以使用lmdb或者干脆把深度和法线以uint16格式压缩存储。我自己的做法是先做一遍“缓存版数据集”把所有样本打平到内存中如果内存不够再分片处理。5.5 数据划分泄露导致指标虚高如果你不用官方划分而是把目录里所有图像按8:2随机切分很可能同一个扫描场景下的相邻帧分别进了训练集和验证集。由于这些帧实在太像模型在训练时几乎“见过”验证帧最终指标会虚高。正确做法是按scan_id分组一个场景的所有帧只能属于训练集或验证集不能跨集合。下面把常见问题和排查方式整理成表格方便你直接对照。问题现象可能原因快速排查与解决深度全黑读取错文件、dtype异常打印shape、dtype、min、max法线偏色取值范围未还原、通道顺序错打印像素向量按[-1,1]或[0,1]转换Loss为NaN无效像素参与损失、法线含NaN构造valid_mask屏蔽无效区域加载太慢高分辨率npy随机IO缓存为npz/lmdb增加num_workers指标虚高同一scan场景跨集合按scan_id划分train/val6. 我的实操心得和后续扩展6.1 拿到数据集后先别写代码先做可视化我见过不少同学拿到DIODE第一件事就是复制网上的Dataloader然后直接train最后发现法线通道都是反的白跑了好几个epoch。我的建议是拿到任何一个新数据集第一件事就是写一个最朴素的可视化脚本随机选择5到10帧把RGB、深度、法线、Valid Mask画在同一个图册里。花一个小时把数据看明白后面能省一天的时间排查问题。DIODE虽然官方说明写得很清楚但不同打包版本的文件命名和具体数值范围还是有差异不要想当然。6.2 从DIODE迁移到其他任务的思路DIODE适合做预训练这点很实用。我们可以在DIODE上先训练一个深度和法线的联合预测模型然后把backbone接回到自己的垂直场景比如机器人导航或工业视觉检测。因为DIODE同时覆盖室内外backbone能学到相对通用的几何表征比直接用ImageNet预训练可能效果更好。当然这也需要实验验证不同任务收益不同但至少在深度估计领域我实测下来DIODE预训练后的微调收敛速度是明显更快。6.3 一点小建议最后再分享一个自己常用的技巧如果DIODE的图像分辨率较高但显存受限不要直接在原始分辨率上训练而是采用“随机裁剪”策略先从训练图里裁出512x512或384x384的小块再输入网络。这样既能保留细节又能等价于一种数据增强还能把BatchSize提上去。等模型稳定后再在更高分辨率上微调几个epoch往往能拿到一个精度和显存兼顾的结果。如果后续想把DIODE用到更系统的工程里我会建议自己做一套数据质量检查工具定期统计depth的范围、normal的分布、mask占比把这些指标画成曲线。数据质量稳定了模型训练才有意义。这一点无论是做研究还是做产品落地都是通用的经验。