生成模型
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被定义为:
\(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;
需要说明的,OnePiece实现的最原始的Marching Cubes算法。得到的Mesh往往点很密集,可以使用ClusteringSimplify
将相同的点或者距离过近的聚合为一个点,需要法向量可以通过ComputeNormals
来计算。