1. 先看效果

参考源码: https://github.com/andyzeng/tsdf-fusion-python

在这里插入图片描述
从图中可以看出,tsdf算法的重建效果还是不错的.该算法是很多牛掰3D重建算法的基础,例如:KinectFusion、InfiniTAM等.

2. 原理解析

2.1 体素栅格建立

根据环境待重建点云分布情况,确定包围所有点云的边界大小,然后再将边界内的空间根据设定的尺寸大小进行体素化,这样就将空间划分成一个个下图所示的小立方体(立方体的尺寸由用户自行定义,越小建模精度越高,越耗时),图像来源https://blog.csdn.net/qq_30339595/article/details/86103576
在这里插入图片描述

2.2 体素栅格更新

定义2.1节建立的体素栅格所在的坐标系为世界坐标系,体素坐标为

(

p

0

w

,

p

1

w

,

.

.

.

,

p

m

w

)

(\mathbf{p}_{0}^{w}, \mathbf{p}_{1}^{w}, …,\mathbf{p}_{m}^{w})

(p0w,p1w,...,pmw),相机的位姿为

(

T

c

0

w

,

T

c

1

w

.

.

.

,

T

c

n

w

)

(\mathbf{T}_{c_{0}}^{w}, \mathbf{T}_{c_{1}}^{w} …, \mathbf{T}_{c_{n}}^{w})

(Tc0w,Tc1w...,Tcnw),相机的内参为

K

\mathbf{K}

K

2.2.1 投影世界坐标系下的体素栅格到图像坐标系

I

i

j

=

[

u

i

j

v

i

j

]

=

K

T

c

i

w

p

j

w

(1)

\mathbf{I}_{ij} = \begin{bmatrix}\mathbf{u}_{ij} \\ \mathbf{v}_{ij}\end{bmatrix} = \mathbf{K}\mathbf{T}_{c_{i}}^{w}\mathbf{p}_{j}^{w} \tag{1}

Iij=[uijvij]=KTciwpjw(1)
式中

i

(

0

,

1

,

.

.

.

.

,

n

)

i\in(0, 1, …., n)

i(0,1,....,n)

j

(

0

,

1

,

.

.

.

,

m

)

j\in(0, 1, …, m)

j(0,1,...,m)

2.2.2 更新栅格tsdf值及对应的权重

首先计算每个体素sdf值:

s

d

f

j

=

t

p

j

w

t

c

i

w

d

e

p

(

I

i

j

)

(2)

sdf_{j} = \left \| \mathbf{t}_{pj}^{w} – \mathbf{t}_{ci}^{w} \right \| – \mathbf{dep}(\mathbf{I}_{ij}) \tag{2}

sdfj=tpjwtciwdep(Iij)(2)
式中

t

p

j

w

\mathbf{t}_{pj}^{w}

tpjw表示第

j

j

j个体素在世界坐标系下的位置信息,

t

c

i

w

\mathbf{t}_{ci}^{w}

tciw表示第

i

i

i个相机位姿在世界坐标系下的位置信息,

d

e

p

(

I

i

j

)

\mathbf{dep}(\mathbf{I}_{ij})

dep(Iij)表示第

j

j

j个体素在第

i

i

i个相机深度图像坐标系下的深度值.

截断每个体素的sdf值:

i

f

 

s

d

f

j

>

0

 

t

s

d

f

j

=

m

i

n

(

1

,

s

d

f

j

/

t

r

u

n

c

)

e

l

s

e

 

t

s

d

f

j

=

m

a

x

(

1

,

s

d

f

j

/

t

r

u

n

c

)

(3)

\begin{aligned} &if sdf_{j} > 0  tsdf_{j} = min(1, sdf_{j}/trunc) \\ &else tsdf_{j} = max(-1, sdf_{j}/trunc) \end{aligned} \tag{3}

if sdfj>0 tsdfj=min(1,sdfj/trunc)else tsdfj=max(1,sdfj/trunc)(3)
式中

t

r

u

n

c

trunc

trunc 表示截断距离,人为设定,可理解为相机深度信息的误差值,如果误差越大,建议该值设置大一些,否则会丢掉很多深度相机获取的信息.

计算每个体素的tsdf值:

t

s

d

f

j

=

t

s

d

