How it works(15) OpenDroneMap结构简析

引入

OpenDroneMap(以下简称ODM),是一套非常完整的开源无人机影像三维重建服务,我以前写过的NodeODM源码阅读就是ODM的子项目.当时我所在的公司主营业务是无人机相关,基于ODM我们打造了一套全自动化无人机数据处理链路:无人机影像在线回传,自动构建成最终的正摄影像.

ODM是一个好产品.举个例子:就算我梳理了ODM的脉络,我其实依旧不理解SLAM,毕竟这是一个门槛比较高的领域,但这并不妨碍我对ODM进行二次服务化的服务平稳好用的运行了大半年.

什么是好的产品?

ODM整个项目在我完全不了解原理的基础上,做到了让初学者即可快速搭建(现成的Docker镜像),无障碍上手(优秀易懂的UI界面),这就是我认为的好产品.尤其在我们的领域,好代码,好算法并不少,好产品很少.

很长时间过去了,ODM也终于更新到1.0版本之上(本文基于0.8.2,因为写的比较早,可能和现在的结构已经大不一样了),我也早就不再从事类似工作了.但ODM确实是一个让我们进入类似领域的好示范.

我依旧不懂SLAM和三维重建相关领域的知识,以下内容大多是靠百度的资料和自己的理解,一定会有错误,权当抛砖引玉了

技术铺垫

生成正摄影像的两条路:

  • 光束平差法:根据相机的位置,俯仰等参数,将照片确定在固定的位置(这是光束法,还没平差),当然,实际应用时,会复杂许多,比如需要打碎图片,整体调光等.
    • 优点:
      • 平地效果好
      • 速度随着图片数量增加下降的没有那么多
    • 缺点:
      • 对原片的飞行姿态参数的准确性要求高
      • 当起伏比较大时不好处理
  • 三维重建:把所有一堆原片打散成一堆细小的素材,利用算法把这些小素材重建三维模型,再通过投影的方法得到正摄,当然,可以得到任何向的影像,不只是正摄
    • 优点:
      • 对相机参数飞行姿态信息没有要求
      • 对任何测区情况都适用
      • 正摄倾斜用一套算法
    • 缺点:
      • 效果不如光束平差法
      • 对重叠率要求较高,否则无法完成三维重建
      • 对测区图像纹理有要求,太过规律的素材难以重建,比如平静的湖面
      • 时间较长且无法预估(跟照片质量有关)

作为一个通用型工具,ODM自然选择了三维重建

sfm三维重建的一般步骤

三维重建和核心思路就是利用同一点在两幅或多幅图像恢复三维信息

  1. 提取特征点:SIFT算法

    SIFT尺度不变特征转换:

    局部影像特征的描述与侦测可以帮助辨识物体,SIFT 特征是基于物体上的一些局部外观的兴趣点而与影像的大小和旋转无关。对于光线、噪声、些微视角改变的容忍度也相当高。基于这些特性,它们是高度显著而且相对容易撷取,在母数庞大的特征数据库中,很容易辨识物体而且鲜有误认。使用 SIFT特征描述对于部分物体遮蔽的侦测率也相当高,甚至只需要3个以上的SIFT物体特征就足以计算出位置与方位

  2. 图片两两匹配:

    所有图片做两两匹配,生成匹配列表

  3. 根据匹配列表计算估算相机参数场景.生成的是稀疏的点云.

    因为不可能图上任何一个点都是特征点

  4. 利用相机参数进行稠密重建

  5. 生成表面素材

三维重建最大的意义其实是让机器能从平面理解空间

sfm三维重建的一般软件链路

  • VisualSFM/Bundler(产生稀疏点云)+CMVS-PMVS(产生稠密点云)+MeshLab(查看点云成果)

  • MVE(稀疏+密集)+MeshLab(查看点云成果)

ODM

ODM用到的C++库

  • mvs-texturing:多视图的密集重建
  • opensfm:sfm三维重建
    • opengv:另一个机器视觉方法集合库
    • opencv:机器视觉方法集合库
    • ceres:用于解决具有边界约束和一般无约束优化问题的非线性最小二乘问题
      • gflags:命令行参数解析,属于非核心工具函数
  • orb_slam2:一个实时slam库(没用上)
    • opencv
    • pangolin:处理视频
  • PCL:点云库

这些库用于构建ODM的c++核心

ODM用到的命令行工具

  • entwine:用于大规模点云的数据组织库
    • PDAL:点云库,用于读取和操作PCL的数据
      • zstd:快速无损压缩
      • hexer:LiDAR生成六边形点集
      • LASzip:LiDAR文件压缩
  • gdal全家桶
    • gdaladdo:内嵌金字塔
    • gdal_merge:图像合并
    • gdalwarp:在这里用作蒙版裁剪
    • gdal_translate:在这里是用作融合两张图像

