
简介MVSNetPyTorch版代码注释版面向三维重建、多视图立体匹配方向的研究者与初学者。这份整理版在开源MVSNet基础上对核心模型、数据加载、训练评估等模块逐行添加详细中文注释并调整目录结构使代码更易阅读、调试与二次开发。压缩包共52个文件约57.31MB涵盖11个Python源码网络定义、训练、评估等流程、16个训练好的ckpt权重、7个MATLAB评估脚本、2个shell启动脚本以及环境配置说明和README注释版文档。目录明确划分datasets数据接口、models核心模块、evaluations评估工具和checkpoints预训练权重并且附有DTU数据集划分文件方便按需查找与复现。配套的train.sh/eval.sh可直接复现训练与测试流程适合希望快速上手MVSNet、理解代价体构建与深度估计原理的读者也便于在此基础上开展改进实验。目前已有669人学习/下载实用性与可读性兼备。1. 这个注释版 MVSNet 的价值,不是省掉原理而是让你少调十次参数你点进这个标题,大概率已经被 MVSNet 的原版代码折磨过一轮了。原始 PyTorch 移植版里,相机矩阵怎么进网络、代价体在哪个维度展开、深度回归的 soft-argmax 写在哪,都得靠自己去猜,张量 reshape 错了还不报错,只是精度慢慢烂掉。代码注释版做的事很朴素:把 MVSNet 的完整推理链路拆开,每一段 reshape 配上 shape 注释,同时调整代码结构,让训练、评估、数据加载各自独立成入口。它的真实收益不是帮你跳过原理,而是让你换数据集、调深度范围、改损失权重的时候,只改一个文件而不是满仓库翻。这篇文章写给三类人:要复现 MVS baseline 的研究生、想把自己的相机数据跑进多视角立体网络的工程师,以及准备微调 MVSNet 做新场景的人。下面按“先读哪里 → 怎么跑起来 → 参数怎么调 → 哪些坑必踩 → 怎么验收”的顺序讲,全程以 PyTorch 实现为落地路径。2. 拿到注释版后先读这三处:特征提取、代价体、深度假设范围带注释的代码不是给你逐行读的,而是给你快速定位“张量从哪来、到哪去”。MVSNet 整个网络可以压缩成三个模块:共享权重的二维特征提取、基于可变单应的代价体构建、深度方向上的 soft-argmax 回归。三个模块之间就是三类核心注释点,读完这三处,整个网络的骨架就出来了。2.1 特征共享与代价体:reshape 这一行才是阅读注释的关键MVSNet 的输入不是单张图,而是一组图:1 张参考图加 N 张源视图。网络先用同一个二维编码器把所有图都提成特征图,特征图分辨率通常是原始输入的 1/4 或 1/8。这一步很好理解,难的是之后:源视图特征要按参考视角去“对齐”,然后再合成一个代价体。# 输入形状示意 # ref: [B, 3, H, W] - 参考图 # src: [B, V-1, 3, H, W] - V-1 张源图 ref_feat feature_net(ref) # [B, F, h, w] src_feat feature_net(src.view(-1, 3, H, W)) # [B*(V-1), F, h, w] src_feat src_feat.view(B, V-1, F, h, w) # 对每个假设深度 d,把每一张源特征 warp 到参考视角 # warped: [B, V-1, F, D, h, w] var_volume torch.stack(warped_list, dim1).var(dim1, unbiasedFalse)这段代码里的关键在最后一行:多视图怎么融合。MVSNet 用的是方差代价体,对所有源视图的 warped feature 在 view 维上做方差,而不是把特征图拼成长通道。注释版基本都会在这个位置的 reshape 上写清楚V维的含义。你阅读时盯住两点:一是warped_list的长度必须等于view_num - 1,二是var(dim1)之后结果是[B, F, D, h, w],之后还要过一个三维卷积网络。你不需要记住每一层的卷积核大小,但要能画出这个 4D 代价体的变化链:[B,V,F,D,h,w]→[B,F,D,h,w]→ 3D U-Net →[B,1,D,h,w]。注释版的价值就在于这五六个关键 shape 都写在代码边上,你花十分钟把这些 shape 在纸上串起来,后面调参数就不会靠猜。2.2 深度假设与单应变换:决定最终重建质量的那组常量代价体不是凭空来的。每个深度假设d都对应一个单应矩阵,把源视图的特征坐标变换到参考视角。MVSNet 的代价体是一个离散的深度扫描结果,深度方向有多少层,取决于你设定的depth_num。单应矩阵的形式注释版会写得很细,常见写法是:# d: 当前深度假设 # n_ref: 参考相机主光轴方向 # C_ref / C_src: 参考相机与源相机光心 H K_src R_src (I - (C_src - C_ref) n_ref.T / d) R_ref.T K_ref.inv()从这个公式能看出三件事,也对应三个调参位置:第一,d是逐层变化的,depth_num越大,深度分辨率越高,显存也几乎线性上涨。第二,n_ref取的是参考视图的方向,所以换参考视图会直接改变采样空间。第三,单应依赖内参K,所以图片一旦 resize,内参必须同步缩放,否则整个深度图会出现系统性偏移,这属于后面会细说的高频坑。深度扫描范围一般由两个值控制:depth_min和depth_max,或者depth_start加depth_interval。很多注释版会把这两个值做成配置文件里的常量,不再让它们在每个 dataloader 里重复出现。你要做的是确认这个范围是否覆盖你场景里目标的最近和最远距离,而不是直接沿用 DTU 的数值。拿自己的室内数据跑 DTU 的深度区间,大概率得到一张整体漂移的深度图。2.3 结构调整后,你只要盯住三个入口文件标题里特别强调了“代码结构有所调整”,这是注释版和老仓库最直观的区别。原始工程常把训练和评估的逻辑混在几个大文件里,改一个数据格式要牵扯三处。常见的整理方式,是把代码拆成三个口:一是train.py,负责训练循环、学习率调度、日志输出;二是eval.py或test.py,负责加载 checkpoint、跑单组 scan、输出深度图;三是dataset.py或dataloader.py,负责读图像、读相机文件、构建 scan 列表。你下载注释版之后,先不要急着跑,花十分钟做一件事:打开目录,找出这三个文件,然后确认它们之间通过什么传递参数。有的版本用argparse,有的版本用 yaml 配置文件。这决定你后面改参数是改命令行还是改 yaml,别把时间浪费在找参数上。3. 跑通最小流程:一台 GPU 上从零配置到输出第一张深度图这章的目标不是训练,而是让你在最短时间内看到一张深度图。做 MVS 的人最容易在训练上过早投入,结果数据格式错了都没发现。先把推理流程跑通,再回头训练,方向才对。3.1 环境准备:用 conda 装 PyTorch 并验证 GPU 可用注释版一般依赖 PyTorch 和几个常见库。最稳的路径是在 conda 里单独建一个环境,不要让 base 环境的包干扰你。conda create -n mvsnet python3.8 -y conda activate mvsnet pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python tensorboard numpy scipycu118指的是 CUDA 11.8 版本,如果你的驱动支持更新的 CUDA 12.x,也可以把cu118换成对应版本。装完先别急着跑训练,验证一下 PyTorch 到底能不能调用显卡:import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果torch.cuda.is_available()返回False,最常见原因有两个:一是 PyTorch 装成了 CPU 版,二是 CUDA 驱动太旧认不出 cu118 编译产物。解决办法是把 torch 卸了重装 GPU 版,而不是去折腾驱动。WSL 环境里这套流程同样适用,但记得先确认 WSL 里nvidia-smi能正常输出。3.2 数据目录:先把 scan 布局摆成注释版期望的样子MVSNet 的数据读取逻辑依赖固定目录结构,尤其依赖相机文件和图像命名的一致性。DTU 数据集的典型布局如下:data/ dtu/ train/ scan1/ images/ 0000.jpg 0001.jpg cams/ 0000_cam.txt 0001_cam.txt depths/ 0000.pfm pair.txt test/ scan40/ scan41/这里最容易出错的是pair.txt的格式。它决定了评价某个参考视图时,网络会拉哪些源视图进来。格式一般是:第一行是 scan 数量,然后每个 scan 下面先写视图编号,再写源视图编号和图片名。注释版的 dataset 构造函数会直接读这个文件。你用自己的数据时,可以先写一个脚本把图像名填进去,保持“第一块大 RT 矩阵、第二块内参、第三块深度范围”的相机文件结构。不同版本读取相机文件的细节不一样,但都离不开这三块信息,你打开dataset.py看第一段 parse 函数就能确认。3.3 最小推理:加载 checkpoint 输出一张深度图环境没问题、数据路径摆对了,就可以跑推理。python eval.py \ --data_root data/dtu/test \ --model_path save/checkpoints/mvsnet_dtu.ckpt \ --scan scan40 \ --view_num 3 \ --depth_num 192 \ --save_dir output/demo参数里scan指定评估哪一个场景;view_num控制每个参考视图同时读入几张源图,最小值为 2,取 3 比较稳妥,取太多显存压力大;depth_num是深度假设层数,推理时一般比训练时更大,比如训练用 48 层,推理用 192 层,这样深度估计更细。跑完之后去save_dir里找输出文件。常见格式是.npy或.pfm。你可以用numpy直接加载并用matplotlib画灰度图,先肉眼看一下深度图是不是连续平滑的。第一次跑通的意义在于确认数据链路是通的,哪怕精度一般,也说明你能在这个代码结构上继续迭代。4. 训练和微调:八个你必须自己重新决定的参数跑通推理只是第一步,MVSNet 真正花时间的地方是训练。注释版的优势在这里体现得最明显:参数集中了,改起来不用到处搜。但集中不是替你决定,每个值都要结合显卡显存、数据分布来调。4.1 训练参数表:先按这个表过一遍默认值我整理了一张常用参数表,注释版里基本都能找到对应项。表的目的是给你一个起点,不是标准答案。参数常见默认值主要影响batch_size2显存占用随 batch 线性增长,别先提这个view_num3训练/ 5评估代价体的V维,显存几乎随它线性涨depth_num48 起步,微调可到 96深度层数,影响显存和深度分辨率depth_min / depth_max按数据集设定影响深度假设范围,错了深度图整体漂移image size640×512 左右平方级影响显存和运行速度learning_rate1e-4 ~ 1e-3Adam 底下一般 1e-4 起步loss_mask由 GT depth 生成决定哪些像素参与 loss 计算save_freq5 epochs训练崩溃时的后悔药基本逻辑是:显存不够先降depth_num,再降view_num,最后才降batch_size。很多新手上来把 batch 设成 1,以为省显存,实际上 batch 从 2 降到 1 对训练稳定性影响不小,而depth_num从 96 降到 48 往往看不出明显精度损失。4.2 损失结构与 mask 占比:权重别瞎调MVSNet 的损失主体是预测深度和 GT 深度之间的 L1 距离,但只有有效像素参与计算。有效像素由 mask 控制,GT depth 中数值大于 0 的位置才算有效。valid_mask (gt_depth 0).float() diff (pred_depth - gt_depth).abs() * valid_mask loss diff.sum() / (valid_mask.sum() 1e-6)这个公式里最容易忽略的是归一化方式。valid_mask.sum()可能只有整图的 60%,如果 mask 区域很小,单个像素的误差会被放大,训练容易震荡。注释版通常会把 mask 比例打印在日志里,你要养成看的习惯。权重调整上,我的建议是先不动多尺度 loss 的权重,保持 1:1:1,等到 baseline 稳定了再根据哪一层误差大去加权。不要一上来就调成 1:0.5:0.2,那等于提前破坏训练稳定性。4.3 两种训练模式:从零训练和迁移微调如果你的数据不是 DTU,最好别从零开始训练 MVSNet。特征提取器学到的图像先验在很多场景下是通用的,直接复用比自己从头训更稳。做法分两步:先冻结特征提取网络,只训练代价体正则化和深度回归部分,跑十几个 epoch 让 loss 稳定下来;再解冻全部参数,用小学习率做全量微调。python train.py \ --data_root data/mydata/train \ --pretrained save/checkpoints/mvsnet_dtu.ckpt \ --freeze_feature_net True \ --epochs 30 \ --lr 1e-4冻结阶段重点关注 loss 是否从一个大值稳步下降。如果 loss 完全不降,大概率不是参数问题,而是数据加载错了,去检查相机文件和 mask。解冻之后把学习率降到原来的 1/5 甚至 1/10,防止破坏已经学到的特征。5. 避坑:注释版项目里最容易翻车的五个场景这部分是血泪经验。每个坑我都按“现象 → 原因 → 解决”来写,你在跑的时候遇到类似症状,直接照方子查。5.1 图片缩放忘改内参,深度图出现整体偏移现象: 用自己的图片跑推理,深度图能出,但整体往一个方向偏,有些地方还出现明显的条纹。训练 loss 能降,但点云拼接后错位。原因: 注释版里图像被 resize 到网络输入尺寸,但相机内参K还是原始分辨率。特征图坐标系和深度回归坐标系用的都是缩放后的图像坐标,内参不缩放,单应矩阵就错。解决: 在数据加载阶段同步缩放内参。K_scaled K.copy() K_scaled[0, 0] * resize_w / orig_w K_scaled[1, 1] * resize_h / orig_h K_scaled[0, 2] * resize_w / orig_w K_scaled[1, 2] * resize_h / orig_h我这里要把orig_w和resize_w的比例放到主对角线的 x 轴焦距和 y 轴焦距上,主点cx、cy也按同比例缩放。不要只缩放焦距不缩放主点,那会让深度图产生斜向的误差带。5.2 深度图几乎全是一个值,先查 depth interval 而不是网络现象: 输出深度图整体发白或整体发黑,灰度直方图集中在一个小范围内,完全没有远近层次。原因: 深度假设范围跟实际场景不匹配。比如场景最近 1 米、最远 5 米,你设的深度范围是 0.1 到 0.5,那么所有像素都会落在深度推断的边界附近。解决: 先用简单的统计方法确认场景深度分布,就是把几张图片的稀疏深度点或三角测量结果打出来,看最近最远值,再设depth_min和depth_max。我在调参时会把这两个值打印进日志,每轮训练前先确认一次,别让这个问题混进训练过程。5.3 显存 OOM 时,最该删的是 view_num 而不是 batch现象: 训练到一半报CUDA out of memory,或者卡在 dataloader 加载后直接崩掉。原因: 不少人第一反应是把batch_size改成 1。但 MVSNet 的代价体在view_num维上要计算方差,每多一个源视图,显存占用就多一大块。有的注释版在view_num3、batch_size2时能跑,改成view_num5后 batch 调到 1 还是崩。解决: 先把view_num降到 3,再看depth_num能不能降低,最后才动batch_size。显存确实还不够,就降输入分辨率,这是影响效率最明显但也是最后的手段。分辨率变化后记得同步改内参。5.4 损失下降但点云全是洞:mask 的锅现象: 训练曲线很漂亮,验证 loss 一路向下,但深度融合出来的点云稀疏,空洞明显。原因: 训练时 mask 区域包含了很多边界像素,或者 GT depth 本身就存在大量无效值。网络在这些像素上被强制学习一个不存在的深度值,结果和 GT 对不上,损失很小只是因为 mask 权重低。解决: 在train.py的日志里把 mask 百分比打出来,如果有效像素占比低于 50%,先检查 mask 生成逻辑。另一个常见来源是 pfm 格式的 GT depth 读取时用错了字节序,读出来全是噪声, mask 也被污染。读 pfm 时打开文件头确认 scale 是 1 还是 -1,-1意味着小端字节序,处理方式不一样。5.5 加载旧权重报 unexpected key:代码结构调整的代价现象: 用注释版加载之前下的 checkpoint,报unexpected key in state_dict,或者加载成功但精度极差。原因: 注释版调整了网络模块的命名。比如原版里某个子模块叫feature_net,注释版拆成了feature_extractor.conv1这类层级命名,权重文件的键名对不上,load_state_dict(strictTrue)直接报错。解决: 先打印 checkpoints 里有哪些键,再打印当前模型的键,对比后写一个键名映射脚本。如果你不打算花时间对齐,直接加载strictFalse,但实际效果基本等于没有预训练权重。我的做法是训练前从同一份权重生成一个兼容版本,生成完就放进训练目录,后续不再依赖原始下载文件。6. 真正验收:把深度图、点云融合和三项指标一次跑完训练结束不是看 loss 数字,而是看深度图和融合点云。我自己的习惯是训练完第一件事不是跑指标,而是把验证集里的 5 张深度图拉出来看一眼。先把深度图按深度范围做归一化,用伪彩色渲染。import matplotlib.pyplot as plt depth np.load(output/demo/depth_0000.npy) valid depth 0 norm_depth (depth - depth.min()) / (depth.max() - depth.min() 1e-6) plt.imsave(val_depth.png, norm_depth, cmapturbo)看深度图时盯三个点:墙面是否平滑、物体边缘是否锐利、远处区域是否出现条纹。平滑但偏糊,说明正则化过强;锐利但背景乱,说明代价体噪声大,需要提高depth_num或增加源视图数量。深度图没问题后,再做点云融合。MVSNet 的验证流程通常是:每个视图出一张深度图,然后反投影成点云,用光度一致性和几何一致性做过滤,最后拼成一个完整模型。到这一步,你会用到第三方的融合脚本,注释版一般会提供调用方式。拼接完之后跑三个指标:accuracy、completeness、overall。accuracy 是预测点云到 GT 点云的距离,completeness 反向,overall 是两者的均值。三项指标分开看,不能只盯一个,因为调节过滤阈值时两者会互相拉扯。我最后说一个自己的教训:有一次验证 accuracy 很好,completeness 很差,我一直以为是网络问题,后来才发现是点云融合时过滤条件太严格,把很多远距离正确点都滤掉了。从那以后我每次调完融合脚本,都会把过滤前后的点云数量一起打印出来对比,数量骤降超过 50% 时,先怀疑参数,再怀疑模型。MVSNet 这类 MVS 网络的调参链条很长,从内参缩放、深度范围到 mask 统计、融合阈值,每一环都能让结果翻车。希望这篇笔记能帮你把每一环都变成可检查、可复现的步骤,而不是靠抽卡式试参。祝顺利。本文还有配套的精品资源点击获取