f

j

1

w

j

1

+

t

s

d

f

j

w

j

w

j

1

+

w

j

(4)

tsdf_{j} = \frac{tsdf_{j-1}\cdot w_{j-1} + tsdf_{j}\cdot w_{j}}{w_{j-1}+w_{j}} \tag{4}

tsdfj=wj1+wjtsdfj1wj1+tsdfjwj(4)
式中初始

w

j

w_{j}

wj一般默认设置为1.

计算每个体素的权值:

w

j

=

w

j

1

+

w

j

(5)

w_{j} = w_{j-1} + w_{j} \tag{5}

wj=wj1+wj(5)

2.3 找等值面

通过2.2节更新完每个体素的tsdf值之后,通过marching cubes算法寻找体素中的等值面作为重构曲面,算法示意图如下:
在这里插入图片描述

其中每个栅格里面的值为对应体素的tsdf值.

3. 源码解析

参考源码: https://github.com/andyzeng/tsdf-fusion-python
注: 这里仅分析cpu版本源码,对于深入理解tsdf原理足够.

3.1 体素栅格建立

 def __init__(self, vol_bnds, voxel_size, use_gpu=True):
 
   # 将点云分布边界转换成numpy数组
   vol_bnds = np.asarray(vol_bnds) 
   assert vol_bnds.shape == (3, 2), "[!] `vol_bnds` should be of shape (3, 2)."

   # 定义体素体参数
   self._vol_bnds = vol_bnds # 体素体边界
   self._voxel_size = float(voxel_size) # 体素体每个立方体边长
   self._trunc_margin = 5 * self._voxel_size  # truncation on SDF # 截断距离,设置为体素边长的5倍
   self._color_const = 256 * 256 # 辅助参数,用于提取rgb值

   # 调整体素体边界确保c连续
   self._vol_dim = np.ceil((self._vol_bnds[:,1]-self._vol_bnds[:,0])/self._voxel_size).copy(order='C').astype(int) # 计算体素体每个维度方向上的栅格数量
   self._vol_bnds[:,1] = self._vol_bnds[:,0]+self._vol_dim*self._voxel_size # 根据各个维度栅格数量,计算体素体边界
   self._vol_origin = self._vol_bnds[:,0].copy(order='C').astype(np.float32) # 使体素体原点为c有序

   # 初始化保存体素体信息的容器
   self._tsdf_vol_cpu = np.ones(self._vol_dim).astype(np.float32) # 用于保存每个体素栅格的tsdf值
   self._weight_vol_cpu = np.zeros(self._vol_dim).astype(np.float32) # 用于保存每个体素栅格的权重值
   self._color_vol_cpu = np.zeros(self._vol_dim).astype(np.float32) # 用于保存每个体素栅格的颜色值(将rgb三个值压缩成一个float32值表示)
     
   # 获取每个体素栅格的坐标
   xv, yv, zv = np.meshgrid(
     range(self._vol_dim[0]),
     range(self._vol_dim[1]),
     range(self._vol_dim[2]),
     indexing='ij'
   )
   self.vox_coords = np.concatenate([
     xv.reshape(1,-1),
     yv.reshape(1,-1),
     zv.reshape(1,-1)
   ], axis=0).astype(int).T

3.2 体素栅格更新