ODM文件夹结构

我们一般只需关心三个文件夹:

  • modules:算法核心

    c++编写,编译后生成可执行文件

  • opendm:全部后勤

    python编写,负责拼接参数,转换格式等辅助性功能

  • stages:流程控制

    整个ODM的运行核心,包含了完整的流程,也是程序的入口,主要是通过opendm的辅助方法构建合理的参数,调用modles里面的核心算法.

    内部也采用命令行直接调用.

内部流程

从stages文件夹内的模块入手,每一个模块都代表一个步骤,每个步骤都包含了若干辅助性功能.

我们应该了解到:

  • 步骤有哪些,都起了什么作用
  • 数据在步骤中是如何流转的
  • 辅助函数都在其中起了什么作用

odm_app.py:整个ODM的入口

我们调用,传参并执行的就是odm_app.py.

流程依次是:

  1. 创建数据集
  2. *数据过大的话,拆分任务
    3. 本地拆分
    4. 远程拆分
  3. *如果拆分的话,进行合并
  4. 执行sfm
  5. 执行mve(快拼或使用opendfm_dense是不参与)
  6. 过滤点云
  7. 执行mesh
  8. 执行texturing
  9. 执行georeferencing
  10. 执行DEM(如果需要dsm和dtm的话)
  11. 生成orthpohoto

* 如果是集群运行的话


types.py:不得不提的工具类

涉及的辅助模块:

  • get_image_size.py:获取图片的宽高
  • location.py:与地理相关的库,在这里用到获取坐标系和从图片生成包含所有坐标的文件
  • gcp.py:从gcp文件中读取坐标信息并整理

ODM_Stage:所有步骤类的管道基类

ODM封装了一种管道,将若干步骤串联起来,数据或变量就完全在管道内自动流转.以上所有的步骤,本质都是不同的管道

管道模式适合于这种没有分支的长流程,封闭性更强,也可以更关注于每一步,而不必关心步骤之间的连接与数据的共享

管道有如下功能,这也是所有步骤都需要的功能:

  • 连接:连到下一个管道,构成一个环
  • 运行:是一个虚函数,需要被子类实现,运行具体的方法
  • 重运行:ODM支持从任意步骤开始重复运行
  • 进度:每个步骤都有自己的独立进度,要和主进度挂钩

ODM_Tree:保存文件树的类

这里可以解答我们的一个疑问,所有的过程数据是全部落地的,通过约定的文件路径进行数据的传递.

ODM对于每一个任务都建立独立的文件夹,ODM_Tree可以将相对路径绑定到独立的任务路径.

ODM_Reconstruction:构建坐标体系的类

ODM可以采用图片exif的坐标信息,也可以使用GCP文件指定.

无论是何种类型,都是为了生成一个坐标集合文本.

这个点集合在这里被用来生成一个proj格式的实体,方便转化坐标

1.dataset.py:创建数据集

dataset是整个ODM的初始步骤,准备好了一切的源头:图片

dataset本身没有复杂的处理流程,只是将文件夹下所有图片整理出来,留作备用

步骤:

  1. 如果没有GCP文件,则从图片提取坐标信息,输出coords.txt
  2. 否则将GCP文件转换为coords.txt

2.run_opensfm:执行sfm

涉及的辅助模块:

  • gsd.py:地面采样间距模块,在这里使用了获取图片的宽高
  • osfm.py:opensfm操作的相关库,在这里使用了其中的OSFMContext这个类,封装了对osfm上下文的操作

顾名思义,整个步骤都是在执行sfm,其中又有如下小步骤:

  1. 从图片文件夹和GCP文件初始化opensfm
  2. 提取元数据
  3. 提取物体,匹配物体
  4. 生成匹配列表,进行三维重构
  5. 提取相机参数,输出reconstruction.json,用于计算地面分辨率
  6. 涉及到参数的操作
    1. 如果是快拼模式:将三维重建导出为点云格式(PLY),输出reconstruction.ply
    2. 如果是osfm_dense模式:计算深度图,输出depthmaps/merged.ply
    3. 从visualfm将三维重建导出为nvm_v3格式输出reconstruction.nvm
  7. 将OpenSfM三维重构转换为Bundle输出格式,输出bundle_r000.outlist_r000.out
  8. 从三维重建导出地理坐标,输出geocoords_transformation.txt

3.mve.py:执行mve,进行稠密重建

mve:Multi-View Environment

mvs:multi view system

涉及的辅助模块:

  • context.py:类似于ODM_Tree,保存的也是一些文件路径,不过是不依任务改变的命令行工具的路径

步骤:

  1. 使用步骤2生成的bundle_r000.out构建mve场景,直接和搭建管道
  2. 搭建dmrecon管道,用以重建深度图
  3. 执行scene2pset,输出密集点云mve_dense_point_cloud.ply
  4. 执行cleanmesh,对输出的点云文件进行清洁,消除许多不必要的面以及不可靠的几何形状和不连贯的零部件

