news 2026/9/16 1:38:47

DIODE数据集全解析:室内外稠密深度与法线标注

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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把场景分成indoorsoutdoors两个大子集,这不是简单的“换了个背景”,而是深度特性差异很大的两类数据。

室内场景通常深度范围在几十厘米到十几米,墙面、地面、桌子、家具会形成大量平面结构,法线方向比较“规整”,很多像素的朝向接近垂直或水平。室外场景则复杂得多,可能有几百米远的建筑、树木、车辆,深度动态范围特别大,法线的分布也更加离散。如果直接把所有样本混在一起训练而不做采样调整,室内样本很快会被“学明白”,室外样本则可能一直欠拟合。

因此,很多论文会在训练时按场景类型做均衡采样,比如每个batch里室内室外各占一半,或者按深度范围重新加权。我们在构建自己的Dataloader时也可以这样做,避免模型被某一种场景主导。

2.4 数据质量背后的采集逻辑

DIODE的样本量虽然不像ImageNet那样庞大,但它的每帧标注都来自激光扫描和三维重建,这带来两个直接好处。

第一个好处是对齐精度高。RGB相机和扫描仪经过标定后,三维点云重投影到相机坐标系,生成深度图和平面法线图时不会出现“深度边缘和图像边缘差几像素”的问题。第二个好处是深度是稠密的,没有传统深度相机常见的黑色空洞。你可以把DIODE理解成“用昂贵的离线设备做高质量标注,再用这些标注去训练轻量级的在线估计模型”,和很多依赖传感器直接采样的数据集在质量逻辑上完全不同。

3. 从零开始加载和使用DIODE

3.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默认的cmap='gray'看,可能一片黑。我会用inferno颜色映射,并且对深度取对数,让近处和远处的细节都可见。

import matplotlib.pyplot as plt def visualize_depth(depth, valid_mask=None): depth_log = np.log1p(depth) plt.imshow(depth_log, cmap="inferno") 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_workers=8,效果会好很多。

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, p=2, dim=1) gt_normal = torch.nn.functional.normalize(gt_normal, p=2, dim=1) cos_sim = (pred_normal * gt_normal).sum(dim=1) # (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.dtypedepth.shapedepth.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里每次读取npy,GPU可能每时每刻都在等数据。建议把这个数据集预先处理成更适合训练的文件格式。最省事的是把所有样本存成.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/val

6. 我的实操心得和后续扩展

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占比,把这些指标画成曲线。数据质量稳定了,模型训练才有意义。这一点无论是做研究还是做产品落地,都是通用的经验。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 1:38:28

Burp Suite实战:抓包、改包、重放、爆破四大核心模块详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 1:38:24

CentOS停服后迁移实战:Rocky Linux入门到运维高手

CentOS 7正式停服那天,我服务器上还有几十台生产机器没迁移完。算下来从接到通知到全部切换,前后折腾了三个多月,踩过的坑比走过的路还多。这期间我最终选定的替代方案就是Rocky Linux,也是今天这篇内容的主角。这篇内容不是那种泛…

作者头像 李华
网站建设 2026/9/16 1:35:40

Matlab中用粒子群算法优化XGBoost超参数实战

简介:本资源是一套基于Matlab实现的PSO-XGBoost混合智能算法分类预测完整方案,面向计算机、电子信息工程及数学等专业的本科生与研究生,适用于课程设计、期末大作业及毕业设计等实践场景,解决传统XGBoost超参数调优依赖经验、泛化…

作者头像 李华
网站建设 2026/9/16 1:34:44

Ubuntu安装Docker与Docker Compose:官方apt源配置与排错指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 1:34:17

Linkly AI:智能链接管理与UTM追踪工具解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 1:34:15

RDMA与GPUDirect RDMA深入解析:从QP/WQE到Zero-Copy内存旁路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华