def integrate(self, color_im, depth_im, cam_intr, cam_pose, obs_weight=1.):

    im_h, im_w = depth_im.shape # 获取图像尺寸

    # 将rgb三个值表示的颜色通道信息转换成一个用float32表示的单通道信息
    color_im = color_im.astype(np.float32)
    color_im = np.floor(color_im[...,2]*self._color_const + color_im[...,1]*256 + color_im[...,0])

      # 将体素栅格坐标转换成像素坐标,对应2.2节的公式(1)
      cam_pts = self.vox2world(self._vol_origin, self.vox_coords, self._voxel_size) # 体素坐标系转换到世界坐标系
      cam_pts = rigid_transform(cam_pts, np.linalg.inv(cam_pose)) # 世界坐标系转换到相机坐标系
      pix_z = cam_pts[:, 2]
      pix = self.cam2pix(cam_pts, cam_intr) # 相机坐标系转换到像素坐标系
      pix_x, pix_y = pix[:, 0], pix[:, 1]

      # 移除像素边界之外的投影点
      valid_pix = np.logical_and(pix_x >= 0,
                  np.logical_and(pix_x < im_w,
                  np.logical_and(pix_y >= 0,
                  np.logical_and(pix_y < im_h,
                  pix_z > 0))))
      depth_val = np.zeros(pix_x.shape)
      depth_val[valid_pix] = depth_im[pix_y[valid_pix], pix_x[valid_pix]]

      # 更新每个体素栅格的tsdf值及对应的权重
      depth_diff = depth_val - pix_z # 计算sdf值,对应公式(2)
      valid_pts = np.logical_and(depth_val > 0, depth_diff >= -self._trunc_margin) # 确定出有效深度值(即sdf值的值要大于负的截断值)
      dist = np.minimum(1, depth_diff / self._trunc_margin) # 计算截断值,对应公式(3)
      valid_vox_x = self.vox_coords[valid_pts, 0]
      valid_vox_y = self.vox_coords[valid_pts, 1]
      valid_vox_z = self.vox_coords[valid_pts, 2]
      w_old = self._weight_vol_cpu[valid_vox_x, valid_vox_y, valid_vox_z] # 提取上个循环对应体素的权重,对应公式(4)中的w_j_1
      tsdf_vals = self._tsdf_vol_cpu[valid_vox_x, valid_vox_y, valid_vox_z] # 提取上个循环对应的tsdf值,对应公式(4)中的tsdf_j_1
      valid_dist = dist[valid_pts]
      tsdf_vol_new, w_new = self.integrate_tsdf(tsdf_vals, valid_dist, w_old, obs_weight) # 计算体素新的权重和tsdf值,对应公式(4)(5)
      self._weight_vol_cpu[valid_vox_x, valid_vox_y, valid_vox_z] = w_new # 将新的权值和tsdf值更新到体素信息容器中
      self._tsdf_vol_cpu[valid_vox_x, valid_vox_y, valid_vox_z] = tsdf_vol_new

      # 更新每个体素栅格的颜色值,其实就是按旧的权重和新的权重加权更新每个体素栅格的rgb值
      old_color = self._color_vol_cpu[valid_vox_x, valid_vox_y, valid_vox_z]
      old_b = np.floor(old_color / self._color_const)
      old_g = np.floor((old_color-old_b*self._color_const)/256)
      old_r = old_color - old_b*self._color_const - old_g*256
      new_color = color_im[pix_y[valid_pts],pix_x[valid_pts]]
      new_b = np.floor(new_color / self._color_const)
      new_g = np.floor((new_color - new_b*self._color_const) /256)
      new_r = new_color - new_b*self._color_const - new_g*256
      new_b = np.minimum(255., np.round((w_old*old_b + obs_weight*new_b) / w_new))
      new_g = np.minimum(255., np.round((w_old*old_g + obs_weight*new_g) / w_new))
      new_r = np.minimum(255., np.round((w_old*old_r + obs_weight*new_r) / w_new))
      self._color_vol_cpu[valid_vox_x, valid_vox_y, valid_vox_z] = new_b*self._color_const + new_g*256 + new_r

3.3 找等值面

  def get_mesh(self):

	# 获取体素栅格的tsdf值及对应的颜色值
    tsdf_vol, color_vol = self.get_volume()

    # 直接使用scikit-image工具包中封装的Marching cubes算法接口提取等值面
    verts, faces, norms, vals = measure.marching_cubes_lewiner(tsdf_vol, level=0)
    verts_ind = np.round(verts).astype(int)
    verts = verts*self._voxel_size+self._vol_origin  # voxel grid coordinates to world coordinates

    # 为每个体素赋值颜色
    rgb_vals = color_vol[verts_ind[:,0], verts_ind[:,1], verts_ind[:,2]]
    colors_b = np.floor(rgb_vals/self._color_const)
    colors_g = np.floor((rgb_vals-colors_b*self._color_const)/256)
    colors_r = rgb_vals-colors_b*self._color_const-colors_g*256
    colors = np.floor(np.asarray([colors_r,colors_g,colors_b])).T
    colors = colors.astype(np.uint8)
    return verts, faces, norms, colors

版权声明:本文为weixin_37835423原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_37835423/article/details/113500937