最小二乘保角参数化(Least Square Conformal Maps)
框架:
用的是中国科大傅孝明老师的框架:框架下载 安装教程 Eigen库配置
概述
LSCM是指最小二乘保角映射,不需要固定边界来进行网格模型参数化。所采用的目标函数的最小化值可以使参数化后的角度变形最小,同时最小值唯一(即解线性方程)。
优缺点:
- 减少了角度扭曲与不一致的缩放
- 存在且具有唯一的最小值,避免局部最优解
- 不需要确定边界的映射,因此可以作用于任意形状的边界
- 会发生三角形的翻转与重叠
参数化最好的情况映射前后的每个三角形面积与角度不变,在u保持角度不变的情况下尽可能保持面积不变。
保角映射(Conformal Map)是一个数学概念,指的是两个曲面映射后和前每一个点上两个向量的夹角不变。例如下图,三维曲面上的顶点 ( u , v ) (u,v) (u,v) ,任意两个互相垂直的、位于此顶点的切平面向量,被映射到另一个平面,两个向量依旧垂直。
也就是说把平面域(u,v)映射到曲面也是保角的,那么通过
X
(
u
,
v
)
X(u,v)
X(u,v)的两个方向的切向正交,并且范式相等,也就是满足柯西-黎曼方程:
N
(
u
,
v
)
×
∂
X
∂
u
(
u
,
v
)
=
∂
X
∂
v
(
u
,
v
)
N(u,v)\times\frac{\partial X}{\partial u}(u,v)=\frac{\partial X}{\partial v}(u,v)
N(u,v)×∂u∂X(u,v)=∂v∂X(u,v)
LSCM算法建立在柯西-黎曼方程的基础上。首先定义一个能量函数。在定义的保角能量函数是和每个三角形的面积成正比的。
保角离散化
定义在光滑曲面上的能量函数需要进行离散化。通过离散化构建一个线性的系统,这个线性的系统的思路就是把u、v坐标表示为一个复数。离散步骤如下:
第一步:假设三维模型的每个三角形有一个局部正交坐标系,那么三角形的3个顶点的局部坐标为$ (x_1,y_1)、(x_2,y_2)、(x_3,y_3) $,法向都是Z轴方向。相邻两个三角形的方向一致
第二步:假设$ U:(x,y)\rightarrow(u,v)$是从三维空间到二维平面的映射,那么在局部坐标系中,柯西-黎曼方程离散化为:
∂
X
∂
u
−
i
∂
X
∂
v
=
0
\frac{\partial X}{\partial u}-i\frac{\partial X}{\partial v}=0
∂u∂X−i∂v∂X=0
第三步:上式X用复数表示为
X
=
x
+
i
y
X=x+iy
X=x+iy,让
U
=
u
+
i
v
U=u+iv
U=u+iv。那么根据反函数导数定理得出:
∂
U
∂
x
+
i
∂
U
∂
y
=
0
\frac{\partial U}{\partial x}+i\frac{\partial U}{\partial y}=0
∂x∂U+i∂y∂U=0
第四步:由于此公式无法被完全满足,因此可以用最小平方的形式满足。
第五步:用
A
T
A_T
AT来表示三角形的面积,从而对于每个三角形定义能量方程:
C
(
T
)
=
∫
T
∣
∂
U
∂
x
+
i
∂
U
∂
y
∣
2
d
A
=
∣
∂
U
∂
x
+
i
∂
U
∂
y
∣
2
A
T
C(T)=\int_{T}\mid \frac{\partial U}{\partial x}+i\frac{\partial U}{\partial y}\mid^2 dA =\mid \frac{\partial U}{\partial x}+i\frac{\partial U}{\partial y}\mid^2A_T
C(T)=∫T∣∂x∂U+i∂y∂U∣2dA=∣∂x∂U+i∂y∂U∣2AT
第六步:对于所有三角形来说,能量函数为所有单个三角形能量函数之和:
C
(
T
)
=
∑
T
∈
T
C
(
T
)
C(T)=\sum_{T \in T}C(T)
C(T)=T∈T∑C(T)
第七步:进而为每个顶点设置一个复数
U
3
U_3
U3,从而使柯西-黎曼方程是最小平方。
第八步:假设
u
u
u在三角形内部是线性变化的,对于一个三角形
(
x
1
,
x
2
)
,
(
x
2
,
y
2
)
,
(
x
3
,
y
3
)
(x_1,x_2),(x_2,y_2),(x_3,y_3)
(x1,x2),(x2,y2),(x3,y3)来说,那么3个顶点上的值
u
1
,
u
2
,
u
3
u_1,u_2,u_3
u1,u2,u3有如下关系:
[
∂
u
∂
x
∂
u
∂
y
]
=
1
d
T
[
y
2
−
y
3
y
3
−
y
1
y
1
−
y
2
x
3
−
x
2
x
1
−
x
3
y
2
−
y
1
]
[
u
1
u
2
u
3
]
\left[ \begin{matrix} \frac{\partial u}{\partial x} \\ \frac{\partial u}{\partial y} \\ \end{matrix} \right]=\frac{1}{d_T} \left[ \begin{matrix} y_2-y_3\quad y_3-y_1 \quad y_1-y_2 \\ x_3-x_2\quad x_1-x_3 \quad y_2-y_1 \\ \end{matrix} \right] \left[ \begin{matrix} u_1\\u_2\\u_3 \end{matrix} \right]
[∂x∂u∂y∂u]=dT1[y2−y3y3−y1y1−y2x3−x2x1−x3y2−y1]⎣⎡u1u2u3⎦⎤
即梯度公式,其中
d
T
=
(
x
1
y
2
−
y
1
x
2
)
+
(
x
2
y
3
−
y
2
x
3
)
+
(
x
3
y
1
−
y
3
x
1
)
d_T=(x_1y_2-y_1x_2)+(x_2y_3-y_2x_3)+(x_3y_1-y_3x_1)
dT=(x1y2−y1x2)+(x2y3−y2x3)+(x3y1−y3x1)是三角形面积的两倍
第九步:把上面的向量修改为复数形式:
∂
U
∂
x
+
i
∂
U
∂
y
=
i
d
T
[
W
1
W
2
W
3
]
[
u
1
u
2
u
3
]
T
\frac{\partial U}{\partial x}+i\frac{\partial U}{\partial y} = \frac{i}{d_T} \left[ \begin{matrix} W_1 \quad W_2 \quad W_3 \end{matrix} \right] \left[ \begin{matrix} u_1 \quad u_2 \quad u_3 \end{matrix} \right]^T
∂x∂U+i∂y∂U=dTi[W1W2W3][u1u2u3]T
其中:
i
∗
i
=
−
1
i*i=-1
i∗i=−1
{
W
1
=
(
x
3
−
x
2
)
+
i
(
y
3
−
y
2
)
W
2
=
(
x
1
−
x
3
)
+
i
(
y
1
−
y
3
)
W
3
=
(
x
2
−
x
1
)
+
i
(
y
2
−
y
1
)
\begin{cases} W_1=(x_3-x_2)+i(y_3-y_2)\\ W_2=(x_1-x_3)+i(y_1-y_3)\\ W_3=(x_2-x_1)+i(y_2-y_1) \end{cases}
⎩⎪⎨⎪⎧W1=(x3−x2)+i(y3−y2)W2=(x1−x3)+i(y1−y3)W3=(x2−x1)+i(y2−y1)
第十步:表示为复数
U
j
=
u
j
+
i
v
j
U_j=u_j+iv_j
Uj=uj+ivj,最终柯西-黎曼方程可以表示为:
∂
U
∂
x
+
i
∂
U
∂
y
=
i
d
T
[
W
1
W
2
W
3
]
[
U
1
U
2
U
3
]
T
\frac{\partial U}{\partial x}+i\frac{\partial U}{\partial y} = \frac{i}{d_T} \left[ \begin{matrix} W_1 \quad W_2 \quad W_3 \end{matrix} \right] \left[ \begin{matrix} U_1 \quad U_2 \quad U_3 \end{matrix} \right]^T
∂x∂U+i∂y∂U=dTi[W1W2W3][U1U2U3]T
第十一步:那么能量函数改变变量后变为:
C
(
U
=
(
U
1
,
⋯
,
U
n
)
T
)
=
∑
T
∈
T
C
(
T
)
C(U=(U_1,\cdots,U_n)^T) = \sum_{T \in T}C(T)
C(U=(U1,⋯,Un)T)=T∈T∑C(T)
第十二步:也就是每个三角形的能量函数为:
C
(
T
)
=
1
d
T
∣
(
W
j
1
,
T
W
j
2
,
T
W
j
3
,
T
)
(
U
j
1
U
j
2
U
j
3
)
T
∣
2
C(T)=\frac{1}{d_T} \mid (W_{j1,T} \quad W_{j2,T} \quad W_{j3,T})(U_{j1} \quad U_{j2} \quad U_{j3})^T \mid ^2
C(T)=dT1∣(Wj1,TWj2,TWj3,T)(Uj1Uj2Uj3)T∣2
第十三步:能量方程
C
(
U
)
C(U)
C(U)是复数
U
1
,
⋯
,
U
n
U_1,\cdots,U_n
U1,⋯,Un的二次形式,用矩阵形式表示为:
C
(
U
)
=
U
∗
C
U
C(U)=U^*CU
C(U)=U∗CU
第十四步:其中
C
C
C是
n
×
n
n\times n
n×n的Hermitia矩阵。
U
∗
U^*
U∗表示
U
U
U的哈密尔顿共轭矩阵。
C
C
C是哈密尔顿Gram矩阵,可以写成:
C
=
M
∗
M
C=M^*M
C=M∗M
第十五步:上述矩阵
M
=
(
m
i
j
)
M=(m_ij)
M=(mij)是大小为
F
×
V
F\times V
F×V的矩阵,其中
m
i
,
j
=
{
W
j
,
T
i
d
T
i
顶
点
j
属
于
三
角
形
T
0
其
他
m_{i,j}= \begin{cases} \frac{W_{j,T_i}}{\sqrt d_{T_i}} \quad 顶点j属于三角形T \\ 0 \quad \quad\quad其他 \end{cases}
mi,j={dTiWj,Ti顶点j属于三角形T0其他
第十六步:如果没有其他的额外约束条件,上述优化问题的解都是0。为了使优化问题避免简单的解,那么需要确定一些点的位置作为约束。
第十七步:如果确定
P
P
P个点,那么
U
=
(
U
f
T
,
U
p
T
)
T
U=(U_f^T,U_p^T)^T
U=(UfT,UpT)T
第十八步:也就是矩阵
M
=
(
m
i
j
)
M=(m_{ij})
M=(mij)分为两部分
M
=
(
M
f
,
M
p
)
M=(M_f,M_p)
M=(Mf,Mp)
其中,
M
f
M_f
Mf是大小为
F
×
(
V
−
P
)
F \times (V-P)
F×(V−P)的矩阵,
M
p
M_p
Mp是
F
×
P
F \times P
F×P的矩阵。
第十九步:由前面得:
C
(
U
)
=
U
∗
M
∗
M
U
=
∣
∣
M
U
∣
∣
2
=
∣
∣
M
f
U
f
+
M
p
U
p
∣
∣
2
C(U)=U^*M^*MU=||MU||^2=||M_fU_f+M_pU_p||^2
C(U)=U∗M∗MU=∣∣MU∣∣2=∣∣MfUf+MpUp∣∣2
第二十步:最终得到线性系统为:
C
(
x
)
=
∣
∣
A
x
−
b
∣
∣
2
C(x)=||Ax-b||^2
C(x)=∣∣Ax−b∣∣2
其中
A
=
[
M
f
1
−
M
f
2
M
f
2
M
f
1
]
A=\left[ \begin{matrix} M_f^1 \quad -M_f^2 \\ M_f^2 \quad M_f^1 \end{matrix} \right]
A=[Mf1−Mf2Mf2Mf1]
b = − [ M p 1 − M p 2 M p 2 − M p 1 ] [ U p 1 U p 2 ] b=-\left[ \begin{matrix} M_p^1 \quad -M_p^2 \\ M_p^2 \quad -M_p^1 \end{matrix} \right] \left[ \begin{matrix} U_p^1 \\ U_p^2 \end{matrix} \right] b=−[Mp1−Mp2Mp2−Mp1][Up1Up2]
第二十一步:这个线性系统具有如下的特征:
- A在约束点等于2的情况下是满秩的。
- 如果约束点等于2,那么 x = ( A T A ) − 1 A T b x=(A^TA)^{-1}A^Tb x=(ATA)−1ATb,假如曲面是可扩展面,那么映射是完全保角的,也就是目标函数的最小值是零。
- 最小值是旋转不变的。
- 最小值和模型的分辨率无关。
代码
//Least Square Conformal Maps
int v_count, f_count, F_P, S_P;
vector<int> VertexMapping, antiVertexMapping;
Eigen::SparseMatrix<double> realMf,imageMf, realMp, imageMp, A2, BM, B_Sparse;
Eigen::MatrixXd NewP;
Eigen::Vector4d U;
vector< vector<Eigen::Vector2d> > edgeVectors;
vector<double> area, New_U, New_V;
//每个3D空间三角形对应的2D平面三角形的三条边,按面编号存储
void CalculateEdgeVectors();
//固定2个顶点
void FixTwoBoundryPoint();
//将顶点序号与坐标相对应
void MapBackUV();
//矩阵转化为稀疏矩阵
void buildSparseMatrix(Eigen::SparseMatrix<double> &A1_sparse, Eigen::MatrixXd A, int A_rows, int A_cols);
//清理变量
void LSCM_End();
//构建A和BM矩阵
void build_A2_BM();
//构建新的mesh
void BuildMesh(Mesh &temp);
//LSCM参数化
Mesh Parameterize();
//Least Square Conformal Maps
//固定2个顶点
void MeshViewerWidget::FixTwoBoundryPoint() {
FindFirstBoundry(); //找到第一个边界点
FindAllBoundry(); //找到全部边界点
int len = boundry.size();
F_P = 0;
S_P = len / 2; //选取任意较远的两个点
}
//每个3D空间三角形对应的2D平面三角形的三条边,按面编号存储
void MeshViewerWidget::CalculateEdgeVectors() {
area.resize(f_count, 0.0);
edgeVectors.resize(f_count);
for (int k = 0; k < f_count; ++k) {
edgeVectors[k].resize(3);//每行为3列
}
//遍历所有的面
for (auto f_it = mesh.faces_begin(); f_it != mesh.faces_end(); f_it++) {
vector<int> v(3, 0);
int i = 0;
for (auto fv_it = mesh.fv_iter(*f_it); fv_it.is_valid(); ++fv_it) { //遍历该面的所有点
v[i] = (*fv_it).idx(); //保存该面顶点编号
i++;
}
//计算3条边的边长
double a, b, c;
a = (mesh.point((mesh.vertex_handle(v[0]))) - mesh.point((mesh.vertex_handle(v[1])))).length();
b = (mesh.point((mesh.vertex_handle(v[1]))) - mesh.point((mesh.vertex_handle(v[2])))).length();
c = (mesh.point((mesh.vertex_handle(v[2]))) - mesh.point((mesh.vertex_handle(v[0])))).length();
double angle = acos((a*a + c * c - b * b) / (2 * a*c));
double l = (a + b + c)/2.0;
area[(*f_it).idx()] = sqrt(l*(l - a)*(l - b)*(l - c)); //三角形面积(海伦公式)
//以U1为原点,构建在2D平面的三角形坐标
Eigen::Vector2d U1, U2, U3;
U1 << 0, 0;
U2 << a, 0;
U3 << c * cos(angle), c*sin(angle);
//构建2D平面3条边
vector< Eigen::Vector2d> triVectors(3);
Eigen::Vector2d e1, e2, e3;
e1 = U3 - U2; //e1为U1的对边
e2 = U1 - U3; //e2为U2的对边
e3 = U2 - U1; //e3为U3的对边
triVectors[0] = e1;
triVectors[1] = e2;
triVectors[2] = e3;
//外层vector按面编号分组,内层vector存放该面的3条2D平面的边
edgeVectors[(*f_it).idx()] = triVectors;
}
}
//构建A和BM矩阵
void MeshViewerWidget::build_A2_BM() {
A2.resize(2 * f_count, 2 * v_count - 4);
int A2_face_offset = f_count;
int A2_vertices_offset = v_count - 2;
BM.resize(2 * f_count, 4);
int BM_face_offset = f_count;
int BM_vertices_offset = 2;
//利用三元组进行稀疏矩阵插值
std::vector<Eigen::Triplet<double>> tripletlist_A2;
std::vector<Eigen::Triplet<double>> tripletlist_BM;
//遍历每个三角形面
for (auto f_it = mesh.faces_begin(); f_it != mesh.faces_end(); f_it++) {
//triEdgeVectors存放对应面序号的2D平面三角形的三条边
vector<Eigen::Vector2d> triEdgeVectors = edgeVectors[(*f_it).idx()];
double triarea = area[(*f_it).idx()];
double denominator = sqrt(2 * triarea);
int v_index[3];
int i = 0;
//遍历该面的所有点,逆时针0,1,2,获得点的编号
for (auto fv_it = mesh.fv_iter(*f_it); fv_it.is_valid(); fv_it++) {
v_index[i] = (*fv_it).idx();
i++;
}
//遍历2D平面上的该面的3条边
for (i = 0; i < 3; i++) {
Eigen::Vector2d edgeVector = triEdgeVectors[i];
//x坐标(实部)
double dx = edgeVector[0];
int f_idx = (*f_it).idx();
int v_idx = VertexMapping[v_index[i]];
//如果不是固定的点,就插入A矩阵的对应位置
if (v_index[i] != F_P && v_index[i] != S_P) {
tripletlist_A2.push_back(Eigen::Triplet<double>(f_idx, v_idx, dx / denominator));
tripletlist_A2.push_back(Eigen::Triplet<double>(f_idx + A2_face_offset, v_idx + A2_vertices_offset, dx / denominator));
}
//如果是固定的点,就插入BM矩阵的对应位置
else if (v_index[i] == F_P) {
tripletlist_BM.push_back(Eigen::Triplet<double>(f_idx, 0, -dx / denominator));
tripletlist_BM.push_back(Eigen::Triplet<double>(f_idx + BM_face_offset, BM_vertices_offset, dx / denominator));
}
else if (v_index[i] == S_P) {
tripletlist_BM.push_back(Eigen::Triplet<double>(f_idx, 1, -dx / denominator));
tripletlist_BM.push_back(Eigen::Triplet<double>(f_idx + BM_face_offset, 1 + BM_vertices_offset, dx / denominator));
}
//y坐标(虚部)
double dy = edgeVector[1];
if (v_index[i] != F_P && v_index[i] != S_P) {
tripletlist_A2.push_back(Eigen::Triplet<double>(f_idx, v_idx + A2_vertices_offset, -dy / denominator));
tripletlist_A2.push_back(Eigen::Triplet<double>(f_idx + A2_face_offset, v_idx, dy / denominator));
}
else if (v_index[i] == F_P) {
tripletlist_BM.push_back(Eigen::Triplet<double>(f_idx, BM_vertices_offset, dy / denominator));
tripletlist_BM.push_back(Eigen::Triplet<double>(f_idx + BM_face_offset, 0, -dy / denominator));
}
else if (v_index[i] == S_P) {
tripletlist_BM.push_back(Eigen::Triplet<double>(f_idx, 1 + BM_vertices_offset, dy / denominator));
tripletlist_BM.push_back(Eigen::Triplet<double>(f_idx + BM_face_offset, 1, -dy / denominator));
}
}
}
//三元组插入稀疏矩阵
A2.setFromTriplets(tripletlist_A2.begin(), tripletlist_A2.end());
BM.setFromTriplets(tripletlist_BM.begin(), tripletlist_BM.end());
}
//将顶点序号与坐标相对应
void MeshViewerWidget::MapBackUV() {
int VertexMapping_len = VertexMapping.size();
int P_1 = antiVertexMapping[VertexMapping_len - 2];
int P_2 = antiVertexMapping[VertexMapping_len - 1];
New_U.resize(v_count);
New_V.resize(v_count);
New_U[P_1] = U[0];
New_V[P_1] = U[2];
New_U[P_2] = U[1];
New_V[P_2] = U[3];
int v_offset = v_count - 2;
for (int i = 0; i < VertexMapping_len - 2; i++) {
//例如Q在NewP中的序号为i,坐标为Q_Y,Q_Y;但它在实际空间的顶点的序号为antiVertexMapping[i],需要将时实际空间中的序号与新坐标相对应.
int index = antiVertexMapping[i];
double P_X = NewP(i);
double P_Y = NewP(v_offset + i);
New_U[index] = P_X;
New_V[index] = P_Y;
}
}
//LSCM参数化
Mesh MeshViewerWidget::Parameterize() {
f_count = mesh.n_faces();
v_count = mesh.n_vertices();
FixTwoBoundryPoint(); //固定2个顶点
CalculateEdgeVectors(); //每个3D空间三角形对应的2D平面三角形的三条边,按面编号存储
VertexMapping.resize(v_count, 0.0);
antiVertexMapping.resize(v_count, 0.0);
//把两个固定的顶点顺序放到矩阵后面,用来做顶点顺序映射,分别定义两个保持正向和反向映射
VertexMapping[F_P] = v_count - 2;
VertexMapping[S_P] = v_count - 1;
antiVertexMapping[v_count - 2] = F_P;
antiVertexMapping[v_count - 1] = S_P;
int itrCount = 0;
//遍历所有的点,构建三角形顶点顺序正向映射与反向映射
for (auto v_it = mesh.vertices_begin(); v_it != mesh.vertices_end(); v_it++) {
if ((*v_it).idx() == F_P || (*v_it).idx() == S_P) {
continue;
}
VertexMapping[(*v_it).idx()] = itrCount;
antiVertexMapping[itrCount] = (*v_it).idx();
itrCount++;
}
build_A2_BM(); //构建A和BM矩阵
//手动设置固定的2个点在平面的位置
U[0] = 0.0;
U[1] = 1.0;
U[2] = 0.0;
U[3] = 0.0;
Eigen::MatrixXd bPart = BM * U;
B_Sparse.resize(2 * f_count, 1);
buildSparseMatrix(B_Sparse, bPart, 2 * f_count, 1); //将bPart转化成稀疏矩阵,方便进行运算
Eigen::LeastSquaresConjugateGradient<Eigen::SparseMatrix<double> > Solver_sparse;
Solver_sparse.compute(A2);
cout << "------------------------------1------------------------------" << endl;
NewP.resize(2 * v_count - 4, 1);
NewP = Solver_sparse.solve(B_Sparse);
cout << "------------------------------2------------------------------" << endl;
MapBackUV(); //将顶点序号与坐标相对应
Mesh temp; //新的2D平面
BuildMesh(temp); //构建新的mesh
return temp;
}
//构建新的mesh
void MeshViewerWidget::BuildMesh(Mesh &temp) {
Mesh::VertexHandle *vhandle; //点的迭代器指针
vhandle = new Mesh::VertexHandle[mesh.n_vertices()]; //开辟的空间大小
for (auto v_it = mesh.vertices_begin(); v_it != mesh.vertices_end(); ++v_it){ //遍历每个点,将每个点对应的2D坐标加入temp
vhandle[(*v_it).idx()] = temp.add_vertex(Mesh::Point(New_U[(*v_it).idx()], New_V[(*v_it).idx()], 0));
}
for (auto f_it = mesh.faces_begin(); f_it != mesh.faces_end(); ++f_it) { //遍历每个面
std::vector<Mesh::VertexHandle> face_vhandles; //存放2D的每个三角形平面
for (auto fv_it = mesh.fv_iter(*f_it); fv_it.is_valid(); ++fv_it) { //遍历该面的所有点
face_vhandles.push_back(vhandle[(*fv_it).idx()]); //通过vhandle将3D的点转化到2D平面上
}
temp.add_face(face_vhandles);
}
}
//矩阵转化为稀疏矩阵
void MeshViewerWidget::buildSparseMatrix(Eigen::SparseMatrix<double> &A1_sparse, Eigen::MatrixXd A, int A_rows, int A_cols) {
std::vector<Eigen::Triplet<double>> tripletlist; //构建类型为三元组的vector
for (int i = 0; i < A_rows; i++)
{
for (int j = 0; j < A_cols; j++)
{
if (std::fabs(A(i, j)) > 1e-6)
{
//按Triplet方式填充,速度快
tripletlist.push_back(Eigen::Triplet<double>(i, j, A(i, j)));
// 直接插入速度慢
//A1_sparse.insert(i, j) = A(i, j);
}
}
}
A1_sparse.setFromTriplets(tripletlist.begin(), tripletlist.end());
// 压缩优化矩阵
//A1_sparse.makeCompressed();
}
//清理变量
void MeshViewerWidget::LSCM_End() {
VertexMapping.clear();
antiVertexMapping.clear();
A2.resize(0, 0);
BM.resize(0, 0);
B_Sparse.resize(0, 0);
NewP.resize(0, 0);
edgeVectors.clear();
area.clear();
New_U.clear();
New_V.clear();
}
演示结果
参考文献
[1]Bruno Lévy,Sylvain Petitjean,Nicolas Ray,Jérome Maillot. Least squares conformal maps for automatic texture atlas generation[J]. ACM Transactions on Graphics (TOG),2002,21(3).
[2]最小二乘保角参数化