生成模型

namespace: integration

通过tracking,optimization,现在我们已经得到了有着精确位姿的图片序列,如何根据这些序列生成高精度的网格模型就是integration要解决的事情。

TSDF

TSDF是一种对于三维场景的隐式表征。世界被划分成一个个体素,而每个体素中存储着到最近表面的距离,这个距离是有符号的,而插值到0的位置就是真正表面上的点。如果这个距离很远,实际上就没什么用,所以可以直接进行截断,这也就是TSDF(Truncated Signed Distance Field)。

在OnePiece中,这样的体素被定义为

class TSDFVoxel
{
    public:
    //距离
    float sdf = 999;
    //权重
    float weight = 0;
    //颜色
    geometry::Point3 color = geometry::Point3(-1,-1,-1);
};

为了提高效率和节省内存,一个普遍的做法是使用空间哈希,只存储距离表面比较近的voxel,在OnePiece中,\(8\times 8 \times 8\)个体素被当成一个立方体(Cube),而这些Cube又通过空间哈希函数来存储。

class VoxelCube
{
    public:
    std::vector<TSDFVoxel> voxels;
    Eigen::Vector3i cube_id;
};

通过Cube的ID可以很容易找到Cube位置,进而找到内部的voxel。关于Cube的哈希映射表被封装到类CubeHandler中。

从Frame到TSDF

对于一个体素(Voxel),一个frame,如何得到它的sdf值?首先,我们通过Voxel的位置以及其所在的Cube,计算得到Voxel中心的世界坐标系的位置,接着,将这个位置投影到frame中,找到该位置的depth,这个depth就是表面的点到相机平面的距离。SDF被定义为:

\[ sdf = z(T^{-1} * p_v) - d(\omega(T^{-1} * p_v)) \]

\(z(\cdot)\)取出三维向量的\(z\)值,\(d(\cdot)\)读取帧在某一位置的depth,\(\omega\)为重投影函数。

有一个问题是,三维世界是无限大的,因此我们不可能对每一个voxel来这样做。这里要介绍一个视锥体的概念,也就是对于相机来说的一个成像有效区域,在视锥体之外的点不考虑。视锥体在OnePiece中定义为Frustum.h

对于某个frame,我们只对视锥体内的cube进行处理。即使这样,voxel的个数依然很多。所以我们可以根据8个顶点采样滤波,如果8个顶点的SDF value符号都一致,则认为这个Cube中没有表面,详细内容可以参考FlashFusion

一个Voxel对于多个frame的计算可能会得到不同的结果,根据weight使用加权相加,来得到最终的值。

将一张rgbd frame融合到一个TSDF中,是通过CubeHandler的成员函数实现的:

void CubeHandler::IntegrateImage(const cv::Mat &depth, 
    const cv::Mat &rgb, const geometry::TransformationMatrix & pose);
void CubeHandler::IntegrateImage(const geometry::RGBDFrame &rgbd, 
    const geometry::TransformationMatrix & pose);

从TSDF到Mesh

我们可以从TSDF中进一步提取出三角网格,比较有名的算法是Marching Cubes。Marching Cubes中,网格提取是通过一个个正方体的。一个正方体有8个顶点,分别有8个sdf值。而每个顶点的sdf值可能大于0,也可能小于0,因此所有可能情况就是\(2^8 = 256\)中情况,但是实际上,有8个大于0的与有8个小于0的情况一样,正如有2个大于0的与有6个大于0的情况一样,最后列举出最基本情况只有15种:

256种情况都被列举出来,形成一个映射表,存储三角形顶点所在的边(我们为每条边设定一个序号),因此给定一个这样的正方体,根据映射表就能提取出三角形的顶点,从而提取出三角形。在TSDF中,8个Voxel正好可以看成是正方体的8个顶点,TSDF到Mesh就是这样进行的。 Mesh的提取也被封装在CubeHandler的成员函数中:

void CubeHandler::ExtractTriangleMesh(geometry::TriangleMesh &mesh);

TSDF的变换

与点云,三角网格相比,对于TSDF进行旋转平移稍有不同。体素是将世界分割成一个个立方体,而在旋转平移之后,变换之后的voxel中心坐标成了实数,需要重新映射到世界坐标系下的体素中,映射到旋转后的中心的8个neighbor体素。

如果只映射到最近的体素,会导致不准确的结果,可能产生空洞以及缺失。

CubeHandler中,提供了上述两种Transform的方法,可以比较(体素分辨率越高越明显):

//映射到最近的voxel
std::shared_ptr<CubeHandler> 
    TransformNearest(const geometry::TransformationMatrix &trans) const;
//映射到8个voxel,加权得到新的value
std::shared_ptr<CubeHandler> 
    Transform(const geometry::TransformationMatrix &trans) const;

8-neighbor(分辨率0.00625)

neareast(分辨率0.00625)

需要说明的,OnePiece实现的最原始的Marching Cubes算法。得到的Mesh往往点很密集,可以使用ClusteringSimplify将相同的点或者距离过近的聚合为一个点,需要法向量可以通过ComputeNormals来计算。