Color Map Optimization for 3D Reconstruction with Consumer Depth Cameras
方案概述
输入
RGB-D
网格重建
KinectFusion
关键帧选取
由于相机是手持的,会导致运动模糊。
为了最大化纹理质量,需要从输入图像序列中选取一个子集。
模糊评估:使用Crete【2007】提出的方法量化模糊程度。
然后,在选定的子集中进一步选取。
选定一帧后,在该帧后的
(
t
−
,
t
+
)
(t_-,t_+)
(t−,t+)时间间隔内选取模糊程度最低的一帧(实际中使用
(
t
−
,
t
+
)
=
(
1
,
5
)
s
(t_-,t_+)=(1,5)s
(t−,t+)=(1,5)s)。关键帧表示为
{
I
i
}
\{I_i\}
{Ii}
对于每个关键帧
I
i
I_i
Ii,将mesh网格渲染到
I
i
I_i
Ii的图像平面上,使用距离
I
i
I_i
Ii最近的深度图算出的相机pose。通过比较每个顶点的深度值和相应的深度进行可见性测试,由此可以确定关键帧
I
i
I_i
Ii对应的顶点子集,这个顶点子集进一步通过投影点到图像边缘的距离(实际中阈值为9个像素)和深度不连续性进行滤除。保留的顶点表示为
{
P
i
}
\{\mathbf P_i\}
{Pi}
相机姿态优化
相机姿态优化本质上是一个非线性最小二乘问题,下面对其目标函数和优化方法进行说明。
目标函数
输入:关键帧选取中确定的纹理图片集合
{
I
i
}
\{I_i\}
{Ii}以及相关联的顶点子集
{
P
i
}
\{\mathbf P_i\}
{Pi}
目标:优化相机姿态矩阵
T
i
\mathbf T_i
Ti,通过
T
i
\mathbf T_i
Ti可以将顶点映射到对应的纹理图片上。
∀
p
∈
P
\forall \mathbf p\in \mathbf P
∀p∈P,最大化
p
\mathbf p
p在所有相关联图像
I
p
=
{
I
i
:
p
∈
P
}
I_p=\{I_i:\mathbf p\in \mathbf P\}
Ip={Ii:p∈P}中的颜色一致性
采用齐次坐标(homogeneous coordinates)进行计算,因此 P i ⊂ P 3 \mathbf P_i\subset\Bbb P^3 Pi⊂P3, T i \mathbf T_i Ti为 4 × 4 4\times4 4×4的矩阵。
齐次坐标(homogeneous coordinates):在投影空间中,两条平行线会相交于无限远处的一点,在笛卡尔坐标 ( X , Y ) (X,Y) (X,Y)的基础上增加一个新的分量 w w w,变成 ( x , y , w ) (x,y,w) (x,y,w), X = x / w , Y = y / w X=x/w,Y=y/w X=x/w,Y=y/w
则目标函数为:
E
(
C
,
T
)
=
∑
i
∑
p
∈
P
(
C
(
p
)
−
Γ
i
(
p
,
T
i
)
)
2
E(\mathbf C,\mathbf T)=\sum_i\sum_{\mathbf p\in \mathbf P}(\mathbf C(\mathbf p)-\Gamma_i(\mathbf p,\mathbf T_i))^2
E(C,T)=i∑p∈P∑(C(p)−Γi(p,Ti))2
其中,
Γ
i
(
p
,
T
i
)
\Gamma_i(\mathbf p,\mathbf T_i)
Γi(p,Ti)是点
p
\mathbf p
p在图像
I
i
I_i
Ii对应投影点的颜色值,
C
(
p
)
\mathbf C(\mathbf p)
C(p)是集合
{
Γ
i
(
p
,
T
i
)
}
I
i
∈
I
p
\{\Gamma_i(\mathbf p,\mathbf T_i)\}_{I_i\in I_\mathbf p}
{Γi(p,Ti)}Ii∈Ip(所有和
p
\mathbf p
p相关联的纹理图像)的平均值。
为了进一步优化,下面阐述一下
Γ
i
(
p
,
T
i
)
\Gamma_i(\mathbf p,\mathbf T_i)
Γi(p,Ti)的计算过程:该过程由三步组成,一次刚体变换
g
\mathbf g
g,一次投影变换
u
\mathbf u
u,以及一次颜色变换
Γ
i
\Gamma_i
Γi,即
Γ
i
(
u
(
g
(
p
,
T
i
)
)
)
\Gamma_i(\mathbf u(\mathbf g(\mathbf p,\mathbf T_i)))
Γi(u(g(p,Ti))):
g
(
p
,
T
i
)
=
T
i
p
u
(
g
x
,
g
y
,
g
z
,
g
w
)
=
(
g
x
f
x
g
z
+
c
x
,
g
y
f
y
g
z
+
c
y
)
T
\mathbf g(\mathbf p,\mathbf T_i)=\mathbf T_i\mathbf p \\ \mathbf u(g_x,g_y,g_z,g_w)=(\frac{g_xf_x}{g_z}+c_x,\frac{g_yf_y}{g_z}+c_y)^\mathbf T
g(p,Ti)=Tipu(gx,gy,gz,gw)=(gzgxfx+cx,gzgyfy+cy)T
其中,
f
x
,
f
y
f_x,f_y
fx,fy表示焦距,
(
c
x
,
c
y
)
T
(c_x,c_y)^\mathbf T
(cx,cy)T表示焦点。则
Γ
i
(
u
x
,
u
y
)
\Gamma_i(u_x,u_y)
Γi(ux,uy)就图像
I
i
I_i
Ii是相应坐标点处的灰度值(双线性插值)。
最后,目标函数可以写作:
E
(
C
,
T
)
=
∑
i
∑
p
∈
P
r
i
,
p
2
E(\mathbf C,\mathbf T)=\sum_i\sum_{\mathbf p\in \mathbf P}r_{i,p}^2
E(C,T)=i∑p∈P∑ri,p2
残差项
r
i
,
p
r_{i,p}
ri,p为:
r
i
,
p
=
C
(
p
)
−
Γ
i
(
u
(
g
(
p
,
T
i
)
)
r_{i,p}=\mathbf C(\mathbf p)-\Gamma_i(\mathbf u(\mathbf g(\mathbf p,\mathbf T_i))
ri,p=C(p)−Γi(u(g(p,Ti))
解释:我们有一个重建好的网格和一组关键帧数据,以及每一张关键帧相应的相机姿态。由于重建过程中估计的相机姿态可能存在一定的误差,直接进行纹理映射会产生重影模糊等问题,因此要根据这些数据对相机姿态进行优化。优化的大体思路是:首先根据网格顶点3D坐标和关键帧的相机姿态,将3D顶点其投影到2D平面,得到若干张投影图,根据每个顶点投影后的uv坐标获取到关键帧相应位置的像素值,每个顶点在每个关键帧(这里就用到了之前的可见性,只需要“可见”的关键帧)中都可以得到一个对应的像素值 Γ i ( p , T i ) \Gamma_i(\mathbf p,\mathbf T_i) Γi(p,Ti),将这些像素值求平均就是 C ( p ) \mathbf C(\mathbf p) C(p),优化的目标就是让 C ( p ) \mathbf C(\mathbf p) C(p)和每一个 Γ i ( p , T i ) \Gamma_i(\mathbf p,\mathbf T_i) Γi(p,Ti)尽可能的接近。
高斯牛顿法
非线性最小二乘问题可以通过高斯牛顿法进行优化求解。我们将上面的目标函数中需要优化的参数(即
C
,
T
\mathbf C, \mathbf T
C,T包含的所有参数)表示为
x
0
=
[
C
0
,
T
0
]
\mathbf x^0=[\mathbf C^0,\mathbf T^0]
x0=[C0,T0]。
对于每个
i
i
i,
T
i
0
\mathbf T_i^0
Ti0是由距离
I
i
I_i
Ii最近的深度图提供的相机姿态,
对于每个
p
\mathbf p
p,
C
0
(
p
)
C^0(\mathbf p)
C0(p)是
{
Γ
i
(
p
,
T
i
0
)
}
I
i
∈
I
p
\lbrace\Gamma_i(\mathbf p,\mathbf T_i^0)\rbrace_{I_i \in I_\mathbf p}
{Γi(p,Ti0)}Ii∈Ip的平均值。
在每次迭代中,对参数
x
\mathbf x
x进行更新:
x
k
+
1
=
x
k
+
Δ
x
\mathbf x^{k+1}=\mathbf x^k+\Delta \mathbf x
xk+1=xk+Δx
其中,
Δ
x
\Delta \mathbf x
Δx是如下线性方程的解:
J
r
T
J
r
Δ
x
=
−
J
r
T
r
\mathbf J_\mathbf r^\mathbf T\mathbf J_\mathbf r\Delta \mathbf x=-\mathbf J_\mathbf r^\mathbf T\mathbf r
JrTJrΔx=−JrTr
其中,
r
=
r
(
x
)
\mathbf r=\mathbf r(\mathbf x)
r=r(x)是目标函数中的残差项,
J
r
=
J
r
(
x
)
\mathbf J_\mathbf r=\mathbf J_\mathbf r(\mathbf x)
Jr=Jr(x)是
r
\mathbf r
r的雅可比矩阵(Jacobian):
r
=
[
r
i
,
p
(
x
)
∣
x
=
x
k
]
(
i
,
p
)
,
J
r
=
[
∇
r
i
,
p
(
x
)
∣
x
=
x
k
]
(
i
,
p
)
\mathbf r=[r_{i,\mathbf p}(\mathbf x)\mid _{\mathbf x=\mathbf x^k}]_{(i,\mathbf p)},\\ \mathbf J_\mathbf r=[\nabla r_{i,\mathbf p}(\mathbf x)\mid _{\mathbf x=\mathbf x^k}]_{(i,\mathbf p)}
r=[ri,p(x)∣x=xk](i,p),Jr=[∇ri,p(x)∣x=xk](i,p)
在每次迭代中,根据目标函数的定义计算残差项r,
r
i
,
p
r_{i,\mathbf p}
ri,p关于
C
\mathbf C
C和
T
j
∣
j
≠
i
\mathbf T_j\mid _{j\neq i}
Tj∣j=i的偏导都是平凡解。为了计算
r
i
,
p
r_{i,\mathbf p}
ri,p关于
T
i
\mathbf T_i
Ti的偏导,我们将
T
i
\mathbf T_i
Ti局部线性化近似为
T
i
k
\mathbf T_i^k
Tik:
T
i
≈
(
1
−
γ
i
β
i
α
i
γ
i
1
−
α
i
b
i
−
β
i
α
i
1
c
i
0
0
0
1
)
T
i
k
\mathbf T_i \approx \begin{pmatrix} 1 & -\gamma_i & \beta_i & \alpha_i \\ \gamma_i & 1 & -\alpha_i & b_i \\ -\beta_i & \alpha_i & 1 & c_i \\ 0 & 0 & 0 & 1 \end{pmatrix}\mathbf T_i^k
Ti≈⎝⎜⎜⎛1γi−βi0−γi1αi0βi−αi10αibici1⎠⎟⎟⎞Tik
令
ξ
i
=
(
α
i
,
β
i
,
γ
i
,
a
i
,
b
i
,
c
i
)
T
\xi_i=(\alpha_i,\beta_i,\gamma_i,a_i,b_i,c_i)^\mathbf T
ξi=(αi,βi,γi,ai,bi,ci)T,
Δ
x
\Delta \mathbf x
Δx则为
C
(
p
)
{C(\mathbf p)}
C(p)和
ξ
i
\xi_i
ξi的组合.由链式法则,可得:
∇
r
i
,
p
(
ξ
i
)
∣
x
=
x
k
=
−
∂
∂
ξ
i
(
Γ
i
∘
u
∘
g
)
∣
x
=
x
k
=
−
∇
Γ
i
(
u
)
J
u
(
g
)
J
g
(
ξ
i
)
∣
x
=
x
k
\nabla r_{i,\mathbf p}(\xi_i)\mid _{\mathbf x=\mathbf x^k}=-\frac{\partial}{\partial \xi_i}(\Gamma_i \circ \mathbf u \circ \mathbf g)\mid _{\mathbf x=\mathbf x^k} =-\nabla \Gamma_i(\mathbf u)\mathbf J_\mathbf u(\mathbf g)\mathbf J_\mathbf g(\xi_i)\mid _{\mathbf x=\mathbf x^k}
∇ri,p(ξi)∣x=xk=−∂ξi∂(Γi∘u∘g)∣x=xk=−∇Γi(u)Ju(g)Jg(ξi)∣x=xk
其中,
∇
Γ
i
\nabla \Gamma_i
∇Γi是
Γ
i
\Gamma_i
Γi的梯度,使用Scharr核计算。
J
u
(
g
)
\mathbf J_\mathbf u(\mathbf g)
Ju(g)是u的Jacobian矩阵,
J
g
(
ξ
i
)
\mathbf J_\mathbf g(\xi_i)
Jg(ξi)是g关于
ξ
i
\xi_i
ξi的Jacobian矩阵.
交替优化法
高斯牛顿法计算复杂度很高,因此作者提出了一种交替优化法,每次迭代交替优化C和T。
当T固定时,非线性最小二乘问题就变成了线性最小而成问题,并且有以下闭解:
C
(
p
)
=
1
n
P
∑
Γ
i
(
p
,
T
i
)
C(\mathbf p)=\frac{1}{n_\mathbf P}\sum \Gamma_i(\mathbf p,\mathbf T_i)
C(p)=nP1∑Γi(p,Ti)
其中
n
P
n_\mathbf P
nP是与顶点p相关的图片的个数,这样就相当于只需计算p在各个与其相关的图片上的投影点的平均灰度值。
当C固定时,目标函数分解为多个独立的目标函数,每个
T
i
\mathbf T_i
Ti对应一个:
E
i
(
T
)
=
∑
p
∈
P
i
r
i
,
p
2
E_i(\mathbf T)=\sum_{\mathbf p \in \mathbf P_i}r_{i, \mathbf p}^2
Ei(T)=p∈Pi∑ri,p2
每一项
E
i
(
T
)
E_i(\mathbf T)
Ei(T)只和
ξ
i
\xi_i
ξi的六个变量有关,在每次迭代中,使用高斯牛顿法迭代更新这六个变量,每个
T
i
\mathbf T_i
Ti都是相互独立的,因此可以并行计算。这样的计算是非常高效的。
非刚体校正
非刚体校正是为了修复不准确的Mesh和镜头畸变等导致的误差。
目标函数
对每一张图片
I
i
I_i
Ii,非刚体校正可以用一个变形函数
F
i
F_i
Fi表示。
F
i
(
v
i
,
l
)
=
v
i
,
l
+
f
i
,
l
\mathbf F_i(\mathbf v_{i,l})=\mathbf v_{i,l}+\mathbf f_{i,l}
Fi(vi,l)=vi,l+fi,l
其中,
v
i
,
l
∈
V
i
\mathbf v_{i,l} \in\mathbf V_i
vi,l∈Vi,表示变形的控制范围。
f
i
,
l
\mathbf f_{i,l}
fi,l是一个二维向量,该图像平面上的所有投影点
u
\mathbf u
u校正为:
F
i
(
u
)
=
u
+
∑
i
θ
l
(
u
)
f
i
,
l
\mathbf F_i(\mathbf u)=\mathbf u+\sum_i \theta_l(\mathbf u)\mathbf f_{i,l}
Fi(u)=u+i∑θl(u)fi,l
θ
l
\theta_l
θl为双线性插值函数,则
F
i
(
u
)
\mathbf F_i(\mathbf u)
Fi(u)是
{
F
i
(
v
i
,
l
)
}
v
i
,
l
∈
V
i
\{\mathbf F_i(\mathbf v_{i,l})\}_{\mathbf v_{i,l}\in\mathbf V_i}
{Fi(vi,l)}vi,l∈Vi的线性组合。
V
i
\mathbf V_i
Vi是一个
20
×
16
20\times16
20×16的网格(
21
×
17
21\times17
21×17个控制点)。
之前的目标函数可纳入非刚体变换:
E
c
(
C
,
T
,
F
)
=
∑
i
∑
p
∈
P
i
r
i
,
p
2
E_c(\mathbf C,\mathbf T,\mathbf F)=\sum_i\sum_{\mathbf p\in \mathbf P_i}r_{i,\mathbf p}^2
Ec(C,T,F)=i∑p∈Pi∑ri,p2
残差项为
r
i
,
p
=
C
(
p
)
−
Γ
i
(
F
i
(
u
(
g
(
p
,
T
i
)
)
)
)
r_{i,\mathbf p}=C(\mathbf p)-\Gamma_i(\mathbf F_i(\mathbf u(\mathbf g(\mathbf p,\mathbf T_i))))
ri,p=C(p)−Γi(Fi(u(g(p,Ti))))
为了防止校正函数漂移,加入
L
2
L^2
L2正则项:
E
r
(
F
)
=
∑
i
∑
l
f
i
,
l
T
f
i
,
l
E_r(\mathbf F)=\sum_i\sum_l\mathbf f_{i,l}^T\mathbf f_{i,l}
Er(F)=i∑l∑fi,lTfi,l
则完整的目标函数为:
E
(
C
,
T
,
F
)
=
E
c
(
C
,
T
,
F
)
+
λ
E
r
(
F
)
E(\mathbf C,\mathbf T,\mathbf F)=E_c(\mathbf C,\mathbf T,\mathbf F)+\lambda Er(\mathbf F)
E(C,T,F)=Ec(C,T,F)+λEr(F)
实际中作者使用
λ
=
0.1
\lambda=0.1
λ=0.1
优化
目标函数引入了
m
+
720
n
m+720n
m+720n个变量,直接使用高斯牛顿法计算量非常大,而交替优化法可以将优化问题转换为独立线性问题,只需720个变量。
在第k论迭代中,首先固定T和F,优化C。
然后固定C,优化T和F。
python实现
在open3d开源库中,集成了这篇论文的算法。作者所用的数据库可以点击这里下载。
在open3d中,给出了demo代码如下:
# examples/Python/Advanced/o3d.color_map.color_map_optimization.py
import open3d as o3d
from trajectory_io import *
import os, sys
sys.path.append("../Utility")
from file import *
path = "[path_to_fountain_dataset]"
debug_mode = False
if __name__ == "__main__":
o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Debug)
# Read RGBD images
rgbd_images = []
depth_image_path = get_file_list(os.path.join(path, "depth/"),
extension=".png")
color_image_path = get_file_list(os.path.join(path, "image/"),
extension=".jpg")
assert (len(depth_image_path) == len(color_image_path))
for i in range(len(depth_image_path)):
depth = o3d.io.read_image(os.path.join(depth_image_path[i]))
color = o3d.io.read_image(os.path.join(color_image_path[i]))
rgbd_image = o3d.geometry.RGBDImage.create_from_color_and_depth(
color, depth, convert_rgb_to_intensity=False)
if debug_mode:
pcd = o3d.geometry.PointCloud.create_from_rgbd_image(
rgbd_image,
o3d.camera.PinholeCameraIntrinsic(
o3d.camera.PinholeCameraIntrinsicParameters.
PrimeSenseDefault))
o3d.visualization.draw_geometries([pcd])
rgbd_images.append(rgbd_image)
# Read camera pose and mesh
camera = o3d.io.read_pinhole_camera_trajectory(
os.path.join(path, "scene/key.log"))
mesh = o3d.io.read_triangle_mesh(
os.path.join(path, "scene", "integrated.ply"))
# Before full optimization, let's just visualize texture map
# with given geometry, RGBD images, and camera poses.
option = o3d.color_map.ColorMapOptimizationOption()
option.maximum_iteration = 0
o3d.color_map.color_map_optimization(mesh, rgbd_images, camera, option)
o3d.visualization.draw_geometries([mesh])
o3d.io.write_triangle_mesh(
os.path.join(path, "scene", "color_map_before_optimization.ply"), mesh)
# Optimize texture and save the mesh as texture_mapped.ply
# This is implementation of following paper
# Q.-Y. Zhou and V. Koltun,
# Color Map Optimization for 3D Reconstruction with Consumer Depth Cameras,
# SIGGRAPH 2014
option.maximum_iteration = 300
option.non_rigid_camera_coordinate = True
o3d.color_map.color_map_optimization(mesh, rgbd_images, camera, option)
o3d.visualization.draw_geometries([mesh])
o3d.io.write_triangle_mesh(
os.path.join(path, "scene", "color_map_after_optimization.ply"), mesh)
使用作者的数据跑出来效果还是很不错的,但是在实测中使用自己扫描的数据,效果就差了一些。