4.odm_filterpoints.py过滤点云

涉及的辅助模块:

  • point_cloud.py:点云处理类,这里用到了点云过滤,就是调用了编译的C++可执行文件.

过滤点云在上几个步骤中,会出现过多的点,需要保留置信度在给定范围的点,保证模型的精确性

点云的来源有3种:

  • 快拼模式下,使用步骤2生成的reconstruction.ply
  • opensfm_dense模式下,使用步骤2生成的depthmaps/merged.ply
  • 默认模式下,使用步骤3中生成的mve_dense_point_cloud.ply

最终产生point_cloud.ply


5.odm_meshing:执行mesh重构

涉及的辅助模块:

  • mesh.py:执行mesh操作的工具
  • gsd.py:在这里用作计算dsm分辨率

步骤:

  1. 使用步骤4产生的point_cloud.ply进行筛选泊松重建输出odm_mesh.ply
    1. 泊松重建
    2. 清理和减少顶点数
  2. 产生2.5d的mesh,输出odm_25dmesh.ply

执行meshing的目的是输出三维网格,纹理映射只能处理三维网格


6.mvstex.py:为模型附上纹理

步骤:

  1. 使用步骤2生成的reconstruction.nvm,步骤5生成的odm_mesh.plyodm_25dmesh.ply直接调用了texrecon工具,利用点云生成纹理
  2. 输出odm_textured_model.obj

7.odm_georeferencing.py:计算地理参考

涉及的辅助模块:

  • croppe.py:计算裁剪区域
  • entwine.py:如果设置为输出EPT格式的点云,则需要使用entwine

步骤:

  1. 利用步骤1生成的坐标点coords.txt ,步骤2产生的geocoords_transformation.txt,bundle_r000.out``list_r000.out,geocoords_transformation.txt,步骤6生成的odm_textured_model.obj调用编译的C++可执行文件进行平差,输出odm_textured_model_geo.obj,odm_georeferencing_model_geo.txt,odm_georeferenced_model.laz,odm_georeferencing_transform.txt
  2. 利用odm_georeferenced_model.laz可以按需生成一系列点云输出文件
    1. 可以输出odm_georeferenced_model.csv
    2. 可以输出odm_georeferenced_model.las
    3. 利用entwine,可以输出entwine_pointcloud
  3. 生成裁切遮罩,保存为odm_georeferenced_model.bounds.gpkg

8. odm_dem.py:按需生成dem文件

涉及的辅助模块:

  • dem模块:封装了全部生成dem所需的方法
  • cropper:切割dem.tif

本步骤是可选的

步骤:

  1. 利用步骤7生成的odm_georeferenced_model.laz,进行pdal形态学分类,修改odm_georeferenced_model.laz
  2. 再利用odm_georeferenced_model.laz,生成dem的tif文件
  3. 利用步骤7生成的odm_georeferenced_model.bounds.gpkg切割dem,修改tif

9.odm_orthophoto.py:生成正摄影像

涉及的模块:

  • orthophoto.py:封装了一个给成果图像增加预览图的gdal命令
  • cutline.py:利用grass计算tif切线

步骤:

  1. 使用步骤7中生成的odm_textured_model_geo.obj或步骤6odm_textured_model.obj(根据模式不同)编译的c++可执行工具生成tif文件,生成成果tif和odm_orthophoto_corners.txt文件
  2. 利用odm_orthophoto_corners.txt文件和gdal_translate给成果tif设定坐标范围,修改tif
  3. 利用步骤7中的odm_georeferenced_model.bounds.gpkg对成果tif计算切割线,修改tif
  4. 利用步骤7中的odm_georeferenced_model.bounds.gpkg对成果tif进行切割,生成最终成果

0.splitmerge.py:大任务的拆分与合并

这是所有流程里最特殊的两个了.也只有在最后才能讲.

这个模块有两个类,分别对应步骤2和步骤3,如果数据并非大任务,则该两个模块不进行任何处理.

分布式是个好东西,但实现却是个难题,需要处理这几个问题:

  • 一个大任务分成几个小任务,当全部小任务完成后,如何收束
    • 收束是否比不分布还耗时
    • 是否会大幅增加逻辑复杂度
    • 当一个小任务失败后,大任务该如何处理
    • 大任务如何拆分成合理的小任务
  • 每个小任务是否和其他任务完全无关
    • 哪些阶段能分布式,哪些不能
      • 能分布式的阶段是否是计算密集阶段

结语

类似的项目并不多,也没有达到接近ODM的完善程度,因此,若是深入了解其中的原理,可能会是进入该领域的一条捷径吧.

你可能感兴趣的:(源码阅读)