LSCM + Mean Value Coordinates 论文笔记

GAMES301-曲面参数化(GAMES 301: Surface Parameterization)作业3解析与matlab实现。

LSCM

LSCM(least squares conformal map)是经典的自由边界共形参数化算法。我们在课程中介绍过共形映射需要满足Cauchy-Riemann方程

{fxx=fyyfxy=fyxJ=[abba]

从Jacobian矩阵来看,Cauchy-Riemann方程说明映射f是一个相似变换(旋转+缩放)因此不会改变向量的夹角。对于大多数网格我们可能很难保证映射严格满足Cauchy-Riemann方程,此时可以构造一个共形能量来描述映射f违背共形映射的程度,而LSCM的原理就是通过最小化共形能量来建立所需的共形映射。

Conformality in a Triangulation

LSCM使用了复平面来推导共形能量。设三角形T在其局部坐标系下的初始构型为{(x1,y1),(x2,y2),(x3,y3)},在复平面上二维向量可以直接转换为复数:

{xi+i yi|i=1,2,3}

类似地,我们把参数化后uv平面上的坐标也表示为复数形式:

{ui+i vi|i=1,2,3}

使用复数形式的好处在于我们可以把坐标转换为标量函数进行推导。假设映射X:CC将参数平面嵌入到三角形局部坐标系中,当X为共形映射时需要满足复数形式的Cauchy-Riemann方程:

XuiXv=0

利用反函数导数定理可以得到X的反函数U需要满足:

Ux+iUy=0

接下来我们定义共形能量C为等式左端的平方在整个三角形上的积分:

C(T)=T|Ux+iUy|2 dA

显然当U为共形映射时C(T)为0,否则三角形上会产生共形能量。对网格上每个三角形进行求和就得到了网格的共形能量:

C(T)=iC(Ti)

Gradient in a Triangle

对于线性三角形网格,其共形能量在三角形内每一点都是相同的。因此我们可以把积分转换为能量与三角形面积相乘:

C(T)=T|Ux+iUy|2 dA=|Ux+iUy|2AT

上式说明我们只需要计算三角形面积AT以及内部的共形能量即可。对于三角形顶点上定义的标量函数u(x,y),其梯度在三角形内部有解析形式:

[uxuy]=12AT[y2y3y3y1y1y2x3x2x1x3x2x1][u1u2u3]

其中u1,u2,u3分别为顶点上的函数值。对于复函数U我们可以整理得到:

Ux+iUy=i2AT[W1W2W3][U1U2U3]

其中

{W1=(x3x2)+i (y3y2)W2=(x1x3)+i (y1y3)W3=(x2x1)+i (y2y1)   {U1=u1+i v1U2=u2+i v2U3=u3+i v3

这样整个三角形上的共形能量可以表示为:

C(T)=|Ux+iUy|2AT=14AT[W1W2W3][U1U2U3]T21AT[W1W2W3][U1U2U3]T2

对所有三角形求和后就得到了网格上的共形能量,其可以表示为关于顶点复函数的二次型:

C(T)=UCU

上标表示共轭转置。其中二次型的系数矩阵C可以分解为:

C=MM

式中M是一个|F|×|V|维复矩阵,每个元素表示vj顶点在Ti三角形上对网格共形能量的贡献:

Mij={Wj,TiATi,if vertex vj belongs to triangle Ti0,otherwise

Least Square Optimization

对于二次型的优化问题我们可以通过求导并令导数为0的方式进行直接求解。但需要注意在LSCM中系数矩阵CM和顶点uv坐标矩阵U都是复数,使用复数的求导法则会比较麻烦。不过实际上我们只需要对复数矩阵进行简单的变形就可以按照通常的实数矩阵进行处理。首先对复数进行分解:

M=A+i B U=u+i v

其中Au表示对应矩阵的实数部分,Bv则表示虚数部分。在这种分解下我们可以重新构造二次型的目标函数:

MU=(A+i B)(u+i v)=([AB]+i [BA])[uv] C(T)=UMMU=(MU)MU=[uTvT]([ATBT]i [BTAT])([AB]+i [BA])[uv]=[uTvT][ATA+BTBATB+BTABT+ATBABTB+ATA][uv]=[uTvT]([ATBTBTAT][ABBA])[uv]=([ABBA][uv])T([ABBA][uv])

上面的推导说明我们只需要处理实数矩阵构成的二次型优化即可。对它求导并令导数为0可以得到uv坐标需要满足的线性方程组:

[ABBA][uv]=0

为了避免出现平凡解,我们需要固定网格在uv平面上至少2个点的坐标。这种做法的几何意义非常直观:由于共形映射无法区分平移、旋转和缩放(共形映射关于相似变换是相互等价的),我们至少需要2个确定的点才能获得唯一的共形映射。此时可以将原始的复系数矩阵拆分成为自由顶点和固定顶点两部分:

M=[MfMp]

由于Mp部分的坐标是已知的,网格的共形能量只与自由顶点有关。把固定顶点的uv坐标带入齐次方程组并展开可以得到:

[AfApBfBpBfBpAfAp][ufupvfvp]=0

重新整理后可以得到自由顶点uv坐标需要满足的线性方程组:

[AfBfBfAf][ufvf]=[ApBpBpAp][upvp]

求解这个线性方程组即可获得所需的共形参数化。

Implementation

整个LSCM算法可参考下面LSCM()函数的实现。整个算法流程包括计算网格三角形面积、计算初始构型、构造稀疏线性方程组、固定网格上两个点、以及最后求解线性方程组得到参数坐标等步骤。其中计算初始构型的project2Plane()函数与作业2完全相同,可以参见之前的代码片断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function uv = LSCM(V, F)
%% LSCM parameterization
%% Args:
%%      V[nV, 3]: vertices in 3D
%%      F[nF, 3]: face connectivity
%% Returns:
%%      uv[nV, 2]: uv coordinates

nV = size(V, 1);
nF = size(F, 1);

%% areas
AT = doubleArea(V, F);
AT_sq = sqrt(AT);

%% rest pose
Xs = zeros(nF, 3, 2);
for i=1:nF
    Xs(i,:,:) = project2Plane(V(F(i, :), :));
end

%% set up sparse complex matrix
A = (Xs(:, [3 1 2], 1) - Xs(:, [2 3 1], 1)) ./ AT_sq;   %% real part
B = (Xs(:, [3 1 2], 2) - Xs(:, [2 3 1], 2)) ./ AT_sq;   %% imag part

MA = sparse(repmat((1:nF)', 1, 3), F, A, nF, nV);
MB = sparse(repmat((1:nF)', 1, 3), F, B, nF, nV);

%% pin 2 points on the boundary
[b1, b2] = pinBoundary(V, F);

%% split free and pinned vertices
p = [b1 b2]; f = setdiff(1:nV, p);
Af = MA(:, f); Ap = MA(:, p);  %% real part
Bf = MB(:, f); Bp = MB(:, p);  %% imag part

AM = [Af -Bf; Bf Af];
b  =-[Ap -Bp; Bp Ap] * [0; 1; 0; 0];

%% solve linear system
uv = zeros(nV, 2);
uv(f, :) = reshape(AM \ b, [nV-2 2]);

%% fix pinned points
uv(p, :) = [[0 0]; [1 0]];

end

pinBoundary()函数实现了选择网格边界的两个固定点的功能,这里我简单地选择了边界的起点和中间点。当然其它的选取方式也是可行的,但需要注意不同的选取方式会影响最终的参数化结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function [b1, b2] = pinBoundary(V, F)
%% A helper function to pin 2 points on the boundary
%% Args:
%%      V[nV, 3]: vertices in 3D
%%      F[nF, 3]: face connectivity
%% Returns:
%%      b1: pinned boundary vertex
%%      b2: pinned boundary vertex

[B, ~] = findBoundary(V, F);
nB = length(B);

%% select the first and middle boundary points
b1 = B(1); b2 = B(round(nB/2));

end

最后,doubleArea()函数给出了计算网格三角形面积的一个向量化实现。和利用循环进行遍历相比,使用向量化的效率会高很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function As = doubleArea(V, F)
%% Compute doubled area of triangles
%% Args:
%%      V[nV, 3]: vertices in 3D
%%      F[nF, 3]: face connectivity
%% Returns:
%%      As[nF, 1]: doubled area of triangles

nF = size(F, 1);

Es = reshape(V(F(:, 2:3), :)-V(F(:, [1 1]), :), [nF 2 3]);
As = vecnorm(cross(Es(:,1,:), Es(:, 2, :), 3), 2, 3);

end

Results

cow.obj网格上使用LSCM可以得到如下的参数化结果。需要说明的是LSCM可以保证参数化后网格不会出现翻转,但无法保证不会出现自交。在本例中我们可以看到网格在边界上出现了自交的情况。

Mean Value Coordinates

Tutte’s Embedding

Mean value coordinates是Tutte’s embedding的一种变体,这里我们首先回忆一下Tutte’s embedding的原理。Tutte’s embedding中网格内部顶点vi的坐标可以表示为相邻顶点的线性组合:

vi=vjN(vi)λjvj

其中系数λj需要满足非负和归一化条件:

λj0, vjN(vi)λj=1

Tutte’s embedding的一个重要性质在于当边界点位于一个凸多边形上时,求解线性方程组得到的参数化是无翻转且无自交的。

Mean Value Coordinates

Tutte’s embedding的缺陷在于参数化后可能会出现比较严重的几何扭曲,因此Tutte’s embedding相关算法的核心在于如何构造合适的系数来减少扭曲。Mean value coordinates使用了夹角半角的正切来构造权重:

λi=wijwj,  wi=tan(αi12)+tan(αi2)viv0

Implementation

使用mean value coordinates作为权重的Tutte’s embedding算法可参见MVCTutte()函数。这里首先使用了LSCM来固定网格参数化边界,然后对内部顶点求解线性方程组来求解uv坐标。在实现时的一个技巧是从三角形内角来考虑权重,比如说在三角形v0vivi+1上内角αi参与的贡献为:

wαi=tan(αi2)viv0vi+tan(αi2)vi+1v0vi+1

因此对于三角形的每个内角我们只需要计算它的半角正切以及邻边边长,最后填到对应位置上进行求和即可。除此之外,在计算半角正切时还可以结合三角函数的变换技巧:

tanα2=sinα2cosα2=2sinα2cosα22cos2α2=sinα1+cosα

而内角α的正弦和余弦可以由邻边方向向量的叉积与内积来计算,这样整个权重计算都可以进行向量化从而提升计算效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function uv = MVCTutte(V, F)
%% Tutte's embedding with mean value coordinates
%% Args:
%%      V[nV, 3]: vertices in 3D
%%      F[nF, 3]: face connectivity
%% Returns:
%%      uv[nV, 2]: uv coordinates

nV = size(V, 1);

[B, ~] = findBoundary(V, F);
nB = length(B);

%% LSCM to pin the boundary
uv = LSCM(V, F);

%% edges
Es    = reshape(V(F(:, [2, 3, 1]), :) - V(F, :), [size(F), 3]);
Enorm = vecnorm(Es, 2, 3);
Edir  = Es ./ Enorm;

%% trigonometry
coss =-dot(Edir(:, :, :), Edir(:, [3, 1, 2], :), 3);
sins = vecnorm(cross(Edir(:, :, :), -Edir(:, [3, 1, 2], :), 3), 2, 3);

%% tan(theta/2) = sin(theta) / (1+cos(theta))
tanHalf = sins ./ (coss + 1);

%% set up linear system
tripI = [F F];
tripJ = [F(:, [2 3 1]) F(:, [3 1 2])];
tripV = [tanHalf./Enorm(:,:) tanHalf./Enorm(:, [3 1 2])];

A = sparse(tripI, tripJ, tripV, nV, nV);
A = A - diag(sum(A, 2));

%% pinned boundary points
A(B, :) = sparse(nB, nV);
A(B, B) = speye(nB);

b = zeros(nV, 2);
b(B,:) = uv(B, :);

%% solve linear system
uv = A \ b;

end

Results

cow.obj网格上使用LSCM确定边界然后使用mean value coordinates计算内部顶点uv坐标的结果如下。从结果来看mean value coordinates的结果与直接使用LSCM差异不大。

Reference