三维重建:实现过程说明
0. 整体在算什么
输入是已完成的空三 :每张图有内外参、稀疏连接点及多视观测关系。MVS 阶段要补全稠密几何 并变成可浏览的三维模型。
数据在工程里大致经历这条链:
原始影像 + 相机畸变
→ 去畸变影像(像素与针孔模型一致)
→ 每张参考图的深度图 + 法线图
→ 多视一致的稠密点云
→ 三角网格(Delaunay + 可见性图割)
→ 网格优化、减面、滤波
→ 每三角面选源图、拼纹理、切 LOD
→ OSGB/B3DM 等瓦片 + 可选 DSM/TDOM
1. 工程接口:CC / Colmap → OSG
在做什么
把第三方空三成果翻译成统一的内存模型 ,再序列化成二进制 .OSG ,供后续所有步骤读取。这一步不做稠密匹配或构网。
CC(BCC/XML)过程
- 解析 XML:空间参考(优先非经纬度投影/局部系)、相机表、影像表、连接点与 tracks。
- 为每个物理相机建立内参向量:
fx, fy, cx, cy, k1, k2, k3, p1, p2, skew(OpenCV 顺序在存储时与 OpenCVdistCoeffs有 k3/p 顺序差异,去畸变时会调换)。 - 每张影像绑定相机 ID、外参(旋转+中心)、原始路径;连接点带各视观测像素坐标。
- 可选:用调用方给的
(x,y,z)覆盖场景坐标原点偏移(大坐标数值稳定)。 - 整体写入
.OSG(二进制块:相机、点、图、路径映射等)。
Colmap 过程
- 读
cameras.bin、images.bin、pointOSG.bin等 COLMAP 导出。 - 映射到同一套
MVSBlock结构(相机模型、影像位姿、稀疏点)。 - 同样
SaveOSG;路径与坐标系与 CC 路径一致,后续流程无差别。
本质 :一次性的格式归一化,把"空三软件私有工程"变成"本引擎可读工程"。
2. 畸变改正
在做什么
PatchMatch 假设中心透视 + 无畸变 。原始 UV 影像带径向/切向畸变,必须先重采样到新像素网格,并更新"影像路径表",使后续只读去畸变图。
实现过程
- 按相机分组 :同一
camera_id的所有影像共享一组 remap 表,避免重复计算。 - 建 remap :
- 无 skew:OpenCV
initUndistortRectifyMap,得到mapx/mapy(浮点映射:畸变像素 → 去畸变像素)。 - 有 skew:手写逐像素映射(考虑
sk/fy等)。
- 无 skew:OpenCV
- 重采样 :对每张图用 remap 输出到
working_dir/undistort/;可用 CUDA Warp、OpenCL 或 CPU。 - 掩码 :若提供
mask_pair.txt,按影像名加载二值掩码,与去畸变结果对齐,写入相机级undist_mask。 - 维护映射表 :JSON 记录
ImageID → 去畸变文件路径,空三里的几何仍用同一套内参(畸变系数在匹配阶段不再使用)。 - 超大图切分 (可选):若平均影像边长 >
image_max_size:- 将单张大图切成多张"虚拟影像",每张有独立 ID 和 ROI;
- 更新空三并另存
split.OSG与新的路径表; - 目的:控制单张加载显存/内存上限。
3. 稠密匹配(PatchMatch)
在做什么
对每张可作为参考图 的影像,在邻接图上估计逐像素深度 + 表面法向 ,输出 depth/ 下与影像 ID 对应的深度/法线文件(尚未有点云)。
3.1 准备:视图对与深度范围
- 按空三邻接关系,为每张图选约 4 张邻接图(1 参考 + N 源),邻接数不足则该图不参与稠密化。
- 深度范围 (每张参考图):
- 把所有可见且被≥2/3 视观测的稀疏点投影到该相机,取深度 min/max;
- 再用场景包围盒 8 角点投影裁剪;
- 上下各扩 10% margin;
- 电力线模式 :抬高
min_depth下限(线状目标稀疏点少,避免范围过宽)。
- 深度图分辨率 = 去畸变图分辨率 ÷
2^depth_scale_level(常用降一级,RGB 上 patch 更大、更稳)。
3.2 GPU PatchMatch 单像素在算什么
对每个参考图像素 (x,y),维护状态:深度 d、法向 n、各源视代价。
代价(NCC):
- 由
(d,n)构造单应:平面诱导 homography,把参考 patch warp 到源图。 - 在参考图取多尺度 patch(如 7×7 与 13×13),在源图用 CUDA 纹理双线性采样对应 patch。
- 计算归一化互相关 NCC;多源视取加权得分。
优化(迭代):
- 空间传播 :从邻像素拷贝
(d,n),沿扫描线用平面几何传播深度(水平/竖直方向解析求交)。 - 随机扰动 :在深度、法向锥内随机试探更优解(
curand)。 - 视图选择 :
SelProbCalc用 forward/backward message 更新各源视被选概率(多视一致性)。 - 多尺度:
scale_level控制先在粗分辨率跑,再细化。
Batch 内:最多约 100 张参考图一批,本批所有参考+邻接图影像一次性上传 GPU;批结束释放 RGB,深度写盘。
3.3 交叉深度过滤(多视几何校验)
对每张参考深度图:
- 像素
(c,r)深度d→ 用 I2W 投影矩阵变到世界点P。 - 对每个邻接图,用 W2I 把
P投到邻接像素(x',y'),读邻接深度d'。 - 若
|d'/d_proj - 1| ≤ max_diff_ratio(默认约 1%)则计一票。 - 票数 ≥
min_num_fuse_point_(默认 2,含自身)则保留该像素,否则置 0。 - 写回深度图,并写
.crs标记已过滤。
实现上用 LRU 缓存邻接深度图,并按"下一 filter 哪张图最省 IO"贪心选序。
3.4 分块稠密(工程调度)
- Pre:统计要跑多少"匹配块 / 过滤块"(按影像分组,非空间切块)。
- Match 块:只跑 PatchMatch,写深度。
- Filter 块:只跑交叉过滤。
匹配尽量整景 batch,避免块边界重复匹配;过滤可分批。
4. 点云融合与 LiDAR
4.1 深度图 → 稠密点云
目标 :把多张深度图合成带颜色、带"被哪些视看到"权重的点集(.vpc)。
过程(逐参考图、可 OpenMP):
- 已有融合点列表
FusePoint(3D 位置、参考法向、融合像素数、各视权重)。 - 扩展已有 3D 点:把 3D 点投回当前参考深度图;在圆形窗口内找深度/法向接近的未标记像素,累加 3D 坐标;记录该视贡献权重(遮挡时记 -1)。
- 种子新点 (当前图尚无足够融合时):扫描未标记像素,合法深度+法向 → 世界坐标,作为新
FusePoint,再对邻域做同样窗口融合。 - 邻域准则:相对深度差 <
max_depth_error_(约 0.6%)、法向夹角 < 45°、窗口内综合代价 < 1。 - 融合结束后:按可见影像 从去畸变图采样 RGB(
texture_bands指定波段),得到彩色点云。 - 可选导出 LAS/PCD/PLY;点云 OSGB 用 Poisson 采样 + 空间分块建树(见 LOD 点云)。
平均点距 mean_distance:由融合点间距统计,供后续网格简化尺度。
4.2 LiDAR(nPointFusionType)
| 类型 | 过程 |
|---|---|
| 0 | 仅 MVS 融合点进入构网 |
| 1 | 以 LAS 为主,可用 CGAL 前进前缘等从激光点直接建壳 |
| 2 | MVS 点与激光点合并后再 Delaunay;dfPointFusionArg 控制融合距离/权重 |
激光文件须与空三同一坐标系;水体掩码可在构网前剔除水面附近三角化。
5. 网格重建与后处理
5.1 Delaunay + 可见性图割(核心构网)
输入 :融合点云,每点带 point_views(视 ID + 可见性权重 α)。
步骤:
- 3D Delaunay 四面体化(CGAL):点做空间排序后插入,得到四面体网格。
- 支撑点(可选):检测狭长四面体,按概率在内部插入辅助点,减轻"穿透"虚假面;水体边界附近特殊处理。
- 赋权 :
- 每个四面体是一图节点;四个面是四条边,容量存在
cell_info。 - 每个相机:定位相机中心所在四面体,与源点相连;相机视锥内可见的凸包 facet 连到 sink/source。
- 对每个点-视射线:沿射线穿过的一系列四面体,在对应面上累加 α_vis(可见性权重);结合 GSD 估计的 σ、形状惩罚等。
- 每个四面体是一图节点;四个面是四条边,容量存在
- 最大流/最小割(IBFS):划分"内部 / 外部"单元。
- 提取表面:相邻单元一侧在源、一侧在汇 → 公共三角面成为 mesh 三角;统一法向朝向。
- 清理:删小连通片、退化面、非流形等。
输出 :块目录下 raw mesh(顶点+三角,无纹理)。
5.2 网格精细化(SceneRefine)
在 raw mesh 上,用多视光度一致性微调顶点(参考 OpenMVS):
- 建立面-邻接、选最多 3 张参考视;
- 在 3×3 窗口内比较渲染灰度与真实影像梯度;
- 平滑项 + 数据项迭代;可选 CUDA Refiner 加速;
- 分辨率比构网低一级(
resolution_level - 1)。
5.3 减面、滤波、边界平滑
- QEM:二次误差度量边折叠,控制面数到与 GSD/点距匹配的量级。
- MeshFilter(含 CUDA):拉普拉斯/双边类平滑,去尖刺噪声。
- MeshBoundarySmoothing:对开放边界(水面、裁剪边)做公平化/洞处理,避免锯齿墙。
6. 纹理映射
在做什么
给每个三角面选一张源影像,裁 patch、拼 atlas、消除接缝,再交给 LOD 写文件。
过程
-
面→多视投影评分 (
ProjectFaces2Views):- 建 mesh 的 AABB 树;
- 对每张去畸变图:视锥裁剪可见三角面 → 光栅化到
face_id缓冲 + 深度缓冲 → 处理遮挡; - 在投影区域内算质量分 (入射角、分辨率、可见面积等),每面得到
(view_id, quality, color)列表。
-
面片选图 (
FaceViewSelection):- 三角邻接建图;连通分量上做多视标签优化(数据项=投影质量,平滑项=邻面同图偏好);
- 得到每面最佳
view_id,相邻面尽量同图以减少接缝。
-
参数化 + 提 patch (
GenerateTexture):- 按连通 patch 做 UV 参数化(含洞处理
ParameterizationHole); - 从选中影像裁三角 patch 图像,记录每面 UV。
- 按连通 patch 做 UV 参数化(含洞处理
-
接缝处理:
- GlobalSeamLeveling:在 patch 边界做颜色线性过渡;
- LocalSeamLeveling:沿 seam 图做局部 Poisson/梯度域融合。
-
打包 (
PackTexture):- 多 patch 装箱进 4096/8192 atlas,生成
face_to_atlas与新 UV; - 按
texture_format、质量参数写 jpg/png/webp。
- 多 patch 装箱进 4096/8192 atlas,生成
-
可选 锐化 (
nSharpeningLevel)在写 atlas 前对 patch 做反锐化。
7. 分块重建与合并
7.1 为什么要分块
单块要同时扛:多视影像缓存 + 深度/点云 + Delaunay + 纹理 ,内存随像素总量增长。分块让每块只在子空三 + 子影像 ROI 上跑完全流程。
7.2 怎么切(BlockCut)
- 统计全局:连接点、每图有效像素、点-视射线。
- 若指定
tile_size > 0:规则网格 切 XY(+Z 约束),按原点(tile_x,tile_y,tile_z)对齐。 - 否则 自适应二分 :
- 估计当前块内存(公式含:patch 批影像、refine、texture 像素量);
- 若超过可用内存 80%:在 X/Y/Z 中选使两侧像素数、射线数、点数最均衡的平面切开;
- 递归直到满足内存或
max_depth; - 块太小(点<100 且像素<10万)直接收为一块。
- 每块:裁剪属于块 bbox 的连接点/影像 ROI,导出子
.OSG到Block_i/,写 JSON 记录块名与 global/tight bbox。
7.3 单块流水线
对块 i 依次:加载块 depth → Fuse → Delaunay 构网 → Refine → Texture → LOD 写 OSGB/B3DM...
块 bbox 外扩 tile_overlape 用于纹理/接边,避免缝上缺纹理。
7.4 合并
- 各块已生成
Data/Tile_x/Tile_x.osgb等子根; - 合并阶段:收集子根路径,生成顶层单一 OSGB/B3DM tileset (
CollectBlocksIntoOne*); - 若
smooth_overlape:在块公共边界对重叠三角做几何/颜色混合,减轻高程台阶。
8. 成果:DSM / TDOM
DSM(GenerateDSM)
- 用块点云或深度,按
dfDsmGsd在 XY 上铺格网; - 每格取最高点(表面模型,非地面);
- 用
pszBoundary栅格化掩码,范围外 NoData; - GDAL 写出 GeoTIFF 等。
TDOM(GenerateTDOM)
- 已有带纹理 mesh:正射投影到水平格网;
- 每像素找最可见/最前三角面,用重心坐标插值纹理颜色;
- 可同时输出 DSM(同一网格 Z 极值);
ConvertModel2TDOM:不跑 OSG,直接对已有 OSGB+metadata 做同样栅格化。
9. 点云分类、轻量化、裁剪
点云分类
- 读块
.vpc点; - 对每个点,用其
point_views里权重最大的视,把 3D 点投到该影像坐标; - 用 GDAL 读与原始影像同名 的分类矢量,空间查询
class_id; - 多点投票得类别,写回 LAS classification 字段。
模型轻量化
遍历 OSGB 的 PageLOD 树,用 OSG Simplifier 按档位减少三角形、压缩纹理分辨率,输出新目录树。
模型裁剪
读范围矢量,对 OSGB 节点做包围盒相交测试,保留/删除子树(bClipMode 控制保留内或外)。
10. 白膜(建筑物 + DSM)
独立于 MVS,输入只有矢量和栅格:
- 读建筑物 footprint 多边形;
- 在 XY 上对 footprint 做 约束 Delaunay 2D 三角剖分(CGAL CDT,孔洞用 nesting level 标记域内域外);
- 三角顶点 Z 从 DSM 采样(顶点在 DSM 上双线性/最近邻取高);
- 沿轮廓竖直拉伸到统一底高(或地形),形成封闭棱柱体;
- 可选 DOM 作为顶面纹理;
- tessellate 后写 OSG Geode → OSGB/OBJ。
得到的是无倾斜摄影纹理的简化建筑壳,用于 LOD1 城市场景。
11. 格式导出(实现层面)
LOD 阶段把同一套 mesh+atlas 栅格化为不同容器:
- OSGB :OSG
PagedLOD树,子文件相对路径,根节点Model.osgb; - B3DM :三角网格 + glTF 二进制 + 3D Tiles
tileset.json,; - OBJ/PLY:顶点、面、mtl/贴图文件;
- S3MB/C3MX:调用第三方 SDK 写行业格式。
本质是:同拓扑 + 同纹理图集,换封装与坐标元数据。
12. LOD:网格与点云
网格 LOD(LOD)
- 对块 mesh 的 AABB 递归八叉分割(
CutNode); - 叶子节点:若三角面数仍多,用 QEM 再简化一档;
- 每层生成较低分辨率纹理(缩小 atlas 或重采样);
- 为每个 tile 计算
geometric_error(与节点尺寸相关),写入 OSGBPagedLOD的切换距离; - 远处加载粗 mesh+糊纹理,近处
REPLACE为细 mesh。
点云 LOD(PLOD)
- 对块内彩色点做 Poisson Disk 采样 降密;
- 空间均匀立方格(如 200 格/边)划分;
- 每格若点数 > 5000 则四分,子格递归;
- 叶节点打包为 OSGB
PointGeode,父节点PagedLOD引用子文件; - 形成与网格类似的多级点云树。