最小二乘保角参数化(Least Square Conformal Maps)

最小二乘保角参数化(Least Square Conformal Maps)

框架:

用的是中国科大傅孝明老师的框架:框架下载 安装教程 Eigen库配置

概述

LSCM是指最小二乘保角映射,不需要固定边界来进行网格模型参数化。所采用的目标函数的最小化值可以使参数化后的角度变形最小,同时最小值唯一(即解线性方程)。

优缺点:

  1. 减少了角度扭曲与不一致的缩放
  2. 存在且具有唯一的最小值,避免局部最优解
  3. 不需要确定边界的映射,因此可以作用于任意形状的边界
  4. 会发生三角形的翻转与重叠

参数化最好的情况映射前后的每个三角形面积与角度不变,在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)×uX(u,v)=vX(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 uXivX=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 xU+iyU=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)=TxU+iyU2dA=xU+iyU2AT
第六步:对于所有三角形来说,能量函数为所有单个三角形能量函数之和:
C ( T ) = ∑ T ∈ T C ( T ) C(T)=\sum_{T \in T}C(T) C(T)=TTC(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] [xuyu]=dT1[y2y3y3y1y1y2x3x2x1x3y2y1]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=(x1y2y1x2)+(x2y3y2x3)+(x3y1y3x1)是三角形面积的两倍

第九步:把上面的向量修改为复数形式:
∂ 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 xU+iyU=dTi[W1W2W3][u1u2u3]T
其中: i ∗ i = − 1 i*i=-1 ii=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=(x3x2)+i(y3y2)W2=(x1x3)+i(y1y3)W3=(x2x1)+i(y2y1)
第十步:表示为复数 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 xU+iyU=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)=TTC(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)T2
第十三步:能量方程 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)=UCU
第十四步:其中 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=MM
第十五步:上述矩阵 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={d TiWj,TijT0
第十六步:如果没有其他的额外约束条件,上述优化问题的解都是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×(VP)的矩阵, 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)=UMMU=MU2=MfUf+MpUp2

第二十步:最终得到线性系统为:
C ( x ) = ∣ ∣ A x − b ∣ ∣ 2 C(x)=||Ax-b||^2 C(x)=Axb2
其中
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=[Mf1Mf2Mf2Mf1]

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=[Mp1Mp2Mp2Mp1][Up1Up2]

第二十一步:这个线性系统具有如下的特征:

  1. A在约束点等于2的情况下是满秩的。
  2. 如果约束点等于2,那么 x = ( A T A ) − 1 A T b x=(A^TA)^{-1}A^Tb x=(ATA)1ATb,假如曲面是可扩展面,那么映射是完全保角的,也就是目标函数的最小值是零。
  3. 最小值是旋转不变的。
  4. 最小值和模型的分辨率无关。

代码

//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]最小二乘保角参数化

  • 5
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值